/*
 *  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.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.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.Context;
import org.jaxen.Function;
import org.jaxen.FunctionCallException;
import org.jaxen.Navigator;
import org.jaxen.function.StringFunction;

import javax.activation.DataHandler;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
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 {

    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 = "";

    /** 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_DEFAULT.equals(argOne) &&
                        !XMLConfigConstants.SCOPE_TRANSPORT.equals(argOne) &&
                            !XMLConfigConstants.SCOPE_REGISTRY.equals(argOne) &&
                            !XMLConfigConstants.SCOPE_FUNC.equals(argOne) &&
                            !XMLConfigConstants.SCOPE_SYSTEM.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_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_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) {
                        OMText omText = (OMText) propEntry.getValue();
                        DataHandler dh = (DataHandler) omText.getDataHandler();
                        if (omText.getDataHandler() != null) {
                            InputStream in = null;
                            try {
                                in = dh.getInputStream();
                                InputStreamReader streamReader = new InputStreamReader(in);
                                BufferedReader stringReader = new BufferedReader(streamReader);
                                StringBuilder omTextString = new StringBuilder(NULL_STRING);
                                String tempStr;
                                while ((tempStr = stringReader.readLine()) != null) {
                                    omTextString.append(tempStr);
                                }
                                return omTextString.toString();
                            } catch (IOException e) {
                                return NULL_STRING;
                            } finally {
                                if (in != null) {
                                    try {
                                        in.close();
                                    } catch (IOException ignore) { }
                                }
                            }
                        } else {
                            omText.getText();
                        }
                    }
                    return propEntry.getValue().toString();
                }
            }
        } else if (XMLConfigConstants.SCOPE_SYSTEM.equals(scope)) {
            String val = System.getProperty(key);
            if (val != null) {
                return val;
            } else {
                if (traceOrDebugOn) {
                    traceOrDebug(traceOn, "No system property is found for key '" + key + "'");
                }
                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);
        }
    }

}

