///|
pub(all) enum TypeExpr {
  Scalar(ValueType)
  List(TypeExpr)
  Record(String)
  /// Opaque MoonBit value owned by an ArkTS handle object with `close()`.
  Handle(String)
  /// Platform value acquired by a native adapter, e.g. `@rawfile.Manager`.
  External(String, 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
  is_async : Bool
  raises : Bool
}

///|
pub struct Project {
  version : Int
  module_dir : String
  package_name : String
  native_module : String
  workspace : String?
  records : Array[RecordDef]
  handles : Array[String]
  functions : Array[Callable]
  adapters : Array[AdapterDef]
}

///|
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 if name.has_prefix("@") => {
      let parts = name[1:].split(".").map(s => s.to_owned()).to_array()
      if parts.length() != 2 || !identifier(parts[0]) || !identifier(parts[1]) {
        raise Invalid("type: platform types are written @package.Type")
      }
      External(parts[0], parts[1])
    }
    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) | Handle(name) => name
    External(pkg_alias, name) => "@" + pkg_alias + "." + name
  }
}

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

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

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

///|
/// Handles and adapters cross the bridge by reference and are never copied.
pub fn TypeExpr::is_opaque(self : TypeExpr) -> Bool {
  match self {
    Handle(_) | External(_, _) => true
    List(element) => element.is_opaque()
    _ => false
  }
}

///|
fn TypeExpr::bind_handles(self : TypeExpr, handles : Array[String]) -> TypeExpr {
  match self {
    Record(name) if handles.contains(name) => Handle(name)
    List(element) => List(element.bind_handles(handles))
    other => other
  }
}

///|
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
}

///|
fn optional_flag(
  value : Json,
  key : String,
  version : Int,
  context : String,
) -> Bool raise ConfigError {
  match field(value, key) {
    None => false
    Some(_) if version < 2 =>
      raise Invalid(context + ": " + key + " requires version 2")
    Some(True) => true
    Some(False) => false
    _ => raise Invalid(context + ": " + key + " must be a boolean")
  }
}

///|
pub fn parse_project(text : String) -> Project raise ConfigError {
  let json = @json.parse(text) catch {
    _ => raise Invalid("config: invalid JSON")
  }
  let version = match field(json, "version") {
    Some(Number(1.0, ..)) => 1
    Some(Number(2.0, ..)) => 2
    _ => raise Invalid("config: version must be 1 or 2")
  }
  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 workspace = match field(json, "workspace") {
    None => None
    Some(_) if version < 2 =>
      raise Invalid("config: workspace requires version 2")
    Some(String(path)) if path != "" => Some(path)
    _ => raise Invalid("config: workspace must be a path")
  }
  let reserved = ["Int", "Double", "Bool", "String", "Bytes", "Unit", "Array"]
  let names : Map[String, Bool] = Map([])
  let handles : Array[String] = []
  match field(json, "handles") {
    None => ()
    Some(_) if version < 2 => raise Invalid("config: handles require version 2")
    Some(Array(entries)) =>
      for entry in entries {
        let name = checked_name(entry, "handles")
        if names.contains(name) || reserved.contains(name) {
          raise Invalid("handles: duplicate/reserved type " + name)
        }
        names[name] = true
        handles.push(name)
      }
    _ => raise Invalid("config: handles must be an array")
  }
  let entries = match field(json, "types") {
    None => []
    Some(Array(entries)) => entries
    _ => raise Invalid("config: types must be an array")
  }
  let records : Array[RecordDef] = []
  for entry in entries {
    let name = checked_name(entry, "types")
    if names.contains(name) || reserved.contains(name) {
      raise Invalid("types: duplicate/reserved type " + name)
    }
    names[name] = true
    records.push({
      name,
      fields: parse_fields(entry, "fields", "types." + name).map(f => {
        name: f.name,
        kind: f.kind.bind_handles(handles),
      }),
    })
  }
  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
    let context = "functions." + name
    let result = parse_type_expr(required_string(entry, "return"), result=true)
    functions.push({
      name,
      params: parse_fields(entry, "params", context).map(p => {
        name: p.name,
        kind: p.kind.bind_handles(handles),
      }),
      result: result.bind_handles(handles),
      is_async: optional_flag(entry, "async", version, context),
      raises: optional_flag(entry, "raises", version, context),
    })
  }
  {
    version,
    module_dir,
    package_name,
    native_module,
    workspace,
    records,
    handles,
    functions,
    adapters: [],
  }
}

///|
pub fn Config::to_project(self : Config) -> Project {
  {
    version: 1,
    module_dir: self.module_dir,
    package_name: self.package_name,
    native_module: self.native_module,
    workspace: None,
    records: [],
    handles: [],
    functions: self.functions.map(f => {
      name: f.name,
      params: f.params.map(p => { name: p.name, kind: Scalar(p.kind), }),
      result: Scalar(f.result),
      is_async: false,
      raises: false,
    }),
    adapters: [],
  }
}

///|
/// Attaches platform adapters discovered from native dependency descriptors.
pub fn Project::with_adapters(
  self : Project,
  adapters : Array[AdapterDef],
) -> Project {
  { ..self, adapters, }
}

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

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