// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

#include "qmljscodeformatter.h"

#include <QLoggingCategory>
#include <QMetaEnum>
#include <QTextBlock>
#include <QTextDocument>

static Q_LOGGING_CATEGORY(formatterLog, "qtc.qmljs.formatter", QtWarningMsg)

namespace QmlJS {

CodeFormatter::BlockData::BlockData()
    : m_indentDepth(0)
    , m_blockRevision(-1)
{
}

CodeFormatter::CodeFormatter()
    : m_tokenIndex(0)
    , m_indentDepth(0)
    , m_tabSize(4)
{
}

CodeFormatter::~CodeFormatter()
{
}

void CodeFormatter::setTabSize(int tabSize)
{
    m_tabSize = tabSize;
}

void CodeFormatter::recalculateStateAfter(const QTextBlock &block)
{
    restoreCurrentState(block.previous());

    const int lexerState = tokenizeBlock(block);
    m_tokenIndex = 0;
    m_newStates.clear();

    //qCDebug(formatterLog) << "Starting to look at " << block.text() << block.blockNumber() + 1;

    for (; m_tokenIndex < m_tokens.size(); ) {
        m_currentToken = tokenAt(m_tokenIndex);
        const int kind = extendedTokenKind(m_currentToken);

        //qCDebug(formatterLog) << "Token" << m_currentLine.mid(m_currentToken.begin(), m_currentToken.length) << m_tokenIndex << "in line" << block.blockNumber() + 1;
        //dump();

        if (kind == Comment
                && state().type != multiline_comment_cont
                && state().type != multiline_comment_start) {
            m_tokenIndex += 1;
            continue;
        }

        switch (m_currentState.top().type) {
        case topmost_intro:
            switch (kind) {
            case Identifier:    enter(objectdefinition_or_js); continue;
            case Import:        enter(top_qml); continue;
            case LeftBrace:     enter(top_js); enter(expression); continue; // if a file starts with {, it's likely json
            default:            enter(top_js); continue;
            } break;

        case top_qml:
            switch (kind) {
            case Import:        enter(import_start); break;
            case Identifier:    enter(binding_or_objectdefinition); break;
            } break;

        case top_js:
            tryStatement();
            break;

        case objectdefinition_or_js:
            switch (kind) {
            case Dot:           break;
            case Identifier:
                if (!m_currentLine.at(m_currentToken.begin()).isUpper()) {
                    turnInto(top_js);
                    continue;
                }
                break;
            case LeftBrace:     turnInto(binding_or_objectdefinition); continue;
            default:            turnInto(top_js); continue;
            } break;

        case import_start:
            enter(import_maybe_dot_or_version_or_as);
            break;

        case import_maybe_dot_or_version_or_as:
            switch (kind) {
            case Dot:           turnInto(import_dot); break;
            case As:            turnInto(import_as); break;
            case Number:        turnInto(import_maybe_as); break;
            default:            leave(); leave(); continue;
            } break;

        case import_maybe_as:
            switch (kind) {
            case As:            turnInto(import_as); break;
            default:            leave(); leave(); continue;
            } break;

        case import_dot:
            switch (kind) {
            case Identifier:    turnInto(import_maybe_dot_or_version_or_as); break;
            default:            leave(); leave(); continue;
            } break;

        case import_as:
            switch (kind) {
            case Identifier:    leave(); leave(); break;
            } break;

        case binding_or_objectdefinition:
            switch (kind) {
            case Colon:         enter(binding_assignment); break;
            case LeftBrace:     enter(objectdefinition_open); break;
            } break;

        case binding_assignment:
            switch (kind) {
            case Semicolon:     leave(true); break;
            case If:            enter(if_statement); break;
            case With:          enter(statement_with_condition); break;
            case Try:           enter(try_statement); break;
            case Switch:        enter(switch_statement); break;
            case LeftBrace:     enter(jsblock_open); break;
            case On:
            case As:
            case List:
            case Import:
            case Signal:
            case Property:
            case Required:
            case Readonly:
            case Identifier:    enter(expression_or_objectdefinition); break;

            // error recovery
            case RightBracket:
            case RightParenthesis:  leave(true); break;

            default:            enter(expression); continue;
            } break;

        case objectdefinition_open:
            switch (kind) {
            case RightBrace:    leave(true); break;
            case Default:
            case Readonly:      enter(property_modifiers); break;
            case Property:      enter(property_start); break;
            case Required:      enter(required_property); break;
            case Function:      enter(function_start); break;
            case Signal:        enter(signal_start); break;
            case Enum:          enter(enum_start); break;
            case On:
            case As:
            case List:
            case Import:
            case Identifier:    enter(binding_or_objectdefinition); break;
            } break;

        case property_modifiers:
            switch (kind) {
            case Property:      turnInto(property_start); break;
            case Default:
            case Readonly:      break;
            case Required:      turnInto(required_property); break;
            default:            leave(true); break;
            } break;

        case property_start:
            switch (kind) {
            case Colon:         enter(binding_assignment); break; // oops, was a binding
            case Var:
            case Identifier:    enter(property_name); break;
            case List:          enter(property_list_open); break;
            default:            leave(true); continue;
            } break;

        case required_property:
            switch (kind) {
            case Property:      turnInto(property_start); break;
            case Default:
            case Readonly:      turnInto(property_modifiers); break;
            case Identifier:    leave(true); break;
            default:            leave(true); continue;
            } break;

        case component_start:
            switch (kind) {
            case Identifier:    turnInto(StateType::component_name); break;
            default:            leave(true); continue;
            } break;

        case property_name:
            turnInto(property_maybe_initializer);
            break;

        case property_list_open: {
            const QStringView tok = QStringView(m_currentLine).mid(
                                                   m_currentToken.begin(),
                                                   m_currentToken.length);
            if (tok == QLatin1String(">"))
                turnInto(property_name);
            break;
        }
        case property_maybe_initializer:
            switch (kind) {
            case Colon:         turnInto(binding_assignment); break;
            default:            leave(true); continue;
            } break;

        case enum_start:
            switch (kind) {
            case LeftBrace: enter(objectliteral_open); break;
            } break;

        case signal_start:
            switch (kind) {
            case Colon:         enter(binding_assignment); break; // oops, was a binding
            default:            enter(signal_maybe_arglist); break;
            } break;

        case signal_maybe_arglist:
            switch (kind) {
            case LeftParenthesis:   turnInto(signal_arglist_open); break;
            default:                leave(true); continue;
            } break;

        case signal_arglist_open:
            switch (kind) {
            case RightParenthesis:  leave(true); break;
            } break;

        case function_start:
            switch (kind) {
            case LeftParenthesis:   enter(function_arglist_open); break;
            } break;

        case function_arglist_open:
            switch (kind) {
            case RightParenthesis:  turnInto(function_arglist_closed); break;
            } break;

        case function_arglist_closed:
            switch (kind) {
            case LeftBrace:         turnInto(jsblock_open); break;
            case Colon:             turnInto(function_type_annotated_return); break;
            default:                leave(true); continue; // error recovery
            } break;

        case function_type_annotated_return:
            switch (kind) {
            case LeftBrace:         turnInto(jsblock_open); break;
            default:                break;
            } break;

        case expression_or_objectdefinition:
            switch (kind) {
            case Dot:
            case Identifier:        break; // need to become an objectdefinition_open in cases like "width: Qt.Foo {"
            case LeftBrace:         turnInto(objectdefinition_open); break;

            // propagate 'leave' from expression state
            case RightBracket:
            case RightParenthesis:  leave(); continue;

            default:                enter(expression); continue; // really? identifier and more tokens might already be gone
            } break;

        case expression_or_label:
            switch (kind) {
            case Colon:             turnInto(labelled_statement); break;

            // propagate 'leave' from expression state
            case RightBracket:
            case RightParenthesis:  leave(); continue;

            default:                enter(expression); continue;
            } break;

        case ternary_op:
            if (kind == Colon) {
                enter(ternary_op_after_colon);
                enter(expression_continuation);
                break;
            }
            Q_FALLTHROUGH();
        case ternary_op_after_colon:
        case expression:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case Comma:             leave(true); break;
            case Delimiter:         enter(expression_continuation); break;
            case RightBracket:
            case RightParenthesis:  leave(); continue;
            case RightBrace:        leave(true); continue;
            case Semicolon:         leave(true); break;
            } break;

        case expression_continuation:
            leave();
            continue;

        case expression_maybe_continuation:
            switch (kind) {
            case Question:
            case Delimiter:
            case LeftBracket:
            case LeftParenthesis:
            case LeftBrace:         leave(); continue;
            default:                leave(true); continue;
            } break;

        case paren_open:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case RightParenthesis:  leave(); break;
            } break;

        case bracket_open:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case Comma:             enter(bracket_element_start); break;
            case RightBracket:      leave(); break;
            } break;

