const PREC = {
  assignment: 1,
  chain: 2,
  ternary: 3,
  or: 4,
  onlyif: 5,
  xor: 6,
  and: 7,
  equality: 8,
  comparison: 9,
  range: 10,
  bit_or: 11,
  bit_xor: 12,
  bit_and: 13,
  shift: 14,
  collection: 15,
  concat: 16,
  additive: 17,
  multiplicative: 18,
  exponent: 19,
  prefix: 20,
  postfix: 21,
  call: 22,
};

const commaSep = (rule) => optional(commaSep1(rule));

const commaSep1 = (rule) => seq(
  rule,
  repeat(seq(',', rule)),
  optional(','),
);

const sepBySemicolon = (rule) => repeat(choice(rule, ';'));

module.exports = grammar({
  name: 'zuzu',

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

  externals: $ => [
    $.pod_block,
  ],

  word: $ => $.identifier,

  supertypes: $ => [
    $._statement,
    $._declaration,
    $._expression,
    $._literal,
  ],

  conflicts: $ => [
    [$.block, $.dict_literal],
    [$.bare_key, $._primary_expression],
    [$.reserved_word_key, $.boolean],
    [$.reserved_word_key, $.null],
    [$.reserved_word_key, $.self],
    [$.reserved_word_key, $.super],
    [$.trait_composition],
  ],

  rules: {
    source_file: $ => seq(optional($.shebang), repeat($._item)),

    shebang: _ => token(seq('#!', /[^\n]*/)),

    _item: $ => choice(
      $._declaration,
      $._statement,
    ),

    comment: $ => token(choice(
      seq('//', /[^\n]*/),
      seq('/*', /[^*]*\*+([^/*][^*]*\*+)*/, '/'),
    )),

    _declaration: $ => choice(
      $.import_statement,
      $.variable_declaration,
      $.function_declaration,
      $.function_predeclaration,
      $.class_declaration,
      $.trait_declaration,
    ),

    _statement: $ => choice(
      $.block,
      $.if_statement,
      $.while_statement,
      $.for_statement,
      $.switch_statement,
      $.try_statement,
      $.return_statement,
      $.throw_statement,
      $.loop_control_statement,
      $.builtin_statement,
      $.assignment_statement,
      $.expression_statement,
      $.empty_statement,
    ),

    empty_statement: _ => ';',

    block: $ => seq(
      '{',
      repeat($._item),
      '}',
    ),

    import_statement: $ => seq(
      'from',
      field('module', $.module_path),
      optional('try'),
      'import',
      field('imports', choice('*', $.import_list)),
      optional(choice(seq('if', field('condition', $._expression)), seq('unless', field('condition', $._expression)))),
      ';',
    ),

    module_path: _ => token(/[A-Za-z_][A-Za-z0-9_]*(\/[A-Za-z_][A-Za-z0-9_]*)*/),

    import_list: $ => commaSep1($.import_specifier),

    import_specifier: $ => seq(
      field('name', choice($.identifier, $.type_identifier)),
      optional(seq('as', field('alias', $.identifier))),
    ),

    variable_declaration: $ => prec(2, seq(
      field('kind', choice('let', 'const')),
      choice(
        field('pattern', $.destructuring_pattern),
        seq(
          optional(field('type', $.type_expression)),
          field('name', $.binding_identifier),
        ),
      ),
      optional($.weak_modifier),
      optional(seq(':=', field('value', $._expression))),
      optional($.weak_modifier),
      ';',
    )),

    destructuring_pattern: $ => seq(
      '{',
      commaSep($.destructuring_entry),
      '}',
    ),

    destructuring_entry: $ => seq(
      optional(field('key', choice(
        $.identifier,
        $.string,
        $.template_string,
        $.parenthesized_expression,
      ))),
      optional(':'),
      optional(field('type', $.type_expression)),
      field('name', $.identifier),
      optional(seq(':=', field('default', $._expression))),
      optional($.weak_modifier),
    ),

    weak_modifier: _ => seq('but', 'weak'),

    function_declaration: $ => seq(
      optional('async'),
      'function',
      field('name', $.identifier),
      field('parameters', $.parameter_list),
      optional(field('return_type', $.return_type)),
      field('body', $.block),
    ),

    function_predeclaration: $ => seq(
      optional('async'),
      'function',
      field('name', $.identifier),
      ';',
    ),

    class_declaration: $ => seq(
      'class',
      field('name', $.binding_identifier),
      optional(seq('extends', field('superclass', $.type_expression))),
      optional($.trait_composition),
      choice(
        field('body', $.class_body),
        ';',
      ),
    ),

    trait_declaration: $ => seq(
      'trait',
      field('name', $.binding_identifier),
      optional($.trait_composition),
      field('body', $.trait_body),
    ),

    trait_composition: $ => seq(
      choice('with', 'but'),
      $.type_expression,
      repeat(seq(',', $.type_expression)),
    ),

    class_body: $ => seq(
      '{',
      repeat(choice(
        $.field_declaration,
        $.method_declaration,
        $.method_predeclaration,
        $.class_declaration,
        $.trait_declaration,
        ';',
      )),
      '}',
    ),

    trait_body: $ => seq(
      '{',
      repeat(choice(
        $.method_declaration,
        $.method_predeclaration,
        $.class_declaration,
        $.trait_declaration,
        ';',
      )),
      '}',
    ),

    field_declaration: $ => seq(
      field('kind', choice('let', 'const')),
      optional(field('type', $.type_expression)),
      field('name', $.identifier),
      optional(seq('with', $.field_accessor_list)),
      optional($.weak_modifier),
      optional(seq(':=', field('value', $._expression))),
      optional($.weak_modifier),
      ';',
    ),

    field_accessor_list: _ => commaSep1(choice('get', 'set', 'clear', 'has')),

    method_declaration: $ => seq(
      repeat(choice('static', 'async')),
      'method',
      field('name', $.identifier),
      field('parameters', $.parameter_list),
      optional(field('return_type', $.return_type)),
      field('body', $.block),
    ),

    method_predeclaration: $ => seq(
      repeat(choice('static', 'async')),
      'method',
      field('name', $.identifier),
      ';',
    ),

    parameter_list: $ => seq(
      '(',
      commaSep($.parameter),
      ')',
    ),

    parameter: $ => choice(
      seq('...', optional(field('type', $.type_expression)), field('name', $.identifier)),
      prec(2, seq(field('type', $.identifier), field('name', $.binding_identifier), optional('?'), '...', optional(field('variadic_type', $.type_expression)), field('variadic_name', $.binding_identifier))),
      prec(1, seq(field('type', $.type_expression), field('name', $.binding_identifier), optional('?'), '...', optional(field('variadic_type', $.type_expression)), field('variadic_name', $.binding_identifier))),
      seq(field('name', $.binding_identifier), optional('?'), '...', optional(field('variadic_type', $.type_expression)), field('variadic_name', $.binding_identifier)),
      prec(2, seq(field('type', $.identifier), field('name', $.binding_identifier), optional('?'), optional(seq(':=', field('default', $._expression))))),
      prec(1, seq(field('type', $.type_expression), field('name', $.binding_identifier), optional('?'), optional(seq(':=', field('default', $._expression))))),
      seq(field('name', $.binding_identifier), optional('?'), optional(seq(':=', field('default', $._expression)))),
    ),

    return_type: $ => seq(choice('->', '→'), $.type_expression),

    type_expression: $ => choice(
      $.type_identifier,
      $.private_type_identifier,
      prec(1, seq($.self, '{', $.type_identifier, '}')),
      $.self,
      seq($.type_identifier, repeat(seq('::', $.type_identifier))),
      prec(1, seq($.type_identifier, '[', $.type_expression, ']')),
    ),

    if_statement: $ => seq(
      'if',
      field('condition', $.parenthesized_expression),
      field('consequence', $.block),
      optional(seq('else', field('alternative', choice($.if_statement, $.block)))),
    ),

    while_statement: $ => seq(
      'while',
      field('condition', $.parenthesized_expression),
      field('body', $.block),
    ),

    for_statement: $ => seq(
      'for',
      '(',
      choice(
        prec(1, seq(optional(choice('let', 'const')), field('name', $.identifier), 'in', field('iterable', $._expression))),
        field('iterable', $._expression),
      ),
      ')',
      field('body', $.block),
      optional(seq('else', field('alternative', $.block))),
    ),

    switch_statement: $ => seq(
      'switch',
      field('value', $.switch_header),
      '{',
      repeat(choice($.case_clause, $.default_clause)),
      '}',
    ),

    switch_header: $ => seq(
      '(',
      field('value', $._expression),
      optional(seq(':', field('mode', choice($.identifier, $.regexp, $.operator_name)))),
      ')',
    ),

    operator_name: _ => token(prec(2, choice(
      '==', '≡', '!=', '≢',
      '=', '≠', '<', '>', '<=', '≤', '>=', '≥', '<=>', '≶', '≷',
      'eq', 'ne', 'gt', 'ge', 'lt', 'le', 'cmp',
      'eqi', 'nei', 'gti', 'gei', 'lti', 'lei', 'cmpi',
      '∣', 'divides', '∤',
      'in', '∈', '∉',
      'subsetof', '⊂', 'supersetof', '⊃', 'equivalentof', '⊂⊃',
      'instanceof', 'does', 'can',
      '~', '@', '@?', '@@',
    ))),

    case_clause: $ => seq(
      'case',
      commaSep1(choice($.case_match_value, $.case_operator_value, $.case_value)),
      ':',
      repeat($._item),
    ),

    case_value: $ => $._expression,

    case_match_value: $ => prec(1, seq('~', $._expression)),

    case_operator_value: $ => prec(1, seq($.operator_name, $._expression)),

    default_clause: $ => seq(
      'default',
      ':',
      repeat($._item),
    ),

    try_statement: $ => prec(1, seq(
      'try',
      field('body', $.block),
      repeat1($.catch_clause),
    )),

    catch_clause: $ => seq(
      'catch',
      optional(seq('(', optional(field('type', $.type_expression)), optional(field('name', $.identifier)), ')')),
      field('body', $.block),
    ),

    return_statement: $ => choice(
      prec(1, seq(
        'return',
        field('value', $.function_expression),
      )),
      seq(
        'return',
        optional(field('value', $._expression)),
        optional(choice(seq('if', $._expression), seq('unless', $._expression))),
        ';',
      ),
    ),

    throw_statement: $ => seq(
      choice('throw', 'die'),
      field('value', $._expression),
      optional(choice(seq('if', $._expression), seq('unless', $._expression))),
      ';',
    ),

    loop_control_statement: $ => seq(
      choice('next', 'continue', 'last'),
      optional(choice(seq('if', $._expression), seq('unless', $._expression))),
      ';',
    ),

    builtin_statement: $ => seq(
      field('name', choice('say', 'print', 'warn', 'assert', 'debug')),
      commaSep($._expression),
      optional(choice(seq('if', $._expression), seq('unless', $._expression))),
      ';',
    ),

    assignment_statement: $ => prec.right(1, seq(
      $.assignment_expression,
      optional(choice(seq('if', $._expression), seq('unless', $._expression), seq('for', $._expression))),
      optional(';'),
    )),

    expression_statement: $ => seq(
      $._non_assignment_expression,
      optional(choice(seq('if', $._expression), seq('unless', $._expression), seq('for', $._expression))),
      ';',
    ),

    _expression: $ => choice(
      $.assignment_expression,
      $._non_assignment_expression,
    ),

    _non_assignment_expression: $ => choice(
      $.chain_expression,
      $.ternary_expression,
      $.binary_expression,
      $.unary_expression,
      $.postfix_expression,
      $.call_expression,
      $.member_expression,
      $.dynamic_member_expression,
      $.index_expression,
      $.slice_expression,
      $.dict_access_expression,
      $.lambda_expression,
      $.function_expression,
      $.let_expression,
      $.try_expression,
      $.do_expression,
      $.await_expression,
      $.spawn_expression,
      $.new_expression,
      $._primary_expression,
    ),

    assignment_expression: $ => prec.right(PREC.assignment, seq(
      field('left', $._expression),
      field('operator', $.assignment_operator),
      field('right', $._expression),
      optional($.weak_modifier),
    )),

    assignment_operator: _ => choice(':=', '~=', '+=', '-=', '*=', '×=', '/=', '÷=', '**=', '_=', '?:='),

    ternary_expression: $ => choice(
      prec.right(PREC.ternary, seq(
        field('condition', $._expression),
        '?',
        field('consequence', $._expression),
        ':',
        field('alternative', $._expression),
      )),
      prec.right(PREC.ternary, seq(
        field('condition', $._expression),
        '?:',
        field('alternative', $._expression),
      )),
    ),

    chain_expression: $ => choice(
      prec.left(PREC.chain, seq(
        field('left', $._expression),
        field('operator', choice('▷', '|>')),
        field('right', $._expression),
      )),
      prec.right(PREC.chain, seq(
        field('left', $._expression),
        field('operator', choice('◁', '<|')),
        field('right', $._expression),
      )),
    ),

    binary_expression: $ => choice(
      ...[
        [PREC.or, choice('or', '⋁', 'or?', '⋁?')],
        [PREC.onlyif, choice('onlyif', '⊨', 'onlyif?', '⊨?')],
        [PREC.xor, choice('xor', '⊻', 'xor?', '⊻?', 'nor', '⊽', 'nor?', '⊽?', 'xnor', '↔', 'xnor?', '↔?')],
        [PREC.and, choice('and', '⋀', 'and?', '⋀?', 'nand', '⊼', 'nand?', '⊼?', 'butnot', '⊭', 'butnot?', '⊭?')],
        [PREC.equality, choice('==', '≡', '!=', '≢', 'default')],
        [PREC.comparison, choice('=', '≠', '<', '>', '<=', '≤', '>=', '≥', '<=>', '≶', '≷', '∣', 'divides', '∤', 'eq', 'ne', 'gt', 'ge', 'lt', 'le', 'cmp', 'eqi', 'nei', 'gti', 'gei', 'lti', 'lei', 'cmpi', 'in', '∈', '∉', 'subsetof', '⊂', 'supersetof', '⊃', 'equivalentof', '⊂⊃', 'instanceof', 'does', 'can', '~', '@', '@?', '@@', '->', '→')],
        [PREC.range, '...'],
        [PREC.bit_or, '|'],
        [PREC.bit_xor, '^'],
        [PREC.bit_and, '&'],
        [PREC.shift, choice('<<', '«', '>>', '»')],
        [PREC.collection, choice('union', '⋃', 'intersection', '⋂', '\\', '∖')],
        [PREC.concat, '_'],
        [PREC.additive, choice('+', '-')],
        [PREC.multiplicative, choice('*', '/', '×', '÷', 'mod')],
      ].map(([precedence, operator]) => prec.left(precedence, seq(
        field('left', $._expression),
        field('operator', operator),
        field('right', $._expression),
      ))),
      prec.right(PREC.exponent, seq(
        field('left', $._expression),
        field('operator', '**'),
        field('right', $._expression),
      )),
    ),

    unary_expression: $ => prec(PREC.prefix, seq(
      field('operator', choice('+', '-', '!', '¬', '~', '√', '#', '\\', 'not', 'abs', 'sqrt', 'floor', 'ceil', 'round', 'int', 'uc', 'lc', 'length', 'typeof', '++', '--')),
      field('argument', $._expression),
    )),

    postfix_expression: $ => prec.left(PREC.postfix, seq(
      field('argument', $._expression),
      field('operator', choice('++', '--')),
    )),

    call_expression: $ => prec.left(PREC.call, seq(
      field('function', $._expression),
      field('arguments', $.argument_list),
    )),

    argument_list: $ => seq(
      '(',
      repeat(choice($.spread_argument, $._expression, $.pair_entry, ',')),
      ')',
    ),

    spread_argument: $ => seq('...', field('value', $._expression)),

    member_expression: $ => prec.left(PREC.postfix, seq(
      field('object', $._expression),
      '.',
      field('property', $.identifier),
    )),

    dynamic_member_expression: $ => prec.left(PREC.postfix, seq(
      field('object', $._expression),
      '.(',
      field('property', $._expression),
      ')',
    )),

    index_expression: $ => prec.left(PREC.postfix, seq(
      field('object', $._expression),
      '[',
      field('index', $._expression),
      ']',
    )),

    slice_expression: $ => prec.left(PREC.postfix, seq(
      field('object', $._expression),
      '[',
      optional(field('start', $._expression)),
      ':',
      optional(field('length', $._expression)),
      ']',
    )),

    dict_access_expression: $ => prec.left(PREC.postfix, choice(
      prec(PREC.postfix + 1, seq(
        field('object', $._expression),
        '{',
        field('key', $.bare_key),
        '}',
      )),
      prec(PREC.postfix, seq(
        field('object', $._expression),
        '{',
        field('key', $._expression),
        '}',
      )),
    )),

    lambda_expression: $ => choice(
      prec(PREC.prefix, seq(
        'fn',
        optional(field('parameters', choice($.parameter_list, $.lambda_parameter))),
        choice('->', '→'),
        field('body', choice($.block, $._expression)),
      )),
      prec(PREC.prefix, seq(
        choice('->', '→'),
        field('body', choice($.block, $._expression)),
      )),
      prec(PREC.prefix, seq(
        'async',
        'fn',
        optional(field('parameters', choice($.parameter_list, $.lambda_parameter))),
        choice('->', '→'),
        field('body', choice($.block, $._expression)),
      )),
    ),

    lambda_parameter: $ => seq(optional(field('type', $.type_expression)), field('name', $.identifier)),

    function_expression: $ => seq(
      optional('async'),
      'function',
      field('parameters', $.parameter_list),
      optional(field('return_type', $.return_type)),
      field('body', $.block),
    ),

    let_expression: $ => prec(-1, seq(
      field('kind', choice('let', 'const')),
      choice(
        field('pattern', $.destructuring_pattern),
        seq(optional(field('type', $.type_expression)), field('name', $.identifier)),
      ),
      ':=',
      field('value', $._expression),
    )),

    try_expression: $ => seq(
      'try',
      field('body', $.block),
      repeat($.catch_clause),
    ),

    do_expression: $ => seq(
      'do',
      field('body', $.block),
    ),

    await_expression: $ => prec(PREC.prefix, seq('await', field('argument', choice($.block, $._expression)))),

    spawn_expression: $ => seq('spawn', field('body', choice($.block, $._expression))),

    new_expression: $ => choice(
      prec(PREC.prefix, seq(
        'new',
        field('class', $.constructor_target),
        optional($.trait_composition),
        field('arguments', $.argument_list),
      )),
      prec(PREC.prefix - 1, seq(
        'new',
        field('class', $.constructor_target),
        optional($.trait_composition),
      )),
    ),

    constructor_target: $ => choice($.type_expression, $.identifier),

    _primary_expression: $ => choice(
      $.identifier,
      $.type_identifier,
      $.magic_global,
      $.self,
      $.super,
      $._literal,
      $.parenthesized_expression,
      $.list_literal,
      $.dict_literal,
      $.pair_list_literal,
      $.bag_literal,
      $.set_literal,
      $.floor_expression,
      $.ceil_expression,
      $.placeholder,
    ),

    parenthesized_expression: $ => seq('(', $._expression, ')'),

    floor_expression: $ => seq('⌊', $._expression, '⌋'),

    ceil_expression: $ => seq('⌈', $._expression, '⌉'),

    list_literal: $ => seq(
      '[',
      repeat(choice($.spread_argument, $._expression, ',')),
      ']',
    ),

    dict_literal: $ => seq(
      '{',
      commaSep($.pair_entry),
      '}',
    ),

    bag_literal: $ => seq('<<<', repeat(choice($._expression, ',')), '>>>'),

    pair_list_literal: $ => seq(
      '{{',
      repeat(choice($.pair_entry, ',')),
      '}}',
    ),

    set_literal: $ => choice(
      seq('<<', commaSep($._expression), $.set_close),
      seq('«', commaSep($._expression), '»'),
      '∅',
    ),

    pair_entry: $ => choice(
      prec(1, seq(
        field('key', $.bare_key),
        ':',
        field('value', $._expression),
      )),
      prec(-1, seq(
        field('key', $._expression),
        ':',
        field('value', $._expression),
      )),
    ),

    _literal: $ => choice(
      $.number,
      $.string,
      $.template_string,
      $.regexp,
      $.boolean,
      $.null,
    ),

    boolean: _ => choice('true', 'false', '⊤', '⊥'),

    null: _ => 'null',

    magic_global: _ => choice('__argc__', '__file__', '__global__', '__system__'),

    self: _ => 'self',

    super: _ => 'super',

    placeholder: _ => '^^',

    number: _ => token(choice(
      /0x[\da-fA-F]+/,
      /0b[01]+/,
      /0o[0-7]+/,
      /\d+\.\d+([eE][+-]?\d+)?/,
      /\d+([eE][+-]?\d+)?/,
    )),

    string: $ => choice(
      $.double_quoted_string,
      $.single_quoted_string,
      $.triple_double_quoted_string,
      $.triple_single_quoted_string,
    ),

    double_quoted_string: _ => token(seq('"', repeat(choice(/\\./, /[^"\\\n]/)), '"')),

    single_quoted_string: _ => token(seq("'", repeat(choice(/\\./, /[^'\\\n]/)), "'")),

    triple_double_quoted_string: _ => token(seq('"""', repeat(choice(/\\./, /[^\\]/)), '"""')),

    triple_single_quoted_string: _ => token(seq("'''", repeat(choice(/\\./, /[^\\]/)), "'''")),

    template_string: $ => choice(
      seq('`', repeat(choice($.template_interpolation, $.template_fragment)), '`'),
      seq('```', repeat(choice($.template_interpolation, $.template_fragment)), '```'),
    ),

    template_interpolation: $ => seq(
      '${',
      field('value', $._expression),
      '}',
    ),

    template_fragment: _ => token(prec(-1, repeat1(choice(
      /\\./,
      /[^`\\$]+/,
      /\$[^{`\\]/,
    )))),

    regexp: _ => token(prec(-1, seq(
      '/',
      repeat1(choice(/\\./, /\[[^\]\n]*(\\.[^\]\n]*)*\]/, /[^\/\\\n]/)),
      '/',
      optional(/[A-Za-z]+/),
    ))),

    bare_key: $ => choice($.identifier, $.reserved_word_key),

    reserved_word_key: _ => choice(
      'async', 'await', 'do', 'false', 'fn', 'function', 'new', 'null', 'self',
      'spawn', 'super', 'true', 'try',
      'not', 'abs', 'sqrt', 'floor', 'ceil', 'round', 'int', 'uc', 'lc',
      'length', 'typeof',
    ),

    binding_identifier: $ => choice($.identifier, $.type_identifier),

    identifier: _ => /[\p{L}_][\p{L}\p{N}_]*/,

    type_identifier: _ => token(prec(1, /[A-Z][A-Za-z0-9_]*/)),

    private_type_identifier: _ => token(prec(-1, /_[A-Z][A-Za-z0-9_]*/)),

    set_close: _ => token(prec(20, '>>')),
  },
});
