/*
 *  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.synapse.mediators.base;

import org.apache.synapse.ContinuationState;
import org.apache.synapse.Mediator;
import org.apache.synapse.MessageContext;
import org.apache.synapse.Nameable;
import org.apache.synapse.SequenceType;
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.SynapseLog;
import org.apache.synapse.aspects.AspectConfiguration;
import org.apache.synapse.aspects.flow.statistics.StatisticIdentityGenerator;
import org.apache.synapse.aspects.flow.statistics.collectors.CloseEventCollector;
import org.apache.synapse.aspects.flow.statistics.collectors.OpenEventCollector;
import org.apache.synapse.aspects.flow.statistics.collectors.RuntimeStatisticCollector;
import org.apache.synapse.aspects.flow.statistics.data.artifact.ArtifactHolder;
import org.apache.synapse.mediators.v2.ScatterGatherUtils;
import org.apache.synapse.transport.customlogsetter.CustomLogSetter;
import org.apache.synapse.aspects.ComponentType;
import org.apache.synapse.continuation.ContinuationStackManager;
import org.apache.synapse.continuation.SeqContinuationState;
import org.apache.synapse.core.SynapseEnvironment;
import org.apache.synapse.mediators.AbstractListMediator;
import org.apache.synapse.mediators.FlowContinuableMediator;
import org.apache.synapse.mediators.MediatorFaultHandler;
import org.apache.synapse.mediators.Value;

import java.util.Stack;

/**
 * The Sequence mediator either refers to a named Sequence mediator instance
 * or is a *Named* list/sequence of other (child) Mediators
 * <p/>
 * If this instance defines a sequence mediator, then the name is required, and
 * an errorHandler sequence name optional. If this instance refers to another (defined)
 * sequence mediator, the errorHandler will not have a meaning, and if an error in
 * encountered in the referred sequence, its errorHandler would execute.
 */
