///|
/// Expression produced by the phase-0 parser.
/// Component calls use `Comp`. `@Custom(...)` sets `is_function`.
pub enum Expr {
  Null
  Bool(Bool)
  Num(Double, String)
  Str(String)
  Ref(String)
  StateRef(String)
  Comp(String, Bool, Array[Expr])
  Arr(Array[Expr])
  Obj(Array[(String, Expr)])
  UnaryOp(String, Expr)
  BinOp(String, Expr, Expr)
  Ternary(Expr, Expr, Expr)
  Member(Expr, String)
  Index(Expr, Expr)
  Assign(String, Expr)
}

///|
pub struct Statement {
  id : String
  expr : Expr
  line : Int32
}

///|
pub struct ParseError {
  line : Int32
  column : Int32
  kind : SyntaxKind
}

///|
pub fn ParseError::message(self : ParseError) -> String {
  self.kind.message()
}

///|
pub struct Program {
  statements : Array[Statement]
  errors : Array[ParseError]
}

///|
fn join_dump(items : Array[Expr]) -> String {
  let buf = StringBuilder()
  let mut index = 0
  for item in items {
    if index > 0 {
      buf.write_string(", ")
    }
    buf.write_string(item.dump())
    index += 1
  }
  buf.to_string()
}

///|
pub fn Expr::dump(self : Expr) -> String {
  match self {
    Null => "Null"
    Bool(true) => "Bool(true)"
    Bool(false) => "Bool(false)"
    Num(_, repr) => "Num(" + repr + ")"
    Str(text) => "Str(" + Json::string(text).stringify() + ")"
    Ref(name) => "Ref(" + name + ")"
    StateRef(name) => "StateRef(" + name + ")"
    Comp(name, is_function, args) =>
      if is_function {
        "Fn(" + name + ", [" + join_dump(args) + "])"
      } else {
        "Comp(" + name + ", [" + join_dump(args) + "])"
      }
    Arr(items) => "Arr([" + join_dump(items) + "])"
    Obj(entries) => {
      let buf = StringBuilder()
      buf.write_string("Obj({")
      let mut index = 0
      for entry in entries {
        if index > 0 {
          buf.write_string(", ")
        }
        let (key, value) = entry
        buf.write_string(Json::string(key).stringify())
        buf.write_string(": ")
        buf.write_string(value.dump())
        index += 1
      }
      buf.write_string("})")
      buf.to_string()
    }
    UnaryOp(op, operand) => "Unary(" + op + ", " + operand.dump() + ")"
    BinOp(op, left, right) =>
      "Bin(" + op + ", " + left.dump() + ", " + right.dump() + ")"
    Ternary(cond, then_branch, else_branch) =>
      "Ternary(" +
      cond.dump() +
      ", " +
      then_branch.dump() +
      ", " +
      else_branch.dump() +
      ")"
    Member(obj, field) => "Member(" + obj.dump() + ", " + field + ")"
    Index(obj, index) => "Index(" + obj.dump() + ", " + index.dump() + ")"
    Assign(target, value) => "Assign(" + target + ", " + value.dump() + ")"
  }
}

///|
fn object_of(fields : Array[(String, Json)]) -> Json {
  let map : Map[String, Json] = Map([])
  for field in fields {
    let (key, value) = field
    map[key] = value
  }
  Json::object(map)
}

