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

import java.io.Closeable;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.royale.compiler.clients.ASC;
import org.apache.royale.compiler.constants.IASKeywordConstants;
import org.apache.royale.compiler.filespecs.IFileSpecification;
import org.apache.royale.compiler.internal.parsing.ITokenStreamFilter;
import org.apache.royale.compiler.internal.parsing.SourceFragmentsReader;
import org.apache.royale.compiler.internal.parsing.TokenBase;
import org.apache.royale.compiler.internal.units.ASCompilationUnit;
import org.apache.royale.compiler.parsing.IASToken;
import org.apache.royale.compiler.parsing.IASTokenizer;
import org.apache.royale.compiler.parsing.IASToken.ASTokenKind;
import org.apache.royale.compiler.problems.CyclicalIncludesProblem;
import org.apache.royale.compiler.problems.ExpectXmlBeforeNamespaceProblem;
import org.apache.royale.compiler.problems.FileNotFoundProblem;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.problems.InternalCompilerProblem2;
import org.apache.royale.compiler.problems.UnexpectedTokenProblem;
import org.apache.royale.utils.ILengthAwareReader;
import org.apache.royale.utils.NonLockingStringReader;
import org.apache.royale.utils.ILengthAwareReader.InputType;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;

/**
 * This Tokenizer provides tokens to be used by various clients, most notably
 * the ASParser. Given the nature of ambiguities in the ActionScript 3 language,
 * this tokenizer also serves to disambiguate tokens based on a combination of
 * look behind and lookahead. For all cases of ambiguity, only one token is
 * needed for look behind, and in our worst case, n tokens forwards where n is
 * the number of tokens that can be produced. Some other state is kept in order
 * to know which type of container we may exist in (function, class, interface,
 * etc). We buffer LA token results to avoid unneeded lookahead
 */
public class StreamingASTokenizer implements ASTokenTypes, IASTokenizer, Closeable
{
    private static final String FOR_EACH = "for each";
    private static final String XML = "xml";
    private static final String DEFAULT_XML_NAMESPACE = "default xml namespace";
    private static final String ZERO = "0";

    /**
     * Map from keyword text to token type.
     * <p>
     * We use a HashMap here to avoid slowing down the performance of the
     * underlying lexer. We are avoid the "longest match" problem, requiring a
     * lot of rescanning on the lexer level to determine keywords from
     * identifiers. And since hash map lookup is constant, this is (in theory)
     * faster than doing this in the scanner since we're not bound by i/o or
     * state machine back-tracing.
     */
    private static final Map<String, Integer> keywordToTokenMap = new ImmutableMap.Builder<String, Integer>()
            .put(IASKeywordConstants.AS, TOKEN_KEYWORD_AS)
            .put(IASKeywordConstants.IS, TOKEN_KEYWORD_IS)
            .put(IASKeywordConstants.INSTANCEOF, TOKEN_KEYWORD_INSTANCEOF)
            .put(IASKeywordConstants.IN, TOKEN_KEYWORD_IN)
            .put(IASKeywordConstants.DELETE, TOKEN_KEYWORD_DELETE)
            .put(IASKeywordConstants.TYPEOF, TOKEN_KEYWORD_TYPEOF)
            .put(IASKeywordConstants.CONST, TOKEN_KEYWORD_CONST)
            .put(IASKeywordConstants.GET, TOKEN_RESERVED_WORD_GET)
            .put(IASKeywordConstants.IMPLEMENTS, TOKEN_RESERVED_WORD_IMPLEMENTS)
            .put(IASKeywordConstants.IMPORT, TOKEN_KEYWORD_IMPORT)
            .put(IASKeywordConstants.USE, TOKEN_KEYWORD_USE)
            .put(IASKeywordConstants.EXTENDS, TOKEN_RESERVED_WORD_EXTENDS)
            .put(IASKeywordConstants.NEW, TOKEN_KEYWORD_NEW)
            .put(IASKeywordConstants.DYNAMIC, TOKEN_MODIFIER_DYNAMIC)
            .put(IASKeywordConstants.FINAL, TOKEN_MODIFIER_FINAL)
            .put(IASKeywordConstants.NATIVE, TOKEN_MODIFIER_NATIVE)
            .put(IASKeywordConstants.OVERRIDE, TOKEN_MODIFIER_OVERRIDE)
            .put(IASKeywordConstants.STATIC, TOKEN_MODIFIER_STATIC)
            .put(IASKeywordConstants.VIRTUAL, TOKEN_MODIFIER_VIRTUAL)
            .put(IASKeywordConstants.ABSTRACT, TOKEN_MODIFIER_ABSTRACT)
            .put(IASKeywordConstants.SET, TOKEN_RESERVED_WORD_SET)
            // Keywords with special token types that affect subsequent blocks
            .put(IASKeywordConstants.CATCH, TOKEN_KEYWORD_CATCH)
            .put(IASKeywordConstants.CLASS, TOKEN_KEYWORD_CLASS)
            .put(IASKeywordConstants.FUNCTION, TOKEN_KEYWORD_FUNCTION)
            .put(IASKeywordConstants.INTERFACE, TOKEN_KEYWORD_INTERFACE)
            .put(IASKeywordConstants.PACKAGE, TOKEN_KEYWORD_PACKAGE)
            // #120009: allow "var" inside parameter list, even though it's not 
            // valid AS (don't turn the subsequent function block open into a block open
            .put(IASKeywordConstants.VAR, TOKEN_KEYWORD_VAR)
            .put(IASKeywordConstants.FALSE, TOKEN_KEYWORD_FALSE)
            .put(IASKeywordConstants.NULL, TOKEN_KEYWORD_NULL)
            .put(IASKeywordConstants.TRUE, TOKEN_KEYWORD_TRUE)
            .put(IASKeywordConstants.PUBLIC, HIDDEN_TOKEN_BUILTIN_NS)
            .put(IASKeywordConstants.PRIVATE, HIDDEN_TOKEN_BUILTIN_NS)
            .put(IASKeywordConstants.PROTECTED, HIDDEN_TOKEN_BUILTIN_NS)
            .put(IASKeywordConstants.INTERNAL, HIDDEN_TOKEN_BUILTIN_NS)
            .put(IASKeywordConstants.INCLUDE, TOKEN_KEYWORD_INCLUDE)
            // Keywords for statements that affect subsequent blocks
            .put(IASKeywordConstants.DO, TOKEN_KEYWORD_DO)
            .put(IASKeywordConstants.WHILE, TOKEN_KEYWORD_WHILE)
            .put(IASKeywordConstants.BREAK, TOKEN_KEYWORD_BREAK)
            .put(IASKeywordConstants.CONTINUE, TOKEN_KEYWORD_CONTINUE)
            .put(IASKeywordConstants.GOTO, TOKEN_RESERVED_WORD_GOTO)
            .put(IASKeywordConstants.FOR, TOKEN_KEYWORD_FOR)
            .put(StreamingASTokenizer.FOR_EACH, TOKEN_KEYWORD_FOR)
            .put(IASKeywordConstants.EACH, TOKEN_RESERVED_WORD_EACH)
            .put(IASKeywordConstants.WITH, TOKEN_KEYWORD_WITH)
            .put(IASKeywordConstants.ELSE, TOKEN_KEYWORD_ELSE)
            .put(IASKeywordConstants.IF, TOKEN_KEYWORD_IF)
            .put(IASKeywordConstants.SWITCH, TOKEN_KEYWORD_SWITCH)
            .put(IASKeywordConstants.CASE, TOKEN_KEYWORD_CASE)
            .put(IASKeywordConstants.DEFAULT, TOKEN_KEYWORD_DEFAULT)
            .put(IASKeywordConstants.TRY, TOKEN_KEYWORD_TRY)
            .put(IASKeywordConstants.FINALLY, TOKEN_KEYWORD_FINALLY)
            // Keywords with a generic keyword token type that have no effect 
            // on subsequent blocks.
            .put(IASKeywordConstants.NAMESPACE, TOKEN_RESERVED_WORD_NAMESPACE)
            .put(IASKeywordConstants.CONFIG, TOKEN_RESERVED_WORD_CONFIG)
            .put(IASKeywordConstants.THROW, TOKEN_KEYWORD_THROW)
            .put(IASKeywordConstants.SUPER, TOKEN_KEYWORD_SUPER)
            .put(IASKeywordConstants.THIS, TOKEN_KEYWORD_THIS)
            .put(IASKeywordConstants.VOID, TOKEN_KEYWORD_VOID)
            .put(IASKeywordConstants.RETURN, TOKEN_KEYWORD_RETURN)
            .build();

