/*
 *Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *WSO2 Inc. 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.transform.pfutils;

import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
import freemarker.cache.FileTemplateLoader;
import freemarker.core.StopException;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.OMText;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.text.StringEscapeUtils;
import org.apache.synapse.MessageContext;
import org.apache.synapse.aspects.flow.statistics.util.StatisticsConstants;
import org.apache.synapse.commons.json.JsonUtil;
import org.apache.synapse.config.SynapsePropertiesLoader;
import org.apache.synapse.core.axis2.Axis2MessageContext;
import org.apache.synapse.mediators.transform.ArgumentDetails;
import org.apache.synapse.util.PayloadHelper;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import java.io.File;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;

import static org.apache.synapse.mediators.transform.pfutils.Constants.ARGS_INJECTING_NAME;
import static org.apache.synapse.mediators.transform.pfutils.Constants.ARGS_INJECTING_PREFIX;
import static org.apache.synapse.mediators.transform.pfutils.Constants.AXIS2_PROPERTY_INJECTING_NAME;
import static org.apache.synapse.mediators.transform.pfutils.Constants.CTX_PROPERTY_INJECTING_NAME;
import static org.apache.synapse.mediators.transform.pfutils.Constants.JSON_PAYLOAD_TYPE;
import static org.apache.synapse.mediators.transform.pfutils.Constants.NOT_SUPPORTING_PAYLOAD_TYPE;
import static org.apache.synapse.mediators.transform.pfutils.Constants.PAYLOAD_INJECTING_NAME;
import static org.apache.synapse.mediators.transform.pfutils.Constants.TEXT_PAYLOAD_TYPE;
import static org.apache.synapse.mediators.transform.pfutils.Constants.TRANSPORT_PROPERTY_INJECTING_NAME;
import static org.apache.synapse.mediators.transform.pfutils.Constants.XML_PAYLOAD_TYPE;
import static org.apache.synapse.util.PayloadHelper.TEXTELT;
import static org.apache.synapse.util.PayloadHelper.getXMLPayload;

/**
 * Template Processor implementation for FreeMarker templating language
 */
public class FreeMarkerTemplateProcessor extends TemplateProcessor {

    private final Configuration cfg;
    private final Gson gson;
    private Template freeMarkerTemplate;

    private boolean usingPayload;
    private boolean usingPropertyCtx;
    private boolean usingPropertyAxis2;
    private boolean usingPropertyTransport;
    private boolean usingArgs;

    private static final Log log = LogFactory.getLog(FreeMarkerTemplateProcessor.class);
    private boolean templateLoaded = false;

    private static final String DEFAULT_FREEMARKER_BASE_PATH_CONF = "conf:/";
    private static final String DEFAULT_FREEMARKER_BASE_PATH_GOV = "gov:/";
    private static final String REGISTRY_PATH = "/registry";
    private static final String CONFIG_PATH = "/config";
    private static final String GOVERNANCE_PATH = "/governance";

    public FreeMarkerTemplateProcessor() {

        cfg = new Configuration(Configuration.VERSION_2_3_30);
        cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
        cfg.setLogTemplateExceptions(false);
        try {
            cfg.setTemplateLoader(getBasePathTemplates());
        } catch (TemplateProcessorException e) {
            log.warn("Unable to load feemarker template base path correctly", e);
        }
        gson = new Gson();
    }

    @Override
    public void init() {

        String format = getFormat();
        if (format != null) {
            compileFreeMarkerTemplate(format, getMediaType());
            templateLoaded = true;
        }
        this.readInputFactoryProperties();
    }

    @Override
    public String processTemplate(String template, String mediaType, MessageContext messageContext) {

        try {

            Map<String, Object> data = new HashMap<>();
            int payloadType = getPayloadType(messageContext);

            injectPayloadVariables(messageContext, payloadType, data);
            injectArgs(messageContext, mediaType, data);
            injectProperties(messageContext, data);

            Writer out = new StringWriter();
            freeMarkerTemplate.process(data, out);
            return out.toString();
        } catch (IOException e) {
            handleException("Error parsing FreeMarker template");
        } catch (StopException e) {
            handleException(e.getMessage());
        } catch (TemplateException e) {
            handleException(generateTemplateErrorMessage(e));
        } catch (SAXException | ParserConfigurationException e) {
            handleException("Error reading payload data");
        }

        return "";
    }

