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

import static com.google.common.base.Predicates.and;
import static com.google.common.collect.Collections2.filter;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

import org.apache.royale.compiler.common.ASImportTarget;
import org.apache.royale.compiler.common.IImportTarget;
import org.apache.royale.compiler.config.CompilerDiagnosticsConstants;
import org.apache.royale.compiler.constants.IASLanguageConstants;
import org.apache.royale.compiler.constants.IMetaAttributeConstants;
import org.apache.royale.compiler.constants.INamespaceConstants;
import org.apache.royale.compiler.definitions.IDefinition;
import org.apache.royale.compiler.definitions.INamespaceDefinition;
import org.apache.royale.compiler.definitions.IFunctionDefinition.FunctionClassification;
import org.apache.royale.compiler.definitions.IParameterDefinition;
import org.apache.royale.compiler.definitions.references.IReference;
import org.apache.royale.compiler.definitions.references.ReferenceFactory;
import org.apache.royale.compiler.internal.definitions.FunctionDefinition;
import org.apache.royale.compiler.internal.definitions.NamespaceDefinition;
import org.apache.royale.compiler.internal.definitions.ParameterDefinition;
import org.apache.royale.compiler.internal.definitions.VariableDefinition;
import org.apache.royale.compiler.internal.parsing.as.ASParser;
import org.apache.royale.compiler.internal.parsing.as.ASToken;
import org.apache.royale.compiler.internal.parsing.as.ASTokenTypes;
import org.apache.royale.compiler.internal.parsing.as.ConfigProcessor;
import org.apache.royale.compiler.internal.scopes.ASFileScope;
import org.apache.royale.compiler.internal.scopes.ASScope;
import org.apache.royale.compiler.internal.scopes.ClosureScope;
import org.apache.royale.compiler.internal.scopes.FunctionScope;
import org.apache.royale.compiler.internal.semantics.PostProcessStep;
import org.apache.royale.compiler.internal.tree.as.parts.FunctionContentsPart;
import org.apache.royale.compiler.internal.tree.as.parts.IFunctionContentsPart;
import org.apache.royale.compiler.parsing.IASToken;
import org.apache.royale.compiler.problems.CanNotInsertSemicolonProblem;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.problems.InternalCompilerProblem2;
import org.apache.royale.compiler.scopes.IASScope;
import org.apache.royale.compiler.tree.ASTNodeID;
import org.apache.royale.compiler.tree.as.IASNode;
import org.apache.royale.compiler.tree.as.ICommonClassNode;
import org.apache.royale.compiler.tree.as.IContainerNode;
import org.apache.royale.compiler.tree.as.IDefinitionNode;
import org.apache.royale.compiler.tree.as.IExpressionNode;
import org.apache.royale.compiler.tree.as.IFunctionNode;
import org.apache.royale.compiler.tree.as.INamespaceDecorationNode;
import org.apache.royale.compiler.tree.as.IParameterNode;
import org.apache.royale.compiler.tree.as.IScopedNode;
import org.apache.royale.compiler.tree.as.ITypeNode;
import org.apache.royale.compiler.tree.as.IVariableNode;
import org.apache.royale.compiler.tree.metadata.IMetaTagNode;
import org.apache.royale.compiler.workspaces.IWorkspace;

import com.google.common.base.Predicate;

/**
 * ActionScript parse tree node representing a function definition
 */
public class FunctionNode extends BaseTypedDefinitionNode implements IFunctionNode
{
    /**
     * Constructor.
     * <p>
     * Creates a {@code FunctionNode} from the "new" keyword token and the
     * function name node.
     * 
     * @param functionKeyword function keyword
     * @param nameNode node containing the name of this function
     */
    public FunctionNode(IASToken functionKeyword, IdentifierNode nameNode)
    {
        init(nameNode);
        
        if (functionKeyword != null)
            contentsPart.setKeywordNode(new KeywordNode(functionKeyword));
    }

