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

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.TimeZone;

import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamSource;

import org.apache.ofbiz.base.conversion.Converter;
import org.apache.ofbiz.base.conversion.Converters;
import org.apache.ofbiz.base.conversion.LocalizedConverter;
import org.apache.ofbiz.base.location.FlexibleLocation;
import org.apache.ofbiz.base.util.Debug;
import org.apache.ofbiz.base.util.ScriptUtil;
import org.apache.ofbiz.base.util.UtilProperties;
import org.apache.ofbiz.base.util.UtilXml;
import org.apache.ofbiz.base.util.collections.FlexibleMapAccessor;
import org.apache.ofbiz.base.util.string.FlexibleStringExpander;
import org.apache.ofbiz.entity.GenericEntity;
import org.apache.ofbiz.minilang.method.MethodContext;
import org.apache.ofbiz.minilang.method.MethodObject;
import org.apache.ofbiz.minilang.method.MethodOperation;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

/**
 * Mini-language utilities.
 */
public final class MiniLangUtil {

    public static final String module = MiniLangUtil.class.getName();

    private static final Set<String> SCRIPT_PREFIXES;

    static {
        Set<String> scriptPrefixes = new HashSet<String>();
        for (String scriptName : ScriptUtil.SCRIPT_NAMES) {
            scriptPrefixes.add(scriptName.concat(":"));
        }
        SCRIPT_PREFIXES = Collections.unmodifiableSet(scriptPrefixes);
    }

    private MiniLangUtil() {}

