/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.camel.model;

import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlRootElement;

import org.apache.camel.Predicate;
import org.apache.camel.Processor;
import org.apache.camel.builder.ExpressionClause;
import org.apache.camel.model.language.ExpressionDefinition;
import org.apache.camel.processor.ChoiceProcessor;
import org.apache.camel.processor.FilterProcessor;
import org.apache.camel.spi.AsPredicate;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.RouteContext;
import org.apache.camel.util.CollectionStringBuffer;
import org.apache.camel.util.ObjectHelper;

/**
 * Routes messages based on a series of predicates
 *
 * @version
 */
@Metadata(label = "eip,routing")
@XmlRootElement(name = "choice")
@XmlAccessorType(XmlAccessType.FIELD)
public class ChoiceDefinition extends ProcessorDefinition<ChoiceDefinition> {
    @XmlElementRef @AsPredicate
    private List<WhenDefinition> whenClauses = new ArrayList<>();
    @XmlElement
    private OtherwiseDefinition otherwise;

    private transient boolean onlyWhenOrOtherwise = true;

    public ChoiceDefinition() {
    }
    
    @Override
    public List<ProcessorDefinition<?>> getOutputs() {
        // wrap the outputs into a list where we can on the inside control the when/otherwise
        // but make it appear as a list on the outside
        return new AbstractList<ProcessorDefinition<?>>() {

            public ProcessorDefinition<?> get(int index) {
                if (index < whenClauses.size()) {
                    return whenClauses.get(index);
                } 
                if (index == whenClauses.size()) {
                    return otherwise;
                }
                throw new IndexOutOfBoundsException("Index " + index + " is out of bounds with size " + size());
            }

            public boolean add(ProcessorDefinition<?> def) {
                if (def instanceof WhenDefinition) {
                    return whenClauses.add((WhenDefinition)def);
                } else if (def instanceof OtherwiseDefinition) {
                    otherwise = (OtherwiseDefinition)def;
                    return true;
                }
                throw new IllegalArgumentException("Expected either a WhenDefinition or OtherwiseDefinition but was "
                        + ObjectHelper.classCanonicalName(def));
            }

            public int size() {
                return whenClauses.size() + (otherwise == null ? 0 : 1);
            }

            public void clear() {
                whenClauses.clear();
                otherwise = null;
            }

            public ProcessorDefinition<?> set(int index, ProcessorDefinition<?> element) {
                if (index < whenClauses.size()) {
                    if (element instanceof WhenDefinition) {
                        return whenClauses.set(index, (WhenDefinition)element);
                    }
                    throw new IllegalArgumentException("Expected WhenDefinition but was "
                            + ObjectHelper.classCanonicalName(element));
                } else if (index == whenClauses.size()) {
                    ProcessorDefinition<?> old = otherwise;
                    otherwise = (OtherwiseDefinition)element;
                    return old;
                }
                throw new IndexOutOfBoundsException("Index " + index + " is out of bounds with size " + size());
            }

            public ProcessorDefinition<?> remove(int index) {
                if (index < whenClauses.size()) {
                    return whenClauses.remove(index);
                } else if (index == whenClauses.size()) {
                    ProcessorDefinition<?> old = otherwise;
                    otherwise = null;
                    return old;
                }
                throw new IndexOutOfBoundsException("Index " + index + " is out of bounds with size " + size());
            }
        };
    }

    @Override
    public boolean isOutputSupported() {
        return true;
    }
    
    @Override
    public String toString() {
        return "Choice[" + getWhenClauses() + (getOtherwise() != null ? " " + getOtherwise() : "") + "]";
    }

    @Override
    public Processor createProcessor(RouteContext routeContext) throws Exception {
        List<FilterProcessor> filters = new ArrayList<>();
        for (WhenDefinition whenClause : whenClauses) {
            // also resolve properties and constant fields on embedded expressions in the when clauses
            ExpressionNode exp = whenClause;
            ExpressionDefinition expressionDefinition = exp.getExpression();
            if (expressionDefinition != null) {
                // resolve properties before we create the processor
                ProcessorDefinitionHelper.resolvePropertyPlaceholders(routeContext.getCamelContext(), expressionDefinition);

                // resolve constant fields (eg Exchange.FILE_NAME)
                ProcessorDefinitionHelper.resolveKnownConstantFields(expressionDefinition);
            }

            FilterProcessor filter = (FilterProcessor) createProcessor(routeContext, whenClause);
            filters.add(filter);
        }
        Processor otherwiseProcessor = null;
        if (otherwise != null) {
            otherwiseProcessor = createProcessor(routeContext, otherwise);
        }
        return new ChoiceProcessor(filters, otherwiseProcessor);
    }