    /**
     * Configuration for out tokenizer
     */
    private static final class TokenizerConfig
    {
        /**
         * Flag that lets us ignore keywords for more general string parsing
         */
        public boolean ignoreKeywords = false;

        /**
         * Flag that lets us be aware of metadata
         */
        public boolean findMetadata = true;

        /**
         * Flag indicating that we are tokenizing full content/files, and not
         * segments
         */
        public boolean completeContent = true;

        /**
         * IFilter for old APIs
         */
        public ITokenStreamFilter filter;

        /**
         * Flag indicating we should collect comments
         */
        public boolean collectComments = false;

        /**
         * Flag indicating we follow include statements, including their tokens
         */
        public boolean followIncludes = true;

        /**
         * Flag indicating if identifier naming is strictly enforced to consider
         * keywords or reserved words invalid, or if it's more lenient, like
         * newer ECMAScript versions introduced after ActionScript 3.
         */
        public boolean strictIdentifierNames = false;
    }

    private Reader reader;

    //underlying lexer
    private RawASTokenizer tokenizer;

    //last exception to prevent us from looping forever
    private Exception lastException = null;

    //LA buffer
    private final List<ASToken> lookAheadBuffer;
    private int bufferSize = 0; //maintain size ourselves since it's faster

    //last token we encountered, used for lookback
    private ASToken lastToken;

    private int offsetAdjustment; //for offset adjustment
    private int lineAdjustment = 0;
    private int columnAdjustment = 0;

    private IncludeHandler includeHandler;

    /**
     * The forked tokenizer for included files. If not null, {@link #next()}
     * will return a token from this tokenizer.
     * <p>
     * After all the tokens are returned from the included source file,
     * {@link #closeIncludeTokenizer()} closes the tokenizer and set this field
     * to null.
     */
    private StreamingASTokenizer forkIncludeTokenizer;

    /**
     * Flag to indicate if we have followed include statements
     */
    private boolean hasEncounteredIncludeStatements = false;

    private TokenizerConfig config;

    /**
     * Source file handler. This is used by resolving included file path.
     * {@link #StreamingASTokenizer(IFileSpecification)} and
     * {@link #StreamingASTokenizer(IFileSpecification, Stack)} sets the value.
     */
    private String sourcePath;

    /**
     * Lexer problems.
     * */
    private final List<ICompilerProblem> problems = new ArrayList<ICompilerProblem>();

    /**
     * Imaginary tokens generated for {@code asc -in} option.
     */
    private Iterator<ASToken> ascIncludeImaginaryTokens;

    /**
     * You should probably not use this constructor. There is some legacy code
     * that uses this constructor, but that code should be updated to use one of
     * the static create methods below.
     * <p>
     * TODO: make this private.
     */
    public StreamingASTokenizer(final Reader reader)
    {
        this();
        setReader(reader);
    }

    /**
     * A pool to reduce duplicated string literals created
     */
    private final HashMap<String, String> stringPool;

    /**
     * You should probably not use this constructor. There is a lot of code that
     * uses this constructor, but that code should be updated to use one of the
     * static create methods below.
     * <p>
     * TODO: make this private.
     */
    public StreamingASTokenizer()
    {
        tokenizer = new RawASTokenizer();
        config = new TokenizerConfig();
        lookAheadBuffer = new ArrayList<ASToken>(5);
        includeHandler = IncludeHandler.creatDefaultIncludeHandler();
        stringPool = new HashMap<String, String>();

        // Initialize string pool with keyword strings. The keyword strings 
        // are declared as constants which are automatically "interned".
        for (final String keyword : keywordToTokenMap.keySet())
        {
            stringPool.put(keyword, keyword);
        }
    }

    /**
     * Creates a tokenizer suitable for the mxml indexing code. fragments the
     * new tokenizer will tokenize.
     * 
     * @return A new tokenizer suitable for tokenizing script fragments in an
     * mxml document that is being tokenized for the full text search index.
     */
    public static StreamingASTokenizer createForMXMLIndexing(String fileName)
    {
        StreamingASTokenizer result = new StreamingASTokenizer();
        result.setPath(fileName);
        result.includeHandler.enterFile(result.sourcePath);
        return result;
    }

    /**
     * Fork a new tokenizer when an "include" directive is found. This method
     * will pass the {@code StructureTracker} of the current tokenizer down to
     * the forked tokenizer.
     * 
     * @param currentTokenizer Current tokenizer.
     * @param fileSpec File specification of the included file.
     * @param includeHandler Include handler.
     * @return A tokenizer for the included file.
     * @throws FileNotFoundException Error.
     */
    private static StreamingASTokenizer createForIncludeFile(
            final StreamingASTokenizer currentTokenizer,
            final IFileSpecification fileSpec,
            final IncludeHandler includeHandler)
            throws FileNotFoundException
    {
        final StreamingASTokenizer tokenizer = create(fileSpec, includeHandler);
        return tokenizer;
    }

    /**
     * Create a tokenizer from a source file. This is the lexer entry-point used
     * by {@link ASCompilationUnit}.
     * 
     * @param fileSpec File specification provides the reader and the file path.
     * @param includeHandler Include handler.
     * @throws FileNotFoundException error
     */
    protected static StreamingASTokenizer create(
            final IFileSpecification fileSpec,
            final IncludeHandler includeHandler)
            throws FileNotFoundException
    {
        assert fileSpec != null : "File specification can't be null.";
        assert includeHandler != null : "Include handler can't be null.";

        final StreamingASTokenizer tokenizer = new StreamingASTokenizer();
        tokenizer.setReader(fileSpec.createReader());
        tokenizer.setPath(fileSpec.getPath());
        tokenizer.includeHandler = includeHandler;
        tokenizer.includeHandler.enterFile(tokenizer.sourcePath);
        return tokenizer;
    }

    /**
     * Create a tokenizer for {@code ASParser#parseFile()}.
     * 
     * @param fileSpec File specification provides the reader and the file path.
     * @param includeHandler Include handler.
     * @param followIncludes True if included files are also parsed.
     * @param includedFiles A list of included file paths.
     * @return Lexer.
     * @throws FileNotFoundException error
     */
    protected static StreamingASTokenizer createForASParser(
            final IFileSpecification fileSpec,
            final IncludeHandler includeHandler,
            final boolean followIncludes,
            final List<String> includedFiles,
            final boolean strictIdentifierNames)
            throws FileNotFoundException
    {
        final StreamingASTokenizer tokenizer = create(fileSpec, includeHandler);
        tokenizer.setFollowIncludes(followIncludes);
        tokenizer.setStrictIdentifierNames(strictIdentifierNames);

        final ImmutableList.Builder<ASToken> imaginaryTokensBuilder =
                new ImmutableList.Builder<ASToken>();
        for (final String filename : includedFiles)
        {
            imaginaryTokensBuilder.add(new ASToken(
                    ASTokenTypes.TOKEN_KEYWORD_INCLUDE,
                    0,
                    0,
                    0,
                    0,
                    "include"));
            imaginaryTokensBuilder.add(new ASToken(
                    ASTokenTypes.TOKEN_LITERAL_STRING,
                    0,
                    0,
                    0,
                    0,
                    '"' + filename + '"'));
        }
        tokenizer.ascIncludeImaginaryTokens = imaginaryTokensBuilder.build().iterator();
        return tokenizer;
    }

    /**
     * This creator doesn't "enter file" on creation.
     */
    protected static StreamingASTokenizer createForInlineScriptScopeBuilding(
            final Reader reader,
            final String path,
            final IncludeHandler includeHandler,
            final int offsetAdjustment,
            final int lineAdjustment,
            final int columnAdjustment)
    {
        assert reader != null : "Reader can't be null";
        assert path != null : "Path can't be null";
        assert includeHandler != null : "IncludeHandler can't be null";

        final StreamingASTokenizer tokenizer = new StreamingASTokenizer();
        tokenizer.setReader(reader);
        tokenizer.setPath(path);
        tokenizer.includeHandler = includeHandler;
        tokenizer.setSourcePositionAdjustment(
                offsetAdjustment, lineAdjustment, columnAdjustment);
        return tokenizer;
    }

    /**
     * Create a tokenizer to parse an Expression.
     */
    protected static StreamingASTokenizer createForInlineExpressionParsing(
            final Reader reader,
            final String path
            )
    {
        assert reader != null : "Reader can't be null";
        assert path != null : "Path can't be null";

        final StreamingASTokenizer tokenizer = new StreamingASTokenizer();
        tokenizer.setReader(reader);
        tokenizer.setPath(path);
        tokenizer.includeHandler.enterFile(path);

        // Have to do this to get the tokenizer to work right - some things, like function expressions,
        // won't tokenize correctly unless the last token is '=' or some other special tokens.
        tokenizer.lastToken = new ASToken(ASTokenTypes.TOKEN_OPERATOR_ASSIGNMENT, -1, -1, -1, -1, "=");

        return tokenizer;
    }

