///|
/// A short-term memory that survives the process.
///
/// The library is `riantr/tesseract/src/tesseract_memory`; the durable store
/// is `riantr/tesseract/src/tesseract_memory_db`. This is the window onto
/// both, and it is a **separate binary** from `cmd/memory` for the reason
/// `cmd/persist` is separate from `cmd/main`: the store is a `.db` file, and
/// the `.db` adapter builds only for `native` and `wasm`. Putting it in
/// `cmd/memory` would take that binary off `js` and `wasm-gc`.
///
/// The commands are the same ones `cmd/memory` has — they live in
/// `riantr/tesseract/src/tesseract_memory_cli` — so the two binaries differ
/// in **where the memory is kept**, not in what they can do to it.
///
/// ```sh
/// moon run cmd/memory_db --target native -- --file notes.db "put a hello"
/// moon run cmd/memory_db --target native -- --file notes.db "keys"
/// moon run cmd/memory_db --target native -- --file notes.db "get a"
/// ```
///
/// The file is written after **every** command, so it is current when the
/// next command starts and current when the process ends. The store is swept
/// before it is written, so the file holds what is **live**: an entry whose
/// time was up is not in it, and it does not come back.
///
/// The **deadlines are absolute**, so a store that was closed and reopened
/// finds its entries with the time they had left, not with the time they were
/// given. And the **clock comes back with them**: a store on a manual clock
/// that is reopened comes back on the timeline it was left on, so `advance`
/// moves it from where it was rather than from zero.
///
/// The **tour** (`demo`) is refused here. It writes a handful of entries into
/// the store it is given, and this store is a file the caller keeps — a tour
/// that left three entries behind would be a surprise in it. The tour in
/// `cmd/memory` is the same tour over a store that ends with the process.
///
/// ## The index
///
/// `--index PATH` names a **SQL index** of the store's entries: a plain SQLite
/// table, one row for each word of each entry, so "which of my entries say
/// 'cat'?" is a lookup rather than a scan. The index is a file of its own, and
/// it is **derivative** — it holds nothing the entries do not, so it can be
/// thrown away and built again.
///
/// ```sh
/// moon run cmd/memory_db --target native -- --file notes.db --index notes.idx "put a the cat sat"
/// moon run cmd/memory_db --target native -- --file notes.db --index notes.idx "index"
/// moon run cmd/memory_db --target native -- --file notes.db --index notes.idx "index-find cat"
/// ```
///
/// The `index` command is the **explicit rebuild**: it puts the index back in
/// step with the store and says how far it had drifted. A rebuild of an index
/// that was already right changes **nothing** and says so — that is the proof
/// that the index holds nothing of its own.
///
/// The index is **not** rebuilt by the other commands. A command that changes
/// the store changes the store, and the index is the caller's to rebuild when
/// it wants it fresh. So `index-find` checks the index against the store
/// before it answers, and says so when the two disagree — a lookup that
/// answered from a stale index without saying so would be worse than no index.

///|
/// How this binary is run, for the help.
fn how_to_run() -> @tesseract_memory_cli.Usage {
  @tesseract_memory_cli.usage(
    "moon run cmd/memory_db --target native -- --file PATH [options]  [args]...",
    [
      "  --file PATH               the checkpoint file this memory is written to",
      "  --index PATH              the SQL index of this memory's entries",
    ],
    [
      "  index                     rebuild the index from the store, and say what changed",
      "  index-find          the entries that contain the word",
    ],
  )
}

///|
/// The index path, and the arguments without it.
///
/// The index is this binary's own business — `cmd/memory` has none, and it
/// must not have one: the index is a `.db` file, and the `.db` adapter does
/// not build on every backend that binary does. So the flag is read here, and
/// the arguments it was in are taken out before the shared layer sees them.
/// A flag with no value after it is not a flag: it is left where it is, so
/// the shared layer reports it as the unknown command it is.
fn take_index(argv : ArrayView[String]) -> (String?, Array[String]) {
  let mut index : String? = None
  let rest : Array[String] = []
  let mut i = 0
  while i < argv.length() {
    let a = argv[i]
    if a == "--index" && i + 1 < argv.length() {
      index = Some(argv[i + 1])
      i = i + 2
      continue
    }
    rest.push(a)
    i = i + 1
  }
  (index, rest)
}

