/*
 *
 *  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 org.apache.royale.abc.ABCConstants;
import org.apache.royale.abc.semantics.Namespace;
import org.apache.royale.compiler.codegen.ISubEmitter;
import org.apache.royale.compiler.codegen.js.IJSEmitter;
import org.apache.royale.compiler.constants.IASLanguageConstants;
import org.apache.royale.compiler.definitions.*;
import org.apache.royale.compiler.definitions.IFunctionDefinition.FunctionClassification;
import org.apache.royale.compiler.definitions.IVariableDefinition.VariableClassification;
import org.apache.royale.compiler.definitions.references.INamespaceResolvedReference;
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.JSRoyaleEmitter;
import org.apache.royale.compiler.internal.codegen.js.goog.JSGoogEmitterTokens;
import org.apache.royale.compiler.internal.codegen.js.royale.JSRoyaleEmitterTokens;
import org.apache.royale.compiler.internal.codegen.js.utils.EmitterUtils;
import org.apache.royale.compiler.internal.definitions.*;
import org.apache.royale.compiler.internal.projects.RoyaleJSProject;
import org.apache.royale.compiler.internal.tree.as.NonResolvingIdentifierNode;
import org.apache.royale.compiler.tree.ASTNodeID;
import org.apache.royale.compiler.tree.as.*;
import org.apache.royale.compiler.utils.NativeUtils;

public class IdentifierEmitter extends JSSubEmitter implements
        ISubEmitter<IIdentifierNode>
{

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

    @Override
    public void emit(IIdentifierNode node)
    {
    	if (node instanceof NonResolvingIdentifierNode)
    	{
            startMapping(node);
    		write(node.getName());
            endMapping(node);
    		return;
    	}
        IDefinition nodeDef = ((IIdentifierNode) node).resolve(getProject());

        IASNode parentNode = node.getParent();
        ASTNodeID parentNodeId = parentNode.getNodeID();
        IASNode grandparentNode = parentNode.getParent();
  //      ASTNodeID grandparentNodeId = (parentNode != null) ? grandparentNode.getNodeID() : null;

        boolean identifierIsAccessorFunction = nodeDef instanceof AccessorDefinition;
        boolean identifierIsPlainFunction = nodeDef instanceof FunctionDefinition
                && !identifierIsAccessorFunction;
        boolean emitName = true;
        boolean wroteMemberAccess = false;
    	JSRoyaleEmitter fjs = (JSRoyaleEmitter)getEmitter();
        RoyaleJSProject project = (RoyaleJSProject)getWalker().getProject();
    	boolean isCustomNamespace = false;
    	boolean isStatic = nodeDef != null && nodeDef.isStatic();
        if (nodeDef instanceof FunctionDefinition &&
          	  fjs.isCustomNamespace((FunctionDefinition)nodeDef))
              isCustomNamespace = true;

        if (isStatic
                && nodeDef instanceof IConstantDefinition
                && project != null && project.config != null
                && project.config.getInlineConstants())
        {
            IConstantDefinition constDef = (IConstantDefinition) nodeDef;
            Object initialValue = constDef.resolveInitialValue(project);
            if (initialValue != null)
            {
                emitInitialValue(parentNode, initialValue);
                return;
            }
        }
        if (isStatic)
        {
            String sname = nodeDef.getParent().getQualifiedName();
            if (sname.length() > 0)
            {
                if (emitStaticConstants(parentNode, nodeDef))
                {
                    return;
                }
                startMapping(node);
                write(getEmitter().formatQualifiedName(sname));
                if (!isCustomNamespace)
                {
                    write(ASEmitterTokens.MEMBER_ACCESS);
                    wroteMemberAccess = true;
                }
                endMapping(node);
            }
        }
        else if (!NativeUtils.isNative(node.getName()))
        {
            // an instance method not in a function call or member access
            boolean generateClosure = identifierIsPlainFunction && ((FunctionDefinition) nodeDef)
                    .getFunctionClassification() == FunctionClassification.CLASS_MEMBER &&
                    (!(parentNodeId == ASTNodeID.FunctionCallID || parentNodeId == ASTNodeID.MemberAccessExpressionID));
            if (generateClosure)
            {
                getEmitter().emitClosureStart();
            }

            boolean wroteSelf = false;
            if (EmitterUtils.writeThis(getProject(), getModel(), node))
            {
                IFunctionObjectNode functionObjectNode = (IFunctionObjectNode) node
                        .getParent().getAncestorOfType(
                                IFunctionObjectNode.class);

                IFunctionNode functionNode = (IFunctionNode) node
                .getParent().getAncestorOfType(
                        IFunctionNode.class);
                IFunctionDefinition functionDef = null;
                if (functionNode != null)
                	functionDef = functionNode.getDefinition();

                startMapping(node);
                if (functionObjectNode != null)
                {
                    write(JSGoogEmitterTokens.SELF);
                    wroteSelf = true;
                }
                else if (functionNode != null && functionDef.getFunctionClassification() == FunctionClassification.LOCAL)
                {
                    write(JSGoogEmitterTokens.SELF);
                    wroteSelf = true;
                }
                else
                {
                    write(ASEmitterTokens.THIS);
                }

                if (!isCustomNamespace) {
                    write(ASEmitterTokens.MEMBER_ACCESS);
                    wroteMemberAccess = true;
                }
                endMapping(node);
            }
            else if (EmitterUtils.writeE4xFilterNode(getProject(), getModel(), node))
            {
                startMapping(node);
            	write("node.");
                endMapping(node);
            }

            if (generateClosure)
            {
                if (isCustomNamespace)
                {
                	Namespace ns = (Namespace)((INamespaceResolvedReference)((FunctionDefinition)nodeDef).getNamespaceReference()).resolveAETNamespace(getProject());
                	INamespaceDefinition nsDef = ((FunctionDefinition)nodeDef).getNamespaceReference().resolveNamespaceReference(getProject());
        			fjs.formatQualifiedName(nsDef.getQualifiedName()); // register with used names
                	String nsName = ns.getName();
                	write(JSRoyaleEmitter.formatNamespacedProperty(nsName, node.getName(), true));
                }
                else
                {
            		String qname = node.getName();
                	if (nodeDef != null && !isStatic && (!(nodeDef instanceof IParameterDefinition)) && nodeDef.isPrivate() && getProject().getAllowPrivateNameConflicts())
                    {
                		qname = getEmitter().formatPrivateName(nodeDef.getParent().getQualifiedName(), qname);
                    }
            		write(qname);
                }

                writeToken(ASEmitterTokens.COMMA);
                if (wroteSelf)
                    write(JSGoogEmitterTokens.SELF);
                else
                	write(ASEmitterTokens.THIS);
                getEmitter().emitClosureEnd(node, nodeDef);
                emitName = false;
            }
        }
        
        if (emitName)
        {
            boolean accessWithNS = !wroteMemberAccess;
            if (nodeDef != null)
            {
                // this can be optimized but this way lets
                // us breakpoint on the node.getName() to make
                // sure it is ok to always use the short name in an MAE
                String qname = nodeDef.getQualifiedName();
                boolean isPackageOrFileMember = false;
                if (nodeDef instanceof IVariableDefinition)
                {
                    IVariableDefinition variable = (IVariableDefinition) nodeDef;
                    VariableClassification classification = variable.getVariableClassification();
                    if (classification == VariableClassification.PACKAGE_MEMBER ||
                            classification == VariableClassification.FILE_MEMBER)
                    {
                        isPackageOrFileMember = true;
                    }
                    String ns = nodeDef.getNamespaceReference().getBaseName();
                    isCustomNamespace = EmitterUtils.isCustomNamespace(ns);
                    if (isCustomNamespace && parentNode instanceof IMemberAccessExpressionNode
                            && ((IMemberAccessExpressionNode) parentNode).getRightOperandNode() == node) {
                        accessWithNS = true;
                        boolean wasCustomNamespace = false;
                        //duplicate some checks in MemberAccessEmitter to avoid double output of MEMBER_ACCESS
                        if (nodeDef instanceof IAccessorDefinition && ((IMemberAccessExpressionNode) parentNode).getOperator() == IOperatorNode.OperatorType.MEMBER_ACCESS)
                            wasCustomNamespace = fjs.isCustomNamespace((FunctionDefinition)nodeDef);
                        
                        if (!wasCustomNamespace) {
                            //we already wrote member access before walking to this node.
                            accessWithNS = false;
                        }
                    }
                }
                else if (nodeDef instanceof IFunctionDefinition)
                {
                    IFunctionDefinition func = (IFunctionDefinition) nodeDef;
                    FunctionClassification classification = func.getFunctionClassification();
                    if (classification == FunctionClassification.PACKAGE_MEMBER ||
                            classification == FunctionClassification.FILE_MEMBER)
                    {
                        isPackageOrFileMember = true;
                    }
                }
                boolean needsFormattedName = false;
                if (isPackageOrFileMember && parentNodeId == ASTNodeID.MemberAccessExpressionID)
                {
                    IMemberAccessExpressionNode parentMemberAccessNode = (IMemberAccessExpressionNode) parentNode;
                    //if the package or file member isn't on the left side of a
                    //member access expression, it shouldn't be fully qualified
                    needsFormattedName = parentMemberAccessNode.getLeftOperandNode() == node;
                }
                if (parentNodeId == ASTNodeID.MemberAccessExpressionID)
                {
                	if (needsFormattedName)
                	{
                        startMapping(node);
                	    write(getEmitter().formatQualifiedName(qname));
                        endMapping(node);
                	}
                    else if (isCustomNamespace)
                    {
                    	String ns = ((INamespaceResolvedReference)(nodeDef.getNamespaceReference())).resolveAETNamespace(getProject()).getName();
                        if (accessWithNS)
                        {
                            write(ASEmitterTokens.MEMBER_ACCESS);
                        }
                        startMapping(node, node.getName());
                    	write(JSRoyaleEmitter.formatNamespacedProperty(ns, qname, false));
                        endMapping(node);
                    }
                	else
                	{
                        String originalQname = null;
                	    if (!(nodeDef.getParent() instanceof IPackageDefinition))
                        {
                            qname = node.getName();
                            if (nodeDef != null && !isStatic && (nodeDef.getParent() instanceof ClassDefinition) && (!(nodeDef instanceof IParameterDefinition)) && nodeDef.isPrivate() && getProject().getAllowPrivateNameConflicts())
                            {
                                originalQname = qname;
                                qname = getEmitter().formatPrivateName(nodeDef.getParent().getQualifiedName(), qname);
                            }
                        }
                        startMapping(node, originalQname);
                    	write(qname);
                        endMapping(node);
                	}
                }
                else if (isPackageOrFileMember)
                {
                    startMapping(node);
                    write(getEmitter().formatQualifiedName(qname));
                    endMapping(node);
                }
                else if (nodeDef instanceof TypeDefinitionBase)
                {
                    if (NativeUtils.isSyntheticJSType(qname) && !(parentNode instanceof IFunctionCallNode))
                    {
                        getEmitter().getModel().needLanguage = true;
                        write(JSRoyaleEmitterTokens.SYNTH_TYPE);
                        write(ASEmitterTokens.PAREN_OPEN);
                        write(ASEmitterTokens.SINGLE_QUOTE);
                        startMapping(node);
                        write(getEmitter().formatQualifiedName(qname));
                        endMapping(node);
                        write(ASEmitterTokens.SINGLE_QUOTE);
                        write(ASEmitterTokens.PAREN_CLOSE);
                    }
                    else
                    {
                        startMapping(node);
                        write(getEmitter().formatQualifiedName(qname));
                        endMapping(node);
                    }
                }
                else if (isCustomNamespace)
                {
                	String ns = ((INamespaceResolvedReference)nodeDef.getNamespaceReference()).resolveAETNamespace(getProject()).getName();
                    if (accessWithNS)
                    {
                        write(ASEmitterTokens.MEMBER_ACCESS);
                    }
                    startMapping(node, node.getName());
                	write(JSRoyaleEmitter.formatNamespacedProperty(ns, qname, false));
                    endMapping(node);
                }
                else
                {
                    String originalSymbolName = null;
                	if (nodeDef != null && !isStatic && (nodeDef.getParent() instanceof ClassDefinition) && (!(nodeDef instanceof IParameterDefinition)) && nodeDef.isPrivate() && getProject().getAllowPrivateNameConflicts())
                    {
                        qname = getEmitter().formatPrivateName(nodeDef.getParent().getQualifiedName(), qname);
                        originalSymbolName = node.getName();
                    }
                    startMapping(node, originalSymbolName, node.getLine(), node.getColumn());
                    write(qname);
                    endMapping(node);
                }
            }
            else if (getModel().inE4xFilter && EmitterUtils.writeE4xFilterNode(getProject(), getModel(), node) /* swapped this out to allow for deeper nesting inside the filter expression ... instead of original:grandparentNodeId == ASTNodeID.E4XFilterID*/
            		&& (!(parentNodeId == ASTNodeID.MemberAccessExpressionID
                    || parentNodeId == ASTNodeID.Op_DescendantsID
                    || parentNodeId == ASTNodeID.FunctionCallID)))
            {
                startMapping(node);
                write("child('");
                write(node.getName());
                write("')");
                endMapping(node);
            }
            else
            {
                startMapping(node);
                write(node.getName());
                endMapping(node);
            }
        }
    }
    
    private void emitInitialValue(IASNode parentNode, Object initialValue) {
        startMapping(parentNode);
        if(initialValue instanceof String)
        {
            write(ASEmitterTokens.DOUBLE_QUOTE);
            write((String) initialValue);
            write(ASEmitterTokens.DOUBLE_QUOTE);
        }
        else if(initialValue == ABCConstants.UNDEFINED_VALUE)
        {
            write(IASLanguageConstants.UNDEFINED);
        }
        else if(initialValue == ABCConstants.NULL_VALUE)
        {
            write(IASLanguageConstants.NULL);
        }
        else
        {
            write(initialValue.toString());
        }
        endMapping(parentNode);
    }
    
    private boolean emitStaticConstants(IASNode parentNode, IDefinition nodeDef)
    {
        String sname = nodeDef.getParent().getQualifiedName();
        if (sname.equals("Array"))
        {
            String baseName = nodeDef.getBaseName();
            if (emitArrayConstant(parentNode, baseName))
            {
                return true;
            }
        }
        else if (sname.equals("int"))
        {
            String baseName = nodeDef.getBaseName();
            if (emitIntConstant(parentNode, baseName))
            {
                return true;
            }
        }
        else if (sname.equals("uint"))
        {
            String baseName = nodeDef.getBaseName();
            if (emitUintConstant(parentNode, baseName))
            {
                return true;
            }
        }
        return false;
    }

    private boolean emitArrayConstant(IASNode parentNode, String baseName) {
        if (baseName.equals("CASEINSENSITIVE"))
        {
            startMapping(parentNode);
            write("1");
            endMapping(parentNode);
            return true;
        }
        else if (baseName.equals("DESCENDING"))
        {
            startMapping(parentNode);
            write("2");
            endMapping(parentNode);
            return true;
        }
        else if (baseName.equals("UNIQUESORT"))
        {
            startMapping(parentNode);
            write("4");
            endMapping(parentNode);
            return true;
        }
        else if (baseName.equals("RETURNINDEXEDARRAY"))
        {
            startMapping(parentNode);
            write("8");
            endMapping(parentNode);
            return true;
        }
        else if (baseName.equals("NUMERIC"))
        {
            startMapping(parentNode);
            write("16");
            endMapping(parentNode);
            return true;
        }
        return false;
    }

    private boolean emitIntConstant(IASNode parentNode, String baseName)
    {
        if (baseName.equals("MAX_VALUE"))
        {
            startMapping(parentNode);
            write("2147483647");
            endMapping(parentNode);
            return true;
        }
        else if (baseName.equals("MIN_VALUE"))
        {
            startMapping(parentNode);
            write("-2147483648");
            endMapping(parentNode);
            return true;
        }
        return false;
    }

    private boolean emitUintConstant(IASNode parentNode, String baseName)
    {
        if (baseName.equals("MAX_VALUE"))
        {
            startMapping(parentNode);
            write("4294967295");
            endMapping(parentNode);
            return true;
        }
        else if (baseName.equals("MIN_VALUE"))
        {
            startMapping(parentNode);
            write("0");
            endMapping(parentNode);
            return true;
        }
        return false;
    }
}
