#include "tree_sitter/parser.h"

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <wctype.h>

// Custom punctuation check for WASM compatibility
// ispunct() is not in tree-sitter's WASM allowed functions list
// https://github.com/tree-sitter/tree-sitter/blob/master/lib/src/wasm/stdlib-symbols.txt
static inline bool is_punct_char(const uint32_t chr)
{
    if (chr >= 0x80) {
        return false;
    }

    return (chr >= 33 && chr <= 47) || // !"#$%&'()*+,-./
           (chr >= 58 && chr <= 64) || // :;<=>?@
           (chr >= 91 && chr <= 96) || // [\]^_`
           (chr >= 123 && chr <= 126); // {|}~
}

enum TokenType {
    COMMENT,
    LINE_CONTINUATION,
    COMMAND_NAME,
    COMMAND_ARGUMENT,
    SINGLE_QUOTE_STRING_START,
    SINGLE_QUOTE_STRING_END,
    DOUBLE_QUOTE_STRING_START,
    DOUBLE_QUOTE_STRING_END,
    FORMATTING_SEQUENCE,
    ESCAPE_SEQUENCE,
    STRING_CONTENT,
    ENTRY_DELIMITER,
    MULTIOUTPUT_VARIABLE_START,
    IDENTIFIER,
    CATCH_IDENTIFIER,
    TRANSPOSE,
    CTRANSPOSE,
    ERROR_SENTINEL,
};

typedef struct
{
    bool is_inside_command;
    bool line_continuation;
    bool is_shell_scape;
    char string_delimiter;
    bool generate_entry_delimiter;
} Scanner;

static const char* const keywords[] = {
    "arguments", "break",       "case",       "catch",  "classdef", "continue", "else", "elseif",
    "end",       "enumeration", "events",     "for",    "function", "global",   "if",   "methods",
    "otherwise", "parfor",      "persistent", "return", "spmd",     "switch",   "try",  "while",
};
static const size_t keywords_size = sizeof(keywords) / sizeof(keywords[0]);

static inline void advance(TSLexer* lexer)
{
    lexer->advance(lexer, false);
}

static inline void skip(TSLexer* lexer)
{
    lexer->advance(lexer, true);
}

static inline bool consume_char(char chr, TSLexer* lexer)
{
    if (lexer->lookahead != chr) {
        return false;
    }
    advance(lexer);
    return true;
}

static inline bool is_eol(const uint32_t chr)
{
    return chr == '\n' || chr == '\r' || chr == ',' || chr == ';';
}

static inline bool iswspace_matlab(const uint32_t chr)
{
    return iswspace(chr) && chr != '\n' && chr != '\r';
}

static inline bool is_identifier(const uint32_t chr, const bool start)
{
    // isalpha or isdigit is SIGSEGVing os some UTF-8 chars, like U+10C6BD
    // (0xF48C9ABD), a file with just those bytes shows the problem.
    if (chr >= 0x80) {
        return false;
    }

    const bool alpha = iswalpha(chr);
    const bool numeric = !start && iswdigit(chr);
    const bool special = chr == '_';

    return alpha || numeric || special;
}

static inline void consume_identifier(TSLexer* lexer, char* buffer)
{
    size_t size = 0;
    if (is_identifier(lexer->lookahead, true)) {
        buffer[size] = (char) lexer->lookahead;
        advance(lexer);
        while (is_identifier(lexer->lookahead, false)) {
            if (size == 255) {
                buffer[0] = 0;
                return;
            }
            buffer[++size] = (char) lexer->lookahead;
            advance(lexer);
        }
        return;
    }
    buffer[0] = 0;
}

static inline int skip_whitespaces(TSLexer* lexer)
{
    // 0b001 -> something skipped
    // 0b010 -> newline skipped
    // 0b100 -> newline was at the end of skipped sequence
    int skipped = 0;
    while (!lexer->eof(lexer) && iswspace(lexer->lookahead)) {
        skipped &= 0b011;
        if (lexer->lookahead == '\n' || lexer->lookahead == '\r') {
            skipped |= 0b111;
        } else {
            skipped |= 0b001;
        }
        skip(lexer);
    }
    return skipped;
}

static inline int consume_whitespaces(TSLexer* lexer)
{
    int skipped = 0;
    while (iswspace(lexer->lookahead)) {
        skipped &= 0b011;
        if (lexer->lookahead == '\n' || lexer->lookahead == '\r') {
            skipped |= 0b111;
        } else {
            skipped |= 0b001;
        }
        advance(lexer);
    }
    return skipped;
}

