///|
/// Which kind of declaration starts at the cursor, for diagnostics.
priv enum DeclarationKind {
  FunctionKind
  TypeKind
  PortKind
  InfixKind
}

///|
fn declaration_kind(c : Cursor) -> DeclarationKind {
  match c.peek() {
    Some({ kind: Keyword(Type), .. }) => TypeKind
    Some({ kind: Keyword(Port), .. }) => PortKind
    Some({ kind: Identifier, lexeme: "infix", .. }) => InfixKind
    _ => FunctionKind
  }
}

///|
/// Type variables of a type or alias declaration, up to `=`.
fn type_parameters(c : Cursor) -> Array[@ast.Node[String]] raise SyntaxError {
  let params = []
  while !c.at_kind(Equals) {
    let p = c.lower("a type variable or `=`")
    params.push(token_node(p, p.lexeme))
  }
  ignore(c.advance())
  params
}

///|
fn value_constructor(
  c : Cursor,
) -> @ast.Node[@ast.ValueConstructor] raise SyntaxError {
  let name = c.upper("a constructor name")
  let arguments = []
  while c.positively_indented() && starts_type(c) {
    arguments.push(type_no_fn(c, false))
  }
  let range = match arguments.last() {
    Some(last) => combine(range_of(name), last.range)
    None => range_of(name)
  }
  node(range, { name: token_node(name, name.lexeme), arguments, })
}

///|
/// `type alias Name a = type` or `type Name a = A x | B`.
fn type_declaration(
  c : Cursor,
  documentation : @ast.Node[String]?,
) -> @ast.Node[@ast.Declaration] raise SyntaxError {
  let type_token = c.expect(Keyword(Type), "`type`")
  let start = match documentation {
    Some(doc) => doc.range.start
    None => location(type_token.span.start)
  }
  if c.peek() is Some({ kind: Identifier, lexeme: "alias", .. }) {
    ignore(c.advance())
    if c.at_lower() {
      raise c.fail_titled(
        "expected a type name",
        "EXPECTING TYPE ALIAS NAME",
        [
          Plain(
            "I am partway through parsing a type alias, but I got stuck here:",
          ),
        ],
        [
          Text([
            Plain("I was expecting a name like "),
            @scanner.Chunk::code("Person"),
            Plain(" or "),
            @scanner.Chunk::code("Point"),
            Plain(" next. Type alias names must start with a capital letter."),
          ]),
        ],
      )
    }
    let name = c.upper("a type name")
    let generics = type_parameters(c)
    let annotation = type_annotation(c)
    return node(
      { start, end: annotation.range.end, },
      AliasDeclaration({
        documentation,
        name: token_node(name, name.lexeme),
        generics,
        type_annotation: annotation,
      }),
    )
  }
  let name = c.upper("a type name")
  let generics = type_parameters(c)
  let constructors = [value_constructor(c)]
  while c.at_kind(Pipe) && c.positively_indented() {
    ignore(c.advance())
    constructors.push(value_constructor(c))
  }
  node(
    { start, end: constructors.last().unwrap().range.end, },
    CustomTypeDeclaration({
      documentation,
      name: token_node(name, name.lexeme),
      generics,
      constructors,
    }),
  )
}

///|
/// `port name : type`. elm-syntax starts the range at column 1 of the name's
/// row, and a doc comment before a port goes to `File.comments`.
fn port_declaration(
  c : Cursor,
) -> @ast.Node[@ast.Declaration] raise SyntaxError {
  let port_token = c.expect(Keyword(Port), "`port`")
  if c.dialect.has(PortInNormalModule) && !c.port_module {
    raise c.error_at(
      "ports need a port module [rule: port-in-normal-module]",
      port_token.span,
      "UNEXPECTED PORTS",
      "You are declaring ports in a normal module:",
      [
        Hint([
          Plain("Start the file with "),
          @scanner.Chunk::code("port module"),
          Plain(" instead of "),
          @scanner.Chunk::code("module"),
          Plain(" to declare ports."),
        ]),
      ],
    )
  }
  let name = c.lower("a port name")
  ignore(c.expect(Colon, "`:`"))
  let annotation = type_annotation(c)
  node(
    {
      start: { row: name.span.start.line, column: 1, },
      end: annotation.range.end,
    },
    PortDeclaration({
      name: token_node(name, name.lexeme),
      type_annotation: annotation,
    }),
  )
}

///|
/// `infix left 6 (+) = add`.
fn infix_declaration(
  c : Cursor,
) -> @ast.Node[@ast.Declaration] raise SyntaxError {
  let infix_token = c.advance()
  let direction_token = c.lower("`left`, `right` or `non`")
  let direction : @ast.InfixDirection = match direction_token.lexeme {
    "left" => Left
    "right" => Right
    "non" => Non
    _ =>
      raise c.error_at(
        "expected `left`, `right` or `non`",
        direction_token.span,
        "SYNTAX PROBLEM",
        "I was expecting `left`, `right` or `non` here:",
        [],
      )
  }
  let precedence_token = c.expect(IntLiteral, "a precedence")
  let precedence = match int_value(c, precedence_token) {
    Ok(n) => n.to_int()
    Err(_) =>
      raise c.error_at(
        "expected a decimal precedence",
        precedence_token.span,
        "SYNTAX PROBLEM",
        "I was expecting a decimal precedence here:",
        [],
      )
  }
  let (open, op, close) = c.within(Parens, () => {
    let open = c.expect(LParen, "`(`")
    if c.dialect.has(InfixDeclaration) && !c.dialect.core_package {
      raise c.error_at(
        "infix declarations are only allowed in elm packages [rule: infix-declaration]",
        { start: open.span.end, end: open.span.end, },
        "UNFINISHED PARENTHESES",
        "I was not expecting an infix declaration here:",
        [
          Text([
            Plain("Only "),
            @scanner.Chunk::code("elm/*"),
            Plain(" and "),
            @scanner.Chunk::code("elm-explorations/*"),
            Plain(
              " packages can declare infix operators. Define a normal function instead.",
            ),
          ]),
        ],
      )
    }
    guard c.peek() is Some({ kind: Operator(op), .. }) else {
      raise c.fail("an operator")
    }
    ignore(c.advance())
    let close = c.expect(RParen, "`)`")
    (open, op, close)
  })
  ignore(c.expect(Equals, "`=`"))
  let function = c.lower("a function name")
  node(
    range_from(infix_token, function),
    InfixDeclaration({
      direction: token_node(direction_token, direction),
      precedence: token_node(precedence_token, precedence),
      operator: node(range_from(open, close), op),
      function: token_node(function, function.lexeme),
    }),
  )
}

///|
/// A top-level declaration. `documentation` is the doc comment attached to
/// it, when there is one (never for ports and infix declarations).
fn declaration(
  c : Cursor,
  documentation : @ast.Node[String]?,
) -> @ast.Node[@ast.Declaration] raise SyntaxError {
  match declaration_kind(c) {
    TypeKind => {
      let context = if c.peek_at(1) is Some({ lexeme: "alias", .. }) {
        TypeAlias
      } else {
        CustomType
      }
      c.within(context, () => type_declaration(c, documentation))
    }
    PortKind => c.within(Port, () => port_declaration(c))
    InfixKind => c.within(Infix, () => infix_declaration(c))
    FunctionKind => {
      let f = match c.peek() {
        Some(t) if is_lower(t) =>
          c.within(Definition(t.lexeme), () => function_parts(c, documentation))
        _ => function_parts(c, documentation)
      }
      node(f.range, FunctionDeclaration(f.value))
    }
  }
}