/*
 * Copyright 2014 Red Hat, Inc. and/or its affiliates.
 *
 * Licensed under the Eclipse Public License version 1.0, available at
 * http://www.eclipse.org/legal/epl-v10.html
 */
package org.jboss.windup.config.operation;

import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.windup.config.GraphRewrite;
import org.jboss.windup.config.Variables;
import org.jboss.windup.config.condition.GraphCondition;
import org.jboss.windup.config.exception.IllegalTypeArgumentException;
import org.jboss.windup.config.operation.iteration.IterationBuilderComplete;
import org.jboss.windup.config.operation.iteration.IterationBuilderOtherwise;
import org.jboss.windup.config.operation.iteration.IterationBuilderOver;
import org.jboss.windup.config.operation.iteration.IterationBuilderPerform;
import org.jboss.windup.config.operation.iteration.IterationBuilderVar;
import org.jboss.windup.config.operation.iteration.IterationBuilderWhen;
import org.jboss.windup.config.operation.iteration.IterationPayloadManager;
import org.jboss.windup.config.operation.iteration.NamedFramesSelector;
import org.jboss.windup.config.operation.iteration.NamedIterationPayloadManager;
import org.jboss.windup.config.operation.iteration.TopLayerSingletonFramesSelector;
import org.jboss.windup.config.operation.iteration.TypedFramesSelector;
import org.jboss.windup.config.operation.iteration.TypedNamedFramesSelector;
import org.jboss.windup.config.operation.iteration.TypedNamedIterationPayloadManager;
import org.jboss.windup.config.selectors.FramesSelector;
import org.jboss.windup.graph.model.WindupVertexFrame;
import org.jboss.windup.util.ThemeProvider;
import org.jboss.windup.util.exception.WindupException;
import org.ocpsoft.common.util.Assert;
import org.ocpsoft.rewrite.config.And;
import org.ocpsoft.rewrite.config.CompositeOperation;
import org.ocpsoft.rewrite.config.Condition;
import org.ocpsoft.rewrite.config.ConfigurationRuleBuilder;
import org.ocpsoft.rewrite.config.DefaultOperationBuilder;
import org.ocpsoft.rewrite.config.NoOp;
import org.ocpsoft.rewrite.config.Operation;
import org.ocpsoft.rewrite.config.Perform;
import org.ocpsoft.rewrite.context.EvaluationContext;
import org.ocpsoft.rewrite.event.Rewrite;

import com.google.common.collect.Iterables;
import org.jboss.windup.util.exception.WindupStopException;
import org.ocpsoft.rewrite.config.CompositeCondition;
import org.ocpsoft.rewrite.param.ParameterStore;
import org.ocpsoft.rewrite.param.Parameterized;

/**
 * Used to iterate over an implicit or explicit variable defined within the corresponding {@link ConfigurationRuleBuilder#when(Condition)} clause in
 * the current rule.
 *
 * @author <a href="mailto:lincolnbaxter@gmail.com">Lincoln Baxter, III</a>
 * @author <a href="http://ondra.zizka.cz">Ondrej Zizka, I, zizka@seznam.cz</a>
 */