static inline void consume_whitespaces_once(TSLexer* lexer)
{
    while (!lexer->eof(lexer) && iswspace(lexer->lookahead)) {
        if (lexer->lookahead == '\n' || lexer->lookahead == '\r') {
            advance(lexer);
            break;
        }
        advance(lexer);
    }
}

void* tree_sitter_matlab_external_scanner_create()
{
    Scanner* scanner = calloc(1, sizeof(Scanner));
    return scanner;
}

void tree_sitter_matlab_external_scanner_destroy(void* payload)
{
    if (payload != NULL) {
        free(payload);
    }
}

unsigned tree_sitter_matlab_external_scanner_serialize(void* payload, char* buffer)
{
    Scanner* scanner = (Scanner*) payload;
    buffer[0] = (char) scanner->is_inside_command;
    buffer[1] = (char) scanner->line_continuation;
    buffer[2] = (char) scanner->is_shell_scape;
    buffer[3] = scanner->string_delimiter;
    buffer[4] = (char) scanner->generate_entry_delimiter;
    return 5;
}

void tree_sitter_matlab_external_scanner_deserialize(
    void* payload,
    const char* buffer,
    unsigned length)
{
    Scanner* scanner = (Scanner*) payload;
    if (length == 5) {
        scanner->is_inside_command = buffer[0];
        scanner->line_continuation = buffer[1];
        scanner->is_shell_scape = buffer[2];
        scanner->string_delimiter = buffer[3];
        scanner->generate_entry_delimiter = buffer[4];
    }
}

static inline void consume_comment_line(TSLexer* lexer)
{
    while (lexer->lookahead != '\n' && lexer->lookahead != '\r' && !lexer->eof(lexer)) {
        advance(lexer);
    }
}

// NOLINTNEXTLINE(*misc-no-recursion)
static bool scan_comment(
    Scanner* scanner,
    TSLexer* lexer,
    bool entry_delimiter,
    bool ctranspose,
    int skipped)
{
    lexer->mark_end(lexer);

    const bool percent = lexer->lookahead == '%';
    const bool line_continuation = lexer->lookahead == '.' && consume_char('.', lexer)
                                   && consume_char('.', lexer) && consume_char('.', lexer);
    const bool block = percent && consume_char('%', lexer) && consume_char('{', lexer);

    // Since we cannot look multiple chars ahead in the main function, this
    // ended up being handled here. It allows the correct detection of numbers
    // like .5 inside matrices/cells: [0 .5].
    if (entry_delimiter && !percent && !line_continuation) {
        if (iswdigit(lexer->lookahead)) {
            lexer->result_symbol = ENTRY_DELIMITER;
            return true;
        }
        if (lexer->lookahead == '\'') {
            advance(lexer);
            lexer->result_symbol = CTRANSPOSE;
            lexer->mark_end(lexer);
            return skipped == 0;
        }
        return false;
    }

    // We are inside a matrix/cell row and there is a line continuation, like this:
    // a = { 1 ...
    //       2 ...
    // }
    if (entry_delimiter && line_continuation) {
        consume_comment_line(lexer);
        consume_whitespaces(lexer);

        lexer->mark_end(lexer);
        lexer->result_symbol = LINE_CONTINUATION;

        const bool is_alpha = iswalpha(lexer->lookahead);
        const bool is_digit = iswdigit(lexer->lookahead);
        const bool is_meta = lexer->lookahead == '?' || lexer->lookahead == '@';
        const bool is_quote = lexer->lookahead == '\'' || lexer->lookahead == '"';
        const bool is_container = lexer->lookahead == '{' || lexer->lookahead == '['
                                  || lexer->lookahead == '(';

        if (lexer->lookahead == '~') {
            advance(lexer);
            scanner->generate_entry_delimiter = lexer->lookahead != '=';
        } else if (lexer->lookahead == '+' || lexer->lookahead == '-') {
            advance(lexer);
            scanner->generate_entry_delimiter = lexer->lookahead != ' ';
        } else if (lexer->lookahead == '.') {
            advance(lexer);
            scanner->generate_entry_delimiter = is_digit;
        } else if (is_alpha || is_digit || is_quote || is_container || is_meta) {
            scanner->generate_entry_delimiter = true;
        }
        return true;
    }

    if (block) {
        if (skipped & 2) {
            return false;
        }

        // If it has things on the same line, it's not a block, just a comment
        while (!lexer->eof(lexer) && iswspace_matlab(lexer->lookahead)) {
            advance(lexer);
        }
        if (!consume_char('\n', lexer) && !consume_char('\r', lexer)) {
            consume_comment_line(lexer);
            lexer->result_symbol = COMMENT;
            lexer->mark_end(lexer);
            return true;
        }

        // Otherwise, find the matching closing block
        int level = 1;
        while (!lexer->eof(lexer)) {
            consume_whitespaces(lexer);
            if (consume_char('%', lexer)) {
                if (consume_char('{', lexer) && (consume_whitespaces(lexer) & 2)) {
                    level++;
                } else if (consume_char('}', lexer)) {
                    lexer->mark_end(lexer);
                    if (consume_whitespaces(lexer) & 2) {
                        level--;
                    }
                }
                if (level == 0) {
                    break;
                }
                continue;
            }
            consume_comment_line(lexer);
            lexer->mark_end(lexer);
        }

        lexer->result_symbol = COMMENT;
        return true;
    }

    if (percent || line_continuation) {
        consume_comment_line(lexer);
        lexer->mark_end(lexer);

        if (!line_continuation) {
            lexer->result_symbol = COMMENT;
            advance(lexer);
        } else {
            lexer->result_symbol = LINE_CONTINUATION;
            consume_whitespaces_once(lexer);
            lexer->mark_end(lexer);
            return true;
        }

        // Merges consecutive comments into one token, unless they are
        // separated by a newline.
        while (!lexer->eof(lexer) && (lexer->lookahead == ' ' || lexer->lookahead == '\t')) {
            advance(lexer);
        }

        if (lexer->lookahead == '%') {
            return scan_comment(scanner, lexer, false, false, 0);
        }

        return true;
    }

    if (ctranspose && lexer->lookahead == '\'') {
        advance(lexer);
        lexer->mark_end(lexer);
        lexer->result_symbol = CTRANSPOSE;
        return true;
    }

    return false;
}

