/*
 *
 *  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 java.util.ArrayList;
import java.util.Collection;
import java.util.EnumSet;

import antlr.Token;

import org.apache.royale.compiler.common.ASModifier;
import org.apache.royale.compiler.common.ISourceLocation;
import org.apache.royale.compiler.common.SourceLocation;
import org.apache.royale.compiler.definitions.IDefinition;
import org.apache.royale.compiler.filespecs.IFileSpecification;
import org.apache.royale.compiler.internal.common.Counter;
import org.apache.royale.compiler.internal.definitions.DefinitionBase;
import org.apache.royale.compiler.internal.parsing.as.OffsetLookup;
import org.apache.royale.compiler.internal.scopes.ASFileScope;
import org.apache.royale.compiler.internal.scopes.ASScope;
import org.apache.royale.compiler.internal.scopes.TypeScope;
import org.apache.royale.compiler.internal.semantics.PostProcessStep;
import org.apache.royale.compiler.problems.ICompilerProblem;
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.IFileNode;
import org.apache.royale.compiler.tree.as.IImportNode;
import org.apache.royale.compiler.tree.as.IPackageNode;
import org.apache.royale.compiler.tree.as.IScopedNode;
import org.apache.royale.compiler.tree.mxml.IMXMLClassDefinitionNode;
import org.apache.royale.compiler.workspaces.IWorkspace;

/**
 * Base class for ActionScript parse tree nodes
 */
public abstract class NodeBase extends SourceLocation implements IASNode
{
    /**
     * Used in calls to CheapArray functions. It represents the type of objects
     * that appear in fSymbols.
     */
    protected static final IASNode[] emptyNodeArray = new IASNode[0];
    

    /**
     * Combine the attributes from the base class with the attributes specific
     * to this class
     * 
     * @param superAttributes The attributes of the base class.
     * @param myAttributes The attributes of this class.
     * @return attribute value
     */
    public static String[] combineAttributes(String[] superAttributes, String[] myAttributes)
    {
        String[] combinedAttributes = new String[superAttributes.length + myAttributes.length];
        for (int i = 0; i < superAttributes.length; i++)
        {
            combinedAttributes[i] = superAttributes[i];
        }
        for (int i = 0; i < myAttributes.length; i++)
        {
            combinedAttributes[superAttributes.length + i] = myAttributes[i];
        }
        return combinedAttributes;
    }

    /**
     * Constructor.
     */
    public NodeBase()
    {
        super();
        parent = null;
        
        if (Counter.COUNT_NODES)
            countNodes();
    }

    /**
     * Parent node.
     * <p>
     * Methods (even in NodeBase itself) should use getParent() instead of
     * accessing this member directly. getParent() is overridden in FileNode to
     * do something special when the FileNode represents a shared file.
     */
    protected IASNode parent;

    @Override
    public IASNode getParent()
    {
        return parent;
    }

    @Override
    public int getChildCount()
    {
        return 0;
    }

    @Override
    public IASNode getChild(int i)
    {
        return null;
    }

    @Override
    public IFileSpecification getFileSpecification()
    {
        // TODO Make sure this works with include processing!!!
        ASFileScope fileScope = getFileScope();
        IWorkspace w = fileScope.getWorkspace();
        return w.getFileSpecification(fileScope.getContainingPath());
    }

    @Override
    public int getSpanningStart()
    {
        return getStart();
    }

    /**
     * Get the node type as a string. For example, this will return
     * "IdentifierNode" for an IdentifierNode.
     * 
     * @return the node type
     */
    public String getNodeKind()
    {
        return getClass().getSimpleName();
    }

    @Override
    public boolean contains(int offset)
    {
        return getAbsoluteStart() < offset && getAbsoluteEnd() >= offset;
    }

