// Copyright (C) 2022 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant

#include "qqmldomcodeformatter_p.h"

#include <QLoggingCategory>
#include <QMetaEnum>

Q_STATIC_LOGGING_CATEGORY(formatterLog, "qt.qmldom.formatter", QtWarningMsg);

QT_BEGIN_NAMESPACE
namespace QQmlJS {
namespace Dom {

using StateType = FormatTextStatus::StateType;
using State = FormatTextStatus::State;

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

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

void FormatPartialStatus::enterState(StateType newState)
{
    int savedIndentDepth = currentIndent;
    defaultOnEnter(newState, &currentIndent, &savedIndentDepth);
    currentStatus.pushState(newState, savedIndentDepth);
    qCDebug(formatterLog) << "enter state" << FormatTextStatus::stateToString(newState);

    if (newState == StateType::BracketOpen)
        enterState(StateType::BracketElementStart);
}

void FormatPartialStatus::leaveState(bool statementDone)
{
    Q_ASSERT(currentStatus.size() > 1);
    if (currentStatus.state().type == StateType::TopmostIntro)
        return;

    // restore indent depth
    State poppedState = currentStatus.popState();
    currentIndent = poppedState.savedIndentDepth;

    StateType topState = currentStatus.state().type;

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

    // if statement is done, may need to leave recursively
    if (statementDone) {
        if (topState == StateType::IfStatement) {
            if (poppedState.type != StateType::MaybeElse)
                enterState(StateType::MaybeElse);
            else
                leaveState(true);
        } else if (topState == StateType::ElseClause) {
            // leave the else *and* the surrounding if, to prevent another else
            leaveState(false);
            leaveState(true);
        } else if (topState == StateType::TryStatement) {
            if (poppedState.type != StateType::MaybeCatchOrFinally
                && poppedState.type != StateType::FinallyStatement) {
                enterState(StateType::MaybeCatchOrFinally);
            } else {
                leaveState(true);
            }
        } else if (!FormatTextStatus::isExpressionEndState(topState)) {
            leaveState(true);
        }
    }
}

void FormatPartialStatus::turnIntoState(StateType newState)
{
    leaveState(false);
    enterState(newState);
}

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

int FormatPartialStatus::column(int index) const
{
    if (index > line.size())
        index = line.size();
    IndentInfo indent(QStringView(line).mid(0, index), options.tabSize, indentOffset);
    return indent.column;
}

QStringView FormatPartialStatus::tokenText(const Token &token) const
{
    return line.mid(token.begin(), token.length);
}

bool FormatPartialStatus::tryInsideExpression(bool alsoExpression)
{
    StateType newState = StateType::Invalid;
    const int kind = tokenAt(tokenIndex).lexKind;
    switch (kind) {
    case QQmlJSGrammar::T_LPAREN:
        newState = StateType::ParenOpen;
        break;
    case QQmlJSGrammar::T_LBRACKET:
        newState = StateType::BracketOpen;
        break;
    case QQmlJSGrammar::T_LBRACE:
        newState = StateType::ObjectliteralOpen;
        break;
    case QQmlJSGrammar::T_FUNCTION:
        newState = StateType::FunctionStart;
        break;
    case QQmlJSGrammar::T_ARROW:
        newState = StateType::LambdaStart;
        break;
    case QQmlJSGrammar::T_QUESTION:
        newState = StateType::TernaryOp;
        break;
    }

    if (newState != StateType::Invalid) {
        if (alsoExpression)
            enterState(StateType::Expression);
        enterState(newState);
        return true;
    }

    return false;
}

bool FormatPartialStatus::tryStatement()
{
    Token t = tokenAt(tokenIndex);
    const int kind = t.lexKind;
    switch (kind) {
    case QQmlJSGrammar::T_AUTOMATIC_SEMICOLON:
    case QQmlJSGrammar::T_COMPATIBILITY_SEMICOLON:
    case QQmlJSGrammar::T_SEMICOLON:
        enterState(StateType::EmptyStatement);
        leaveState(true);
        return true;
    case QQmlJSGrammar::T_BREAK:
    case QQmlJSGrammar::T_CONTINUE:
        enterState(StateType::BreakcontinueStatement);
        return true;
    case QQmlJSGrammar::T_THROW:
        enterState(StateType::ThrowStatement);
        enterState(StateType::Expression);
        return true;
    case QQmlJSGrammar::T_RETURN:
        enterState(StateType::ReturnStatement);
        enterState(StateType::Expression);
        return true;
    case QQmlJSGrammar::T_WHILE:
    case QQmlJSGrammar::T_FOR:
    case QQmlJSGrammar::T_CATCH:
        enterState(StateType::StatementWithCondition);
        return true;
    case QQmlJSGrammar::T_SWITCH:
        enterState(StateType::SwitchStatement);
        return true;
    case QQmlJSGrammar::T_IF:
        enterState(StateType::IfStatement);
        return true;
    case QQmlJSGrammar::T_DO:
        enterState(StateType::DoStatement);
        enterState(StateType::Substatement);
        return true;
    case QQmlJSGrammar::T_CASE:
    case QQmlJSGrammar::T_DEFAULT:
        enterState(StateType::CaseStart);
        return true;
    case QQmlJSGrammar::T_TRY:
        enterState(StateType::TryStatement);
        return true;
    case QQmlJSGrammar::T_LBRACE:
        enterState(StateType::JsblockOpen);
        return true;
    case QQmlJSGrammar::T_VAR:
    case QQmlJSGrammar::T_PLUS_PLUS:
    case QQmlJSGrammar::T_MINUS_MINUS:
    case QQmlJSGrammar::T_IMPORT:
    case QQmlJSGrammar::T_SIGNAL:
    case QQmlJSGrammar::T_ON:
    case QQmlJSGrammar::T_AS:
    case QQmlJSGrammar::T_PROPERTY:
    case QQmlJSGrammar::T_REQUIRED:
    case QQmlJSGrammar::T_READONLY:
    case QQmlJSGrammar::T_FUNCTION:
    case QQmlJSGrammar::T_NUMERIC_LITERAL:
    case QQmlJSGrammar::T_LPAREN:
        enterState(StateType::Expression);
        // look at the token again
        tokenIndex -= 1;
        return true;
    default:
        if (Token::lexKindIsIdentifier(kind)) {
            enterState(StateType::ExpressionOrLabel);
            return true;
        } else if (Token::lexKindIsDelimiter(kind) || Token::lexKindIsStringType(kind)) {
            enterState(StateType::Expression);
            // look at the token again
            tokenIndex -= 1;
            return true;
        }
    }
    return false;
}

void FormatPartialStatus::dump() const
{
    qCDebug(formatterLog) << "Current token index" << tokenIndex;
    qCDebug(formatterLog) << "Current state:";
    for (const State &s : currentStatus.states)
        qCDebug(formatterLog) << FormatTextStatus::stateToString(s.type) << s.savedIndentDepth;
    qCDebug(formatterLog) << "Current lexerState:" << currentStatus.lexerState.state;
    qCDebug(formatterLog) << "Current indent:" << currentIndent;
}

void FormatPartialStatus::handleTokens()
{
    auto enter = [this](StateType newState) { this->enterState(newState); };

    auto leave = [this](bool statementDone = false) { this->leaveState(statementDone); };

    auto turnInto = [this](StateType newState) { this->turnIntoState(newState); };

    qCDebug(formatterLog) << "Starting to look at " << line;

    for (; tokenIndex < lineTokens.size();) {
        Token currentToken = tokenAt(tokenIndex);
        const int kind = currentToken.lexKind;

        qCDebug(formatterLog) << "Token: " << tokenText(currentToken);

        if (Token::lexKindIsComment(kind)
            && currentStatus.state().type != StateType::MultilineCommentCont
            && currentStatus.state().type != StateType::MultilineCommentStart) {
            tokenIndex += 1;
            continue;
        }

        switch (currentStatus.state().type) {
        case StateType::TopmostIntro:
            switch (kind) {
            case QQmlJSGrammar::T_IDENTIFIER:
                enter(StateType::ObjectdefinitionOrJs);
                continue;
            case QQmlJSGrammar::T_IMPORT:
                enter(StateType::TopQml);
                continue;
            case QQmlJSGrammar::T_LBRACE:
                enter(StateType::TopJs);
                enter(StateType::Expression);
                continue; // if a file starts with {, it's likely json
            default:
                enter(StateType::TopJs);
                continue;
            }
            break;

        case StateType::TopQml:
            switch (kind) {
            case QQmlJSGrammar::T_IMPORT:
                enter(StateType::ImportStart);
                break;
            case QQmlJSGrammar::T_IDENTIFIER:
                enter(StateType::BindingOrObjectdefinition);
                break;
            default:
                if (Token::lexKindIsIdentifier(kind))
                    enter(StateType::BindingOrObjectdefinition);
                break;
            }
            break;

        case StateType::TopJs:
            tryStatement();
            break;

        case StateType::ObjectdefinitionOrJs:
            switch (kind) {
            case QQmlJSGrammar::T_DOT:
                break;
            case QQmlJSGrammar::T_LBRACE:
                turnInto(StateType::BindingOrObjectdefinition);
                continue;
            default:
                if (!Token::lexKindIsIdentifier(kind) || !line.at(currentToken.begin()).isUpper()) {
                    turnInto(StateType::TopJs);
                    continue;
                }
            }
            break;

        case StateType::ImportStart:
            enter(StateType::ImportMaybeDotOrVersionOrAs);
            break;

        case StateType::ImportMaybeDotOrVersionOrAs:
            switch (kind) {
            case QQmlJSGrammar::T_DOT:
                turnInto(StateType::ImportDot);
                break;
            case QQmlJSGrammar::T_AS:
                turnInto(StateType::ImportAs);
                break;
            case QQmlJSGrammar::T_NUMERIC_LITERAL:
            case QQmlJSGrammar::T_VERSION_NUMBER:
                turnInto(StateType::ImportMaybeAs);
                break;
            default:
                leave();
                leave();
                continue;
            }
            break;

        case StateType::ImportMaybeAs:
            switch (kind) {
            case QQmlJSGrammar::T_AS:
                turnInto(StateType::ImportAs);
                break;
            default:
                leave();
                leave();
                continue;
            }
            break;

        case StateType::ImportDot:
            if (Token::lexKindIsIdentifier(kind)) {
                turnInto(StateType::ImportMaybeDotOrVersionOrAs);
            } else {
                leave();
                leave();
                continue;
            }
            break;

        case StateType::ImportAs:
            if (Token::lexKindIsIdentifier(kind)) {
                leave();
                leave();
            }
            break;

        case StateType::BindingOrObjectdefinition:
            switch (kind) {
            case QQmlJSGrammar::T_COLON:
                enter(StateType::BindingAssignment);
                break;
            case QQmlJSGrammar::T_LBRACE:
                enter(StateType::ObjectdefinitionOpen);
                break;
            }
            break;

        case StateType::BindingAssignment:
            switch (kind) {
            case QQmlJSGrammar::T_AUTOMATIC_SEMICOLON:
            case QQmlJSGrammar::T_COMPATIBILITY_SEMICOLON:
            case QQmlJSGrammar::T_SEMICOLON:
                leave(true);
                break;
            case QQmlJSGrammar::T_IF:
                enter(StateType::IfStatement);
                break;
            case QQmlJSGrammar::T_WITH:
                enter(StateType::StatementWithCondition);
                break;
            case QQmlJSGrammar::T_TRY:
                enter(StateType::TryStatement);
                break;
            case QQmlJSGrammar::T_SWITCH:
                enter(StateType::SwitchStatement);
                break;
            case QQmlJSGrammar::T_LBRACE:
                enter(StateType::JsblockOpen);
                break;
            case QQmlJSGrammar::T_ON:
            case QQmlJSGrammar::T_AS:
            case QQmlJSGrammar::T_IMPORT:
            case QQmlJSGrammar::T_SIGNAL:
            case QQmlJSGrammar::T_PROPERTY:
            case QQmlJSGrammar::T_REQUIRED:
            case QQmlJSGrammar::T_READONLY:
            case QQmlJSGrammar::T_IDENTIFIER:
                enter(StateType::ExpressionOrObjectdefinition);
                break;

                // error recovery
            case QQmlJSGrammar::T_RBRACKET:
            case QQmlJSGrammar::T_RPAREN:
                leave(true);
                break;

            default:
                enter(StateType::Expression);
                continue;
            }
            break;

        case StateType::ObjectdefinitionOpen:
            switch (kind) {
            case QQmlJSGrammar::T_RBRACE:
                leave(true);
                break;
            case QQmlJSGrammar::T_DEFAULT:
            case QQmlJSGrammar::T_READONLY:
                enter(StateType::PropertyModifiers);
                break;
            case QQmlJSGrammar::T_PROPERTY:
                enter(StateType::PropertyStart);
                break;
            case QQmlJSGrammar::T_REQUIRED:
                enter(StateType::RequiredProperty);
                break;
            case QQmlJSGrammar::T_COMPONENT:
                enter(StateType::ComponentStart);
                break;
            case QQmlJSGrammar::T_FUNCTION:
                enter(StateType::FunctionStart);
                break;
            case QQmlJSGrammar::T_SIGNAL:
                enter(StateType::SignalStart);
                break;
            case QQmlJSGrammar::T_ENUM:
                enter(StateType::EnumStart);
                break;
            case QQmlJSGrammar::T_ON:
            case QQmlJSGrammar::T_AS:
            case QQmlJSGrammar::T_IMPORT:
                enter(StateType::BindingOrObjectdefinition);
                break;
            default:
                if (Token::lexKindIsIdentifier(kind))
                    enter(StateType::BindingOrObjectdefinition);
                break;
            }
            break;

        case StateType::PropertyModifiers:
            switch (kind) {
            case QQmlJSGrammar::T_PROPERTY:
                turnInto(StateType::PropertyStart);
                break;
            case QQmlJSGrammar::T_DEFAULT:
            case QQmlJSGrammar::T_READONLY:
                break;
            case QQmlJSGrammar::T_REQUIRED:
                turnInto(StateType::RequiredProperty);
                break;
            default:
                leave(true);
                break;
            }
            break;

        case StateType::PropertyStart:
            switch (kind) {
            case QQmlJSGrammar::T_COLON:
                enter(StateType::BindingAssignment);
                break; // oops, was a binding
            case QQmlJSGrammar::T_VAR:
            case QQmlJSGrammar::T_IDENTIFIER:
                enter(StateType::PropertyName);
                break;
            default:
                bool kindIsIdentifier = Token::lexKindIsIdentifier(kind);
                if (kindIsIdentifier && tokenText(currentToken) == u"list") {
                    enter(StateType::PropertyListOpen);
                } else if (kindIsIdentifier) {
                    enter(StateType::PropertyName);
                } else {
                    leave(true);
                    continue;
                }
            }
            break;

        case StateType::RequiredProperty:
            switch (kind) {
            case QQmlJSGrammar::T_PROPERTY:
                turnInto(StateType::PropertyStart);
                break;
            case QQmlJSGrammar::T_DEFAULT:
            case QQmlJSGrammar::T_READONLY:
                turnInto(StateType::PropertyModifiers);
                break;
            case QQmlJSGrammar::T_IDENTIFIER:
                leave(true);
                break;
            default:
                leave(true);
                continue;
            }
            break;

        case StateType::ComponentStart:
            switch (kind) {
            case QQmlJSGrammar::T_IDENTIFIER:
                turnInto(StateType::ComponentName);
                break;
            default:
                leave(true);
                continue;
            }
            break;

        case StateType::ComponentName:
            switch (kind) {
            case QQmlJSGrammar::T_COLON:
                enter(StateType::BindingAssignment);
                break;
            default:
                leave(true);
                continue;
            }
            break;

        case StateType::PropertyName:
            turnInto(StateType::PropertyMaybeInitializer);
            break;

        case StateType::PropertyListOpen: {
            const QStringView tok = tokenText(currentToken);
            if (tok == u">")
                turnInto(StateType::PropertyName);
            break;
        }
        case StateType::PropertyMaybeInitializer:
            switch (kind) {
            case QQmlJSGrammar::T_COLON:
                turnInto(StateType::BindingAssignment);
                break;
            default:
                leave(true);
                continue;
            }
            break;

        case StateType::EnumStart:
            switch (kind) {
            case QQmlJSGrammar::T_LBRACE:
                enter(StateType::ObjectliteralOpen);
                break;
            }
            break;

        case StateType::SignalStart:
            switch (kind) {
            case QQmlJSGrammar::T_COLON:
                enter(StateType::BindingAssignment);
                break; // oops, was a binding
            default:
                enter(StateType::SignalMaybeArglist);
                break;
            }
            break;

        case StateType::SignalMaybeArglist:
            switch (kind) {
            case QQmlJSGrammar::T_LPAREN:
                turnInto(StateType::SignalArglistOpen);
                break;
            default:
                leave(true);
                continue;
            }
            break;

        case StateType::SignalArglistOpen:
            switch (kind) {
            case QQmlJSGrammar::T_RPAREN:
                leave(true);
                break;
            }
            break;

        case StateType::FunctionStart:
            switch (kind) {
            case QQmlJSGrammar::T_LPAREN:
                enter(StateType::FunctionArglistOpen);
                break;
            }
            break;

        case StateType::FunctionArglistOpen:
            switch (kind) {
            case QQmlJSGrammar::T_COLON:
                enter(StateType::TypeAnnotation);
                break;
            case QQmlJSGrammar::T_RPAREN:
                turnInto(StateType::FunctionArglistClosed);
                break;
            }
            break;

        case StateType::FunctionArglistClosed:
            switch (kind) {
            case QQmlJSGrammar::T_COLON:
                enter(StateType::TypeAnnotation);
                break;
            case QQmlJSGrammar::T_LBRACE:
                turnInto(StateType::JsblockOpen);
                break;
            default:
                leave(true);
                continue; // error recovery
            }
            break;

        case StateType::LambdaStart:
            switch (kind) {
            case QQmlJSGrammar::T_LBRACE:
                turnInto(StateType::JsblockOpen);
                break;
            default:
                turnInto(StateType::Expression);
            }
            break;

        case StateType::TypeAnnotation:
            switch (kind) {
            case QQmlJSGrammar::T_IDENTIFIER:
            case QQmlJSGrammar::T_DOT:
                break;
            case QQmlJSGrammar::T_LT:
                turnInto(StateType::TypeParameter);
                break;
            default:
                leave();
                continue; // error recovery
            }
            break;

        case StateType::TypeParameter:
            switch (kind) {
            case QQmlJSGrammar::T_LT:
                enter(StateType::TypeParameter);
                break;
            case QQmlJSGrammar::T_GT:
                leave();
                break;
            }
            break;

        case StateType::ExpressionOrObjectdefinition:
            switch (kind) {
            case QQmlJSGrammar::T_DOT:
                break; // need to become an objectdefinition_open in cases like "width: Qt.Foo
                // {"
            case QQmlJSGrammar::T_LBRACE:
                turnInto(StateType::ObjectdefinitionOpen);
                break;

                // propagate 'leave' from expression state
            case QQmlJSGrammar::T_RBRACKET:
            case QQmlJSGrammar::T_RPAREN:
                leave();
                continue;

            default:
                if (Token::lexKindIsIdentifier(kind))
                    break; // need to become an objectdefinition_open in cases like "width:
                // Qt.Foo
                enter(StateType::Expression);
                continue; // really? identifier and more tokens might already be gone
            }
            break;

        case StateType::ExpressionOrLabel:
            switch (kind) {
            case QQmlJSGrammar::T_COLON:
                turnInto(StateType::LabelledStatement);
                break;

                // propagate 'leave' from expression state
            case QQmlJSGrammar::T_RBRACKET:
            case QQmlJSGrammar::T_RPAREN:
                leave();
                continue;

            default:
                enter(StateType::Expression);
                continue;
            }
            break;

        case StateType::TernaryOp:
            if (kind == QQmlJSGrammar::T_COLON) {
                enter(StateType::TernaryOpAfterColon);
                enter(StateType::ExpressionContinuation);
                break;
            }
            Q_FALLTHROUGH();
        case StateType::TernaryOpAfterColon:
        case StateType::Expression:
            if (tryInsideExpression(false))
                break;
            switch (kind) {
            case QQmlJSGrammar::T_COMMA:
                leave(true);
                break;
            case QQmlJSGrammar::T_RBRACKET:
            case QQmlJSGrammar::T_RPAREN:
                leave();
                continue;
            case QQmlJSGrammar::T_RBRACE:
                leave(true);
                continue;
            case QQmlJSGrammar::T_AUTOMATIC_SEMICOLON:
            case QQmlJSGrammar::T_COMPATIBILITY_SEMICOLON:
            case QQmlJSGrammar::T_SEMICOLON:
                leave(true);
                break;
            default:
                if (Token::lexKindIsDelimiter(kind))
                    enter(StateType::ExpressionContinuation);
                break;
            }
            break;

        case StateType::ExpressionContinuation:
            leave();
            continue;

        case StateType::ExpressionMaybeContinuation:
            switch (kind) {
            case QQmlJSGrammar::T_QUESTION:
            case QQmlJSGrammar::T_LBRACKET:
            case QQmlJSGrammar::T_LPAREN:
            case QQmlJSGrammar::T_LBRACE:
                leave();
                continue;
            default:
                leave(!Token::lexKindIsDelimiter(kind));
                continue;
            }
            break;

        case StateType::ParenOpen:
            if (tryInsideExpression(false))
                break;
            switch (kind) {
            case QQmlJSGrammar::T_RPAREN:
                leave();
                break;
            }
            break;

        case StateType::BracketOpen:
            if (tryInsideExpression(false))
                break;
            switch (kind) {
            case QQmlJSGrammar::T_COMMA:
                enter(StateType::BracketElementStart);
                break;
            case QQmlJSGrammar::T_RBRACKET:
                leave();
                break;
            }
            break;

        case StateType::ObjectliteralOpen:
            if (tryInsideExpression(false))
                break;
            switch (kind) {
            case QQmlJSGrammar::T_COLON:
                enter(StateType::ObjectliteralAssignment);
                break;
            case QQmlJSGrammar::T_RBRACKET:
            case QQmlJSGrammar::T_RPAREN:
                leave();
                continue; // error recovery
            case QQmlJSGrammar::T_RBRACE:
                leave(true);
                break;
            }
            break;

            // pretty much like expression, but ends with , or }
        case StateType::ObjectliteralAssignment:
            if (tryInsideExpression(false))
                break;
            switch (kind) {
            case QQmlJSGrammar::T_COMMA:
                leave();
                break;
            case QQmlJSGrammar::T_RBRACKET:
            case QQmlJSGrammar::T_RPAREN:
                leave();
                continue; // error recovery
            case QQmlJSGrammar::T_RBRACE:
                leave();
                continue; // so we also leave objectliteral_open
            default:
                if (Token::lexKindIsDelimiter(kind))
                    enter(StateType::ExpressionContinuation);
                break;
            }
            break;

        case StateType::BracketElementStart:
            if (Token::lexKindIsIdentifier(kind)) {
                turnInto(StateType::BracketElementMaybeObjectdefinition);
            } else {
                leave();
                continue;
            }
            break;

        case StateType::BracketElementMaybeObjectdefinition:
            switch (kind) {
            case QQmlJSGrammar::T_LBRACE:
                turnInto(StateType::ObjectdefinitionOpen);
                break;
            default:
                leave();
                continue;
            }
            break;

        case StateType::JsblockOpen:
        case StateType::SubstatementOpen:
            if (tryStatement())
                break;
            switch (kind) {
            case QQmlJSGrammar::T_RBRACE:
                leave(true);
                break;
            }
            break;

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

        case StateType::Substatement:
            // prefer substatement_open over block_open
            if (kind != QQmlJSGrammar::T_LBRACE) {
                if (tryStatement())
                    break;
            }
            switch (kind) {
            case QQmlJSGrammar::T_LBRACE:
                turnInto(StateType::SubstatementOpen);
                break;
            }
            break;

        case StateType::IfStatement:
            switch (kind) {
            case QQmlJSGrammar::T_LPAREN:
                enter(StateType::ConditionOpen);
                break;
            default:
                leave(true);
                break; // error recovery
            }
            break;

        case StateType::MaybeElse:
            switch (kind) {
            case QQmlJSGrammar::T_ELSE:
                turnInto(StateType::ElseClause);
                enter(StateType::Substatement);
                break;
            default:
                leave(true);
                continue;
            }
            break;

        case StateType::MaybeCatchOrFinally:
            switch (kind) {
            case QQmlJSGrammar::T_CATCH:
                turnInto(StateType::CatchStatement);
                break;
            case QQmlJSGrammar::T_FINALLY:
                turnInto(StateType::FinallyStatement);
                break;
            default:
                leave(true);
                continue;
            }
            break;

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

        case StateType::ConditionOpen:
            if (tryInsideExpression(false))
                break;
            switch (kind) {
            case QQmlJSGrammar::T_RPAREN:
                turnInto(StateType::Substatement);
                break;
            }
            break;

        case StateType::SwitchStatement:
        case StateType::CatchStatement:
        case StateType::StatementWithCondition:
            switch (kind) {
            case QQmlJSGrammar::T_LPAREN:
                enter(StateType::StatementWithConditionParenOpen);
                break;
            default:
                leave(true);
            }
            break;

        case StateType::StatementWithConditionParenOpen:
            if (tryInsideExpression(false))
                break;
            switch (kind) {
            case QQmlJSGrammar::T_RPAREN:
                turnInto(StateType::Substatement);
                break;
            }
            break;

        case StateType::TryStatement:
        case StateType::FinallyStatement:
            switch (kind) {
            case QQmlJSGrammar::T_LBRACE:
                enter(StateType::JsblockOpen);
                break;
            default:
                leave(true);
                break;
            }
            break;

        case StateType::DoStatement:
            switch (kind) {
            case QQmlJSGrammar::T_WHILE:
                break;
            case QQmlJSGrammar::T_LPAREN:
                enter(StateType::DoStatementWhileParenOpen);
                break;
            default:
                leave(true);
                continue; // error recovery
            }
            break;

        case StateType::DoStatementWhileParenOpen:
            if (tryInsideExpression(false))
                break;
            switch (kind) {
            case QQmlJSGrammar::T_RPAREN:
                leave();
                leave(true);
                break;
            }
            break;

        case StateType::BreakcontinueStatement:
            if (Token ::lexKindIsIdentifier(kind)) {
                leave(true);
            } else {
                leave(true);
                continue; // try again
            }
            break;

        case StateType::CaseStart:
            switch (kind) {
            case QQmlJSGrammar::T_COLON:
                turnInto(StateType::CaseCont);
                break;
            }
            break;

        case StateType::CaseCont:
            if (kind != QQmlJSGrammar::T_CASE && kind != QQmlJSGrammar::T_DEFAULT && tryStatement())
                break;
            switch (kind) {
            case QQmlJSGrammar::T_RBRACE:
                leave();
                continue;
            case QQmlJSGrammar::T_DEFAULT:
            case QQmlJSGrammar::T_CASE:
                leave();
                continue;
            }
            break;

        case StateType::MultilineCommentStart:
        case StateType::MultilineCommentCont:
            if (!Token::lexKindIsComment(kind)) {
                leave();
                continue;
            } else if (tokenIndex == lineTokens.size() - 1
                       && !currentStatus.lexerState.isMultiline()) {
                leave();
            } else if (tokenIndex == 0) {
                // to allow enter/leave to update the indentDepth
                turnInto(StateType::MultilineCommentCont);
            }
            break;

        default:
            qWarning() << "Unhandled state" << currentStatus.state().typeStr();
            break;
        } // end of state switch

        ++tokenIndex;
    }

    StateType topState = currentStatus.state().type;

    // if there's no colon on the same line, it's not a label
    if (topState == StateType::ExpressionOrLabel)
        enterState(StateType::Expression);
    // if not followed by an identifier on the same line, it's done
    else if (topState == StateType::BreakcontinueStatement)
        leaveState(true);

    topState = currentStatus.state().type;

    // some states might be continued on the next line
    if (topState == StateType::Expression || topState == StateType::ExpressionOrObjectdefinition
        || topState == StateType::ObjectliteralAssignment
        || topState == StateType::TernaryOpAfterColon) {
        enterState(StateType::ExpressionMaybeContinuation);
    }
    // multi-line comment start?
    if (topState != StateType::MultilineCommentStart && topState != StateType::MultilineCommentCont
        && currentStatus.lexerState.state.tokenKind == QQmlJSGrammar::T_PARTIAL_COMMENT) {
        enterState(StateType::MultilineCommentStart);
    }
    currentStatus.finalIndent = currentIndent;
}

// adjusts the indentation of the current line based on the status of the previous one, and what
// it starts with
int indentForLineStartingWithToken(const FormatTextStatus &oldStatus, const FormatOptions &,
                                   int tokenKind)
{
    State topState = oldStatus.state();
    State previousState = oldStatus.state(1);
    int indentDepth = oldStatus.finalIndent;

    // keep user-adjusted indent in multiline comments
    if (topState.type == StateType::MultilineCommentStart
        || topState.type == StateType::MultilineCommentCont) {
        if (!Token::lexKindIsInvalid(tokenKind))
            return -1;
    }
    // don't touch multi-line strings at all
    if (oldStatus.lexerState.state.tokenKind == QQmlJSGrammar::T_PARTIAL_DOUBLE_QUOTE_STRING_LITERAL
        || oldStatus.lexerState.state.tokenKind == QQmlJSGrammar::T_PARTIAL_SINGLE_QUOTE_STRING_LITERAL
        || oldStatus.lexerState.state.tokenKind == QQmlJSGrammar::T_PARTIAL_TEMPLATE_HEAD
        || oldStatus.lexerState.state.tokenKind == QQmlJSGrammar::T_PARTIAL_TEMPLATE_MIDDLE) {
        return -1;
    }

    switch (tokenKind) {
    case QQmlJSGrammar::T_LBRACE:
        if (topState.type == StateType::Substatement
            || topState.type == StateType::BindingAssignment
            || topState.type == StateType::CaseCont) {
            return topState.savedIndentDepth;
        }
        break;
    case QQmlJSGrammar::T_RBRACE: {
        if (topState.type == StateType::JsblockOpen && previousState.type == StateType::CaseCont) {
            return previousState.savedIndentDepth;
        }
        for (int i = 0; oldStatus.state(i).type != StateType::TopmostIntro; ++i) {
            const StateType type = oldStatus.state(i).type;
            if (type == StateType::ObjectdefinitionOpen || type == StateType::JsblockOpen
                || type == StateType::SubstatementOpen || type == StateType::ObjectliteralOpen) {
                return oldStatus.state(i).savedIndentDepth;
            }
        }
        break;
    }
    case QQmlJSGrammar::T_RBRACKET:
        for (int i = 0; oldStatus.state(i).type != StateType::TopmostIntro; ++i) {
            const StateType type = oldStatus.state(i).type;
            if (type == StateType::BracketOpen) {
                return oldStatus.state(i).savedIndentDepth;
            }
        }
        break;
    case QQmlJSGrammar::T_LBRACKET:
    case QQmlJSGrammar::T_LPAREN:
        if (topState.type == StateType::ExpressionMaybeContinuation)
            return topState.savedIndentDepth;
        break;
    case QQmlJSGrammar::T_ELSE:
        if (topState.type == StateType::MaybeElse) {
            return oldStatus.state(1).savedIndentDepth;
        } else if (topState.type == StateType::ExpressionMaybeContinuation) {
            bool hasElse = false;
            for (int i = 1; oldStatus.state(i).type != StateType::TopmostIntro; ++i) {
                const StateType type = oldStatus.state(i).type;
                if (type == StateType::ElseClause)
                    hasElse = true;
                if (type == StateType::IfStatement) {
                    if (hasElse) {
                        hasElse = false;
                    } else {
                        return oldStatus.state(i).savedIndentDepth;
                    }
                }
            }
        }
        break;
    case QQmlJSGrammar::T_CATCH:
    case QQmlJSGrammar::T_FINALLY:
        if (topState.type == StateType::MaybeCatchOrFinally)
            return oldStatus.state(1).savedIndentDepth;
        break;
    case QQmlJSGrammar::T_COLON:
        if (topState.type == StateType::TernaryOp)
            return indentDepth - 2;
        break;
    case QQmlJSGrammar::T_QUESTION:
        if (topState.type == StateType::ExpressionMaybeContinuation)
            return topState.savedIndentDepth;
        break;

    case QQmlJSGrammar::T_DEFAULT:
    case QQmlJSGrammar::T_CASE:
        for (int i = 0; oldStatus.state(i).type != StateType::TopmostIntro; ++i) {
            const StateType type = oldStatus.state(i).type;
            if (type == StateType::SwitchStatement || type == StateType::CaseCont) {
                return oldStatus.state(i).savedIndentDepth;
            } else if (type == StateType::TopmostIntro) {
                break;
            }
        }
        break;
    default:
        if (Token::lexKindIsDelimiter(tokenKind)
            && topState.type == StateType::ExpressionMaybeContinuation)
            return topState.savedIndentDepth;

        break;
    }
    return indentDepth;
}

// sets currentIndent to the correct indent for the current line
int FormatPartialStatus::indentLine()
{
    Q_ASSERT(currentStatus.size() >= 1);
    int firstToken = (lineTokens.isEmpty() ? QQmlJSGrammar::T_NONE : tokenAt(0).lexKind);
    int indent = indentForLineStartingWithToken(initialStatus, options, firstToken);
    recalculateWithIndent(indent);
    return indent;
}

int FormatPartialStatus::indentForNewLineAfter() const
{
    // should be just currentIndent?
    int indent = indentForLineStartingWithToken(currentStatus, options, QQmlJSGrammar::T_NONE);
    if (indent < 0)
        return currentIndent;
    return indent;
}

void FormatPartialStatus::recalculateWithIndent(int indent)
{
    if (indent >= 0) {
        indentOffset = 0;
        int i = 0;
        while (i < line.size() && line.at(i).isSpace())
            ++i;
        indentOffset = indent - column(i);
    }
    currentIndent = initialStatus.finalIndent;
    auto lexerState = currentStatus.lexerState;
    currentStatus = initialStatus;
    currentStatus.lexerState = lexerState;
    tokenIndex = 0;
    handleTokens();
}

FormatPartialStatus formatCodeLine(QStringView line, const FormatOptions &options,
                                   const FormatTextStatus &initialStatus)
{
    FormatPartialStatus status(line, options, initialStatus);

    status.handleTokens();

    return status;
}

void FormatPartialStatus::defaultOnEnter(StateType newState, int *indentDepth,
                                         int *savedIndentDepth) const
{
    const State &parentState = currentStatus.state();
    const Token &tk = tokenAt(tokenIndex);
    const int tokenPosition = column(tk.begin());
    const bool firstToken = (tokenIndex == 0);
    const bool lastToken = (tokenIndex == lineTokens.size() - 1);

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

        if (firstToken)
            *savedIndentDepth = tokenPosition;

        *indentDepth = *savedIndentDepth + options.indentSize;
        break;
    }

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

