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

import org.apache.axiom.om.OMAttribute;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.util.AXIOMUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.Mediator;
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.SynapseException;
import org.apache.synapse.config.SynapseConfiguration;
import org.apache.synapse.config.XMLToObjectMapper;
import org.apache.synapse.config.xml.eventing.EventPublisherMediatorFactory;
import org.apache.synapse.libraries.imports.SynapseImport;
import org.apache.synapse.libraries.model.Library;
import org.apache.synapse.mediators.Value;
import org.apache.synapse.mediators.template.InvokeMediator;
import org.apache.synapse.mediators.template.InvokeParam;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

/**
 *
 *
 * This class is based on J2SE Service Provider model
 * http://java.sun.com/j2se/1.3/docs/guide/jar/jar.html#Service%20Provider
 */

public class MediatorFactoryFinder implements XMLToObjectMapper {

 	private Map<String, Library> synapseLibraryMap;
    private Map<String, SynapseImport> synapseImportMap;

	private static final Log log = LogFactory.getLog(MediatorFactoryFinder.class);

    private static final Class[] mediatorFactories = {
            SequenceMediatorFactory.class,
            LogMediatorFactory.class,
            SendMediatorFactory.class,
            FilterMediatorFactory.class,
            SynapseMediatorFactory.class,
            DropMediatorFactory.class,
            HeaderMediatorFactory.class,
            FaultMediatorFactory.class,
            PropertyMediatorFactory.class,
            PropertyGroupMediatorFactory.class,
            SwitchMediatorFactory.class,
            InMediatorFactory.class,
            OutMediatorFactory.class,
            ClassMediatorFactory.class,
            ValidateMediatorFactory.class,
            XSLTMediatorFactory.class,
            AnnotatedCommandMediatorFactory.class,
            POJOCommandMediatorFactory.class,
            CloneMediatorFactory.class,
            IterateMediatorFactory.class,
            AggregateMediatorFactory.class,
            DBReportMediatorFactory.class,
            DBLookupMediatorFactory.class,
            CalloutMediatorFactory.class,
            EventPublisherMediatorFactory.class,
            TransactionMediatorFactory.class,
            EnqueueMediatorFactory.class,
            ConditionalRouterMediatorFactory.class,
            SamplingThrottleMediatorFactory.class,
            URLRewriteMediatorFactory.class,
            EnrichMediatorFactory.class,
            MessageStoreMediatorFactory.class,
            TemplateMediatorFactory.class,
            InvokeMediatorFactory.class,
            PayloadFactoryMediatorFactory.class,
            BeanMediatorFactory.class,
            EJBMediatorFactory.class,
            CallMediatorFactory.class,
            LoopBackMediatorFactory.class,
            RespondMediatorFactory.class,
            CommentMediatorFactory.class,
            ForEachMediatorFactory.class,
            JSONTransformMediatorFactory.class,
            NTLMMediatorFactory.class,
            VariableMediatorFactory.class,
            ScatterGatherMediatorFactory.class,
            ThrowErrorMediatorFactory.class
    };

    private final static MediatorFactoryFinder instance  = new MediatorFactoryFinder();

    /**
     * A map of mediator QNames to implementation class
     */
    private static Map<QName, Class> factoryMap = new HashMap<QName, Class>();

    private static boolean initialized = false;

    public static synchronized MediatorFactoryFinder getInstance() {
        if (!initialized) {
            loadMediatorFactories();
        }
        return instance;
    }

    /**
     * Force re initialization next time
     */
    public static synchronized void reset() {
        factoryMap.clear();
        initialized = false;
    }

    private MediatorFactoryFinder() {
    }

    private static void loadMediatorFactories() {
        for (Class c : mediatorFactories) {
            try {
                MediatorFactory fac = (MediatorFactory) c.newInstance();
                factoryMap.put(fac.getTagQName(), c);
            } catch (Exception e) {
                throw new SynapseException("Error instantiating " + c.getName(), e);
            }
        }
        // now iterate through the available pluggable mediator factories
        registerExtensions();
        initialized = true;
    }

