/**
 * Copyright (C) 2010 Orbeon, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU Lesser General Public License as published by the Free Software Foundation; either version
 * 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
 */
package org.orbeon.oxf.processor.pipeline.choose;

import org.apache.commons.collections.CollectionUtils;
import org.apache.log4j.Logger;
import org.orbeon.oxf.cache.OutputCacheKey;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.util.XPath;
import org.orbeon.oxf.xml.XMLReceiver;
import org.orbeon.oxf.processor.*;
import org.orbeon.oxf.util.LoggerFactory;
import org.orbeon.oxf.util.PooledXPathExpression;
import org.orbeon.oxf.util.XPathCache;
import org.orbeon.oxf.xml.NamespaceMapping;
import org.orbeon.oxf.xml.dom4j.LocationData;
import org.orbeon.saxon.Configuration;
import org.orbeon.saxon.om.DocumentInfo;

import java.util.*;

public class ConcreteChooseProcessor extends ProcessorImpl {

    public static Logger logger = LoggerFactory.createLogger(ConcreteChooseProcessor.class);

    // Created when constructed
    private LocationData locationData;
    private List branchConditions;
    private List<NamespaceMapping> branchNamespaces;
    private List<Processor> branchProcessors;
    private Set outputsById;
    private Set outputsByParamRef;
    private List<Map<String, ProcessorInput>> branchInputs = new ArrayList<Map<String, ProcessorInput>>();  // List [Map: (String inputName) -> (ProcessorInput)]
    private List<Map<Object, ProcessorOutput>> branchOutputs = new ArrayList<Map<Object, ProcessorOutput>>(); // List [Map: (String outputName) -> (ProcessorOutput)]

    /**
     * @param branchConditions  List of Strings: XPath expression for each branch
     *                          (except the optimal last <otherwise>)
     * @param branchNamespaces  List of NamespaceContext objects: namespaces declared in
     *                          the context of the given XPath expression
     * @param branchProcessors  List of Processor objects: one for each branch
     * @param inputs            Set of Strings: all the ids possibly referenced by
     *                          a processor in any branch
     * @param outputsById       Set of Strings: outputs of the choose referenced
     *                          by and other processor
     * @param outputsByParamRef Set of Strings: outputs of the choose referencing
     *                          pipeline outputs
     */
    public ConcreteChooseProcessor(String id, LocationData locationData,
                                   List branchConditions, List<NamespaceMapping> branchNamespaces, List<Processor> branchProcessors,
                                   Set inputs, Set outputsById, Set outputsByParamRef) {
        setId(id);
        this.locationData = locationData;
        this.branchConditions = branchConditions;
        this.branchNamespaces = branchNamespaces;
        this.branchProcessors = branchProcessors;
        this.outputsById = outputsById;
        this.outputsByParamRef = outputsByParamRef;

        // Add inputs
        addInputInfo(new ProcessorInputOutputInfo(AbstractChooseProcessor.CHOOSE_DATA_INPUT));
        for (Iterator i = inputs.iterator(); i.hasNext();) {
            String name = (String) i.next();
            addInputInfo(new ProcessorInputOutputInfo(name));
        }

        // Add outputs
        for (Iterator i = CollectionUtils.union(outputsById, outputsByParamRef).iterator(); i.hasNext();) {
            String name = (String) i.next();
            addOutputInfo(new ProcessorInputOutputInfo(name));
        }

        for (Iterator i = branchProcessors.iterator(); i.hasNext();) {
            Processor processor = (Processor) i.next();

            // Create ProcessorInput for each branch
            Map<String, ProcessorInput> currentBranchInputs = new HashMap<String, ProcessorInput>();
            branchInputs.add(currentBranchInputs);
            for (Iterator j = inputs.iterator(); j.hasNext();) {
                String inputName = (String) j.next();
                currentBranchInputs.put(inputName, processor.createInput(inputName));
            }

            // Create ProcessorOutput for each branch
            Map<Object, ProcessorOutput> currentBranchOutputs = new HashMap<Object, ProcessorOutput>();
            branchOutputs.add(currentBranchOutputs);
            for (Iterator j = CollectionUtils.union(outputsById, outputsByParamRef).iterator(); j.hasNext();) {
                String outputName = (String) j.next();
                currentBranchOutputs.put(outputName, processor.createOutput(outputName));
            }
        }
    }

    /**
     * Those outputs that must be connected to an outer pipeline output
     */
    public Set getOutputsByParamRef() {
        return outputsByParamRef;
    }

    public Set getOutputsById() {
        return outputsById;
    }