    /**
     * This method can create a {@code StreamingASTokenizer} with optional
     * "follow includes". If {@code IncludeHandler} is not null, it will follow
     * {@code include} directives.
     * 
     * @param reader Input to the tokenizer.
     * @param path File path of the input.
     * @param includeHandler If not null, the created tokenizer will follow
     * {@code include} directives.
     * @return A {@code StreamingASTokenizer}.
     */
    public static StreamingASTokenizer createForRepairingASTokenizer(
            final Reader reader,
            final String path,
            final IncludeHandler includeHandler)
    {
        assert path != null || includeHandler == null : "We need a source path to follow includes";
        final StreamingASTokenizer tokenizer = new StreamingASTokenizer();
        tokenizer.setReader(reader);
        tokenizer.setPath(path);
        if (includeHandler != null)
        {
            tokenizer.includeHandler = includeHandler;
            includeHandler.enterFile(path);
        }
        return tokenizer;
    }

    /**
     * Sets the {@link Reader} that supplies the content to this tokenizer. It
     * is up to the client to close any previous readers that have been in use.
     * It is also up to the client to close the reader once it has been used
     * 
     * @param reader a {@link Reader}
     */
    public void setReader(final Reader reader)
    {
        setReader(reader, 0, 0, 0);
    }

    /**
     * Sets the {@link Reader} that supplies the content to this tokenizer. It
     * is up to the client to close any previous readers that have been in use.
     * It is also up to the client to close the reader once it has been used
     * 
     * @param reader a {@link Reader}
     * @param offset Offset adjustment. If the specified reader is reading from
     * a string extracted from a source file, this should be the offset of the
     * first character read from the reader in the source file.
     * @param line Line adjustment.
     * @param column Column adjustment
     */
    public void setReader(final Reader reader, int offset, int line, int column)
    {
        this.reader = reader;
        tokenizer = new RawASTokenizer();
        tokenizer.yyreset(reader);
        tokenizer.setCollectComments(config.collectComments);
        tokenizer.setSourcePath(sourcePath);
        setSourcePositionAdjustment(offset, line, column);
    }

    /**
     * Sets the path to the file this tokenizer is scanning
     * 
     * @param path a file path
     */
    @Override
    public void setPath(String path)
    {
        assert path != null : "path of tokenizer shouldn't be null";
        sourcePath = path;
        tokenizer.setSourcePath(path);
    }

    /**
     * Allows for the adjustment of offset, line and column information when
     * parsing subsequences of text. This should be called before tokenization
     * has started
     * 
     * @param offset The offset where the fragment starts.
     * @param line The line where the fragment starts. This should be a
     * ZERO-based line number
     * @param column The column where the fragment starts. This should be a
     * ZERO-based column number
     */
    public void setSourcePositionAdjustment(int offset, int line, int column)
    {
        offsetAdjustment = offset;
        lineAdjustment = line;
        columnAdjustment = column;
    }

    /**
     * Sets whether we comments are collected: single line and multi-line.
     * Default is <code>false</code>
     * 
     * @param collect true if we should collect comments
     */
    @Override
    public void setCollectComments(final boolean collect)
    {
        config.collectComments = collect;
        
        if (tokenizer != null)
            tokenizer.setCollectComments(collect);
    }

    /**
     * Sets whether we follow include statements, including their tokens.
     * Default is <code>true</code>
     * 
     * @param followIncludes true if we should follow includes
     */
    @Override
    public void setFollowIncludes(final boolean followIncludes)
    {
        config.followIncludes = followIncludes;
    }

    public void setStrictIdentifierNames(boolean value)
    {
        config.strictIdentifierNames = value;
    }

    /**
     * Closes the underlying reader
     */
    @Override
    public void close() throws IOException
    {
        if (tokenizer != null)
        {
            tokenizer.reset();
            tokenizer.yyclose(); //close the reader
        }
    }

    /**
     * Sets whether we ignore keywords while scanning. Default is
     * <code>false</code>
     * 
     * @param ignore true if we should ignore keywords
     */
    public void setIgnoreKeywords(final boolean ignore)
    {
        config.ignoreKeywords = ignore;
    }

    /**
     * Sets whether we are scanning a full file, or a fragment. Default is
     * <code>true</code>
     * 
     * @param full true if we are scanning a full file.
     */
    public void setScanningFullContent(final boolean full)
    {
        config.completeContent = full;
    }

    /**
     * Sets whether we will find metadata constructs Default is
     * <code>true</code>
     * 
     * @param aware true if we will find metadata
     */
    public void setIsMetadataAware(final boolean aware)
    {
        config.findMetadata = aware;
    }

    /**
     * Sets the {@link ITokenStreamFilter} used to filter out unwanted tokens
     * 
     * @param filter the token filter to alter the stream returned from the
     * tokenizer
     */
    public void setTokenFilter(ITokenStreamFilter filter)
    {
        config.filter = filter;
    }

    /**
     * Sets the include handler used by this tokenizer to get
     * {@link IFileSpecification} for included files.
     * 
     * @param handler {@link IncludeHandler} this tokenizer should use.
     */
    public void setIncludeHandler(IncludeHandler handler)
    {
        includeHandler = handler;
    }

    /**
     * Indicated that we have tokenization problems. Can be called once scanning
     * has begun
     * 
     * @return true if problems have been encountered
     */
    public boolean hasTokenizationProblems()
    {
        return tokenizer.hasProblems() || problems.size() > 0;
    }

    /**
     * Indicated whether this tokenizer has encountered include statements,
     * regardless of whether it is set to follow them or not
     * 
     * @return true if we have encountered includes
     */
    public boolean hasEncounteredIncludeStatements()
    {
        return hasEncounteredIncludeStatements;
    }

    /**
     * Returns a collection of problems that have been encountered while
     * scanning.
     * 
     * @return a list of problems, never null
     */
    public List<ICompilerProblem> getTokenizationProblems()
    {
        ArrayList<ICompilerProblem> problems = new ArrayList<ICompilerProblem>(this.problems);
        problems.addAll(tokenizer.getProblems());
        return problems;
    }

    public ASToken[] getTokens(final Reader reader, ITokenStreamFilter filter)
    {
        setReader(reader);
        List<ASToken> tokenList = initializeTokenList(reader);
        ASToken token = null;
        do
        {
            token = next();
            if (token != null && filter.accept(token))
                tokenList.add(token.clone()); //make a copy because of object pool
        }
        while (token != null);
        return tokenList.toArray(new ASToken[0]);
    }

    @Override
    public ASToken[] getTokens(final Reader reader)
    {
        if (config.filter != null)
            return getTokens(reader, config.filter);
        setReader(reader);
        List<ASToken> tokenList = initializeTokenList(reader);
        ASToken token = null;
        do
        {
            token = next();
            if (token != null)
                tokenList.add(token.clone()); //copy ctor because of object pool
        }
        while (token != null);
        return tokenList.toArray(new ASToken[0]);
    }

    /**
     * @param reader
     * @return
     */
    private List<ASToken> initializeTokenList(final Reader reader)
    {
        List<ASToken> tokenList;
        int listSize = 8012;
        if (reader instanceof NonLockingStringReader)
        {
            //we know the length of this string.  For string of length x, their are roughly x/5 tokens that
            //can be constructed from that string.  size the array appropriately.
            listSize = 5;
            if (((NonLockingStringReader)reader).getLength() > 0)
            {
                listSize = Math.max((int)((NonLockingStringReader)reader).getLength() / 5, 5);
            }

        }
        else if (reader instanceof ILengthAwareReader && ((ILengthAwareReader)reader).getInputType() == InputType.FILE)
        {
            listSize = 9;
            if (((ILengthAwareReader)reader).getLength() > 0)
            {
                listSize = Math.max((int)((ILengthAwareReader)reader).getLength() / 9, 9);

            }
        }
        tokenList = new ArrayList<ASToken>(listSize);
        return tokenList;
    }

    @Override
    public IASToken[] getTokens(final String range)
    {
        return getTokens(new NonLockingStringReader(range));
    }

    /**
     * Returns the next token that can be produced from the underlying reader
     * 
     * @param filter an {@link ITokenStreamFilter} to restrict the tokens that
     * are returned
     * @return an ASToken, or null if no more tokens can be produced
     */
    public final ASToken next(final ITokenStreamFilter filter)
    {
        ASToken retVal = null;
        while (true)
        {
            retVal = next();
            if (retVal == null || filter.accept(retVal))
            {
                break;
            }
        }
        return retVal;
    }