    private FileTemplateLoader getBasePathTemplates() throws TemplateProcessorException {
        String freemarkerConfBasePath =
                SynapsePropertiesLoader.getPropertyValue(
                        StatisticsConstants.FREEMARKER_TEMPLATE_BASE_PATH, DEFAULT_FREEMARKER_BASE_PATH_CONF);
        String[] freemarkerBasePathSplit = freemarkerConfBasePath.split("conf:|gov:");
        String carbonHome = System.getProperty("carbon.home");
        String freemarkerBasePath = System.getProperty("carbon.home");
        if (carbonHome != null) {
            freemarkerBasePath = freemarkerBasePath.concat(REGISTRY_PATH);
        } else {
            throw new TemplateProcessorException("Carbon home not found ");
        }
        if (freemarkerConfBasePath.startsWith(DEFAULT_FREEMARKER_BASE_PATH_CONF)) {
            freemarkerBasePath +=  CONFIG_PATH;
        } else if (freemarkerConfBasePath.startsWith(DEFAULT_FREEMARKER_BASE_PATH_GOV)) {
            freemarkerBasePath += GOVERNANCE_PATH;
        } else {
            freemarkerBasePath += CONFIG_PATH;
            log.warn("Error while loading freemarker path " + freemarkerConfBasePath + ". Using default path " + freemarkerBasePath);
        }
        if (freemarkerBasePathSplit.length > 1) {
            freemarkerBasePath += freemarkerBasePathSplit[1];
        }
        try {
            return new FileTemplateLoader(new File(freemarkerBasePath));
        } catch (IOException e) {
            throw new TemplateProcessorException("Error while reading templates directory " + freemarkerBasePath, e);
        }
    }

    private String generateTemplateErrorMessage(TemplateException e) {

        if (log.isDebugEnabled()) {
            return e.getMessageWithoutStackTop();
        } else {
            return "Error parsing FreeMarker template, " +
                    "Syntax error or invalid reference : " +
                    e.getBlamedExpressionString() +
                    " At line: " +
                    e.getLineNumber() +
                    " column: " +
                    e.getColumnNumber();
        }

    }

    private void compileFreeMarkerTemplate(String templateString, String mediaType) {

        try {
            if (XML_TYPE.equals(mediaType) &&
                    StringUtils.isNotEmpty(templateString) && !templateString.startsWith("<#ftl")) {
                templateString = "<pfPadding>" + templateString + "</pfPadding>";
            }

            freeMarkerTemplate = new Template("synapse-template", templateString, cfg);
            findRequiredInjections(templateString);
        } catch (IOException e) {
            handleException("Error compiling FreeMarking template : " + e.getMessage());
        }
    }

    private void findRequiredInjections(String templateString) {

        usingPayload = templateString.contains(PAYLOAD_INJECTING_NAME);
        usingArgs = templateString.contains(ARGS_INJECTING_NAME);
        usingPropertyCtx = templateString.contains(CTX_PROPERTY_INJECTING_NAME);
        usingPropertyAxis2 = templateString.contains(AXIS2_PROPERTY_INJECTING_NAME);
        usingPropertyTransport = templateString.contains(TRANSPORT_PROPERTY_INJECTING_NAME);
    }

    /**
     * Inject argument values to freemarker
     *
     * @param messageContext Message context
     * @param mediaType      Output media type
     * @param data           Freemarker data input
     */
    private void injectArgs(MessageContext messageContext, String mediaType, Map<String, Object> data) {

        if (usingArgs) {
            Map<String, Object> argsValues = new HashMap<>();
            Map<String, ArgumentDetails>[] argValues = getArgValues(mediaType, messageContext);
            for (int i = 0; i < argValues.length; i++) {
                Map<String, ArgumentDetails> argValue = argValues[i];
                Map.Entry<String, ArgumentDetails> argumentDetailsEntry = argValue.entrySet().iterator().next();
                String replacementValue = prepareReplacementValue(mediaType, messageContext, argumentDetailsEntry);
                argsValues.put(ARGS_INJECTING_PREFIX + (i + 1), replacementValue);
            }
            data.put(ARGS_INJECTING_NAME, argsValues);
        }
    }

