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

/* $Id: AdvancedMessageFormat.java 1805177 2017-08-16 11:02:44Z ssteiner $ */

package org.apache.fop.util.text;

import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

import org.apache.xmlgraphics.util.Service;


/**
 * Formats messages based on a template and with a set of named parameters. This is similar to
 * {@link java.text.MessageFormat} but uses named parameters and supports conditional sub-groups.
 * <p>
 * Example:
 * </p>
 * <p><code>Missing field "{fieldName}"[ at location: {location}]!</code></p>
 * <ul>
 *   <li>Curly brackets ("{}") are used for fields.</li>
 *   <li>Square brackets ("[]") are used to delimit conditional sub-groups. A sub-group is
 *     conditional when all fields inside the sub-group have a null value. In the case, everything
 *     between the brackets is skipped.</li>
 * </ul>
 */
public class AdvancedMessageFormat {

    /** Regex that matches "," but not "\," (escaped comma) */
    static final Pattern COMMA_SEPARATOR_REGEX = Pattern.compile("(?<!\\\\),");

    private static final Map<String, PartFactory> PART_FACTORIES
        = new java.util.HashMap<String, PartFactory>();
    private static final List<ObjectFormatter> OBJECT_FORMATTERS
        = new java.util.ArrayList<ObjectFormatter>();
    private static final Map<Object, Function> FUNCTIONS
        = new java.util.HashMap<Object, Function>();

    private CompositePart rootPart;

    static {
        Iterator<Object> iter;
        iter = Service.providers(PartFactory.class);
        while (iter.hasNext()) {
            PartFactory factory = (PartFactory)iter.next();
            PART_FACTORIES.put(factory.getFormat(), factory);
        }
        iter = Service.providers(ObjectFormatter.class);
        while (iter.hasNext()) {
            OBJECT_FORMATTERS.add((ObjectFormatter)iter.next());
        }
        iter = Service.providers(Function.class);
        while (iter.hasNext()) {
            Function function = (Function)iter.next();
            FUNCTIONS.put(function.getName(), function);
        }
    }

    /**
     * Construct a new message format.
     * @param pattern the message format pattern.
     */
    public AdvancedMessageFormat(CharSequence pattern) {
        parsePattern(pattern);
    }

    private void parsePattern(CharSequence pattern) {
        rootPart = new CompositePart(false);
        StringBuffer sb = new StringBuffer();
        parseInnerPattern(pattern, rootPart, sb, 0);
    }

    private int parseInnerPattern(CharSequence pattern, CompositePart parent,
            StringBuffer sb, int start) {
        assert sb.length() == 0;
        int i = start;
        int len = pattern.length();
        loop:
        while (i < len) {
            char ch = pattern.charAt(i);
            switch (ch) {
            case '{':
                if (sb.length() > 0) {
                    parent.addChild(new TextPart(sb.toString()));
                    sb.setLength(0);
                }
                i++;
                int nesting = 1;
                while (i < len) {
                    ch = pattern.charAt(i);
                    if (ch == '{') {
                        nesting++;
                    } else if (ch == '}') {
                        nesting--;
                        if (nesting == 0) {
                            i++;
                            break;
                        }
                    }
                    sb.append(ch);
                    i++;
                }
                parent.addChild(parseField(sb.toString()));
                sb.setLength(0);
                break;
            case ']':
                i++;
                break loop; //Current composite is finished
            case '[':
                if (sb.length() > 0) {
                    parent.addChild(new TextPart(sb.toString()));
                    sb.setLength(0);
                }
                i++;
                CompositePart composite = new CompositePart(true);
                parent.addChild(composite);
                i += parseInnerPattern(pattern, composite, sb, i);
                break;
            case '|':
                if (sb.length() > 0) {
                    parent.addChild(new TextPart(sb.toString()));
                    sb.setLength(0);
                }
                parent.newSection();
                i++;
                break;
            case '\\':
                if (i < len - 1) {
                    i++;
                    ch = pattern.charAt(i);
                }
                sb.append(ch);
                i++;
                break;
            default:
                sb.append(ch);
                i++;
                break;
            }
        }
        if (sb.length() > 0) {
            parent.addChild(new TextPart(sb.toString()));
            sb.setLength(0);
        }
        return i - start;
    }