    /**
     * Returns the next token that can be produced from the underlying reader.
     * <p>
     * If the forked "include file tokenizer" is open (not null), return the
     * next token from it. If the forked tokenizer reaches the end of the
     * included file, close (set to null) the forked tokenizer and return token
     * from the main source file.
     * 
     * @return an ASToken, or null if no more tokens can be produced
     */
    public final ASToken next()
    {
        ASToken retVal = null;
        // If the lexer for the included file is open, read from the included tokenizer.
        boolean consumeSemi = false;
        try
        {
            // Return token from the main file.
            if (forkIncludeTokenizer != null)
            {
                retVal = forkIncludeTokenizer.next();

                // Check if the forked tokenizer reached EOF. 
                if (retVal == null)
                {
                    closeIncludeTokenizer();
                    // We should consume the next semicolon we find.
                    // Most include statements are terminated with a semicolon,
                    // and because we read the contents of the included file,
                    // this could cause problems with a semicolon in a place
                    // we don't want it.
                    consumeSemi = true; 
                }
                else
                    return retVal;
            }
            if (bufferSize > 0)
            {
                retVal = lookAheadBuffer.remove(0);
                bufferSize--;
            }
            else
            {
                retVal = nextTokenFromReader();

            }
            if (retVal == null)
                return null;
            final int tokenType = retVal.getType();

            switch (tokenType)
            {
                // if we're seeing each in this part of the loop, it's not a
                // syntactic keyword
                // since we do lookahead when we see "for", checking for "each"
                case TOKEN_RESERVED_WORD_EACH:
                    treatKeywordAsIdentifier(retVal);
                    processUserDefinedNamespace(retVal, 0);
                    return retVal;
                case TOKEN_KEYWORD_INCLUDE:
                {
                    if (!config.strictIdentifierNames && lastToken != null)
                    {
                        int lastTokenType = lastToken.getType();
                        switch (lastTokenType)
                        {
                            case TOKEN_KEYWORD_VAR:
                            case TOKEN_KEYWORD_FUNCTION:
                            case TOKEN_RESERVED_WORD_GET:
                            case TOKEN_RESERVED_WORD_SET:
                            case TOKEN_OPERATOR_MEMBER_ACCESS:
                            {
                                retVal.setType(TOKEN_IDENTIFIER);
                                return retVal;
                            }
                        }
                    }
                    // "followIncludes=false" is usually used for code model
                    // partitioner. They want the "include" token.
                    if (!config.followIncludes)
                        return retVal;

                    final ASToken token = LT(1);

                    // "include" at EOF is always a keyword
                    if (token == null)
                        return retVal;

                    if (!matches(token, TOKEN_LITERAL_STRING))
                    {
                        treatKeywordAsIdentifier(retVal); // it's an identifier
                        processUserDefinedNamespace(retVal, 0);
                    }
                    else
                    {
                        hasEncounteredIncludeStatements = true;
                        // Consume the file path after the include token.
                        consume(1);
                        final String filenameTokenText = token.getText();
                        final String includeString = filenameTokenText.substring(1, filenameTokenText.length() - 1);

                        if (sourcePath == null)
                            throw new NullPointerException("Source file is needed for resolving included file path.");
                        IFileSpecification includedFileSpec = null;
                        //respond to problems from our file handler
                        includedFileSpec = includeHandler.getFileSpecificationForInclude(sourcePath, includeString);
                        //
                        if (includedFileSpec == null)
                        {
                            ICompilerProblem problem = new FileNotFoundProblem(token, filenameTokenText); //the text will be the path not found
                            problems.add(problem);
                            retVal = next();
                            return retVal;
                        }
                        if (includeHandler.isCyclicInclude(includedFileSpec.getPath()))
                        {
                            ICompilerProblem problem = new CyclicalIncludesProblem(token);
                            problems.add(problem);
                            retVal = next();
                            return retVal;
                        }
                        else
                        {
                            // Fork a tokenizer for the included file
                            try
                            {
                                forkIncludeTokenizer = createForIncludeFile(this, includedFileSpec, includeHandler);
                                retVal = forkIncludeTokenizer.next();
                            }
                            catch (FileNotFoundException fnfe)
                            {
                                includeHandler.handleFileNotFound(includedFileSpec);
                                ICompilerProblem problem = new FileNotFoundProblem(token, includedFileSpec.getPath());
                                problems.add(problem);
                                retVal = next();
                                return retVal;
                            }
                        }
                    }

                    // Recover from compiler problems and continue.
                    if (retVal == null)
                    {
                        // Included file is empty. 
                        closeIncludeTokenizer();
                        // Fall back to main source.
                        retVal = this.next();
                    }
                    return retVal;
                }
                case TOKEN_RESERVED_WORD_CONFIG:
                    if (matches(LT(1), TOKEN_RESERVED_WORD_NAMESPACE))
                    { //we config namespace
                        retVal.setType(TOKEN_RESERVED_WORD_CONFIG);
                        return retVal;
                    }
                    treatKeywordAsIdentifier(retVal); //identifier
                    processUserDefinedNamespace(retVal, 0);
                    return retVal;
                case HIDDEN_TOKEN_BUILTIN_NS:
                    if (matches(LT(1), TOKEN_OPERATOR_NS_QUALIFIER))
                    { //we have public:: and this structure is not an annotation but a name ref
                        retVal.setType(TOKEN_NAMESPACE_NAME);
                        return retVal;
                    }
                    retVal.setType(TOKEN_NAMESPACE_ANNOTATION);
                    return retVal;
                case TOKEN_MODIFIER_DYNAMIC:
                case TOKEN_MODIFIER_FINAL:
                case TOKEN_MODIFIER_NATIVE:
                case TOKEN_MODIFIER_OVERRIDE:
                case TOKEN_MODIFIER_STATIC:
                case TOKEN_MODIFIER_VIRTUAL:
                case TOKEN_MODIFIER_ABSTRACT:
                {
                    // previous token is either a modifier or a namespace, or if
                    // null, assume keyword
                    // next token is from a definition or a modifier or a namespace
                    final ASToken nextToken = LT(1);
                    if (nextToken != null)
                    {
                        switch (nextToken.getType())
                        {
                            case TOKEN_KEYWORD_CLASS:
                            case TOKEN_KEYWORD_FUNCTION:
                            case TOKEN_KEYWORD_INTERFACE:
                            case TOKEN_RESERVED_WORD_NAMESPACE:
                            case TOKEN_KEYWORD_VAR:
                            case TOKEN_KEYWORD_CONST:
                            case TOKEN_MODIFIER_DYNAMIC:
                            case TOKEN_MODIFIER_FINAL:
                            case TOKEN_MODIFIER_NATIVE:
                            case TOKEN_MODIFIER_OVERRIDE:
                            case TOKEN_MODIFIER_STATIC:
                            case TOKEN_MODIFIER_VIRTUAL:
                            case TOKEN_MODIFIER_ABSTRACT:
                            case TOKEN_NAMESPACE_ANNOTATION:
                            case TOKEN_NAMESPACE_NAME:
                            case HIDDEN_TOKEN_BUILTIN_NS:
                                return retVal;
                            case TOKEN_IDENTIFIER:
                                if (isUserDefinedNamespace(nextToken, 1)) // we're already looking ahead one so make sure we look ahead one further
                                    return retVal;
                            default:
                                // Not applicable to other token types.
                                break;
                        }
                    }
                    treatKeywordAsIdentifier(retVal);
                    processUserDefinedNamespace(retVal, 0);
                    return retVal;
                }
                    //we combine +/- for numeric literals here
                case TOKEN_OPERATOR_MINUS:
                case TOKEN_OPERATOR_PLUS:
                {
                    if (lastToken == null || !lastToken.canPreceedSignedOperator())
                    {
                        final ASToken nextToken = LT(1);
                        if (nextToken != null)
                        {
                            switch (nextToken.getType())
                            {
                                case TOKEN_LITERAL_NUMBER:
                                case TOKEN_LITERAL_HEX_NUMBER:
                                    retVal.setEnd(nextToken.getEnd());
                                    retVal.setEndLine(nextToken.getEndLine());
                                    retVal.setEndColumn(nextToken.getEndColumn());
                                    final StringBuilder builder = new StringBuilder(retVal.getText());
                                    builder.append(nextToken.getText());
                                    retVal.setText(poolString(builder.toString()));
                                    consume(1);
                                    retVal.setType(nextToken.getType());
                                    break;
                                default:
                                    // ignore other tokens
                                    break;
                            }
                        }
                    }

                    return retVal;
                }
                    //RECOGNIZE: for each
                case TOKEN_KEYWORD_FOR:
                {
                    final ASToken token = LT(1);
                    if (matches(token, TOKEN_RESERVED_WORD_EACH))
                    {
                        retVal.setEnd(token.getEnd());
                        retVal.setText(FOR_EACH);
                        consume(1);
                        return retVal;
                    }
                    if (!config.strictIdentifierNames && lastToken != null)
                    {
                        int lastTokenType = lastToken.getType();
                        switch (lastTokenType)
                        {
                            case TOKEN_KEYWORD_VAR:
                            case TOKEN_KEYWORD_FUNCTION:
                            case TOKEN_RESERVED_WORD_GET:
                            case TOKEN_RESERVED_WORD_SET:
                            case TOKEN_OPERATOR_MEMBER_ACCESS:
                                retVal.setType(TOKEN_IDENTIFIER);
                        }
                    }
                    return retVal;
                }
                    //RECOGNIZE: default xml namespace
                    //default xml namespace must exist on the same line
                case TOKEN_KEYWORD_DEFAULT:
                {
                    final ASToken maybeNS = LT(2);
                    final boolean foundTokenNamespace = maybeNS != null &&
                                                        maybeNS.getType() == TOKEN_RESERVED_WORD_NAMESPACE;
                    final ASToken maybeXML = LT(1);
                    if (foundTokenNamespace)
                    {
                        final boolean foundTokenXML = maybeXML != null &&
                                                      maybeXML.getType() == TOKEN_IDENTIFIER &&
                                                      XML.equals(maybeXML.getText());
                        if (!foundTokenXML)
                        {
                            final ICompilerProblem problem =
                                    new ExpectXmlBeforeNamespaceProblem(maybeNS);
                            problems.add(problem);
                        }

                        //combine all of these tokens together
                        retVal.setEnd(maybeNS.getEnd());
                        retVal.setText(DEFAULT_XML_NAMESPACE);
                        retVal.setType(TOKEN_DIRECTIVE_DEFAULT_XML);
                        consume(2);
                    }
                    // if this isn't "default xml namespace" then
                    // see if it is the default case in a switch
                    // otherwise, assume it is an identiferName
                    else if (!config.strictIdentifierNames &&
                            maybeXML != null && 
                            maybeXML.getType() != TOKEN_COLON)
                        retVal.setType(TOKEN_IDENTIFIER);
                    else if (!config.strictIdentifierNames && lastToken != null)
                    {
                        int lastTokenType = lastToken.getType();
                        switch (lastTokenType)
                        {
                            case TOKEN_KEYWORD_VAR:
                            case TOKEN_KEYWORD_FUNCTION:
                            case TOKEN_RESERVED_WORD_GET:
                            case TOKEN_RESERVED_WORD_SET:
                            case TOKEN_OPERATOR_MEMBER_ACCESS:
                                retVal.setType(TOKEN_IDENTIFIER);
                        }
                    }
                    return retVal;
                }
                case TOKEN_KEYWORD_VOID:
                {
                    //check for void 0
                    final ASToken token = LT(1);
                    if (matches(token, TOKEN_LITERAL_NUMBER) && ZERO.equals(token.getText()))
                    {
                        retVal.setType(TOKEN_VOID_0);
                        combineText(retVal, token);
                        consume(1);
                    }
                    //check for void(0)
                    else if (matches(token, TOKEN_PAREN_OPEN))
                    {
                        final ASToken zeroT = LT(2);
                        if (matches(zeroT, TOKEN_LITERAL_NUMBER) && ZERO.equals(zeroT.getText()))
                        {
                            final ASToken closeParenT = LT(3);
                            if (matches(closeParenT, TOKEN_PAREN_CLOSE))
                            {
                                combineText(retVal, token);
                                combineText(retVal, zeroT);
                                combineText(retVal, closeParenT);
                                retVal.setType(TOKEN_VOID_0);
                                consume(3);
                            }
                        }
                    }
                    return retVal;
                }
                case TOKEN_IDENTIFIER:
                {
                    //check for user-defined namespace before we return anything
                    processUserDefinedNamespace(retVal, 0);
                    return retVal;
                }
                    //this is for metadata processing
                case TOKEN_SQUARE_OPEN:
                {
                    retVal = tryParseMetadata(retVal);
                    return retVal;
                }
                case HIDDEN_TOKEN_STAR_ASSIGNMENT:
                {
                    //this is to solve an ambiguous case, where we can't tell the difference between 
                    //var foo:*=null and foo *= null;
                    retVal.setType(TOKEN_OPERATOR_STAR);
                    retVal.setEnd(retVal.getEnd() - 1);
                    retVal.setText("*");
                    //add the equals
                    final ASToken nextToken = tokenizer.buildToken(TOKEN_OPERATOR_ASSIGNMENT,
                                retVal.getEnd() + 1, retVal.getEnd() + 2,
                                retVal.getLine(), retVal.getColumn(), "=");
                    nextToken.setSourcePath(sourcePath);
                    addTokenToBuffer(nextToken);
                    return retVal;
                }
                case TOKEN_SEMICOLON:
                    if (consumeSemi)
                    {
                        return next();
                    }
                    return retVal;
                case TOKEN_VOID_0:
                case TOKEN_LITERAL_REGEXP:
                case TOKEN_COMMA:
                case TOKEN_COLON:
                case TOKEN_PAREN_OPEN:
                case TOKEN_PAREN_CLOSE:
                case TOKEN_SQUARE_CLOSE:
                case TOKEN_ELLIPSIS:
                case TOKEN_OPERATOR_PLUS_ASSIGNMENT:
                case TOKEN_OPERATOR_MINUS_ASSIGNMENT:
                case TOKEN_OPERATOR_MULTIPLICATION_ASSIGNMENT:
                case TOKEN_OPERATOR_DIVISION_ASSIGNMENT:
                case TOKEN_OPERATOR_MODULO_ASSIGNMENT:
                case TOKEN_OPERATOR_BITWISE_AND_ASSIGNMENT:
                case TOKEN_OPERATOR_BITWISE_OR_ASSIGNMENT:
                case TOKEN_OPERATOR_BITWISE_XOR_ASSIGNMENT:
                case TOKEN_OPERATOR_BITWISE_LEFT_SHIFT_ASSIGNMENT:
                case TOKEN_OPERATOR_BITWISE_RIGHT_SHIFT_ASSIGNMENT:
                case TOKEN_OPERATOR_BITWISE_UNSIGNED_RIGHT_SHIFT_ASSIGNMENT:
                case TOKEN_OPERATOR_STAR:
                case TOKEN_OPERATOR_NS_QUALIFIER:
                case TOKEN_ASDOC_COMMENT:
                case TOKEN_OPERATOR_DIVISION:
                case TOKEN_OPERATOR_MODULO:
                case TOKEN_OPERATOR_BITWISE_LEFT_SHIFT:
                case TOKEN_OPERATOR_BITWISE_RIGHT_SHIFT:
                case TOKEN_OPERATOR_BITWISE_UNSIGNED_RIGHT_SHIFT:
                case TOKEN_OPERATOR_LESS_THAN:
                case TOKEN_OPERATOR_GREATER_THAN:
                case TOKEN_OPERATOR_LESS_THAN_EQUALS:
                case TOKEN_OPERATOR_GREATER_THAN_EQUALS:
                case TOKEN_OPERATOR_EQUAL:
                case TOKEN_OPERATOR_NOT_EQUAL:
                case TOKEN_OPERATOR_STRICT_EQUAL:
                case TOKEN_OPERATOR_STRICT_NOT_EQUAL:
                case TOKEN_OPERATOR_BITWISE_AND:
                case TOKEN_OPERATOR_BITWISE_XOR:
                case TOKEN_OPERATOR_BITWISE_OR:
                case TOKEN_OPERATOR_LOGICAL_AND:
                case TOKEN_OPERATOR_LOGICAL_OR:
                case TOKEN_OPERATOR_LOGICAL_AND_ASSIGNMENT:
                case TOKEN_OPERATOR_LOGICAL_OR_ASSIGNMENT:
                case TOKEN_OPERATOR_NULLISH_COALESCING:
                case TOKEN_TYPED_COLLECTION_OPEN:
                case TOKEN_TYPED_COLLECTION_CLOSE:
                case TOKEN_OPERATOR_MEMBER_ACCESS:
                case TOKEN_RESERVED_WORD_NAMESPACE:
                case TOKEN_RESERVED_WORD_GET:
                case TOKEN_RESERVED_WORD_SET:
                case TOKEN_OPERATOR_ASSIGNMENT:
                case TOKEN_TYPED_LITERAL_CLOSE:
                case TOKEN_TYPED_LITERAL_OPEN:
                case TOKEN_OPERATOR_TERNARY:
                case TOKEN_OPERATOR_DECREMENT:
                case TOKEN_OPERATOR_INCREMENT:
                case TOKEN_OPERATOR_ATSIGN:
                case TOKEN_OPERATOR_BITWISE_NOT:
                case TOKEN_OPERATOR_LOGICAL_NOT:
                case TOKEN_E4X_BINDING_CLOSE:
                case TOKEN_E4X_BINDING_OPEN:
                case TOKEN_OPERATOR_DESCENDANT_ACCESS:
                case TOKEN_OPERATOR_NULL_CONDITIONAL_ACCESS:
                case TOKEN_NAMESPACE_ANNOTATION:
                case TOKEN_NAMESPACE_NAME:
                case TOKEN_BLOCK_OPEN:
                case TOKEN_BLOCK_CLOSE:
                case TOKEN_KEYWORD_FUNCTION:
                    return retVal;
                case HIDDEN_TOKEN_MULTI_LINE_COMMENT:
                case HIDDEN_TOKEN_SINGLE_LINE_COMMENT:
                    if (tokenizer.isCollectingComments())
                    {
                        return retVal;
                    }
                    assert (false);
                    return null;
                case TOKEN_KEYWORD_INSTANCEOF:
                case TOKEN_KEYWORD_AS:
                case TOKEN_KEYWORD_IN:
                case TOKEN_KEYWORD_IS:
                    if(!config.strictIdentifierNames)
                    {
                        if (lastToken != null)
                        {
                            int lastTokenType = lastToken.getType();
                            switch (lastTokenType)
                            {
                                case TOKEN_SEMICOLON:
                                case TOKEN_BLOCK_OPEN:
                                case TOKEN_COMMA:
                                    retVal.setType(TOKEN_IDENTIFIER);
                                    return retVal;
                            }
                        }
                        else 
                        {
                            // we are first token so assume identifier
                            retVal.setType(TOKEN_IDENTIFIER);
                            return retVal;
                        }
                    }
                    // and fall through
                case TOKEN_KEYWORD_DELETE:
                    if(!config.strictIdentifierNames)
                    {
                        ASToken nextToken = LT(1);
                        if (nextToken != null)
                        {
                            int nextTokenType = nextToken.getType();
                            switch (nextTokenType)
                            {
                                // if followed by a token assume it is the
                                // keyword and not the identiferName;
                                case TOKEN_IDENTIFIER:
                                    return retVal;
                                // followed by a comma or semicolon
                                // probably being used in an expression
                                case TOKEN_COMMA:
                                case TOKEN_SEMICOLON:
                                    retVal.setType(TOKEN_IDENTIFIER);
                                    return retVal;
                            }
                        }
                    }
                    // and fall through
                case TOKEN_KEYWORD_BREAK:
                case TOKEN_KEYWORD_CASE:
                case TOKEN_KEYWORD_CATCH:
                case TOKEN_KEYWORD_CLASS:
                case TOKEN_KEYWORD_CONST:
                case TOKEN_KEYWORD_CONTINUE:
                case TOKEN_KEYWORD_DO:
                case TOKEN_KEYWORD_ELSE:
                case TOKEN_KEYWORD_FALSE:
                case TOKEN_KEYWORD_FINALLY:
                case TOKEN_KEYWORD_IF:
                case TOKEN_KEYWORD_IMPORT:
                case TOKEN_KEYWORD_INTERFACE:
                case TOKEN_KEYWORD_NULL:
                case TOKEN_KEYWORD_PACKAGE:
                case TOKEN_KEYWORD_SUPER:
                case TOKEN_KEYWORD_SWITCH:
                case TOKEN_KEYWORD_THIS:
                case TOKEN_KEYWORD_TRUE:
                case TOKEN_KEYWORD_TRY:
                case TOKEN_KEYWORD_TYPEOF:
                case TOKEN_KEYWORD_USE:
                case TOKEN_KEYWORD_VAR:
                case TOKEN_KEYWORD_WHILE:
                case TOKEN_KEYWORD_WITH:
                case TOKEN_KEYWORD_RETURN:
                case TOKEN_KEYWORD_THROW:
                case TOKEN_KEYWORD_NEW:
                    if (!config.strictIdentifierNames && lastToken != null)
                    {
                        int lastTokenType = lastToken.getType();
                        switch (lastTokenType)
                        {
                            case TOKEN_KEYWORD_VAR:
                            case TOKEN_KEYWORD_FUNCTION:
                            case TOKEN_RESERVED_WORD_GET:
                            case TOKEN_RESERVED_WORD_SET:
                            case TOKEN_OPERATOR_MEMBER_ACCESS:
                                retVal.setType(TOKEN_IDENTIFIER);
                        }
                    }
                    return retVal;
                default:
                    if (ASToken.isE4X(tokenType))
                        return retVal;

                    if (retVal.isKeywordOrContextualReservedWord() || retVal.isLiteral())
                        return retVal;

                    // If we reach here, the token fails to match any processing logic.
                    final UnexpectedTokenProblem problem = new UnexpectedTokenProblem(
                            retVal,
                            ASTokenKind.UNKNOWN);
                    problems.add(problem);
            }
        }
        catch (final Exception e)
        {
            if (lastException != null)
            {
                if (lastException.getClass().isInstance(e))
                {
                    ICompilerProblem problem = new InternalCompilerProblem2(sourcePath, e, "StreamingASTokenizer");
                    problems.add(problem);
                    return null;
                }
            }
            else
            {
                lastException = e;
                retVal = null;
                return next();
            }
        }
        finally
        {
            consumeSemi = false;
            lastToken = retVal;
        }
        return null;
    }