    /**
     * Constructor.
     * <p>
     * Creates a new FunctionNode with a custom part for this functions
     * contents.
     * 
     * @param node the name of the node
     * @param part the {@link IFunctionContentsPart}
     */
    public FunctionNode(IdentifierNode node, IFunctionContentsPart part)
    {
        super.init(node);
        contentsPart = part;
    }

    /**
     * Contents of the function, including args, etc
     */
    protected IFunctionContentsPart contentsPart;
    
    /**
     * Does this method need a Definition added for "arguments". This will be
     * set to true during scope building if we encounter an IdentifierNode that
     * refers to "arguments".
     */
    boolean needsArguments = false;

    /**
     * The configuration processor used to re-parse the function body.
     */
    private ConfigProcessor configProcessor = null;
    
    /**
     * The open curly token of the function body.
     */
    private ASToken openT = null;
    
    /**
     * True if the function body is a deferred node.
     */
    private boolean isBodyDeferred = false;

    /**
     * Lock used when parsing the function body
     */
    private ReentrantLock deferredBodyParsingLock = new ReentrantLock();

    /**
     * A count of the number of calls to parse the function body.  The body
     * won't actually be thrown away until the count reaches zero again.
     */
    private int deferredBodyParsingReferenceCount = 0;

    /**
     * Cached function body text. If it's empty, the function body has to be
     * reloaded from the file using a seek-able reader, which might be slow for
     * large file.
     */
    private String functionBodyText;


    /**
     * Save the problems until later if we were parsed from somewhere we don't have a problems collection
     */
    private Collection<ICompilerProblem> parseProblems;

    /**
     * Indicates whether we've called rememberLocalFunction() on the parent
     * function yet (if a parent function even exists in the first place) -JT
     */
    private boolean isRemembered = false;
    
    //
    // NodeBase overrides
    //

    @Override
    public ASTNodeID getNodeID()
    {
        return ASTNodeID.FunctionID;
    }
    
    @Override
    public int getSpanningStart()
    {
        return getNodeStartForTooling();
    }

    @Override
    protected void setChildren(boolean fillInOffsets)
    {
        addDecorationChildren(fillInOffsets);
        addChildInOrder(contentsPart.getFunctionKeywordNode(), fillInOffsets);
        addChildInOrder(nameNode, fillInOffsets);
        addChildInOrder(contentsPart.getParametersNode(), fillInOffsets);
        addChildInOrder(typeNode, fillInOffsets);
        addChildInOrder(contentsPart.getContents(), fillInOffsets);
    }
    
    @Override
    public void normalize(boolean fillInOffsets)
    {
        super.normalize(fillInOffsets);
        contentsPart.optimize();
    }