static bool scan_command(Scanner* scanner, TSLexer* lexer, const bool* valid_symbols)
{
    // Special case: shell escape
    if (lexer->lookahead == '!') {
        advance(lexer);
        while (iswspace_matlab(lexer->lookahead)) {
            advance(lexer);
        }
        while (lexer->lookahead != ' ' && lexer->lookahead != '\n' && !lexer->eof(lexer)) {
            advance(lexer);
        }
        lexer->result_symbol = COMMAND_NAME;
        lexer->mark_end(lexer);
        while (iswspace_matlab(lexer->lookahead)) {
            advance(lexer);
        }
        scanner->is_inside_command = lexer->lookahead != '\n';
        scanner->is_shell_scape = scanner->is_inside_command;
        return true;
    }

    if (!is_identifier(lexer->lookahead, true)) {
        return false;
    }

    char buffer[256] = {0};
    consume_identifier(lexer, buffer);
    lexer->mark_end(lexer);
    const char* allowed_commands[] = {"methods", "arguments", "enumeration", "events"};
    if (buffer[0] != 0) {
        if (lexer->lookahead == '.') {
            // Since it is not followed by a space, it cannot be a command.
            if ((strcmp("get", buffer) == 0 || strcmp("set", buffer) == 0)) {
                return false;
            }
            // so it is ok to consume to identify a line continuation
            // NOLINTNEXTLINE(*misc-redundant-expression)
            if (consume_char('.', lexer) && consume_char('.', lexer) && consume_char('.', lexer)) {
                // If it is a keyword, yield to the internal scanner
                for (size_t i = 0; i < keywords_size; i++) {
                    if (strcmp(keywords[i], buffer) == 0) {
                        if (strcmp("enumeration", buffer) == 0) {
                            goto check_enumeration;
                        }
                        return false;
                    }
                }
            }
            lexer->result_symbol = IDENTIFIER;
            return true;
        }
        // The following keywords are allowed as commands if they get 1 argument
        for (unsigned i = 0; i < sizeof(allowed_commands) / sizeof(allowed_commands[0]); i++) {
            if (strcmp(allowed_commands[i], buffer) == 0) {
                goto check_command_for_argument;
            }
        }
        for (unsigned i = 0; i < keywords_size; i++) {
            if (strcmp(keywords[i], buffer) == 0) {
                return false;
            }
        }
    }
    goto skip_command_check;

check_enumeration: {
    const int skipped = consume_whitespaces(lexer);
    if (skipped & 2) {
        // enumeration can be a function
        if (lexer->lookahead == '(') {
            lexer->result_symbol = IDENTIFIER;
            return true;
        }
    }
    return false;
}

check_command_for_argument:
    // If this is a keyword-command, check if it has an argument.
    // If it has no arguments, this is a keyword, not a command.
    lexer->result_symbol = COMMAND_NAME;
    while (!lexer->eof(lexer) && iswspace_matlab(lexer->lookahead)) {
        advance(lexer);
    }
    if (is_identifier(lexer->lookahead, true)) {
        scanner->is_inside_command = true;
        return true;
    }
    return false;

skip_command_check:
    // First case: found an end-of-line already, so this is a command for sure.
    // example:
    // pwd
    // pwd;
    // pwd,
    if (is_eol(lexer->lookahead)) {
        lexer->result_symbol = valid_symbols[CATCH_IDENTIFIER] ? CATCH_IDENTIFIER : COMMAND_NAME;
        return true;
    }

    // If it's not followed by a space, it may be something else, like A' for
    // example. Or A+2.
    if (lexer->lookahead != ' ') {
        lexer->result_symbol = IDENTIFIER;
        return true;
    }

    // If followed by a line continuation, look after it
    const int skipped = consume_whitespaces(lexer);
    if (skipped & 2) {
        // `catch e `
        if (valid_symbols[CATCH_IDENTIFIER]) {
            lexer->result_symbol = CATCH_IDENTIFIER;
            return true;
        }
        // Command followed by spaces then newline
        scanner->is_inside_command = false;
        lexer->result_symbol = COMMAND_NAME;
        return true;
    }
    if (lexer->lookahead == '.' && consume_char('.', lexer) && consume_char('.', lexer)
        && consume_char('.', lexer)) {
        lexer->result_symbol = IDENTIFIER;
        return true;
    }

    // If it is followed by a space, it doesn't mean it's a command yet.
    // It could be A + 2 or A = 2. Let's check what is the first char after
    // all whitespaces. We mark it already as this is the right place, and we
    // only need to make sure this is a command and not something else from
    // this point on.
    lexer->result_symbol = COMMAND_NAME;
    while (!lexer->eof(lexer) && iswspace_matlab(lexer->lookahead)) {
        advance(lexer);
    }

    // Check for end-of-line again, since it may be that the user just put a
    // space at the end, like `pwd ;`
    if (is_eol(lexer->lookahead) || lexer->lookahead == '%') {
        if (valid_symbols[CATCH_IDENTIFIER] && (skipped & 4) == 0) {
            lexer->result_symbol = CATCH_IDENTIFIER;
            return true;
        }
        scanner->is_inside_command = true;
        return true;
    }

    // The first char of the first argument cannot be /=()/
    if (lexer->lookahead == '=' || lexer->lookahead == '(' || lexer->lookahead == ')') {
        lexer->result_symbol = IDENTIFIER;
        return true;
    }

    // If it is a single quote, it is a command.
    if (lexer->lookahead == '\'') {
        scanner->is_inside_command = true;
        return true;
    }

    // If it is an identifier char, then it's a command
    if (is_identifier(lexer->lookahead, false)) {
        scanner->is_inside_command = true;
        return true;
    }

    // If it is a char greater than 0xC0, then assume it's a valid UTF-8
    // char, and that this is a command.
    if (lexer->lookahead >= 0xC0) {
        scanner->is_inside_command = true;
        return true;
    }

    // Let's now consider punctuation marks.
    if (is_punct_char(lexer->lookahead)) {
        // In this case, we advance and look at what comes next too.
        const uint32_t first = lexer->lookahead;
        advance(lexer);
        const uint32_t second = lexer->lookahead;

        // If it's the end-of-line, then it's a command.
        if (is_eol(second)) {
            scanner->is_inside_command = true;
            return true;
        }

        if (iswspace_matlab(second)) {
            // If it is a space, then it depends on what we have, since
            // `disp + ;` is a valid command but `disp + 2;` isn't.
            const char operators[] = {
                '!',
                '&',
                '*',
                '+',
                '-',
                '/',
                '<',
                '>',
                '@',
                '\\',
                '^',
                '|',
            };
            bool is_invalid = false;
            for (size_t i = 0; i < sizeof(operators); i++) {
                if (first == (uint32_t) operators[i]) {
                    is_invalid = true;
                    break;
                }
            }
            // If it is an operator, this can only be a command if there
            // are no further arguments.
            if (is_invalid) {
                advance(lexer);
                while (iswspace_matlab(lexer->lookahead)) {
                    advance(lexer);
                }
                scanner->is_inside_command = is_eol(lexer->lookahead);
                lexer->result_symbol = scanner->is_inside_command ? COMMAND_NAME : IDENTIFIER;
                return true;
            }

            // If it's not an operator, then this is a command.
            scanner->is_inside_command = true;
            return true;
        }

        // Now we check for the rest of the operators.
        // Since they have 2 digits, it matters if the next is a space.
        advance(lexer);

        if (lexer->lookahead != ' ') {
            scanner->is_inside_command = true;
            return true;
        }

        const char operators[][2] = {
            {'&', '&'},
            {'|', '|'},
            {'=', '='},
            {'~', '='},
            {'<', '='},
            {'>', '='},
            {'.', '+'},
            {'.', '-'},
            {'.', '*'},
            {'.', '/'},
            {'.', '\\'},
            {'.', '^'},
        };

        for (int i = 0; i < 12; i++) {
            if ((uint32_t) operators[i][0] == first && (uint32_t) operators[i][1] == second) {
                lexer->result_symbol = IDENTIFIER;
                return true;
            }
        }

        scanner->is_inside_command = true;
        return true;
    }

    return false;
}

