/**
 * 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.dom4j.Element;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.processor.*;
import org.orbeon.oxf.processor.pipeline.PipelineProcessor;
import org.orbeon.oxf.processor.pipeline.ast.*;
import org.orbeon.oxf.xml.NamespaceMapping;
import org.orbeon.oxf.xml.XMLConstants;
import org.orbeon.oxf.xml.dom4j.Dom4jUtils;
import org.orbeon.oxf.xml.dom4j.LocationData;

import java.util.*;

/**
 * <p><b>Usage</b>
 * <p>This processor is not intended to be used by itelf. Instead, it is used by
 * the PipelineProcessor (so we can keep the PipelineProcessor simpler and
 * see the "choose" functionnality as if it was just an other processor).
 * <p/>
 * <p><b>Lifecycle</b>
 * <ol>
 * <li>createInput("config") is called.
 * <li>createInstance() is called.
 * <li>At this point the config (<p:choose> block) is read and we determine
 * for each branch all the inputs (<input ref="..."> with no corresponding
 * <output id="...">) and all the outputs (<output id="..."> with no
 * <input ref="...">).
 * <li>We ensure that the collection of outputs is exactly the
 * same for each branch.
 * <li>We create a pipeline config with the text of each branch, adding the
 * appropriate <p:param> and appropriate ref="..." attribute to reference
 * those parameters.
 * <li>The collections of pipelines can be cached.
 * <li>We read the input referenced in the <p:choose> and choose which pipeline
 * has to be executed.
 * <li>We connect the selected pipeline with the input of this processor.
 * <li>We start every all the outputs of the pipeline, store the result in a
 * SAXStore and play it back when the read of the corresponding output
 * is called.
 * </ol>
 */
public class AbstractChooseProcessor extends ProcessorImpl implements AbstractProcessor {

    public static final String CHOOSE_DATA_INPUT = "$data";
    private ASTChoose chooseAST;
    private Object validity;

    public AbstractChooseProcessor(ASTChoose chooseAST, Object validity) {
        this.chooseAST = chooseAST;
        this.validity = validity;
        setLocationData(chooseAST.getLocationData());
    }