    @Override
    public ProcessorOutput createOutput(String name) {
        final String _name = name;
        final ProcessorOutput output = new ProcessorOutputImpl(ConcreteChooseProcessor.this, name) {
            public void readImpl(PipelineContext context, XMLReceiver xmlReceiver) {
                final State state = (State) getState(context);
                if (!state.started)
                    start(context);
                final ProcessorOutput branchOutput = state.selectedBranchOutputs.get(_name);
                branchOutput.read(context, xmlReceiver);
            }

            @Override
            public OutputCacheKey getKeyImpl(PipelineContext pipelineContext) {
                if (isInputInCache(pipelineContext, AbstractChooseProcessor.CHOOSE_DATA_INPUT)) {
                    final State state = (State) getState(pipelineContext);
                    if (!state.started)
                        start(pipelineContext);
                    return state.selectedBranchOutputs.get(_name).getKey(pipelineContext);
                } else
                    return null;
            }

            @Override
            protected Object getValidityImpl(PipelineContext pipelineContext) {
                if (isInputInCache(pipelineContext, AbstractChooseProcessor.CHOOSE_DATA_INPUT)) {
                    final State state = (State) getState(pipelineContext);
                    if (!state.started)
                        start(pipelineContext);
                    return state.selectedBranchOutputs.get(_name).getValidity(pipelineContext);
                } else
                    return null;
            }
        };
        addOutput(name, output);
        return output;
    }

    @Override
    public void start(PipelineContext pipelineContext) {
        final State state = (State) getState(pipelineContext);
        if (state.started)
            throw new IllegalStateException("ASTChoose Processor already started");

        // Choose which branch we want to run (we cache the decision)
        DocumentInfo hrefDocumentInfo = null;
        int branchIndex = 0;
        int selectedBranch = -1;
        for (Iterator i = branchConditions.iterator(); i.hasNext(); branchIndex++) {
            // Evaluate expression
            final String condition = (String) i.next();
            if (condition == null) {
                selectedBranch = branchIndex;
                break;
            }
            // LATER: Try to cache the XPath expressions.

            // Lazily read input in case there is only a p:otherwise
            if (hrefDocumentInfo == null) {
                final Configuration configuration = XPath.GlobalConfiguration();
                hrefDocumentInfo = readCacheInputAsTinyTree(pipelineContext, configuration, AbstractChooseProcessor.CHOOSE_DATA_INPUT);
            }

            try {
                final NamespaceMapping namespaces = branchNamespaces.get(branchIndex);
                final PooledXPathExpression expression = XPathCache.getXPathExpression(hrefDocumentInfo.getConfiguration(),
                    hrefDocumentInfo, "boolean(" + condition + ")", namespaces, null,
                    org.orbeon.oxf.pipeline.api.FunctionLibrary.instance(), null, locationData);// TODO: location should be that of branch

                if (((Boolean) expression.evaluateSingleToJavaReturnToPoolOrNull()).booleanValue()) {
                    selectedBranch = branchIndex;
                    break;
                }
            } catch (Exception e) {
                if (logger.isDebugEnabled())
                    logger.debug("Choose: condition evaluation failed for condition: " + condition + " at " + branchProcessors.get(branchIndex));// TODO: location should be that of branch
                throw new ValidationException("Choose: condition evaluation failed for condition: " + condition, e, locationData);// TODO: location should be that of branch
            }
        }

        // In case the source document is large, and not cached, make it gc-able quicker (not sure if that makes a big difference!)
        hrefDocumentInfo = null;

        if (selectedBranch == -1) {
            // No branch was selected: this is not acceptable if there are output to the choose
            if (!outputsById.isEmpty() || !outputsByParamRef.isEmpty())
                throw new ValidationException("Condition failed for every branch of choose: "
                        + branchConditions.toString(), locationData);
        } else {

            // Initialize variables depending on selected branch
            final Processor selectedBranchProcessor = branchProcessors.get(selectedBranch);
            final Map<String, ProcessorInput> selectedBranchInputs = branchInputs.get(selectedBranch);
            state.selectedBranchOutputs = branchOutputs.get(selectedBranch);

            // Connect branch inputs
            for (Iterator<String> i = selectedBranchInputs.keySet().iterator(); i.hasNext();) {
                final String branchInputName = i.next();
                final ProcessorInput branchInput = selectedBranchInputs.get(branchInputName);
                final ProcessorInput chooseInput = getInputByName(branchInputName);
                branchInput.setOutput(chooseInput.getOutput());
            }

            // Connect branch outputs, or start processor
            selectedBranchProcessor.reset(pipelineContext);
            if (outputsById.size() == 0 && outputsByParamRef.size() == 0) {
                if (logger.isDebugEnabled()) {
                    final String condition = (String) branchConditions.get(selectedBranch);
                    // TODO: location should be that of branch
                    if (condition != null)
                        logger.debug("Choose: taking when branch with test: " + condition + " at " + locationData);
                    else
                        logger.debug("Choose: taking otherwise branch at " + locationData);
                }
                selectedBranchProcessor.start(pipelineContext);
            }
            state.started = true;
        }
    }

    @Override
    public void reset(PipelineContext context) {
        setState(context, new State());
        for (final Processor processor : branchProcessors)
            processor.reset(context);
    }

    private static class State {
        public boolean started = false;
        public Map<Object, ProcessorOutput> selectedBranchOutputs;
    }
}