///|
/// Rebuilds the index from the store, and says what changed.
///
/// The store is what the index is built from, so the rebuild is the only thing
/// that puts the two in step. The report says how far the index was from the
/// store — and a rebuild of an index that was already right says nothing at
/// all, which is the proof that the index holds nothing of its own.
fn rebuild_index(path : String, m : @tesseract_memory.Memory) -> Unit {
  let index = match @tesseract_memory_index.open(path) {
    Ok(i) => i
    Err(message) => {
      @tesseract_memory_cli.warn(message)
      return
    }
  }
  match index.rebuild(m) {
    Ok(report) => {
      let rows = match index.rows() {
        Ok(n) => n.to_string()
        Err(message) => {
          @tesseract_memory_cli.warn(message)
          return
        }
      }
      @tesseract_memory_cli.out(
        "index " + index.path() + ": " + rows + " rows, " + report.describe(),
      )
    }
    Err(message) => @tesseract_memory_cli.warn(message)
  }
}

///|
/// The entries that contain a word.
///
/// The index is checked against the store **before** it answers, so a stale
/// index says so instead of quietly answering from the past. The check changes
/// nothing; the rebuild is a separate thing, on purpose.
fn find_in_index(
  path : String,
  word : String,
  m : @tesseract_memory.Memory,
) -> Unit {
  let index = match @tesseract_memory_index.open(path) {
    Ok(i) => i
    Err(message) => {
      @tesseract_memory_cli.warn(message)
      return
    }
  }
  match index.verify(m) {
    Ok(report) =>
      if report.changed > 0 || report.added > 0 || report.removed > 0 {
        @tesseract_memory_cli.warn(
          "the index at " +
          index.path() +
          " is out of step with the store: " +
          report.describe() +
          " (run `index` to rebuild it)",
        )
      }
    Err(message) => @tesseract_memory_cli.warn(message)
  }
  match index.lookup(word) {
    Ok(found) => {
      if found.length() == 0 {
        @tesseract_memory_cli.out(word + " -> (nothing)")
        return
      }
      for (scope, key) in found {
        @tesseract_memory_cli.out(word + " -> " + scope + "/" + key)
      }
    }
    Err(message) => @tesseract_memory_cli.warn(message)
  }
}

///|
fn main {
  let all = @env.args()
  let argv = if all.length() > 0 { all[1:] } else { all }
  let (index_path, rest) = take_index(argv)
  let (options, commands) = @tesseract_memory_cli.parse_session(rest)
  if commands.length() == 0 {
    @tesseract_memory_cli.print_help(how_to_run())
    return
  }
  match options.problem() {
    Some(message) => {
      @tesseract_memory_cli.warn(message)
      return
    }
    None => ()
  }
  // A durable memory without a file is not a durable memory: there would be
  // nothing to come back to.
  let path = match options.file {
    Some(p) => p
    None => {
      @tesseract_memory_cli.warn("a durable memory needs a file: --file ")
      @tesseract_memory_cli.print_help(how_to_run())
      return
    }
  }
  let clock = if options.manual {
    @tesseract_memory.memory_clock_manual(0L)
  } else {
    @tesseract_memory.memory_clock_system()
  }
  let store = match
    @tesseract_memory_db.open_with_clock(path, options.config(), clock) {
    Ok(s) => s
    Err(message) => {
      @tesseract_memory_cli.warn(message)
      return
    }
  }
  // The handle the store hands out is the store itself, so what the commands
  // write through it is what the checkpoint writes out.
  let m = store.memory()
  let mut scope = m.scope(options.scope_name)
  let mut k = 0
  while k < commands.length() {
    let text = commands[k]
    if @tesseract_memory_cli.is_demo(text) {
      @tesseract_memory_cli.warn(
        "the tour writes into the store; the in-memory tour is cmd/memory",
      )
    } else {
      let words = @tesseract_memory_cli.words_of(text)
      let verb = if words.length() > 0 { words[0] } else { "" }
      if verb == "index" || verb == "index-find" {
        // The index commands are this binary's own, because the index is a
        // `.db` file and the layer is shared with a binary that has no `.db`.
        let index_file = match index_path {
          Some(p) => p
          None => {
            @tesseract_memory_cli.warn("the index needs a file: --index ")
            k = k + 1
            continue
          }
        }
        if verb == "index" {
          rebuild_index(index_file, m)
        } else if words.length() < 2 {
          @tesseract_memory_cli.warn("index-find needs a word")
        } else {
          find_in_index(index_file, words[1], m)
        }
      } else {
        scope = @tesseract_memory_cli.run_command(m, scope, text, how_to_run())
      }
    }
    // The file is written after every command, so it is current when the next
    // command starts, and current when the process ends.
    match store.checkpoint() {
      Ok(_) => ()
      Err(message) =>
        @tesseract_memory_cli.warn(
          "could not write the checkpoint to " + store.path() + ": " + message,
        )
    }
    k = k + 1
  }
}