// Several rules copied from https://github.com/mitchellh/tree-sitter-proto/blob/main/grammar.js

// FlatBuffers spec: https://flatbuffers.dev/flatbuffers_grammar.html
const letter = /[a-zA-Z]/,
  decimal_digit = /[0-9]/,
  octal_digit = /[0-7]/,
  hex_digit = /[0-9A-Fa-f]/;

function comma_separate(x) {
  return seq(x, repeat(seq(",", x)));
}

const FIELD_KEY = {
  array_type: "array_type",
  array_value: "array_value",
  array_value_item: "array_value_item",
  attribute_name: "attribute_name",
  documentation: "documentation",
  enum_int_constant: "enum_int_constant",
  enum_key: "enum_key",
  enum_name: "enum_name",
  enum_type: "enum_type",
  enum_val_decl: "enum_val_decl",
  field_and_value: "field_and_value",
  field_key: "field_key",
  field_type: "field_type",
  field_value: "field_value",
  field_with_type: "field_with_type",
  field_without_type: "field_without_type",
  file_extension_constant: "file_extension_constant",
  file_identifier_constant: "file_identifier_constant",
  full_ident: "full_ident",
  include_name: "include_name",
  metadata: "metadata",
  namespace_ident: "namespace_ident",
  object_key: "object_key",
  object_value: "object_value",
  root_type_ident: "root_type_ident",
  rpc_method: "rpc_method",
  rpc_method_name: "rpc_method_name",
  rpc_name: "rpc_name",
  rpc_parameter: "rpc_parameter",
  rpc_return_type: "rpc_return_type",
  scalar_value: "scalar_value",
  single_value: "single_value",
  string_constant: "string_constant",
  table_or_struct_name: "table_or_struct_name",
  union_field_decl: "union_field_decl",
  union_field_key: "union_field_key",
  union_field_value: "union_field_value",
  union_name: "union_name",
};

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

  extras: ($) => [$.comment, /\s/],

  rules: {
    // schema = include* ( namespace_decl | type_decl | enum_decl | root_decl | file_extension_decl |
    //  file_identifier_decl | attribute_decl | rpc_decl | object )*
    source_file: ($) =>
      seq(
        repeat($.include),
        optional(
          repeat(
            choice(
              $.namespace_decl,
              $.type_decl,
              $.enum_decl,
              // Different from the spec since it is outdated.
              $.union_decl,
              $.root_decl,
              $.file_extension_decl,
              $.file_identifier_decl,
              $.attribute_decl,
              $.rpc_decl,
              $.object,
            ),
          ),
        ),
      ),

    documentation: ($) => token(seq("///", /.*/)),
    include: ($) =>
      seq("include", field(FIELD_KEY.include_name, $.string_constant), ";"),

    namespace_decl: ($) =>
      seq("namespace", field(FIELD_KEY.namespace_ident, $.full_ident), ";"),

    attribute_decl: ($) =>
      seq(
        "attribute",
        choice(
          field(FIELD_KEY.attribute_name, $.identifier),
          seq('"', field(FIELD_KEY.attribute_name, $.identifier), '"'),
        ),
        ";",
      ),

    type_decl: ($) =>
      seq(
        repeat(field(FIELD_KEY.documentation, $.documentation)),
        choice("table", "struct"),
        field(FIELD_KEY.table_or_struct_name, $.identifier),
        optional(field(FIELD_KEY.metadata, $.metadata)),
        "{",
        repeat($.field_decl),
        "}",
      ),

    enum_decl: ($) =>
      seq(
        repeat(field(FIELD_KEY.documentation, $.documentation)),
        seq(
          "enum",
          field(FIELD_KEY.enum_name, $.identifier),
          ":",
          field(FIELD_KEY.enum_type, $.type),
        ),
        optional(field(FIELD_KEY.metadata, $.metadata)),
        "{",
        comma_separate(field(FIELD_KEY.enum_val_decl, $.enum_val_decl)),
        optional(","),
        "}",
      ),

    union_decl: ($) =>
      seq(
        repeat(field(FIELD_KEY.documentation, $.documentation)),
        "union",
        field(FIELD_KEY.union_name, $.identifier),
        optional(field(FIELD_KEY.metadata, $.metadata)),
        "{",
        choice(
          seq(
            field(FIELD_KEY.union_field_decl, $.union_field_decl),
            repeat(
              seq(",", field(FIELD_KEY.union_field_decl, $.union_field_decl)),
            ),
            optional(","),
          ),
          field(
            FIELD_KEY.field_without_type,
            seq($.full_ident, repeat(seq(",", $.full_ident)), optional(",")),
          ),
        ),
        "}",
      ),

    object: ($) =>
      seq(
        "{",
        comma_separate(
          repeat(field(FIELD_KEY.documentation, $.documentation)),
          field(FIELD_KEY.object_key, $.identifier),
          ":",
          field(FIELD_KEY.object_value, $.value),
        ),
        "}",
      ),

    root_decl: ($) =>
      seq("root_type", field(FIELD_KEY.root_type_ident, $.identifier), ";"),

    file_extension_decl: ($) =>
      seq(
        "file_extension",
        field(FIELD_KEY.file_extension_constant, $.string_constant),
        ";",
      ),

    file_identifier_decl: ($) =>
      seq(
        "file_identifier",
        field(FIELD_KEY.file_identifier_constant, $.string_constant),
        ";",
      ),

    value: ($) =>
      choice(
        field(FIELD_KEY.single_value, $.single_value),
        field(FIELD_KEY.object_value, $.object),
        field(
          FIELD_KEY.array_value,
          seq(
            "[",
            comma_separate(field(FIELD_KEY.array_value_item, $.value)),
            "]",
          ),
        ),
      ),

    enum_val_decl: ($) =>
      seq(
        repeat(field(FIELD_KEY.documentation, $.documentation)),
        field(FIELD_KEY.enum_key, $.identifier),
        optional(seq("=", field(FIELD_KEY.enum_int_constant, $.int_constant))),
      ),

    field_decl: ($) =>
      seq(
        repeat(field(FIELD_KEY.documentation, $.documentation)),
        field(FIELD_KEY.field_key, $.identifier),
        ":",
        field(FIELD_KEY.field_type, $.type),
        optional(
          seq(
            "=",
            // To support default value with enum field. Different from spec(only scalar is supported) since it is outdated.
            field(FIELD_KEY.field_value, $.value),
          ),
        ),
        optional(field(FIELD_KEY.metadata, $.metadata)),
        ";",
      ),

    union_field_decl: ($) =>
      seq(
        repeat(field(FIELD_KEY.documentation, $.documentation)),
        field(FIELD_KEY.union_field_key, $.identifier),
        ":",
        field(FIELD_KEY.union_field_value, $.type),
      ),

    type: ($) =>
      choice(
        "bool",
        "byte",
        "ubyte",
        "short",
        "ushort",
        "int",
        "uint",
        "float",
        "long",
        "ulong",
        "double",
        "int8",
        "uint8",
        "int16",
        "uint16",
        "int32",
        "uint32",
        "int64",
        "uint64",
        "float32",
        "float64",
        "string",
        field(FIELD_KEY.full_ident, $.full_ident),
        field(FIELD_KEY.array_type, seq("[", $.type, "]")),
      ),

    rpc_decl: ($) =>
      seq(
        repeat(field(FIELD_KEY.documentation, $.documentation)),
        "rpc_service",
        field(FIELD_KEY.rpc_name, $.identifier),
        "{",
        field(FIELD_KEY.rpc_method, repeat($.rpc_method)),
        "}",
      ),

    rpc_method: ($) =>
      seq(
        repeat(field(FIELD_KEY.documentation, $.documentation)),
        field(FIELD_KEY.rpc_method_name, $.identifier),
        "(",
        field(FIELD_KEY.rpc_parameter, $.identifier),
        ")",
        ":",
        field(FIELD_KEY.rpc_return_type, $.identifier),
        optional(field(FIELD_KEY.metadata, $.metadata)),
        ";",
      ),

    metadata: ($) =>
      seq(
        "(",
        comma_separate(field(FIELD_KEY.field_and_value, $.field_and_value)),
        ")",
      ),

    field_and_value: ($) =>
      seq(
        field(FIELD_KEY.field_key, $.identifier),
        optional(seq(":", field(FIELD_KEY.field_value, $.single_value))),
      ),

    // single_value = fullIdent | ( [ "-" | "+" ] intLit ) | ( [ "-" | "+" ] floatLit ) | strLit | boolLit
    single_value: ($) =>
      choice(
        field(FIELD_KEY.scalar_value, $.scalar),
        field(FIELD_KEY.string_constant, $.string_constant),
        field(FIELD_KEY.full_ident, $.full_ident),
      ),

    // ident = letter { letter | decimalDigit | "_" }
    identifier: ($) =>
      token(seq(letter, optional(repeat(choice(letter, decimal_digit, "_"))))),

    // fullIdent = ident { "." ident }
    full_ident: ($) =>
      seq($.identifier, optional(repeat(seq(".", $.identifier)))),

    scalar: ($) => choice($.float_constant, $.int_constant, $.bool_constant),

    // boolLit = "true" | "false"
    bool_constant: ($) => choice($.true, $.false),
    int_constant: ($) => seq(optional($.plus_minus_constant), $.int_lit),
    float_constant: ($) => seq(optional($.plus_minus_constant), $.float_lit),

    plus_minus_constant: ($) => choice($.plus_token, $.minus_token),
    plus_token: ($) => token("+"),
    minus_token: ($) => token("-"),

    true: ($) => "true",
    false: ($) => "false",

    // intLit = decimalLit | hexLit
    int_lit: ($) => choice($.decimal_lit, $.hex_lit),

    // decimalLit = ( "1" ... "9") { decimalDigit }
    decimal_lit: ($) =>
      choice(token(seq(/[1-9]/, repeat(decimal_digit))), token("0")),

    // hexLit = "0" ( "x" | "X" ) hexDigit { hexDigit }
    hex_lit: ($) =>
      token(seq("0", choice("x", "X"), hex_digit, repeat(hex_digit))),

    // floatLit = ( decimals "." [ decimals ] [ exponent ] | decimals exponent | "."decimals [ exponent ] )
    //  | "inf" | "infinity" | "nan"
    // decimals  = decimalDigit { decimalDigit }
    // exponent  = ( "e" | "E" ) [ "+" | "-" ] decimals
    float_lit: ($) => {
      const decimals = seq(decimal_digit, repeat(decimal_digit));

      const exponent = seq(
        choice("e", "E"),
        optional(choice("+", "-")),
        decimals,
      );

      return choice(
        choice($.inf_token, $.nan_token),
        token(
          choice(
            seq(decimals, ".", optional(decimals), optional(exponent)),
            seq(decimals, exponent),
            seq(".", decimals, optional(exponent)),
          ),
        ),
      );
    },

    inf_token: ($) => choice(token("infinity"), token("inf")),

    nan_token: ($) => token("nan"),

    string_constant: ($) =>
      seq(
        '"',
        repeat(choice(token.immediate(prec(1, /[^"\\]+/)), $.escape_sequence)),
        '"',
      ),

    escape_sequence: ($) =>
      token.immediate(
        seq(
          "\\",
          choice(
            /[^xuU]/,
            /\d{2,3}/,
            /x[0-9a-fA-F]{2,}/,
            /u[0-9a-fA-F]{4}/,
            /U[0-9a-fA-F]{8}/,
          ),
        ),
      ),

    comment: ($) =>
      token(
        choice(
          prec(1, seq("//", /.*/)),
          seq("/*", /[^*]*\*+([^/*][^*]*\*+)*/, "/"),
        ),
      ),
  },
});
