// Hybrid attributes: one definition read two ways.
//
// A hybrid is a value that exists both as a SQL expression over columns and as a
// computed property of a record. SQLAlchemy gets this from a Python property whose
// getter works on an instance and whose class-level evaluation is rewritten into SQL;
// MoonBit has no property interception, so both halves are written out — but written
// once, in one value, which is what stops the two from drifting.
//
// The SQL half is identifier-level text (`{t}.first_name || ' ' || {t}.last_name`), so
// it carries no bound values and can be spliced into a predicate or an ORDER BY by the
// builder. `{t}` is the table qualifier, substituted by `Hybrid::over`, which is what a
// query over a join needs.

///|
/// A computed attribute with two readings: `expr` is SQL over the model's columns, and
/// `read` computes the same value from a record. `expr` writes `{t}` where the table
/// qualifier belongs, so one definition serves an unqualified single-table query and a
/// qualified one over a join.
pub struct Hybrid[T] {
  name : String
  expr : String
  read : (T) -> Value
}

///|
/// Define a hybrid attribute. `expr` is trusted SQL over column names, like
/// `Select::raw`: it may contain literals and operators, but no bound values — a
/// placeholder there would have no parameter to bind to. `read` must produce the value
/// the expression evaluates to; the pair is what a test comparing both readings
/// checks.
pub fn[T] hybrid(
  name : String,
  expr : String,
  read : (T) -> Value,
) -> Hybrid[T] {
  { name, expr, read, }
}

///|
/// The attribute's name — what its projection is labelled with.
pub fn[T] Hybrid::name(self : Hybrid[T]) -> String {
  self.name
}

///|
/// The expression as written, with `{t}` still in it — for a query with one table and
/// no ambiguity.
pub fn[T] Hybrid::expr(self : Hybrid[T]) -> String {
  self.expr
}

///|
/// The expression with `{t}` replaced by `table`, for a query that joins and has to say
/// which table the columns come from.
pub fn[T] Hybrid::over(self : Hybrid[T], table : String) -> String {
  replace_all(self.expr, "{t}", table)
}

///|
/// The SQL expression to use in this query: `table` when there is one, the expression
/// as written otherwise.
pub fn[T] Hybrid::sql(self : Hybrid[T], table? : String) -> String {
  match table {
    Some(t) => self.over(t)
    None => self.expr
  }
}

///|
/// The attribute's value on one record — the in-memory reading of the same definition.
pub fn[T] Hybrid::value(self : Hybrid[T], record : T) -> Value {
  (self.read)(record)
}

///|
/// Replace every occurrence of `needle` in `text`. Used for the `{t}` table
/// placeholder, so the needle and the expressions it appears in are ASCII.
fn replace_all(text : String, needle : String, replacement : String) -> String {
  if needle == "" {
    return text
  }
  let width = needle.length()
  let sb = StringBuilder()
  let mut at = 0
  while at < text.length() {
    let mut hit = at + width <= text.length()
    if hit {
      let mut k = 0
      while k < width {
        if text[at + k] != needle[k] {
          hit = false
        }
        k = k + 1
      }
    }
    if hit {
      sb.write_string(replacement)
      at = at + width
    } else {
      sb.write_char(text[at].unsafe_to_char())
      at = at + 1
    }
  }
  sb.to_string()
}

///|
/// The table a query reads from, when it is one named table — what a hybrid's `{t}`
/// placeholder is filled with. A derived table or a LATERAL has no name to qualify
/// with, so those return `None` and the caller has to name one.
fn Select::qualifier(self : Select) -> String? {
  match self.from_ {
    Tbl(name) => Some(name)
    Alias(_, name) => Some(name)
    _ => None
  }
}

///|
/// The hybrid's expression, qualified for this query: `table` when the caller names one,
/// the query's own table when its `FROM` is a named table, and the expression as written
/// when it has no `{t}` to fill.
///
/// Raises when the expression needs a qualifier and the query has none to offer, rather
/// than splicing a `{t}` into SQL the server would reject.
fn[T] Select::hybrid_sql(
  self : Select,
  h : Hybrid[T],
  table : String?,
) -> String raise @moondb.DbError {
  match table {
    Some(name) => h.over(name)
    None =>
      if !h.expr().contains("{t}") {
        h.expr()
      } else {
        match self.qualifier() {
          Some(name) => h.over(name)
          None =>
            raise @moondb.QueryError(
              "the hybrid " +
              h.name() +
              " uses {t} and this query reads from a derived table: name a table",
            )
        }
      }
  }
}

///|
/// Filter on a hybrid attribute: `expr op ?`. The expression is the hybrid's own, so the
/// predicate means the same thing the in-memory reading computes, and the value binds
/// as a parameter like any other.
pub fn[T] Select::where_hybrid(
  self : Select,
  h : Hybrid[T],
  op : String,
  val : Value,
  table? : String,
) -> Select raise @moondb.DbError {
  self.where_(self.hybrid_sql(h, table), op, val)
}

///|
/// Order by a hybrid attribute — the usual reason to define one, since a computed
/// column is not available to `ORDER BY` under its name.
pub fn[T] Select::order_hybrid(
  self : Select,
  h : Hybrid[T],
  ord : Order,
  table? : String,
) -> Select raise @moondb.DbError {
  self.order_by(self.hybrid_sql(h, table), ord)
}

///|
/// Project a hybrid attribute under its name, so a fetched row carries the computed
/// value the database worked out — `expr AS name`, SQLAlchemy's `hybrid_property` in a
/// `select()`.
pub fn[T] Select::project_hybrid(
  self : Select,
  h : Hybrid[T],
  table? : String,
) -> Select raise @moondb.DbError {
  self.label(self.hybrid_sql(h, table), h.name())
}