// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: MIT

// cSpell: ignore mult prec

module.exports = grammar({
  name: "slint",

  extras: ($) => [/[\s\r\n]+/, $.line_comment, $.block_comment],
  conflicts: ($) => [
    [$._assignment_value_block],
    [$.assignment_block],
    // Caused by accepting arbitrary expressions in the radial-gradient/conical-gradient without a separator!
    [$.unary_prec_operator, $.add_prec_operator]
],

  externals: ($) => [$.block_comment],

  // Substituted into their call sites: as rules of their own they would add a
  // reduction that conflicts with $.anon_struct_assignment.
  inline: ($) => [$._statement_identifier, $._statement_type_identifier],

  rules: {
    sourcefile: ($) => repeat($._definition),

    _definition: ($) =>
      choice($.import_statement, $._export, $._local_type, $.rust_attr),

    _local_type: ($) =>
      choice(
        $.struct_definition,
        $.enum_definition,
        $.global_definition,
        $.interface_definition,
        $.component_definition,
      ),

    _export: ($) => prec.left(choice(
      $.exported_definition,
      $.export_statement
    )),

    export_type: ($) => seq(
      field("local_name", $._type_identifier),
      optional(seq("as", field("export_name", $._type_identifier))),
    ),

    exported_definition: ($) => seq(
      "export",
      $._local_type
    ),

    export_statement: ($) => seq(
      "export",
      "{", commaSep($.export_type), optional(","), "}",
      optional(seq("from", field("from", $.string_value), ";"))
    ),

    _rust_attr_args: ($) => seq("(", repeat(choice(/[^()"]+/, $.string_value, $._rust_attr_args)), ")"),

    rust_attr: ($) => seq(repeat1(seq("@rust-attr", $._rust_attr_args)), choice($.exported_definition, $._local_type)),

    import_statement: ($) =>
      seq(
        "import",
        optional(seq("{", commaSep($.import_type), optional(","), "}", "from")),
        $.string_value,
        ";",
      ),

    import_type: ($) =>
      seq(
        field("import_name", $.user_type_identifier),
        optional(seq("as", field("local_name", $.user_type_identifier))),
      ),

    component: ($) =>
      seq(
        optional(seq(field("id", $._statement_identifier), ":=")),
        field("type", $._statement_type_identifier),
        $.block,
      ),

    component_definition: ($) =>
      seq(
        choice(
          seq(
            // new syntax
            "component",
            field("name", $.user_type_identifier),
          ),
          seq(
            // old syntax
            field("name", $.user_type_identifier),
            ":=",
            field("base_type", $.user_type_identifier),
          ),
        ),
        repeat($.component_modifier),
        $.block,
      ),

    component_modifier: ($) => seq("inherits", field("base_type", $.user_type_identifier)),

    _property_type: ($) => seq("<", field("type", $.type), ">"),

    imperative_block: ($) =>
      seq("{", repeat($.imperative_statement), "}"),

    imperative_statement: ($) =>
      // use right-precedence here (e.g. prefer rules that end later)
      // Because we want to attach the trailing ";" to the statement,
      // and not parse it as a separate ";" statement.
      prec.right(
        1,
        choice(
          seq($.assignment_block, optional(";")),
          seq($.assignment_expr, optional(";")),
          seq($.if_expr, optional(";")),
          $.let_statement,
          seq($.expression, optional(";")),
          $.return_statement,
          // An empty/duplicate `;` is valid Slint
          ";",
        ),
      ),

    return_statement: ($) => seq("return", optional($.expression), ";"),

    _binding: ($) =>
      field(
        "binding",
        choice(seq($.imperative_block, optional(";")), seq($.expression, ";")),
      ),

    property_deprecation: ($) =>
      seq(
        "@deprecated",
        optional(seq("(", field("message", $.string_value), ")")),
      ),

    property: ($) =>
      seq(
        field("deprecation", optional($.property_deprecation)),
        field("visibility", optional($.property_visibility)),
        "property",
        seq(
          $._property_type,
          field("name", $.simple_identifier),
          choice(
            seq(field("binding_op", ":"), $._binding),
            seq(field("binding_op", "<=>"), $.expression, ";"),
            ";",
          ),
        ),
      ),

    binding_alias: ($) =>
      seq(
        field("deprecation", optional($.property_deprecation)),
        field("visibility", optional($.property_visibility)),
        optional("property"),
        field("name", $._statement_identifier),
        "<=>",
        field("alias", $.expression),
        ";",
      ),

    implement_statement: ($) =>
      seq(
        "implement",
        field("interface", $.user_type_identifier),
        "<=>",
        field("target", $.simple_identifier),
        ";",
      ),

    global_block: ($) =>
      seq(
        "{",
        repeat(
          choice(
            $.property,
            $.binding_alias,
            $.callback,
            $.callback_alias,
            $.callback_event,
            $.changed_event,
            $.function_definition,
          ),
        ),
        "}",
      ),

    global_definition: ($) =>
      seq(
        "global",
        field("name", $.user_type_identifier),
        optional(":="), // old syntax!
        $.global_block,
      ),

    interface_block: ($) =>
      seq(
        "{",
        repeat(choice($.property, $.callback, $.function_declaration)),
        "}",
      ),

    interface_definition: ($) =>
      seq("interface", field("name", $.user_type_identifier), $.interface_block),

    struct_field_definition: ($) =>
      seq(
        field("name", $.simple_identifier),
        ":",
        field("type", $.type),
        optional(seq("=", field("default_value", $.expression))),
      ),

    struct_block: ($) =>
      seq(
        "{",
        commaSep($.struct_field_definition),
        optional(","),
        "}",
      ),

    struct_definition: ($) =>
      seq(
        "struct",
        field("name", $.user_type_identifier),
        optional(":="), // old syntax!
        $.struct_block,
      ),

    enum_block: ($) =>
      seq(
        "{",
        commaSep(field("name", $.user_type_identifier)),
        optional(","),
        "}",
      ),

    enum_definition: ($) =>
      seq("enum", field("name", $.user_type_identifier), $.enum_block),

    anon_struct_assignment: ($) =>
      seq(
        field("member", $.simple_identifier),
        ":",
        field("value", $.expression)
      ),

    anon_struct_block: ($) =>
      prec(
        100,
        seq(
          "{",
          commaSep($.anon_struct_assignment),
          optional(","),
          "}",
        ),
      ),

    block: ($) => seq("{", repeat($._block_statement), "}"),

    _block_statement: ($) =>
      choice(
        $.animate_statement,
        $.binding_alias,
        $.callback,
        $.callback_alias,
        $.callback_event,
        $.changed_event,
        $.children_identifier, // No `;` after this one!
        $.component,
        $.for_loop,
        $.function_definition,
        $.if_statement,
        $.implement_statement,
        $.match_statement,
        $.property,
        $.property_assignment,
        $.slot_declaration,
        $.slot_assignment,
        $.slot_forwarding,
        $.states_definition,
        $.transitions_definition,
      ),

    slot_declaration: ($) =>
      seq("slot", field("name", $.simple_identifier), ";"),

    // `slot` is a contextual keyword: it only introduces a slot declaration when a
    // name follows it, so it stays a valid identifier everywhere else. As with
    // "changed" in $.callback_event, the lexer would otherwise always prefer the
    // "slot" keyword over the identifier regex, so alias it back to an identifier
    // in every statement that may start with one.
    _statement_identifier: ($) =>
      choice($.simple_identifier, alias("slot", $.simple_identifier)),

    _statement_type_identifier: ($) =>
      choice($.user_type_identifier, alias("slot", $.user_type_identifier)),

    slot_assignment: ($) =>
      seq(field("name", $._statement_identifier), "<<", $.component),

    slot_forwarding: ($) =>
      seq(field("name", $._statement_identifier), "<<", field("value", $.expression), ";"),

    property_assignment: ($) =>
      seq(
        field("property", $._statement_identifier),
        ":",
        field(
          "value",
          choice(
            seq($.imperative_block, optional(";")),
            seq($.expression, ";"),
          ),
        ),
      ),

    in_out_transition: ($) =>
      seq(
        choice("in", "out", "in-out"),
        optional(seq($.expression, ":")),
        "{",
        repeat($.animate_statement),
        "}",
      ),

    transitions_definition: ($) =>
      seq("transitions", "[", repeat($.in_out_transition), "]"),

    state_definition: ($) =>
      seq(
        field("name", $.simple_identifier),
        optional(seq("when", field("condition", $.expression))),
        ":",
        "{",
        repeat(
          choice(
            $.in_out_transition,
            $.assignment_block,
            seq($.assignment_expr, ";"),
          ),
        ),
        optional($.assignment_expr),
        "}",
      ),

    states_definition: ($) =>
      seq(
        "states",
        "[",
        repeat($.state_definition),
        "]",
      ),

    animate_statement: ($) =>
      seq("animate", choice("*", commaSep1($.expression)), $.animate_body),

    animate_option_identifier: (_) =>
      choice("delay", "duration", "iteration-count", "direction", "easing", "enabled"),

    animate_option: ($) =>
      seq(
        field("option", $.animate_option_identifier),
        ":",
        field("expression", $.expression),
        ";",
      ),

    animate_body: ($) => seq("{", repeat($.animate_option), "}"),

    if_expr: ($) =>
      seq(
        "if",
        field("condition", $.expression),
        $.imperative_block,
        optional(seq("else", choice($.if_expr, $.imperative_block))),
      ),

    if_statement: ($) =>
      seq("if", field("condition", $.expression), ":", $.component),

    for_loop: ($) =>
      seq(
        "for",
        field("identifier", $.simple_indexed_identifier),
        "in",
        field("range", $.for_range),
        ":",
        $.component,
      ),

    for_range: ($) => choice($.value_list, $.expression),

    match_statement: ($) =>
      seq("match",
        field("value", $.simple_identifier),
        "{",
        repeat($.match_case),
        optional($.wildcard_match_case),
        "}",
      ),

    match_case: ($) =>
      seq(
        field("case", choice($._basic_value,
          seq($.user_type_identifier, ".", $.user_type_identifier))),
        ":",
        choice($.component, seq("{", "}"))
      ),

    wildcard_match_case: ($) =>
      seq("*", ":", $.component),

    type_list: ($) => seq("[", commaSep($.type), optional(","), "]"),

    type: ($) => choice($._type_identifier, $.type_list, $.struct_block),

    let_statement: ($) =>
      seq(
        "let",
        field("name", $.simple_identifier),
        optional(seq(":", field("type", $.type))),
        "=",
        field("value", $.expression),
        ";",
      ),

    _assignment_setup: ($) =>
      seq(field("name", $.expression), field("op", $.assignment_prec_operator)),

    _assignment_value_block: ($) => field("value", seq($.block, optional(";"))),

    assignment_block: ($) =>
      seq($._assignment_setup, $._assignment_value_block, optional(";")),

    assignment_expr: ($) =>
      prec.right(2, seq($._assignment_setup, field("value", $.expression))),

    expression: ($) =>
      prec.right(
        choice(
          $.closure_expression,
          $.keys,
          $.parens_op,
          $.index_op,
          $.tr,
          $.markdown,
          $.adjacent_string_expression,
          $.value,
          $.gradient_call,
          $.image_call,
          $.reference_identifier,
          $.simple_identifier,
          $.function_call,
          $.member_access,
          $.unary_expression,
          $.binary_expression,
          $.ternary_expression,
        ),
      ),

    parens_op: ($) => seq("(", field("left", $.expression), ")"),

    closure_expression: ($) =>
      prec.right(
        1,
        seq(
          "(",
          field("argument", $.simple_identifier),
          $._closure_arrow,
          field("body", $.expression),
        ),
      ),

    // Keep `)` and `=>` together so `(foo)` remains a parenthesized expression
    // when no arrow follows.
    _closure_arrow: (_) => token(seq(")", /[\s\r\n]*/, "=>")),

    index_op: ($) =>
      prec(
        18,
        seq(
          field("left", $.expression),
          "[",
          field("index", $.expression),
          "]",
        ),
      ),

    // @tr(...)
    tr: ($) =>
      seq(
        "@tr",
        "(",
        optional(field("context", seq($.string_value, "=>"))),
        field("message", $.string_value),
        optional(
          seq(
            "|",
            field("pipe", $.string_value),
            "%",
            field("percent", $.expression),
          ),
        ),
        field(
          "arguments",
          optional(seq(",", commaSep1($.expression), optional(","))),
        ),
        ")",
      ),

    // @markdown(...)
    markdown: ($) => seq("@markdown", $.arguments),

    adjacent_string_expression: ($) =>
      prec.left(15, seq(choice($.string_value, $.adjacent_string_expression), $.string_value)),

    // @keys(...)
    _keys_entry: ($) => choice(seq($.simple_identifier, optional("?")), $.string_value),
    keys: ($) =>
      seq(
        "@keys",
        "(",
        optional(
          seq(
            $._keys_entry,
            repeat(seq(
              "+",
              $._keys_entry
            ))
          )),
        ")",
      ),

    member_access: ($) =>
      prec.left(
        17,
        seq(
          field("base", $.expression),
          ".",
          field(
            "member",
            $.expression,
          ),
        ),
      ),

    unary_expression: ($) =>
      prec.left(
        14,
        seq(field("op", $.unary_prec_operator), field("expr", $.expression)),
      ),

    binary_expression: ($) =>
      prec.left(
        1,
        choice(
          $._add_binary_expression,
          $._comparison_binary_expression,
          $._logic_binary_expression,
          $._mult_binary_expression,
        ),
      ),

    _add_binary_expression: ($) =>
      prec.left(
        11,
        seq(
          field("left", $.expression),
          field("op", $.add_prec_operator),
          field("right", $.expression),
        ),
      ),

    _comparison_binary_expression: ($) =>
      prec.left(
        9,
        seq(
          field("left", $.expression),
          field("op", $.comparison_operator),
          field("right", $.expression),
        ),
      ),

    logical_and: (_) => "&&",
    logical_or: (_) => "||",

    _logic_binary_expression: ($) =>
      choice(
        prec.left(
          4,
          seq(
            field("left", $.expression),
            field("op", $.logical_and),
            field("right", $.expression),
          ),
        ),
        prec.left(
          3,
          seq(
            field("left", $.expression),
            field("op", $.logical_or),
            field("right", $.expression),
          ),
        ),
      ),

    _mult_binary_expression: ($) =>
      prec.left(
        12,
        seq(
          field("left", $.expression),
          field("op", $.mult_prec_operator),
          field("right", $.expression),
        ),
      ),

    ternary_expression: ($) =>
      prec.left(
        3,
        seq(
          field("condition", $.expression),
          "?",
          field("left", $.expression),
          ":",
          field("right", $.expression),
        ),
      ),

    callback: ($) =>
      seq(
        optional($.purity),
        "callback",
        field("name", $.simple_identifier),
        optional($._callback_signature),
        optional(seq("->", field("return_type", $.type))),
        ";",
      ),

    purity: (_) => field("value", "pure"),

    function_visibility: (_) => field("value", choice("public", "private", "protected")),

    function_definition: ($) =>
      seq(
        repeat(choice($.purity, $.function_visibility)),
        "function",
        field("name", $.simple_identifier),
        optional($._function_signature),
        optional(seq("->", field("return_type", $.type))),
        $.imperative_block,
      ),

    function_declaration: ($) =>
      seq(
        repeat(choice($.purity, $.function_visibility)),
        "function",
        field("name", $.simple_identifier),
        optional($._function_signature),
        optional(seq("->", field("return_type", $.type))),
        ";",
      ),

    callback_alias: ($) =>
      seq(
        optional($.purity),
        "callback",
        field("name", $.simple_identifier),
        "<=>",
        field("alias", $.expression),
        ";",
      ),

    // We need to add an alias for the changed contextual keyword here.
    // in a $.block, the callback_event is competing with the changed_event.
    //
    // If Tree-sitter finds the text "changed", it will already at the lexer stage decide
    // that this is not an identifier because it matches the "changed" in changed_event,
    // which takes precedence over the regex-based identifier lexer rule.
    //
    // By using the alias here we can force Tree-sitter to treat it also as an identifier.
    callback_event: ($) =>
      seq(
        field("name", choice($._statement_identifier, alias("changed", $.simple_identifier))),
        optional(field("arguments", $.arguments)),
        "=>",
        field("action", $._binding),
      ),

    changed_event: ($) =>
      seq(
        "changed",
        field("property", $.simple_identifier),
        "=>",
        field("action", $._binding),
      ),

    function_call: ($) =>
      prec(
        17,
        seq(
          field("name", $.simple_identifier),
          field("arguments", $.arguments),
        ),
      ),

    gradient_call: ($) =>
      choice(
        seq(
          field("name", $.linear_gradient_identifier),
          "(",
          field(
            "arguments",
            seq(
              field("angle", $.expression),
              ",",
              field("colors", commaSep2($.gradient_color)),
              optional(","),
            ),
          ),
          ")",
        ),
        seq(
          field("name", $.radial_gradient_identifier),
          "(",
          field(
            "arguments",
            seq(
              field("type", $.radial_gradient_kind),
              optional(field("radius", $.length_value)),
              optional(seq(
                "at",
                field("center_x", $.expression),
                field("center_y", $.expression),
              )),
              ",",
              field("colors", commaSep2($.gradient_color)),
              optional(","),
            ),
          ),
          ")",
        ),
        seq(
          field("name", $.conic_gradient_identifier),
          "(",
          field(
            "arguments",
            seq(
              optional(seq("from", field("from_angle", $.angle_value))),
              optional(seq(
                "at",
                field("center_x", $.expression),
                field("center_y", $.expression),
              )),
              optional(","),
              field("colors", commaSep2($.conic_gradient_color)),
              optional(","),
            ),
          ),
          ")",
        ),
      ),

    gradient_color: ($) => seq($.argument, optional($.expression)),
    conic_gradient_color: ($) => seq($.argument, $.expression),

    image_call: ($) =>
      seq(
        field("name", "@image-url"),
        "(",
        field("image", $.string_value),
        optional(seq(",", "nine-slice", "(", repeat($.int_value), ")")),
        ")",
      ),

    typed_identifier: ($) =>
      seq(field("name", $.simple_identifier), ":", field("type", $.type)),

    _function_signature: ($) =>
      seq(
        "(",
        field(
          "arguments",
          optional(seq(commaSep1($.typed_identifier), optional(","))),
        ),
        ")",
      ),

    _callback_signature: ($) =>
      prec.left(
        seq(
          "(",
          field("arguments", optional(seq(commaSep1(choice($.typed_identifier, $.type)), optional(",")))),
          ")",
        ),
      ),

    argument: ($) => $.expression,

    arguments: ($) =>
      prec(
        17,
        seq("(", optional(seq(commaSep1($.argument), optional(","))), ")"),
      ),

    unary_prec_operator: (_) => choice("!", "-", "+"),

    add_prec_operator: (_) => choice("+", "-"),
    mult_prec_operator: (_) => choice("*", "/"),
    comparison_operator: (_) => choice(">", "<", ">=", "<=", "==", "!="),
    assignment_prec_operator: (_) =>
      prec.left(1, choice("=", ":", "+=", "-=", "*=", "/=")),

    // This is inspired from tree-sitter-javascript
    // https://github.com/tree-sitter/tree-sitter-javascript/blob/fdeb68ac8d2bd5a78b943528bb68ceda3aade2eb/grammar.js#L866
    /////////////////////////////////////////////////////////////////////
    _string: ($) =>
      choice(
        seq(
          '"',
          repeat(choice($._unescaped_string_fragment, $.escape_sequence)),
          '"',
        ),
      ),

    _unescaped_string_fragment: (_) => token.immediate(prec(1, /[^"\\]+/)),

    escape_sequence: ($) =>
      seq(
        "\\",
        choice(
          /u\{[0-9a-fA-F]+\}/,
          "n",
          "\\",
          '"',
          seq("{", $.expression, "}"),
          // The lexer accepts a backslash before any character; an escape it
          // doesn't recognize is a semantic error, not a syntax one. Keeping
          // such a backslash lets it act as a path separator in an import or
          // @image-url address. The already-handled characters are excluded so
          // this doesn't compete with them.
          /[^"n{\\]/,
        ),
      ),
    /////////////////////////////////////////////////////////////////////

    property_visibility: (_) => choice("private", "in", "out", "in-out", "in_out"),

    _identifier: (_) => /[a-zA-Z_][a-zA-Z0-9_-]*/,
    simple_identifier: ($) => $._identifier,
    simple_indexed_identifier: ($) =>
      seq(
        field("name", $.simple_identifier),
        optional(seq("[", field("index_var", $.simple_identifier), "]")),
      ),

    builtin_type_identifier: (_) =>
      choice(
        "int",
        "float",
        "bool",
        "string",
        "color",
        "brush",
        "styled-text",
        "physical-length",
        "length",
        "duration",
        "angle",
        "easing",
        "percent",
        "image",
        "relative-font-size",
      ),

    easing_kind_identifier: ($) =>
      choice(
        "linear",
        "ease-in-quad",
        "ease-out-quad",
        "ease-in-out-quad",
        "ease",
        "ease-in",
        "ease-out",
        "ease-in-out",
        "ease-in-quart",
        "ease-out-quart",
        "ease-in-out-quart",
        "ease-in-quint",
        "ease-out-quint",
        "ease-in-out-quint",
        "ease-in-expo",
        "ease-out-expo",
        "ease-in-out-expo",
        "ease-in-sine",
        "ease-out-sine",
        "ease-in-out-sine",
        "ease-in-back",
        "ease-out-back",
        "ease-in-out-back",
        "ease-in-circ",
        "ease-out-circ",
        "ease-in-out-circ",
        "ease-in-elastic",
        "ease-out-elastic",
        "ease-in-out-elastic",
        "ease-in-bounce",
        "ease-out-bounce",
        "ease-in-out-bounce",
        seq("cubic-bezier", $.arguments),
      ),

    user_type_identifier: ($) => prec(1, $._identifier),
    _type_identifier: ($) =>
      choice($.builtin_type_identifier, $.user_type_identifier),

    value_list: ($) => seq("[", commaSep($.expression), optional(","), "]"),

    value: ($) =>
      prec(10, choice($.anon_struct_block, $.value_list, $._basic_value)),

    children_identifier: (_) => "@children",

    linear_gradient_identifier: (_) =>
      choice("@linear-gradient", "@linear_gradient"),
    radial_gradient_identifier: (_) =>
      choice("@radial-gradient", "@radial_gradient"),
    radial_gradient_kind: (_) => "circle", // currently only one
    conic_gradient_identifier: (_) =>
      choice("@conic-gradient", "@conic_gradient"),

    reference_identifier: (_) => choice("parent", "root", "self"),

    _number: ($) => choice($._int_number, $._float_number),

    _int_number: (_) => /\d+/,
    _float_number: (_) => /(\d*\.\d+|\d+\.\d*)/,

    int_value: ($) => $._int_number,
    float_value: ($) => $._float_number,
    bool_value: (_) => choice("true", "false"),
    string_value: ($) => $._string,
    color_value: (_) => /#[\da-fA-F]+/,
    // brush_value: ($) => ???,
    physical_length_value: ($) => seq($._number, token.immediate("phx")),
    length_value: ($) =>
      seq($._number, token.immediate(choice("px", "cm", "mm", "in", "pt"))),
    duration_value: ($) => seq($._number, token.immediate(choice("ms", "s"))),
    angle_value: ($) =>
      seq($._number, token.immediate(choice("deg", "grad", "turn", "rad"))),
    // easing_value: ($) => ???,
    percent_value: ($) => seq($._number, token.immediate("%")),
    // image_value: ($) => ???.
    relative_font_size_value: ($) => seq($._number, token.immediate("rem")),

    _basic_value: ($) =>
      choice(
        $.int_value,
        $.float_value,
        $.bool_value,
        $.string_value,
        $.color_value,
        $.physical_length_value,
        $.length_value,
        $.duration_value,
        $.angle_value,
        $.percent_value,
        $.relative_font_size_value,
        $.easing_kind_identifier,
      ),

    // Single-line comment
    line_comment: (_) => token(seq("//", /[^\n\r]*/)),

    // Multi-line comment — handled by the external scanner (src/scanner.c)
    // because Slint supports balanced nesting: /* /* */ */ is valid.
    // block_comment is defined in externals above.
  },
});

function commaSep(rule) {
  return optional(commaSep1(rule));
}

function commaSep1(rule) {
  return seq(rule, repeat(seq(",", rule)));
}

function commaSep2(rule) {
  return seq(rule, ",", rule, repeat(seq(",", rule)));
}