    /**
     * Register pluggable mediator factories from the classpath
     *
     * This looks for JAR files containing a META-INF/services that adheres to the following
     * http://java.sun.com/j2se/1.3/docs/guide/jar/jar.html#Service%20Provider
     */
    private static void registerExtensions() {

        // register MediatorFactory extensions
        Iterator it = java.util.ServiceLoader.load(MediatorFactory.class).iterator();
        while (it.hasNext()) {
            MediatorFactory mf = (MediatorFactory) it.next();
            QName tag = mf.getTagQName();
            factoryMap.put(tag, mf.getClass());
            if (log.isDebugEnabled()) {
                log.debug("Added MediatorFactory " + mf.getClass() + " to handle " + tag);
            }
        }
    }

    /**
     * This method returns a Processor given an OMElement. This will be used
     * recursively by the elements which contain processor elements themselves
     * (e.g. rules)
     *
     * @param element XML representation of a mediator
     * @param properties bag of properties to pass in any information to the factory
     * @return Processor
     */
    public Mediator getMediator(OMElement element, Properties properties) {
        return this.getMediator(element, properties, null);
    }

    /**
     * This method returns a Processor given an OMElement. This will be used
     * recursively by the elements which contain processor elements themselves
     * (e.g. rules)
     *
     * @param element XML representation of a mediator
     * @param properties bag of properties to pass in any information to the factory
     * @param configuration SynapseConfiguration to which the mediator is being added
     * @return Processor
     */
    public Mediator getMediator(OMElement element, Properties properties, SynapseConfiguration configuration) {
        return this.getMediator(element, properties, configuration, null);
    }

    /**
     * This method returns a Processor given an OMElement. This will be used
     * recursively by the elements which contain processor elements themselves
     * (e.g. rules)
     *
     * @param element            XML representation of a mediator
     * @param properties         bag of properties to pass in any information to the factory
     * @param configuration      SynapseConfiguration to which the mediator is being added
     * @param artifactIdentifier Artifact identifier of the artifact being deployed
     * @return Processor
     */
    public Mediator getMediator(OMElement element, Properties properties, SynapseConfiguration configuration, String artifactIdentifier) {

        String localName = element.getLocalName();
        QName qName;
        if (element.getNamespace() != null) {
            qName = new QName(element.getNamespace().getNamespaceURI(), localName);
        } else {
            qName = new QName(localName);
        }
        if (log.isDebugEnabled()) {
            log.debug("getMediator(" + qName + ")");
        }
        Class cls = factoryMap.get(qName);

        if (cls == null && localName.indexOf('.') > -1) {
            String newLocalName = localName.substring(0, localName.indexOf('.'));
            qName = new QName(element.getNamespace().getNamespaceURI(), newLocalName);
            if (log.isDebugEnabled()) {
                log.debug("getMediator.2(" + qName + ")");
            }
            cls = factoryMap.get(qName);
        }

        if (cls == null) {
            if (synapseLibraryMap != null
                    && !synapseLibraryMap.isEmpty()) {
                for (Map.Entry<String, Library> entry : synapseLibraryMap.entrySet()) {
                    if (entry.getValue().getLibArtifactDetails().containsKey(localName)) {
                        return getDynamicInvokeMediator(element, entry.getValue().getPackage(), properties);
                    }
                }
            }

            // Synapse imports is look-up from synapse configurations. however to make the fix backward compatible
            // kept the look-up from synapse imports as it is.
            if (configuration != null) {
                if (configuration.getSynapseImports() != null && !configuration.getSynapseImports().isEmpty()) {
                    for (Map.Entry<String, SynapseImport> entry : configuration.getSynapseImports().entrySet()) {
                        boolean isVersionedDeploy = FactoryUtils.isVersionedDeployment(properties) || artifactIdentifier != null;
                        if (isVersionedDeploy && artifactIdentifier == null) {
                            artifactIdentifier = properties.getProperty(SynapseConstants.SYNAPSE_ARTIFACT_IDENTIFIER);
                        }

                        if (localName.startsWith(entry.getValue().getLibName()) &&
                                (!isVersionedDeploy || StringUtils.equals(artifactIdentifier,
                                        getArtifactIdentifierFromLibraryName(entry.getValue().getLibPackage())))) {
                            return getDynamicInvokeMediator(element, entry.getValue().getLibPackage(), properties);
                        }
                    }
                }
            } else {
                if (synapseImportMap != null && !synapseImportMap.isEmpty()) {
                    for (Map.Entry<String, SynapseImport> entry : synapseImportMap.entrySet()) {
                        boolean isVersionedDeploy = FactoryUtils.isVersionedDeployment(properties) || artifactIdentifier != null;
                        if (isVersionedDeploy && artifactIdentifier == null) {
                            artifactIdentifier = properties.getProperty(SynapseConstants.SYNAPSE_ARTIFACT_IDENTIFIER);
                        }
                        if (localName.startsWith(entry.getValue().getLibName()) &&
                                (!isVersionedDeploy || StringUtils.equals(artifactIdentifier,
                                        getArtifactIdentifierFromLibraryName(entry.getValue().getLibPackage())))) {
                            return getDynamicInvokeMediator(element, entry.getValue().getLibPackage(), properties);
                        }
                    }
                }
            }

            String msg = "Unknown mediator referenced by configuration element : " + qName;
            log.error(msg);
            throw new SynapseException(msg);
        }

        try {
			MediatorFactory mf = (MediatorFactory) cls.newInstance();
			return mf.createMediator(element, properties);

        } catch (InstantiationException e) {
            String msg = "Error initializing mediator factory : " + cls;
            log.error(msg);
            throw new SynapseException(msg, e);

        } catch (IllegalAccessException e) {
            String msg = "Error initializing mediator factory : " + cls;
            log.error(msg);
            throw new SynapseException(msg, e);
		}
	}