    /**
     * Determine whether the offset "loosely" fits within this node. With normal
     * nodes, this will return true if the offset matches fLocation.fStart or
     * fLocation.fEnd or anything in between. With bogus nodes (which have
     * fStart == fEnd), this will return true if the offset comes after fStart
     * and before any other nodes in the parse tree
     * 
     * @param offset the offset to test
     * @return true if the offset is "loosely" contained within this node
     */
    public boolean looselyContains(int offset)
    {
        if (getAbsoluteStart() == getAbsoluteEnd())
        {
            // This is a bogus placeholder node (generally an identifier that's been inserted to complete an expression).
            // We'll pretend that it extends all the way to the start offset of the next node.  Now to find that next node...
            if (getAbsoluteStart() <= offset)
            {
                NodeBase containingNode = (NodeBase)getParent();
                // Step 1: Find a node that extends beyond the target offset
                while (containingNode != null && containingNode.getAbsoluteEnd() <= getAbsoluteEnd())
                    containingNode = (NodeBase)containingNode.getParent();
                // If no such node exists, we know that there aren't any tokens in the parse
                // tree that end after this bogus token.  So we can go ahead and pretend this node
                // extends all the way out.
                if (containingNode == null)
                    return true;
                // Step 2: Find a node that starts after the target offset
                IASNode succeedingNode = containingNode.getSucceedingNode(getAbsoluteEnd());
                // If no such node exists, we know that there aren't any tokens in the parse
                // tree that start after this bogus token.  So we can go ahead and pretend this
                // node extends all the way out.
                if (succeedingNode == null)
                    return true;
                // Otherwise, pretend this node extends all the way to the next token.
                if (succeedingNode.getAbsoluteStart() >= offset)
                    return true;
            }
        }
        else
        {
            // This is a real token.  See if the test offset fits within (including the boundaries, since we're looking
            // at "loose" containment.
            return getAbsoluteStart() <= offset && getAbsoluteEnd() >= offset;
        }
        return false;
    }

    @Override
    public IASNode getSucceedingNode(int offset)
    {
        // This node ends before the offset is even reached.  This is hopeless.
        if (getAbsoluteEnd() <= offset)
            return null;

        // See if one of our children starts after the offset
        for (int i = 0; i < getChildCount(); i++)
        {
            IASNode child = getChild(i);
            
            if (child.getAbsoluteStart() > offset)
                return child;
            else if (child.getAbsoluteEnd() > offset)
                return child.getSucceedingNode(offset);
        }
        
        return null;
    }

    /**
     * Determine whether this node is transparent. If a node is transparent, it
     * can never be returned from getContainingNode... the offset will be
     * considered part of the parent node instead.
     * 
     * @return true if the node is transparent
     */
    public boolean isTransparent()
    {
        return false;
    }

    @Override
    public IASNode getContainingNode(int offset)
    {
        // This node doesn't even contain the offset.  This is hopeless.
        if (!contains(offset))
        {
            return null;
        }
        IASNode containingNode = this;
        int childCount = getChildCount();
        // See if the offset is contained within one of our children.
        for (int i = 0; i < childCount; i++)
        {
            IASNode child = getChild(i);
            if (child.getAbsoluteStart() <= offset)
            {
                if (child.contains(offset))
                {
                    containingNode = child.getContainingNode(offset);
                    if (child instanceof NodeBase && ((NodeBase)child).canContinueContainmentSearch(containingNode, this, i, true))
                        continue;
                    break;
                }
            }
            else
            {
                if (child instanceof NodeBase && ((NodeBase)child).canContinueContainmentSearch(containingNode, this, i, false))
                    continue;
                // We've passed this offset without finding a child that contains it.  This is
                // the nearest enclosing node.
                break;
            }
        }
        // Make sure we don't return a transparent node
        while (containingNode != null && containingNode instanceof NodeBase && ((NodeBase)containingNode).isTransparent())
        {
            containingNode = containingNode.getParent();
        }
        return containingNode;
    }

    @Override
    public boolean isTerminal()
    {
        return false;
    }

    @Override
    public IScopedNode getContainingScope()
    {
        return (IScopedNode)getAncestorOfType(IScopedNode.class);
    }

    // TODO Can probably eliminate this.
    protected boolean canContinueContainmentSearch(IASNode containingNode, IASNode currentNode, int childOffset, boolean offsetsStillValid)
    {
        return false;
    }

    @Override
    public IASNode getAncestorOfType(Class<? extends IASNode> nodeType)
    {
        IASNode current = getParent();
        while (current != null && !(nodeType.isInstance(current)))
            current = current.getParent();
        if (current != null)
            return current;
        return null;
    }