    @Override
    protected void analyze(EnumSet<PostProcessStep> set, ASScope scope, Collection<ICompilerProblem> problems)
    {
        if (!isRemembered)
        {
            //previously, we remembered local functions only during
            //POPULATE_SCOPE. however, in MXML, functions get created multiple
            //times, and the second time around, analyze() is NOT called with
            //POPULATE_SCOPE. this causes our function to be forgotten.
            //better to check if we've remembered or not no matter which steps
            //were passed in. -JT
            final IFunctionNode parentFunctionNode = (IFunctionNode)getAncestorOfType(IFunctionNode.class);
            if (parentFunctionNode != null)
            {
                isRemembered = true;
                parentFunctionNode.rememberLocalFunction(this);
            }
        }

        if (set.contains(PostProcessStep.POPULATE_SCOPE))
        {
            FunctionDefinition definition = buildDefinition();
            setDefinition(definition);

            // if the parent is an anonymous function, then don't add the function definition to the scope
            // we have already added the anonymous function to the scope and this function definition does not
            // belong to the scope
            if (this.getParent() instanceof FunctionObjectNode)
            {
                String funcName = definition.getBaseName();  
                if (funcName.length() == 0)
                    scope.setAsContainingScopeOfAnonymousFunction(definition);
                else 
                {
                    // If the parent is an "anonymous" function with a name, then
                    // the name will have to be visibly within the function body, so it can 
                    // call itself recursively. So make a special closure scope
                    // Add a closure scope below the containing scope
                    ASScope closureScope = new ClosureScope(scope);
                    scope = closureScope;               // now build the function scope below this..
                    scope.addDefinition(definition);    // This sets the containing scope of the def
                }
            }
            else
                scope.addDefinition(definition);    // This sets the containing scope of the def

            ScopedBlockNode contents = contentsPart.getContents();
            if (contents != null)
            {
                ASScope localScope = new FunctionScope(scope, contents);
                definition.setContainedScope(localScope);
                scope = localScope;
            }
        }

        if (set.contains(PostProcessStep.RECONNECT_DEFINITIONS))
        {
            reconnectDef(scope);
            final FunctionDefinition functionDef = this.getDefinition();
            if (functionDef != null)
            {
                scope = functionDef.getContainedScope();
                ScopedBlockNode contents = contentsPart.getContents();
                // scope can be null for generated binding wrappers of
                // getters and setters
                if (contents != null && scope != null)
                {
                    contents.reconnectScope(scope);
                }
            }
        }

        // Recurse on the function parameters.
        ContainerNode parameters = contentsPart.getParametersNode();
        if (parameters != null)
        {
            parameters.analyze(set, scope, problems);
            if (set.contains(PostProcessStep.POPULATE_SCOPE))
            {
                // Set the parameters.
                IParameterNode[] argumentNodes = getParameterNodes();
                int n = argumentNodes.length;
                ParameterDefinition[] arguments = new ParameterDefinition[n];
                for (int i = 0; i < n; i++)
                {
                    if (argumentNodes[i] instanceof ParameterNode)
                        arguments[i] = (ParameterDefinition)((ParameterNode)argumentNodes[i]).getDefinition();
                }

                ((FunctionDefinition)this.definition).setParameters(arguments);
            }

        }
        // Recurse on the function block.
        BlockNode contents = contentsPart.getContents();
        if (contents != null)
            contents.analyze(set, scope, problems);

        if (set.contains(PostProcessStep.POPULATE_SCOPE))
            tryAddDefaultArgument();        
    }
    
    /*
     * For debugging only.
     * Builds a string such as <code>"doSomething(int, String):void"</code>
     * from the signature of the function being defined.
     */
    @Override
    protected boolean buildInnerString(StringBuilder sb)
    {
        sb.append(getName());

        sb.append('(');
        IVariableNode[] args = getParameterNodes();
        for (int i = 0; i < args.length; i++)
        {
            IVariableNode arg = args[i];
            sb.append(arg.getVariableType());
            if (i < args.length - 1)
                sb.append(", ");
        }
        sb.append(')');

        if (getReturnType().length() > 0)
            sb.append(":" + getReturnType());

        return true;
    }
    
    //
    // TreeNode overrides
    //

    @Override
    protected int getInitialChildCount()
    {
        return 4;
    }

    //
    // BaseDefinitionNode overrides overrides
    //

    @Override
    protected void init(ExpressionNodeBase idNode)
    {
        super.init(idNode);
        
        contentsPart = createContentsPart();
    }
    
    @Override
    public INamespaceDecorationNode getNamespaceNode()
    {
        INamespaceDecorationNode namespaceNode = super.getNamespaceNode();
        
        if (isConstructor())
        {
            if (namespaceNode != null
                    && (namespaceNode.getName().equals(INamespaceConstants.public_) || namespaceNode.getName().equals(INamespaceConstants.private_)))
            {
                // if the existing node is already public or private, return it
                return namespaceNode;
            }
            IASNode parentNode = getParent();
            if (parentNode instanceof IDefinitionNode)
            {
                IDefinitionNode defNode = (IDefinitionNode) parentNode;
                IMetaTagNode[] metaTagNodes = defNode.getMetaTags().getTagsByName(IMetaAttributeConstants.ATTRIBUTE_PRIVATE_CONSTRUCTOR);
                if (metaTagNodes != null && metaTagNodes.length > 0)
                {
                    // if the parent class has [RoyalePrivateConstructor]
                    // metadata, the constructor should be considered private
                    // and we should generate a fake namespace node
                    NamespaceIdentifierNode priv = new NamespaceIdentifierNode(INamespaceConstants.private_);
                    priv.span(-1, -1, -1, -1, -1, -1);
                    priv.setDecorationTarget(this);
                    return priv;
                }
            }
            // if there is no namespace node, the namespace defaults to public
            // and we'll generate a fake node
            NamespaceIdentifierNode pub = new NamespaceIdentifierNode(INamespaceConstants.public_);
            pub.span(-1, -1, -1, -1, -1, -1);
            pub.setDecorationTarget(this);
            return pub;
        }
        
        return namespaceNode;
    }
    