    case StateType::BindingAssignment:
    case StateType::ObjectliteralAssignment:
        if (lastToken)
            *indentDepth = *savedIndentDepth + options.indentSize;
        else
            *indentDepth = column(tokenAt(tokenIndex + 1).begin());
        break;

    case StateType::ExpressionOrObjectdefinition:
        *indentDepth = tokenPosition;
        break;

    case StateType::ExpressionOrLabel:
        if (*indentDepth == tokenPosition)
            *indentDepth += 2 * options.indentSize;
        else
            *indentDepth = tokenPosition;
        break;

    case StateType::Expression:
        if (*indentDepth == tokenPosition) {
            // expression_or_objectdefinition doesn't want the indent
            // expression_or_label already has it
            if (parentState.type != StateType::ExpressionOrObjectdefinition
                && parentState.type != StateType::ExpressionOrLabel
                && parentState.type != StateType::BindingAssignment) {
                *indentDepth += 2 * options.indentSize;
            }
        }
        // expression_or_objectdefinition and expression_or_label have already consumed the
        // first token
        else if (parentState.type != StateType::ExpressionOrObjectdefinition
                 && parentState.type != StateType::ExpressionOrLabel) {
            *indentDepth = tokenPosition;
        }
        break;

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

    case StateType::BracketOpen:
        if (parentState.type == StateType::Expression
            && currentStatus.state(1).type == StateType::BindingAssignment) {
            *savedIndentDepth = currentStatus.state(2).savedIndentDepth;
            *indentDepth = *savedIndentDepth + options.indentSize;
        } else if (parentState.type == StateType::ObjectliteralAssignment) {
            *savedIndentDepth = parentState.savedIndentDepth;
            *indentDepth = *savedIndentDepth + options.indentSize;
        } else if (!lastToken) {
            *indentDepth = tokenPosition + 1;
        } else {
            *indentDepth = *savedIndentDepth + options.indentSize;
        }
        break;

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