static bool scan_command_argument(Scanner* scanner, TSLexer* lexer)
{
    // If this is a shell escape command, we just break arguments in spaces
    // since we don't know what shell it is.
    if (scanner->is_shell_scape) {
        if (lexer->eof(lexer)) {
            return false;
        }

        while (lexer->lookahead != ' ' && lexer->lookahead != '\n' && !lexer->eof(lexer)) {
            advance(lexer);
        }
        lexer->result_symbol = COMMAND_ARGUMENT;
        lexer->mark_end(lexer);
        while (iswspace_matlab(lexer->lookahead)) {
            advance(lexer);
        }
        if (lexer->lookahead == '\n') {
            scanner->is_inside_command = false;
            scanner->is_shell_scape = false;
        }
        return true;
    }

    // Avoids infinite loop when the argument is right before the eof.
    if (lexer->eof(lexer)) {
        return false;
    }

    bool quote = false;
    int32_t parens = 0;
    bool consumed = false;

    while (!lexer->eof(lexer)) {
        // No matter what, found new line
        const bool cond1 = lexer->lookahead == '\n' || lexer->lookahead == '\r';
        // No quotes, no parens, found $._end_of_line or space
        const bool cond2 = !quote && parens == 0
                           && (is_eol(lexer->lookahead) || iswspace_matlab(lexer->lookahead));
        // Inside parens, no quotes, found ;
        const bool cond3 = !quote && parens != 0 && lexer->lookahead == ';';
        if (cond1 || cond2 || cond3) {
            lexer->result_symbol = COMMAND_ARGUMENT;
            lexer->mark_end(lexer);

            while (iswspace_matlab(lexer->lookahead)) {
                advance(lexer);
            }

            if (is_eol(lexer->lookahead) || cond1) {
                scanner->line_continuation = false;
                scanner->is_inside_command = false;
            }

            return true;
        }

        // Line comment, finish.
        if ((!quote || (quote && parens != 0)) && lexer->lookahead == '%') {
            scanner->is_inside_command = false;
            if (consumed) {
                lexer->result_symbol = COMMAND_ARGUMENT;
                lexer->mark_end(lexer);
                return true;
            }
            return scan_comment(scanner, lexer, false, false, 0);
        }

        // Line continuation
        if ((!quote || (quote && parens != 0)) && lexer->lookahead == '.') {
            lexer->result_symbol = COMMAND_ARGUMENT;
            lexer->mark_end(lexer);
            advance(lexer);
            if (lexer->lookahead == '.') {
                advance(lexer);
                if (lexer->lookahead == '.') {
                    if (consumed) {
                        scanner->line_continuation = true;
                    } else {
                        consume_comment_line(lexer);
                        lexer->result_symbol = LINE_CONTINUATION;
                        lexer->mark_end(lexer);
                    }
                    return true;
                }
                consumed = true;
                continue;
            }
            consumed = true;
            continue;
        }

        if ((lexer->lookahead == '(' || lexer->lookahead == '[' || lexer->lookahead == '{')
            && (!quote || (quote && parens != 0))) {
            parens++;
        }

        if ((lexer->lookahead == ')' || lexer->lookahead == ']' || lexer->lookahead == '}')
            && (!quote || (quote && parens != 0))) {
            parens--;
        }

        if (lexer->lookahead == '\'') {
            quote = !quote;
        }

        advance(lexer);
        consumed = true;
    }

    // Mark as argument so the scanner doesnt get called again in an infinite
    // loop.
    if (lexer->eof(lexer)) {
        lexer->result_symbol = COMMAND_ARGUMENT;
        lexer->mark_end(lexer);
        return true;
    }

    return false;
}