        case objectliteral_open:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case Colon:             enter(objectliteral_assignment); break;
            case RightBracket:
            case RightParenthesis:  leave(); continue; // error recovery
            case RightBrace:        leave(true); break;
            } break;

        // pretty much like expression, but ends with , or }
        case objectliteral_assignment:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case Comma:             leave(); break;
            case Delimiter:         enter(expression_continuation); break;
            case RightBracket:
            case RightParenthesis:  leave(); continue; // error recovery
            case RightBrace:        leave(); continue; // so we also leave objectliteral_open
            } break;

        case bracket_element_start:
            switch (kind) {
            case Identifier:        turnInto(bracket_element_maybe_objectdefinition); break;
            default:                leave(); continue;
            } break;

        case bracket_element_maybe_objectdefinition:
            switch (kind) {
            case LeftBrace:         turnInto(objectdefinition_open); break;
            default:                leave(); continue;
            } break;

        case jsblock_open:
        case substatement_open:
            if (tryStatement())
                break;
            switch (kind) {
            case RightBrace:        leave(true); break;
            } break;

        case labelled_statement:
            if (tryStatement())
                break;
            leave(true); // error recovery
            break;

        case substatement:
            // prefer substatement_open over block_open
            if (kind != LeftBrace) {
                if (tryStatement())
                    break;
            }
            switch (kind) {
            case LeftBrace:         turnInto(substatement_open); break;
            } break;

        case if_statement:
            switch (kind) {
            case LeftParenthesis:   enter(condition_open); break;
            default:                leave(true); break; // error recovery
            } break;

        case maybe_else:
            switch (kind) {
            case Else:              turnInto(else_clause); enter(substatement); break;
            default:                leave(true); continue;
            } break;

        case maybe_catch_or_finally:
            switch (kind) {
            case Catch:             turnInto(catch_statement); break;
            case Finally:           turnInto(finally_statement); break;
            default:                leave(true); continue;
            } break;

        case else_clause:
            // ### shouldn't happen
            dump();
            Q_ASSERT(false);
            leave(true);
            break;

        case condition_open:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case RightParenthesis:  turnInto(substatement); break;
            } break;

        case switch_statement:
        case catch_statement:
        case statement_with_condition:
            switch (kind) {
            case LeftParenthesis:   enter(statement_with_condition_paren_open); break;
            default:                leave(true);
            } break;

        case statement_with_condition_paren_open:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case RightParenthesis:  turnInto(substatement); break;
            } break;

        case try_statement:
        case finally_statement:
            switch (kind) {
            case LeftBrace:         enter(jsblock_open); break;
            default:                leave(true); break;
            } break;

        case do_statement:
            switch (kind) {
            case While:             break;
            case LeftParenthesis:   enter(do_statement_while_paren_open); break;
            default:                leave(true); continue; // error recovery
            } break;

        case do_statement_while_paren_open:
            if (tryInsideExpression())
                break;
            switch (kind) {
            case RightParenthesis:  leave(); leave(true); break;
            } break;

        case breakcontinue_statement:
            switch (kind) {
            case Identifier:        leave(true); break;
            default:                leave(true); continue; // try again
            } break;

        case case_start:
            switch (kind) {
            case Colon:             turnInto(case_cont); break;
            } break;

        case case_cont:
            if (kind != Case && kind != Default && tryStatement())
                break;
            switch (kind) {
            case RightBrace:        leave(); continue;
            case Default:
            case Case:              leave(); continue;
            } break;

        case multiline_comment_start:
        case multiline_comment_cont:
            if (kind != Comment) {
                leave();
                continue;
            } else if (m_tokenIndex == m_tokens.size() - 1
                       && (lexerState & Scanner::MultiLineMask) == Scanner::Normal) {
                leave();
            } else if (m_tokenIndex == 0) {
                // to allow enter/leave to update the indentDepth
                turnInto(multiline_comment_cont);
            }
            break;

        default:
            qWarning() << "Unhandled state" << m_currentState.top().type;
            break;
        } // end of state switch

        ++m_tokenIndex;
    }

    int topState = m_currentState.top().type;

    // if there's no colon on the same line, it's not a label
    if (topState == expression_or_label)
        enter(expression);
    // if not followed by an identifier on the same line, it's done
    else if (topState == breakcontinue_statement)
        leave(true);

    topState = m_currentState.top().type;

    // some states might be continued on the next line
    if (topState == expression
            || topState == expression_or_objectdefinition
            || topState == objectliteral_assignment
            || topState == ternary_op_after_colon) {
        enter(expression_maybe_continuation);
    }
    // multi-line comment start?
    if (topState != multiline_comment_start
            && topState != multiline_comment_cont
            && (lexerState & Scanner::MultiLineMask) == Scanner::MultiLineComment) {
        enter(multiline_comment_start);
    }

    saveCurrentState(block);
}

