///|
/// One step from the previous snapshot to the next one.
///
/// A rename is not in this list on purpose: nothing in the source says whether
/// a vanished column and a new one are the same column, and guessing wrong
/// silently deletes data. A rename shows up as a drop and an add, which is at
/// least visible in the generated SQL before it runs.
pub(all) enum Change {
  CreateTable(TableDef)
  DropTable(String)
  AddColumn(table~ : String, column~ : ColumnDef)
  DropColumn(table~ : String, column~ : String)
  AlterColumn(table~ : String, before~ : ColumnDef, after~ : ColumnDef)
  CreateIndex(table~ : String, index~ : IndexDef)
  DropIndex(table~ : String, index~ : String)
} derive(Debug, Eq)

///|
/// Compare two snapshots, in the order the statements should run.
///
/// Tables are created before columns move, so a new foreign key can point at a
/// table this same migration introduces.
pub fn diff(before : Snapshot, after : Snapshot) -> Array[Change] {
  let changes : Array[Change] = []
  // Index drops go first: on some engines a column cannot be dropped or
  // retyped while an index still covers it.
  for t in after.tables {
    if before.table(t.name) is Some(old) {
      for idx in old.indexes {
        if t.index(idx.name) != Some(idx) {
          changes.push(DropIndex(table=t.name, index=idx.name))
        }
      }
    }
  }
  for t in after.tables {
    if before.table(t.name) is None {
      changes.push(CreateTable(t))
    }
  }
  for t in before.tables {
    if after.table(t.name) is None {
      changes.push(DropTable(t.name))
    }
  }
  for t in after.tables {
    guard before.table(t.name) is Some(old) else { continue }
    for c in t.columns {
      match old.column(c.name) {
        None => changes.push(AddColumn(table=t.name, column=c))
        Some(prev) =>
          if prev != c {
            changes.push(AlterColumn(table=t.name, before=prev, after=c))
          }
      }
    }
    for c in old.columns {
      if t.column(c.name) is None {
        changes.push(DropColumn(table=t.name, column=c.name))
      }
    }
  }
  for t in after.tables {
    guard before.table(t.name) is Some(old) else { continue }
    for idx in t.indexes {
      if old.index(idx.name) != Some(idx) {
        changes.push(CreateIndex(table=t.name, index=idx))
      }
    }
  }
  changes
}

///|
/// A one-line description of a change, for `status` output and for naming a
/// migration after what it does.
pub fn Change::summary(self : Change) -> String {
  match self {
    CreateTable(t) => "create table \{t.name}"
    DropTable(name) => "drop table \{name}"
    AddColumn(table~, column~) => "add column \{table}.\{column.name}"
    DropColumn(table~, column~) => "drop column \{table}.\{column}"
    AlterColumn(table~, before=_, after~) =>
      "alter column \{table}.\{after.name}"
    CreateIndex(table=_, index~) => "create index \{index.name}"
    DropIndex(table=_, index~) => "drop index \{index}"
  }
}

///|
/// A file-name slug derived from what the migration actually does.
///
/// drizzle-kit names migrations after a random word pair; naming them after
/// the change means `git log` on the migrations directory reads as a schema
/// changelog. Falls back to `migration` when there is nothing to say.
pub fn auto_name(changes : Array[Change]) -> String {
  guard changes.length() > 0 else { return "migration" }
  let head = slug(changes[0].summary())
  if changes.length() == 1 {
    head
  } else {
    "\{head}_and_\{changes.length() - 1}_more"
  }
}

///|
/// Lower-case, underscore-separated, safe in a file name.
fn slug(s : String) -> String {
  let buf = StringBuilder()
  let mut written = false
  let mut pending_sep = false
  for c in s {
    if c.is_ascii_alphabetic() || c.is_ascii_digit() {
      if pending_sep && written {
        buf.write_char('_')
      }
      pending_sep = false
      written = true
      buf.write_char(c.to_ascii_lowercase())
    } else {
      pending_sep = true
    }
  }
  buf.to_string()
}

///|
/// Order tables so that a table comes after everything it references.
///
/// SQLite would not mind — it resolves foreign keys lazily — but a migration
/// file is also something a person reads, and "referenced first" is the order
/// the schema is easiest to follow in. It is also the order the stricter
/// dialects will need when their emitter arrives.
///
/// A reference cycle is left in name order rather than reported: two tables
/// pointing at each other is a legal schema, and this function's job is
/// presentation, not validation.
pub fn creation_order(tables : Array[TableDef]) -> Array[TableDef] {
  let out : Array[TableDef] = []
  let visiting : Array[String] = []
  fn visit(t : TableDef) -> Unit {
    if out.search_by(o => o.name == t.name) is Some(_) ||
      visiting.contains(t.name) {
      return
    }
    visiting.push(t.name)
    for c in t.columns {
      if c.references is Some(r) && r.table != t.name {
        if tables.search_by(o => o.name == r.table) is Some(i) {
          visit(tables[i])
        }
      }
    }
    let _ = visiting.pop()
    out.push(t)
  }

  for t in tables {
    visit(t)
  }
  out
}