static bool scan_string_open(Scanner* scanner, TSLexer* lexer)
{
    switch (lexer->lookahead) {
    case '"':
        scanner->string_delimiter = '"';
        advance(lexer);
        lexer->result_symbol = DOUBLE_QUOTE_STRING_START;
        lexer->mark_end(lexer);
        return true;
    case '\'':
        scanner->string_delimiter = '\'';
        advance(lexer);
        lexer->result_symbol = SINGLE_QUOTE_STRING_START;
        lexer->mark_end(lexer);
        // A single quote string has to be ended in the same line.
        while (!lexer->eof(lexer) && lexer->lookahead != '\n') {
            if (lexer->lookahead == '\'') {
                return true;
            }
            advance(lexer);
        }
        return false;
    default:
        return false;
    }
}

static bool scan_string_close(Scanner* scanner, TSLexer* lexer)
{
    if (lexer->lookahead == scanner->string_delimiter) {
        advance(lexer);
        if (lexer->lookahead == scanner->string_delimiter) {
            advance(lexer);
            lexer->result_symbol = STRING_CONTENT;
            goto content;
        }
        lexer->result_symbol = scanner->string_delimiter == '"' ? DOUBLE_QUOTE_STRING_END
                                                                : SINGLE_QUOTE_STRING_END;
        lexer->mark_end(lexer);
        scanner->string_delimiter = 0;
        return true;
    }

    // This means this string is not properly terminated.
    if (lexer->lookahead == '\n' || lexer->lookahead == '\r' || lexer->eof(lexer)) {
        scanner->string_delimiter = 0;
        return false;
    }

    if (lexer->lookahead == '%') {
        advance(lexer);

        if (lexer->lookahead == '%') {
            advance(lexer);
            lexer->result_symbol = FORMATTING_SEQUENCE;
            lexer->mark_end(lexer);
            return true;
        }

        const char* valid_tokens = "1234567890.-+ #btcdeEfgGosuxX";
        const char* end_tokens = "cdeEfgGosuxX";
        while (!lexer->eof(lexer) && lexer->lookahead != '\n' && lexer->lookahead != '\r') {
            bool is_valid = false;
            for (size_t i = 0; i < strlen(valid_tokens); i++) {
                if ((int32_t) valid_tokens[i] == lexer->lookahead) {
                    is_valid = true;
                    break;
                }
            }

            if (!is_valid) {
                lexer->result_symbol = STRING_CONTENT;
                goto content;
            }

            for (int i = 0; i < 12; i++) {
                if (end_tokens[i] == lexer->lookahead) {
                    advance(lexer);
                    lexer->result_symbol = FORMATTING_SEQUENCE;
                    lexer->mark_end(lexer);
                    return true;
                }
            }

            advance(lexer);
        }

        scanner->string_delimiter = 0;
        return false;
    }

    if (lexer->lookahead == '\\') {
        advance(lexer);

        if (lexer->lookahead == 'x') {
            advance(lexer);
            while (!lexer->eof(lexer)) {
                const char* hexa_chars = "1234567890abcdefABCDEF";
                bool is_valid = false;
                for (int i = 0; i < 22; i++) {
                    if (hexa_chars[i] == lexer->lookahead) {
                        is_valid = true;
                        break;
                    }
                }

                if (!is_valid) {
                    lexer->result_symbol = ESCAPE_SEQUENCE;
                    lexer->mark_end(lexer);
                    return true;
                }

                advance(lexer);
            }
        }

        if (lexer->lookahead >= '0' && lexer->lookahead <= '7') {
            while (lexer->lookahead >= '0' && lexer->lookahead <= '7' && !lexer->eof(lexer)) {
                advance(lexer);
            }

            lexer->result_symbol = ESCAPE_SEQUENCE;
            lexer->mark_end(lexer);
            return true;
        }

        const char* escapes = "abfnrtv\\";
        bool is_valid = false;
        for (int i = 0; i < 8; i++) {
            if (escapes[i] == lexer->lookahead) {
                is_valid = true;
                break;
            }
        }

        if (is_valid) {
            advance(lexer);
            lexer->result_symbol = ESCAPE_SEQUENCE;
            lexer->mark_end(lexer);
            return true;
        }
    }

content:
    while (lexer->lookahead != '\n' && lexer->lookahead != '\r' && !lexer->eof(lexer)) {
        // In MATLAB '' and "" are valid inside their own kind: 'It''s ok' "He said ""it's ok"""
        if (lexer->lookahead == scanner->string_delimiter) {
            lexer->result_symbol = STRING_CONTENT;
            lexer->mark_end(lexer);
            advance(lexer);
            if (lexer->lookahead != scanner->string_delimiter) {
                return true;
            }
            advance(lexer);
            continue;
        }

        // The scanner will be called again, and this time we will match in the if
        // before this while.
        if (lexer->lookahead == '%' || lexer->lookahead == '\\') {
            lexer->result_symbol = STRING_CONTENT;
            lexer->mark_end(lexer);
            advance(lexer);
            if (lexer->lookahead == scanner->string_delimiter || iswspace_matlab(lexer->lookahead)) {
                goto content;
            }
            return true;
        }

        advance(lexer);
    }

    // Mark end of content here and end of string on next call. This is an
    // unterminated string and it's better to wrongly finish it here, otherwise
    // the error will appear god knows how many lines after this and it will be
    // hard for the user to understand what went wrong.
    if (lexer->lookahead == '\n' || lexer->lookahead == '\r' || lexer->eof(lexer)) {
        lexer->result_symbol = STRING_CONTENT;
        lexer->mark_end(lexer);
        return true;
    }

    scanner->string_delimiter = 0;
    return false;
}