///|
pub fn Expr::to_json(self : Expr) -> Json {
  match self {
    Null => object_of([("k", Json::string("Null"))])
    Bool(value) =>
      object_of([("k", Json::string("Bool")), ("v", Json::boolean(value))])
    Num(value, repr) =>
      object_of([("k", Json::string("Num")), ("v", Json::number(value, repr~))])
    Str(text) =>
      object_of([("k", Json::string("Str")), ("v", Json::string(text))])
    Ref(name) =>
      object_of([("k", Json::string("Ref")), ("n", Json::string(name))])
    StateRef(name) =>
      object_of([("k", Json::string("StateRef")), ("n", Json::string(name))])
    Comp(name, is_function, args) => {
      let encoded : Array[Json] = []
      for arg in args {
        encoded.push(arg.to_json())
      }
      let fields : Array[(String, Json)] = [
        ("k", Json::string("Comp")),
        ("name", Json::string(name)),
        ("args", Json::array(encoded)),
      ]
      if is_function {
        fields.push(("fn", Json::boolean(true)))
      }
      object_of(fields)
    }
    Arr(items) => {
      let encoded : Array[Json] = []
      for item in items {
        encoded.push(item.to_json())
      }
      object_of([("k", Json::string("Arr")), ("els", Json::array(encoded))])
    }
    Obj(entries) => {
      let encoded : Array[Json] = []
      for entry in entries {
        let (key, value) = entry
        encoded.push(Json::array([Json::string(key), value.to_json()]))
      }
      object_of([("k", Json::string("Obj")), ("entries", Json::array(encoded))])
    }
    UnaryOp(op, operand) =>
      object_of([
        ("k", Json::string("UnaryOp")),
        ("op", Json::string(op)),
        ("operand", operand.to_json()),
      ])
    BinOp(op, left, right) =>
      object_of([
        ("k", Json::string("BinOp")),
        ("op", Json::string(op)),
        ("left", left.to_json()),
        ("right", right.to_json()),
      ])
    Ternary(cond, then_branch, else_branch) =>
      object_of([
        ("k", Json::string("Ternary")),
        ("cond", cond.to_json()),
        ("then", then_branch.to_json()),
        ("else", else_branch.to_json()),
      ])
    Member(obj, field) =>
      object_of([
        ("k", Json::string("Member")),
        ("obj", obj.to_json()),
        ("field", Json::string(field)),
      ])
    Index(obj, index) =>
      object_of([
        ("k", Json::string("Index")),
        ("obj", obj.to_json()),
        ("index", index.to_json()),
      ])
    Assign(target, value) =>
      object_of([
        ("k", Json::string("Assign")),
        ("target", Json::string(target)),
        ("value", value.to_json()),
      ])
  }
}

///|
pub fn Program::to_json(self : Program) -> Json {
  let statements : Array[Json] = []
  for stmt in self.statements {
    statements.push(
      object_of([
        ("id", Json::string(stmt.id)),
        ("line", Json::number(Double::from_int(stmt.line))),
        ("expr", stmt.expr.to_json()),
      ]),
    )
  }
  let errors : Array[Json] = []
  for err in self.errors {
    errors.push(
      object_of([
        ("line", Json::number(Double::from_int(err.line))),
        ("column", Json::number(Double::from_int(err.column))),
        ("message", Json::string(err.message())),
      ]),
    )
  }
  object_of([
    ("statements", Json::array(statements)),
    ("errors", Json::array(errors)),
  ])
}

///|
fn write_hex_byte(buf : StringBuilder, code : Int32) -> Unit {
  let digits = "0123456789abcdef"
  match digits.at(code / 16).to_int().to_char() {
    Some(ch) => buf.write_char(ch)
    None => ()
  }
  match digits.at(code % 16).to_int().to_char() {
    Some(ch) => buf.write_char(ch)
    None => ()
  }
}

///|
fn write_json_string(buf : StringBuilder, text : String) -> Unit {
  buf.write_char('"')
  let mut index = 0
  let length = text.length()
  while index < length {
    let code = text.at(index).to_int()
    if code == 34 {
      buf.write_string("\\\"")
    } else if code == 92 {
      buf.write_string("\\\\")
    } else if code == 8 {
      buf.write_string("\\b")
    } else if code == 12 {
      buf.write_string("\\f")
    } else if code == 10 {
      buf.write_string("\\n")
    } else if code == 13 {
      buf.write_string("\\r")
    } else if code == 9 {
      buf.write_string("\\t")
    } else if code < 32 {
      buf.write_string("\\u00")
      write_hex_byte(buf, code)
    } else {
      match code.to_char() {
        Some(ch) => buf.write_char(ch)
        None => ()
      }
    }
    index += 1
  }
  buf.write_char('"')
}

///|
fn write_expr_list(buf : StringBuilder, items : Array[Expr]) -> Unit {
  buf.write_char('[')
  let mut index = 0
  for item in items {
    if index > 0 {
      buf.write_char(',')
    }
    write_expr_json(buf, item)
    index += 1
  }
  buf.write_char(']')
}