int CodeFormatter::indentFor(const QTextBlock &block)
{
//    qCDebug(formatterLog) << "indenting for" << block.blockNumber() + 1;

    restoreCurrentState(block.previous());
    correctIndentation(block);
    return m_indentDepth;
}

int CodeFormatter::indentForNewLineAfter(const QTextBlock &block)
{
    restoreCurrentState(block);

    m_tokens.clear();
    m_currentLine.clear();
    const int startLexerState = loadLexerState(block.previous());
    adjustIndent(m_tokens, startLexerState, &m_indentDepth);

    return m_indentDepth;
}

void CodeFormatter::updateStateUntil(const QTextBlock &endBlock)
{
    QStack<State> previousState = initialState();
    QTextBlock it = endBlock.document()->firstBlock();

    // find the first block that needs recalculation
    for (; it.isValid() && it != endBlock; it = it.next()) {
        BlockData blockData;
        if (!loadBlockData(it, &blockData))
            break;
        if (blockData.m_blockRevision != it.revision())
            break;
        if (previousState != blockData.m_beginState)
            break;
        if (loadLexerState(it) == -1)
            break;

        previousState = blockData.m_endState;
    }

    if (it == endBlock)
        return;

    // update everthing until endBlock
    for (; it.isValid() && it != endBlock; it = it.next()) {
        recalculateStateAfter(it);
    }

    // invalidate everything below by marking the state in endBlock as invalid
    if (it.isValid()) {
        BlockData invalidBlockData;
        saveBlockData(&it, invalidBlockData);
    }
}