static inline bool scan_multioutput_var_start(TSLexer* lexer)
{
    advance(lexer);
    lexer->result_symbol = MULTIOUTPUT_VARIABLE_START;
    lexer->mark_end(lexer);

    // We can have arrays inside function calls inside the multi-output variable, so we have to keep
    // track.
    unsigned sb_count = 0;

    while (!lexer->eof(lexer)) {
        // NOLINTNEXTLINE(*misc-redundant-expression)
        if (consume_char('.', lexer) && consume_char('.', lexer) && consume_char('.', lexer)) {
            consume_comment_line(lexer);
            advance(lexer);
        }

        if (lexer->lookahead == '[') {
            sb_count++;
            advance(lexer);
        }

        if (lexer->lookahead != ']') {
            advance(lexer);
        } else if (sb_count > 0) {
            sb_count--;
            advance(lexer);
        } else {
            break;
        }
    }

    if (lexer->lookahead != ']') {
        return false;
    }

    advance(lexer);

    while (!lexer->eof(lexer)) {
        // NOLINTNEXTLINE(*misc-redundant-expression)
        if (consume_char('.', lexer) && consume_char('.', lexer) && consume_char('.', lexer)) {
            consume_comment_line(lexer);
            advance(lexer);
        } else if (iswspace_matlab(lexer->lookahead)) {
            advance(lexer);
        } else {
            break;
        }
    }

    if (lexer->lookahead == '=') {
        advance(lexer);
        if (lexer->lookahead != '=') {
            return true;
        }
    }

    return false;
}

