/*
 *
 * Copyright (c) 2006-2020, Speedment, Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); You may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.speedment.common.codegen.internal;

import com.speedment.common.codegen.Generator;
import com.speedment.common.codegen.Meta;
import com.speedment.common.codegen.Transform;
import com.speedment.common.codegen.TransformFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import static java.util.Objects.requireNonNull;

/**
 * A transform that uses a series of transforms to complete the transformation.
 * For an example, if you have two transforms A → B and A → C you can define
 * a <code>BridgeTransform</code> for A → C.
 * 
 * @author Emil Forslund
 * @param <A> the from type
 * @param <B> the to type
 */
public class BridgeTransform<A, B> implements Transform<A, B> {
    
    private final List<Transform<?, ?>> steps;
    private final Class<A> from;
    private final Class<B> to;
    private final TransformFactory factory;
    private Class<?> end;
    
    /**
     * Constructs a new transform from one model class to another. The bridge
     * requires a factory to create the intermediate steps.
     * 
     * @param from the type to transform from
     * @param to the type to transform to
     * @param factory a factory with all the required steps installed
     */
    private BridgeTransform(Class<A> from, Class<B> to, TransformFactory factory) {
        this.from    = requireNonNull(from);
        this.to      = requireNonNull(to);
        this.factory = requireNonNull(factory);
        this.steps   = new ArrayList<>();
        this.end     = requireNonNull(from);
    }
    
    /**
     * Creates a shallow copy of a bridge.
     * 
     * @param prototype the prototype
     */
    private BridgeTransform(BridgeTransform<A, B> prototype) {
        steps   = new ArrayList<>(prototype.steps);
        from    = prototype.from;
        to      = prototype.to;
        end     = prototype.end;
        factory = prototype.factory;
    }
    
    /**
     * Transforms the specified model using this transform. A code generator is
     * supplied so that the transform can initiate new generation processes to
     * resolve dependencies.
     * 
     * @param gen   the current code generator 
     * @param model the model to transform
     * @return      the transformation result or empty if any of the steps 
     *              returned empty
     */
    @Override
    public Optional<B> transform(Generator gen, A model) {
        requireNonNull(gen);
        requireNonNull(model);
        
        Object o = model;
        
        for (final Transform<?, ?> step : steps) {
            if (o == null) {
                return Optional.empty();
            } else {
                @SuppressWarnings("unchecked")
                final Transform<Object, ?> step2 = (Transform<Object, ?>) step;
                
                o = gen.transform(step2, o, factory)
                    .map(Meta::getResult)
                    .orElse(null);
            }
        }
        
        if (o == null) {
            return Optional.empty();
        } else {
            if (to.isAssignableFrom(o.getClass())) {
                @SuppressWarnings("unchecked")
                final B result = (B) o;
                return Optional.of(result);
            } else {
                throw new IllegalStateException(
                    "The bridge between '" + 
                    from.getSimpleName() + 
                    "' to '" + 
                    to.getSimpleName() + 
                    "' is not complete."
                );
            }
        }
    }
    
    /**
     * Creates a bridge from one model type to another. A factory is supplied so
     * that intermediate steps can be resolved.
     * 
     * @param <A>     the initial type of a model to transform
     * @param <B>     the final type of a model after transformation
     * @param <T>     the type of a transform between A and B
     * @param factory a factory with all intermediate steps installed
     * @param from    the initial class of a model to transform
     * @param to      the final class of a model after transformation
     * @return        a <code>Stream</code> of all unique paths between A and B
     */
    public static <A, B, T extends Transform<A, B>> Stream<T> create(TransformFactory factory, Class<A> from, Class<B> to) {
        return create(factory, new BridgeTransform<>(from, to, factory));
    }
    
    /**
     * Takes a bridge and completes it if it is not finished. Returns all valid 
     * paths through the graph as a <code>Stream</code>.
     * 
     * @param <A>     the initial type of a model to transform
     * @param <B>     the final type of a model after transformation
     * @param <T>     the type of a transform between A and B
     * @param factory a factory with all intermediate steps installed
     * @param bridge  the incomplete bridge to finish
     * @return        a <code>Stream</code> of all unique paths between A and B
     */
    private static <A, B, T extends Transform<A, B>> Stream<T> create(TransformFactory factory, BridgeTransform<A, B> bridge) {
        requireNonNull(factory);
        requireNonNull(bridge);
        
        if (bridge.end.equals(bridge.to)) {
            @SuppressWarnings("unchecked")
            final T result = (T) bridge;
            return Stream.of(result);
        } else {
            final List<Stream<T>> bridges = new ArrayList<>();
            
            factory.allFrom(bridge.end).stream().forEachOrdered(e -> {
                
                final BridgeTransform<A, B> br = new BridgeTransform<>(bridge);
                
                @SuppressWarnings("unchecked")
                Class<Object> a = (Class<Object>) bridge.end;
                
                @SuppressWarnings("unchecked")
                Class<Object> b = (Class<Object>) e.getKey();
                
                @SuppressWarnings("unchecked")
                Transform<Object, Object> transform = (Transform<Object, Object>) e.getValue();
                
                if (br.addStep(a, b, transform)) {
                    bridges.add(create(factory, br));
                }
            });
            
            return bridges.stream().flatMap(i -> i);
        }
    }

    /**
     * Returns true if this transform is or contains the specified 
     * transformer. This is used internally by the code generator to avoid 
     * circular paths.
     * 
     * @param transformer  the type of the transformer to check
     * @return             true if this transform is or contains the input
     */
    @Override
    public boolean is(Class<? extends Transform<?, ?>> transformer) {
        return steps.stream().anyMatch(t -> t.is(requireNonNull(transformer)));
    }
    
    /**
     * Attempts to add a new step to the bridge. If the step is already part of
     * the bridge, it will not be added. Returns true if the step was added.
     * 
     * @param <A2>  the initial type of the step
     * @param <B2>  the output type of the step
     * @param from  the initial type of the step
     * @param to    the output type of the step
     * @param step  the step to attempt to add
     * @return      true if the step was added
     */
    private <A2, B2> boolean addStep(Class<A2> from, Class<B2> to, Transform<A2, B2> step) {
        requireNonNull(from);
        requireNonNull(to);
        requireNonNull(step);
        
        if (end == null || from.equals(end)) {
            if (steps.contains(step)) {
                return false;
            } else {
                steps.add(step);
                end = to;
                return true;
            }
        } else {
            throw new IllegalArgumentException("Transform " + step + " has a different entry class (" + from + ") than the last class in the current build (" + end + ").");
        }
    }
}