void CodeFormatter::updateLineStateChange(const QTextBlock &block)
{
    if (!block.isValid())
        return;

    BlockData blockData;
    if (loadBlockData(block, &blockData) && blockData.m_blockRevision == block.revision())
        return;

    recalculateStateAfter(block);

    // invalidate everything below by marking the next block's state as invalid
    QTextBlock next = block.next();
    if (!next.isValid())
        return;

    saveBlockData(&next, BlockData());
}

CodeFormatter::State CodeFormatter::state(int belowTop) const
{
    if (belowTop < m_currentState.size())
        return m_currentState.at(m_currentState.size() - 1 - belowTop);
    else
        return State();
}

const QVector<CodeFormatter::State> &CodeFormatter::newStatesThisLine() const
{
    return m_newStates;
}

int CodeFormatter::tokenIndex() const
{
    return m_tokenIndex;
}

int CodeFormatter::tokenCount() const
{
    return m_tokens.size();
}

const Token &CodeFormatter::currentToken() const
{
    return m_currentToken;
}

void CodeFormatter::invalidateCache(QTextDocument *document)
{
    if (!document)
        return;

    BlockData invalidBlockData;
    QTextBlock it = document->firstBlock();
    for (; it.isValid(); it = it.next()) {
        saveBlockData(&it, invalidBlockData);
    }
}

void CodeFormatter::enter(int newState)
{
    int savedIndentDepth = m_indentDepth;
    onEnter(newState, &m_indentDepth, &savedIndentDepth);
    State s(newState, savedIndentDepth);
    m_currentState.push(s);
    m_newStates.push(s);

    //qCDebug(formatterLog) << "enter state" << stateToString(newState);

    if (newState == bracket_open)
        enter(bracket_element_start);
}

void CodeFormatter::leave(bool statementDone)
{
    Q_ASSERT(m_currentState.size() > 1);
    if (m_currentState.top().type == topmost_intro)
        return;

    if (m_newStates.size() > 0)
        m_newStates.pop();

    // restore indent depth
    State poppedState = m_currentState.pop();
    m_indentDepth = poppedState.savedIndentDepth;

    int topState = m_currentState.top().type;

    //qCDebug(formatterLog) << "left state" << stateToString(poppedState.type) << ", now in state" << stateToString(topState);

    // if statement is done, may need to leave recursively
    if (statementDone) {
        if (topState == if_statement) {
            if (poppedState.type != maybe_else)
                enter(maybe_else);
            else
                leave(true);
        } else if (topState == else_clause) {
            // leave the else *and* the surrounding if, to prevent another else
            leave();
            leave(true);
        } else if (topState == try_statement) {
            if (poppedState.type != maybe_catch_or_finally
                    && poppedState.type != finally_statement) {
                enter(maybe_catch_or_finally);
            } else {
                leave(true);
            }
        } else if (!isExpressionEndState(topState)) {
            leave(true);
        }
    }
}

