/*
 *
 *  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.definitions;

import org.apache.royale.abc.ABCConstants;
import org.apache.royale.compiler.common.DependencyType;
import org.apache.royale.compiler.constants.IASKeywordConstants;
import org.apache.royale.compiler.definitions.IClassDefinition;
import org.apache.royale.compiler.definitions.IDefinition;
import org.apache.royale.compiler.definitions.IPackageDefinition;
import org.apache.royale.compiler.definitions.IVariableDefinition;
import org.apache.royale.compiler.definitions.metadata.IMetaTag;
import org.apache.royale.compiler.internal.as.codegen.CodeGeneratorManager;
import org.apache.royale.compiler.internal.as.codegen.ICodeGenerator;
import org.apache.royale.compiler.internal.as.codegen.ICodeGenerator.IConstantValue;
import org.apache.royale.compiler.internal.scopes.ASScope;
import org.apache.royale.compiler.internal.scopes.CatchScope;
import org.apache.royale.compiler.internal.scopes.FunctionScope;
import org.apache.royale.compiler.internal.semantics.SemanticUtils;
import org.apache.royale.compiler.internal.tree.as.LiteralNode;
import org.apache.royale.compiler.internal.tree.as.NodeBase;
import org.apache.royale.compiler.projects.ICompilerProject;
import org.apache.royale.compiler.scopes.IASScope;
import org.apache.royale.compiler.tree.ASTNodeID;
import org.apache.royale.compiler.tree.as.IExpressionNode;
import org.apache.royale.compiler.tree.as.IImportNode;
import org.apache.royale.compiler.tree.as.IScopedNode;
import org.apache.royale.compiler.tree.as.IVariableNode;

import java.util.Collection;

/**
 * Instances of this class represent definitions of ActionScript variables and
 * constants in the symbol table.
 * <p>
 * After a variable definition is in the symbol table, it should always be
 * accessed through the read-only <code>IVariableDefinition</code> interface.
 */
public class VariableDefinition extends DefinitionBase implements IVariableDefinition
{
    /**
     * Constructor.
     * 
     * @param name The name of the variable.
     */
    public VariableDefinition(String name)
    {
        super(name);
    }

    /**
     * Constructor.
     * 
     * @param name The name of the variable.
     * @param initialValue The initial value of the variable.
     */
    public VariableDefinition(String name, Object initialValue)
    {
        this(name);
        this.initValue = initialValue;
    }

    /**
     * The initial value of this VariableDefinition, if it is known this is used
     * for variables that come from an ABC with a value specified in the traits
     * entry.
     */
    protected Object initValue;

    @Override
    public VariableClassification getVariableClassification()
    {
        // Determine whether or not a variable is local
        // by looking at its containing scope.
        //
        // Local variables don't always have a containing
        // function definition ( MXML event specifiers is one
        // example ).
        ASScope containingScope = getContainingASScope();
        if (containingScope instanceof FunctionScope)
            return VariableClassification.LOCAL;
        if (containingScope instanceof CatchScope)
            return VariableClassification.LOCAL;

        IDefinition parent = getParent();

        if (parent instanceof IClassDefinition)
            return VariableClassification.CLASS_MEMBER;
        if (parent instanceof IPackageDefinition)
            return VariableClassification.PACKAGE_MEMBER;
        if (parent == null)
        {
            if (inPackageNamespace())
                return VariableClassification.PACKAGE_MEMBER;

            return VariableClassification.FILE_MEMBER;
        }

        assert false;
        return null;
    }

    // TODO Remove everything below here when Royale has been integrated into Fb and Fc.

    /**
     * Gets the {@link DependencyType} that should be used when resolving the
     * type of this variable definition.
     * <p>
     * This method is intended to be overridden by sub-classes.
     * 
     * @return The {@link DependencyType} that should be used when resolving the
     * type of this variable definition
     */
    protected DependencyType getTypeDependencyType()
    {
        DependencyType dt = DependencyType.EXPRESSION;
        if (getVariableClassification() != VariableClassification.LOCAL)
            dt = DependencyType.SIGNATURE;
        return dt;
    }

    @Override
    public IVariableNode getNode()
    {
        return (IVariableNode)super.getNode();
    }

    /**
     * Get the node that produced this definition if it has not yet been collected.
     * This will return null if the node has been collected, and won't do any work to get the
     * node back (like reparsing the file).
     * @return  The Variable Node that produced this definition, or null if it does not exist
     */
    private IVariableNode getNodeIfExists()
    {
        return (IVariableNode)nodeRef.getNodeIfExists();
    }
    
    @Override
    public IVariableNode getVariableNode()
    {
        return getNode();
    }

    @Override
    public boolean matches(DefinitionBase node)
    {
        boolean matches = super.matches(node);
        if (!matches)
            return false;
        if (node == this)
            return true;
        VariableDefinition vNode = (VariableDefinition)node;

        VariableClassification classification = vNode.getVariableClassification();
        if (classification != getVariableClassification())
            return false;

        // Along with local, file member and parameter, name offsets needs to be compared for class/interface members also.
        // This is required to differentiate members having same name belonging to different class/interface
        // within the same AS file - See FBG-3494 for an example.
        if (classification == VariableClassification.LOCAL || classification == VariableClassification.FILE_MEMBER || classification == VariableClassification.PARAMETER
                || classification == VariableClassification.CLASS_MEMBER || classification == VariableClassification.INTERFACE_MEMBER)
        {
            if (vNode.getNameStart() != getNameStart() || vNode.getNameEnd() != getNameEnd())
            {
                return false;
            }
        }

        return true;
    }