    /**
     * Returns <code>true</code> if <code>str</code> contains a script.
     * @param str The string to test
     * @return <code>true</code> if <code>str</code> contains a script
     */
    public static boolean containsScript(String str) {
        if (str.length() > 0) {
            for (String scriptPrefix : SCRIPT_PREFIXES) {
                if (str.contains(scriptPrefix)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Returns <code>true</code> if the Mini-language script engine has been configured to
     * auto-correct version changes.
     * @return <code>true</code> if the Mini-language script engine has been configured to
     * auto-correct version changes
     */
    public static boolean autoCorrectOn() {
        return "true".equals(UtilProperties.getPropertyValue("minilang", "autocorrect"));
    }

    /**
     * Calls an object method.
     * @param operation A reference to the <code>MethodOperation</code> calling this method
     * @param methodContext
     * @param parameters
     * @param methodClass
     * @param methodObject
     * @param methodName
     * @param retFieldFma
     * @throws MiniLangRuntimeException
     */
    public static void callMethod(MethodOperation operation, MethodContext methodContext, List<MethodObject<?>> parameters, Class<?> methodClass, Object methodObject, String methodName, FlexibleMapAccessor<Object> retFieldFma) throws MiniLangRuntimeException {
        Object[] args = null;
        Class<?>[] parameterTypes = null;
        if (parameters != null) {
            args = new Object[parameters.size()];
            parameterTypes = new Class<?>[parameters.size()];
            int i = 0;
            for (MethodObject<?> methodObjectDef : parameters) {
                args[i] = methodObjectDef.getObject(methodContext);
                Class<?> typeClass = null;
                try {
                    typeClass = methodObjectDef.getTypeClass(methodContext);
                } catch (ClassNotFoundException e) {
                    throw new MiniLangRuntimeException(e, operation);
                }
                parameterTypes[i] = typeClass;
                i++;
            }
        }
        try {
            Method method = methodClass.getMethod(methodName, parameterTypes);
            Object retValue = method.invoke(methodObject, args);
            if (!retFieldFma.isEmpty()) {
                retFieldFma.put(methodContext.getEnvMap(), retValue);
            }
        } catch (Exception e) {
            throw new MiniLangRuntimeException(e, operation);
        }
    }

    /**
     * Returns <code>obj</code> converted to the type specified in <code>targetClass</code>.
     * @param obj
     * @param targetClass
     * @param locale
     * @param timeZone
     * @param format
     * @return The converted object
     * @throws Exception
     */
    @SuppressWarnings("unchecked")
    public static Object convertType(Object obj, Class<?> targetClass, Locale locale, TimeZone timeZone, String format) throws Exception {
        if (obj == null || obj == GenericEntity.NULL_FIELD) {
            return null;
        }
        if (obj instanceof Node) {
            Node node = (Node) obj;
            String nodeValue = node.getTextContent();
            if (targetClass == String.class) {
                return nodeValue;
            } else {
                return convertType(nodeValue, targetClass, locale, timeZone, format);
            }
        }
        if (targetClass == PlainString.class) {
            return obj.toString();
        }
        Class<?> sourceClass = obj.getClass();
        if (sourceClass == targetClass) {
            return obj;
        }
        Converter<Object, Object> converter = (Converter<Object, Object>) Converters.getConverter(sourceClass, targetClass);
        LocalizedConverter<Object, Object> localizedConverter = null;
        if (converter instanceof LocalizedConverter) {
            localizedConverter = (LocalizedConverter<Object, Object>) converter;
            if (locale == null) {
                locale = Locale.getDefault();
            }
            if (timeZone == null) {
                timeZone = TimeZone.getDefault();
            }
            if (format != null && format.isEmpty()) {
                format = null;
            }
            return localizedConverter.convert(obj, locale, timeZone, format);
        }
        return converter.convert(obj);
    }

    /**
     * Flags a Mini-language XML document as corrected.
     * @param element
     */
    public static void flagDocumentAsCorrected(Element element) {
        Document doc = element.getOwnerDocument();
        if (doc != null) {
            doc.setUserData("autoCorrected", "true", null);
        }
    }

    /**
     * Returns a <code>object</code>-derived <code>Class</code> that is
     * suitable for use with the conversion framework. If the argument is
     * <code>null</code> or a <code>String</code>, the method returns the
     * {@link PlainString} class.
     * 
     * @param object
     * @return A <code>Class</code> that is
     * suitable for use with the conversion framework
     */
    public static Class<?> getObjectClassForConversion(Object object) {
        if (object == null || object instanceof String) {
            return PlainString.class;
        } else {
            if (object instanceof java.util.Map<?, ?>) {
                return java.util.Map.class;
            } else if (object instanceof java.util.List<?>) {
                return java.util.List.class;
            } else if (object instanceof java.util.Set<?>) {
                return java.util.Set.class;
            } else {
                return object.getClass();
            }
        }
    }

    /**
     * Returns <code>true</code> if <code>attributeValue</code> is a
     * constant value (it does not contain an expression).
     * 
     * @param attributeValue The value to test
     * @return <code>true</code> if <code>attributeValue</code> is a
     * constant value
     */
    public static boolean isConstantAttribute(String attributeValue) {
        if (attributeValue.length() > 0) {
            return !FlexibleStringExpander.containsExpression(FlexibleStringExpander.getInstance(attributeValue));
        }
        return true;
    }

    /**
     * Returns <code>true</code> if <code>attributeValue</code> is a
     * constant value (it does not contain an expression) or a constant
     * plus expression value.
     * 
     * @param attributeValue The value to test
     * @return <code>true</code> if <code>attributeValue</code> is a
     * constant value or a constant plus expression value
     */
    public static boolean isConstantPlusExpressionAttribute(String attributeValue) {
        if (attributeValue.length() > 0) {
            if (attributeValue.startsWith("${") && attributeValue.endsWith("}")) {
                // A lot of existing code uses concatenated expressions, and they can be difficult
                // to convert to a single expression, so we will allow them for now.
                String expression = attributeValue.substring(2, attributeValue.length() - 1);
                if (!expression.contains("${")) {
                    return true;
                }
            }
            FlexibleStringExpander fse = FlexibleStringExpander.getInstance(attributeValue);
            return FlexibleStringExpander.containsConstant(fse);
        }
        return true;
    }

    /**
     * Returns <code>true</code> if <code>document</code> contains corrections.
     * 
     * @param document The document to test
     * @return  <code>true</code> if <code>document</code> contains corrections
     */
    public static boolean isDocumentAutoCorrected(Document document) {
        return "true".equals(document.getUserData("autoCorrected"));
    }

    /**
     * Writes a Mini-language <code>Document</code> to disk. The XML file is styled by the
     * config/MiniLang.xslt style sheet.
     * 
     * @param xmlURL
     * @param document
     */
    public static void writeMiniLangDocument(URL xmlURL, Document document) {
        URL styleSheetURL = null;
        Transformer transformer = null;
        try {
            styleSheetURL = FlexibleLocation.resolveLocation("component://minilang/config/MiniLang.xslt");
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            try (InputStream styleSheetInStream = styleSheetURL.openStream()) {
            transformer = transformerFactory.newTransformer(new StreamSource(styleSheetInStream));
            }
        } catch (Exception e) {
            Debug.logWarning(e, "Error reading minilang/config/MiniLang.xslt: ", module);
            return;
        }
        try (FileOutputStream fos = new FileOutputStream(xmlURL.getFile())) {
            UtilXml.transformDomDocument(transformer, document, fos);
            Debug.logInfo("Saved Mini-language file " + xmlURL, module);
        } catch (Exception e) {
            Debug.logWarning(e, "Error writing mini-language file " + xmlURL + ": ", module);
        }
    }

    public static class PlainString {}

}