    @Override
    public String getNamespace()
    {
        INamespaceDecorationNode ns = getNamespaceNode();
        if (ns != null)
        {
            String nameString = ns.getName();
            // If public or private, just return it.
            if (nameString.equals(INamespaceConstants.public_) || nameString.equals(INamespaceConstants.private_))
                return nameString;

            // Otherwise, check to see if we are a constructor and always return
            // public
            if (isConstructor())
                return INamespaceConstants.public_;
            
            // Just return the value.
            return nameString;
        }
        
        // If we are null, make sure to check if we are a constructor.
        if (isConstructor())
            return INamespaceConstants.public_;
        
        return null;
    }
    
    @Override
    public boolean hasNamespace(String namespace)
    {
        if (isConstructor())
            return namespace.compareTo(INamespaceConstants.public_) == 0;

        return super.hasNamespace(namespace);
    }
    
    @Override
    public FunctionDefinition getDefinition()
    {
        return (FunctionDefinition)super.getDefinition();
    }
    
    @Override
    protected void setDefinition(IDefinition def)
    {
        assert def instanceof FunctionDefinition;
        super.setDefinition(def);
    }

    //
    // IFunctionNode implementations
    //
    
    @Override
    public boolean isImplicit()
    {
        if (getParent() != null)
        {
            if (getParent().getParent() instanceof ClassNode)
            {
                ClassNode containingClass = (ClassNode)getParent().getParent();
                if (containingClass.getDefaultConstructorNode() == this)
                    return true;
            }
            else if (getParent().getParent() instanceof InterfaceNode)
            {
                InterfaceNode containingInterface = (InterfaceNode)getParent().getParent();
                if (containingInterface.getCastFunctionNode() == this)
                    return true;
            }
        }
        return false;
    }
    
    @Override
    public String getQualifiedName()
    {
        String qualifiedName = null;
        if (isPackageLevelFunction())
        {
            IImportTarget importTarget = ASImportTarget.buildImportFromPackageName(getWorkspace(), getPackageName());
            qualifiedName = importTarget.getQualifiedName(getName());
        }
        if (qualifiedName == null)
            qualifiedName = getName();
        return qualifiedName;
    }

    @Override
    public String getShortName()
    {
        return getName();
    }
    
    @Override
    public final ScopedBlockNode getScopedNode()
    {
        return contentsPart.getContents();
    }
    
    @Override
    public FunctionClassification getFunctionClassification()
    {
        IScopedNode scopedNode = getScopeNode();
        IASNode node = scopedNode;
        
        if (node instanceof ICommonClassNode || node.getParent() instanceof ICommonClassNode)
            return FunctionClassification.CLASS_MEMBER;
        
        if (node.getParent() instanceof InterfaceNode)
            return FunctionClassification.INTERFACE_MEMBER;
        
        if (node.getParent() instanceof PackageNode)
            return FunctionClassification.PACKAGE_MEMBER;
        
        if (node instanceof FileNode)// this is an include
            return FunctionClassification.FILE_MEMBER;
        
        return FunctionClassification.LOCAL;
    }

    @Override
    public boolean isGetter()
    {
        return this instanceof GetterNode;
    }

    @Override
    public boolean isSetter()
    {
        return this instanceof SetterNode;
    }
    