    /**
     * Inject  payload in to FreeMarker
     *
     * @param messageContext MessageContext
     * @param payloadType    Input payload type
     * @param data           FreeMarker data input
     * @throws SAXException
     * @throws IOException
     * @throws ParserConfigurationException
     */
    private void injectPayloadVariables(MessageContext messageContext, int payloadType, Map<String, Object> data)
            throws SAXException, IOException, ParserConfigurationException {

        if (usingPayload) {
            if (payloadType == XML_PAYLOAD_TYPE) {
                injectXmlPayload(messageContext, data);
            } else if (payloadType == JSON_PAYLOAD_TYPE) {
                injectJsonPayload((Axis2MessageContext) messageContext, data);
            } else if (payloadType == TEXT_PAYLOAD_TYPE) {
                injectTextPayload(messageContext, data);
            } else {
                data.put(PAYLOAD_INJECTING_NAME, "");
            }
        }
    }

    /**
     * Inject a JSON payload in the FreeMarker
     *
     * @param messageContext Message context
     * @param data           FreeMarker data input
     */
    private void injectJsonPayload(Axis2MessageContext messageContext, Map<String, Object> data) {

        org.apache.axis2.context.MessageContext axis2MessageContext = messageContext.getAxis2MessageContext();
        String jsonPayloadString = JsonUtil.jsonPayloadToString(axis2MessageContext);
        try {
            JsonElement jsonElement = new JsonParser().parse(jsonPayloadString);
            if(jsonElement.isJsonObject()){
                injectJsonObject(data, jsonElement);
            }else if(jsonElement.isJsonArray()){
                injectJsonArray(data, jsonElement);
            }else if(jsonElement.isJsonPrimitive()){
                injectJsonPrimitive(data, jsonElement);
            } else if (jsonElement.isJsonNull()) {
                data.put(PAYLOAD_INJECTING_NAME, "null");
            }
        } catch (JsonSyntaxException e) {
            handleException("Invalid JSON payload");
        }
       
    }

    /**
     * Inject a JSON array in to FreeMarker
     *
     * @param data              FreeMarker data input
     * @param jsonElement JSON element
     */
    private void injectJsonArray(Map<String, Object> data, JsonElement jsonElement) {

        List<Object> array;
        Type type = new TypeToken<List<Object>>() {}.getType();
        array = gson.fromJson(jsonElement, type);
        data.put(PAYLOAD_INJECTING_NAME, array);
    }  
    
    /**
     * Inject a JSON primitive in to FreeMarker
     *
     * @param data              FreeMarker data input
     * @param jsonElement JSON element
     */
    private void injectJsonPrimitive(Map<String, Object> data, JsonElement jsonElement) {

        JsonPrimitive jsonPrimitive = jsonElement.getAsJsonPrimitive();
        data.put(PAYLOAD_INJECTING_NAME, jsonPrimitive.toString());
    }   
    
    /**
     * Inject a JSON object in to FreeMarker
     *
     * @param data              FreeMarker data input
     * @param jsonElement JSON element
     */
    private void injectJsonObject(Map<String, Object> data, JsonElement jsonElement) {

        Map<String, Object> map;
        Type type = new TypeToken<Map<String, Object>>() {}.getType();
        map = gson.fromJson(jsonElement, type);
        data.put(PAYLOAD_INJECTING_NAME, map);
    }

    /**
     * Inject an XML payload in to FreeMarker
     *
     * @param messageContext Message context
     * @param data           FreeMarker data input
     * @throws SAXException
     * @throws IOException
     * @throws ParserConfigurationException
     */
    private void injectXmlPayload(MessageContext messageContext, Map<String, Object> data)
            throws SAXException, IOException, ParserConfigurationException {

        data.put(PAYLOAD_INJECTING_NAME, freemarker.ext.dom.NodeModel.parse(
                new InputSource(new StringReader(
                        messageContext.getEnvelope().getBody().getFirstElement().toString()))));
    }

    /**
     * Inject an Text payload in to FreeMarker
     *
     * @param messageContext Message context
     * @param data           FreeMarker data input
     */
    private void injectTextPayload(MessageContext messageContext, Map<String, Object> data) {

        String textPayload;
        OMElement el = getXMLPayload(messageContext.getEnvelope());
        if (el == null || !el.getQName().equals(TEXTELT)) {
            textPayload = "";
        } else {
            textPayload = getTextValue(el);
        }

        data.put(PAYLOAD_INJECTING_NAME, textPayload);
    }

