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

import org.apache.axiom.om.OMText;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.addressing.RelatesTo;
import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.transport.base.BaseConstants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.MessageContext;
import org.apache.synapse.SynapseConstants;
import org.apache.synapse.commons.util.FilePropertyLoader;
import org.apache.synapse.config.Entry;
import org.apache.synapse.config.xml.XMLConfigConstants;
import org.apache.synapse.core.axis2.Axis2MessageContext;
import org.apache.synapse.mediators.template.TemplateContext;
import org.apache.synapse.registry.Registry;
import org.jaxen.*;
import org.jaxen.function.StringFunction;

import javax.xml.xpath.XPathFunction;
import javax.xml.xpath.XPathFunctionException;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Stack;

/**
 * Implements the XPath extension function synapse:get-property(scope,prop-name)
 */
public class GetPropertyFunction implements Function , XPathFunction {

    private static final Log log = LogFactory.getLog(GetPropertyFunction.class);
    private static final Log trace = LogFactory.getLog(SynapseConstants.TRACE_LOGGER);

    public static final String NULL_STRING = "";

    public static Map filePropertyMap = FilePropertyLoader.getPropertyMap();

    /** Synapse Message context*/
    private final MessageContext synCtx;

    public GetPropertyFunction(MessageContext synCtx) {
        this.synCtx = synCtx;
    }

    public MessageContext getSynCtx() {
        return synCtx;
    }

    /**
     * Returns the string value of the property which is get from the corresponding context to the provided scope .
     * The default scope is used to get property from the synapse message context
     *
     * @param context the context at the point in the expression when the function is called
     * @param args  arguments of the functions 
     * @return The string value of a property
     * @throws FunctionCallException
     */
    public Object call(Context context, List args) throws FunctionCallException {

        if (synCtx == null) {
            if (log.isDebugEnabled()) {
                log.debug("Synapse message context has not been set for the " +
                        "XPath extension function 'synapse:get-property(prop-name)'");
            }
            return null;
        }
        
        boolean traceOn = synCtx.getTracingState() == SynapseConstants.TRACING_ON;
        boolean traceOrDebugOn = traceOn || log.isDebugEnabled();

        if (args == null || args.size() == 0) {
            if (traceOrDebugOn) {
                traceOrDebug(traceOn, "Property key value for lookup is not specified");
            }
            return NULL_STRING;

        } else {
            int size = args.size();
            if (size == 1) {
                return evaluate(
                    XMLConfigConstants.SCOPE_DEFAULT, args.get(0), null, context.getNavigator());

            } else if (size == 2) {
                String argOne = StringFunction.evaluate(args.get(0), context.getNavigator());
                if (argOne != null) {
                    if (!XMLConfigConstants.SCOPE_AXIS2.equals(argOne) && !XMLConfigConstants.SCOPE_OPERATION.equals(argOne) &&
                        !XMLConfigConstants.SCOPE_DEFAULT.equals(argOne) &&
                        !XMLConfigConstants.SCOPE_TRANSPORT.equals(argOne) &&
                            !XMLConfigConstants.SCOPE_REGISTRY.equals(argOne) &&
                            !XMLConfigConstants.SCOPE_FUNC.equals(argOne) &&
                            !XMLConfigConstants.SCOPE_SYSTEM.equals(argOne) &&
                            !XMLConfigConstants.SCOPE_ENVIRONMENT.equals(argOne) &&
                            !XMLConfigConstants.SCOPE_FILE.equals(argOne)) {
                        return evaluate(XMLConfigConstants.SCOPE_DEFAULT, args.get(0),
                            args.get(1), context.getNavigator());
                    } else {
                        return evaluate(args.get(0), args.get(1), null, context.getNavigator());
                    }
                }
            } else if (size == 3) {
                return evaluate(args.get(0), args.get(1), args.get(2), context.getNavigator());
            } else {

                String msg = "Invalid arguments for synapse:get-property(prop-name) 0r  " +
                    "synapse:get-property(scope, prop-name) XPath function ";
                if (traceOn) {
                    trace.error(msg);
                }
                log.error(msg);
                throw new FunctionCallException(msg);
            }
        }
        return NULL_STRING;
    }