    private Part parseField(String field) {
        String[] parts = COMMA_SEPARATOR_REGEX.split(field, 3);
        String fieldName = parts[0];
        if (parts.length == 1) {
            if (fieldName.startsWith("#")) {
                return new FunctionPart(fieldName.substring(1));
            } else {
                return new SimpleFieldPart(fieldName);
            }
        } else {
            String format = parts[1];
            PartFactory factory = PART_FACTORIES.get(format);
            if (factory == null) {
                throw new IllegalArgumentException(
                        "No PartFactory available under the name: " + format);
            }
            if (parts.length == 2) {
                return factory.newPart(fieldName, null);
            } else {
                return factory.newPart(fieldName, parts[2]);
            }
        }
    }

    private static Function getFunction(String functionName) {
        return FUNCTIONS.get(functionName);
    }

    /**
     * Formats a message with the given parameters.
     * @param params a Map of named parameters (Contents: &lt;String, Object&gt;)
     * @return the formatted message
     */
    public String format(Map<String, Object> params) {
        StringBuffer sb = new StringBuffer();
        format(params, sb);
        return sb.toString();
    }

    /**
     * Formats a message with the given parameters.
     * @param params a Map of named parameters (Contents: &lt;String, Object&gt;)
     * @param target the target StringBuffer to write the formatted message to
     */
    public void format(Map<String, Object> params, StringBuffer target) {
        rootPart.write(target, params);
    }

    /**
     * Represents a message template part. This interface is implemented by various variants of
     * the single curly braces pattern ({field}, {field,if,yes,no} etc.).
     */
    public interface Part {

        /**
         * Writes the formatted part to a string buffer.
         * @param sb the target string buffer
         * @param params the parameters to work with
         */
        void write(StringBuffer sb, Map<String, Object> params);

        /**
         * Indicates whether there is any content that is generated by this message part.
         * @param params the parameters to work with
         * @return true if the part has content
         */
        boolean isGenerated(Map<String, Object> params);
    }

    /**
     * Implementations of this interface parse a field part and return message parts.
     */
    public interface PartFactory {

        /**
         * Creates a new part by parsing the values parameter to configure the part.
         * @param fieldName the field name
         * @param values the unparsed parameter values
         * @return the new message part
         */
        Part newPart(String fieldName, String values);

        /**
         * Returns the name of the message part format.
         * @return the name of the message part format
         */
        String getFormat();
    }

    /**
     * Implementations of this interface format certain objects to strings.
     */
    public interface ObjectFormatter {

        /**
         * Formats an object to a string and writes the result to a string buffer.
         * @param sb the target string buffer
         * @param obj the object to be formatted
         */
        void format(StringBuffer sb, Object obj);

        /**
         * Indicates whether a given object is supported.
         * @param obj the object
         * @return true if the object is supported by the formatter
         */
        boolean supportsObject(Object obj);
    }

    /**
     * Implementations of this interface do some computation based on the message parameters
     * given to it. Note: at the moment, this has to be done in a local-independent way since
     * there is no locale information.
     */
    public interface Function {

        /**
         * Executes the function.
         * @param params the message parameters
         * @return the function result
         */
        Object evaluate(Map<String, Object> params);

        /**
         * Returns the name of the function.
         * @return the name of the function
         */
        Object getName();
    }

    private static class TextPart implements Part {

        private String text;

        public TextPart(String text) {
            this.text = text;
        }

        public void write(StringBuffer sb, Map<String, Object> params) {
            sb.append(text);
        }

        public boolean isGenerated(Map<String, Object> params) {
            return true;
        }

        /** {@inheritDoc} */
        public String toString() {
            return this.text;
        }
    }

    private static class SimpleFieldPart implements Part {

        private String fieldName;