    public Processor createInstance() {

        // We store here the "refs with no id" and "ids with no ref" for each branch.
        // Those are list of collections (one collection for each branch).
        final List refsWithNoId = new ArrayList();
        final List idsWithNoRef = new ArrayList();
        final List paramRefs = new ArrayList();

        for (Iterator astIterator = chooseAST.getWhen().iterator(); astIterator.hasNext();) {

            // Get info about id used in this branch
            final ASTWhen when = (ASTWhen) astIterator.next();
            IdInfo idInfo = when.getIdInfo();
            paramRefs.add(idInfo.getOutputRefs());

            // Determine all <p:input ref="..."> with no <p:output id="...">.
            // Those are the inputs of this processor.
            final Set branchRefsWithNoId = new HashSet(idInfo.getInputRefs());
            branchRefsWithNoId.removeAll(idInfo.getOutputIds());
            refsWithNoId.add(branchRefsWithNoId);

            // Determine all <p:output id="..."> with no <p:input ref="...">.
            // Those are the outputs of this processor.
            final Set branchIdsWithNoRef = new HashSet(idInfo.getOutputIds());
            branchIdsWithNoRef.removeAll(idInfo.getInputRefs());
            idsWithNoRef.add(branchIdsWithNoRef);
        }

        // Make sure that the "ids with no ref" are the same for each branch
        if (idsWithNoRef.size() > 1) {
            final Collection firstBranchIdsWithNoRef = (Collection) idsWithNoRef.get(0);
            int branchId = 0;
            for (Iterator i = idsWithNoRef.iterator(); i.hasNext();) {
                branchId++;
                final Collection branchIdsWithNoRef = (Collection) i.next();
                if (branchIdsWithNoRef != firstBranchIdsWithNoRef &&
                        !CollectionUtils.isEqualCollection(branchIdsWithNoRef, firstBranchIdsWithNoRef))
                    throw new ValidationException("ASTChoose branch number " + branchId +
                            " does not declare the same ids " + branchIdsWithNoRef.toString() +
                            " as the previous branches " + firstBranchIdsWithNoRef.toString(), getLocationData());
            }
        }

        // Make sure that the "param ref" are the same for each branch
        if (paramRefs.size() > 1) {
            final Collection firstBranchParamRefs = (Collection) paramRefs.get(0);
            int branchId = 0;
            for (Iterator i = paramRefs.iterator(); i.hasNext();) {
                branchId++;
                final Collection branchParamRefs = (Collection) i.next();
                if (branchParamRefs != firstBranchParamRefs &&
                        !CollectionUtils.isEqualCollection(branchParamRefs, firstBranchParamRefs))
                    throw new ValidationException("ASTChoose branch number " + branchId +
                            " does not declare the same refs " + branchParamRefs.toString() +
                            " as the previous branches " + firstBranchParamRefs.toString(), getLocationData());
            }
        }

        // Compute the union of "refs with no id" for all the branches
        final Set allRefsWithNoId = new HashSet();
        for (Iterator i = refsWithNoId.iterator(); i.hasNext();)
            allRefsWithNoId.addAll((Set) i.next());

        // Create the list of inputs based on allRefsWithNoId
        final List astParams = new ArrayList();
        for (int i = 0; i < 2; i++) {
            final Set parameters;
            if (i == 0) {
                parameters = allRefsWithNoId;
            } else {
                parameters = new HashSet();
                parameters.addAll((Set) idsWithNoRef.get(0));
                parameters.addAll((Set) paramRefs.get(0));
            }

            for (Iterator j = parameters.iterator(); j.hasNext();) {
                final String paramName = (String) j.next();
                ASTParam astParam = new ASTParam();
                astParam.setType(i == 0 ? ASTParam.INPUT : ASTParam.OUTPUT);
                astParam.setName(paramName);
                astParams.add(astParam);
            }
        }

        // For each branch, create a new pipeline processor
        final List<Processor> branchProcessors = new ArrayList();
        final List branchConditions = new ArrayList();
        final List<NamespaceMapping> branchNamespaces = new ArrayList<NamespaceMapping>();

        for (Iterator astIterator = chooseAST.getWhen().iterator(); astIterator.hasNext();) {
            final ASTWhen astWhen = (ASTWhen) astIterator.next();

            // Save condition
            branchConditions.add(astWhen.getTest());
            // Get namespaces declared at this point in the pipeline
            if (astWhen.getNode() != null && astWhen.getNamespaces().mapping.size() != 0) {
                throw new ValidationException("ASTWhen cannot have both a node and namespaces defined", astWhen.getLocationData());
            }
            branchNamespaces.add(astWhen.getNode() != null
                    ? new NamespaceMapping(Dom4jUtils.getNamespaceContextNoDefault((Element) astWhen.getNode()))
                    : astWhen.getNamespaces());

            // Add an identity processor to connect the output of the branch to
            // the <param type="output"> of the pipeline
            final Set idsToConvert = (Set) idsWithNoRef.get(0);
            for (Iterator i = idsToConvert.iterator(); i.hasNext();) {
                final String id = (String) i.next();
                final ASTProcessorCall identityConnector = new ASTProcessorCall(XMLConstants.IDENTITY_PROCESSOR_QNAME);
                {
                    identityConnector.addInput(new ASTInput("data", new ASTHrefId(new ASTOutput(null, id))));
                    final ASTParam outParam = new ASTParam(ASTParam.OUTPUT, id);
                    final LocationData locDat = Dom4jUtils.getLocationData();
                    final ASTOutput astOut = new ASTOutput("data", outParam);
                    astOut.setLocationData(locDat);
                    identityConnector.addOutput(astOut);
                }
                astWhen.addStatement(identityConnector);
            }

            final ASTPipeline astPipeline = new ASTPipeline();
            astPipeline.setValidity(validity);
            astPipeline.getParams().addAll(astParams);
            astPipeline.getStatements().addAll(astWhen.getStatements());
            astPipeline.setNode(astWhen.getNode());
            final Processor pipelineProcessor = new PipelineProcessor(astPipeline);
            if (getId() != null)
                pipelineProcessor.setId(getId() + "-branch" + branchProcessors.size());
            branchProcessors.add(pipelineProcessor);
        }

        return new ConcreteChooseProcessor(getId(), getLocationData(),
                branchConditions, branchNamespaces, branchProcessors,
                allRefsWithNoId, (Set) idsWithNoRef.get(0), (Set) paramRefs.get(0));
    }
}
