// The `model` tree: tables declared with `#orm` annotations, and the build rule
// that turns them into `T::model()`.
//
// The annotation is the declaration. `moonorm`'s own generator reads these structs
// and writes the `@moonorm.Model[T]` — columns, row decoder, insert projection —
// into `.g.mbt` at build time, so what this generator emits is the table
// itself rather than a hand-built copy of it. Only the parts moonorm cannot know
// are written here: the migrations and, for a SQL schema, the CRUD helpers.

///|
/// The package the annotated models live in.
let orm_pkg = "internal/model"

///|
/// `ty` without the trailing `?` that makes it optional.
fn orm_base(ty : String) -> String {
  if @text.ends_with(ty, "?") {
    ty[0:ty.length() - 1].to_owned()
  } else {
    ty
  }
}

///|
/// The `#orm.column` a field needs beyond what its type already says: a primary
/// key, the SQL kind of a MoonBit type `moonorm` has no default column for, and
/// `autoincrement` on an optional integer key. `None` when the type says it all.
fn orm_annotation(
  ty : String,
  primary_key : Bool,
  default : String?,
) -> String? {
  let base = orm_base(ty)
  let args : Array[String] = []
  if primary_key {
    args.push("primary_key=true")
    // Only an optional key can be left for the database to assign.
    if @text.ends_with(ty, "?") && (base == "Int32" || base == "Int64") {
      args.push("autoincrement=true")
    }
  }
  match orm_kind(base) {
    Some(kind) => args.push("kind=" + @text.quote(kind))
    None => ()
  }
  match default {
    Some(text) => args.push("default=" + @text.quote(text))
    None => ()
  }
  if args.length() == 0 {
    None
  } else {
    Some("#orm.column(" + args.join(", ") + ")")
  }
}

///|
/// The `kind=` argument for a MoonBit type `moonorm` has no default column for.
/// `Int32` is 32 bits wide where `moonorm`'s `IntType` is the platform word, so it
/// has to say which column it wants; every other type in the `.api`/DDL vocabulary
/// maps on its own.
fn orm_kind(base : String) -> String? {
  match base {
    "Int32" => Some("int")
    _ => None
  }
}

///|
/// One annotated field: its column annotation, if it needs one, over the field
/// itself. A field whose type has no single-column storage — a slice, a map, a
/// nested `type` — takes `#orm.skip`, which keeps it out of the table and fills it
/// from `Default` when a row is read.
fn orm_field(
  field : String,
  ty : String,
  primary_key : Bool,
  default : String?,
) -> String {
  let note = match @model.storage_class(orm_base(ty)) {
    Some(_) =>
      match orm_annotation(ty, primary_key, default) {
        Some(annotation) => "  " + annotation + "\n"
        None => ""
      }
    None => "  #orm.skip\n"
  }
  note + "  " + field + " : " + ty + "\n"
}

///|
/// Whether any field of a struct takes `#orm.skip`, which is what makes
/// `#derive(Default)` necessary.
fn orm_skips(fields : Array[(String, String)]) -> Bool {
  for f in fields {
    if @model.storage_class(orm_base(f.1)) is None {
      return true
    }
  }
  false
}

///|
/// The annotated struct for one table. `table` is the SQL name, `name` the MoonBit
/// type, and `fields` its `(field, type)` pairs in declaration order.
fn orm_struct(
  name : String,
  table : String,
  doc : String,
  fields : Array[(String, String)],
  key : String,
  defaults : Map[String, String],
) -> String {
  let pairs : Array[(String, String)] = []
  for f in fields {
    pairs.push(f)
  }
  let skips = orm_skips(pairs)
  let mut out = "///|\n/// " + doc + "\n"
  if skips {
    out = out + "#derive(Default)\n"
  }
  out = out +
    "#orm.table(name=" +
    @text.quote(table) +
    ")\npub(all) struct " +
    name +
    " {\n"
  for f in pairs {
    let default = defaults.get(f.0)
    out = out + orm_field(f.0, f.1, f.0 == key, default)
  }
  out = out + "}\n"
  if skips {
    // The derive puts the impl in the type's own package, which promotes its
    // methods onto the type; saying so explicitly keeps that a choice.
    out = out +
      "\n///|\n/// `" +
      name +
      "` with every field at its default, which is what a `#orm.skip` field is\n/// filled from when a row is read.\npub extend " +
      name +
      " with Default::{default}\n"
  }
  out
}

///|
/// The `moon.pkg` of the model package: the library, the rule that regenerates the
/// model from the annotations, and the formatter exemption for what it writes.
fn orm_pkg_file(imports : Array[(String, String)]) -> String {
  let mut out = pkg_file(imports)
  out = out +
    "\ndev_build(rule: \"orm\", input: \"models.mbt\", output: \"models.g.mbt\")\n"
  out = out + "\nformatter(ignore: [ \"models.g.mbt\" ])\n"
  out
}

///|
/// The `moon.mod` of a model module: the two libraries and the rule that runs
/// `moonorm`'s generator over the annotated file.
fn orm_mod_file(module_ : String) -> String {
  mod_file(module_, ["moonbitstack/moonorm", "moonbitstack/moondb"]) +
  "\n" +
  rule_line("orm", generator_command("moonbitstack/moonorm", "orm"))
}