    /**
     * This method exposes all the MediatorFactories and its Extensions
     * @return factoryMap
     */
    public Map<QName, Class> getFactoryMap() {
        return factoryMap;
    }

    /**
     * Allow the mediator factory finder to act as an XMLToObjectMapper for Mediators
     * (i.e. Sequence Mediator) loaded dynamically from a Registry
     * @param om node from which the object is expected
     * @return Object buit from the om node
     */
    public Object getObjectFromOMNode(OMNode om, Properties properties) {
        if (om instanceof OMElement) {
            SynapseConfiguration configuration = properties.get(SynapseConstants.SYNAPSE_CONFIGURATION) != null ?
                    (SynapseConfiguration) properties.get(SynapseConstants.SYNAPSE_CONFIGURATION) : null;
            return getMediator((OMElement) om, properties, configuration);
        } else {
            handleException("Invalid mediator configuration XML : " + om);
        }
        return null;
    }

    public Object getObjectFromOMNode(OMNode om, Properties properties, String artifactIdentifier) {
        if (om instanceof OMElement) {
            SynapseConfiguration configuration = properties.get(SynapseConstants.SYNAPSE_CONFIGURATION) != null ?
                    (SynapseConfiguration) properties.get(SynapseConstants.SYNAPSE_CONFIGURATION) : null;
            return getMediator((OMElement) om, properties, configuration, artifactIdentifier);
        } else {
            handleException("Invalid mediator configuration XML : " + om);
        }
        return null;
    }

    private void handleException(String msg) {
        log.error(msg);
        throw new SynapseException(msg);
    }

    public Map<String, Library> getSynapseLibraryMap() {
        return synapseLibraryMap;
    }

    public void setSynapseLibraryMap(Map<String, Library> synapseLibraryMap) {
        this.synapseLibraryMap = synapseLibraryMap;
    }

    public Map<String, SynapseImport> getSynapseImportMap() {
        return synapseImportMap;
    }

    public void setSynapseImportMap(Map<String, SynapseImport> synapseImportMap) {
        this.synapseImportMap = synapseImportMap;
    }

    public static void main(String[] args) throws Exception{
        String connectorStr = "<sfdc.getContact xmlns=\"http://ws.apache.org/ns/synapse\">\n" +
                "\t\t <parameter name=\"param1\" value=\"val1\"/>\n" +
                "\t\t <parameter name=\"param2\" value=\"val2\"/>\n" +
                "\t</sfdc.getContact>";

        OMElement inConnectorElem = AXIOMUtil.stringToOM(connectorStr);
        String libName = "synapse.lang.eip";

        InvokeMediator invokeMediator = MediatorFactoryFinder.getInstance().getDynamicInvokeMediator(inConnectorElem, libName);
        invokeMediator.getTargetTemplate();


    }

    public OMElement getCallTemplateFromConnector(OMElement connectorElem, String libraryName) {
        String callTemplateConfig = "<call-template target=\"synapse.lang.eip.sfdc.getContact\">\n" +
                "            <with-param name=\"p1\" value=\"abc\"/>\n" +
                "            <with-param name=\"p2\" value=\"efg\"/>\n" +
                "        </call-template>";
        OMElement callTemplateElem = null;

        try {
            callTemplateElem = AXIOMUtil.stringToOM(callTemplateConfig);


        } catch (XMLStreamException e) {
            e.printStackTrace();
        }
        return callTemplateElem;
    }

