///|
enum PrecType {
  NonAssoc
  Left
  Right
  Dynamic
} derive(Eq, Hash)

///|
impl Show for PrecType with to_string(self) {
  match self {
    PrecType::NonAssoc => "PREC"
    PrecType::Left => "PREC_LEFT"
    PrecType::Right => "PREC_RIGHT"
    PrecType::Dynamic => "PREC_DYNAMIC"
  }
}

///|
impl Show for PrecType with output(self, logger) {
  logger.write_string(self.to_string())
}

///|
impl ToJson for PrecType with to_json(self) {
  self.to_string().to_json()
}

///|
enum PrecValue {
  Number(Int)
  String(String)
} derive(Show, Hash, Eq)

///|
impl ToJson for PrecValue with to_json(self : PrecValue) -> Json {
  match self {
    PrecValue::Number(value) => value.to_json()
    PrecValue::String(value) => value.to_json()
  }
}

///|
impl @json.FromJson for PrecValue with from_json(
  json : Json,
  json_path : @json.JsonPath,
) -> PrecValue raise @json.JsonDecodeError {
  match json {
    Number(value, ..) => PrecValue::Number(value.to_int())
    String(value) => PrecValue::String(value)
    _ => {
      let message = "PrecValue::from_json: expected number or string, got \{json}"
      raise @json.JsonDecodeError((json_path, message))
    }
  }
}

///|
pub enum Rule {
  Alias(named~ : Bool, String, Rule)
  Blank
  Symbol(String)
  String(String)
  Repeat(Rule)
  Repeat1(Rule)
  Choice(Array[Rule])
  Seq(Array[Rule])
  Field(name~ : String, Rule)
  Token(immediate~ : Bool, Rule)
  Prec(prec~ : PrecType, value~ : PrecValue, Rule)
  Pattern(String)
} derive(Show, Hash, Eq)

///|
fn[E : Error] Rule::visit_rule(
  self : Rule,
  visitor : (Rule) -> Unit raise E,
) -> Unit raise E {
  visitor(self)
  match self {
    Alias(_, rule, ..) => rule.visit_rule(visitor)
    Prec(rule, ..) => rule.visit_rule(visitor)
    Token(rule, ..) => rule.visit_rule(visitor)
    Field(rule, ..) => rule.visit_rule(visitor)
    Seq(rules) | Choice(rules) =>
      for rule in rules {
        rule.visit_rule(visitor)
      }
    Repeat1(rule) | Repeat(rule) => rule.visit_rule(visitor)
    String(_) => ()
    Symbol(_) => ()
    Blank => ()
    Pattern(_) => ()
  }
}