    @Override
    public String getPackageName()
    {
        // Starting with this node's parent, walk up the parent chain
        // looking for a package node or an MXML class definition node.
        // These two types of nodes understand what their package is.
        IASNode current = getParent();
        while (current != null &&
               !(current instanceof IPackageNode ||
                 current instanceof IMXMLClassDefinitionNode))
        {
            current = current.getParent();
        }

        if (current instanceof IPackageNode)
            return ((IPackageNode)current).getPackageName();
        else if (current instanceof IMXMLClassDefinitionNode)
            return ((IMXMLClassDefinitionNode)current).getPackageName();

        return null;
    }

    /**
     * Set the start offset of the node to fall just after the token. Used
     * during parsing.
     * 
     * @param token start this node after this token
     */
    public void startAfter(Token token)
    {
        if (token instanceof ISourceLocation)
        {
            startAfter((ISourceLocation) token);
        }
    }
    
    /**
     * Set the start offset of the node to fall just after the token. Used
     * during parsing.
     * 
     * @param location start this node after this token
     */
    public final void startAfter(ISourceLocation location)
    {
        final int end = location.getEnd();
        if (end != UNKNOWN)
        {
            setSourcePath(location.getSourcePath());
            setStart(end);
            setLine(location.getEndLine());
            setColumn(location.getEndColumn());
        }
    }

    /**
     * Set the start offset of the node to fall just before the token. Used
     * during parsing.
     * 
     * @param token start this node before this token
     */
    public final void startBefore(Token token)
    {
        if (token instanceof ISourceLocation)
            startBefore((ISourceLocation)token);
    }

    /**
     * Set the start offset of the node to fall just before the token. Used
     * during parsing.
     * 
     * @param location start this node before this token
     */
    public final void startBefore(ISourceLocation location)
    {
        final int start = location.getStart();
        if (start != UNKNOWN)
        {
            setSourcePath(location.getSourcePath());
            setStart(start);
            setLine(location.getLine());
            setColumn(location.getColumn());
        }
    }

    /**
     * Set the end offset of the node to fall just after the token. Used during
     * parsing.
     * 
     * @param token end this node after this token
     */
    public final void endAfter(Token token)
    {
        if (token instanceof ISourceLocation)
            endAfter((ISourceLocation)token);
    }

    /**
     * Set the end offset of the node to fall just after the token. Used during
     * parsing.
     * 
     * @param location end this node after this token
     */
    public final void endAfter(ISourceLocation location)
    {
        final int end = location.getEnd();
        if (end != UNKNOWN)
        {
            setEnd(end);
            setEndLine(location.getEndLine());
            setEndColumn(location.getEndColumn());
        }
    }

    /**
     * Set the end offset of the node to fall just before the token. Used during
     * parsing.
     * 
     * @param token start this node before this token
     */
    public final void endBefore(Token token)
    {
        if (token instanceof ISourceLocation)
            endBefore((ISourceLocation)token);
    }

    /**
     * Set the end offset of the node to fall just before the token. Used during
     * parsing.
     * 
     * @param location start this node before this token
     */
    public final void endBefore(ISourceLocation location)
    {
        final int start = location.getStart();
        if (start != UNKNOWN)
        {
            setEnd(start);
            setEndLine(location.getLine());
            setEndColumn(location.getColumn());
        }
    }

    /**
     * Set the start and end offsets of the node to coincide with the token's
     * offsets. Used during parsing.
     * 
     * @param token the token to take the offsets from
     */
    public final void span(Token token)
    {
        if (token instanceof ISourceLocation)
            span((ISourceLocation)token);
    }

    /**
     * Set the start and end offsets of the node to coincide with the tokens'
     * offsets. Used during parsing.
     * 
     * @param firstToken the token to take the start offset and line number from
     * @param lastToken the token to take the end offset from
     */
    public final void span(Token firstToken, Token lastToken)
    {
        if (firstToken instanceof ISourceLocation && lastToken instanceof ISourceLocation)
            span((ISourceLocation)firstToken, (ISourceLocation)lastToken);
    }

    /**
     * Set the start and end offsets of the node. Used during parsing.
     * 
     * @param start start offset for the node
     * @param end end offset for the node
     * @param line line number for the node
     */
    public final void span(int start, int end, int line, int column)
    {
        setStart(start);
        setEnd(end);
        setLine(line);
        setColumn(column);
    }