    /**
     * Get text value from OMNode
     *
     * @param node OMNode to get text value
     * @return text value of the node
     */
    private String getTextValue(OMNode node) {

        switch (node.getType()) {
            case OMNode.ELEMENT_NODE:
                StringBuilder sb = new StringBuilder();
                Iterator<OMNode> children = ((OMElement) node).getChildren();
                while (children.hasNext()) {
                    sb.append(getTextValue(children.next()));
                }
                return sb.toString();
            case OMNode.TEXT_NODE:
                String text = ((OMText) node).getText();
                return StringEscapeUtils.escapeXml11(text);
            default:
                return "";
        }
    }

    private void injectProperties(MessageContext synCtx, Map<String, Object> data) {

        injectCtxProperties(synCtx, data);
        injectAxis2Properties(synCtx, data);
        injectTransportProperties(synCtx, data);
    }

    private void injectCtxProperties(MessageContext synCtx, Map<String, Object> data) {

        if (usingPropertyCtx) {
            Map<String, String> properties = new HashMap<>();
            Map<String, Object> propertyMap = ((Axis2MessageContext) synCtx).getProperties();

            for (Map.Entry<String, Object> propertyEntry : propertyMap.entrySet()) {
                String propertyKey = propertyEntry.getKey();
                Object propertyValue = propertyEntry.getValue();
                if (propertyValue != null) {
                    properties.put(propertyKey, propertyValue.toString());
                }
            }

            data.put(CTX_PROPERTY_INJECTING_NAME, properties);
        }
    }

    private void injectAxis2Properties(MessageContext synCtx, Map<String, Object> data) {

        if (usingPropertyAxis2) {
            Map<String, String> properties = new HashMap<>();
            org.apache.axis2.context.MessageContext axis2MessageContext
                    = ((Axis2MessageContext) synCtx).getAxis2MessageContext();
            Iterator<String> propertyNames =
                    axis2MessageContext.getPropertyNames(); // note :: getProperties() in axis2 
            // message context is deprecated 
            while (propertyNames.hasNext()) {
                String propertyName = propertyNames.next();
                Object propertyValue = axis2MessageContext.getProperty(propertyName);
                if (propertyValue != null) {
                    properties.put(propertyName, propertyValue.toString());
                }
            }
            data.put(AXIS2_PROPERTY_INJECTING_NAME, properties);
        }
    }

    private void injectTransportProperties(MessageContext synCtx, Map<String, Object> data) {

        if (usingPropertyTransport) {
            Map<String, String> properties = new HashMap<>();
            org.apache.axis2.context.MessageContext axis2MessageContext
                    = ((Axis2MessageContext) synCtx).getAxis2MessageContext();
            Object headers = axis2MessageContext.getProperty(
                    org.apache.axis2.context.MessageContext.TRANSPORT_HEADERS);

            if (headers instanceof Map) {
                Map headersMap = (Map) headers;
                for (Object propertyEntry : headersMap.entrySet()) {
                    if (propertyEntry instanceof Map.Entry) {
                        Map.Entry entry = (Map.Entry) propertyEntry;
                        String propertyKey = entry.getKey().toString();
                        Object propertyValue = entry.getValue();
                        if (propertyValue != null) {
                            properties.put(propertyKey, propertyValue.toString());
                        }
                    }

                }
            }
            data.put(TRANSPORT_PROPERTY_INJECTING_NAME, properties);
        }
    }

    /**
     * Get the input payload type
     *
     * @param messageContext Message context
     * @return Type of the input paylaod
     */
    private int getPayloadType(MessageContext messageContext) {

        int payloadType = 0;
        try {
            payloadType = PayloadHelper.getPayloadType(messageContext);
        } catch (NullPointerException e) {
            return NOT_SUPPORTING_PAYLOAD_TYPE;
        }

        if (payloadType == PayloadHelper.XMLPAYLOADTYPE) {
            if (JsonUtil.hasAJsonPayload(((Axis2MessageContext) messageContext).getAxis2MessageContext())) {
                return JSON_PAYLOAD_TYPE;
            }  else {
                return XML_PAYLOAD_TYPE;
            }
        } else if (payloadType == PayloadHelper.TEXTPAYLOADTYPE) {
            return TEXT_PAYLOAD_TYPE;
        }

        return NOT_SUPPORTING_PAYLOAD_TYPE;
    }

    /**
     * Get whether the template format was received
     *
     * @return Status of the format of the template
     */
    public boolean getTemplateStatus(){
        return templateLoaded;
    }

}
