/**
 *  Copyright (C) 2002-2012   The FreeCol Team
 *
 *  This file is part of FreeCol.
 *
 *  FreeCol is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  FreeCol is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with FreeCol.  If not, see <http://www.gnu.org/licenses/>.
 */

package net.sf.freecol.common.model;

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

import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.stream.XMLStreamWriter;


/**
 * The <code>StringTemplate</code> represents a non-localized string
 * that can be localized by looking up its value in a message bundle
 * or similar Map. The StringTemplate may contain variables (keys)
 * delimited by the '%' character, such as "%amount%" that will be
 * replaced with a string or a StringTemplate. If the StringTemplate
 * contains replacement values but no keys, then it is considered a
 * "label" StringTemplate, and its value will be used to join the
 * replacement values.
 *
 * @version 1.0
 */
public class StringTemplate extends FreeColObject {

    /**
     * The type of this StringTemplate, either NAME, a proper name
     * that must not be localized (e.g. "George Washington"), or KEY,
     * a string that must be localized (e.g. "model.goods.food.name"),
     * or TEMPLATE, a key with replacements to apply to the localized
     * value of the key, or LABEL, a separator string that will be
     * used to join the replacement values.
     */
    public static enum TemplateType { NAME, KEY, TEMPLATE, LABEL }

    /**
     * The TemplateType of this StringTemplate. Defaults to KEY.
     */
    private TemplateType templateType = TemplateType.KEY;

    /**
     * An alternative key to use if the Id is not contained in the
     * message bundle.
     */
    private String defaultId;

    /**
     * The keys to replace within the string template.
     */
    private List<String> keys;

    /**
     * The values with which to replace the keys in the string template.
     */
    private List<StringTemplate> replacements;



    protected StringTemplate() {
        // empty constructor
    }

    public StringTemplate(String id, StringTemplate template) {
        setId(id);
        this.templateType = template.templateType;
        this.keys = template.keys;
        this.replacements = template.replacements;
    }

    /**
     * Creates a new <code>Template</code> instance.
     *
     * @param template a <code>String</code> value
     * @param templateType a <code>TemplateType</code> value
     */
    protected StringTemplate(String template, TemplateType templateType) {
	setId(template);
        this.templateType = templateType;
        switch (templateType) {
        case TEMPLATE:
            keys = new ArrayList<String>();
        case LABEL:
            replacements = new ArrayList<StringTemplate>();
        }
    }

    /**
     * Get the <code>DefaultId</code> value.
     *
     * @return a <code>String</code> value
     */
    public final String getDefaultId() {
        return defaultId;
    }

    /**
     * Set the <code>DefaultId</code> value.
     *
     * @param newDefaultId The new DefaultId value.
     * @return a <code>StringTemplate</code> value
     */
    public StringTemplate setDefaultId(final String newDefaultId) {
        this.defaultId = newDefaultId;
        return this;
    }

    // Factory methods

    public static StringTemplate name(String value) {
        return new StringTemplate(value, TemplateType.NAME);
    }

    public static StringTemplate key(String value) {
        return new StringTemplate(value, TemplateType.KEY);
    }

    public static StringTemplate template(String value) {
        return new StringTemplate(value, TemplateType.TEMPLATE);
    }

    public static StringTemplate label(String value) {
        return new StringTemplate(value, TemplateType.LABEL);
    }


    /**
     * Get the <code>TemplateType</code> value.
     *
     * @return a <code>TemplateType</code> value
     */
    public final TemplateType getTemplateType() {
        return templateType;
    }

    /**
     * Get the <code>Keys</code> value.
     *
     * @return a <code>List<String></code> value
     */
    public final List<String> getKeys() {
        return keys;
    }

    /**
     * Get the <code>Replacements</code> value.
     *
     * @return a <code>List<StringTemplate></code> value
     */
    public final List<StringTemplate> getReplacements() {
        return replacements;
    }


