///|
pub(all) enum ValueType {
  Int32
  Float64
  Boolean
  Text
  Binary
  Void
} derive(Eq, Debug)

///|
pub struct Parameter {
  name : String
  kind : ValueType
}

///|
pub struct Function {
  name : String
  params : Array[Parameter]
  result : ValueType
}

///|
pub struct Config {
  module_dir : String
  package_name : String
  native_module : String
  functions : Array[Function]
}

///|
pub(all) suberror ConfigError {
  Invalid(String)
} derive(Debug)

///|
fn field(value : Json, key : String) -> Json? {
  match value {
    Object(obj) => obj.get(key)
    _ => None
  }
}

///|
fn required_string(value : Json, key : String) -> String raise ConfigError {
  match field(value, key) {
    Some(String(text)) if text != "" => text
    _ => raise Invalid("missing/non-string field: " + key)
  }
}

///|
pub fn identifier(name : String) -> Bool {
  if name.is_empty() {
    return false
  }
  for index, ch in name {
    if !(ch >= 'a' && ch <= 'z') &&
      !(ch >= 'A' && ch <= 'Z') &&
      ch != '_' &&
      !(index > 0 && ch >= '0' && ch <= '9') {
      return false
    }
  }
  !name.has_prefix("__")
}

///|
fn parse_type(name : String, result~ : Bool) -> ValueType raise ConfigError {
  match name {
    "Int" => Int32
    "Double" => Float64
    "Bool" => Boolean
    "String" => Text
    "Bytes" => Binary
    "Unit" if result => Void
    _ => raise Invalid("unsupported type: " + name)
  }
}

///|
pub fn parse_config(text : String) -> Config raise ConfigError {
  let json = @json.parse(text) catch { _ => raise Invalid("invalid JSON") }
  match field(json, "version") {
    Some(Number(1.0, ..)) => ()
    _ => raise Invalid("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("invalid package name")
  }
  let native_module = required_string(json, "nativeModule")
  if !identifier(native_module) {
    raise Invalid("invalid nativeModule")
  }
  let values = match field(json, "functions") {
    Some(Array(values)) if !values.is_empty() => values
    _ => raise Invalid("functions must be a non-empty array")
  }
  let functions : Array[Function] = []
  let names : Map[String, Bool] = Map([])
  for value in values {
    let name = required_string(value, "name")
    if !identifier(name) || names.contains(name) {
      raise Invalid("invalid/duplicate function: " + name)
    }
    names[name] = true
    let entries = match field(value, "params") {
      Some(Array(entries)) => entries
      _ => raise Invalid("params must be an array: " + name)
    }
    let params : Array[Parameter] = []
    let parameters : Map[String, Bool] = Map([])
    for entry in entries {
      let pname = required_string(entry, "name")
      if !identifier(pname) || parameters.contains(pname) {
        raise Invalid("invalid/duplicate parameter: " + pname)
      }
      parameters[pname] = true
      params.push({
        name: pname,
        kind: parse_type(required_string(entry, "type"), result=false),
      })
    }
    functions.push({
      name,
      params,
      result: parse_type(required_string(value, "return"), result=true),
    })
  }
  { module_dir, package_name, native_module, functions, }
}

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

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

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

///|
pub fn ValueType::moon_name(self : ValueType) -> String {
  match self {
    Int32 => "Int"
    Float64 => "Double"
    Boolean => "Bool"
    Text => "String"
    Binary => "Bytes"
    Void => "Unit"
  }
}

///|
pub fn ValueType::c_name(self : ValueType) -> String {
  match self {
    Int32 | Boolean => "int32_t"
    Float64 => "double"
    Text => "moonbit_string_t"
    Binary => "moonbit_bytes_t"
    Void => "void"
  }
}

///|
pub fn ValueType::ark_name(self : ValueType) -> String {
  match self {
    Int32 | Float64 => "number"
    Boolean => "boolean"
    Text => "string"
    Binary => "Uint8Array"
    Void => "void"
  }
}