    public Collection<ICompilerProblem> runPostProcess(EnumSet<PostProcessStep> set, ASScope containingScope)
    {
        ArrayList<ICompilerProblem> problems = new ArrayList<ICompilerProblem>(10);
        normalize(set.contains(PostProcessStep.CALCULATE_OFFSETS));
        if (set.contains(PostProcessStep.POPULATE_SCOPE) || set.contains(PostProcessStep.RECONNECT_DEFINITIONS))
            analyze(set, containingScope, problems);
        return problems;
    }

    protected void analyze(EnumSet<PostProcessStep> set, ASScope scope, Collection<ICompilerProblem> problems)
    {
        int childrenSize = getChildCount();
        // Populate this scope with definitions found among the children
        for (int i = 0; i < childrenSize; i++)
        {
            IASNode child = getChild(i);
            if (child instanceof NodeBase)
            {
                if (child.getParent() == null)
                    ((NodeBase)child).setParent(this);
                ((NodeBase)child).analyze(set, scope, problems);
            }
        }
    }

    /**
     * Changes the position of two children in our tree. Neither child can be
     * null and both must have the same parent
     * 
     * @param child the child to replace target
     * @param target the child to replace child
     */
    protected void swapChildren(NodeBase child, NodeBase target)
    {
        //no op in this impl
    }

    /**
     * Replaces the child with the given target. The child will be removed from
     * the tree, and its parentage will be updated
     * 
     * @param child the {@link NodeBase} to replace
     * @param target the {@link NodeBase} to replace the replaced
     */
    protected void replaceChild(NodeBase child, NodeBase target)
    {
        //no op
    }

    /**
     * Normalize the tree. Move custom children into the real child list and
     * fill in missing offsets so that offset lookup will work. Used during
     * parsing.
     */
    public void normalize(boolean fillInOffsets)
    {
        // The list of children doesn't change, so the child count should be constant throughout the loop
        int childrenSize = getChildCount();
        // Normalize the regular children
        for (int i = 0; i < childrenSize; i++)
        {
            IASNode child = getChild(i);
            if (child instanceof NodeBase)
            {
                ((NodeBase)child).setParent(this);
                ((NodeBase)child).normalize(fillInOffsets);
            }
        }
        // Add the special children (which get normalized as they're added)
        if (childrenSize == 0)
            setChildren(fillInOffsets);
        // Fill in offsets based on the child nodes
        if (fillInOffsets)
            fillInOffsets();
        // fill in any dangling ends
        //fillEndOffsets();
        // get rid of unused child space
    }

    /**
     * Allow various subclasses to do special kludgy things like identify
     * mx.core.Application.application
     */
    protected void connectedToProjectScope()
    {
        // The list of children doesn't change, so the child count should be constant throughout the loop
        int childrenSize = getChildCount();
        for (int i = 0; i < childrenSize; i++)
        {
            IASNode child = getChild(i);
            if (child instanceof NodeBase)
                ((NodeBase)child).connectedToProjectScope();
        }
    }

    /**
     * If this node has custom children (names, arguments, etc), shove them into
     * the list of children. Used during parsing.
     */
    protected void setChildren(boolean fillInOffsets)
    {
        //nothing in this class
    }

    /**
     * If the start and end offsets haven't been set explicitly, fill them in
     * based on the offsets of the children. Used during parsing. If the end
     * offset is less than any of the kids' offsets, change fLocation.fEnd to
     * encompass kids.
     */
    protected void fillInOffsets()
    {
        int numChildren = getChildCount();
        if (numChildren > 0)
        {
            int start = getAbsoluteStart();
            int end = getAbsoluteEnd();
            if (start == -1)
            {
                for (int i = 0; i < numChildren; i++)
                {
                    IASNode child = getChild(i);
                    int childStart = child.getAbsoluteStart();
                    if (childStart != -1)
                    {
                        if (getSourcePath() == null)
                            setSourcePath(child.getSourcePath());
                        setStart(childStart);
                        setLine(child.getLine());
                        setColumn(child.getColumn());
                        break;
                    }
                }
            }
            for (int i = numChildren - 1; i >= 0; i--)
            {
                IASNode child = getChild(i);
                int childEnd = child.getAbsoluteEnd();
                if (childEnd != -1)
                {
                    if (end < childEnd)
                    {
                        setEnd(childEnd);
                        setEndLine(child.getEndLine());
                        setEndColumn(child.getEndColumn());
                    }
                    break;
                }
            }
        }
    }