    @Override
    public boolean isConstructor()
    {
        String name = getName();
        String returnType = getReturnType();
        
        // Allow constructors that have a (bogus) return type
        if (!returnType.equals("") &&
            !returnType.equals(name) &&
            !isAnyType() &&
            !isVoidType())
        {
            return false;
        }
        
        if (getParent() != null &&
            getParent().getParent() != null && 
            (getParent().getParent() instanceof ClassNode ||
             getParent().getParent() instanceof InterfaceNode))
        {
            if (name.equals(((IDefinitionNode) getParent().getParent()).getShortName()))
                return true;
        }
        
        return false;
    }

    @Override
    public boolean isCastFunction()
    {
        String name = getName();
        String returnType = getReturnType();
        
        if (!returnType.equals("") && !returnType.equals(name))
            return false;
        
        if (getParent() != null &&
            getParent().getParent() != null &&
            getParent().getParent() instanceof ITypeNode)
        {
            if (name.equals(((ITypeNode)getParent().getParent()).getShortName()))
                return true;
        }
        
        return false;
    }
    
    @Override
    public ContainerNode getParametersContainerNode()
    {
        return contentsPart.getParametersNode();
    }

    @Override
    public IParameterNode[] getParameterNodes()
    {
        IParameterNode[] variables = {};
        
        ContainerNode arguments = contentsPart.getParametersNode();
        
        if (arguments != null)
        {
            int argumentscount = arguments.getChildCount();
            variables = new IParameterNode[argumentscount];
            for (int i = 0; i < argumentscount; i++)
            {
                IASNode argument = arguments.getChild(i);
                if (argument instanceof IParameterNode)
                    variables[i] = (IParameterNode)argument;
            }
        }
        
        return variables;
    }

    @Override
    public IExpressionNode getReturnTypeNode()
    {
        return getTypeNode();
    }
    
    @Override
    public String getReturnType()
    {
        return getTypeName();
    }
    
    @Override
    public boolean hasBody()
    {
        ScopedBlockNode sbn = getScopedNode();
        
        return sbn.getChildCount() > 0 ||
               sbn.getContainerType() != IContainerNode.ContainerType.SYNTHESIZED;
    }
    
    //
    // Other methods
    //

    protected IFunctionContentsPart createContentsPart()
    {
        return new FunctionContentsPart();
    }

    private void tryAddDefaultArgument()
    {
        FunctionDefinition def = getDefinition();
        ASScope funcScope = def.getContainedScope();

        if (needsArguments && funcScope.getLocalDefinitionSetByName(IASLanguageConstants.arguments) == null)
        {
            // Add the arguments Array to the function scope
            // only do this if there is not already a local property, or parameter that is named arguments
            // and something in the function body references arguments - this should avoid creating the 
            // definition when it's not needed.
            VariableDefinition argumentsDef = new VariableDefinition(IASLanguageConstants.arguments);
            argumentsDef.setNamespaceReference(NamespaceDefinition.getDefaultNamespaceDefinition(funcScope));
            argumentsDef.setTypeReference(ReferenceFactory.builtinReference(IASLanguageConstants.BuiltinType.ARRAY));

            argumentsDef.setImplicit();

            funcScope.addDefinition(argumentsDef);
        }
    }

    private void setConstructorIfNeeded(FunctionDefinition funcDef)
    {
        if (isConstructor())
        {
            IASNode parentParent = getParent().getParent();
            if( parentParent instanceof ClassNode)
            {
                ClassNode classNode = (ClassNode)parentParent;
                if (classNode.getConstructorNode() == null)
                {
                    if (nameNode instanceof IdentifierNode)
                    {
                        ((IdentifierNode)nameNode).setReferenceValue(classNode.getDefinition());
                        classNode.constructorNode = this;
                    }
                }
                // if the namespace reference is private, don't change it
                if(!(funcDef.getNamespaceReference() instanceof INamespaceDefinition.IPrivateNamespaceDefinition))
                {
                    funcDef.setNamespaceReference(NamespaceDefinition.getCodeModelImplicitDefinitionNamespace());
                }
            }
            else
            {
                funcDef.setNamespaceReference(NamespaceDefinition.getCodeModelImplicitDefinitionNamespace());
            }
        }
    }