    /**
     * Return the replacement value for a given key, or null if there
     * is none.
     *
     * @param key a <code>String</code> value
     * @return a <code>String</code> value
     */
    public final StringTemplate getReplacement(String key) {
        for (int index = 0; index < keys.size(); index++) {
            if (key.equals(keys.get(index))) {
                if (replacements.size() > index) {
                    return replacements.get(index);
                } else {
                    return null;
                }
            }
        }
        return null;
    }


    /**
     * Add a new key and replacement to the StringTemplate. This is
     * only possible if the StringTemplate is of type TEMPLATE.
     *
     * @param key a <code>String</code> value
     * @param value a <code>String</code> value
     * @return a <code>StringTemplate</code> value
     */
    public StringTemplate add(String key, String value) {
        if (templateType == TemplateType.TEMPLATE) {
            keys.add(key);
            replacements.add(new StringTemplate(value, TemplateType.KEY));
        } else {
            throw new IllegalArgumentException("Cannot add key-value pair to StringTemplate type "
                                               + templateType.toString());
        }
        return this;
    }

    /**
     * Add a replacement value without a key to the StringTemplate.
     * This is only possible if the StringTemplate is of type LABEL.
     *
     * @param value a <code>String</code> value
     * @return a <code>StringTemplate</code> value
     */
    public StringTemplate add(String value) {
	if (templateType == TemplateType.LABEL) {
	    replacements.add(new StringTemplate(value, TemplateType.KEY));
	} else {
	    throw new IllegalArgumentException("Cannot add a single string to StringTemplate type "
                                               + templateType.toString());
	}
	return this;
    }

    /**
     * Add a new key and replacement to the StringTemplate. The
     * replacement must be a proper name. This is only possible if the
     * StringTemplate is of type TEMPLATE.
     *
     * @param key a <code>String</code> value
     * @param value a <code>String</code> value
     * @return a <code>StringTemplate</code> value
     */
    public StringTemplate addName(String key, String value) {
        if (templateType == TemplateType.TEMPLATE) {
            keys.add(key);
            replacements.add(new StringTemplate(value, TemplateType.NAME));
        } else {
            throw new IllegalArgumentException("Cannot add key-value pair to StringTemplate type "
                                               + templateType.toString());
        }
        return this;
    }

    /**
     * Add a new key and replacement to the StringTemplate. The
     * replacement must be a proper name. This is only possible if the
     * StringTemplate is of type TEMPLATE.
     *
     * @param key a <code>String</code> value
     * @param object a <code>FreeColObject</code> value
     * @return a <code>StringTemplate</code> value
     */
    public StringTemplate addName(String key, FreeColObject object) {
        if (templateType == TemplateType.TEMPLATE) {
            keys.add(key);
            replacements.add(new StringTemplate(object.getId() + ".name", TemplateType.KEY));
        } else {
            throw new IllegalArgumentException("Cannot add key-value pair to StringTemplate type "
                                               + templateType.toString());
        }
        return this;
    }

    /**
     * Add a replacement value without a key to the StringTemplate.
     * The replacement must be a proper name.  This is only possible
     * if the StringTemplate is of type LABEL.
     *
     * @param value a <code>String</code> value
     * @return a <code>StringTemplate</code> value
     */
    public StringTemplate addName(String value) {
	if (templateType == TemplateType.LABEL) {
	    replacements.add(new StringTemplate(value, TemplateType.NAME));
	} else {
	    throw new IllegalArgumentException("Cannot add a single string to StringTemplate type "
                                               + templateType.toString());
	}
	return this;
    }

    /**
     * Add a key and an integer value to replace it to this
     * StringTemplate.
     *
     * @param key a <code>String</code> value
     * @param amount a <code>Number</code> value
     * @return a <code>StringTemplate</code> value
     */
    public StringTemplate addAmount(String key, Number amount) {
        addName(key, amount.toString());
        return this;
    }