void CodeFormatter::correctIndentation(const QTextBlock &block)
{
    tokenizeBlock(block);
    Q_ASSERT(m_currentState.size() >= 1);

    const int startLexerState = loadLexerState(block.previous());
    adjustIndent(m_tokens, startLexerState, &m_indentDepth);
}

bool CodeFormatter::tryInsideExpression(bool alsoExpression)
{
    int newState = -1;
    const int kind = extendedTokenKind(m_currentToken);
    switch (kind) {
    case LeftParenthesis:   newState = paren_open; break;
    case LeftBracket:       newState = bracket_open; break;
    case LeftBrace:         newState = objectliteral_open; break;
    case Function:          newState = function_start; break;
    case Question:          newState = ternary_op; break;
    }

    if (newState != -1) {
        if (alsoExpression)
            enter(expression);
        enter(newState);
        return true;
    }

    return false;
}

bool CodeFormatter::tryStatement()
{
    const int kind = extendedTokenKind(m_currentToken);
    switch (kind) {
    case Semicolon:
        enter(empty_statement);
        leave(true);
        return true;
    case Break:
    case Continue:
        enter(breakcontinue_statement);
        return true;
    case Throw:
        enter(throw_statement);
        enter(expression);
        return true;
    case Return:
        enter(return_statement);
        enter(expression);
        return true;
    case While:
    case For:
    case Catch:
        enter(statement_with_condition);
        return true;
    case Switch:
        enter(switch_statement);
        return true;
    case If:
        enter(if_statement);
        return true;
    case Do:
        enter(do_statement);
        enter(substatement);
        return true;
    case Case:
    case Default:
        enter(case_start);
        return true;
    case Try:
        enter(try_statement);
        return true;
    case LeftBrace:
        enter(jsblock_open);
        return true;
    case Identifier:
        enter(expression_or_label);
        return true;
    case Delimiter:
    case Var:
    case PlusPlus:
    case MinusMinus:
    case Import:
    case Signal:
    case On:
    case As:
    case List:
    case Property:
    case Required:
    case Readonly:
    case Function:
    case Number:
    case String:
    case LeftParenthesis:
        enter(expression);
        // look at the token again
        m_tokenIndex -= 1;
        return true;
    }
    return false;
}

bool CodeFormatter::isBracelessState(int type) const
{
    return
            type == if_statement ||
            type == else_clause ||
            type == substatement ||
            type == binding_assignment ||
            type == binding_or_objectdefinition;
}

bool CodeFormatter::isExpressionEndState(int type) const
{
    return
            type == topmost_intro ||
            type == top_js ||
            type == objectdefinition_open ||
            type == do_statement ||
            type == jsblock_open ||
            type == substatement_open ||
            type == bracket_open ||
            type == paren_open ||
            type == case_cont ||
            type == objectliteral_open;
}

const Token &CodeFormatter::tokenAt(int idx) const
{
    static const Token empty;
    if (idx < 0 || idx >= m_tokens.size())
        return empty;
    else
        return m_tokens.at(idx);
}

int CodeFormatter::column(int index) const
{
    int col = 0;
    if (index > m_currentLine.length())
        index = m_currentLine.length();

    const QChar tab = QLatin1Char('\t');

    for (int i = 0; i < index; i++) {
        if (m_currentLine[i] == tab)
            col = ((col / m_tabSize) + 1) * m_tabSize;
        else
            col++;
    }
    return col;
}

QStringView CodeFormatter::currentTokenText() const
{
    return QStringView(m_currentLine).mid(m_currentToken.begin(), m_currentToken.length);
}

void CodeFormatter::turnInto(int newState)
{
    leave(false);
    enter(newState);
}

void CodeFormatter::saveCurrentState(const QTextBlock &block)
{
    if (!block.isValid())
        return;

    BlockData blockData;
    blockData.m_blockRevision = block.revision();
    blockData.m_beginState = m_beginState;
    blockData.m_endState = m_currentState;
    blockData.m_indentDepth = m_indentDepth;

    QTextBlock saveableBlock(block);
    saveBlockData(&saveableBlock, blockData);
}

void CodeFormatter::restoreCurrentState(const QTextBlock &block)
{
    if (block.isValid()) {
        BlockData blockData;
        if (loadBlockData(block, &blockData)) {
            m_indentDepth = blockData.m_indentDepth;
            m_currentState = blockData.m_endState;
            m_beginState = m_currentState;
            return;
        }
    }

    m_currentState = initialState();
    m_beginState = m_currentState;
    m_indentDepth = 0;
}