    case StateType::DoStatementWhileParenOpen:
    case StateType::StatementWithConditionParenOpen:
    case StateType::SignalArglistOpen:
    case StateType::FunctionArglistOpen:
    case StateType::ParenOpen:
        if (!lastToken)
            *indentDepth = tokenPosition + 1;
        else
            *indentDepth += options.indentSize;
        break;

    case StateType::TernaryOp:
        if (!lastToken)
            *indentDepth = tokenPosition + tk.length + 1;
        else
            *indentDepth += options.indentSize;
        break;

    case StateType::JsblockOpen:
        // closing brace should be aligned to case
        if (parentState.type == StateType::CaseCont) {
            *savedIndentDepth = parentState.savedIndentDepth;
            break;
        }
        Q_FALLTHROUGH();
    case StateType::SubstatementOpen:
        // special case for "foo: {" and "property int foo: {"
        if (parentState.type == StateType::BindingAssignment)
            *savedIndentDepth = currentStatus.state(1).savedIndentDepth;
        *indentDepth = *savedIndentDepth + options.indentSize;
        break;

    case StateType::Substatement:
        *indentDepth += options.indentSize;
        break;

    case StateType::ObjectliteralOpen:
        if (parentState.type == StateType::Expression
            || parentState.type == StateType::ObjectliteralAssignment) {
            // undo the continuation indent of the expression
            if (currentStatus.state(1).type == StateType::ExpressionOrLabel)
                *indentDepth = currentStatus.state(1).savedIndentDepth;
            else
                *indentDepth = parentState.savedIndentDepth;
            *savedIndentDepth = *indentDepth;
        }
        *indentDepth += options.indentSize;
        break;