    /**
     * Set the parent node. Used during parsing.
     * 
     * @param parent parent node
     */
    public void setParent(NodeBase parent)
    {
        this.parent = parent;
    }

    /**
     * Get the nearest containing scope for this node. Used during type
     * decoration.
     * 
     * @return nearest containing scope for this node
     */
    // TODO Make this more efficient using overrides on BlockNode and FileNode.
    public IScopedNode getScopeNode()
    {
        if (this instanceof IScopedNode && ((IScopedNode)this).getScope() != null)
            return (IScopedNode)this;
        IASNode parent = getParent();
        if (parent != null && parent instanceof NodeBase)
            return ((NodeBase)parent).getScopeNode();
        return null;
    }

    /**
     * Get the path of the file in which this parse tree node resides.
     * <p>
     * The tree builder can set a node's origin source file using
     * {@link #setSourcePath(String)}. If the source path was set, return the
     * source path; Otherwise, use root {@link FileNode}'s path.
     * 
     * @return file path that contains this node
     */
    public String getContainingFilePath()
    {
        String path = getSourcePath();
        if (path != null)
            return path;

        final FileNode fileNode = (FileNode)getAncestorOfType(FileNode.class);
        if (fileNode != null)
            return fileNode.getSourcePath();
        return null;
    }

    /**
     * For debugging only. Displays this node and its children in a form like
     * this:
     * 
     * <pre>
     * FileNode "D:\tests\UIComponent.as" 0:0 0-467464  D:\tests\UIComponent.as
     *   PackageNode "mx.core" 12:1 436-465667 D:\tests\UIComponent.as
     *     FullNameNode "mx.core" 0:0 444-451 null
     *       IdentifierNode "mx" 12:9 444-446 D:\tests\UIComponent.as
     *       IdentifierNode "core" 12:12 447-451 D:\tests\UIComponent.as
     *     ScopedBlockNode 13:1 454-465667 D:\tests\UIComponent.as
     *       ImportNode "flash.accessibility.Accessibility" 14:1 457-497 D:\tests\UIComponent.as
     *       FullNameNode "flash.accessibility.Accessibility" 0:0 464-497 null
     *         FullNameNode "flash.accessibility" 0:0 464-483 null
     *           IdentifierNode "flash" 14:8 464-469 D:\tests\UIComponent.as
     *           IdentifierNode "accessibility" 14:14 470-483 D:\tests\UIComponent.as
     *         IdentifierNode "Accessibility" 14:28 484-497 D:\tests\UIComponent.as
     *       ...
     * </pre>
     * <p>
     * Subclasses may not override this, because we want to maintain regularity
     * for how all nodes display.
     */
    @Override
    public final String toString()
    {
        StringBuilder sb = new StringBuilder();
        buildStringRecursive(sb, 0, false);
        return sb.toString();
    }

    /**
     * For debugging only. Called by {@code toString()}, and recursively by
     * itself, to display this node on one line and each descendant node on
     * subsequent indented lines.
     * 
     * * */
    public void buildStringRecursive(StringBuilder sb, int level, boolean skipSrcPath)
    {
        // Indent two spaces for each nesting level.
        for (int i = 0; i < level; i++)
        {
            sb.append("  ");
        }

        // Build the string that represents this node.
        buildOuterString(sb, skipSrcPath);
      
        sb.append('\n');
        
        //To test scopes in ParserSuite
        if(skipSrcPath && (this instanceof IScopedNode)) {
            for (int i = 0; i < level+1; i++)
                sb.append("  ");
            sb.append("[Scope]");
            sb.append("\n");
            IScopedNode scopedNode = (IScopedNode)this;
            IASScope scope = scopedNode.getScope();
            Collection<IDefinition> definitions = scope.getAllLocalDefinitions();
            for(IDefinition def : definitions) {
                for (int i = 0; i < level+2; i++)
                    sb.append("  ");
                ((DefinitionBase)def).buildString(sb, false);
                sb.append('\n');
            }
        }

        // Recurse over the child nodes.
        int n = getChildCount();
        for (int i = 0; i < n; i++)
        {
            NodeBase child = (NodeBase)getChild(i);
            // The child can be null if we're toString()'ing
            // in the debugger during tree building.
            if (child != null)
                child.buildStringRecursive(sb, level + 1, skipSrcPath);
        }
    }

