// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html#License
/*
*******************************************************************************
*   Copyright (C) 2011-2014, International Business Machines
*   Corporation and others.  All Rights Reserved.
*******************************************************************************
*   created on: 2011jul14
*   created by: Markus W. Scherer
*/

package com.ibm.icu.text;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Utilities for working with a MessagePattern.
 * Intended for use in tools when convenience is more important than
 * minimizing runtime and object creations.
 *
 * <p>This class only has static methods.
 * Each of the nested classes is immutable and thread-safe.
 *
 * <p>This class and its nested classes are not intended for public subclassing.
 * @stable ICU 49
 * @author Markus Scherer
 */
public final class MessagePatternUtil {

    // Private constructor preventing object instantiation
    private MessagePatternUtil() {
    }

    /**
     * Factory method, builds and returns a MessageNode from a MessageFormat pattern string.
     * @param patternString a MessageFormat pattern string
     * @return a MessageNode or a ComplexArgStyleNode
     * @throws IllegalArgumentException if the MessagePattern is empty
     *         or does not represent a MessageFormat pattern
     * @stable ICU 49
     */
    public static MessageNode buildMessageNode(String patternString) {
        return buildMessageNode(new MessagePattern(patternString));
    }

    /**
     * Factory method, builds and returns a MessageNode from a MessagePattern.
     * @param pattern a parsed MessageFormat pattern string
     * @return a MessageNode or a ComplexArgStyleNode
     * @throws IllegalArgumentException if the MessagePattern is empty
     *         or does not represent a MessageFormat pattern
     * @stable ICU 49
     */
    public static MessageNode buildMessageNode(MessagePattern pattern) {
        int limit = pattern.countParts() - 1;
        if (limit < 0) {
            throw new IllegalArgumentException("The MessagePattern is empty");
        } else if (pattern.getPartType(0) != MessagePattern.Part.Type.MSG_START) {
            throw new IllegalArgumentException(
            "The MessagePattern does not represent a MessageFormat pattern");
        }
        return buildMessageNode(pattern, 0, limit);
    }

    /**
     * Common base class for all elements in a tree of nodes
     * returned by {@link MessagePatternUtil#buildMessageNode(MessagePattern)}.
     * This class and all subclasses are immutable and thread-safe.
     * @stable ICU 49
     */
    public static class Node {
        private Node() {}
    }

    /**
     * A Node representing a parsed MessageFormat pattern string.
     * @stable ICU 49
     */
    public static class MessageNode extends Node {
        /**
         * @return the list of MessageContentsNode nodes that this message contains
         * @stable ICU 49
         */
        public List<MessageContentsNode> getContents() {
            return list;
        }
        /**
         * {@inheritDoc}
         * @stable ICU 49
         */
        @Override
        public String toString() {
            return list.toString();
        }

        private MessageNode() {
            super();
        }
        private void addContentsNode(MessageContentsNode node) {
            if (node instanceof TextNode && !list.isEmpty()) {
                // Coalesce adjacent text nodes.
                MessageContentsNode lastNode = list.get(list.size() - 1);
                if (lastNode instanceof TextNode) {
                    TextNode textNode = (TextNode)lastNode;
                    textNode.text = textNode.text + ((TextNode)node).text;
                    return;
                }
            }
            list.add(node);
        }
        private MessageNode freeze() {
            list = Collections.unmodifiableList(list);
            return this;
        }

        private volatile List<MessageContentsNode> list = new ArrayList<MessageContentsNode>();
    }

    /**
     * A piece of MessageNode contents.
     * Use getType() to determine the type and the actual Node subclass.
     * @stable ICU 49
     */
    public static class MessageContentsNode extends Node {
        /**
         * The type of a piece of MessageNode contents.
         * @stable ICU 49
         */
        public enum Type {
            /**
             * This is a TextNode containing literal text (downcast and call getText()).
             * @stable ICU 49
             */
            TEXT,
            /**
             * This is an ArgNode representing a message argument
             * (downcast and use specific methods).
             * @stable ICU 49
             */
            ARG,
            /**
             * This Node represents a place in a plural argument's variant where
             * the formatted (plural-offset) value is to be put.
             * @stable ICU 49
             */
            REPLACE_NUMBER
        }
        /**
         * Returns the type of this piece of MessageNode contents.
         * @stable ICU 49
         */
        public Type getType() {
            return type;
        }
        /**
         * {@inheritDoc}
         * @stable ICU 49
         */
        @Override
        public String toString() {
            // Note: There is no specific subclass for REPLACE_NUMBER
            // because it would not provide any additional API.
            // Therefore we have a little bit of REPLACE_NUMBER-specific code
            // here in the contents-node base class.
            return "{REPLACE_NUMBER}";
        }

