///|
pub(all) enum TypeExpr {
  Scalar(ValueType)
  List(TypeExpr)
  Record(String)
} derive(Eq, Debug)

///|
pub struct FieldDef {
  name : String
  kind : TypeExpr
}

///|
pub struct RecordDef {
  name : String
  fields : Array[FieldDef]
}

///|
pub struct Callable {
  name : String
  params : Array[FieldDef]
  result : TypeExpr
}

///|
pub struct Project {
  module_dir : String
  package_name : String
  native_module : String
  records : Array[RecordDef]
  functions : Array[Callable]
}

///|
priv struct TypeCursor {
  tokens : Array[String]
  mut position : Int
}

///|
fn type_tokens(text : String) -> Array[String] {
  let tokens : Array[String] = []
  let mut word = ""
  for ch in text {
    if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
      if !word.is_empty() {
        tokens.push(word)
        word = ""
      }
    } else if ch == '[' || ch == ']' {
      if !word.is_empty() {
        tokens.push(word)
        word = ""
      }
      tokens.push(ch.to_string())
    } else {
      word += ch.to_string()
    }
  }
  if !word.is_empty() {
    tokens.push(word)
  }
  tokens
}

///|
fn TypeCursor::take(self : TypeCursor) -> String raise ConfigError {
  if self.position >= self.tokens.length() {
    raise Invalid("type: unexpected end of expression")
  }
  let token = self.tokens[self.position]
  self.position += 1
  token
}

///|
fn TypeCursor::expression(
  self : TypeCursor,
  depth : Int,
  unit~ : Bool,
) -> TypeExpr raise ConfigError {
  if depth > 32 {
    raise Invalid("type: maximum nesting depth is 32")
  }
  match self.take() {
    "Array" => {
      if self.take() != "[" {
        raise Invalid("type: Array requires one type argument")
      }
      let element = self.expression(depth + 1, unit=false)
      if self.take() != "]" {
        raise Invalid("type: expected closing ]")
      }
      List(element)
    }
    "Int" => Scalar(Int32)
    "Double" => Scalar(Float64)
    "Bool" => Scalar(Boolean)
    "String" => Scalar(Text)
    "Bytes" => Scalar(Binary)
    "Unit" if unit => Scalar(Void)
    "Unit" => raise Invalid("type: Unit is only supported as a function result")
    name if identifier(name) => Record(name)
    name => raise Invalid("type: unsupported expression " + name)
  }
}

///|
pub fn parse_type_expr(
  text : String,
  result? : Bool = false,
) -> TypeExpr raise ConfigError {
  let cursor = TypeCursor::{ tokens: type_tokens(text), position: 0, }
  let kind = cursor.expression(0, unit=result)
  if cursor.position != cursor.tokens.length() {
    raise Invalid("type: trailing tokens in " + text)
  }
  kind
}

///|
pub fn TypeExpr::moon_name(self : TypeExpr) -> String {
  match self {
    Scalar(kind) => kind.moon_name()
    List(element) => "Array[" + element.moon_name() + "]"
    Record(name) => name
  }
}

///|
pub fn TypeExpr::wrapper_name(self : TypeExpr) -> String {
  match self {
    Scalar(kind) => kind.moon_name()
    List(element) => "Array[" + element.wrapper_name() + "]"
    Record(name) => "@core." + name
  }
}

///|
pub fn TypeExpr::c_name(self : TypeExpr) -> String {
  match self {
    Scalar(kind) => kind.c_name()
    List(_) | Record(_) => "void *"
  }
}

///|
pub fn TypeExpr::ark_name(self : TypeExpr) -> String {
  match self {
    Scalar(kind) => kind.ark_name()
    List(element) => "Array<" + element.ark_name() + ">"
    Record(name) => name
  }
}

///|
pub fn TypeExpr::is_reference(self : TypeExpr) -> Bool {
  match self {
    Scalar(Text | Binary) | List(_) | Record(_) => true
    _ => false
  }
}

///|
fn checked_name(value : Json, context : String) -> String raise ConfigError {
  let name = required_string(value, "name")
  if !identifier(name) {
    raise Invalid(context + ": invalid identifier " + name)
  }
  name
}

///|
fn parse_fields(
  value : Json,
  key : String,
  context : String,
) -> Array[FieldDef] raise ConfigError {
  let values = match field(value, key) {
    Some(Array(values)) => values
    _ => raise Invalid(context + ": " + key + " must be an array")
  }
  let fields : Array[FieldDef] = []
  let seen : Map[String, Bool] = Map([])
  for value in values {
    let name = checked_name(value, context)
    if seen.contains(name) {
      raise Invalid(context + ": duplicate field " + name)
    }
    seen[name] = true
    fields.push({ name, kind: parse_type_expr(required_string(value, "type")), })
  }
  fields
}

///|
pub fn parse_project(text : String) -> Project raise ConfigError {
  let json = @json.parse(text) catch {
    _ => raise Invalid("config: invalid JSON")
  }
  match field(json, "version") {
    Some(Number(1.0, ..)) => ()
    _ => raise Invalid("config: version must be 1")
  }
  let module_dir = required_string(json, "module")
  let package_name = required_string(json, "package")
  if package_name.contains("\"") ||
    package_name.contains("\\") ||
    package_name.contains("..") ||
    package_name.contains("\n") ||
    package_name.contains(" ") ||
    !package_name.contains("/") {
    raise Invalid("config: invalid package name")
  }
  let native_module = required_string(json, "nativeModule")
  if !identifier(native_module) {
    raise Invalid("config: invalid nativeModule")
  }
  let entries = match field(json, "types") {
    None => []
    Some(Array(entries)) => entries
    _ => raise Invalid("config: types must be an array")
  }
  let records : Array[RecordDef] = []
  let names : Map[String, Bool] = Map([])
  for entry in entries {
    let name = checked_name(entry, "types")
    if names.contains(name) ||
      ["Int", "Double", "Bool", "String", "Bytes", "Unit", "Array"].contains(
        name,
      ) {
      raise Invalid("types: duplicate/reserved type " + name)
    }
    names[name] = true
    records.push({
      name,
      fields: parse_fields(entry, "fields", "types." + name),
    })
  }
  let entries = match field(json, "functions") {
    Some(Array(entries)) if !entries.is_empty() => entries
    _ => raise Invalid("config: functions must be a non-empty array")
  }
  let functions : Array[Callable] = []
  let names : Map[String, Bool] = Map([])
  for entry in entries {
    let name = checked_name(entry, "functions")
    if names.contains(name) {
      raise Invalid("functions: duplicate function " + name)
    }
    names[name] = true
    functions.push({
      name,
      params: parse_fields(entry, "params", "functions." + name),
      result: parse_type_expr(required_string(entry, "return"), result=true),
    })
  }
  { module_dir, package_name, native_module, records, functions, }
}

///|
pub fn Config::to_project(self : Config) -> Project {
  {
    module_dir: self.module_dir,
    package_name: self.package_name,
    native_module: self.native_module,
    records: [],
    functions: self.functions.map(f => {
      name: f.name,
      params: f.params.map(p => { name: p.name, kind: Scalar(p.kind), }),
      result: Scalar(f.result),
    }),
  }
}

///|
#deprecated
pub extend TypeExpr with Eq::{equal, not_equal}

///|
#deprecated
pub extend TypeExpr with @debug.Debug::{to_repr}