    FunctionDefinition buildDefinition()
    {
        String definitionName = getName();
      //  System.out.println("buildDefinition: " + definitionName);
        FunctionDefinition definition = createFunctionDefinition(definitionName);
        definition.setNode(this);

        fillInNamespaceAndModifiers(definition);
        fillInMetadata(definition);

        // Set the return type. If a type annotation doesn't appear in the source,
        // the return type in the definition will be "".
        IReference returnType = typeNode != null ? typeNode.computeTypeReference() : null;
        definition.setReturnTypeReference(returnType);

        definition.setTypeReference(ReferenceFactory.builtinReference(IASLanguageConstants.BuiltinType.FUNCTION));
        setConstructorIfNeeded(definition);
        return definition;
    }

    /**
     * Method to create the correct Definition class - needed so we can create
     * different Definition types for getters, setters, and plain old functions
     * 
     * @return A Definition object to represent this function
     */
    protected FunctionDefinition createFunctionDefinition(String name)
    {
        return new FunctionDefinition(name);
    }

    /**
     * Get the function keyword
     * 
     * @return node containing the function keyword
     */
    public KeywordNode getFunctionKeywordNode()
    {
        return contentsPart.getFunctionKeywordNode();
    }

    /**
     * Determine whether this is a package-level function (i.e. a function
     * defined in a package, as opposed to a class or some other scope)
     * 
     * @return true if this is a package-level function
     */
    public boolean isPackageLevelFunction()
    {
        // regular package-level function
        IASNode parent = getParent();
        IASNode parent2 = parent.getParent();
       
        if (parent instanceof BlockNode && parent2 instanceof PackageNode)
            return true;
        
        // constructor
        if (parent2 != null)
        {
            IASNode parent3 = parent2.getParent();
            
            if (isConstructor() &&
                parent2 instanceof ClassNode &&
                parent3 instanceof BlockNode &&
                parent3.getParent() instanceof PackageNode)
            {
                return true;
            }
        }
        
        return false;
    }

    /**
     * Get the real namespace node, or null if there isn't one. Used by
     * semantics. This differs from getNamespaceNode above, as it will not
     * construct an implicit public namespace if one is missing
     */
    public INamespaceDecorationNode getActualNamespaceNode()
    {
        return super.getNamespaceNode();
    }

    /**
     * Is this a constructor of the specified class (assumes that this function
     * definition is actually located in the body of the specified class)
     */
    public boolean isConstructorOf(ClassNode classNode)
    {
        return this.equals(classNode.getConstructorNode());
    }