public class SequenceMediator extends AbstractListMediator implements Nameable,
                                                                      FlowContinuableMediator {

    /** The name of the this sequence */
    private String name = null;
    /** The local registry key which is used to pick a sequence definition*/
    private Value key = null;
    /** The name of the error handler which is used to handle error during the mediation */
    private String errorHandler = null;
    /** is this definition dynamic */
    private boolean dynamic = false;
    /** flag to ensure that each and every sequence is initialized and destroyed atmost once */
    private boolean initialized = false;
    /** the registry key to load this definition if dynamic */
    private String registryKey = null;
    /** The name of the file where this sequence is defined */
    private String fileName;
    /** type of the sequence*/
    private SequenceType sequenceType = SequenceType.NAMED;
    /** Reference to the synapse environment */
    private SynapseEnvironment synapseEnv;
    /** Name of the car file which the sequence deployed from */
    private String artifactContainerName;
    /**
     * Whether the sequence is edited through the management console or not
     */
    private boolean isEdited;

    /**
     * If this mediator refers to another named Sequence, execute that. Else
     * execute the list of mediators (children) contained within this. If a referenced
     * named sequence mediator instance cannot be found at runtime, an exception is
     * thrown. This may occur due to invalid configuration of an erroneous runtime
     * change of the synapse configuration. It is the responsibility of the
     * SynapseConfiguration builder to ensure that dead references are not present.
     *
     * @param synCtx the synapse message
     * @return as per standard mediator result
     */
    public boolean mediate(MessageContext synCtx) {

        if (synCtx.getEnvironment().isDebuggerEnabled()) {
            if (super.divertMediationRoute(synCtx)) {
                return true;
            }
        }

        SynapseLog synLog = getLog(synCtx);

        if (sequenceType == SequenceType.NAMED) {
            CustomLogSetter.getInstance().setLogAppender(artifactContainerName);
        }

        if (synLog.isTraceOrDebugEnabled()) {
            synLog.traceOrDebug("Start : Sequence "
                    + (name == null ? (key == null ? "<anonymous" : "key=<" + key) : "<"
                    + name) + ">");

            if (synLog.isTraceTraceEnabled()) {
                synLog.traceTrace("Message : " + synCtx.getEnvelope());
            }
        }

        if (key == null) {

            // The onError sequence for handling errors which may occur during the
            // mediation through this sequence
            Mediator errorHandlerMediator = null;
            Integer statisticReportingIndex = null;
            if (RuntimeStatisticCollector.isStatisticsEnabled()) {
                statisticReportingIndex = reportOpenStatistics(synCtx, false);
            }
            // Setting Required property to reportForComponent the sequence aspects

            try {
                // push the errorHandler sequence into the current message as the fault handler
                if (errorHandler != null) {
                    errorHandlerMediator = synCtx.getSequence(errorHandler);

                    if (errorHandlerMediator != null) {
                        if (synLog.isTraceOrDebugEnabled()) {
                            synLog.traceOrDebug("Setting the onError handler : " +
                                    errorHandler + " for the sequence : " + name);
                        }
                        synCtx.pushFaultHandler(
                                new MediatorFaultHandler(errorHandlerMediator));
                    } else {
                        synLog.auditWarn("onError handler : " + errorHandler + " for sequence : " +
                                name + " cannot be found");
                    }
                }

                // Add a new SeqContinuationState as we branched to new Sequence.
                boolean skipAddition = ContinuationStackManager.isSkipSeqContinuationStateAddition(synCtx);
                if (!skipAddition) {
                    if (dynamic && registryKey != null) {
                        ContinuationStackManager.addSeqContinuationState(synCtx, registryKey, sequenceType);
                    } else {
                        ContinuationStackManager.addSeqContinuationState(synCtx, name, sequenceType);
                    }
                }

                boolean result = super.mediate(synCtx);

                if (result && !skipAddition && !ScatterGatherUtils.isScatterMessage(synCtx)) {
                    // if flow completed remove the previously added SeqContinuationState
                    ContinuationStackManager.removeSeqContinuationState(synCtx, sequenceType);
                }

                // if we pushed an error handler, pop it from the fault stack
                // before we exit normally without an exception
                if (result && errorHandlerMediator != null) {
                    Stack faultStack = synCtx.getFaultStack();
                    if (faultStack != null && !faultStack.isEmpty()) {
                        Object o = faultStack.peek();
                        if (o instanceof MediatorFaultHandler &&
                                errorHandlerMediator.equals(
                                        ((MediatorFaultHandler) o).getFaultMediator())) {
                                faultStack.pop();
                        }
                    }
                }

                if (synLog.isTraceOrDebugEnabled()) {
                    if (synLog.isTraceTraceEnabled()) {
                        synLog.traceTrace("Message : " + synCtx.getEnvelope());
                    }

                    synLog.traceOrDebug(
                            "End : Sequence <" + (name == null ? "anonymous" : name) + ">");
                }


                return result;

            } finally {
                // End Statistics
                if (RuntimeStatisticCollector.isStatisticsEnabled()) {
                    reportCloseStatistics(synCtx, statisticReportingIndex);
                }
            }

        } else {
            String sequenceKey = key.evaluateValue(synCtx);
            //Mediator m = synCtx.getSequence(key);
            Mediator m = synCtx.getSequence(sequenceKey);
            if (m == null) {
                handleException("Sequence named " + key + " cannot be found", synCtx);

            } else {
                if (synLog.isTraceOrDebugEnabled()) {
                    synLog.traceOrDebug("Executing with key " + key);
                }

                // Update the SeqContinuationState position with the mediator position of
                // sequence mediator in the sequence
                ContinuationStackManager.updateSeqContinuationState(synCtx, getMediatorPosition());

                boolean result = m.mediate(synCtx);

                if (synLog.isTraceOrDebugEnabled()) {
                    synLog.traceOrDebug("End : Sequence key=<" + key + ">");
                }
                return result;
            }
        }
        return false;
    }

    public boolean mediate(MessageContext synCtx, ContinuationState continuationState) {

        SynapseLog synLog = getLog(synCtx);

        if (sequenceType == SequenceType.NAMED) {
            CustomLogSetter.getInstance().setLogAppender(artifactContainerName);
        }

        if (synLog.isTraceOrDebugEnabled()) {
            synLog.traceOrDebug("Mediating using the SeqContinuationState type : " +
                                ((SeqContinuationState) continuationState).getSeqType() +
                                " name : " +
                                ((SeqContinuationState) continuationState).getSeqName());
        }

        boolean result;
        if (!continuationState.hasChild()) {
            result = super.mediate(synCtx, continuationState.getPosition() + 1);
        } else {
            // if children exists first mediate from them starting from grandchild.
            do {
                FlowContinuableMediator mediator =
                        (FlowContinuableMediator) getChild(continuationState.getPosition());

                result = mediator.mediate(synCtx,
                                          continuationState.getChildContState());

                if (RuntimeStatisticCollector.isStatisticsEnabled()) {
                    ((Mediator) mediator).reportCloseStatistics(synCtx, null);
                }
                if (result) {
                    // if flow completed remove leaf child
                    continuationState.removeLeafChild();
                }
            } while (result && continuationState.hasChild());

            if (result) {
                // after mediating from children, mediate from current SeqContinuationState
                result = super.mediate(synCtx, continuationState.getPosition() + 1);
            }
        }

        if (result) {
            //pop the Error handler belonging to current mediator
            Stack faultStack = synCtx.getFaultStack();
            if (faultStack != null && !faultStack.isEmpty()) {
                Object o = faultStack.peek();
                if (errorHandler != null) {
                    Mediator errorHandlerMediator = synCtx.getSequence(errorHandler);
                    if (o instanceof MediatorFaultHandler &&
                            errorHandlerMediator.equals(
                                    ((MediatorFaultHandler) o).getFaultMediator())) {
                        faultStack.pop();
                    }
                }
            }
            // if flow completed, remove top ContinuationState from stack
            ContinuationStackManager.popContinuationStateStack(synCtx);
        }

        return result;
    }

    /**
     * This method will ensure that each and every sequence wil only be initialized at most once
     * @param se - environment to be initialized
     */
    @Override
    public synchronized void init(SynapseEnvironment se) {
        if (!initialized) {
            synapseEnv = se;
            super.init(se);
            initialized = true;

            if (!isDynamic()) {
                // mark as available, if this is marked previously as unavailable in the environment
                se.clearUnavailabilityOfArtifact(name);
            }

            if (key != null) {
                if (key.getKeyValue() != null) {

                    SequenceMediator sequenceMediator =
                            (SequenceMediator) se.getSynapseConfiguration().
                                    getSequence(key.getKeyValue());

                    if (sequenceMediator == null || sequenceMediator.isDynamic()) {
                        // undefined or dynamic sequences are treated as unavailable
                        // in the environment.
                        // At the time of their initialization, these will be marked as available.
                        se.addUnavailableArtifactRef(key.getKeyValue());
                    }
                } else {
                    // sequences referred by key-expressions are treated as unavailable at initialization
                    se.addUnavailableArtifactRef(key.getExpression().toString());
                }
            }
        }
    }

    @Override
    public synchronized void destroy() {
        if (initialized) {
            super.destroy();
            initialized = false;

            if (key != null) {
                // Clearing unavailable sequence references added by this sequence
                if (key.getKeyValue() != null) {

                    SequenceMediator sequenceMediator =
                            (SequenceMediator) synapseEnv.getSynapseConfiguration().
                                    getSequence(key.getKeyValue());

                    if (sequenceMediator == null || sequenceMediator.isDynamic()) {
                        synapseEnv.removeUnavailableArtifactRef(key.getKeyValue());
                    }

                } else {
                    synapseEnv.removeUnavailableArtifactRef(key.getExpression().toString());
                }
            }
        }
    }

    /**
     * To get the name of the sequence
     * @return the name of the sequence
     */
    public String getName() {
        return name;
    }

    /**
     * setting the name of the sequence
     * @param name the name of the this sequence
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * To get the key which is used to pick the sequence definition from the local registry
     * @return  return the key which is used to pick the sequence definition from the local registry
     */
    public Value getKey() {
        return key;
    }

    /**
     * To set the local registry key in order to pick the sequence definition
     * @param key the local registry key
     */
    public void setKey(Value key) {
        this.key = key;
    }

    /**
     *
     * @return  Returns the errorhandler sequence name
     */
    public String getErrorHandler() {
        return errorHandler;
    }

    /**
     * @param errorHandler to used handle error will appear during the
     *        mediation through this sequence
     */
    public void setErrorHandler(String errorHandler) {
        this.errorHandler = errorHandler;
    }

    /**
     * Is this a dynamic sequence?
     * @return true if dynamic
     */
    public boolean isDynamic() {
        return dynamic;
    }

    /**
     * Mark this as a dynamic sequence
     * @param dynamic true if this is a dynamic sequence
     */
    public void setDynamic(boolean dynamic) {
        this.dynamic = dynamic;
    }

    /**
     * Return the registry key used to load this sequence dynamically
     * @return  registry key
     */
    public String getRegistryKey() {
        return registryKey;
    }

    /**
     * To get the registry key used to load this sequence dynamically
     * @param registryKey  returns the registry key which point to this sequence
     */
    public void setRegistryKey(String registryKey) {
        this.registryKey = registryKey;
    }

    public String getAuditId() {
        return getName();
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public boolean isInitialized() {
        return initialized;
    }

    public void setArtifactContainerName (String name) {
        artifactContainerName = name;
    }

    public String getArtifactContainerName () {
        return artifactContainerName;
    }

    public boolean isEdited() {
        return isEdited;
    }

    public void setIsEdited(boolean isEdited) {
        this.isEdited = isEdited;
    }

    @Override
    public boolean isContentAware() {
        return false;
    }

    public void setSequenceType(SequenceType sequenceType) {
        this.sequenceType = sequenceType;
    }

    public String getSequenceNameForStatistics() {
        if (this.name != null) {
            return this.name;
        } else {
            if (this.sequenceType != SequenceType.ANON) {
                return this.sequenceType.toString();
            } else {
                return SynapseConstants.ANONYMOUS_SEQUENCE;
            }
        }
    }

    @Override
    public Integer reportOpenStatistics(MessageContext messageContext, boolean isContentAltering) {
        if (key == null) {
            return OpenEventCollector.reportEntryEvent(messageContext, getSequenceNameForStatistics(),
                    getAspectConfiguration(), ComponentType.SEQUENCE);
        }
        return null;
    }

    @Override
    public void reportCloseStatistics(MessageContext messageContext, Integer currentIndex) {
        if (key == null) {
            CloseEventCollector
                    .tryEndFlow(messageContext, getSequenceNameForStatistics(), ComponentType.SEQUENCE,
                                     currentIndex, isContentAltering());
        }
    }

    @Override
    public void setComponentStatisticsId(ArtifactHolder holder) {
        String sequenceId = null;
//        if (sequenceType != SequenceType.ANON) {
            if (getAspectConfiguration() == null) {
                configure(new AspectConfiguration(name));
            }
            if (getKey()!= null) {
                sequenceId = StatisticIdentityGenerator
                        .getIdReferencingComponent(getKey().getKeyValue(), ComponentType.SEQUENCE, holder);
            } else {
                sequenceId = StatisticIdentityGenerator
                        .getIdForFlowContinuableMediator(getSequenceNameForStatistics(), ComponentType.SEQUENCE, holder);
            }
            getAspectConfiguration().setUniqueId(sequenceId);
//        }

        setStatisticIdForMediators(holder);

        if (sequenceType != SequenceType.ANON) {
            StatisticIdentityGenerator.reportingFlowContinuableEndEvent(sequenceId, ComponentType.SEQUENCE, holder);
        }
    }
}
