/**
 ****************************************************************************
 * Copyright (C) The Apache Software Foundation. All rights reserved.        *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included  with this distribution in *
 * the LICENSE file.                                                         *
 ****************************************************************************
 */
package org.apache.cocoon.transformation;

import org.apache.cocoon.Roles;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.acting.LangSelect;
import org.apache.cocoon.components.parser.Parser;
import org.apache.cocoon.environment.Source;
import org.apache.cocoon.environment.SourceResolver;

import org.apache.avalon.excalibur.pool.Poolable;
import org.apache.avalon.framework.component.ComponentManager;
import org.apache.avalon.framework.component.ComponentException;
import org.apache.avalon.framework.component.Composable;
import org.apache.avalon.framework.component.Component;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.avalon.framework.logger.Loggable;

import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.AttributesImpl;
import org.xml.sax.helpers.DefaultHandler;

import java.io.IOException;

import java.util.Map;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.ArrayList;
import java.util.Locale;

import java.text.MessageFormat;

import java.net.URL;
import java.net.MalformedURLException;

/**
 * I18nTransformer. Cocoon2 port of Infozone groups I18nProcessor.
 * <p>
 * Sitemap configuration:
 * </p>
 * <p>
 * &lt;map:transformer<br>
 *	name="translate"<br>
 *	src="org.apache.cocoon.transformation.I18nTransformer2"/&gt;<br>
 * </p>
 * <p>
 * &lt;map:match pattern="file"&gt;<br>
 *	&lt;map:generate src="file.xml"/&gt;<br>
 * 	&lt;map:transform type="translate"&gt;<br>
 *		&lt;parameter name="default_lang" value="fi"/&gt;<br>
 *		&lt;parameter name="available_lang_1" value="fi"/&gt;<br>
 *		&lt;parameter name="available_lang_2" value="en"/&gt;<br>
 *		&lt;parameter name="available_lang_3" value="sv"/&gt;<br>
 *		&lt;parameter name="src"<br>
 *			value="translations/file_trans.xml"/&gt;<br>
 *	&lt;/map:transform&gt;<br>
 * </p>
 * <p>
 * When user requests .../file?lang=fi<br>
 * transformer substitutes text surrounded &lt;i18n:text&gt; with
 * translations from file_trans.xml.<br>
 * Attributes listed in &lt;i18n:attr&gt; attribute are also translated
 * </p>
 * <p>
 * file.xml:<br>
 * &lt;root xmlns:i18n="http://apache.org/cocoon/i18n/2.0"&gt;<br>
 * 	&lt;elem i18n:attr="title" title="translate_me"&gt;Text&lt;/elem&gt;<br>
 * 	&lt;elem&gt;&lt;i18n:text&gt;Translate me&lt;/i18n:text&gt;&lt;/elem&gt;<br>
 * &lt;/root&gt;
 * </p>
 * <p>
 * file_trans.xml:<br>
 * &lt;translations&gt;<br>
 * 	&lt;entry&gt;&lt;key&gt;Translate me&lt;/key&gt;<br>
 * 		&lt;translation lang="sv"&gt;Översätta mej&lt;/translation&gt;<br>
 * 		&lt;translation lang="fi"&gt;Käännä minut&lt;/translation&gt;<br>
 *	&lt;/entry&gt;<br>
 * &lt;/translations&gt;<br>
 * </p>
 * <p>
 *
 * @todo Caching dictionaries in memory.<br>
 * @todo Date and Number i18n.<br>
 * @todo Multiple dictionary support. <br>
*
 * @author <a href="mailto:kpiroumian@flagship.ru">Konstantin Piroumian</a>
 * @author <a href="mailto:lassi.immonen@valkeus.com">Lassi Immonen</a>
 */