    /**
     * For debugging only. Produces a string such as
     * <code>public var i:int</code>.
     */
    @Override
    protected void buildInnerString(StringBuilder sb)
    {
        sb.append(getNamespaceReferenceAsString());
        sb.append(' ');
        
        if (isStatic())
        {
            sb.append(IASKeywordConstants.STATIC);
            sb.append(' ');
        }

        sb.append(IASKeywordConstants.VAR);
        sb.append(' ');

        sb.append(getBaseName());

        String type = getTypeAsDisplayString();
        if (!type.isEmpty())
        {
            sb.append(':');
            sb.append(type);
        }
    }

    public void setInitializer(IExpressionNode initExpr)
    {
        if( initExpr != null )
        {
            if( initExpr instanceof LiteralNode )
            {
                // Do some quick an dirty evaluation if the initializer is just a literal
                // no point in copying the expression and running it through the burm later
                switch(initExpr.getNodeID() )
                {
                    case LiteralBooleanID:
                        initValue = SemanticUtils.getBooleanContent(initExpr);
                        return;
                    case LiteralDoubleID:
                        initValue = SemanticUtils.getDoubleContent(initExpr);
                        return;
                    case LiteralStringID:
                        initValue = SemanticUtils.getStringLiteralContent(initExpr);
                        return;
                    case LiteralIntegerID:
                        initValue = SemanticUtils.getIntegerContent(initExpr);
                        return;
                    case LiteralNullID:
                        initValue = ABCConstants.NULL_VALUE;
                        return;
                    case LiteralUintID:
                        initValue = SemanticUtils.getUintContent(initExpr);
                        return;
                }
            }
            setHasInit();
            // If we're a local then we can depend on the weak reference back to the original
            // AST - if that AST is ever collected then we shouldn't be able to resolve back
            // to the definition of a local.
            if( getVariableClassification() != VariableClassification.LOCAL )
                this.initializer = initExpr.copyForInitializer( new DefinitionScopedNode() );
        }
    }
    private void setHasInit()
    {
        flags |= FLAG_HAS_INIT;
    }
    
    boolean hasInit()
    {
        return (flags & FLAG_HAS_INIT) != 0;
    }

    /**
     * IScopedNode impmlementation to make the containing scope available to the
     * copied initializer expression
     */
    private class DefinitionScopedNode extends NodeBase implements IScopedNode
    {
        public IASScope getScope ()
        {
            return getContainingASScope();
        }

        public void getAllImports (Collection<String> imports)
        {
        }

        public void getAllImportNodes (Collection<IImportNode> imports)
        {
        }

        public ASTNodeID getNodeID ()
        {
            return ASTNodeID.UnknownID;
        }
    }
    
    /**
     * Try to calculate the initial value for this VariableDefinition.
     * 
     * @param project the project to use to resolve the intializer
     * @return the initial value of this definition, or null if one can't be
     * determined.
     */
    @Override
    public Object resolveInitialValue(ICompilerProject project)
    {
        if (initValue != null)
            return initValue;

        IExpressionNode initExpr = getInitExpression();
        if (initExpr != null)
        {
            ICodeGenerator codeGenerator = CodeGeneratorManager.getCodeGenerator();
            IConstantValue constantValue = codeGenerator.generateConstantValue(initExpr, project);
            if (constantValue != null)
                return constantValue.getValue();
        }

        return null;
    }

    private IExpressionNode initializer;

    protected IExpressionNode getInitExpression()
    {
        IExpressionNode init = null;
        if( hasInit() )
        {
            if( this.getVariableClassification() == VariableClassification.LOCAL )
            {
                IVariableNode n = getNodeIfExists();
                assert n != null : "The AST for a local var should still be in memory!";
                init = n.getAssignedValueNode();
            }
            else
            {
                init = initializer;
            }
        }
        return init;
    }

    @Override
    public boolean isSkinPart()
    {
        return getSkinPart() != null;
    }

    @Override
    public boolean isRequiredSkinPart()
    {
        IMetaTag skinPart = getSkinPart();
        if (skinPart == null)
            return false;

        return isRequiredSkinPart(skinPart);
    }

    /**
     * Tell this definition whether it was declared in a control flow block
     * or not.  Default setting is false.
     *
     * @param b true if this definition was declared in control flow, otherwise false.
     */
    public void setDeclaredInControlFlow(boolean b)
    {
        if (b)
            flags |= FLAG_DECLARED_IN_CONTROL_FLOW;
        else
            flags &= ~FLAG_DECLARED_IN_CONTROL_FLOW;
    }

    /**
     * Was this variable declared inside a control flow construct?
     * <p>
     * This is used in constant-evaluating the value of the variable.
     *
     * @return true if the Definition was declared inside a control-flow block
     */
    public boolean declaredInControlFlow()
    {
        return (flags & FLAG_DECLARED_IN_CONTROL_FLOW) != 0;
    }
}
