/*
 * 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.netbeans.modules.javascript2.editor.formatter;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import org.netbeans.api.annotations.common.CheckForNull;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.editor.indent.spi.Context;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;

/**
 *
 * @author Petr Hejl
 */
public final class FormatTokenStream implements Iterable<FormatToken> {

    private static final Logger LOGGER = Logger.getLogger(FormatTokenStream.class.getName());

    private final TreeMap<Integer, FormatToken> tokenPosition = new TreeMap<Integer, FormatToken>();

    private final Map<FormatToken, Integer> originalIndents = new HashMap<>();

    private FormatToken firstToken;

    private FormatToken lastToken;

    private FormatTokenStream() {
        super();
    }

    public static FormatTokenStream create(Context context, TokenSequence<? extends JsTokenId> ts,
            int start, int end) {

        FormatTokenStream ret = new FormatTokenStream();
        int diff = ts.move(start);
        if (diff <= 0) {
            ts.movePrevious();
        }
        ret.addToken(FormatToken.forFormat(FormatToken.Kind.SOURCE_START));

        while (ts.moveNext() && ts.offset() < end) {
            Token<? extends JsTokenId> token = ts.token();
            JsTokenId id = token.id();
            switch (id) {
                case EOL:
                    ret.addToken(FormatToken.forAny(FormatToken.Kind.EOL, ts.offset(), token.text(), id));
                    break;
                case WHITESPACE:
                    ret.addToken(FormatToken.forAny(FormatToken.Kind.WHITESPACE, ts.offset(), token.text(), id));
                    break;
                case BLOCK_COMMENT:
                    ret.addToken(FormatToken.forAny(FormatToken.Kind.BLOCK_COMMENT, ts.offset(), token.text(), id));
                    break;
                case DOC_COMMENT:
                    ret.addToken(FormatToken.forAny(FormatToken.Kind.DOC_COMMENT, ts.offset(), token.text(), id));
                    break;
                case LINE_COMMENT:
                    ret.addToken(FormatToken.forAny(FormatToken.Kind.LINE_COMMENT, ts.offset(), token.text(), id));
                    break;
                case OPERATOR_GREATER:
                case OPERATOR_LOWER:
                case OPERATOR_EQUALS:
                case OPERATOR_EQUALS_EXACTLY:
                case OPERATOR_LOWER_EQUALS:
                case OPERATOR_GREATER_EQUALS:
                case OPERATOR_NOT_EQUALS:
                case OPERATOR_NOT_EQUALS_EXACTLY:
                case OPERATOR_AND:
                case OPERATOR_OR:
                case OPERATOR_EXPONENTIATION:
                case OPERATOR_MULTIPLICATION:
                case OPERATOR_DIVISION:
                case OPERATOR_BITWISE_AND:
                case OPERATOR_BITWISE_OR:
                case OPERATOR_BITWISE_XOR:
                case OPERATOR_MODULUS:
                case OPERATOR_LEFT_SHIFT_ARITHMETIC:
                case OPERATOR_RIGHT_SHIFT_ARITHMETIC:
                case OPERATOR_RIGHT_SHIFT:
                case OPERATOR_PLUS:
                case OPERATOR_MINUS:
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.BEFORE_BINARY_OPERATOR_WRAP));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.BEFORE_BINARY_OPERATOR));
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_BINARY_OPERATOR));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_BINARY_OPERATOR_WRAP));
                    break;
                case OPERATOR_ASSIGNMENT:
                case OPERATOR_PLUS_ASSIGNMENT:
                case OPERATOR_MINUS_ASSIGNMENT:
                case OPERATOR_EXPONENTIATION_ASSIGNMENT:
                case OPERATOR_MULTIPLICATION_ASSIGNMENT:
                case OPERATOR_DIVISION_ASSIGNMENT:
                case OPERATOR_BITWISE_AND_ASSIGNMENT:
                case OPERATOR_BITWISE_OR_ASSIGNMENT:
                case OPERATOR_BITWISE_XOR_ASSIGNMENT:
                case OPERATOR_MODULUS_ASSIGNMENT:
                case OPERATOR_LEFT_SHIFT_ARITHMETIC_ASSIGNMENT:
                case OPERATOR_RIGHT_SHIFT_ARITHMETIC_ASSIGNMENT:
                case OPERATOR_RIGHT_SHIFT_ASSIGNMENT:
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.BEFORE_ASSIGNMENT_OPERATOR));
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_ASSIGNMENT_OPERATOR));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_ASSIGNMENT_OPERATOR_WRAP));
                    break;
                case OPERATOR_ARROW:
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.BEFORE_ARROW_OPERATOR));
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_ARROW_OPERATOR));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_ARROW_OPERATOR_WRAP));
                    break;
                case OPERATOR_COMMA:
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.BEFORE_COMMA));
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_COMMA));
                    break;
                case OPERATOR_DOT:
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.BEFORE_DOT));
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_DOT));
                    break;
                case KEYWORD_IF:
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_IF_KEYWORD));
                    break;
                case KEYWORD_WHILE:
                    // we do not put before here, we do it in visitor to put it
                    // only for do while
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_WHILE_KEYWORD));
                    break;
                case KEYWORD_FOR:
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_FOR_KEYWORD));
                    break;
                case KEYWORD_WITH:
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_WITH_KEYWORD));
                    break;
                case KEYWORD_SWITCH:
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_SWITCH_KEYWORD));
                    break;
                case KEYWORD_CATCH:
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.BEFORE_CATCH_KEYWORD));
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_CATCH_KEYWORD));
                    break;
                case KEYWORD_ELSE:
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.BEFORE_ELSE_KEYWORD));
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    break;
                case KEYWORD_FINALLY:
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.BEFORE_FINALLY_KEYWORD));
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    break;
                case KEYWORD_VAR:
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_VAR_KEYWORD));
                    break;
                case KEYWORD_NEW:
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_NEW_KEYWORD));
                    break;
                case KEYWORD_TYPEOF:
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_TYPEOF_KEYWORD));
                    break;
                case OPERATOR_SEMICOLON:
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.BEFORE_SEMICOLON));
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_SEMICOLON));
                    break;
                case BRACKET_LEFT_PAREN:
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.AFTER_LEFT_PARENTHESIS));
                    break;
                case BRACKET_RIGHT_PAREN:
                    ret.addToken(FormatToken.forFormat(FormatToken.Kind.BEFORE_RIGHT_PARENTHESIS));
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    break;
                case JSX_TEXT:
                    FormatToken jsxToken = FormatToken.forText(ts.offset(), token.text(), id);
                    if (context != null) {
                        try {
                            int indent = context.lineIndent(context.lineStartOffset(ts.offset()));
                            ret.setOriginalIndent(jsxToken, indent);
                        } catch (BadLocationException ex) {
                            LOGGER.log(Level.INFO, null, ex);
                        }
                    }
                    ret.addToken(jsxToken);
                    break;
                default:
                    ret.addToken(FormatToken.forText(ts.offset(), token.text(), id));
                    break;
            }

        }
        return ret;
    }

    @CheckForNull
    public static FormatToken getNextNonVirtual(FormatToken token) {
        FormatToken current = token.next();
        while (current != null && current.isVirtual()) {
            current = current.next();
        }
        return current;
    }

    @CheckForNull
    public static FormatToken getNextNonWhite(FormatToken token, boolean allowEol) {
        FormatToken current = token.next();
        while (current != null && (current.isVirtual()
                || current.getKind() == FormatToken.Kind.WHITESPACE
                || (allowEol && current.getKind() == FormatToken.Kind.EOL))) {
            current = current.next();
        }
        return current;
    }

    @CheckForNull
    public static FormatToken getNextImportant(FormatToken token) {
        FormatToken current = token.next();
        while (current != null && (current.isVirtual()
                || current.getKind() == FormatToken.Kind.WHITESPACE
                || current.getKind() == FormatToken.Kind.EOL
                || current.getKind() == FormatToken.Kind.DOC_COMMENT
                || current.getKind() == FormatToken.Kind.BLOCK_COMMENT
                || current.getKind() == FormatToken.Kind.LINE_COMMENT)) {
            current = current.next();
        }
        return current;
    }

    @CheckForNull
    public static FormatToken getPreviousNonVirtual(FormatToken token) {
        FormatToken current = token.previous();
        while (current != null && current.isVirtual()) {
            current = current.previous();
        }
        return current;
    }

    public FormatToken getToken(int offset) {
        return tokenPosition.get(offset);
    }

    /**
     * Returns token containing the offset if any. Returned token does not
     * have to start at the offset but it has to cover it.
     * @param offset the give offset
     * @return token containing the offset or <code>null</code>
     */
    public FormatToken getCoveringToken(int offset) {
        Map.Entry<Integer, FormatToken> entry = tokenPosition.floorEntry(offset);
        if (entry != null) {
            FormatToken token = entry.getValue();
            if (!token.isVirtual()) {
                if (token.getOffset() == offset) {
                    return token;
                }
                int endPos = token.getOffset() + token.getText().length();
                if (offset >= token.getOffset() && offset < endPos) {
                    return token;
                }
            }
        }
        return null;
    }

    @Override
    public Iterator<FormatToken> iterator() {
        return new FormatTokenIterator();
    }

    public List<FormatToken> getTokens() {
        List<FormatToken> tokens = new ArrayList<FormatToken>((int) (tokenPosition.size() * 1.5));
        for (FormatToken token : this) {
            tokens.add(token);
        }
        return tokens;
    }

    public void addToken(FormatToken token) {
        if (firstToken == null) {
            firstToken = token;
            lastToken = token;
        } else {
            lastToken.setNext(token);
            token.setPrevious(lastToken);
            lastToken = token;
        }

        if (token.getOffset() >= 0) {
            tokenPosition.put(token.getOffset(), token);
        }
    }

    public void removeToken(FormatToken token) {
        assert token.isVirtual() : token;
        FormatToken previous = token.previous();
        FormatToken next = token.next();

        token.setNext(null);
        token.setPrevious(null);

        if (token.getOffset() >= 0) {
            tokenPosition.remove(token.getOffset());
        }

        if (previous == null) {
            assert firstToken == token;
            firstToken = next;
            next.setPrevious(null);
        }
        if (next == null) {
            assert lastToken == token;
            lastToken = previous;
            previous.setNext(null);
        }
        if (previous == null || next == null) {
            return;
        }
        previous.setNext(next);
        next.setPrevious(previous);
    }

    public void setOriginalIndent(FormatToken token, int indent) {
        originalIndents.put(token, indent);
    }

    public Integer getOriginalIndent(FormatToken token) {
        return originalIndents.get(token);
    }

    private class FormatTokenIterator implements Iterator<FormatToken> {

        private FormatToken current = firstToken;

        @Override
        public boolean hasNext() {
            return current != null;
        }

        @Override
        public FormatToken next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            FormatToken ret = current;
            current = current.next();
            return ret;
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("Remove operation not supported.");
        }

    }
}
