// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// The type inside `T?` or `Option[T]`, or `None` when `ty` is not optional.
fn inner_option(ty : String) -> String? {
  let ty = ty.trim(chars=" ")
  if ty.has_suffix("?") {
    Some(ty.exact_view(end=ty.length() - 1).trim(chars=" ").to_owned())
  } else {
    inside(ty.to_owned(), "Option[")
  }
}

///|
/// What sits between `head` and the closing bracket of `ty`.
fn inside(ty : String, head : String) -> String? {
  if ty.has_prefix(head) && ty.has_suffix("]") {
    Some(
      ty
      .exact_view(start=head.length(), end=ty.length() - 1)
      .trim(chars=" ")
      .to_owned(),
    )
  } else {
    None
  }
}

///|
/// The value type of a `Map[String, T]`. A key other than `String` has no description.
fn map_value(args : String) -> String? {
  guard args.find(",") is Some(comma) else { return None }
  guard args.exact_view(end=comma).trim(chars=" ") == "String" else {
    return None
  }
  Some(args.exact_view(start=comma + 1).trim(chars=" ").to_owned())
}

///|
/// The description type of a field written as `ty`, `None` when nothing says.
/// A name from another package is taken to be a model of the same name there:
/// nothing here can look, and a `Ref` is only a name.
fn kind_of(ty : String, known : Known) -> @moonmodel.Type? {
  let ty = ty.trim(chars=" ").to_owned()
  match ty {
    "Bool" => return Some(@moonmodel.Bool)
    "Int" | "Int32" | "Int16" | "UInt16" | "Byte" => return Some(@moonmodel.Int)
    "Int64" | "UInt" | "UInt32" | "UInt64" => return Some(@moonmodel.Big)
    "Double" | "Float" => return Some(@moonmodel.Real)
    "String" => return Some(@moonmodel.Text)
    "Bytes" => return Some(@moonmodel.Bytes)
    "Json" => return Some(@moonmodel.Any)
    _ => ()
  }
  for head in ["Array[", "FixedArray[", "ArrayView["] {
    if inside(ty, head) is Some(item) {
      return kind_of(item, known).map(k => @moonmodel.List(k))
    }
  }
  if inside(ty, "Map[") is Some(args) {
    return map_value(args)
      .bind(v => kind_of(v, known))
      .map(k => @moonmodel.Dict(k))
  }
  // An annotated enum is the set of its constructors, written as text.
  if known.enums.contains(ty) {
    return Some(@moonmodel.Text)
  }
  if known.structs.get(ty) is Some(name) {
    return Some(@moonmodel.Ref(name))
  }
  if ty.has_prefix("@") {
    match ty.rev_find(".") {
      Some(at) => {
        let name = ty.exact_view(start=at + 1).to_owned()
        return Some(@moonmodel.Ref(name))
      }
      None => ()
    }
  }
  None
}

///|
/// What a type is for the sake of a bound and of a default's spelling.
///
/// Finer than "is it a number" and coarser than the type itself: a whole
/// number's default is written as one and a real's with a decimal point, and
/// the bounds differ in which of the two they may be written on.
priv enum Family {
  Flag
  Integer
  Real
  Textual
  Sequence
  Mapping
  Opaque
}

///|
fn family(kind : @moonmodel.Type) -> Family {
  match kind {
    Bool => Flag
    Int | Big => Integer
    Real => Real
    // A layout is about how a value is written, not about what it is, so a
    // bound and a default follow the kind inside it.
    Wire(layout) => family(layout.kind)
    Text => Textual
    List(_) => Sequence
    Dict(_) => Mapping
    _ => Opaque
  }
}

///|
/// A description type as the code that builds it, in the package the
/// generated file lives in.
fn code_of(kind : @moonmodel.Type) -> String raise {
  match kind {
    Bool => "Bool"
    Int => "Int"
    Big => "Big"
    Real => "Real"
    Text => "Text"
    Bytes => "Bytes"
    Stamp => "Stamp"
    Any => "Any"
    List(inner) => "List(\{code_of(inner)})"
    Dict(inner) => "Dict(\{code_of(inner)})"
    Ref(name) => "Ref(\{quoted(name)})"
    Wire(layout) => {
      let parts = ["@moonmodel.Layout::new(\{code_of(layout.kind)}"]
      match layout.bits {
        Some(bits) => parts.push("bits=\{bits}")
        None => ()
      }
      if layout.unsigned {
        parts.push("unsigned=true")
      }
      if layout.order == @moonmodel.Order::Big {
        parts.push("order=@moonmodel.Order::Big")
      }
      if layout.reference {
        parts.push("reference=true")
      }
      "Wire(\{parts.join(", ")}))"
    }
    Nest(_) => fail("moonmodel: an annotation cannot state a nested model")
    Raw(_) => fail("moonmodel: an annotation cannot state a schema by hand")
  }
}

///|
/// A number written in a string, as a `Double` literal: an attribute has no
/// numbers of its own.
fn real(text : String, where_ : String) -> String raise {
  let number = @string.parse_double(text) catch {
    _ => fail("\{where_}: `\{text}` is not a number")
  }
  let shown = number.to_string()
  if shown.contains(".") || shown.contains("e") {
    shown
  } else {
    shown + ".0"
  }
}

///|
/// A whole number written in a string.
fn whole(text : String, where_ : String) -> String raise {
  let number : Int32 = @string.parse_int(text) catch {
    _ => fail("\{where_}: `\{text}` is not a whole number")
  }
  number.to_string()
}

///|
/// A value written in a string, as the `Json` literal for a field of this
/// family. The field's type says how to read it, so a text field's `"0"` stays
/// text.
fn value(text : String, kind : Family, where_ : String) -> String raise {
  match kind {
    Textual => quoted(text)
    Integer => whole(text, where_)
    Real => real(text, where_)
    Flag => {
      guard text is ("true" | "false") else {
        fail("\{where_}: `\{text}` is not true or false")
      }
      text
    }
    // A list, a mapping and a value of no shape at all are written as the
    // JSON they are: an attribute has no syntax of its own for them.
    Sequence | Mapping | Opaque => {
      let parsed = @json.parse(text) catch {
        _ => fail("\{where_}: `\{text}` is not JSON")
      }
      literal(parsed)
    }
  }
}

///|
/// A `Json` as the literal that builds it where a `Json` is expected.
fn literal(json : Json) -> String {
  match json {
    Null => "null"
    True => "true"
    False => "false"
    Number(n, ..) => n.to_string()
    String(s) => quoted(s)
    Array(items) => "[\{items.map(literal).join(", ")}]"
    Object(map) if map.is_empty() => "Json::object(Map([]))"
    Object(map) => {
      let pairs = map.iter().map(p => "\{quoted(p.0)}: \{literal(p.1)}")
      "{ \{pairs.collect().join(", ")} }"
    }
  }
}

///|
/// `HeroTeam` as `hero_team`, for the name of the `let` that holds a description.
fn snake(name : String) -> String {
  let out = StringBuilder()
  for i, c in name.to_array() {
    if c >= 'A' && c <= 'Z' {
      if i > 0 {
        out.write_char('_')
      }
      out.write_char((c.to_int() + 32).unsafe_to_char())
    } else {
      out.write_char(c)
    }
  }
  out.to_string()
}