///|
fn[T : ToJson] write_json_show(value : T, logger : &Logger) -> Unit {
  logger.write_string(value.to_json().stringify())
}

///|
pub fn bytes_to_hex(data : Bytes) -> String {
  let out = StringBuilder(size_hint=data.length() * 2)
  let digit = fn(v : Int) -> Char {
    if v < 10 {
      (48 + v).unsafe_to_char()
    } else {
      (87 + v).unsafe_to_char()
    }
  }
  for b in data {
    let v = b.to_int()
    let hi = (v >> 4) & 0x0f
    let lo = v & 0x0f
    out.write_char(digit(hi))
    out.write_char(digit(lo))
  }
  out.to_string()
}

///|
pub fn decode_json_column(
  value : Json,
  storage : String,
) -> Json raise ParamDecodeError {
  match storage {
    "bson" =>
      match @bson.json_bytes_to_bytes(value) {
        Some(data) =>
          @bson.from_bytes(data) catch {
            BsonError(msg) =>
              raise ParamDecodeError("BSON decode error: \{msg}")
          }
        None => value
      }
    _ => value
  }
}

///|
pub fn param_row_from_json(row : Json) -> Map[String, Param] {
  let out : Map[String, Param] = Map([])
  if row is Object(obj) {
    for key in obj.keys() {
      if obj.get(key) is Some(value) {
        out.set(key, to_param(value))
      }
    }
  }
  out
}

///|
pub(all) enum WhereType {
  Eq
  Ne
  Gt
  Lt
  Gte
  Lte
  Like
} derive(Eq, ToJson, FromJson)

///|
pub impl Show for WhereType with fn output(self : WhereType, logger : &Logger) {
  write_json_show(self, logger)
}

///|
pub(all) struct Where {
  col : String
  value : Param
  ty : WhereType
} derive(Eq, ToJson, FromJson)

///|
pub impl Show for Where with fn output(self : Where, logger : &Logger) {
  write_json_show(self, logger)
}

///|
pub(all) enum OrderBy {
  Asc(String)
  Desc(String)
} derive(Eq, ToJson, FromJson)

///|
pub impl Show for OrderBy with fn output(self : OrderBy, logger : &Logger) {
  write_json_show(self, logger)
}

///|
pub(all) enum SortDirection {
  Asc
  Desc
} derive(Debug, Eq, ToJson, FromJson)

///|
pub impl Show for SortDirection with fn output(
  self : SortDirection,
  logger : &Logger,
) {
  match self {
    Asc => logger.write_string("Asc")
    Desc => logger.write_string("Desc")
  }
}

///|
pub(all) struct Sort {
  property : String
  direction : SortDirection
} derive(Debug, Eq, ToJson, FromJson)

///|
pub impl Show for Sort with fn output(self : Sort, logger : &Logger) {
  match self.direction {
    Asc => logger.write_string("Sort(\{self.property} Asc)")
    Desc => logger.write_string("Sort(\{self.property} Desc)")
  }
}

///|
pub(all) struct Pageable {
  page : Int
  size : Int
  sort : Sort?
} derive(Debug, Eq, ToJson, FromJson)

///|
pub impl Show for Pageable with fn output(self : Pageable, logger : &Logger) {
  match self.sort {
    Some(s) =>
      logger.write_string(
        "Pageable(page=\{self.page}, size=\{self.size}, sort=\{s})",
      )
    None =>
      logger.write_string("Pageable(page=\{self.page}, size=\{self.size})")
  }
}

///|
pub(all) struct Page[T] {
  content : FixedArray[T]
  total_elements : Int
  total_pages : Int
  number : Int
  size : Int
  first : Bool
  last : Bool
  empty : Bool
} derive(Eq, ToJson, FromJson)

///|
pub impl[T] Show for Page[T] with fn output(self, logger) {
  logger.write_string(
    "Page(content=\{self.content.length()}, total_elements=\{self.total_elements}, total_pages=\{self.total_pages}, number=\{self.number}, size=\{self.size})",
  )
}

///|
pub(all) struct Set {
  col : String
  value : Param
} derive(Eq, ToJson, FromJson)

///|
pub impl Show for Set with fn output(self : Set, logger : &Logger) {
  write_json_show(self, logger)
}

///|
pub(open) trait QueryBuilder {
  fn to_query(Self) -> Query
}

///|
pub(open) trait Engine {
  async fn page(Self, query : &QueryBuilder, pageable : Pageable) -> QueryResult
  async fn page_raw(
    Self,
    sql : String,
    params : FixedArray[Param],
    pageable : Pageable,
  ) -> QueryResult
  /// Execute a normalized query model (statement/raw/transaction).
  async fn exec(Self, query : &QueryBuilder) -> QueryResult
  ///|
  /// Convenience wrapper for raw SQL; internally equivalent to exec(QueryRaw(...)).
  async fn exec_raw(Self, sql : String, params : FixedArray[Param]) -> QueryResult
  ///|
  /// Apply table migration from schema model.
  async fn migrate_table(Self, table_schema : @table.Table) -> QueryResult
  ///|
  /// Close the database connection.
  fn close(Self) -> Unit
}