    case StateType::StatementWithCondition:
    case StateType::TryStatement:
    case StateType::CatchStatement:
    case StateType::FinallyStatement:
    case StateType::IfStatement:
    case StateType::DoStatement:
    case StateType::SwitchStatement:
        if (firstToken || parentState.type == StateType::BindingAssignment)
            *savedIndentDepth = tokenPosition;
        // ### continuation
        *indentDepth = *savedIndentDepth; // + 2*options.indentSize;
        // special case for 'else if'
        if (!firstToken && newState == StateType::IfStatement
            && parentState.type == StateType::Substatement
            && currentStatus.state(1).type == StateType::ElseClause) {
            *indentDepth = currentStatus.state(1).savedIndentDepth;
            *savedIndentDepth = *indentDepth;
        }
        break;

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

    case StateType::ConditionOpen:
        // fixed extra indent when continuing 'if (', but not for 'else if ('
        if (tokenPosition <= *indentDepth + options.indentSize)
            *indentDepth += 2 * options.indentSize;
        else
            *indentDepth = tokenPosition + 1;
        break;

    case StateType::CaseStart:
        *savedIndentDepth = tokenPosition;
        break;

    case StateType::CaseCont:
        *indentDepth += options.indentSize;
        break;

    case StateType::MultilineCommentStart:
        *indentDepth = tokenPosition + 2;
        break;

    case StateType::MultilineCommentCont:
        *indentDepth = tokenPosition;
        break;
    default:
        break;
    }
}

} // namespace Dom
} // namespace QQmlJS
QT_END_NAMESPACE
