const PREC = {
  assign: 1,
  logical_or: 2,
  logical_and: 3,
  bit_or: 4,
  bit_xor: 5,
  bit_and: 6,
  equality: 7,
  comparison: 8,
  shift: 9,
  additive: 10,
  multiplicative: 11,
  unary: 12,
  call: 13,
};

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

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

  word: $ => $.identifier,

  rules: {
    source_file: $ => repeat($._declaration),

    _declaration: $ => choice(
      $.import_declaration,
      $.const_declaration,
      $.alias_declaration,
      $.port_declaration,
      $.mmio_declaration,
      $.embed_declaration,
      $.global_declaration,
      $.struct_item,
      $.extern_declaration,
      $.function_item,
      $.layout_declaration,
    ),

    import_declaration: $ => seq('import', $.path, optional(';')),
    const_declaration: $ => seq(optional('pub'), 'const', $.identifier, ':', $.type, '=', $._expression, optional(';')),
    alias_declaration: $ => seq(optional('pub'), 'alias', $.identifier, '=', $.type, optional(';')),
    port_declaration: $ => seq(optional('pub'), 'port', $.identifier, ':', $.type, '=', $._expression, optional(';')),
    mmio_declaration: $ => seq(optional('pub'), optional('volatile'), 'mmio', $.identifier, ':', $.type, '=', $._expression, optional(';')),
    embed_declaration: $ => seq(optional('pub'), 'embed', $.identifier, ':', 'bytes', '=', $.embed_source, optional($.embed_options), optional(';')),
    global_declaration: $ => seq(optional('pub'), 'global', $.identifier, ':', $.type, '=', $._expression, optional(';')),
    struct_item: $ => seq(optional('pub'), 'struct', field('name', $.identifier), '{', repeat($.field_declaration), '}'),
    extern_declaration: $ => seq(optional('pub'), 'extern', 'asm', 'fn', $.identifier, $.parameters, optional($.return_type), optional(';')),
    function_item: $ => seq(repeat(choice('pub', 'inline', 'naked', 'interrupt')), 'fn', field('name', $.identifier), $.parameters, optional($.return_type), $.block),

    layout_declaration: $ => seq('layout', $.identifier, '{', repeat($.layout_item), '}'),
    layout_item: $ => choice(
      seq('load', $.integer_literal, optional(';')),
      seq('entry', $.integer_literal, optional(';')),
      seq('stack', $.integer_literal, optional(';')),
      seq('region', $.identifier, $.integer_literal, '..', $.integer_literal, repeat(choice('read', 'write', 'execute', 'volatile', 'reserved')), optional(';')),
      seq('section', $.section_name, '->', $.identifier, 'align', $.integer_literal, optional(';')),
      seq('symbol', $.identifier, '=', $._expression, optional(';')),
    ),

    field_declaration: $ => seq($.identifier, ':', $.type, optional(';')),
    parameters: $ => seq('(', optional(seq($.parameter, repeat(seq(',', $.parameter)), optional(','))), ')'),
    parameter: $ => seq($.identifier, ':', $.type),
    return_type: $ => seq('->', $.type),

    block: $ => seq('{', repeat($._statement), '}'),
    _statement: $ => choice(
      $.let_statement,
      $.if_statement,
      $.while_statement,
      $.loop_statement,
      $.break_statement,
      $.continue_statement,
      $.return_statement,
      $.asm_statement,
      $.out_statement,
      $.assignment_statement,
      $.expression_statement,
    ),
    let_statement: $ => seq('let', $.identifier, ':', $.type, '=', $._expression, optional(';')),
    if_statement: $ => seq('if', $._expression, $.block, optional(seq('else', choice($.if_statement, $.block)))),
    while_statement: $ => seq('while', $._expression, $.block),
    loop_statement: $ => seq('loop', $.block),
    break_statement: $ => seq('break', optional(';')),
    continue_statement: $ => seq('continue', optional(';')),
    return_statement: $ => seq('return', optional($._expression), optional(';')),
    out_statement: $ => seq('out', $.path, ',', $._expression, optional(';')),
    assignment_statement: $ => seq($.place, $.assignment_operator, $._expression, optional(';')),
    expression_statement: $ => seq($._expression, optional(';')),

    asm_statement: $ => seq('asm', optional('volatile'), optional($.asm_operands), '{', repeat($.string_literal), '}'),
    asm_operands: $ => seq('(', optional(seq($.asm_operand, repeat(seq(',', $.asm_operand)), optional(','))), ')'),
    asm_operand: $ => choice(
      seq('in', $.identifier, ':', $.type, optional(seq('as', $.asm_operand_class))),
      seq('out', $.identifier, ':', $.type, optional(seq('as', $.asm_operand_class))),
      seq('clobber', $.identifier),
    ),
    asm_operand_class: _ => choice('reg8', 'reg16', 'reg24', 'mem', 'imm'),

    place: $ => choice(
      $.identifier,
      seq('*', $._expression),
      seq($.path, '[', $._expression, ']'),
      seq($.identifier, '.', $.identifier),
    ),
    assignment_operator: _ => choice('<<=', '>>=', '+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '='),

    _expression: $ => choice(
      $.boolean_literal,
      $.char_literal,
      $.string_literal,
      $.integer_literal,
      $.path,
      $.array_literal,
      $.struct_literal,
      $.call_expression,
      $.index_expression,
      $.field_expression,
      $.cast_expression,
      $.in_expression,
      $.unary_expression,
      $.binary_expression,
      $.parenthesized_expression,
    ),

    binary_expression: $ => choice(
      ...[
        [PREC.logical_or, '||'],
        [PREC.logical_and, '&&'],
        [PREC.bit_or, '|'],
        [PREC.bit_xor, '^'],
        [PREC.bit_and, '&'],
        [PREC.equality, choice('==', '!=')],
        [PREC.comparison, choice('<', '<=', '>', '>=')],
        [PREC.shift, choice('<<', '>>')],
        [PREC.additive, choice('+', '-')],
        [PREC.multiplicative, choice('*', '/', '%')],
      ].map(([precedence, operator]) => prec.left(precedence, seq($._expression, operator, $._expression)))
    ),
    unary_expression: $ => prec(PREC.unary, seq(choice('-', '~', '!', '*', '&'), $._expression)),
    call_expression: $ => prec(PREC.call, seq(field('function', $.path), $.arguments)),
    index_expression: $ => prec(PREC.call, seq($.path, '[', $._expression, ']')),
    field_expression: $ => prec(PREC.call, seq($.identifier, '.', $.identifier)),
    cast_expression: $ => seq('cast', '<', $.type, '>', '(', $._expression, ')'),
    in_expression: $ => seq('in', $.path),
    parenthesized_expression: $ => seq('(', $._expression, ')'),
    array_literal: $ => seq('[', optional($.argument_list), ']'),
    struct_literal: $ => seq($.path, '{', optional(seq($.field_initializer, repeat(seq(',', $.field_initializer)), optional(','))), '}'),
    field_initializer: $ => seq($.identifier, ':', $._expression),
    arguments: $ => seq('(', optional($.argument_list), ')'),
    argument_list: $ => seq($._expression, repeat(seq(',', $._expression)), optional(',')),

    embed_source: $ => choice(
      seq('file', '(', $.string_literal, ')'),
      seq('bytes', '[', optional($.argument_list), ']'),
      seq('text', '(', $.string_literal, ')'),
      seq('cstr', '(', $.string_literal, ')'),
      seq('repeat', '(', $._expression, ',', $._expression, ')'),
    ),
    embed_options: $ => repeat1(choice(seq('section', $.section_name), seq('align', $._expression))),

    type: $ => choice($.primitive_type, $.pointer_type, $.array_type, $.path),
    primitive_type: _ => choice('u8', 'i8', 'u16', 'i16', 'u24', 'i24', 'bytes'),
    pointer_type: $ => seq('ptr', '<', $.type, '>'),
    array_type: $ => seq('[', $.type, ';', $._expression, ']'),

    path: $ => seq($.identifier, repeat(seq('.', $.identifier))),
    section_name: $ => token(seq(optional('.'), /[A-Za-z_][A-Za-z0-9_]*/, repeat(seq('.', /[A-Za-z_][A-Za-z0-9_]*/)))),
    identifier: _ => /[A-Za-z_][A-Za-z0-9_]*/,
    boolean_literal: _ => choice('true', 'false'),
    integer_literal: _ => token(seq(choice(/0x[0-9A-Fa-f]+/, /0b[01]+/, /[0-9]+/), optional(choice('u8', 'i8', 'u16', 'i16', 'u24', 'i24')))),
    char_literal: _ => /'(\\[n0t\\'"]|[^'\\])'/,
    string_literal: _ => /"(\\[n0t\\'"]|[^"\\])*"/,
    line_comment: _ => token(seq('//', /.*/)),
  },
});