///|
impl @json.FromJson for Rule with from_json(
  json : Json,
  json_path : @json.JsonPath,
) -> Rule raise @json.JsonDecodeError {
  match json {
    {
      "type": "ALIAS",
      "named": True
      | False as named,
      "value": String(value),
      "content": rule,
      ..
    } =>
      Rule::Alias(
        named=@json.from_json(named),
        value,
        Rule::from_json(rule, json_path.add_key("content")),
      )
    { "type": "BLANK", .. } => Rule::Blank
    { "type": "SYMBOL", "name": String(name), .. } => Rule::Symbol(name)
    { "type": "STRING", "value": String(value), .. } => Rule::String(value)
    { "type": "CHOICE", "members": Array(members_json), .. } => {
      let members_json_path = json_path.add_key("members")
      let rules = []
      for i, member_json in members_json {
        rules.push(Rule::from_json(member_json, members_json_path.add_index(i)))
      }
      Rule::Choice(rules)
    }
    { "type": "SEQ", "members": Array(members_json), .. } => {
      let members_json_path = json_path.add_key("members")
      let rules = []
      for i, member_json in members_json {
        rules.push(Rule::from_json(member_json, members_json_path.add_index(i)))
      }
      Rule::Seq(rules)
    }
    { "type": "REPEAT", "content": rule, .. } =>
      Rule::Repeat(Rule::from_json(rule, json_path.add_key("content")))
    { "type": "REPEAT1", "content": rule, .. } =>
      Rule::Repeat1(Rule::from_json(rule, json_path.add_key("content")))
    { "type": "TOKEN", "content": rule, .. } =>
      Rule::Token(
        immediate=false,
        Rule::from_json(rule, json_path.add_key("content")),
      )
    { "type": "IMMEDIATE_TOKEN", "content": rule, .. } =>
      Rule::Token(
        immediate=true,
        Rule::from_json(rule, json_path.add_key("content")),
      )
    { "type": "FIELD", "name": String(name), "content": rule, .. } =>
      Rule::Field(name~, Rule::from_json(rule, json_path.add_key("content")))
    {
      "type": String([.. "PREC", .. suffix]),
      "value": value,
      "content": rule,
      ..
    } => {
      let prec = match suffix {
        [.. ""] => PrecType::NonAssoc
        [.. "_LEFT"] => PrecType::Left
        [.. "_RIGHT"] => PrecType::Right
        [.. "_DYNAMIC"] => PrecType::Dynamic
        suffix => {
          let message = "Rule::from_json: invalid precedence type \"PREC\{suffix}\""
          raise @json.JsonDecodeError((json_path, message))
        }
      }
      Rule::Prec(
        prec~,
        value=value |> @json.from_json(path=json_path.add_key("value")),
        Rule::from_json(rule, json_path.add_key("content")),
      )
    }
    { "type": "PATTERN", "value": String(value), .. } => Rule::Pattern(value)
    { "type": String(type_), .. } => {
      let message = "Rule::from_json: unknown rule type \"\{type_}\""
      raise @json.JsonDecodeError((json_path, message))
    }
    { "type": type_, .. } => {
      let message = "Rule::from_json: expected object with \"type\" field, got \{type_}"
      raise @json.JsonDecodeError((json_path, message))
    }
    json => {
      let message = "Rule::from_json: expected object with \"type\" field, got \{json}"
      raise @json.JsonDecodeError((json_path, message))
    }
  }
}

///|
impl ToJson for Rule with to_json(self : Rule) -> Json {
  match self {
    Alias(named~, value, rule) =>
      {
        "type": "ALIAS",
        "value": value.to_json(),
        "named": named.to_json(),
        "content": rule.to_json(),
      }
    Prec(prec~, value~, rule) =>
      {
        "type": prec.to_json(),
        "value": value.to_json(),
        "content": rule.to_json(),
      }
    Token(immediate~, rule) => {
      let token_type = if immediate { "IMMEDIATE_TOKEN" } else { "TOKEN" }
      { "type": token_type.to_json(), "content": rule.to_json() }
    }
    Field(name~, rule) =>
      { "type": "FIELD", "name": Json::string(name), "content": rule.to_json() }
    Seq(rules) => { "type": "SEQ", "members": rules.to_json() }
    Choice(rules) => { "type": "CHOICE", "members": rules.to_json() }
    Repeat1(rule) => { "type": "REPEAT1", "content": rule.to_json() }
    Repeat(rule) => { "type": "REPEAT", "content": rule.to_json() }
    String(value) => { "type": "STRING", "value": value.to_json() }
    Symbol(name) => { "type": "SYMBOL", "name": name.to_json() }
    Blank => { "type": "BLANK" }
    Pattern(value) => { "type": "PATTERN", "value": value.to_json() }
  }
}

///|
struct RuleName {
  name : String
  mut rule : Rule?
} derive(Show)

///|
impl @json.FromJson for RuleName with from_json(
  json : Json,
  json_path : @json.JsonPath,
) -> RuleName raise @json.JsonDecodeError {
  guard json is String(name) else {
    let message = "RuleName::from_json: expected string, got \{json}"
    raise @json.JsonDecodeError((json_path, message))
  }
  { name, rule: None }
}

///|
impl ToJson for RuleName with to_json(self : RuleName) -> Json {
  self.name.to_json()
}

///|
pub struct Grammar {
  schema : String
  name : String
  rules : Map[String, Rule]
  extras : Array[Rule]
  supertypes : Array[RuleName]
  reserved : Map[String, Rule]
  precedences : Array[Array[Rule]]
  externals : Array[Rule]
  inline : Array[RuleName]
  conflicts : Array[Array[RuleName]]
  word : RuleName?
} derive(Show)

