/*
 *  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.filters;

import org.apache.synapse.ContinuationState;
import org.apache.synapse.Mediator;
import org.apache.synapse.MessageContext;
import org.apache.synapse.SynapseLog;
import org.apache.synapse.aspects.AspectConfiguration;
import org.apache.synapse.aspects.ComponentType;
import org.apache.synapse.aspects.flow.statistics.StatisticIdentityGenerator;
import org.apache.synapse.aspects.flow.statistics.collectors.RuntimeStatisticCollector;
import org.apache.synapse.aspects.flow.statistics.data.artifact.ArtifactHolder;
import org.apache.synapse.config.xml.AnonymousListMediator;
import org.apache.synapse.config.xml.SynapsePath;
import org.apache.synapse.continuation.ContinuationStackManager;
import org.apache.synapse.continuation.ReliantContinuationState;
import org.apache.synapse.core.SynapseEnvironment;
import org.apache.synapse.mediators.AbstractListMediator;
import org.apache.synapse.mediators.FlowContinuableMediator;
import org.apache.synapse.mediators.ListMediator;
import org.apache.synapse.mediators.base.SequenceMediator;
import org.apache.synapse.util.xpath.SynapseJsonPath;
import org.apache.synapse.util.xpath.SynapseXPath;
import org.jaxen.JaxenException;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The filter mediator combines the regex and xpath filtering functionality. If an xpath
 * is set, it is evaluated; else the given regex is evaluated against the source xpath.
 */