static bool scan_identifier(TSLexer* lexer);
static bool scan_entry_delimiter(TSLexer* lexer, int skipped)
{
    lexer->mark_end(lexer);
    lexer->result_symbol = ENTRY_DELIMITER;

    if (skipped & 2) {
        return false;
    }

    if (lexer->lookahead == ',') {
        advance(lexer);
        lexer->mark_end(lexer);
        lexer->result_symbol = ENTRY_DELIMITER;
        return true;
    }

    if (lexer->lookahead == '.') {
        advance(lexer);
        advance(lexer);
        return iswdigit(lexer->lookahead);
    }

    if (lexer->lookahead == '{' || lexer->lookahead == '(' || lexer->lookahead == '\'') {
        return skipped != 0;
    }

    if (lexer->lookahead == '[') {
        return true;
    }

    // These chars mean we cannot end the cell here, as the expression will
    // surely continue OR we need to just leave the char there and the internal
    // parser will do the rest.
    const char no_end[] = {']', '}', '&', '|', '=', '<', '>', '*', '/', '\\', '^', ';', ':'};
    for (size_t i = 0; i < sizeof(no_end); i++) {
        if ((int32_t) no_end[i] == lexer->lookahead) {
            return false;
        }
    }

    if (lexer->lookahead == '~') {
        advance(lexer);
        return lexer->lookahead != '=';
    }

    const char maybe_end[] = {'+', '-'};
    for (size_t i = 0; i < sizeof(maybe_end); i++) {
        if ((int32_t) maybe_end[i] == lexer->lookahead) {
            advance(lexer);
            if (lexer->lookahead == ' ') {
                return false;
            }
            return skipped != 0;
        }
    }

    if (skipped != 0) {
        return true;
    }

    if (is_identifier(lexer->lookahead, true)) {
        return scan_identifier(lexer);
    }

    return false;
}