    public InvokeMediator getDynamicInvokeMediator(OMElement connectorElem, String libraryName) {

        return getDynamicInvokeMediator(connectorElem, libraryName, new Properties());
    }

    public InvokeMediator getDynamicInvokeMediator(OMElement connectorElem, String libraryName, Properties properties) {

        InvokeMediator invokeMediator = new InvokeMediator();
        if (connectorElem.getLocalName() != null
                && libraryName != null
                && !libraryName.equals("")) {
            invokeMediator.setTargetTemplate(libraryName + "." + connectorElem.getLocalName());
            invokeMediator.setOperation(connectorElem.getLocalName());
        }

        // load configuration based references for the given connector
        OMAttribute config_key = connectorElem.getAttribute(new QName(XMLConfigConstants.CONFIG_REF));
        if (config_key != null) {
            invokeMediator.setConnector(true);
            // ValueFactory for creating dynamic or static Value
            ValueFactory keyFac = new ValueFactory();
            // create dynamic or static key based on OMElement
            Value generatedKey = keyFac.createValue(XMLConfigConstants.CONFIG_REF, connectorElem);
            // setKey
            if (generatedKey.getKeyValue() != null && !generatedKey.hasExprTypeKey()) {
                generatedKey = new Value(FactoryUtils.getFullyQualifiedName(properties, config_key.getAttributeValue()));
            }
            invokeMediator.setKey(generatedKey);
        }

        buildParamteres(connectorElem, invokeMediator);

        invokeMediator.setPackageName(libraryName);
        invokeMediator.setDynamicMediator(true);
        return invokeMediator;
    }

    private void buildParamteres(OMElement connectorElem, InvokeMediator invokeMediator) {

        Iterator parameters = connectorElem.getChildElements();
        while (parameters.hasNext()) {
            OMNode paramNode = (OMNode) parameters.next();
            if (paramNode instanceof OMElement) {
                String paramName = ((OMElement) paramNode).getLocalName();
                if (StringUtils.isNotEmpty(paramName)) {
                    invokeMediator.addInvokeParam(paramName, createInvokeParam((OMElement) paramNode));
                }
            }
        }
    }

    /**
     * Creates the {@link InvokeParam} object from the given connector parameter node.
     *
     * @param paramNode connector parameter node
     * @return {@link InvokeParam} object
     */
    private InvokeParam createInvokeParam(OMElement paramNode) {

        String paramName = paramNode.getLocalName();
        InvokeParam connectorParam = new InvokeParam();
        connectorParam.setParamName(paramName);

        // Add attributes to the connectorParam
        Iterator attributesIterator = paramNode.getAllAttributes();
        while (attributesIterator.hasNext()) {
            OMAttribute attribute = (OMAttribute) attributesIterator.next();
            String attributeName = attribute.getLocalName();
            String attributeValue = attribute.getAttributeValue();
            if (StringUtils.isNotEmpty(attributeName) && StringUtils.isNotEmpty(attributeValue)) {
                Value value = new ValueFactory().createValue(attributeName, paramNode);
                connectorParam.addAttribute2Expression(attributeName, value);
            }
        }

        // Add child parameters to the connectorParam
        Iterator childParams = paramNode.getChildElements();
        if (childParams.hasNext()) {
            do {
                InvokeParam childParam = createInvokeParam((OMElement) childParams.next());
                if (childParam != null) {
                    connectorParam.addChildParam(childParam);
                }
            } while (childParams.hasNext());
        } else {
            // If there are no child parameters, add the inline value to the connectorParam
            String paramValueStr = paramNode.getText();
            if (StringUtils.isNotEmpty(paramValueStr)) {
                Value paramValue = new ValueFactory().createTextValue(paramNode);
                connectorParam.setInlineValue(paramValue);
            }
        }
        return connectorParam;
    }

    private String getArtifactIdentifierFromLibraryName(String libraryName) {
        if (StringUtils.countMatches(libraryName, FactoryUtils.DOUBLE_UNDERSCORE) < 3) {
            return libraryName;
        }
        return libraryName.substring(0, libraryName.lastIndexOf(FactoryUtils.DOUBLE_UNDERSCORE));
    }
}