    /**
     * Get any saved parsing problems from lazily parsing the function body.
     * This list will be nulled out after this method is called,
     * so we don't start leaking parser problems.
     * 
     * @return Collection of problems encountered while parsing the function
     *  or an empty list if there were none.
     *
     */
    public Collection<ICompilerProblem> getParsingProblems()
    {
        if (parseProblems != null)
        {
            Collection<ICompilerProblem> problems = parseProblems;
            parseProblems = null;
            return problems;
        }
        
        return Collections.emptyList();
    }
    /**
     * Build AST for the function body from the buffered function body text.
     * <p>
     * Make sure {@link PostProcessStep#POPULATE_SCOPE} has been applied to the
     * containing {@code FileNode} This method always populate scopes of the
     * rebuilt function node. If the scopes for the containing nodes weren't
     * initialized, the rebuilt scopes can attach itself to it's parent.
     */
    public final void parseFunctionBody(final Collection<ICompilerProblem> problems)
    {
        if (!isBodyDeferred)
            return;

        deferredBodyParsingLock.lock();
        try
        {
            deferredBodyParsingReferenceCount++;

            assert problems != null : "Problems collection can't be null";

            final ScopedBlockNode contents = contentsPart.getContents();
            assert contents != null : "Function body node can't be null: function " + getName();

            // Only re-parse if the function body text is not empty, and the 
            // function body node doesn't have any children.
            if (contents.getChildCount() > 0)
                return;
            assert deferredBodyParsingReferenceCount == 1;

            assert openT != null : "Expected '{' token.";

            final String sourcePath = getSourcePath();
            assert sourcePath != null && !sourcePath.isEmpty() : "Source path not set.";

            final FileNode fileNode = (FileNode)getAncestorOfType(FileNode.class);
            assert fileNode != null : "FileNode not found: function " + getName();
            final IWorkspace workspace = fileNode.getWorkspace();

            ASFileScope fileScope = fileNode.getFileScope();
            fileScope.addParsedFunctionBodies(this);

            try
            {
                // select function body source
                final Reader sourceReader;
                if (functionBodyText != null)
                {
                    // from cached body text
                    sourceReader = new StringReader(functionBodyText);
                }
                else
                {
                    // from file using offset
                	if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.WORKSPACE) == CompilerDiagnosticsConstants.WORKSPACE)
                		System.out.println("FunctionNode waiting for lock in parseFunctionBody");
                    sourceReader = workspace.getFileSpecification(sourcePath).createReader();
                	if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.WORKSPACE) == CompilerDiagnosticsConstants.WORKSPACE)
                		System.out.println("FunctionNode done with lock in parseFunctionBody");
                    sourceReader.skip(openT.getLocalEnd());
                }

                assert !anyNonParametersInScope(contents);
                
                // rebuild function body AST
                
                // The incoming "problems" collection might be under modifications from
                // other code generation threads. In order to filter problems after parsing,
                // a local problem collection is created and added to the main problem
                // collection later.
                final List<ICompilerProblem> functionLocalProblems = new ArrayList<ICompilerProblem>();
                ASParser.parseFunctionBody(
                        contents,
                        sourceReader,
                        sourcePath,
                        openT,
                        functionLocalProblems,
                        workspace,
                        fileNode,
                        configProcessor);
                filterObsoleteProblems(fileNode, functionLocalProblems);
                problems.addAll(functionLocalProblems);

                // dispose cached function body info
                
                functionBodyText = null;
                
                // We should release "openT" as well. However, incremental compilation
                // needs this to redo code-generation.
                // openT = null;

                // connect function and its body scope
                final EnumSet<PostProcessStep> postProcess = EnumSet.of(
                    PostProcessStep.CALCULATE_OFFSETS,
                    PostProcessStep.POPULATE_SCOPE,
                    PostProcessStep.RECONNECT_DEFINITIONS);
                
                problems.addAll(contents.runPostProcess(postProcess, contents.getASScope()));
                
                // add implicit "arguments" argument to the local scope
                tryAddDefaultArgument();
            }
            catch (IOException e)
            {
                problems.add(new InternalCompilerProblem2(this.getSourcePath(), e, "function body parser"));
            }
        }
        finally
        {
            deferredBodyParsingLock.unlock();
        }
    }

    /**
     * {@link BaseASParser} is stateful. The state is essential in compiler
     * problem creation. However, when re-parsing a deferred function body, the
     * parser state can't be fully restored. As a result, some compiler problems
     * can be obsolete. This function removes those unwanted problems.
     * 
     * @param fileNode AST root node.
     * @param problems compiler problems found in the deferred function body.
     */
    private void filterObsoleteProblems(FileNode fileNode, Collection<ICompilerProblem> localProblems)
    {
        final int functionStartLine = this.getLine();
        final Collection<ICompilerProblem> problems = fileNode.getProblems();
        final Collection<ICompilerProblem> filteredLocalProblems = filter(
                localProblems,
                and(problemAtLine(functionStartLine), problemOfType(CanNotInsertSemicolonProblem.class)));

        // If the function signature has syntax error, the first "unterminated statement" problem
        // from function body is then obsolete.
        if (!filteredLocalProblems.isEmpty())
        {
            final Collection<ICompilerProblem> functionSignatureProblems =
                    filter(problems, problemAtLine(functionStartLine));
            if (!functionSignatureProblems.isEmpty())
            {
                localProblems.removeAll(filteredLocalProblems);
            }
        }
    }

    /**
     * IFilter {@link ICompilerProblem} collections by line number.
     */
    private static Predicate<ICompilerProblem> problemAtLine(final int line)
    {
        return new Predicate<ICompilerProblem>()
        {
            @Override
            public boolean apply(ICompilerProblem problem)
            {
                return problem.getLine() == line;
            }
            @Override
            public boolean test(ICompilerProblem input)
            {
                return apply(input);
            }
        };
    }

    /**
     * IFilter {@link ICompilerProblem} collections by class.
     */
    private static Predicate<ICompilerProblem> problemOfType(final Class<? extends ICompilerProblem> problemClass)
    {
        return new Predicate<ICompilerProblem>()
        {
            @Override
            public boolean apply(ICompilerProblem problem)
            {
                return problemClass.isInstance(problem);
            }
            @Override
            public boolean test(ICompilerProblem input)
            {
                return apply(input);
            }
        };
    }

    private static boolean anyNonParametersInScope(ScopedBlockNode contents)
    {
        IASScope sc = contents.getScope();
        Collection<IDefinition> ldfs = sc.getAllLocalDefinitions();
        for (IDefinition def : ldfs)
        {
            if (!(def instanceof IParameterDefinition))
                return true;
        }
        return false;
    }

    /**
     * Delete all children nodes in a function body.
     */
    public final void discardFunctionBody()
    {
        if (!isBodyDeferred || containsLocalFunctions())
            return;

        deferredBodyParsingLock.lock();
        try
        {
            deferredBodyParsingReferenceCount--;
            // only discard the body once there are 0 reference to it
            if (deferredBodyParsingReferenceCount > 0)
                return;

            final ScopedBlockNode contents = getScopedNode();
            if (contents.getChildCount() > 0)
            {
                final FileNode fileNode = (FileNode)getAncestorOfType(FileNode.class);
                ASFileScope fileScope = fileNode.getFileScope();
                fileScope.removeParsedFunctionBodies(this);

                contents.removeAllChildren();

                // Now we need to remove all the definitions in this function scope, except
                // we keep the parameters. This is because the initial "skeleton parse" goes as
                // far as the parameters.
                // So when we throw away the body, we still need to keep the parameter definitions
                IASScope functionScope = contents.getScope();
                Collection<IDefinition> localDefs = functionScope.getAllLocalDefinitions();
                for (IDefinition def : localDefs)
                {
                    if (! (def instanceof IParameterDefinition))
                    {
                        ASScope asScope = (ASScope)functionScope;
                        asScope.removeDefinition(def);
                    }
                }
            }
            assert (contents.getScope() == null) || (!anyNonParametersInScope(contents));
        }
        finally
        {
            deferredBodyParsingLock.unlock();
        }
    }

    public final boolean hasBeenParsed()
    {
        if (!isBodyDeferred)
            return true;

        deferredBodyParsingLock.lock();
        try
        {
            return deferredBodyParsingReferenceCount > 0;
        }
        finally
        {
            deferredBodyParsingLock.unlock();
        }
    }

    /**
     * Store the function body text on the function node so that the AST nodes
     * can be rebuilt later.
     */
    public final void setFunctionBodyInfo(ASToken openT, ASToken lastTokenInBody,
                                          ConfigProcessor configProcessor,
                                          StringBuilder bodyCache)
    {
        assert openT != null : "Open curly token can't be null";
        assert openT.getType() == ASTokenTypes.TOKEN_BLOCK_OPEN : "Expected '{' token.";
        assert lastTokenInBody != null : "Last token in function body can't be null.";
        assert configProcessor != null : "Project config variables can't be null.";

        this.openT = openT.clone();
        this.configProcessor = configProcessor;
        this.isBodyDeferred = true;
        
        if (bodyCache == null)
            this.functionBodyText = null;
        else
            this.functionBodyText = bodyCache.toString();
    }
    
    private ArrayList<IFunctionNode> localFunctions;
    
    @Override
    public List<IFunctionNode> getLocalFunctions()
    {
        return localFunctions;
    }

    @Override
    public boolean containsLocalFunctions()
    {
        return localFunctions != null;
    }

    @Override
    public void rememberLocalFunction(IFunctionNode value)
    {
        if (localFunctions == null)
            localFunctions = new ArrayList<IFunctionNode>();
        
        localFunctions.add(value);
    }
}