QStack<CodeFormatter::State> CodeFormatter::initialState()
{
    static QStack<CodeFormatter::State> initialState;
    if (initialState.isEmpty())
        initialState.push(State(topmost_intro, 0));
    return initialState;
}

int CodeFormatter::tokenizeBlock(const QTextBlock &block)
{
    int startState = loadLexerState(block.previous());
    if (block.blockNumber() == 0)
        startState = 0;
    Q_ASSERT(startState != -1);

    Scanner tokenize;
    tokenize.setScanComments(true);

    m_currentLine = block.text();
    // to determine whether a line was joined, Tokenizer needs a
    // newline character at the end
    m_currentLine.append(QLatin1Char('\n'));
    m_tokens = tokenize(m_currentLine, startState);

    const int lexerState = tokenize.state();
    QTextBlock saveableBlock(block);
    saveLexerState(&saveableBlock, lexerState);
    return lexerState;
}

CodeFormatter::TokenKind CodeFormatter::extendedTokenKind(const QmlJS::Token &token) const
{
    const int kind = token.kind;
    const QStringView text = QStringView(m_currentLine).mid(token.begin(), token.length);

    if (kind == Identifier) {
        if (text == QLatin1String("as"))
            return As;
        if (text == QLatin1String("import"))
            return Import;
        if (text == QLatin1String("signal"))
            return Signal;
        if (text == QLatin1String("property"))
            return Property;
        if (text == QLatin1String("readonly"))
            return Readonly;
        if (text == QLatin1String("component"))
            return Component;
        if (text == QLatin1String("required"))
            return Required;
        if (text == QLatin1String("on"))
            return On;
        if (text == QLatin1String("list"))
            return List;
        if (text == QLatin1String("enum"))
            return Enum;
    } else if (kind == Keyword) {
        const char char1 = text.at(0).toLatin1();
        const char char2 = text.at(1).toLatin1();
        const char char3 = (text.size() > 2 ? text.at(2).toLatin1() : 0);
        switch (char1) {
        case 'v':
            return Var;
        case 'i':
            if (char2 == 'f')
                return If;
            else if (char3 == 's')
                return Instanceof;
            else
                return In;
        case 'f':
            if (char2 == 'o')
                return For;
            else if (char2 == 'u')
                return Function;
            else
                return Finally;
        case 'e':
            return Else;
        case 'n':
            return New;
        case 'r':
            return Return;
        case 's':
            return Switch;
        case 'w':
            if (char2 == 'h')
                return While;
            return With;
        case 'c':
            if (char3 == 's')
                return Case;
            if (char3 == 't')
                return Catch;
            return Continue;
        case 'd':
            if (char3 == 'l')
                return Delete;
            if (char3 == 'f')
                return Default;
            if (char3 == 'b')
                return Debugger;
            return Do;
        case 't':
            if (char3 == 'i')
                return This;
            if (char3 == 'y')
                return Try;
            if (char3 == 'r')
                return Throw;
            return Typeof;
        case 'b':
            return Break;
        }
    } else if (kind == Delimiter) {
        if (text == QLatin1String("?"))
            return Question;
        else if (text == QLatin1String("++"))
            return PlusPlus;
        else if (text == QLatin1String("--"))
            return MinusMinus;
    }

    return static_cast<TokenKind>(kind);
}

void CodeFormatter::dump() const
{
    qCDebug(formatterLog) << "Current token index" << m_tokenIndex;
    qCDebug(formatterLog) << "Current state:";
    for (const State &s : m_currentState) {
        qCDebug(formatterLog) << stateToString(s.type) << s.savedIndentDepth;
    }
    qCDebug(formatterLog) << "Current indent depth:" << m_indentDepth;
}

QString CodeFormatter::stateToString(int type) const
{
    const QMetaEnum &metaEnum = staticMetaObject.enumerator(staticMetaObject.indexOfEnumerator("StateType"));
    return QString::fromUtf8(metaEnum.valueToKey(type));
}

QtStyleCodeFormatter::QtStyleCodeFormatter()
    : m_indentSize(4)
{}

void QtStyleCodeFormatter::setIndentSize(int size)
{
    m_indentSize = size;
}