    /**
     * Error recovery: convert the given keyword token into an identifier token,
     * and log a syntax error.
     * 
     * @param token Keyword token.
     */
    private void treatKeywordAsIdentifier(final ASToken token)
    {
        assert token != null : "token can't be null";
        assert token.isKeywordOrContextualReservedWord() : "only transfer reserved words";

        if (token.isKeyword())
        {
            final UnexpectedTokenProblem problem = new UnexpectedTokenProblem(token, ASTokenKind.IDENTIFIER);
            problems.add(problem);
        }
        token.setType(TOKEN_IDENTIFIER);
    }

    /**
     * Decide within the current context whether the following content can be
     * parsed as a metadata tag token.
     * 
     * @param nextToken The next token coming from
     * {@link #nextTokenFromReader()}.
     * @return If the following content can be a metadata tag, the result is a
     * token of type {@link ASTokenTypes#TOKEN_ATTRIBUTE}. Otherwise, the
     * argument {@code nextToken} is returned.
     * @throws Exception Parsing error.
     */
    private ASToken tryParseMetadata(ASToken nextToken) throws Exception
    {
        // Do not initialize this variable so that Java flow-analysis can check if
        // the following rules cover all the possibilities.
        final boolean isNextMetadata;

        if (!config.findMetadata)
        {
            // The lexer is configured to not recognize metadata.
            isNextMetadata = false;
        }
        else if (lastToken == null)
        {
            // An "[" at the beginning of a script is always a part of a metadata.
            isNextMetadata = true;
        }
        else
        {
            switch (lastToken.getType())
            {
                case HIDDEN_TOKEN_SINGLE_LINE_COMMENT:
                case HIDDEN_TOKEN_MULTI_LINE_COMMENT:
                case TOKEN_ASDOC_COMMENT:
                case TOKEN_SEMICOLON:
                case TOKEN_ATTRIBUTE:
                case TOKEN_BLOCK_OPEN:
                    // "[" after these tokens are always part of a metadata token.
                    isNextMetadata = true;
                    break;

                case TOKEN_SQUARE_CLOSE:
                case TOKEN_IDENTIFIER:
                    // "[" following a "]" is an array access.
                    // "[" following an identifier is an array access.
                    isNextMetadata = false;
                    break;
                    
                case TOKEN_KEYWORD_INCLUDE:
                case TOKEN_BLOCK_CLOSE:
                case TOKEN_OPERATOR_STAR:
                    // "[" after these tokens are part of a metadata token, if
                    // the "[" is on a new line.
                    isNextMetadata = !lastToken.matchesLine(nextToken);
                    break;

                default:
                    // If we are lexing an entire file
                    // then at this point we "know" that the next token
                    // is not meta-data.
                    if (config.completeContent)
                    {
                        isNextMetadata = false;
                    }
                    else
                    {
                        // In "fragment" mode which is used by the syntax coloring code
                        // in builder, we assume the following list of tokens can not
                        // precede meta-data because they all start or occur in expressions.
                        switch (lastToken.getType())
                        {
                            case TOKEN_OPERATOR_EQUAL:
                            case TOKEN_OPERATOR_TERNARY:
                            case TOKEN_COLON:
                            case TOKEN_OPERATOR_PLUS:
                            case TOKEN_OPERATOR_MINUS:
                            case TOKEN_OPERATOR_STAR:
                            case TOKEN_OPERATOR_DIVISION:
                            case TOKEN_OPERATOR_MODULO:
                            case TOKEN_OPERATOR_BITWISE_AND:
                            case TOKEN_OPERATOR_BITWISE_OR:
                            case TOKEN_OPERATOR_NULLISH_COALESCING:
                            case TOKEN_KEYWORD_AS:
                            case TOKEN_OPERATOR_BITWISE_XOR:
                            case TOKEN_OPERATOR_LOGICAL_AND:
                            case TOKEN_OPERATOR_LOGICAL_OR:
                            case TOKEN_PAREN_OPEN:
                            case TOKEN_COMMA:
                            case TOKEN_OPERATOR_BITWISE_NOT:
                            case TOKEN_OPERATOR_LOGICAL_NOT:
                            case TOKEN_OPERATOR_ASSIGNMENT:
                            case TOKEN_OPERATOR_BITWISE_LEFT_SHIFT:
                            case TOKEN_OPERATOR_BITWISE_RIGHT_SHIFT:
                            case TOKEN_OPERATOR_BITWISE_UNSIGNED_RIGHT_SHIFT:
                            case TOKEN_OPERATOR_LESS_THAN:
                            case TOKEN_OPERATOR_GREATER_THAN:
                            case TOKEN_OPERATOR_LESS_THAN_EQUALS:
                            case TOKEN_OPERATOR_GREATER_THAN_EQUALS:
                            case TOKEN_OPERATOR_NOT_EQUAL:
                            case TOKEN_OPERATOR_STRICT_EQUAL:
                            case TOKEN_OPERATOR_STRICT_NOT_EQUAL:
                            case TOKEN_OPERATOR_PLUS_ASSIGNMENT:
                            case TOKEN_OPERATOR_MINUS_ASSIGNMENT:
                            case TOKEN_OPERATOR_MULTIPLICATION_ASSIGNMENT:
                            case TOKEN_OPERATOR_DIVISION_ASSIGNMENT:
                            case TOKEN_OPERATOR_MODULO_ASSIGNMENT:
                            case TOKEN_OPERATOR_BITWISE_AND_ASSIGNMENT:
                            case TOKEN_OPERATOR_BITWISE_OR_ASSIGNMENT:
                            case TOKEN_OPERATOR_BITWISE_XOR_ASSIGNMENT:
                            case TOKEN_OPERATOR_BITWISE_LEFT_SHIFT_ASSIGNMENT:
                            case TOKEN_OPERATOR_BITWISE_RIGHT_SHIFT_ASSIGNMENT:
                            case TOKEN_OPERATOR_BITWISE_UNSIGNED_RIGHT_SHIFT_ASSIGNMENT:
                                isNextMetadata = false;
                                break;
                            default:
                                isNextMetadata = true;
                                break;
                        }
                    }
                    break;
            }
        }

        final ASToken result;
        if (isNextMetadata)
            result = consumeMetadata(nextToken);
        else
            result = nextToken;

        return result;
    }