    /**
     * Add a key and a StringTemplate to replace it to this
     * StringTemplate.
     *
     * @param key a <code>String</code> value
     * @param template a <code>StringTemplate</code> value
     * @return a <code>StringTemplate</code> value
     */
    public StringTemplate addStringTemplate(String key, StringTemplate template) {
        if (templateType == TemplateType.TEMPLATE) {
            keys.add(key);
            replacements.add(template);
        } else {
            throw new IllegalArgumentException("Cannot add a key-template pair to a StringTemplate type "
                                               + templateType.toString());
        }
	return this;
    }

    /**
     * Add a StringTemplate to this LABEL StringTemplate.
     *
     * @param template a <code>StringTemplate</code> value
     * @return a <code>StringTemplate</code> value
     */
    public StringTemplate addStringTemplate(StringTemplate template) {
        if (templateType == TemplateType.LABEL) {
            replacements.add(template);
        } else {
	    throw new IllegalArgumentException("Cannot add a StringTemplate to StringTemplate type "
                                               + templateType.toString());
        }
	return this;
    }

    public String toString() {
        String result = templateType.toString() + ": ";
        switch (templateType) {
        case LABEL:
            if (replacements == null) {
                result += getId();
            } else {
                for (StringTemplate object : replacements) {
                    result += object + getId();
                }
            }
            break;
        case TEMPLATE:
            result += getId();
            if (defaultId != null) {
                result += " (" + defaultId + ")";
            }
            result += " [";
            for (int index = 0; index < keys.size(); index++) {
                result += "[" + keys.get(index) + ": "
                    + replacements.get(index).toString() + "]";
            }
            result += "]";
            break;
        case KEY:
            result += getId();
            if (defaultId != null) {
                result += " (" + defaultId + ")";
            }
            break;
        case NAME:
        default:
            result += getId();
        }
        return result;
    }


    public boolean equals(Object o) {
        if (o instanceof StringTemplate) {
            StringTemplate t = (StringTemplate) o;
            if (!getId().equals(t.getId()) || templateType != t.templateType) {
                return false;
            }
            if (defaultId == null) {
                if (t.defaultId != null) {
                    return false;
                }
            } else if (t.defaultId == null) {
                return false;
            } else if (!defaultId.equals(t.defaultId)) {
                return false;
            }
            if (templateType == TemplateType.LABEL) {
                if (replacements.size() == t.replacements.size()) {
                    for (int index = 0; index < replacements.size(); index++) {
                        if (!replacements.get(index).equals(t.replacements.get(index))) {
                            return false;
                        }
                    }
                } else {
                    return false;
                }
            } else if (templateType == TemplateType.TEMPLATE) {
                if (keys.size() == t.keys.size()
                    && replacements.size() == t.replacements.size()
                    && keys.size() == replacements.size()) {
                    for (int index = 0; index < replacements.size(); index++) {
                        if (!keys.get(index).equals(t.keys.get(index))
                            || !replacements.get(index).equals(t.replacements.get(index))) {
                            return false;
                        }
                    }
                } else {
                    return false;
                }
            }
            return true;
        } else {
            return false;
        }
    }

    public int hashCode() {
        int result = 17;
        result = result * 31 + getId().hashCode();
        result = result * 31 + templateType.ordinal();
        if (defaultId != null) {
            result = result * 31 + defaultId.hashCode();
        }
        if (templateType == TemplateType.LABEL) {
            for (StringTemplate replacement : replacements) {
                result = result * 31 + replacement.hashCode();
            }
        } else if (templateType == TemplateType.TEMPLATE) {
            for (int index = 0; index < keys.size(); index++) {
                result = result * 31 + keys.get(index).hashCode();
                result = result * 31 + replacements.get(index).hashCode();
            }
        }
        return result;
    }