    /**
     * For debugging only. Called by {@code buildStringRecursive()} to display
     * information for this node only.
     * <p>
     * An example is
     * 
     * <pre>
     * PackageNode "mx.core" 12:1 436-465667 D:\tests\UIComponent.as
     * </pre>.
     * <p>
     * The type of node (PackageNode) is displayed first, followed by
     * node-specific information ("mx.core") followed by location information in
     * the form
     * 
     * <pre>
     * line:column start-end sourcepath
     * </pre>
     * 
     */
    private void buildOuterString(StringBuilder sb, boolean skipSrcPath)
    {
        // First display the type of node, as represented by 
        // the short name of the node class.
        sb.append(getNodeKind());

        sb.append("(").append(getNodeID().name()).append(")");
        
        sb.append(' ');

        // Display optional node-specific information in the middle.
        if (buildInnerString(sb))
            sb.append(' ');

        // The SourceLocation superclass handles producing a string such as
        // "17:5 160-188" C:/test.as".
        if(skipSrcPath) 
            sb.append(getOffsetsString());
        else
            sb.append(super.toString());
    }

    /**
     * For debugging only. This method is called by {@code buildOuterString()}.
     * It is overridden by subclasses to display optional node-specific
     * information in the middle of the string, between the node type and the
     * location information.
     */
    protected boolean buildInnerString(StringBuilder sb)
    {
        return false;
    }
    
    /**
     * For debugging only.
     */
    public String getInnerString()
    {
        StringBuilder sb = new StringBuilder();
        buildInnerString(sb);
        return sb.toString();
    }

    public ASFileScope getFileScope()
    {
        ASScope scope = getASScope();
        assert scope != null;
        while (!(scope instanceof ASFileScope))
        {
            scope = scope.getContainingScope();
            assert scope != null;
        }
        return (ASFileScope)scope;
    }

    /**
     * @return {@link OffsetLookup} object for the current tree or null.
     */
    protected final OffsetLookup tryGetOffsetLookup()
    {
        // Try FileNode.getOffsetLookup()
        final IASNode fileNode = getAncestorOfType(IFileNode.class);
        if (fileNode != null)
            return ((IFileNode)fileNode).getOffsetLookup();
        else
            return null;
    }

    /**
     * Get's the {@link IWorkspace} in which this {@link NodeBase} lives.
     * 
     * @return The {@link IWorkspace} in which this {@link NodeBase} lives.
     */
    public IWorkspace getWorkspace()
    {
        return getFileScope().getWorkspace();
    }

    /**
     * Get the scope this Node uses for name resolution as an ASScope.
     * 
     * @return the ASScope for this node, or null if there isn't one.
     */
    public ASScope getASScope()
    {
        IScopedNode scopeNode = getContainingScope();
        IASScope scope = scopeNode != null ? scopeNode.getScope() : null;

        // If the ScopedNode had a null scope, keep looking up the tree until we
        // find one with a non-null scope.
        // TODO: Is it a bug that an IScopedNode returns null for it's scope?
        // TODO: this seems like a leftover from block scoping - for example, a
        // TODO: ForLoopNode is an IScopedNode, but it doesn't really have a scope
        while (scope == null && scopeNode != null)
        {
            scopeNode = scopeNode.getContainingScope();
            scope = scopeNode != null ? scopeNode.getScope() : null;
        }

        if (scope instanceof TypeScope)
        {
            TypeScope typeScope = (TypeScope)scope;
            if (this instanceof BaseDefinitionNode)
            {
                if (((BaseDefinitionNode)this).hasModifier(ASModifier.STATIC))
                    scope = typeScope.getStaticScope();
                else
                    scope = typeScope.getInstanceScope();
            }
            else
            {
                // Do we need the class or instance scope?
                BaseDefinitionNode bdn = (BaseDefinitionNode)this.getAncestorOfType(BaseDefinitionNode.class);
                if (bdn instanceof ClassNode)
                {
                    // We must be loose code in a class
                    scope = typeScope.getStaticScope();
                }
                else
                {
                    if (bdn != null && bdn.hasModifier(ASModifier.STATIC)
                            // Namespaces are always static
                            || bdn instanceof NamespaceNode)
                        scope = typeScope.getStaticScope();
                    else
                        scope = typeScope.getInstanceScope();
                }
            }
        }
        ASScope asScope = scope instanceof ASScope ? (ASScope)scope : null;
        return asScope;
    }