void QtStyleCodeFormatter::onEnter(int newState, int *indentDepth, int *savedIndentDepth) const
{
    const State &parentState = state();
    const Token &tk = currentToken();
    const int tokenPosition = column(tk.begin());
    const bool firstToken = (tokenIndex() == 0);
    const bool lastToken = (tokenIndex() == tokenCount() - 1);

    switch (newState) {
    case objectdefinition_open: {
        // special case for things like "gradient: Gradient {"
        if (parentState.type == binding_assignment)
            *savedIndentDepth = state(1).savedIndentDepth;

        if (firstToken)
            *savedIndentDepth = tokenPosition;

        *indentDepth = *savedIndentDepth + m_indentSize;
        break;
    }

    case binding_or_objectdefinition:
        if (firstToken)
            *indentDepth = *savedIndentDepth = tokenPosition;
        break;

    case binding_assignment:
    case objectliteral_assignment:
        if (lastToken)
            *indentDepth = *savedIndentDepth + 4;
        else
            *indentDepth = column(tokenAt(tokenIndex() + 1).begin());
        break;

    case expression_or_objectdefinition:
        *indentDepth = tokenPosition;
        break;

    case expression_or_label:
        if (*indentDepth == tokenPosition)
            *indentDepth += 2*m_indentSize;
        else
            *indentDepth = tokenPosition;
        break;

    case expression:
        if (*indentDepth == tokenPosition) {
            // expression_or_objectdefinition doesn't want the indent
            // expression_or_label already has it
            if (parentState.type != expression_or_objectdefinition
                    && parentState.type != expression_or_label
                    && parentState.type != binding_assignment) {
                *indentDepth += 2*m_indentSize;
            }
        }
        // expression_or_objectdefinition and expression_or_label have already consumed the first token
        else if (parentState.type != expression_or_objectdefinition
                 && parentState.type != expression_or_label) {
            *indentDepth = tokenPosition;
        }
        break;

    case expression_maybe_continuation:
        // set indent depth to indent we'd get if the expression ended here
        for (int i = 1; state(i).type != topmost_intro; ++i) {
            const int type = state(i).type;
            if (isExpressionEndState(type) && !isBracelessState(type)) {
                *indentDepth = state(i - 1).savedIndentDepth;
                break;
            }
        }
        break;

    case bracket_open:
        if (parentState.type == expression && state(1).type == binding_assignment) {
            *savedIndentDepth = state(2).savedIndentDepth;
            *indentDepth = *savedIndentDepth + m_indentSize;
        } else if (parentState.type == objectliteral_assignment) {
            *savedIndentDepth = parentState.savedIndentDepth;
            *indentDepth = *savedIndentDepth + m_indentSize;
        } else if (!lastToken) {
            *indentDepth = tokenPosition + 1;
        } else {
            *indentDepth = *savedIndentDepth + m_indentSize;
        }
        break;

    case function_start:
        // align to the beginning of the line
        *savedIndentDepth = *indentDepth = column(tokenAt(0).begin());
        break;

    case do_statement_while_paren_open:
    case statement_with_condition_paren_open:
    case signal_arglist_open:
    case function_arglist_open:
    case paren_open:
        if (!lastToken)
            *indentDepth = tokenPosition + 1;
        else
            *indentDepth += m_indentSize;
        break;

    case ternary_op:
        if (!lastToken)
            *indentDepth = tokenPosition + tk.length + 1;
        else
            *indentDepth += m_indentSize;
        break;

    case jsblock_open:
        // closing brace should be aligned to case
        if (parentState.type == case_cont) {
            *savedIndentDepth = parentState.savedIndentDepth;
            break;
        }
        Q_FALLTHROUGH();
    case substatement_open:
        // special case for "foo: {" and "property int foo: {"
        if (parentState.type == binding_assignment)
            *savedIndentDepth = state(1).savedIndentDepth;
        *indentDepth = *savedIndentDepth + m_indentSize;
        break;

    case substatement:
        *indentDepth += m_indentSize;
        break;

    case objectliteral_open:
        if (parentState.type == expression
                || parentState.type == objectliteral_assignment) {
            // undo the continuation indent of the expression
            if (state(1).type == expression_or_label)
                *indentDepth = state(1).savedIndentDepth;
            else
                *indentDepth = parentState.savedIndentDepth;
            *savedIndentDepth = *indentDepth;
        }
        *indentDepth += m_indentSize;
        break;


    case statement_with_condition:
    case try_statement:
    case catch_statement:
    case finally_statement:
    case if_statement:
    case do_statement:
    case switch_statement:
        if (firstToken || parentState.type == binding_assignment)
            *savedIndentDepth = tokenPosition;
        // ### continuation
        *indentDepth = *savedIndentDepth; // + 2*m_indentSize;
        // special case for 'else if'
        if (!firstToken
                && newState == if_statement
                && parentState.type == substatement
                && state(1).type == else_clause) {
            *indentDepth = state(1).savedIndentDepth;
            *savedIndentDepth = *indentDepth;
        }
        break;

    case maybe_else:
    case maybe_catch_or_finally: {
        // set indent to where leave(true) would put it
        int lastNonEndState = 0;
        while (!isExpressionEndState(state(lastNonEndState + 1).type))
            ++lastNonEndState;
        *indentDepth = state(lastNonEndState).savedIndentDepth;
        break;
    }

    case condition_open:
        // fixed extra indent when continuing 'if (', but not for 'else if ('
        if (tokenPosition <= *indentDepth + m_indentSize)
            *indentDepth += 2*m_indentSize;
        else
            *indentDepth = tokenPosition + 1;
        break;

    case case_start:
        *savedIndentDepth = tokenPosition;
        break;

    case case_cont:
        *indentDepth += m_indentSize;
        break;

    case multiline_comment_start:
        *indentDepth = tokenPosition + 2;
        break;

    case multiline_comment_cont:
        *indentDepth = tokenPosition;
        break;
    }
}