    /**
     * This method writes an XML-representation of this object to
     * the given stream.
     *
     * @param out The target stream.
     * @throws XMLStreamException if there are any problems writing
     *      to the stream.
     */
    protected void toXMLImpl(XMLStreamWriter out) throws XMLStreamException {
        super.toXML(out, getXMLElementTagName());
    }

    /**
     * Write the attributes of this object to a stream.
     *
     * @param out The target stream.
     * @throws XMLStreamException if there are any problems writing to
     *     the stream.
     */
    @Override
    protected void writeAttributes(XMLStreamWriter out)
        throws XMLStreamException {
        super.writeAttributes(out);

        out.writeAttribute("templateType", templateType.toString());
        if (defaultId != null) {
            out.writeAttribute("defaultId", defaultId);
        }
    }

    /**
     * Write the children of this object to a stream.
     *
     * @param out The target stream.
     * @throws XMLStreamException if there are any problems writing to
     *     the stream.
     */
    @Override
    protected void writeChildren(XMLStreamWriter out)
        throws XMLStreamException {
        super.writeChildren(out);

        if (keys != null) {
            for (String key : keys) {
                out.writeStartElement("key");
                out.writeAttribute(VALUE_TAG, key);
                out.writeEndElement();
            }
        }
        if (replacements != null) {
            for (StringTemplate replacement : replacements) {
                replacement.toXMLImpl(out);
            }
        }
    }

    /**
     * Reads the attributes of this object from an XML stream.
     *
     * @param in The XML input stream.
     * @exception XMLStreamException if a problem was encountered
     *     during parsing.
     */
    @Override
    protected void readAttributes(XMLStreamReader in)
        throws XMLStreamException {
        // @compat 0.10.x
        String id = in.getAttributeValue(null, ID_ATTRIBUTE_TAG);
        if (id == null) {
            id = in.getAttributeValue(null, ID_ATTRIBUTE);
        }
        setId(id);
        String typeString = in.getAttributeValue(null, "templateType");
        if (typeString == null) {
            templateType = TemplateType.TEMPLATE;
        } else {
            templateType = Enum.valueOf(TemplateType.class, typeString);
        }
        // end compatibility code
        defaultId = in.getAttributeValue(null, "defaultId");
        switch (templateType) {
        case TEMPLATE:
            keys = new ArrayList<String>();
        case LABEL:
            replacements = new ArrayList<StringTemplate>();
        }
    }

    /**
     * Reads the children of this object from an XML stream.
     *
     * @param in The XML input stream.
     * @exception XMLStreamException if a problem was encountered
     *     during parsing.
     */
    @Override
    protected void readChildren(XMLStreamReader in) throws XMLStreamException {
        while (in.nextTag() != XMLStreamConstants.END_ELEMENT) {
            if ("key".equals(in.getLocalName())) {
                keys.add(in.getAttributeValue(null, VALUE_TAG));
                in.nextTag();
            } else if (getXMLElementTagName().equals(in.getLocalName())) {
                StringTemplate replacement = new StringTemplate();
                replacement.readFromXML(in);
                replacements.add(replacement);
            } else if ("data".equals(in.getLocalName())) { // @compat 0.9.x
                readOldFormat(readFromArrayElement("data", in, new String[0]));
                // end compatibility code
            } else if ("strings".equals(in.getLocalName())) { // @compat 0.9.x
                // TODO: remove compatibility code for HistoryEvent
                readOldFormat(readFromArrayElement("strings", in, new String[0]));
                // end compatibility code
            }
        }
    }

    // @compat 0.9.x
    private void readOldFormat(String[] data) {
        for (int index = 0; index < data.length; index += 2) {
            keys.add(data[index]);
            replacements.add(new StringTemplate(data[index + 1], TemplateType.NAME));
        }
    }
    // end compatibility code

    /**
     * Returns the tag name of the root element representing this object.
     *
     * @return "stringTemplate".
     */
    public static String getXMLElementTagName() {
        return "stringTemplate";
    }
}