    @Override
    public void addOutput(ProcessorDefinition<?> output) {
        if (onlyWhenOrOtherwise) {
            if (output instanceof WhenDefinition || output instanceof OtherwiseDefinition) {
                // okay we are adding a when or otherwise so allow any kind of output after this again
                onlyWhenOrOtherwise = false;
            } else {
                throw new IllegalArgumentException("A new choice clause should start with a when() or otherwise(). "
                    + "If you intend to end the entire choice and are using endChoice() then use end() instead.");
            }
        }
        super.addOutput(output);
    }

    @Override
    public ProcessorDefinition<?> end() {
        // we end a block so only when or otherwise is supported
        onlyWhenOrOtherwise = true;
        return super.end();
    }

    @Override
    public ChoiceDefinition endChoice() {
        // we end a block so only when or otherwise is supported
        onlyWhenOrOtherwise = true;
        return super.endChoice();
    }

    // Fluent API
    // -------------------------------------------------------------------------

    /**
     * Sets the predicate for the when node
     *
     * @param predicate the predicate
     * @return the builder
     */
    public ChoiceDefinition when(@AsPredicate Predicate predicate) {
        addClause(new WhenDefinition(predicate));
        return this;
    }

    /**
     * Creates an expression for the when node
     *
     * @return expression to be used as builder to configure the when node
     */
    @AsPredicate
    public ExpressionClause<ChoiceDefinition> when() {
        ExpressionClause<ChoiceDefinition> clause = new ExpressionClause<>(this);
        addClause(new WhenDefinition(clause));
        return clause;
    }
    
    private void addClause(ProcessorDefinition<?> when) {
        onlyWhenOrOtherwise = true;
        popBlock();
        addOutput(when);
        pushBlock(when);
    }
    
    /**
     * Sets the otherwise node
     *
     * @return the builder
     */
    public ChoiceDefinition otherwise() {
        OtherwiseDefinition answer = new OtherwiseDefinition();
        addClause(answer);
        return this;
    }

    @Override
    public void setId(String value) {
        // when setting id, we should set it on the fine grained element, if possible
        if (otherwise != null) {
            otherwise.setId(value);
        } else if (!getWhenClauses().isEmpty()) {
            int size = getWhenClauses().size();
            getWhenClauses().get(size - 1).setId(value);
        } else {
            super.setId(value);
        }
    }

    // Properties
    // -------------------------------------------------------------------------

    @Override
    public String getShortName() {
        return "choice";
    }

    @Override
    public String getLabel() {
        CollectionStringBuffer buffer = new CollectionStringBuffer("choice[");
        List<WhenDefinition> list = getWhenClauses();
        for (WhenDefinition whenType : list) {
            buffer.append(whenType.getLabel());
        }
        buffer.append("]");
        return buffer.toString();
    }

    public List<WhenDefinition> getWhenClauses() {
        return whenClauses;
    }

    /**
     * Sets the when clauses
     */
    public void setWhenClauses(List<WhenDefinition> whenClauses) {
        this.whenClauses = whenClauses;
    }

    public OtherwiseDefinition getOtherwise() {
        return otherwise;
    }

    public void setOtherwise(OtherwiseDefinition otherwise) {
        this.otherwise = otherwise;
    }

    @Override
    public void configureChild(ProcessorDefinition<?> output) {
        if (whenClauses == null || whenClauses.isEmpty()) {
            return;
        }
        for (WhenDefinition when : whenClauses) {
            if (when.getExpression() instanceof ExpressionClause) {
                ExpressionClause<?> clause = (ExpressionClause<?>) when.getExpression();
                if (clause.getExpressionType() != null) {
                    // if using the Java DSL then the expression may have been set using the
                    // ExpressionClause which is a fancy builder to define expressions and predicates
                    // using fluent builders in the DSL. However we need afterwards a callback to
                    // reset the expression to the expression type the ExpressionClause did build for us
                    when.setExpression(clause.getExpressionType());
                }
            }
        }
    }
}