        private MessageContentsNode(Type type) {
            super();
            this.type = type;
        }
        private static MessageContentsNode createReplaceNumberNode() {
            return new MessageContentsNode(Type.REPLACE_NUMBER);
        }

        private Type type;
    }

    /**
     * Literal text, a piece of MessageNode contents.
     * @stable ICU 49
     */
    public static class TextNode extends MessageContentsNode {
        /**
         * @return the literal text at this point in the message
         * @stable ICU 49
         */
        public String getText() {
            return text;
        }
        /**
         * {@inheritDoc}
         * @stable ICU 49
         */
        @Override
        public String toString() {
            return "«" + text + "»";
        }

        private TextNode(String text) {
            super(Type.TEXT);
            this.text = text;
        }

        private String text;
    }

    /**
     * A piece of MessageNode contents representing a message argument and its details.
     * @stable ICU 49
     */
    public static class ArgNode extends MessageContentsNode {
        /**
         * @return the argument type
         * @stable ICU 49
         */
        public MessagePattern.ArgType getArgType() {
            return argType;
        }
        /**
         * @return the argument name string (the decimal-digit string if the argument has a number)
         * @stable ICU 49
         */
        public String getName() {
            return name;
        }
        /**
         * @return the argument number, or -1 if none (for a named argument)
         * @stable ICU 49
         */
        public int getNumber() {
            return number;
        }
        /**
         * @return the argument type string, or null if none was specified
         * @stable ICU 49
         */
        public String getTypeName() {
            return typeName;
        }
        /**
         * @return the simple-argument style string,
         *         or null if no style is specified and for other argument types
         * @stable ICU 49
         */
        public String getSimpleStyle() {
            return style;
        }
        /**
         * @return the complex-argument-style object,
         *         or null if the argument type is NONE_ARG or SIMPLE_ARG
         * @stable ICU 49
         */
        public ComplexArgStyleNode getComplexStyle() {
            return complexStyle;
        }
        /**
         * {@inheritDoc}
         * @stable ICU 49
         */
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append('{').append(name);
            if (argType != MessagePattern.ArgType.NONE) {
                sb.append(',').append(typeName);
                if (argType == MessagePattern.ArgType.SIMPLE) {
                    if (style != null) {
                        sb.append(',').append(style);
                    }
                } else {
                    sb.append(',').append(complexStyle.toString());
                }
            }
            return sb.append('}').toString();
        }

        private ArgNode() {
            super(Type.ARG);
        }
        private static ArgNode createArgNode() {
            return new ArgNode();
        }