public class Iteration extends DefaultOperationBuilder
        implements IterationBuilderVar, IterationBuilderOver,
        IterationBuilderWhen, IterationBuilderPerform, IterationBuilderOtherwise,
        IterationBuilderComplete, CompositeOperation, Parameterized {
    private static final String VAR_INSTANCE_STRING = "_instance";
    public static final String DEFAULT_VARIABLE_LIST_STRING = "default";
    public static final String DEFAULT_SINGLE_VARIABLE_STRING = singleVariableIterationName(DEFAULT_VARIABLE_LIST_STRING);

    private Condition condition;
    private Operation operationPerform;
    private Operation operationOtherwise;

    private IterationPayloadManager payloadManager;
    private final FramesSelector selectionManager;

    /**
     * Calculates the default name for the single variable in the selection with the given name.
     */
    public static String singleVariableIterationName(String selectionName) {
        return selectionName + VAR_INSTANCE_STRING;
    }

    /**
     * Create a new {@link Iteration}
     */
    private Iteration(FramesSelector selectionManager) {
        Assert.notNull(selectionManager, "Selection manager must not be null.");
        this.selectionManager = selectionManager;
    }

    /**
     * Begin an {@link Iteration} over the named selection of the given type. Also sets the name and type of the variable for this iteration's
     * "current element". The type serves for automatic type check.
     */
    public static IterationBuilderOver over(Class<? extends WindupVertexFrame> sourceType, String source) {
        Iteration iterationImpl = new Iteration(new TypedNamedFramesSelector(sourceType, source));
        iterationImpl.setPayloadManager(new TypedNamedIterationPayloadManager(sourceType,
                singleVariableIterationName(source)));
        return iterationImpl;
    }

    /**
     * Begin an {@link Iteration} over the named selection. Also sets the name of the variable for this iteration's "current element".
     */
    public static IterationBuilderOver over(String source) {
        Iteration iterationImpl = new Iteration(new NamedFramesSelector(source));
        iterationImpl.setPayloadManager(new NamedIterationPayloadManager(singleVariableIterationName(source)));
        return iterationImpl;
    }

    /**
     * Begin an {@link Iteration} over the selection of the given type, named with the default name. Also sets the name of the variable for this
     * iteration's "current element" to have the default value.
     */
    public static IterationBuilderOver over(Class<? extends WindupVertexFrame> sourceType) {
        Iteration iterationImpl = new Iteration(new TypedFramesSelector(sourceType));
        iterationImpl.setPayloadManager(new TypedNamedIterationPayloadManager(sourceType,
                DEFAULT_SINGLE_VARIABLE_STRING));
        return iterationImpl;
    }

    /**
     * Begin an {@link Iteration} over the selection that is placed on the top of the {@link Variables}. Also sets the name of the variable for this
     * iteration's "current element" (i.e payload) to have the default value.
     */
    public static IterationBuilderOver over() {
        Iteration iterationImpl = new Iteration(new TopLayerSingletonFramesSelector());
        iterationImpl.setPayloadManager(new NamedIterationPayloadManager(DEFAULT_SINGLE_VARIABLE_STRING));
        return iterationImpl;
    }

    /**
     * These are really just needed to pass through to the condition, since the visitor system
     * doesn't expect operations to have conditions and wouldn't otherwise find them.
     */
    @Override
    public Set<String> getRequiredParameterNames() {
        return getRequiredParameterNames(this.condition);
    }

    /**
     * These are really just needed to pass through to the condition, since the visitor system
     * doesn't expect operations to have conditions and wouldn't otherwise find them.
     */
    @Override
    public void setParameterStore(ParameterStore store) {
        setParameterStore(store, this.condition);
    }

    private Set<String> getRequiredParameterNames(Condition condition) {
        Set<String> result = new HashSet<>();
        if (condition instanceof Parameterized)
            result.addAll(((Parameterized) condition).getRequiredParameterNames());

        if (condition instanceof CompositeCondition)
            ((CompositeCondition) condition).getConditions().forEach(innerCondition -> result.addAll(getRequiredParameterNames(innerCondition)));

        return result;
    }

    private void setParameterStore(ParameterStore store, Condition condition) {
        if (condition instanceof Parameterized)
            ((Parameterized) condition).setParameterStore(store);

        if (condition instanceof CompositeCondition)
            ((CompositeCondition) condition).getConditions().forEach(innerCondition -> setParameterStore(store, innerCondition));
    }

    /**
     * Change the name of the single variable of the given type. If this method is not called, the name is calculated using the
     * {@link Iteration#singleVariableIterationName(String)} method.
     */
    @Override
    public IterationBuilderVar as(Class<? extends WindupVertexFrame> varType, String var) {
        setPayloadManager(new TypedNamedIterationPayloadManager(varType, var));
        return this;
    }

    /**
     * Change the name of the single variable. If this method is not called, the name is calculated using the
     * {@link Iteration#singleVariableIterationName(String)} method.
     */
    @Override
    public IterationBuilderVar as(String var) {
        setPayloadManager(new NamedIterationPayloadManager(var));
        return this;
    }

    public IterationBuilderWhen all(Condition... condition) {
        this.condition = And.all(condition);
        return this;
    }

    @Override
    public IterationBuilderWhen when(Condition condition) {
        this.condition = condition;
        return this;
    }

    @Override
    public IterationBuilderPerform perform(Operation operation) {
        this.operationPerform = operation;
        return this;
    }

    @Override
    public IterationBuilderPerform perform(Operation... operations) {
        this.operationPerform = Perform.all(operations);
        return this;
    }

    @Override
    public IterationBuilderOtherwise otherwise(Operation operation) {
        this.operationOtherwise = operation;
        return this;
    }

    /**
     * Visual end of the iteration.
     */
    @Override
    public IterationBuilderComplete endIteration() {
        return this;
    }

    /**
     * Called internally to actually process the Iteration.
     */
    @Override
    public void perform(Rewrite event, EvaluationContext context) {
        perform((GraphRewrite) event, context);
    }

    /**
     * Called internally to actually process the Iteration. Loops over the frames to iterate, and performs their .perform( ... ) or .otherwise( ... )
     * parts.
     */
    public void perform(GraphRewrite event, EvaluationContext context) {
        Variables variables = Variables.instance(event);
        Iterable<? extends WindupVertexFrame> frames = getSelectionManager().getFrames(event, context);

        boolean hasCommitOperation = OperationUtil.hasCommitOperation(operationPerform) || OperationUtil.hasCommitOperation(operationOtherwise);
        boolean hasIterationOperation = OperationUtil.hasIterationProgress(operationPerform)
                || OperationUtil.hasIterationProgress(operationOtherwise);
        DefaultOperationBuilder commit = new NoOp();
        DefaultOperationBuilder iterationProgressOperation = new NoOp();

        if (!hasCommitOperation || !hasIterationOperation) {
            int frameCount = Iterables.size(frames);
            if (frameCount > 100) {
                if (!hasCommitOperation)
                    commit = Commit.every(1000);

                if (!hasIterationOperation) {
                    // Use 500 here as 100 might be noisy.
                    iterationProgressOperation = IterationProgress.monitoring("Rule Progress", 500);
                }
            }
        }
        Operation commitAndProgress = commit.and(iterationProgressOperation);

        event.getRewriteContext().put(DEFAULT_VARIABLE_LIST_STRING, frames); // set the current frames
        try {
            for (WindupVertexFrame frame : frames)
                try {
                    variables.push();
                    getPayloadManager().setCurrentPayload(variables, frame);
                    boolean conditionResult = true;
                    if (condition != null) {
                        final String payloadVariableName = getPayloadVariableName(event, context);
                        passInputVariableNameToConditionTree(condition, payloadVariableName);
                        conditionResult = condition.evaluate(event, context);
                        /*
                         * Add special clear layer for perform, because condition used one and could have added new variables. The condition result put into
                         * variables is ignored.
                         */
                        variables.push();
                        getPayloadManager().setCurrentPayload(variables, frame);
                    }
                    if (conditionResult) {
                        if (operationPerform != null) {
                            operationPerform.perform(event, context);
                        }
                    } else if (condition != null) {
                        if (operationOtherwise != null) {
                            operationOtherwise.perform(event, context);
                        }
                    }
                    commitAndProgress.perform(event, context);

                    getPayloadManager().removeCurrentPayload(variables);
                    // remove the perform layer
                    variables.pop();
                    if (condition != null) {
                        // remove the condition layer
                        variables.pop();
                    }
                } catch (WindupStopException ex) {
                    throw new WindupStopException(ThemeProvider.getInstance().getTheme().getBrandNameAcronym() + " stop requested in " + this.toString(), ex);
                } catch (Exception e) {
                    throw new WindupException("Failed when iterating " + frame.toPrettyString() + ", due to: " + e.getMessage(), e);
                }
        } finally {
            event.getRewriteContext().put(DEFAULT_VARIABLE_LIST_STRING, null);
        }
    }


    private void passInputVariableNameToConditionTree(Condition condition, String payloadVariableName) throws IllegalStateException {
        // Automatically set the input variable to point to the current payload.
        if (condition instanceof GraphCondition) {
            ((GraphCondition) condition).setInputVariablesName(payloadVariableName);
        }
        // WINDUP-1057 - we need to pass the variable name manually to the GraphCondition's nested in CompositeCondition's.
        if (condition instanceof CompositeCondition) {
            CompositeCondition composite = (CompositeCondition) condition;
            for (Condition childCondition : composite.getConditions()) {
                passInputVariableNameToConditionTree(childCondition, payloadVariableName);
            }
        }
    }

    @Override
    public List<Operation> getOperations() {
        return Arrays.asList(operationPerform, operationOtherwise);
    }

    /**
     * Return the current {@link Iteration} payload variable name.
     *
     * @throws IllegalStateException if there is more than one variable in the {@link Variables} stack, and the payload name cannot be determined.
     */
    public static String getPayloadVariableName(GraphRewrite event, EvaluationContext ctx) throws IllegalStateException {
        Variables variables = Variables.instance(event);
        Map<String, Iterable<? extends WindupVertexFrame>> topLayer = variables.peek();
        if (topLayer.keySet().size() != 1) {
            throw new IllegalStateException("Cannot determine Iteration payload variable name because the top "
                    + "layer of " + Variables.class.getSimpleName() + " stack contains " + topLayer.keySet().size() + " variables: "
                    + topLayer.keySet());
        }
        String name = topLayer.keySet().iterator().next();
        return name;
    }

    /**
     * Set the current {@link Iteration} payload.
     */
    public static void setCurrentPayload(Variables stack, String name, WindupVertexFrame frame)
            throws IllegalArgumentException {
        Map<String, Iterable<? extends WindupVertexFrame>> vars = stack.peek();

        Iterable<? extends WindupVertexFrame> existingValue = vars.get(name);
        if (!(existingValue == null || existingValue instanceof IterationPayload)) {
            throw new IllegalArgumentException("Variable \"" + name
                    + "\" has already been assigned and cannot be used as an " + Iteration.class.getSimpleName()
                    + " variable.");
        }

        vars.put(name, new IterationPayload<>(frame));
    }

    /**
     * Get the {@link Iteration} payload with the given name.
     *
     * @throws IllegalArgumentException if the given variable refers to a non-payload.
     */
    @SuppressWarnings("unchecked")
    public static <FRAMETYPE extends WindupVertexFrame> FRAMETYPE getCurrentPayload(Variables stack, String name)
            throws IllegalStateException, IllegalArgumentException {
        Map<String, Iterable<? extends WindupVertexFrame>> vars = stack.peek();

        Iterable<? extends WindupVertexFrame> existingValue = vars.get(name);
        if (!(existingValue == null || existingValue instanceof IterationPayload)) {
            throw new IllegalArgumentException("Variable \"" + name
                    + "\" is not an " + Iteration.class.getSimpleName() + " variable.");
        }

        Object object = stack.findSingletonVariable(name);
        return (FRAMETYPE) object;
    }

    /**
     * Get the {@link Iteration} payload with the given name and type.
     *
     * @throws IllegalArgumentException if the given variable refers to a non-payload.
     */
    @SuppressWarnings("unchecked")
    public static <FRAMETYPE extends WindupVertexFrame> FRAMETYPE getCurrentPayload(Variables stack,
                                                                                    Class<FRAMETYPE> type, String name) throws IllegalStateException, IllegalArgumentException {
        Map<String, Iterable<? extends WindupVertexFrame>> vars = stack.peek();

        Iterable<? extends WindupVertexFrame> existingValue = vars.get(name);
        if (!(existingValue == null || existingValue instanceof IterationPayload)) {
            throw new IllegalArgumentException("Variable \"" + name
                    + "\" is not an " + Iteration.class.getSimpleName() + " variable.");
        }

        Object object = stack.findSingletonVariable(type, name);
        return (FRAMETYPE) object;
    }

    /**
     * Remove the current {@link Iteration} payload.
     */
    public static <FRAMETYPE extends WindupVertexFrame> FRAMETYPE removeCurrentPayload(Variables stack,
                                                                                       Class<FRAMETYPE> type, String name)
            throws IllegalStateException, IllegalTypeArgumentException {
        FRAMETYPE payload = getCurrentPayload(stack, type, name);

        Map<String, Iterable<? extends WindupVertexFrame>> vars = stack.peek();
        vars.remove(name);

        return payload;
    }

    /**
     * Remove the current {@link Iteration} payload.
     */
    public static <FRAMETYPE extends WindupVertexFrame> FRAMETYPE removeCurrentPayload(Variables stack, String name)
            throws IllegalStateException, IllegalTypeArgumentException {
        FRAMETYPE payload = getCurrentPayload(stack, name);

        Map<String, Iterable<? extends WindupVertexFrame>> vars = stack.peek();
        vars.remove(name);

        return payload;
    }

    public void setPayloadManager(IterationPayloadManager payloadManager) {
        Assert.notNull(payloadManager, "Payload manager must not be null.");
        this.payloadManager = payloadManager;
    }

    public FramesSelector getSelectionManager() {
        return selectionManager;
    }

    public IterationPayloadManager getPayloadManager() {
        return payloadManager;
    }

    private static class IterationPayload<T> extends HashSet<T> {
        private static final long serialVersionUID = 7725055142596456025L;

        public IterationPayload(T element) {
            super(1);
            super.add(element);
        }

        @Override
        public boolean add(T e) {
            throw new UnsupportedOperationException("Iteration payloads are not modifiable.");
        }

        @Override
        public boolean remove(Object o) {
            throw new UnsupportedOperationException("Iteration payloads are not modifiable.");
        }

        @Override
        public void clear() {
            throw new UnsupportedOperationException("Iteration payloads are not modifiable.");
        }

        @Override
        public boolean addAll(Collection<? extends T> c) {
            throw new UnsupportedOperationException("Iteration payloads are not modifiable.");
        }

        @Override
        public boolean removeAll(Collection<?> c) {
            throw new UnsupportedOperationException("Iteration payloads are not modifiable.");
        }

        @Override
        public boolean retainAll(Collection<?> c) {
            throw new UnsupportedOperationException("Iteration payloads are not modifiable.");
        }

    }

    /**
     * @return Description of this iteration, e.g. "Iteration.over(?).as(...).when(...).perform(...)".
     */
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("Iteration.over(?)");
        if (!getPayloadManager().getPayLoadName().equals(DEFAULT_SINGLE_VARIABLE_STRING)) {
            builder.append(".as(").append(getPayloadManager().getPayLoadName()).append(")");
        }
        if (condition != null) {
            builder.append(".when(").append(condition).append(")");
        }
        if (operationPerform != null) {
            builder.append(".perform(").append(operationPerform).append(")");
        }
        if (operationOtherwise != null) {
            builder.append(".otherwise(").append(operationOtherwise).append(")");
        }
        return builder.toString();
    }
}