static bool scan_identifier(TSLexer* lexer)
{
    char buffer[256] = {0};
    consume_identifier(lexer, buffer);
    if (buffer[0] != 0) {
        if (lexer->lookahead == '.') {
            if ((strcmp("get", buffer) == 0 || strcmp("set", buffer) == 0)) {
                return false;
            }
            lexer->result_symbol = IDENTIFIER;
            lexer->mark_end(lexer);
            return true;
        }
        for (size_t i = 0; i < keywords_size; i++) {
            if (strcmp(keywords[i], buffer) == 0) {
                return false;
            }
        }
        lexer->result_symbol = IDENTIFIER;
        lexer->mark_end(lexer);
        return true;
    }
    return false;
}

static bool scan_transpose(TSLexer* lexer)
{
    if (lexer->lookahead == '\'') {
        advance(lexer);
        lexer->mark_end(lexer);
        lexer->result_symbol = TRANSPOSE;
        return true;
    }
    if (lexer->lookahead == '.' && consume_char('\'', lexer)) {
        advance(lexer);
        lexer->mark_end(lexer);
        lexer->result_symbol = CTRANSPOSE;
        return true;
    }
    return false;
}

bool tree_sitter_matlab_external_scanner_scan(void* payload, TSLexer* lexer, const bool* valid_symbols)
{
    Scanner* scanner = (Scanner*) payload;

    if (scanner->generate_entry_delimiter) {
        scanner->generate_entry_delimiter = false;
        lexer->mark_end(lexer);
        lexer->result_symbol = ENTRY_DELIMITER;
        return true;
    }

    if (scanner->string_delimiter == 0) {
        int skipped = skip_whitespaces(lexer);

        if ((scanner->line_continuation || !scanner->is_inside_command) && valid_symbols[COMMENT]
            && (lexer->lookahead == '%' || ((skipped & 2) == 0 && lexer->lookahead == '.'))) {
            return scan_comment(
                scanner, lexer, valid_symbols[ENTRY_DELIMITER], valid_symbols[CTRANSPOSE], skipped);
        }

        if (!scanner->is_inside_command) {
            if (skipped == 0 && valid_symbols[TRANSPOSE]) {
                if (scan_transpose(lexer)) {
                    return true;
                }
            }

            if ((valid_symbols[SINGLE_QUOTE_STRING_START] && lexer->lookahead == '\'')
                || (valid_symbols[DOUBLE_QUOTE_STRING_START] && lexer->lookahead == '"')) {
                return scan_string_open(scanner, lexer);
            }

            if (!scanner->line_continuation) {
                if (valid_symbols[MULTIOUTPUT_VARIABLE_START] && lexer->lookahead == '[') {
                    return scan_multioutput_var_start(lexer);
                }

                if (valid_symbols[ENTRY_DELIMITER]) {
                    return scan_entry_delimiter(lexer, skipped);
                }
            }

            if (valid_symbols[COMMAND_NAME]) {
                scanner->is_inside_command = false;
                scanner->is_shell_scape = false;
                return scan_command(scanner, lexer, valid_symbols);
            }

            if (valid_symbols[IDENTIFIER] && (skipped & 2) == 0) {
                scanner->is_inside_command = false;
                scanner->is_shell_scape = false;
                return scan_identifier(lexer);
            }
        } else {
            if (valid_symbols[COMMAND_ARGUMENT]) {
                return scan_command_argument(scanner, lexer);
            }
        }
    } else {
        if (valid_symbols[DOUBLE_QUOTE_STRING_END] || valid_symbols[SINGLE_QUOTE_STRING_END]
            || valid_symbols[FORMATTING_SEQUENCE]) {
            return scan_string_close(scanner, lexer);
        }
    }

    return false;
}