    /**
     * Close the forked include file tokenizer, and set it to null.
     */
    private void closeIncludeTokenizer()
    {
        if (forkIncludeTokenizer == null)
            return;

        try
        {
            problems.addAll(forkIncludeTokenizer.problems);
            forkIncludeTokenizer.close();
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }
        includeHandler.leaveFile(forkIncludeTokenizer.getEndOffset());
        forkIncludeTokenizer = null;
    }

    /**
     * @throws Exception
     */
    private final ASToken consumeMetadata(final ASToken startToken) throws Exception
    {
        final ASToken originalToken = new ASToken(startToken);
        MetaDataPayloadToken payload = new MetaDataPayloadToken(originalToken);
        final ArrayList<ASToken> safetyNet = new ArrayList<ASToken>(5);
        boolean isMetadata = true;
        while (true)
        {
            tokenizer.setReuseLastToken();
            final ASToken next = LT(1);
            if (next == null)
            {
                break;
            }
            safetyNet.add(new ASToken(next)); //sadly, we have to deal with the extra object creation if we're wrong
            payload.addToken(next); //here too

            if (!next.canExistInMetadata())
            {
                isMetadata = false;
                //consume the last token we saw so that we don't get ourselves into an infinite loop
                //it was the last token of the metadata, and this makes "next" the current token.
                consume(1);
                break;
            }
            consume(1);
            if (next.getType() == TOKEN_SQUARE_CLOSE)
            {
                break;
            }
        }
        if (!isMetadata)
        { //we're wrong, so let's add back the tokens to our lookahead buffer
            lookAheadBuffer.addAll(safetyNet);
            bufferSize = lookAheadBuffer.size();
            return originalToken;
        }
        return payload;

    }

