/*
 *
 *  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.compiler.codegen.ISubEmitter;
import org.apache.royale.compiler.codegen.js.IJSEmitter;
import org.apache.royale.compiler.constants.IASLanguageConstants;
import org.apache.royale.compiler.definitions.IClassDefinition;
import org.apache.royale.compiler.definitions.IDefinition;
import org.apache.royale.compiler.definitions.IFunctionDefinition;
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.utils.EmitterUtils;
import org.apache.royale.compiler.internal.projects.RoyaleProject;
import org.apache.royale.compiler.internal.semantics.SemanticUtils;
import org.apache.royale.compiler.internal.tree.as.FunctionCallNode;
import org.apache.royale.compiler.internal.tree.as.IdentifierNode;
import org.apache.royale.compiler.internal.tree.as.LabeledStatementNode;
import org.apache.royale.compiler.scopes.IDefinitionSet;
import org.apache.royale.compiler.tree.ASTNodeID;
import org.apache.royale.compiler.tree.as.*;

public class ForEachEmitter extends JSSubEmitter implements
        ISubEmitter<IForLoopNode>
{

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

    @Override
    public void emit(IForLoopNode node)
    {
        IContainerNode cnode = node.getConditionalsContainerNode();
        IBinaryOperatorNode bnode = (IBinaryOperatorNode) cnode.getChild(0);
        IExpressionNode childNode = bnode.getLeftOperandNode();
        IExpressionNode rnode = bnode.getRightOperandNode();

        final String iterName = getModel().getCurrentForeachName();
        getModel().incForeachLoopCount();
        final String targetName = iterName + "_target";
        
        startMapping(rnode);
        write(ASEmitterTokens.VAR);
        write(ASEmitterTokens.SPACE);
        write(targetName);
        write(ASEmitterTokens.SPACE);
        write(ASEmitterTokens.EQUAL);
        write(ASEmitterTokens.SPACE);
        endMapping(rnode);
        IASNode obj = bnode.getChild(1);
        getWalker().walk(obj);
        startMapping(rnode);
        write(ASEmitterTokens.SEMICOLON);
        endMapping(rnode);
        writeNewline();

        if (node.getParent().getNodeID() == ASTNodeID.BlockID &&
        		node.getParent().getParent().getNodeID() == ASTNodeID.LabledStatementID)
        {
        	// emit label here
        	LabeledStatementNode labelNode = (LabeledStatementNode)node.getParent().getParent();
            writeToken(labelNode.getLabel());
            writeToken(ASEmitterTokens.COLON);

        }

        startMapping(node);
        write(ASEmitterTokens.FOR);
        write(ASEmitterTokens.SPACE);
        write(ASEmitterTokens.PAREN_OPEN);
        endMapping(node);
        startMapping(rnode);
        write(ASEmitterTokens.VAR);
        write(ASEmitterTokens.SPACE);
        write(iterName);
        endMapping(rnode);
        startMapping(bnode, childNode);
        write(ASEmitterTokens.SPACE);
        write(ASEmitterTokens.IN);
        write(ASEmitterTokens.SPACE);
        endMapping(bnode);
        startMapping(rnode);
        write(targetName);
        boolean isXML = false;
        boolean isProxy = false;
        if (obj.getNodeID() == ASTNodeID.IdentifierID)
        {
        	if (((JSRoyaleEmitter)getEmitter()).isXMLish((IdentifierNode)obj))
        	{
        		write(".elementNames()");
        		isXML = true;
        	}
            if (((JSRoyaleEmitter)getEmitter()).isProxy((IdentifierNode)obj))
            {
                emitNullSafeProxyTarget(targetName);
                isProxy = true;
            }
        }
        else if (obj.getNodeID() == ASTNodeID.Op_DescendantsID)
        {
            //it should always be XMLList... but check anyway
            if (((JSRoyaleEmitter)getEmitter()).isXMLList((IMemberAccessExpressionNode)obj))
            {
                write(".elementNames()");
                isXML = true;
            }
        }
        else if (obj.getNodeID() == ASTNodeID.MemberAccessExpressionID)
        {
            if (((JSRoyaleEmitter)getEmitter()).isXMLList((IMemberAccessExpressionNode)obj))
            {
                write(".elementNames()");
                isXML = true;
            }
            if (((JSRoyaleEmitter)getEmitter()).isProxy((IMemberAccessExpressionNode)obj))
            {
                emitNullSafeProxyTarget(targetName);
                isProxy = true;
            }
        }
        else if (obj.getNodeID() == ASTNodeID.Op_AsID)
        {
        	IASNode asChild = obj.getChild(1);        	
        	if (asChild.getNodeID() == ASTNodeID.IdentifierID)
        	{
        		String asName = ((IdentifierNode)asChild).getName();
        		if (asName.equals(IASLanguageConstants.XML) || asName.equals(IASLanguageConstants.XMLList))
        		{
                    write(".elementNames()");
                    isXML = true;
        		} else {
                    IClassDefinition asTarget = (IClassDefinition) ((IdentifierNode) asChild).resolve(getProject());
                    if (asTarget != null) {
                        //check ancestry for proxy
                        RoyaleProject project = (RoyaleProject) getProject();
                        if (asTarget.isInstanceOf(project.getProxyBaseClass(), project)){
                            emitNullSafeProxyTarget(targetName);
                            isProxy = true;
                        }
                    }
                }
        	}
        }
        else if (obj.getNodeID() == ASTNodeID.FunctionCallID)
        {
        	FunctionCallNode func = (FunctionCallNode)obj;
        	IExpressionNode funcName = func.getNameNode();
        	if (funcName.getNodeID() == ASTNodeID.IdentifierID)
        	{
        		String asName = ((IdentifierNode)funcName).getName();
        		if (asName.equals(IASLanguageConstants.XML) || asName.equals(IASLanguageConstants.XMLList))
        		{
                    write(".elementNames()");
                    isXML = true;
        		} else {
        		    IDefinition funcNameDef = funcName.resolve(getProject());
                    //if it is IClassDefinition, then it is a type cast, otherwise...
        		    if (funcNameDef instanceof IFunctionDefinition) {
                        //then it is a regular function call, so check the return type, and use that for the IClassDefinition type check:
                        funcNameDef = ((IFunctionDefinition)funcNameDef).resolveReturnType(getProject());
                    }
        		    if (funcNameDef instanceof IClassDefinition) {
        		        //this is a a hard cast
                        //check ancestry for proxy
                        RoyaleProject project = (RoyaleProject) getProject();
                        if (((IClassDefinition) funcNameDef).isInstanceOf(project.getProxyBaseClass(), project)){
                            emitNullSafeProxyTarget(targetName);
                            isProxy = true;
                        }
                    }
                }
        	} else if (funcName instanceof IMemberAccessExpressionNode) {
                IFunctionDefinition funcDef = (IFunctionDefinition) ((IMemberAccessExpressionNode) funcName).getRightOperandNode().resolve(getProject());
                if (funcDef == null) {
                    //we need to check the LHS for XMLishness, and then resolve the method name against the determined XMLish definition (XML or XMLList),
                    // and then check its return type once we find the public FunctionDefinition for the method name
                    // (because although it is a member of something XMLish, it may not return something that is also XMLish, such as a QName, a String, a uint, or a Namespace etc)
                    IDefinitionSet matchingDefinitions = null;
                    if (EmitterUtils.isLeftNodeXML(((IMemberAccessExpressionNode) funcName).getLeftOperandNode(), getProject())) {
                        if (((IMemberAccessExpressionNode) funcName).getRightOperandNode().getNodeID() == ASTNodeID.IdentifierID) {
                            matchingDefinitions = getProject().getBuiltinType(IASLanguageConstants.BuiltinType.XML).getContainedScope().getLocalDefinitionSetByName(((IIdentifierNode)((IMemberAccessExpressionNode) funcName).getRightOperandNode()).getName());
                        }
                    } else if (EmitterUtils.isLeftNodeXMLList(((IMemberAccessExpressionNode) funcName).getLeftOperandNode(), getProject())) {
                        if (((IMemberAccessExpressionNode) funcName).getRightOperandNode().getNodeID() == ASTNodeID.IdentifierID) {
                            matchingDefinitions = getProject().getBuiltinType(IASLanguageConstants.BuiltinType.XMLLIST).getContainedScope().getLocalDefinitionSetByName(((IIdentifierNode)((IMemberAccessExpressionNode) funcName).getRightOperandNode()).getName());
                        }
                    }
                    if (matchingDefinitions != null) {
                        for (int i = 0; i< matchingDefinitions.getSize(); i++) {
                            IDefinition functionDefinition = matchingDefinitions.getDefinition(i);
                            if (functionDefinition instanceof IFunctionDefinition) {
                                if (functionDefinition.isPublic()) {
                                    isXML = SemanticUtils.isXMLish((((IFunctionDefinition) functionDefinition).resolveReturnType(getProject())), getProject());
                                    break;
                                }
                            }
                        }
                    }

                    //@todo should we emit a warning here if wasXMLish (from either of the first 2 checks) && !isXML (from the matchingDefinitions check)?
                    // results will not be consistent in this case.
                    // e.g. looping over a QName or Namespace instance
                    // It is probably rare and ill-advised, but it definitely won't work well in javascript currently for those classes, for example.

                } else {
                    IClassDefinition returnType = (IClassDefinition) funcDef.resolveReturnType(getProject());
                    isXML = SemanticUtils.isXMLish(returnType, getProject());
                    if (!isXML) {
                        //check for Proxy
                        RoyaleProject project = (RoyaleProject) getProject();
                        if (returnType.isInstanceOf(project.getProxyBaseClass(), project)){
                            emitNullSafeProxyTarget(targetName);
                            isProxy = true;
                        }
                    }
                }
                if (isXML) {
                    write(".elementNames()");
                }
            } //@todo what about dynamic access node for function call? e.g. myXML[string_Value_Here]() ... not so easy really, would likely need a runtime helper/wrapper.
        }
        endMapping(rnode);
        startMapping(node, cnode);
        writeToken(ASEmitterTokens.PAREN_CLOSE);
        endMapping(node);
        writeNewline();
        write(ASEmitterTokens.BLOCK_OPEN);
        writeNewline();
        
        if (childNode instanceof IVariableExpressionNode)
        {
            startMapping(childNode);
            write(ASEmitterTokens.VAR);
            write(ASEmitterTokens.SPACE);
            write(((IVariableNode) childNode.getChild(0)).getName()); //it's always a local var
            //putting this in here instead of common code following the 2 blocks to keep sourcemap tests passing
            write(ASEmitterTokens.SPACE);
            write(ASEmitterTokens.EQUAL);
            write(ASEmitterTokens.SPACE);
            endMapping(childNode);
        }
        else { //IdentifierNode
            getWalker().walk(childNode); //we need to walk here, to deal with non-local var identifiers
            startMapping(childNode);
            write(ASEmitterTokens.SPACE);
            write(ASEmitterTokens.EQUAL);
            write(ASEmitterTokens.SPACE);
            endMapping(childNode);
        }
      
        startMapping(rnode);
        write(targetName);
        if (isXML)
        {
        	write("[");
        	write(iterName);
        	write("]");
        }
        else if (isProxy)
        {
            write(".getProperty(");
            write(iterName);
            write(")");
        }
        else
        {
	        write(ASEmitterTokens.SQUARE_OPEN);
	        write(iterName);
	        write(ASEmitterTokens.SQUARE_CLOSE);
        }
        write(ASEmitterTokens.SEMICOLON);
        endMapping(rnode);
        writeNewline();
        getWalker().walk(node.getStatementContentsNode());
        write(ASEmitterTokens.BLOCK_CLOSE);
        writeNewline();
    }

    void emitNullSafeProxyTarget(String targetName) {
        //output the null-safe target expression:
        // for something:Object in targetName && targetName.propertyNames()
        write(ASEmitterTokens.SPACE);
        writeToken(ASEmitterTokens.LOGICAL_AND);
        write(targetName);
        write(".propertyNames()");
    }

    //@todo, check XML also at some point, maybe switch to the following
    /*
    void emitNullSafeXMLishTarget(String targetName) {
        //output the null-safe target expression:
        // for something:Object in targetName && targetName.propertyNames()
        write(ASEmitterTokens.SPACE);
        writeToken(ASEmitterTokens.LOGICAL_AND);
        write(targetName);
        write(".elementNames()");
    }
    */

}