        public SimpleFieldPart(String fieldName) {
            this.fieldName = fieldName;
        }

        public void write(StringBuffer sb, Map<String, Object> params) {
            if (!params.containsKey(fieldName)) {
                throw new IllegalArgumentException(
                        "Message pattern contains unsupported field name: " + fieldName);
            }
            Object obj = params.get(fieldName);
            formatObject(obj, sb);
        }

        public boolean isGenerated(Map<String, Object> params) {
            Object obj = params.get(fieldName);
            return obj != null;
        }

        /** {@inheritDoc} */
        public String toString() {
            return "{" + this.fieldName + "}";
        }
    }

    /**
     * Formats an object to a string and writes the result to a string buffer. This method
     * usually uses the object's <code>toString()</code> method unless there is an
     * {@link ObjectFormatter} that supports the object. {@link ObjectFormatter}s are registered
     * through the service provider mechanism defined by the JAR specification.
     * @param obj the object to be formatted
     * @param target the target string buffer
     */
    public static void formatObject(Object obj, StringBuffer target) {
        if (obj instanceof String) {
            target.append(obj);
        } else {
            boolean handled = false;
            for (ObjectFormatter formatter : OBJECT_FORMATTERS) {
                if (formatter.supportsObject(obj)) {
                    formatter.format(target, obj);
                    handled = true;
                    break;
                }
            }
            if (!handled) {
                target.append(String.valueOf(obj));
            }
        }
    }

    private static class FunctionPart implements Part {

        private Function function;

        public FunctionPart(String functionName) {
            this.function = getFunction(functionName);
            if (this.function == null) {
                throw new IllegalArgumentException("Unknown function: " + functionName);
            }
        }

        public void write(StringBuffer sb, Map<String, Object> params) {
            Object obj = this.function.evaluate(params);
            formatObject(obj, sb);
        }

        public boolean isGenerated(Map<String, Object> params) {
            Object obj = this.function.evaluate(params);
            return obj != null;
        }

        /** {@inheritDoc} */
        public String toString() {
            return "{#" + this.function.getName() + "}";
        }
    }

    private static class CompositePart implements Part {

        protected List<Part> parts = new java.util.ArrayList<Part>();
        private boolean conditional;
        private boolean hasSections;

        public CompositePart(boolean conditional) {
            this.conditional = conditional;
        }

        private CompositePart(List<Part> parts) {
            this.parts.addAll(parts);
            this.conditional = true;
        }

        public void addChild(Part part) {
            if (part == null) {
                throw new NullPointerException("part must not be null");
            }
            if (hasSections) {
                CompositePart composite = (CompositePart) this.parts.get(this.parts.size() - 1);
                composite.addChild(part);
            } else {
                this.parts.add(part);
            }
        }

        public void newSection() {
            if (!hasSections) {
                List<Part> p = this.parts;
                //Dropping into a different mode...
                this.parts = new java.util.ArrayList<Part>();
                this.parts.add(new CompositePart(p));
                hasSections = true;
            }
            this.parts.add(new CompositePart(true));
        }

        public void write(StringBuffer sb, Map<String, Object> params) {
            if (hasSections) {
                for (Part part : this.parts) {
                    if (part.isGenerated(params)) {
                        part.write(sb, params);
                        break;
                    }
                }
            } else {
                if (isGenerated(params)) {
                    for (Part part : this.parts) {
                        part.write(sb, params);
                    }
                }
            }
        }

        public boolean isGenerated(Map<String, Object> params) {
            if (hasSections) {
                for (Part part : this.parts) {
                    if (part.isGenerated(params)) {
                        return true;
                    }
                }
                return false;
            } else {
                if (conditional) {
                    for (Part part : this.parts) {
                        if (!part.isGenerated(params)) {
                            return false;
                        }
                    }
                }
                return true;
            }
        }

        /** {@inheritDoc} */
        public String toString() {
            return this.parts.toString();
        }
    }


    static String unescapeComma(String string) {
        return string.replaceAll("\\\\,", ",");
    }
}