    private final void fill(final int distance) throws Exception
    {
        int pos = 0;
        while (pos < distance)
        {
            addTokenToBuffer(nextTokenFromReader());
            pos++;
        }
    }

    /**
     * @param nextToken
     */
    private final void addTokenToBuffer(final ASToken nextToken)
    {
        bufferSize++;
        lookAheadBuffer.add(nextToken);
        // at EOF, nextToken can be null.
        if (nextToken != null)
            nextToken.lock();
    }

    /**
     * Get the pooled version of a given string.
     * 
     * @param text String literal.
     * @return Pooled string.
     */
    private final String poolString(final String text)
    {
        String pooledString = stringPool.get(text);
        if (pooledString == null)
        {
            stringPool.put(text, text);
            pooledString = text;
        }
        return pooledString;
    }

    /**
     * Get the next token from the source input. If this tokenizer is created
     * for a source file by {@link ASC}, and there are files included by
     * {@code -in} option, the tokenizer will return the
     * "injected include tokens" before real tokens coming from the JFlex
     * generated tokenizer.
     * 
     * @return next token from the source input
     * @throws IOException error
     * @see ASCompilationUnit#createMainCompilationUnitForASC()
     */
    private final ASToken nextTokenFromReader() throws IOException
    {
        final ASToken nextToken;
        if (ascIncludeImaginaryTokens != null && ascIncludeImaginaryTokens.hasNext())
            nextToken = ascIncludeImaginaryTokens.next();
        else if (tokenizer.hasBufferToken())
            nextToken = tokenizer.getBufferToken();
        else
            nextToken = tokenizer.nextToken();

        if (nextToken != null)
        {
            // Converting unicode on-the-fly in the lexer is much slower than
            // converting it here after the token is made, especially for 
            // identifiers.
            switch (nextToken.getType())
            {
                case TOKEN_LITERAL_NUMBER:
                    nextToken.setText(poolString(nextToken.getText()));
                    break;
                case TOKEN_LITERAL_REGEXP:
                    // Any "backslash-u" entities left after "convertUnicode"
                    // are invalid unicode escape sequences. According to AS3
                    // behavior, the backslash character is dropped.
                    nextToken.setText(poolString(convertUnicode(nextToken.getText()).replaceAll("\\\\u", "u")));
                    break;
                case TOKEN_IDENTIFIER:
                    // Intern 'identifiers' and 'keywords'. 
                    // 'keywords' were 'identifiers' before they are analyzed.
                    final String originalIdentifierName = nextToken.getText();
                    final String normalizedIdentifierName = poolString(convertUnicode(originalIdentifierName));
                    nextToken.setText(normalizedIdentifierName);
                    if (!config.ignoreKeywords)
                    {
                        /**
                         * If the identifier has escaped unicode sequence, it
                         * can't be a keyword.
                         * <p>
                         * According to ASL syntax spec chapter 3.4:
                         * <blockquote> Unicode escape sequences may be used to
                         * spell the names of identifiers that would otherwise
                         * be keywords. This is in contrast to ECMAScript.
                         * </blockquote>
                         */
                        if (originalIdentifierName.equals(normalizedIdentifierName))
                        {
                            // do keyword analysis here
                            final Integer info = keywordToTokenMap.get(nextToken.getText());
                            if (info != null)
                                nextToken.setType(info);
                        }
                    }
                    break;
                default:
                    // Ignore other tokens.
                    break;
            }

            //so we want to adjust all of our offsets here, BUT
            //the column is really only valid for the first line, which is line 0.
            //if we're not the first line, don't bother
            nextToken.adjustLocation(
                    offsetAdjustment,
                    lineAdjustment,
                    nextToken.getLine() == 0 ? columnAdjustment : 0);
            nextToken.storeLocalOffset();

            if (includeHandler != null)
            {
                nextToken.setSourcePath(includeHandler.getIncludeStackTop());
                includeHandler.onNextToken(nextToken);
            }

            if (nextToken.getSourcePath() == null)
                nextToken.setSourcePath(sourcePath);

            if (reader instanceof SourceFragmentsReader)
                ((SourceFragmentsReader)reader).adjustLocation(nextToken);
        }
        return nextToken;
    }