///|
fn write_expr_json(buf : StringBuilder, expr : Expr) -> Unit {
  match expr {
    Null => buf.write_string("{\"k\":\"Null\"}")
    Bool(true) => buf.write_string("{\"k\":\"Bool\",\"v\":true}")
    Bool(false) => buf.write_string("{\"k\":\"Bool\",\"v\":false}")
    Num(_, repr) => {
      buf.write_string("{\"k\":\"Num\",\"v\":")
      buf.write_string(repr)
      buf.write_char('}')
    }
    Str(text) => {
      buf.write_string("{\"k\":\"Str\",\"v\":")
      write_json_string(buf, text)
      buf.write_char('}')
    }
    Ref(name) => {
      buf.write_string("{\"k\":\"Ref\",\"n\":")
      write_json_string(buf, name)
      buf.write_char('}')
    }
    StateRef(name) => {
      buf.write_string("{\"k\":\"StateRef\",\"n\":")
      write_json_string(buf, name)
      buf.write_char('}')
    }
    Comp(name, is_function, args) => {
      buf.write_string("{\"k\":\"Comp\",\"name\":")
      write_json_string(buf, name)
      buf.write_string(",\"args\":")
      write_expr_list(buf, args)
      match mapped_param_names(name) {
        Some(params) => {
          buf.write_string(",\"mappedProps\":{")
          let mut index = 0
          let mut wrote = false
          while index < params.length() && index < args.length() {
            if wrote {
              buf.write_char(',')
            }
            wrote = true
            write_json_string(buf, params[index])
            buf.write_char(':')
            write_expr_json(buf, args[index])
            index = index + 1
          }
          buf.write_char('}')
        }
        None => ()
      }
      if is_function {
        buf.write_string(",\"fn\":true")
      }
      buf.write_char('}')
    }
    Arr(items) => {
      buf.write_string("{\"k\":\"Arr\",\"els\":")
      write_expr_list(buf, items)
      buf.write_char('}')
    }
    Obj(entries) => {
      buf.write_string("{\"k\":\"Obj\",\"entries\":[")
      let mut index = 0
      for entry in entries {
        if index > 0 {
          buf.write_char(',')
        }
        let (key, value) = entry
        buf.write_char('[')
        write_json_string(buf, key)
        buf.write_char(',')
        write_expr_json(buf, value)
        buf.write_char(']')
        index += 1
      }
      buf.write_string("]}")
    }
    UnaryOp(op, operand) => {
      buf.write_string("{\"k\":\"UnaryOp\",\"op\":")
      write_json_string(buf, op)
      buf.write_string(",\"operand\":")
      write_expr_json(buf, operand)
      buf.write_char('}')
    }
    BinOp(op, left, right) => {
      buf.write_string("{\"k\":\"BinOp\",\"op\":")
      write_json_string(buf, op)
      buf.write_string(",\"left\":")
      write_expr_json(buf, left)
      buf.write_string(",\"right\":")
      write_expr_json(buf, right)
      buf.write_char('}')
    }
    Ternary(cond, then_branch, else_branch) => {
      buf.write_string("{\"k\":\"Ternary\",\"cond\":")
      write_expr_json(buf, cond)
      buf.write_string(",\"then\":")
      write_expr_json(buf, then_branch)
      buf.write_string(",\"else\":")
      write_expr_json(buf, else_branch)
      buf.write_char('}')
    }
    Member(obj, field) => {
      buf.write_string("{\"k\":\"Member\",\"obj\":")
      write_expr_json(buf, obj)
      buf.write_string(",\"field\":")
      write_json_string(buf, field)
      buf.write_char('}')
    }
    Index(obj, index) => {
      buf.write_string("{\"k\":\"Index\",\"obj\":")
      write_expr_json(buf, obj)
      buf.write_string(",\"index\":")
      write_expr_json(buf, index)
      buf.write_char('}')
    }
    Assign(target, value) => {
      buf.write_string("{\"k\":\"Assign\",\"target\":")
      write_json_string(buf, target)
      buf.write_string(",\"value\":")
      write_expr_json(buf, value)
      buf.write_char('}')
    }
  }
}

///|
/// Statement JSON without the intermediate `Json` map. The Wasm binding parses this back in JavaScript.
pub fn Program::compact_json(self : Program) -> String {
  let buf = StringBuilder()
  buf.write_string("{\"statements\":[")
  let mut index = 0
  for stmt in self.statements {
    if index > 0 {
      buf.write_char(',')
    }
    buf.write_string("{\"id\":")
    write_json_string(buf, stmt.id)
    buf.write_string(",\"line\":")
    buf.write_string(stmt.line.to_string())
    buf.write_string(",\"expr\":")
    write_expr_json(buf, stmt.expr)
    buf.write_char('}')
    index += 1
  }
  buf.write_string("],\"errors\":[")
  index = 0
  for err in self.errors {
    if index > 0 {
      buf.write_char(',')
    }
    buf.write_string("{\"line\":")
    buf.write_string(err.line.to_string())
    buf.write_string(",\"column\":")
    buf.write_string(err.column.to_string())
    buf.write_string(",\"message\":")
    write_json_string(buf, err.message())
    buf.write_char('}')
    index += 1
  }
  buf.write_string("]}")
  buf.to_string()
}

///|
pub fn Token::to_json(self : Token) -> Json {
  let fields : Array[(String, Json)] = [
    ("t", Json::string(self.kind.name())),
    ("line", Json::number(Double::from_int(self.line))),
    ("column", Json::number(Double::from_int(self.column))),
  ]
  if self.text != "" {
    fields.push(("v", Json::string(self.text)))
  }
  object_of(fields)
}