public class I18nTransformer2 extends AbstractTransformer
implements Composable, Poolable {

    protected ComponentManager manager;

    /**
     * The parsed dictionary data.
     */
    public Map dictionary;

    /**
     * The namespace for i18n is "http://apache.org/cocoon/i18n/2.0"
     */
    public final static String I18N_NAMESPACE_URI =
            "http://apache.org/cocoon/i18n/2.0";

    //
    // Dictionary elements and attributes
    //
    public final static String I18N_DICTIONARY_ELEMENT = "dictionary";
    public final static String I18N_ENTRY_ELEMENT = "entry";
    public final static String I18N_KEY_ELEMENT = "key";
    public final static String I18N_TRANSLATION_ELEMENT = "translation";

    //
    // Text elements and attributes
    //
    public final static String I18N_LANG = "lang";
    public final static String I18N_KEY_ATTRIBUTE = "key";
    public final static String I18N_ATTR_ATTRIBUTE = "attr";
    public final static String I18N_TEXT_ELEMENT = "text";
    public final static String I18N_TRANSLATE_ELEMENT = "translate";
    public final static String I18N_PARAM_ELEMENT = "param";
    public final static String I18N_DATE_ELEMENT = "date";
    public final static String I18N_NUMBER_ELEMENT = "number";

    // States of the transformer
    private final static int STATE_OUTSIDE = 0;
    private final static int STATE_INSIDE_TEXT = 1;
    private final static int STATE_INSIDE_PARAM = 2;
    private final static int STATE_INSIDE_TRANSLATE = 3;
//    private final static int STATE_INSIDE_PARAM_TEXT = 4;
    private final static int STATE_INSIDE_TRANSLATE_TEXT = 5;
    private final static int STATE_TRANSLATE_KEY = 6;
    private final static int STATE_TRANSLATE_TEXT_KEY = 7;

    /**
     * Current state of the transformer.
     */
    private int current_state = STATE_OUTSIDE;

    /**
     * Previous state. Used to translate text inside params and translate elements.
     */
     private int prev_state = STATE_OUTSIDE;

    /**
     * The i18n:key attribute is stored for the current element.
     * If no translation found for the key then the character
     * data of element is used as default value.
     */
    private String current_key = null;

    /**
     * Translated text inside the i18n:text element.
     */
    private String translated_text = null;

    /**
     * Translated text, ready for param substitution.
     */
    private String substitute_text = null;

    /**
     * Current parameter value (translated or not)
     */
    private String param_value = null;

    /**
     * @todo Named parameter substitution.
     * i18n:params are stored in a HashMap for named substitutions, <br>
     * name attribute is used as a key.<br>
     * <i>MessageFormat class does not support named params.
     * Some kind of mapping (name to index) must be used.</i>
     */
    private HashMap namedParams = null;

    /**
     * i18n:params are stored for index substitutions.
     */
    private ArrayList indexedParams = new ArrayList();

    /**
     * Message formatter for param substitution.
     */
    private MessageFormat formatter = new MessageFormat("");

    /**
     * Current language id.
     */
    private String lang;

    /**
     * @todo Locale full support.
     * Do we really need it? We can use a combination of
     * language code and country code (en_US) instead. <br>
     * Also, different encodings can be specified: ru_RU_koi8
     */
    private Locale locale;

    public void setLang(String lang) {
        this.lang = lang;
    }

    public void setLocale(Locale locale) {
        this.locale = locale;
    }

    /**
     *  Uses <code>org.apache.cocoon.acting.LangSelect.getLang()</code>
     *  to get language user has selected. First it checks is lang set in
     *  objectModel.
     */
    public void setup(SourceResolver resolver, Map objectModel, String source,
            Parameters parameters)
            throws ProcessingException, SAXException, IOException {

        // Set current language and locale
        String lang = (String)(objectModel.get("lang"));
        if (lang == null) {
            lang = LangSelect.getLang(objectModel, parameters);
        }
        setLang(lang);

        Locale locale = null;
        int ind = lang.indexOf("_");
        if (ind != -1) {
            int lind = lang.lastIndexOf("_");
            if (ind == lind) {
                locale = new Locale(lang.substring(0, ind - 1),
                    lang.substring(ind + 1));
            }
            else {
                locale = new Locale(lang.substring(0, ind - 1),
                    lang.substring(ind + 1, lind - 1),
                    lang.substring(lind + 1));
            }
        }
        else {
            locale = new Locale(lang, "");
        }
        setLocale(locale);
        formatter.setLocale(locale);

        // FIXME (KP)
        // We need another way of specifying dictionaries, e.g.
        // <parameter name="dictionary.en" src="dict.xml" />
        // <parameter name="dictionary.de" src="dict_de.xml" />
        // etc.
        String translations_file = parameters.getParameter("src", null);

        // FIXME (KP)
        // Add multiple dictionary support!
        initialiseDictionary(resolver.resolve(translations_file));
    }


    public void compose(ComponentManager manager) {
        this.manager = manager;
    }

    public void startElement(String uri, String name, String raw,
            Attributes attr) throws SAXException {

        if (I18N_NAMESPACE_URI.equals(uri)) {
            this.getLogger().debug("Starting i18n element: " + name);
            startI18NElement(name, attr);
            return;
        }

        super.startElement(uri, name, raw, translateAttributes(name, attr));
    }


    public void endElement(String uri, String name, String raw)
            throws SAXException {

        if (I18N_NAMESPACE_URI.equals(uri)) {
            endI18NElement(name);
            return;
        }

        super.endElement(uri, name, raw);
    }

    public void characters(char[] ch, int start, int len) throws SAXException {

        if (current_state != STATE_OUTSIDE) {
            i18nCharacters(ch, start, len);
            return;
        }

        super.characters(ch, start, len);

    }

    // My own content handlers

    private void startI18NElement(String name, Attributes attr)
    throws SAXException {
        this.getLogger().debug("Start i18n element: " + name);
        if (I18N_TEXT_ELEMENT.equals(name)) {
            if (current_state != STATE_OUTSIDE
                && current_state != STATE_INSIDE_PARAM
                && current_state != STATE_INSIDE_TRANSLATE) {
                throw new SAXException(this.getClass().getName()
                    + ": nested i18n:text elements are not allowed. Current state: " + current_state);
            }
            prev_state = current_state;
            current_state = STATE_INSIDE_TEXT;
            current_key = attr.getValue(I18N_NAMESPACE_URI, I18N_KEY_ATTRIBUTE);
        }
        else if (I18N_TRANSLATE_ELEMENT.equals(name)) {
            if (current_state != STATE_OUTSIDE) {
//                throw new SAXException(this.getClass().getName()
//                    + ": i18n:translate element must be used "
//                    + "outside of other i18n elements. Current state: " + current_state);
            }
            current_state = STATE_INSIDE_TRANSLATE;
        }
        else if (I18N_PARAM_ELEMENT.equals(name)) {
            if (current_state != STATE_INSIDE_TRANSLATE) {
                throw new SAXException(this.getClass().getName()
                    + ": i18n:param element can be used only inside "
                    + "i18n:translate element. Current state: " + current_state);
            }
            current_state = STATE_INSIDE_PARAM;
        }
    }


    private void endI18NElement(String name) throws SAXException {
        this.getLogger().debug("End i18n element: " + name);
        switch (current_state) {
            case STATE_INSIDE_TEXT: {
                endTextElement();
                break;
            }
            case STATE_INSIDE_TRANSLATE: {
                endTranslateElement();
                break;
            }
            case STATE_INSIDE_PARAM: {
                endParamElement();
                break;
            }
        }
    }


    /*
     */
    private String stripWhitespace(String s) {
        // FIXME (KP) Must be a better way to determine whitespace-only nodes.
        // trim() function does not remove spaces if string does not contain
        // anything else.
//        if (s == null) {
//            return null;
//        }
        String result = (s + "!").trim();
        return result.substring(0, result.length() - 1);
    }

    private void i18nCharacters(char[] ch, int start, int len)
    throws SAXException{

        String text2translate = new String(ch, start, len);
        text2translate = stripWhitespace(text2translate);
        if (text2translate == null || text2translate.length() == 0) {
//            this.getLogger().warn(this.getClass().getName() + ": null i18n text found");
            return;
        }

        this.getLogger().debug("Text 2 translate: '" + text2translate + "'");

        switch (current_state) {
            case STATE_INSIDE_TEXT: {
                if (current_key != null) {
                    translated_text = (String)(dictionary.get(current_key));
                    if (translated_text == null) {
                        translated_text = text2translate;
                    }
                    current_key = null;
                }
                else if (len > 0) {
                    translated_text = (String)(dictionary.get(text2translate));
                }

                break;
            }
            case STATE_INSIDE_TRANSLATE: {
                // Store text for param substitution (do not translate)
                if (len > 0 && substitute_text == null) {
                    substitute_text = text2translate;
                }
                break;
            }
            case STATE_INSIDE_PARAM: {
                // Store translation for param substitution
                if (len > 0 && param_value == null) {
                    param_value = text2translate;
                }
                break;
            }

        }
    }

    private Attributes translateAttributes(String name, Attributes attr)
    throws SAXException {
        if (attr == null) {
            return attr;
        }

        AttributesImpl temp_attr = new AttributesImpl(attr);

        // Translate all attributes from i18n:attr="name1 name2 ..."
        // using their values as keys
        int i18n_attr_index =
            temp_attr.getIndex(I18N_NAMESPACE_URI, I18N_ATTR_ATTRIBUTE);

        if (i18n_attr_index != -1) {

            StringTokenizer st =
                new StringTokenizer(temp_attr.getValue(i18n_attr_index));
            // remove the i18n:attr attribute - we don't need it
            temp_attr.removeAttribute(i18n_attr_index);
            while (st.hasMoreElements()) {
            // translate all listed attributes
                String attr_name = st.nextToken();
                int attr_index = temp_attr.getIndex(attr_name);

                if (attr_index != -1) {
                    String text2translate = temp_attr.getValue(attr_index);
                    String result = (String)(dictionary.get(text2translate));
                    // set the translated value
                    if (result != null) {
                        temp_attr.setValue(attr_index, result);
                    }
                    else {
                        getLogger().warn("translation not found for attribute "
                        + attr_name + " in element: " + name);
                    }
                }
                else {
                    getLogger().warn("i18n attribute '" + attr_name
                        + "' not found in element: " + name);
                }
            }
            return temp_attr;
        }

        return attr;
    }

    private void endTextElement() throws SAXException {
        this.getLogger().debug("End text element, translated_text: " + translated_text);
        switch (prev_state) {
            case STATE_OUTSIDE: {
                // simply translate text (key translation already performed)
                if (translated_text != null) {
                    super.contentHandler.characters(translated_text.toCharArray(),
                        0, translated_text.length());
                }
                else {
                    // Translation not found.
                    super.contentHandler.characters("".toCharArray(),
                        0, 0);
                }
                break;
            }
            case STATE_INSIDE_TRANSLATE: {
                substitute_text = translated_text;
                break;
            }
            case STATE_INSIDE_PARAM: {
                param_value = translated_text;
                break;
            }
        }
        translated_text = null;
        current_state = prev_state;
        prev_state = STATE_OUTSIDE;
    }

    private void endParamElement() {
        this.getLogger().debug("Substitution param: " + param_value);
        indexedParams.add(param_value);
        param_value = null;
        current_state = STATE_INSIDE_TRANSLATE;
    }

    private void endTranslateElement() throws SAXException {

        if (substitute_text == null) {
            return;
        }

        String result;
        if (indexedParams.size() > 0 && substitute_text.length() > 0) {
            this.getLogger().debug("Text for susbtitution: " + substitute_text);
            result = formatter.format(substitute_text, indexedParams.toArray());
            this.getLogger().debug("Result of susbtitution: " + result);
        }
        else {
            result = substitute_text;
        }

        super.contentHandler.characters(result.toCharArray(), 0, result.length());
        indexedParams.clear();
        substitute_text = null;
        current_state = STATE_OUTSIDE;
    }

    /**
     *Gets translations from xml file to dictionary.
     */
    class I18nContentHandler extends DefaultHandler {
        boolean in_entry = false;
        boolean in_key = false;
        boolean in_translation = false;

        String key = null;
        String translation = null;


        public void startElement(String namespace, String name, String raw,
                Attributes attr) throws SAXException {

            if (name.equals(I18N_ENTRY_ELEMENT)) {
                in_entry = true;
            } else {
                if (in_entry) {
                    if (name.equals(I18N_KEY_ELEMENT)) {
                        in_key = true;
                    } else {
                        if (name.equals(I18N_TRANSLATION_ELEMENT)
                                && attr.getValue(I18N_LANG).equals(lang)) {
                            in_translation = true;
                        }
                    }
                }
            }
        }


        public void endElement(String namespace, String name, String raw)
                throws SAXException {

            if (name.equals(I18N_ENTRY_ELEMENT)) {
                if (key != null && translation != null) {
                    dictionary.put(key, translation);
                    key = null;
                    translation = null;
                }
                in_entry = false;
            } else if (name.equals(I18N_KEY_ELEMENT)) {
                in_key = false;
            } else {
                if (name.equals(I18N_TRANSLATION_ELEMENT)) {
                    in_translation = false;
                }
            }

        }


        public void characters(char[] ary, int start, int length)
                throws SAXException {
            if (in_key) {
                key = new String(ary, start, length);

            } else {
                if (in_translation) {
                    translation = new String(ary, start, length);
                }
            }
        }

    }


    /**
     *Loads translations from given URL
     */
    private void initialiseDictionary(Source inputSource)
            throws SAXException, MalformedURLException, IOException {

        Parser parser = null;

        try
        {
            parser = (Parser)(manager.lookup(Roles.PARSER));
            InputSource input = new InputSource(inputSource.getInputStream());

            // How this could be cached?
            this.dictionary = new HashMap();
            I18nContentHandler i18n_handler = new I18nContentHandler();
            parser.setContentHandler(i18n_handler);
            parser.parse(input);
        } catch(SAXException e) {
            getLogger().error("Error in initialiseDictionary", e);
            throw e;
        } catch(MalformedURLException e) {
            getLogger().error("Error in initialiseDictionary", e);
            throw e;
        } catch(IOException e) {
            getLogger().error("Error in initialiseDictionary", e);
            throw e;
        } catch(ComponentException e) {
            getLogger().error("Error in initialiseDictionary", e);
            throw new SAXException("ComponentException in initialiseDictionary");
        } finally {
            if(parser != null) this.manager.release((Component) parser);
        }
    }
}