///|
/// The tree a model module is made of: the module file, the package with its build
/// rule, the annotated models, the migrations, and the CRUD helpers when there are
/// any. `crud` is `Once` — it is the half the author edits.
fn orm_tree(
  module_ : String,
  st : @style.Style,
  dir : String,
  models : String,
  migrate : String,
  crud : String,
) -> Array[TreeFile] {
  let imports : Array[(String, String)] = [("moonbitstack/moonorm", "")]
  if migrate != "" || crud != "" {
    imports.push(("moonbitstack/moondb", ""))
  }
  let out : Array[TreeFile] = [
    { path: at(dir, "moon.mod"), content: orm_mod_file(module_), regen: Once, },
    {
      path: at(dir, orm_pkg + "/moon.pkg"),
      content: orm_pkg_file(imports),
      regen: Once,
    },
    {
      path: at(dir, orm_pkg + "/" + st.format("models") + ".mbt"),
      content: models,
      regen: Always,
    },
  ]
  if migrate != "" {
    out.push({
      path: at(dir, orm_pkg + "/" + st.format("migrate") + ".mbt"),
      content: migrate,
      regen: Always,
    })
  }
  if crud != "" {
    out.push({
      path: at(dir, orm_pkg + "/" + st.format("crud") + ".mbt"),
      content: crud,
      regen: Once,
    })
  }
  out
}

///|
/// The annotated models and migrations for `spec`: every `type` block is a table,
/// its `id` field (when it has one) the primary key, and a field with no single
/// column — a slice, a map, a nested type — is skipped rather than stored.
pub fn generate_orm_tree(
  spec : @api.Spec,
  style? : @style.Style,
  dir~ : String = "",
) -> Array[TreeFile] {
  let st = style.unwrap_or(@style.Style::gozero())
  let mut models = "// Code generated by moonctl from the `.api` spec. DO NOT EDIT.\n//\n// Each struct is a table: `#orm.table` names it, and the `@moonorm.Model` is\n// generated from these annotations into `" +
    st.format("models") +
    ".g.mbt` on build.\n"
  let mut migrate = "// Code generated by moonctl from the `.api` spec. DO NOT EDIT.\n"
  for t in spec.types {
    let fields = @api.flat_fields(t, spec.types)
    let table = @text.to_snake(t.name)
    let pairs : Array[(String, String)] = []
    for f in fields {
      pairs.push((f.mbt_name(), f.type_))
    }
    let has_key = fields.any(f => {
      f.mbt_name() == "id" && @model.storage_class(f.type_) is Some(_)
    })
    models = models +
      "\n" +
      orm_struct(
        t.name,
        table,
        "`" +
        t.name +
        "` table, from the `.api` `type` block of the " +
        spec.service +
        " service.",
        pairs,
        if has_key {
          "id"
        } else {
          ""
        },
        Map([]),
      )
    if has_key {
      migrate = migrate +
        "\n" +
        @model.migration_updown(table, t.name + "::model()")
    }
  }
  orm_tree(
    spec.service,
    st,
    dir,
    models,
    if @text.ends_with(migrate, "DO NOT EDIT.\n") {
      ""
    } else {
      migrate
    },
    "",
  )
}

///|
/// The annotated models, migrations and CRUD helpers for a SQL schema. The table
/// and column names are the schema's; a nullable column is declared optional, so
/// `#orm` reads it as one.
pub fn generate_orm_tree_from_ddl(
  tables : Array[@model.DdlTable],
  module_ : String,
  style? : @style.Style,
  dir~ : String = "",
) -> Array[TreeFile] {
  let st = style.unwrap_or(@style.Style::gozero())
  let mut models = "// Code generated by moonctl from the SQL schema. DO NOT EDIT.\n//\n// Each struct is a table: `#orm.table` names it, and the `@moonorm.Model` is\n// generated from these annotations into `" +
    st.format("models") +
    ".g.mbt` on build.\n"
  let mut migrate = "// Code generated by moonctl from the SQL schema. DO NOT EDIT.\n"
  let mut crud = "// Code generated by moonctl from the SQL schema. DO NOT EDIT.\n"
  for t in tables {
    let name = @text.to_pascal(t.name)
    let pairs : Array[(String, String)] = []
    let defaults : Map[String, String] = Map([])
    let mut key = ""
    for c in t.columns {
      pairs.push((c.name, ddl_field_type(c)))
      if c.primary_key && key == "" {
        key = c.name
      }
      match c.default_ {
        Some(text) => defaults.set(c.name, text)
        None => ()
      }
    }
    models = models +
      "\n" +
      orm_struct(name, t.name, "`" + t.name + "` table.", pairs, key, defaults)
    migrate = migrate + "\n" + @model.migration_for(t, name + "::model()")
    crud = crud +
      "\n" +
      @model.crud_for(t, name + "::model()", nullable_aware=true)
  }
  orm_tree(module_, st, dir, models, migrate, crud)
}

///|
/// The MoonBit type a column is declared as in the annotated tree: a nullable
/// column is optional, which is how `#orm` spells `NULL`. The flat generator keeps
/// the column's own type and says `nullable` on the model instead; here the type
/// carries it, so a `None` has to be bound as SQL `NULL` — which is what
/// `nullable_aware` asks `crud_for` for.
fn ddl_field_type(c : @model.DdlColumn) -> String {
  if c.nullable && !c.primary_key {
    c.type_ + "?"
  } else {
    c.type_
  }
}