/*
 *
 *  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.royale.compiler.internal.codegen.js.jx;

import java.util.Arrays;
import java.util.List;
import java.util.Stack;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.royale.compiler.codegen.ISubEmitter;
import org.apache.royale.compiler.codegen.js.IJSEmitter;
import org.apache.royale.compiler.common.IMetaInfo;
import org.apache.royale.compiler.internal.codegen.as.ASEmitterTokens;
import org.apache.royale.compiler.internal.codegen.js.JSSubEmitter;
import org.apache.royale.compiler.internal.codegen.js.royale.JSRoyaleEmitterTokens;
import org.apache.royale.compiler.internal.tree.as.LiteralNode;
import org.apache.royale.compiler.internal.tree.as.RegExpLiteralNode;
import org.apache.royale.compiler.internal.tree.as.XMLLiteralNode;
import org.apache.royale.compiler.tree.as.IASNode;
import org.apache.royale.compiler.tree.as.IFunctionNode;
import org.apache.royale.compiler.tree.as.ILiteralNode;
import org.apache.royale.compiler.tree.as.ILiteralNode.LiteralType;
import org.apache.royale.utils.JSXUtil;

public class LiteralEmitter extends JSSubEmitter implements
        ISubEmitter<ILiteralNode>
{
    private final List<String> JSX_ATTRIBUTES_NO_RENAME = Arrays.asList("ref", "key");

    public LiteralEmitter(IJSEmitter emitter)
    {
        super(emitter);
    }

    @Override
    public void emit(ILiteralNode node)
    {
        boolean isWritten = false;

        String newlineReplacement = "\\\\n";
        String s = node.getValue(true);
        if (!(node instanceof RegExpLiteralNode))
        {
            if (node.getLiteralType() == LiteralType.XML)
            {
                boolean jsx = false;
                IFunctionNode functionNode = (IFunctionNode) node
                        .getAncestorOfType(IFunctionNode.class);
                if (functionNode != null)
                {
                    IMetaInfo[] metaInfos = functionNode.getMetaInfos();
                    for (IMetaInfo metaInfo : metaInfos)
                    {
                        if (metaInfo.getTagName().equals(JSRoyaleEmitterTokens.JSX.getToken()))
                        {
                            jsx = true;
                            break;
                        }
                    }
                }
                XMLLiteralNode xmlNode = (XMLLiteralNode) node;
                if (jsx)
                {
                    emitJSX(xmlNode);
                    return;
                }
                else
                {
                    newlineReplacement = "\\\\\n";
                    if (xmlNode.getContentsNode().getChildCount() == 1)
                    {
                        if (s.contains("'"))
                            s = "\"" + s + "\"";
                        else
                            s = "'" + s + "'";
                    }
                    else
                    {
                        StringBuilder sb = new StringBuilder();
                        // probably contains {initializers}
                        boolean inAttribute = false;
                        int n = xmlNode.getContentsNode().getChildCount();
                        for (int i = 0; i < n; i++)
                        {
                            if (i > 0)
                                sb.append(" + ");
                            IASNode child = xmlNode.getContentsNode().getChild(i);
                            if (child instanceof LiteralNode)
                            {
                                s = ((LiteralNode)child).getValue(true);
                                if (s.contains("'"))
                                    sb.append("\"" + s + "\"");
                                else
                                    sb.append("'" + s + "'");
                            }
                            else
                            {
                                s = getEmitter().stringifyNode(child);
                                if (inAttribute)
                                {
                                    sb.append("'\"' + ");

                                    sb.append(s);

                                    sb.append(" + '\"'");
                                }
                                else
                                    sb.append(s);
                            }
                            inAttribute = s.endsWith("=");
                        }
                        s = sb.toString();
                    }
                    char c = s.charAt(0);
                    if (c == '"')
                    {
                        s = s.substring(1, s.length() - 1);
                        s = s.replace("\"", "__QUOTE_PLACEHOLDER__");
                        s = "\"" + s + "\"";
                    }
                    // use formatQualifiedName to get XML in the usedNames dependencies
                    s = "new " + getEmitter().formatQualifiedName("XML") + "( " + s + ")";
                }
            }
            s = s.replaceAll("\n", "__NEWLINE_PLACEHOLDER__");
            s = s.replaceAll("\r", "__CR_PLACEHOLDER__");
            s = s.replaceAll("\t", "__TAB_PLACEHOLDER__");
            s = s.replaceAll("\f", "__FORMFEED_PLACEHOLDER__");
            s = s.replaceAll("\b", "__BACKSPACE_PLACEHOLDER__");
            s = s.replaceAll("\\\\", "__ESCAPE_PLACEHOLDER__");
            s = s.replaceAll("\\\\\"", "__QUOTE_PLACEHOLDER__");
            //s = "\'" + s.replaceAll("\'", "\\\\\'") + "\'";
            s = s.replaceAll("__QUOTE_PLACEHOLDER__", "\\\\\"");
            s = s.replaceAll("__ESCAPE_PLACEHOLDER__", "\\\\\\\\");
            s = s.replaceAll("__BACKSPACE_PLACEHOLDER__", "\\\\b");
            s = s.replaceAll("__FORMFEED_PLACEHOLDER__", "\\\\f");
            s = s.replaceAll("__TAB_PLACEHOLDER__", "\\\\t");
            s = s.replaceAll("__CR_PLACEHOLDER__", "\\\\r");
            s = s.replaceAll("__NEWLINE_PLACEHOLDER__", newlineReplacement);
            if (node.getLiteralType() == LiteralType.STRING)
            {
                char c = s.charAt(0);
                if (c == '"')
                {
                    s = s.substring(1, s.length() - 1);
                    s = s.replace("\"", "\\\"");
                    s = "\"" + s + "\"";
                }
                else if (c == '\'')
                {
                    s = s.substring(1, s.length() - 1);
                    s = s.replace("'", "\\'");
                    s = "'" + s + "'";
                }
                s = s.replace("\u00A0", "\\u00A0");
                s = s.replace("\u2028", "\\u2028");
                s = s.replace("\u2029", "\\u2029");
                s = s.replace("\u3000", "\\u3000");
            }

        }
        else
        {
            s = s.replaceAll("\\\\\\\\", "__ESCAPE__ESCAPE__PLACEHOLDER__");
            s = s.replaceAll("\\\\/", "__SLASH_PLACEHOLDER__");
            s = s.replaceAll("\\\\([^u])", "__ESCAPE_PLACEHOLDER__$1");
            s = s.replaceAll("\"", "__DOUBLE_QUOTE_PLACEHOLDER__");
            //s = s.replaceAll("\\\\s", "__S_PLACEHOLDER__");
            //s = s.replaceAll("\\\\w", "__W_PLACEHOLDER__");
            //s = s.replaceAll("\\\\\\.", "__DOT_PLACEHOLDER__");
            s = StringEscapeUtils.escapeJava(s);
            //s = s.replaceAll("__S_PLACEHOLDER__", "\\\\s");
            //s = s.replaceAll("__W_PLACEHOLDER__", "\\\\w");
            //s = s.replaceAll("__DOT_PLACEHOLDER__", "\\\\.");
            s = s.replaceAll("__DOUBLE_QUOTE_PLACEHOLDER__", "\"");
            s = s.replaceAll("__ESCAPE_PLACEHOLDER__", "\\\\");
            s = s.replaceAll("__SLASH_PLACEHOLDER__", "\\\\/");
            s = s.replaceAll("__ESCAPE__ESCAPE__PLACEHOLDER__", "\\\\\\\\");
            s = s.replaceAll("\\\\n", "\\\\u000a");
            s = s.replaceAll("\\\\r", "\\\\u000d");
            s = s.replaceAll("\\\\t", "\\\\u0009");        	
            s = s.replaceAll(" ", "\\\\u0020");
        }

        if (!isWritten)
        {
            startMapping(node);
            write(s);
            endMapping(node);
        }
    }

    private void emitJSX(XMLLiteralNode node)
    {
        int childCount = node.getContentsNode().getChildCount();
        Stack<String> elementStack = new Stack<String>();
        String elementName = null;
        boolean endsWithAttribute = false;
        boolean afterOpenTag = false;
        for (int i = 0; i < childCount; i++)
        {
            IASNode child = node.getContentsNode().getChild(i);
            if (child instanceof ILiteralNode)
            {
                ILiteralNode literalChild = (ILiteralNode) child;
                if (literalChild.getLiteralType() != LiteralType.XML)
                {
                    //inside {} syntax. emit normally.
                    if (afterOpenTag)
                    {
                        writeToken(ASEmitterTokens.COMMA);
                    }
                    getEmitter().getWalker().walk(literalChild);
                    continue;
                }
                String value = literalChild.getValue(true);
                while (value.length() > 0)
                {
                    int nextTagStartIndex = value.indexOf("<");
                    int nextTagEndIndex = value.indexOf(">");
                    boolean selfClosing = false;
                    boolean startsWithAttribute = false;
                    if (nextTagEndIndex > 0
                            && value.charAt(nextTagEndIndex - 1) == '/')
                    {
                        selfClosing = true;
                    }
                    if (endsWithAttribute)
                    {
                        //we'll fall back into attribute parsing below
                        endsWithAttribute = false;
                        startsWithAttribute = true;
                        elementName = elementStack.peek();
                    }
                    else if (nextTagStartIndex == 0)
                    {
                        //assume that the name ends at the end of the open tag
                        int endNameIndex = nextTagEndIndex;
                        if (endNameIndex == -1)
                        {
                            //literal ends with an attribute that uses {} syntax
                            endNameIndex = value.length() - 1;
                        }
                        int attributeIndex = -1;
                        for (int j = 0, count = value.length(); j < count; j++)
                        {
                            int charAt = value.charAt(j);
                            if (charAt == ' ' || charAt == '\t'
                                    || charAt == '\r' || charAt == '\n')
                            {
                                attributeIndex = j;
                                break;
                            }
                        }
                        if (attributeIndex > 0 && attributeIndex < endNameIndex)
                        {
                            //if there are attributes, the name does not end at
                            //the end of the open tag
                            endNameIndex = attributeIndex;
                        }
                        elementName = value.substring(1, endNameIndex);
                        if (elementName.endsWith("/"))
                        {
                            elementName = elementName.substring(0, elementName.length() - 1);
                        }
                        if (elementName.startsWith("/"))
                        {
                            //the close tag of the current element
                            elementName = elementName.substring(1);
                            elementName = getElementNameToEmit(elementName, node);
                            if (elementStack.size() > 0)
                            {
                                indentPop();
                            }
                            write(ASEmitterTokens.PAREN_CLOSE);
                            String topOfStack = elementStack.pop();
                            assert topOfStack.equals(elementName);
                            value = value.substring(nextTagEndIndex + 1);
                            continue;
                        }
                        else
                        {
                            //the open tag of a new element
                            if (elementStack.size() > 0)
                            {
                                indentPush();
                                writeNewline(ASEmitterTokens.COMMA);
                            }
                            elementName = getElementNameToEmit(elementName, node);
                            elementStack.push(elementName);
                            write("React.createElement");
                            write(ASEmitterTokens.PAREN_OPEN);
                            write(elementName);
                            value = value.substring(endNameIndex);
                            value = value.replaceAll("^\\s+", "");
                            //we changed the string, so find it again
                            nextTagEndIndex = value.indexOf(">");
                            afterOpenTag = false;
                        }
                    }
                    else
                    {
                        //we're inside an element's open and closing tags
                        if (nextTagStartIndex == -1)
                        {
                            //literal ends with an attribute that uses {} syntax
                            nextTagStartIndex = value.length();
                        }
                        String elementText = value.substring(0, nextTagStartIndex);
                        int oldLength = elementText.length();
                        //remove carriage return completely
                        elementText = elementText.replaceAll("\r", "");
                        //remove whitespace after new line
                        elementText = elementText.replaceAll("\n\\s+", "\n");
                        //remove whitespace before new line
                        elementText = elementText.replaceAll("\\s+\n", "\n");
                        //remove new lines
                        elementText = elementText.replaceAll("\n", "");
                        //if we've removed new lines, do not end with whitespace
                        //unless it's right before the start of {}
                        if (oldLength != elementText.length()
                                && nextTagStartIndex != value.length())
                        {
                            elementText = elementText.replaceAll("\\s+$", "");
                        }
                        //if there's nothing but whitespace between tags, ignore
                        //it, unless it's right before the start of {}
                        if (!elementText.matches("^\\s*$")
                                || nextTagStartIndex == value.length())
                        {
                            writeToken(ASEmitterTokens.COMMA);
                            emitJSXText(elementText);
                        }
                        value = value.substring(nextTagStartIndex);
                        continue;
                    }
                    //parse the tag's attributes
                    if (nextTagEndIndex == -1)
                    {
                        //literal ends with an attribute that uses {} syntax
                        endsWithAttribute = true;
                        nextTagEndIndex = value.length() - 1;
                    }
                    int attributesEndIndex = (selfClosing && nextTagEndIndex > 0) ? nextTagEndIndex - 1 : nextTagEndIndex;
                    String attributes = value.substring(0, attributesEndIndex);
                    emitJSXAttributes(attributes, startsWithAttribute, endsWithAttribute, elementStack.peek().startsWith("'"));
                    if (selfClosing)
                    {
                        //end of open tag, including attributes
                        write(ASEmitterTokens.PAREN_CLOSE);
                        if (elementStack.size() > 0)
                        {
                            indentPop();
                        }
                        String topOfStack = elementStack.pop();
                        assert topOfStack.equals(elementName);
                    }
                    value = value.substring(nextTagEndIndex + 1);
                    if (!endsWithAttribute)
                    {
                        //don't trim here because we want to preserve whitespace
                        //inside the open and close tags
                        afterOpenTag = true;
                    }
                }
            }
            else
            {
                if (!endsWithAttribute || afterOpenTag)
                {
                    writeToken(ASEmitterTokens.COMMA);
                }
                //not a literal, and inside {} syntax. emit normally.
                getEmitter().getWalker().walk(child);
            }
        }
    }

    private void emitJSXAttributes(String value, boolean startsWithAttribute, boolean endsWithAttribute, boolean isHTML)
    {
        int attributeCount = 0;
        while (true)
        {
            int charCount = value.length();
            if (charCount == 0)
            {
                break;
            }
            int endAttributeNameIndex = value.indexOf("=");
            if (endAttributeNameIndex == -1)
            {
                endAttributeNameIndex = value.length();
            }
            String attributeName = value.substring(0, endAttributeNameIndex);
            writeToken(ASEmitterTokens.COMMA);
            if (!startsWithAttribute && attributeCount == 0)
            {
                writeToken(ASEmitterTokens.BLOCK_OPEN);
            }
            attributeName = attributeName.trim();
            //if it's HTML, we don't want anything to be renamed by Closure
            //compiler. if the attribute is a special built-in value, like "ref"
            //or "key", we never want it to be renamed.
            if (isHTML || JSX_ATTRIBUTES_NO_RENAME.contains(attributeName))
            {
                emitJSXText(attributeName);
            }
            else
            {
                write(attributeName);
            }
            writeToken(ASEmitterTokens.COLON);
            attributeCount++;
            if ((endAttributeNameIndex + 1) >= charCount)
            {
                //literal ends with an attribute that uses {} syntax
                break;
            }
            int quoteChar = value.charAt(endAttributeNameIndex + 1);
            int startAttributeValueIndex = endAttributeNameIndex + 2;
            if (startAttributeValueIndex > charCount)
            {
                startAttributeValueIndex = charCount;
            }
            int endAttributeValueIndex = value.indexOf(quoteChar, startAttributeValueIndex);
            if (endAttributeValueIndex == -1 || endAttributeValueIndex > charCount)
            {
                endAttributeValueIndex = charCount;
            }
            if (endAttributeValueIndex > 0)
            {
                String attributeValue = value.substring(startAttributeValueIndex, endAttributeValueIndex);
                emitJSXText(attributeValue);
                value = value.substring(endAttributeValueIndex + 1);
                value = value.replaceAll("^\\s+", "");
            }
        }
        if (!endsWithAttribute)
        {
            if (!startsWithAttribute && attributeCount == 0)
            {
                writeToken(ASEmitterTokens.COMMA);
                write(ASEmitterTokens.NULL);
            }
            else
            {
                write(ASEmitterTokens.SPACE);
                write(ASEmitterTokens.BLOCK_CLOSE);
            }
        }
    }

    private void emitJSXText(String elementText)
    {
        write(ASEmitterTokens.SINGLE_QUOTE);
        elementText = elementText.replaceAll("'", "\\\\\'");
        write(elementText);
        write(ASEmitterTokens.SINGLE_QUOTE);
    }

    private String getElementNameToEmit(String elementName, IASNode node)
    {
        String qualifiedTypeName = JSXUtil.getQualifiedTypeForElementName(elementName, node, getProject());
        if (qualifiedTypeName != null)
        {
            return getEmitter().formatQualifiedName(qualifiedTypeName);
        }
        //it's a basic HTML tag
        return ASEmitterTokens.SINGLE_QUOTE.getToken() + elementName + ASEmitterTokens.SINGLE_QUOTE.getToken();
    }
}