void QtStyleCodeFormatter::adjustIndent(const QList<Token> &tokens, int startLexerState, int *indentDepth) const
{
    State topState = state();
    State previousState = state(1);

    // keep user-adjusted indent in multiline comments
    if (topState.type == multiline_comment_start
            || topState.type == multiline_comment_cont) {
        if (!tokens.isEmpty()) {
            *indentDepth = column(tokens.at(0).begin());
            return;
        }
    }
    // don't touch multi-line strings at all
    if ((startLexerState & Scanner::MultiLineMask) == Scanner::MultiLineStringDQuote
            || (startLexerState & Scanner::MultiLineMask) == Scanner::MultiLineStringSQuote) {
        *indentDepth = -1;
        return;
    }

    const int kind = extendedTokenKind(tokenAt(0));
    switch (kind) {
    case LeftBrace:
        if (topState.type == substatement
                || topState.type == binding_assignment
                || topState.type == case_cont) {
            *indentDepth = topState.savedIndentDepth;
        }
        break;
    case RightBrace: {
        if (topState.type == jsblock_open && previousState.type == case_cont) {
            *indentDepth = previousState.savedIndentDepth;
            break;
        }
        for (int i = 0; state(i).type != topmost_intro; ++i) {
            const int type = state(i).type;
            if (type == objectdefinition_open
                    || type == jsblock_open
                    || type == substatement_open
                    || type == objectliteral_open) {
                *indentDepth = state(i).savedIndentDepth;
                break;
            }
        }
        break;
    }
    case RightBracket:
        for (int i = 0; state(i).type != topmost_intro; ++i) {
            const int type = state(i).type;
            if (type == bracket_open) {
                *indentDepth = state(i).savedIndentDepth;
                break;
            }
        }
        break;

    case LeftBracket:
    case LeftParenthesis:
    case Delimiter:
        if (topState.type == expression_maybe_continuation)
            *indentDepth = topState.savedIndentDepth;
        break;

    case Else:
        if (topState.type == maybe_else) {
            *indentDepth = state(1).savedIndentDepth;
        } else if (topState.type == expression_maybe_continuation) {
            bool hasElse = false;
            for (int i = 1; state(i).type != topmost_intro; ++i) {
                const int type = state(i).type;
                if (type == else_clause)
                    hasElse = true;
                if (type == if_statement) {
                    if (hasElse) {
                        hasElse = false;
                    } else {
                        *indentDepth = state(i).savedIndentDepth;
                        break;
                    }
                }
            }
        }
        break;

    case Catch:
    case Finally:
        if (topState.type == maybe_catch_or_finally)
            *indentDepth = state(1).savedIndentDepth;
        break;

    case Colon:
        if (topState.type == ternary_op)
            *indentDepth -= 2;
        break;

    case Question:
        if (topState.type == expression_maybe_continuation)
            *indentDepth = topState.savedIndentDepth;
        break;

    case Default:
    case Case:
        for (int i = 0; state(i).type != topmost_intro; ++i) {
            const int type = state(i).type;
            if (type == switch_statement || type == case_cont) {
                *indentDepth = state(i).savedIndentDepth;
                break;
            } else if (type == topmost_intro) {
                break;
            }
        }
        break;
    }
}

} // namespace QmlJS