///|
pub impl @json.FromJson for Grammar with from_json(
  json : Json,
  json_path : @json.JsonPath,
) -> Grammar raise @json.JsonDecodeError {
  match json {
    Object(
      {
        "$schema": String(schema),
        "name": String(name),
        "rules": Object(rules_json),
        "extras": Array(extras_json),
        "supertypes": Array(supertypes_json),
        "reserved": Object(reserved_json),
        "precedences": Array(precedences),
        "externals": Array(externals),
        "inline": Array(inline),
        "conflicts": Array(conflicts),
        ..
      } as json
    ) => {
      let word = match json.get("word") {
        None => None
        Some(Null) => None
        Some(String(name)) =>
          Some(RuleName::from_json(name.to_json(), json_path.add_key("word")))
        _ => {
          let message = "Grammar::from_json: expected string or null for \"word\""
          raise @json.JsonDecodeError((json_path.add_key("word"), message))
        }
      }
      Grammar::{
        schema,
        name,
        rules: {
          let rules = {}
          for name, rule_json in rules_json {
            rules[name] = Rule::from_json(
              rule_json,
              json_path.add_key("rules").add_key(name),
            )
          }
          rules
        },
        extras: extras_json.map(rule_json => {
          Rule::from_json(rule_json, json_path.add_key("extras"))
        }),
        supertypes: supertypes_json.map(rule_name_json => {
          RuleName::from_json(rule_name_json, json_path.add_key("supertypes"))
        }),
        reserved: {
          let reserved = {}
          for name, rule_json in reserved_json {
            reserved[name] = Rule::from_json(
              rule_json,
              json_path.add_key("reserved").add_key(name),
            )
          }
          reserved
        },
        precedences: precedences.map(rules_json => {
          @json.from_json(rules_json, path=json_path.add_key("precedences"))
        }),
        externals: externals.map(rule_json => {
          @json.from_json(rule_json, path=json_path.add_key("externals"))
        }),
        inline: inline.map(rule_name_json => {
          @json.from_json(rule_name_json, path=json_path.add_key("inline"))
        }),
        conflicts: conflicts.map(rule_names_json => {
          @json.from_json(rule_names_json, path=json_path.add_key("conflicts"))
        }),
        word,
      }
    }
    _ => {
      let message = "Grammar::from_json: expected object with required fields"
      raise @json.JsonDecodeError((json_path, message))
    }
  }
}

///|
pub impl ToJson for Grammar with to_json(self : Grammar) -> Json {
  {
    "$schema": self.schema.to_json(),
    "name": self.name.to_json(),
    "rules": self.rules.to_json(),
    "extras": self.extras.to_json(),
    "supertypes": self.supertypes.to_json(),
    "reserved": self.reserved.to_json(),
    "precedences": self.precedences.to_json(),
    "externals": self.externals.to_json(),
    "inline": self.inline.to_json(),
    "conflicts": self.conflicts.to_json(),
    "word": match self.word {
      None => Json::null()
      Some(word) => word.to_json()
    },
  }
}

///|
suberror GrammarParseError {
  JsonDecodeError(@json.JsonDecodeError)
  RuleNotFound(String)
} derive(Show)

///|
pub fn Grammar::from_json(json : Json) -> Grammar raise GrammarParseError {
  let grammar : Grammar = @json.from_json(json) catch {
    error => raise JsonDecodeError(error)
  }
  for _, rule in grammar.rules {
    let visitor = fn(rule : Rule) -> Unit raise GrammarParseError {
      match rule {
        Symbol(name) =>
          if grammar.rules.get(name) is None {
            raise RuleNotFound(name)
          }
        _ => return
      }
    }
    rule.visit_rule(visitor)
  }
  for supertype in grammar.supertypes {
    guard grammar.rules.get(supertype.name) is Some(rule) else {
      raise RuleNotFound(supertype.name)
    }
    supertype.rule = Some(rule)
  }
  for inline in grammar.inline {
    guard grammar.rules.get(inline.name) is Some(rule) else {
      raise RuleNotFound(inline.name)
    }
    inline.rule = Some(rule)
  }
  for conflict in grammar.conflicts {
    for rule_name in conflict {
      guard grammar.rules.get(rule_name.name) is Some(rule) else {
        raise RuleNotFound(rule_name.name)
      }
      rule_name.rule = Some(rule)
    }
  }
  if grammar.word is Some(word) {
    guard grammar.rules.get(word.name) is Some(rule) else {
      raise RuleNotFound(word.name)
    }
    word.rule = Some(rule)
  }
  grammar
}