    /**
     * Consume tokens in the buffer
     * 
     * @param distance the number of tokens to consume
     */
    private final void consume(int distance)
    {
        if (bufferSize >= distance)
        {
            for (; distance > 0; distance--)
            {
                lookAheadBuffer.remove(bufferSize - 1);
                bufferSize--;
            }
        }
    }

    /**
     * Returns the next token that will be produced by the underlying lexer
     * 
     * @param distance distance to look ahead
     * @return an {@link ASToken}
     * @throws Exception
     */
    private final ASToken LT(final int distance) throws Exception
    {
        if (bufferSize < distance)
        {
            fill(distance - bufferSize);
        }
        return lookAheadBuffer.get(distance - 1);
    }

    private static final boolean matches(final ASToken token, final int type)
    {
        return token != null && token.getType() == type;
    }

    /**
     * Retrieve the end offset of the file.
     * <p>
     * The result is the end offset of the file, not the offset of the last
     * token, this allows any trailing space to be included so that the parser
     * can span the result {@code FileNode} to the entire file.
     * 
     * @return the end offset of the input file
     */
    public final int getEndOffset()
    {
        return tokenizer.getOffset() + offsetAdjustment;
    }

    /**
     * Computers whether the following token is a user-defined namespace. This
     * method calls processUserDefinedNamespace which will change token types
     * 
     * @param token token to start our analysis
     * @param lookaheadOffset offset of the tokens to look at
     * @return true if we're a user-defined namespace
     * @throws Exception
     */
    private final boolean isUserDefinedNamespace(final ASToken token, final int lookaheadOffset) throws Exception
    {
        processUserDefinedNamespace(token, lookaheadOffset);
        return token.getType() == TOKEN_NAMESPACE_ANNOTATION || token.getType() == TOKEN_NAMESPACE_NAME;
    }

    /**
     * Because AS3 supports qualified/unqualified namespaces as decorators on
     * definitions, we need to detect them before we even make it to the parser.
     * These look exactly like names/qnames, and so if they're on the same line
     * as a definition they might be a namespace name instead of a standard
     * identifier. This method will detect these cases, and change token types
     * accordingly
     * 
     * @param token token token to start our analysis
     * @param lookaheadOffset offset of the tokens to look at
     * @throws Exception
     */
    private final void processUserDefinedNamespace(final ASToken token, final int lookaheadOffset) throws Exception
    {
        token.lock();

        //determine if we have a user-defined namespace
        //our first token will be an identifier, and the cases we're looking for are:
        //1.) user_namespace (function|var|dynamic|static|final|native|override)
        //2.) my.pack.user_namespace (function|var|dynamic|static|final|native|override)
        //option number 1 is probably the 99% case so optimize for it
        ASToken nextToken = LT(1 + lookaheadOffset);
        if (token.matchesLine(nextToken))
        {
            // If the next token is an identifier check to see if it should
            // be modified to a TOKEN_NAMESPACE_ANNOTATION
            // This is so that code like:
            //    ns1 ns2 var x;
            // gets parsed correctly (2 namespace annotations, which is an error)
            if (nextToken.getType() == TOKEN_IDENTIFIER)
                processUserDefinedNamespace(nextToken, 1 + lookaheadOffset);

            switch (nextToken.getType())
            {
                case TOKEN_KEYWORD_FUNCTION:
                case TOKEN_KEYWORD_VAR:
                case TOKEN_KEYWORD_CONST:
                case TOKEN_RESERVED_WORD_NAMESPACE:
                case TOKEN_MODIFIER_DYNAMIC:
                case TOKEN_MODIFIER_FINAL:
                case TOKEN_MODIFIER_NATIVE:
                case TOKEN_MODIFIER_OVERRIDE:
                case TOKEN_MODIFIER_STATIC:
                case TOKEN_MODIFIER_VIRTUAL:
                case TOKEN_MODIFIER_ABSTRACT:
                case TOKEN_KEYWORD_CLASS:
                case TOKEN_KEYWORD_INTERFACE:
                case TOKEN_NAMESPACE_ANNOTATION:
                case HIDDEN_TOKEN_BUILTIN_NS:
                    token.setType(TOKEN_NAMESPACE_ANNOTATION);
                    return;
                case TOKEN_OPERATOR_NS_QUALIFIER: //simple name with a :: binding after it.  has to be a NS
                    token.setType(TOKEN_NAMESPACE_NAME);
                    return;
            }
            if (nextToken.getType() == TOKEN_OPERATOR_MEMBER_ACCESS)
            {
                int nextValidPart = TOKEN_IDENTIFIER;
                final ArrayList<ASToken> toTransform = new ArrayList<ASToken>(3);
                toTransform.add(token);
                toTransform.add(nextToken);
                int laDistance = lookaheadOffset + 1;
                while (true)
                {
                    nextToken = LT(++laDistance);
                    if (token.matchesLine(nextToken))
                    {
                        if (nextToken.getType() == nextValidPart)
                        {
                            nextValidPart = (nextToken.getType() == TOKEN_IDENTIFIER) ? TOKEN_OPERATOR_MEMBER_ACCESS : TOKEN_IDENTIFIER;
                            toTransform.add(nextToken);
                        }
                        else if (nextValidPart != TOKEN_IDENTIFIER && nextToken.canFollowUserNamespace())
                        {
                            // Next token is in the follow set of a namespace,
                            // so all the buffered tokens need to be converted
                            // into namespace tokens.
                            for (final ASToken ttToken : toTransform)
                            {
                                if (ttToken.getType() == TOKEN_IDENTIFIER)
                                    ttToken.setType(TOKEN_NAMESPACE_ANNOTATION);
                                else
                                    ttToken.setType(TOKEN_OPERATOR_MEMBER_ACCESS);
                            }
                            break;
                        }
                        else
                        {
                            break;
                        }
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }
    }

    /**
     * Combines the text of two tokens, adding whitespace between them and
     * adjusting offsets appropriately
     * 
     * @param target the base token that we will add the next to
     * @param source the source of the text to add
     */
    private final void combineText(TokenBase target, TokenBase source)
    {
        StringBuilder text = new StringBuilder();
        text.append(target.getText());
        //add whitespace for gaps between tokens
        for (int i = 0; i < (source.getStart() - target.getEnd()); i++)
        {
            text.append(" ");
        }
        text.append(source.getText());
        target.setText(poolString(text.toString()));
        target.setEnd(target.getStart() + text.length());
    }

    /**
     * Unicode pattern for {@code \u0000}.
     */
    private static final Pattern UNICODE_PATTERN = Pattern.compile(BaseRawASTokenizer.PATTERN_U4);

    /**
     * Leading characters of a unicode pattern.
     */
    private static final String UNICODE_LEADING_CHARS = "\\u";

    /**
     * Convert escaped unicode sequence in a string. For example:
     * {@code foo\u0051bar} is converted into {@code fooQbar}.
     * 
     * @param text input string
     * @return converted text
     */
    static String convertUnicode(final String text)
    {
        // Calling Pattern.matcher() is much slower than String.contains(), so
        // we need this predicate to skip unnecessary RegEx computation.
        if (text.contains(UNICODE_LEADING_CHARS))
        {
            final StringBuilder result = new StringBuilder();
            final Matcher matcher = UNICODE_PATTERN.matcher(text);
            int start = 0;
            while (matcher.find())
            {
                result.append(text, start, matcher.start());
                result.append(Character.toChars(BaseRawASTokenizer.decodeEscapedUnicode(matcher.group())));
                start = matcher.end();
            }
            result.append(text, start, text.length());
            return result.toString();
        }
        else
        {
            return text;
        }
    }

    /**
     * Gets the source path to the file being tokenized.
     */
    public String getSourcePath()
    {
        return sourcePath;
    }
}
