package flow.netbeans.markdown.highlighter;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Deque;
import java.util.List;

/**
 * The {@link MarkdownTokenListBuilder} is used to build a list of tokens during
 * a depth-first left-to-right traversal of a PegDown AST. This functionality 
 * was extracted from the {@link MarkdownLexerVisitor} to simplify unit testing.
 * @author Holger Stenger
 */
public class MarkdownTokenListBuilder {
    private final Deque<MarkdownLexerVisitor.MarkdownTreeToken> stack;

    private final List<MarkdownToken> tokens;

    /**
     * Creates a {@link MarkdownTokenListBuilder}. The total length of the tokens 
     * created by the builder will be equal to the given total length. If the first 
     * added token does not start at position {@code 0}, the builder will insert
     * a synthetic token to fill the gap. Likewise, if the last added token does 
     * not end at position {@code totalLength}, the builder will insert a synthetic 
     * token to fill the gap.
     * @param totalLength The total length of the created tokens.
     */
    public MarkdownTokenListBuilder(int totalLength) {
        stack = new ArrayDeque<MarkdownLexerVisitor.MarkdownTreeToken>();
        tokens = new ArrayList<MarkdownToken>();
        // Push guard tree token.
        stack.push(new MarkdownLexerVisitor.MarkdownTreeToken(MarkdownTokenId.PLAIN, stack.size(), 0,
                totalLength));
    }

    /**
     * Add a token which contains no nested tokens. This method is equivalent to 
     * a call to {@link #beginTreeToken(flow.netbeans.markdown.highlighter.MarkdownTokenId, int, int)} 
     * directly followed by a call to {@link #endTreeToken()}.
     * @param id The token id.
     * @param startIndex The start index of the token (inclusive).
     * @param endIndex The end index of the token (exclusive).
     */
    public void addLeafTreeToken(MarkdownTokenId id, int startIndex, int endIndex) {
        beginTreeToken(id, startIndex, endIndex);
        endTreeToken();
    }

    /**
     * Begins a token which can contain nested tokens. The call to this method has 
     * to be matched with a call to {@link #endTreeToken()}.
     * @param id The token id.
     * @param startIndex The start index of the token (inclusive).
     * @param endIndex The end index of the token (exclusive).
     */
    public void beginTreeToken(final MarkdownTokenId id, final int startIndex, final int endIndex) {
        if (stack.isEmpty()) {
            throw new IllegalStateException("No tokens can be added after calling build");
        }
        MarkdownLexerVisitor.MarkdownTreeToken topTreeToken = stack.peek();
        final int effectiveStartIndex = Math.max(startIndex, topTreeToken.getStartIndex());
        final int effectiveEndIndex = Math.min(endIndex, topTreeToken.getEndIndex());
        addToken(topTreeToken.getId(), topTreeToken.getRemainderIndex(), effectiveStartIndex);
        topTreeToken.setRemainderIndex(effectiveEndIndex);
        MarkdownLexerVisitor.MarkdownTreeToken treeToken = new MarkdownLexerVisitor.MarkdownTreeToken(id, stack.size(),
                effectiveStartIndex, effectiveEndIndex);
//        LOG.log(Level.INFO, "Added tree token to stack: {0}", treeToken.toString());
        stack.push(treeToken);
    }

    /**
     * Ends a token which has been begun by a call to
     * {@link #beginTreeToken(flow.netbeans.markdown.highlighter.MarkdownTokenId, int, int)}.
     */
    public void endTreeToken() {
        if (stack.size() < 2) {
            // Protect guard tree token.
            throw new IllegalStateException("No token on stack");
        }
        endTreeTokenUnchecked();
    }

    private void endTreeTokenUnchecked() {
        MarkdownLexerVisitor.MarkdownTreeToken treeToken = stack.pop();
//        LOG.log(Level.INFO, "Removed tree token from stack: {0}", treeToken.toString());
        addToken(treeToken.getId(), treeToken.getRemainderIndex(), treeToken.getEndIndex());
    }

    private void addToken(MarkdownTokenId id, int startIndex, int endIndex) {
        if (startIndex < endIndex) {
            MarkdownToken token = new MarkdownToken(id, startIndex, endIndex);
//            LOG.log(Level.INFO, "Added token to list: {0}", token);
            tokens.add(token);
        }
    }

    /**
     * Finishes the token list construction.
     * @return The list of created tokens.
     */
    public List<MarkdownToken> build() {
        if (stack.size() > 1) {
            throw new IllegalStateException("Still tokens on stack");
        }
        if (stack.size() == 1) {
            // Pop guard tree token.
            endTreeTokenUnchecked();
        }
        return Collections.unmodifiableList(tokens);
    }
}