    /**
     * Returns the string value of the property using arg one as key and arg two as scope
     *
     * @param scopeObject scope will decide from where property will be picked up from
     *        i.e. axis2, transport, default/synapse
     * @param keyObject the key of the property
     * @param navigator object model which can be used for navigation around
     * @param dateformat The dateformat that need to convert
     * @return The String value of property using arg one as key and arg two as scope
     */
    public Object evaluate(Object scopeObject, Object keyObject, Object dateformat,Navigator navigator) {

        boolean traceOn = synCtx.getTracingState() == SynapseConstants.TRACING_ON;
        boolean traceOrDebugOn = traceOn || log.isDebugEnabled();

        String scope = StringFunction.evaluate(scopeObject, navigator);
        String key = StringFunction.evaluate(keyObject, navigator);

        if (key == null || "".equals(key)) {
            if (traceOrDebugOn) {
                traceOrDebug(traceOn,
                    "property-name should be provided when executing synapse:get-property" +
                    "(scope,prop-name) or synapse:get-property(prop-name) Xpath function");
            }
            return NULL_STRING;
        }

        //Irrespective of the scope ,if the dateformat has provided ,
        // should return the Date according to the provided format

        if (SynapseConstants.SYSTEM_DATE.equals(key)) {
            if (dateformat != null) {
                Format formatter = new SimpleDateFormat(dateformat.toString());
                return formatter.format(new java.util.Date());
            } else {
                Format formatter = new SimpleDateFormat();
                return formatter.format(new java.util.Date());
            }
        }

        //return the current system time as a string , don't care scope
        if (SynapseConstants.SYSTEM_TIME.equals(key)) {

            return Long.toString(System.currentTimeMillis());
        }

        if (XMLConfigConstants.SCOPE_DEFAULT.equals(scope)) {

            if (SynapseConstants.HEADER_TO.equals(key)) {
                EndpointReference toEPR = synCtx.getTo();
                if (toEPR != null) {
                    return toEPR.getAddress();
                } else {
                    return NULL_STRING;
                }
            } else if (SynapseConstants.HEADER_FROM.equals(key)) {
                EndpointReference fromEPR = synCtx.getFrom();
                if (fromEPR != null) {
                    return fromEPR.getAddress();
                } else {
                    return NULL_STRING;
                }
            } else if (SynapseConstants.HEADER_ACTION.equals(key)) {
                String wsaAction = synCtx.getWSAAction();
                if (wsaAction != null) {
                    return wsaAction;
                } else {
                    return NULL_STRING;
                }
            } else if (SynapseConstants.HEADER_FAULT.equals(key)) {
                EndpointReference faultEPR = synCtx.getFaultTo();
                if (faultEPR != null) {
                    return faultEPR.getAddress();
                } else {
                    return NULL_STRING;
                }
            } else if (SynapseConstants.HEADER_REPLY_TO.equals(key)) {
                EndpointReference replyToEPR = synCtx.getReplyTo();
                if (replyToEPR != null) {
                    return replyToEPR.getAddress();
                } else {
                    return NULL_STRING;
                }
            } else if (SynapseConstants.HEADER_RELATES_TO.equals(key)) {
                RelatesTo relatesTo = synCtx.getRelatesTo();
                if (relatesTo != null) {
                    return relatesTo.getValue();
                } else {
                    return NULL_STRING;
                }
            } else if (SynapseConstants.HEADER_MESSAGE_ID.equals(key)) {
                String messageID = synCtx.getMessageID();
                if (messageID != null) {
                    return messageID;
                } else {
                    return NULL_STRING;
                }
            } else if (SynapseConstants.PROPERTY_FAULT.equals(key)) {
                if (synCtx.getEnvelope().hasFault()) {
                    return SynapseConstants.TRUE;
                } else if (synCtx instanceof Axis2MessageContext) {
                    org.apache.axis2.context.MessageContext axis2MessageContext
                            = ((Axis2MessageContext) synCtx).getAxis2MessageContext();
                    if (axis2MessageContext.getProperty(BaseConstants.FAULT_MESSAGE) != null
                            && SynapseConstants.TRUE.equals(
                            axis2MessageContext.getProperty(BaseConstants.FAULT_MESSAGE))) {
                        return SynapseConstants.TRUE;
                    }
                } else {
                    return NULL_STRING;
                }
            } else if (SynapseConstants.PROPERTY_MESSAGE_FORMAT.equals(key)) {
                if (synCtx.isDoingPOX())
                    return SynapseConstants.FORMAT_POX;
                else if (synCtx.isDoingGET())
                    return SynapseConstants.FORMAT_GET;
                else if (synCtx.isSOAP11())
                    return SynapseConstants.FORMAT_SOAP11;
                else
                    return SynapseConstants.FORMAT_SOAP12;
            } else if (SynapseConstants.PROPERTY_OPERATION_NAME.equals(key) ||
                    SynapseConstants.PROPERTY_OPERATION_NAMESPACE.equals(key)) {
                if (synCtx instanceof Axis2MessageContext) {
                    AxisOperation axisOperation
                            = ((Axis2MessageContext) synCtx).getAxis2MessageContext().getAxisOperation();
                    if (axisOperation != null) {
                        if (SynapseConstants.PROPERTY_OPERATION_NAMESPACE.equals(key)) {
                            return axisOperation.getName().getNamespaceURI();
                        } else {
                            return axisOperation.getName().getLocalPart();
                        }
                    }
                }
            } else {
                Object result = synCtx.getProperty(key);
                if (result != null) {
                    return result;
                } else {
                    return synCtx.getLocalEntry(key);
                }
            }
        } else if (XMLConfigConstants.SCOPE_AXIS2.equals(scope)
                && synCtx instanceof Axis2MessageContext) {

            org.apache.axis2.context.MessageContext axis2MessageContext
                    = ((Axis2MessageContext) synCtx).getAxis2MessageContext();
            return axis2MessageContext.getProperty(key);

        } else if (XMLConfigConstants.SCOPE_FUNC.equals(scope)) {
            Stack<TemplateContext> functionStack = (Stack) synCtx.getProperty(SynapseConstants.SYNAPSE__FUNCTION__STACK);
            TemplateContext topCtxt = functionStack.peek();
            if (topCtxt!=null) {
                return topCtxt.getParameterValue(key);
            }
        } else if (XMLConfigConstants.SCOPE_TRANSPORT.equals(scope)
                && synCtx instanceof Axis2MessageContext) {

            org.apache.axis2.context.MessageContext axis2MessageContext
                    = ((Axis2MessageContext) synCtx).getAxis2MessageContext();
            Object headers = axis2MessageContext.getProperty(
                    org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);

            if (headers != null && headers instanceof Map) {
                Map headersMap = (Map) headers;
                return headersMap.get(key);
            }

        } else if(XMLConfigConstants.SCOPE_OPERATION.equals(scope)
                && synCtx instanceof Axis2MessageContext){
        	Axis2MessageContext axis2smc = (Axis2MessageContext) synCtx;
            org.apache.axis2.context.MessageContext axis2MessageCtx =
                    axis2smc.getAxis2MessageContext();
            return axis2smc.getAxis2MessageContext().getOperationContext().getProperty(key);
        }else if (XMLConfigConstants.SCOPE_REGISTRY.equals(scope)) {
            String[] regParam = key.split("@");
            String regPath = null;
            String propName = null;
            if (regParam.length == 2) {
                regPath = regParam[0];
                propName = regParam[1];
            } else if (regParam.length == 1) {
                regPath = regParam[0];
            }

            Entry propEntry = synCtx.getConfiguration().getEntryDefinition(regPath);
            if (propEntry == null) {
                propEntry = new Entry();
                propEntry.setType(Entry.REMOTE_ENTRY);
                propEntry.setKey(key);
            }
            Registry registry = synCtx.getConfiguration().getRegistry();
            if (registry != null) {
                registry.getResource(propEntry, new Properties());
                if (propName != null) {
                    Properties reqProperties = propEntry.getEntryProperties();
                    if (reqProperties != null) {
                        if (reqProperties.get(propName) != null) {
                            return reqProperties.getProperty(propName);
                        }
                    }
                } else if (propEntry.getValue() != null) {
                    if (propEntry.getValue() instanceof OMText) {
                        return ((OMText) propEntry.getValue()).getText();
                    }
                    return propEntry.getValue().toString();
                }
            }
        } else if (XMLConfigConstants.SCOPE_SYSTEM.equals(scope)) {
            String propVal = System.getProperty(key);
            if (propVal != null) {
                return propVal;
            } else {
                if (traceOrDebugOn) {
                    traceOrDebug(traceOn, "System property " + key + " not found");
                }
                return NULL_STRING;
            }

        } else if (XMLConfigConstants.SCOPE_ENVIRONMENT.equals(scope)) {
            String propVal = System.getenv(key);
            if (propVal != null) {
                return propVal;
            } else {
                if (traceOrDebugOn) {
                    traceOrDebug(traceOn, "Environment property " + key + " not found");
                }
                return NULL_STRING;
            }

        } else if (XMLConfigConstants.SCOPE_FILE.equals(scope)) {
            Object propVal = filePropertyMap.get(key);
            if (propVal != null) {
                return (String) propVal;
            } else {
                if (traceOrDebugOn) {
                    traceOrDebug(traceOn, "Property " + key + " not found in properties file");
                }
                return NULL_STRING;
            }

        } else {
            if (traceOrDebugOn) {
                traceOrDebug(traceOn, "Invalid scope : '" + scope + "' has been set for the " +
                        "synapse:get-property(scope,prop-name) XPath function");
            }
        }
        return NULL_STRING;
    }

    private void traceOrDebug(boolean traceOn, String msg) {
        if (traceOn) {
            trace.info(msg);
        }
        if (log.isDebugEnabled()) {
            log.debug(msg);
        }
    }

    /**
     * Wraps jaxon xpath function as a javax.xml.xpath.XPathFunction
     * @param args  List of argument for custom xpath function
     * @return result of xpath evaluation
     * @throws XPathFunctionException
     */
    public Object evaluate(List args) throws XPathFunctionException {
        try {
            return call(new Context(new ContextSupport()) ,args);
        } catch (FunctionCallException e) {
            throw new XPathFunctionException(e);
        }
    }
}