    /**
     * Get the node's local start offset.
     */
    @Override
    public int getStart()
    {
        final OffsetLookup offsetLookup = tryGetOffsetLookup();
        if (offsetLookup != null)
        {
            // to handle start offset in an included file
            int absoluteOffset = getAbsoluteStart();
            final String pathBeforeCaret = offsetLookup.getFilename(absoluteOffset - 1);
            // if the offset is the first offset in the included file return without adjustment
            if (pathBeforeCaret != null && pathBeforeCaret.equals(getSourcePath()))
                return offsetLookup.getLocalOffset(absoluteOffset-1)+1;
            
            return offsetLookup.getLocalOffset(absoluteOffset);
        }
        
        return super.getStart();
    }

    /**
     * Get the node's local end offset.
     */
    @Override
    public int getEnd()
    {
        final OffsetLookup offsetLookup = tryGetOffsetLookup();
        return offsetLookup != null ?
                // to handle end offset in an included file
                offsetLookup.getLocalOffset(super.getEnd() - 1) + 1 :
                super.getEnd();
    }

    /**
     * @return The node's absolute start offset.
     */
    @Override
    public int getAbsoluteStart()
    {
        return super.getStart();
    }

    /**
     * @return The node's absolute end offset.
     */
    @Override
    public int getAbsoluteEnd()
    {
        return super.getEnd();
    }
    
    /**
     * Collects the import nodes that are descendants of this node
     * but which are not contained within a scoped node.
     * <p>
     * This is a helper method for {@link IScopedNode#getAllImportNodes}().
     * Since that method walks up the chain of scoped nodes, we don't want
     * to look inside scoped nodes that were already processed.
     * 
     * @param importNodes The collection of import nodes being built.
     */
    public void collectImportNodes(Collection<IImportNode> importNodes)
    {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; ++i)
        {
            IASNode child = getChild(i);
            
            if (child instanceof IImportNode)
                importNodes.add((IImportNode)child);

            else if (!(child instanceof IScopedNode))
                ((NodeBase)child).collectImportNodes(importNodes);
        }
    }

    /**
     * Used only in asserts.
     */
    public boolean verify()
    {
        // Verify the id.
        ASTNodeID id = getNodeID();
        assert id != null &&
               id != ASTNodeID.InvalidNodeID &&
               id != ASTNodeID.UnknownID : "Definition has bad id";

        // TODO: Verify the source location eventually.
        //      assert getSourcePath() != null : "Node has null source path:\n" + toString();
        //      assert getStart() != UNKNOWN : "Node has unknown start:\n" + toString();
        //      assert getEnd() != UNKNOWN : "Node has unknown end:\n" + toString();
        //      assert getLine() != UNKNOWN : "Node has unknown line:\n" + toString();
        //      assert getColumn() != UNKNOWN : "Node has unknown column:\n" + toString();

        // Verify the children.
        int n = getChildCount();
        for (int i = 0; i < n; i++)
        {
            // Any null children?
            NodeBase child = (NodeBase)getChild(i);
            assert child != null : "Node has null child";

            // Does each child have this node as its parent?
            // (Note: Two node classes override getParent() to not return the parent,
            // so exclude these for now.)
            if (!(child instanceof NamespaceIdentifierNode || child instanceof QualifiedNamespaceExpressionNode))
            {
                assert child.getParent() == this : "Child node has bad parent";
            }

            // Recurse on each child.
            child.verify();
        }

        return true;
    }
    
    /**
     * Counts various types of nodes that are created,
     * as well as the total number of nodes.
     */
    private void countNodes()
    {
        Counter counter = Counter.getInstance();
        counter.incrementCount(getClass().getSimpleName());
        counter.incrementCount("nodes");
    }
}