public class FilterMediator extends AbstractListMediator implements
    org.apache.synapse.mediators.FilterMediator, FlowContinuableMediator {

    private SynapsePath source = null;
    private Pattern regex = null;
    private SynapsePath xpath = null;
    private AnonymousListMediator elseMediator = null;
    private boolean thenElementPresent = false;
    private String thenKey = null;
    private String elseKey = null;
    private SynapseEnvironment synapseEnv;

    @Override
    public void init(SynapseEnvironment se) {
        super.init(se);
        synapseEnv = se;
        if (elseMediator != null) {
            elseMediator.init(se);
        } else if (elseKey != null) {
            SequenceMediator elseSequence =
                    (SequenceMediator) se.getSynapseConfiguration().
                            getSequence(elseKey);

            if (elseSequence == null || elseSequence.isDynamic()) {
                se.addUnavailableArtifactRef(elseKey);
            }
        }

        if (thenKey != null) {
            SequenceMediator thenSequence =
                    (SequenceMediator) se.getSynapseConfiguration().
                            getSequence(thenKey);

            if (thenSequence == null || thenSequence.isDynamic()) {
                se.addUnavailableArtifactRef(thenKey);
            }
        }
    }

    @Override
    public void destroy() {
        super.destroy();
        if (elseMediator != null) {
            elseMediator.destroy();
        } else if (elseKey != null) {
            SequenceMediator elseSequence =
                    (SequenceMediator) synapseEnv.getSynapseConfiguration().
                            getSequence(elseKey);

            if (elseSequence == null || elseSequence.isDynamic()) {
                synapseEnv.removeUnavailableArtifactRef(elseKey);
            }
        }

        if (thenKey != null) {
            SequenceMediator thenSequence =
                    (SequenceMediator) synapseEnv.getSynapseConfiguration().
                            getSequence(thenKey);

            if (thenSequence == null || thenSequence.isDynamic()) {
                synapseEnv.removeUnavailableArtifactRef(thenKey);
            }
        }
    }

    /**
     * Executes the list of sub/child mediators, if the filter condition is satisfied
     *
     * @param synCtx the current message
     * @return true if filter condition fails. else returns as per List mediator semantics
     */
    public boolean mediate(MessageContext synCtx) {

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

        SynapseLog synLog = getLog(synCtx);

        if (synLog.isTraceOrDebugEnabled()) {
            synLog.traceOrDebug("Start : Filter mediator");

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

        boolean result = false;
        if (test(synCtx)) {
            if (thenKey != null) {

                if (synLog.isTraceOrDebugEnabled()) {
                    synLog.traceOrDebug((xpath == null ?
                        "Source : " + source + " against : " + regex.pattern() + " matches" :
                        "XPath expression : "  + xpath + " evaluates to true") +
                        " - executing then sequence with key : " + thenKey);
                }

                ContinuationStackManager.updateSeqContinuationState(synCtx, getMediatorPosition());
                Mediator seq = synCtx.getSequence(thenKey);
                if (seq != null) {
                    result = seq.mediate(synCtx);
                } else {
                    handleException("Couldn't find the referred then sequence with key : "
                        + thenKey, synCtx);
                }
                
            } else {

                if (synLog.isTraceOrDebugEnabled()) {
                    synLog.traceOrDebug((xpath == null ?
                        "Source : " + source + " against : " + regex.pattern() + " matches" :
                        "XPath expression : "  + xpath + " evaluates to true") +
                        " - executing child mediators");
                }

                ContinuationStackManager.
                        addReliantContinuationState(synCtx, 0, getMediatorPosition());
                result = super.mediate(synCtx);
                if (result) {
                    ContinuationStackManager.removeReliantContinuationState(synCtx);
                }
            }
        } else {
            if (elseKey != null) {

                if (synLog.isTraceOrDebugEnabled()) {
                    synLog.traceOrDebug((xpath == null ?
                        "Source : " + source + " against : " + regex.pattern() + " does not match" :
                        "XPath expression : "  + xpath + " evaluates to false") +
                        " - executing the else sequence with key : " + elseKey);
                }

                ContinuationStackManager.updateSeqContinuationState(synCtx, getMediatorPosition());
                Mediator elseSeq = synCtx.getSequence(elseKey);

                if (elseSeq != null) {
                    result = elseSeq.mediate(synCtx);
                } else {
                    handleException("Couldn't find the referred else sequence with key : "
                        + elseKey, synCtx);
                }
                
            } else if (elseMediator != null) {

                if (synLog.isTraceOrDebugEnabled()) {
                    synLog.traceOrDebug((xpath == null ?
                        "Source : " + source + " against : " + regex.pattern() + " does not match" :
                        "XPath expression : "  + xpath + " evaluates to false") +
                        " - executing the else path child mediators");
                }
                ContinuationStackManager.addReliantContinuationState(synCtx, 1, getMediatorPosition());
                result = elseMediator.mediate(synCtx);
                if (result) {
                    ContinuationStackManager.removeReliantContinuationState(synCtx);
                }

            } else {

                if (synLog.isTraceOrDebugEnabled()) {
                    synLog.traceOrDebug((xpath == null ?
                        "Source : " + source + " against : " + regex.pattern() + " does not match" :
                        "XPath expression : "  + xpath + " evaluates to false and no else path") +
                        " - skipping child mediators");
                }
                result = true;
            }
        }

        synLog.traceOrDebug("End : Filter mediator ");
        return result;
    }

    public boolean mediate(MessageContext synCtx,
                           ContinuationState continuationState) {
        SynapseLog synLog = getLog(synCtx);

        if (synLog.isTraceOrDebugEnabled()) {
            synLog.traceOrDebug("Filter mediator : Mediating from ContinuationState");
        }

        boolean result;
        int subBranch = ((ReliantContinuationState) continuationState).getSubBranch();
        boolean isStatisticsEnabled = RuntimeStatisticCollector.isStatisticsEnabled();
        if (subBranch == 0) {
           if (!continuationState.hasChild()) {
               result = super.mediate(synCtx, continuationState.getPosition() + 1);
           } else {
               FlowContinuableMediator mediator =
                       (FlowContinuableMediator) getChild(continuationState.getPosition());

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

               if (isStatisticsEnabled) {
                   ((Mediator) mediator).reportCloseStatistics(synCtx, null);
               }
           }
        } else {
            if (!continuationState.hasChild()) {
                result = elseMediator.mediate(synCtx, continuationState.getPosition() + 1);
            } else {
                FlowContinuableMediator mediator =
                        (FlowContinuableMediator) elseMediator.getChild(
                                continuationState.getPosition());

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

                if (isStatisticsEnabled) {
                    ((Mediator) mediator).reportCloseStatistics(synCtx, null);
                }
            }
            if (isStatisticsEnabled) {
                elseMediator.reportCloseStatistics(synCtx, null);
            }
        }

        return result;
    }

    /**
     * Tests the supplied condition after evaluation against the given XPath
     * or Regex (against a source XPath). When a regular expression is supplied
     * the source XPath is evaluated into a String value, and matched against
     * the given regex
     *
     * @param synCtx the current message for evaluation of the test condition
     * @return true if evaluation of the XPath/Regex results in true
     */
    public boolean test(MessageContext synCtx) {

        SynapseLog synLog = getLog(synCtx);

        if (xpath != null) {
            try {
                if (xpath instanceof SynapseXPath) {
                    return xpath.booleanValueOf(synCtx);
                } else if (xpath instanceof SynapseJsonPath) {
                    return ((SynapseJsonPath) xpath).booleanValueOf(synCtx);
                }
            } catch (JaxenException e) {
                handleException("Error evaluating XPath expression : " + xpath, e, synCtx);
            }

        } else if (source != null && regex != null) {
            String sourceString = source.stringValueOf(synCtx);
            if (sourceString == null) {
                if (synLog.isTraceOrDebugEnabled()) {
                    synLog.traceOrDebug("Source String : " + source + " evaluates to null");
                }
                return false;
            }
            Matcher matcher = regex.matcher(sourceString);
            if (matcher == null) {
                if (synLog.isTraceOrDebugEnabled()) {
                    synLog.traceOrDebug("Regex pattern matcher for : " + regex.pattern() +
                        "against source : " + sourceString + " is null");
                }
                return false;
            }
            return matcher.matches();
        }

        return false; // never executes
    }


    public SynapsePath getSource() {
        return source;
    }

    public void setSource(SynapsePath source) {
        this.source = source;
    }

    public Pattern getRegex() {
        return regex;
    }

    public void setRegex(Pattern regex) {
        this.regex = regex;
    }

    public SynapsePath getXpath() {
        return xpath;
    }

    public void setXpath(SynapsePath xpath) {
        this.xpath = xpath;
    }

    public ListMediator getElseMediator() {
        return elseMediator;
    }

    public void setElseMediator(AnonymousListMediator elseMediator) {
        this.elseMediator = elseMediator;
    }

    public boolean isThenElementPresent() {
        return thenElementPresent;
    }

    public void setThenElementPresent(boolean thenElementPresent) {
        this.thenElementPresent = thenElementPresent;
    }

    public String getThenKey() {
        return thenKey;
    }

    public void setThenKey(String thenKey) {
        this.thenKey = thenKey;
    }

    public String getElseKey() {
        return elseKey;
    }

    public void setElseKey(String elseKey) {
        this.elseKey = elseKey;
    }

    @Override
    public boolean isContentAware() {
        if (xpath != null) {
            return xpath.isContentAware();
        } else if (source != null) {
            return source.isContentAware();
        }
        return false;
    }

    @Override public void setComponentStatisticsId(ArtifactHolder holder) {
        if (getAspectConfiguration() == null) {
            configure(new AspectConfiguration(getMediatorName()));
        }
        String mediatorId =
                StatisticIdentityGenerator.getIdForFlowContinuableMediator(getMediatorName(), ComponentType.MEDIATOR, holder);
        getAspectConfiguration().setUniqueId(mediatorId);
        String childId;

        StatisticIdentityGenerator.reportingBranchingEvents(holder);
        if (thenKey != null) {
            childId = StatisticIdentityGenerator.getIdReferencingComponent(thenKey, ComponentType.SEQUENCE, holder);
            StatisticIdentityGenerator.reportingEndEvent(childId, ComponentType.SEQUENCE, holder);
        } else {
            setStatisticIdForMediators(holder);
        }
        StatisticIdentityGenerator.reportingEndBranchingEvent(holder);

        StatisticIdentityGenerator.reportingBranchingEvents(holder);
        if (elseKey != null) {
            childId = StatisticIdentityGenerator.getIdReferencingComponent(elseKey, ComponentType.SEQUENCE, holder);
            StatisticIdentityGenerator.reportingEndEvent(childId, ComponentType.SEQUENCE, holder);
        } else if (elseMediator != null) {
            elseMediator.setStatisticIdForMediators(holder);
        }
        StatisticIdentityGenerator.reportingFlowContinuableEndEvent(mediatorId, ComponentType.MEDIATOR, holder);
        StatisticIdentityGenerator.reportingEndBranchingEvent(holder);
    }
}