        private MessagePattern.ArgType argType;
        private String name;
        private int number = -1;
        private String typeName;
        private String style;
        private ComplexArgStyleNode complexStyle;
    }

    /**
     * A Node representing details of the argument style of a complex argument.
     * (Which is a choice/plural/select argument which selects among nested messages.)
     * @stable ICU 49
     */
    public static class ComplexArgStyleNode extends Node {
        /**
         * @return the argument type (same as getArgType() on the parent ArgNode)
         * @stable ICU 49
         */
        public MessagePattern.ArgType getArgType() {
            return argType;
        }
        /**
         * @return true if this is a plural style with an explicit offset
         * @stable ICU 49
         */
        public boolean hasExplicitOffset() {
            return explicitOffset;
        }
        /**
         * @return the plural offset, or 0 if this is not a plural style or
         *         the offset is explicitly or implicitly 0
         * @stable ICU 49
         */
        public double getOffset() {
            return offset;
        }
        /**
         * @return the list of variants: the nested messages with their selection criteria
         * @stable ICU 49
         */
        public List<VariantNode> getVariants() {
            return list;
        }
        /**
         * Separates the variants by type.
         * Intended for use with plural and select argument styles,
         * not useful for choice argument styles.
         *
         * <p>Both parameters are used only for output, and are first cleared.
         * @param numericVariants Variants with numeric-value selectors (if any) are added here.
         *        Can be null for a select argument style.
         * @param keywordVariants Variants with keyword selectors, except "other", are added here.
         *        For a plural argument, if this list is empty after the call, then
         *        all variants except "other" have explicit values
         *        and PluralRules need not be called.
         * @return the "other" variant (the first one if there are several),
         *         null if none (choice style)
         * @stable ICU 49
         */
        public VariantNode getVariantsByType(List<VariantNode> numericVariants,
                                             List<VariantNode> keywordVariants) {
            if (numericVariants != null) {
                numericVariants.clear();
            }
            keywordVariants.clear();
            VariantNode other = null;
            for (VariantNode variant : list) {
                if (variant.isSelectorNumeric()) {
                    numericVariants.add(variant);
                } else if ("other".equals(variant.getSelector())) {
                    if (other == null) {
                        // Return the first "other" variant. (MessagePattern allows duplicates.)
                        other = variant;
                    }
                } else {
                    keywordVariants.add(variant);
                }
            }
            return other;
        }
        /**
         * {@inheritDoc}
         * @stable ICU 49
         */
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append('(').append(argType.toString()).append(" style) ");
            if (hasExplicitOffset()) {
                sb.append("offset:").append(offset).append(' ');
            }
            return sb.append(list.toString()).toString();
        }

        private ComplexArgStyleNode(MessagePattern.ArgType argType) {
            super();
            this.argType = argType;
        }
        private void addVariant(VariantNode variant) {
            list.add(variant);
        }
        private ComplexArgStyleNode freeze() {
            list = Collections.unmodifiableList(list);
            return this;
        }

        private MessagePattern.ArgType argType;
        private double offset;
        private boolean explicitOffset;
        private volatile List<VariantNode> list = new ArrayList<VariantNode>();
    }

    /**
     * A Node representing a nested message (nested inside an argument)
     * with its selection criterium.
     * @stable ICU 49
     */
    public static class VariantNode extends Node {
        /**
         * Returns the selector string.
         * For example: A plural/select keyword ("few"), a plural explicit value ("=1"),
         * a choice comparison operator ("#").
         * @return the selector string
         * @stable ICU 49
         */
        public String getSelector() {
            return selector;
        }
        /**
         * @return true for choice variants and for plural explicit values
         * @stable ICU 49
         */
        public boolean isSelectorNumeric() {
            return numericValue != MessagePattern.NO_NUMERIC_VALUE;
        }
        /**
         * @return the selector's numeric value, or NO_NUMERIC_VALUE if !isSelectorNumeric()
         * @stable ICU 49
         */
        public double getSelectorValue() {
            return numericValue;
        }
        /**
         * @return the nested message
         * @stable ICU 49
         */
        public MessageNode getMessage() {
            return msgNode;
        }
        /**
         * {@inheritDoc}
         * @stable ICU 49
         */
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            if (isSelectorNumeric()) {
                sb.append(numericValue).append(" (").append(selector).append(") {");
            } else {
                sb.append(selector).append(" {");
            }
            return sb.append(msgNode.toString()).append('}').toString();
        }

        private VariantNode() {
            super();
        }

        private String selector;
        private double numericValue = MessagePattern.NO_NUMERIC_VALUE;
        private MessageNode msgNode;
    }

    private static MessageNode buildMessageNode(MessagePattern pattern, int start, int limit) {
        int prevPatternIndex = pattern.getPart(start).getLimit();
        MessageNode node = new MessageNode();
        for (int i = start + 1;; ++i) {
            MessagePattern.Part part = pattern.getPart(i);
            int patternIndex = part.getIndex();
            if (prevPatternIndex < patternIndex) {
                node.addContentsNode(
                        new TextNode(pattern.getPatternString().substring(prevPatternIndex,
                                     patternIndex)));
            }
            if (i == limit) {
                break;
            }
            MessagePattern.Part.Type partType = part.getType();
            if (partType == MessagePattern.Part.Type.ARG_START) {
                int argLimit = pattern.getLimitPartIndex(i);
                node.addContentsNode(buildArgNode(pattern, i, argLimit));
                i = argLimit;
                part = pattern.getPart(i);
            } else if (partType == MessagePattern.Part.Type.REPLACE_NUMBER) {
                node.addContentsNode(MessageContentsNode.createReplaceNumberNode());
                // else: ignore SKIP_SYNTAX and INSERT_CHAR parts.
            }
            prevPatternIndex = part.getLimit();
        }
        return node.freeze();
    }

    private static ArgNode buildArgNode(MessagePattern pattern, int start, int limit) {
        ArgNode node = ArgNode.createArgNode();
        MessagePattern.Part part = pattern.getPart(start);
        MessagePattern.ArgType argType = node.argType = part.getArgType();
        part = pattern.getPart(++start);  // ARG_NAME or ARG_NUMBER
        node.name = pattern.getSubstring(part);
        if (part.getType() == MessagePattern.Part.Type.ARG_NUMBER) {
            node.number = part.getValue();
        }
        ++start;
        switch(argType) {
        case SIMPLE:
            // ARG_TYPE
            node.typeName = pattern.getSubstring(pattern.getPart(start++));
            if (start < limit) {
                // ARG_STYLE
                node.style = pattern.getSubstring(pattern.getPart(start));
            }
            break;
        case CHOICE:
            node.typeName = "choice";
            node.complexStyle = buildChoiceStyleNode(pattern, start, limit);
            break;
        case PLURAL:
            node.typeName = "plural";
            node.complexStyle = buildPluralStyleNode(pattern, start, limit, argType);
            break;
        case SELECT:
            node.typeName = "select";
            node.complexStyle = buildSelectStyleNode(pattern, start, limit);
            break;
        case SELECTORDINAL:
            node.typeName = "selectordinal";
            node.complexStyle = buildPluralStyleNode(pattern, start, limit, argType);
            break;
        default:
            // NONE type, nothing else to do
            break;
        }
        return node;
    }

    private static ComplexArgStyleNode buildChoiceStyleNode(MessagePattern pattern,
                                                            int start, int limit) {
        ComplexArgStyleNode node = new ComplexArgStyleNode(MessagePattern.ArgType.CHOICE);
        while (start < limit) {
            int valueIndex = start;
            MessagePattern.Part part = pattern.getPart(start);
            double value = pattern.getNumericValue(part);
            start += 2;
            int msgLimit = pattern.getLimitPartIndex(start);
            VariantNode variant = new VariantNode();
            variant.selector = pattern.getSubstring(pattern.getPart(valueIndex + 1));
            variant.numericValue = value;
            variant.msgNode = buildMessageNode(pattern, start, msgLimit);
            node.addVariant(variant);
            start = msgLimit + 1;
        }
        return node.freeze();
    }

    private static ComplexArgStyleNode buildPluralStyleNode(MessagePattern pattern,
                                                            int start, int limit,
                                                            MessagePattern.ArgType argType) {
        ComplexArgStyleNode node = new ComplexArgStyleNode(argType);
        MessagePattern.Part offset = pattern.getPart(start);
        if (offset.getType().hasNumericValue()) {
            node.explicitOffset = true;
            node.offset = pattern.getNumericValue(offset);
            ++start;
        }
        while (start < limit) {
            MessagePattern.Part selector = pattern.getPart(start++);
            double value = MessagePattern.NO_NUMERIC_VALUE;
            MessagePattern.Part part = pattern.getPart(start);
            if (part.getType().hasNumericValue()) {
                value = pattern.getNumericValue(part);
                ++start;
            }
            int msgLimit = pattern.getLimitPartIndex(start);
            VariantNode variant = new VariantNode();
            variant.selector = pattern.getSubstring(selector);
            variant.numericValue = value;
            variant.msgNode = buildMessageNode(pattern, start, msgLimit);
            node.addVariant(variant);
            start = msgLimit + 1;
        }
        return node.freeze();
    }

    private static ComplexArgStyleNode buildSelectStyleNode(MessagePattern pattern,
                                                            int start, int limit) {
        ComplexArgStyleNode node = new ComplexArgStyleNode(MessagePattern.ArgType.SELECT);
        while (start < limit) {
            MessagePattern.Part selector = pattern.getPart(start++);
            int msgLimit = pattern.getLimitPartIndex(start);
            VariantNode variant = new VariantNode();
            variant.selector = pattern.getSubstring(selector);
            variant.msgNode = buildMessageNode(pattern, start, msgLimit);
            node.addVariant(variant);
            start = msgLimit + 1;
        }
        return node.freeze();
    }
}
