// Values produced by BQL expressions and their text/JSON rendering.

///|
pub(all) enum QueryValue {
  QInt(@bigint.BigInt)
  QDict(Array[(String, QueryValue)])
  QList(Array[QueryValue])
  QRecord(String, Array[(String, QueryValue)])
  QEnum(String, String)
  QPythonType(String)
  QText(String)
  QSet(Array[String])
  QFrozenSet(Array[String])
  QNumber(@decimal.Decimal)
  QNegativeZero(Int32)
  QInterval(@bigint.BigInt, @bigint.BigInt)
  QDate(@ast.Date)
  QAmount(@ast.Amount)
  QPosition(@booking.Position)
  QInventory(@booking.Inventory)
  QNull
  QBool(Bool)
}

///|
pub(all) enum StatementResult {
  Table
  DirectiveText(String)
}

///|
pub(all) struct QueryResult {
  kind : StatementResult
  columns : Array[String]
  rows : Array[Array[QueryValue]]
  error : String?
}

///|
pub fn QueryResult::is_ok(self : QueryResult) -> Bool {
  self.error is None
}

///|
fn query_json_escape(text : String) -> String {
  let sb = StringBuilder()
  for i in 0.. c
      None => continue
    }
    match ch {
      '"' => sb.write_string("\\\"")
      '\\' => sb.write_string("\\\\")
      '\n' => sb.write_string("\\n")
      '\r' => sb.write_string("\\r")
      '\t' => sb.write_string("\\t")
      other =>
        if other.to_int() < 32 {
          sb.write_string("\\u00" + repr_hex(other.to_int(), 2))
        } else {
          sb.write_char(other)
        }
    }
  }
  sb.to_string()
}

///|
pub fn QueryValue::to_text(self : QueryValue) -> String {
  match self {
    QInterval(months, days) => interval_repr(months, days)
    QInt(n) => n.to_string()
    QDict(_) | QList(_) | QRecord(_, _) => python_repr(self)
    QEnum(name, value) => name + "." + value
    QPythonType(name) => ""
    QFrozenSet(values) =>
      if values.is_empty() {
        "frozenset()"
      } else {
        let sorted = values.copy()
        sorted.sort_by(fn(a, b) { a.lexical_compare(b) })
        "frozenset({" +
        sorted
        .map(fn(s) {
          "'" +
          s.replace_all(old="\\", new="\\\\").replace_all(old="'", new="\\'") +
          "'"
        })
        .join(", ") +
        "})"
      }
    QSet(values) => {
      let sorted = values.copy()
      sorted.sort_by(fn(a, b) { a.lexical_compare(b) })
      sorted.join(",")
    }
    QText(text) => text
    QNegativeZero(scale) =>
      "-" + python_decimal(@decimal.Decimal::zero_scaled(scale))
    QNumber(number) => python_decimal(number)
    QDate(date) => date.to_string()
    QAmount(amount) => python_amount(amount)
    QPosition(position) => python_position(position)
    QInventory(inventory) => {
      let sb = StringBuilder()
      let mut first = true
      for position in inventory.positions {
        if !first {
          sb.write_string(", ")
        }
        sb.write_string(position.to_string())
        first = false
      }
      sb.to_string()
    }
    QBool(b) => if b { "TRUE" } else { "FALSE" }
    QNull => ""
  }
}

///|
fn query_amount_json(amount : @ast.Amount) -> String {
  "{\"number\":\"" +
  python_decimal(amount.number) +
  "\",\"currency\":\"" +
  query_json_escape(amount.currency) +
  "\"}"
}

///|
fn query_cost_json(cost : @ast.Cost?) -> String {
  match cost {
    Some(c) =>
      "{ \"number\": \"" +
      python_decimal(c.number) +
      "\", \"currency\": \"" +
      query_json_escape(c.currency) +
      "\", \"date\":" +
      (match c.date {
        Some(d) => "\"" + d.to_string() + "\""
        None => "null"
      }) +
      ", \"label\":" +
      (match c.label {
        Some(l) => "\"" + query_json_escape(l) + "\""
        None => "null"
      }) +
      " }"
    None => "null"
  }
}

///|
fn query_position_json(position : @booking.Position) -> String {
  "{\"units\":" +
  query_amount_json(position.units) +
  ",\"cost\":" +
  query_cost_json(position.cost) +
  "}"
}

///|
pub fn QueryValue::to_json(self : QueryValue) -> String {
  match self {
    QInterval(_, _) => nested_json(self)
    QInt(n) => n.to_string()
    QDict(_) | QList(_) | QRecord(_, _) | QEnum(_, _) | QPythonType(_) =>
      nested_json(self)
    QSet(values) | QFrozenSet(values) =>
      "[" +
      values.map(fn(s) { "\"" + query_json_escape(s) + "\"" }).join(",") +
      "]"
    QText(text) => "\"" + query_json_escape(text) + "\""
    QNegativeZero(scale) =>
      "-" + python_decimal(@decimal.Decimal::zero_scaled(scale))
    QNumber(number) => python_decimal(number)
    QDate(date) => "\"" + date.to_string() + "\""
    QAmount(amount) => query_amount_json(amount)
    QPosition(position) => query_position_json(position)
    QInventory(inventory) => {
      let sb = StringBuilder()
      sb.write_string("[")
      let mut first = true
      for position in inventory.positions {
        if !first {
          sb.write_string(",")
        }
        sb.write_string(query_position_json(position))
        first = false
      }
      sb.write_string("]")
      sb.to_string()
    }
    QBool(b) => if b { "true" } else { "false" }
    QNull => "null"
  }
}

///|
fn value_number(value : QueryValue) -> @decimal.Decimal? {
  match value {
    QBool(b) => Some(@decimal.Decimal::from_int(if b { 1 } else { 0 }))
    QInt(n) => Some(@decimal.Decimal::new(n, 0))
    QNegativeZero(scale) => Some(@decimal.Decimal::zero_scaled(scale))
    QNumber(number) => Some(number)
    QAmount(amount) => Some(amount.number)
    QPosition(position) => Some(position.units.number)
    _ => None
  }
}

///|
fn value_compare(a : QueryValue, b : QueryValue) -> Int32? {
  match (a, b) {
    (QNull, QNull) => Some(0)
    (QNull, _) => None
    (_, QNull) => None
    (QDate(da), QDate(db)) => Some(da.compare(db))
    (QText(ta), QText(tb)) => Some(python_text_compare(ta, tb))
    (QAmount(aa), QAmount(ab)) =>
      if aa.currency == ab.currency {
        Some(aa.number.compare(ab.number))
      } else {
        None
      }
    (QPosition(pa), QPosition(pb)) =>
      value_compare(QAmount(pa.units), QAmount(pb.units))
    (QInventory(_), _) => None
    (_, QInventory(_)) => None
    _ =>
      match (value_number(a), value_number(b)) {
        (Some(na), Some(nb)) => Some(na.compare(nb))
        _ => None
      }
  }
}

///|
pub fn QueryResult::to_text(self : QueryResult) -> String {
  let sb = StringBuilder()
  match self.error {
    Some(message) => {
      sb.write_string("ERROR: ")
      sb.write_string(message)
      sb.write_string("\n")
      return sb.to_string()
    }
    None => ()
  }
  if self.kind is DirectiveText(text) {
    return text
  }
  let widths : Array[Int32] = []
  for column in self.columns {
    widths.push(column.length())
  }
  let rendered : Array[Array[String]] = []
  for row in self.rows {
    let cells : Array[String] = []
    for i in 0.. widths[i] {
        widths[i] = text.length()
      }
      cells.push(text)
    }
    rendered.push(cells)
  }
  for i in 0.. 0 {
      sb.write_string("  ")
    }
    sb.write_string(pad_right(self.columns[i], widths[i]))
  }
  sb.write_string("\n")
  for i in 0.. 0 {
      sb.write_string("  ")
    }
    let mut dashes = 0
    while dashes < widths[i] {
      sb.write_string("-")
      dashes = dashes + 1
    }
  }
  sb.write_string("\n")
  for cells in rendered {
    for i in 0.. 0 {
        sb.write_string("  ")
      }
      sb.write_string(pad_right(cells[i], widths[i]))
    }
    sb.write_string("\n")
  }
  sb.to_string()
}

///|
fn pad_right(text : String, width : Int32) -> String {
  if text.length() >= width {
    return text
  }
  let sb = StringBuilder()
  sb.write_string(text)
  let mut i = text.length()
  while i < width {
    sb.write_string(" ")
    i = i + 1
  }
  sb.to_string()
}

///|
pub fn QueryResult::to_json(self : QueryResult) -> String {
  let sb = StringBuilder()
  match self.error {
    Some(message) => return "{\"error\":\"" + query_json_escape(message) + "\"}"
    None => ()
  }
  if self.kind is DirectiveText(text) {
    sb.write_string(
      "{\"kind\":\"print\",\"text\":" + QText(text).to_json() + ",",
    )
  } else {
    sb.write_char('{')
  }
  sb.write_string("\"columns\":[")
  for i in 0.. 0 {
      sb.write_string(",")
    }
    sb.write_string("\"")
    sb.write_string(query_json_escape(self.columns[i]))
    sb.write_string("\"")
  }
  sb.write_string("],\"rows\":[")
  for r in 0.. 0 {
      sb.write_string(",")
    }
    sb.write_string("[")
    let row = self.rows[r]
    for i in 0.. 0 {
        sb.write_string(",")
      }
      sb.write_string(row[i].to_json())
    }
    sb.write_string("]")
  }
  sb.write_string("]}")
  sb.to_string()
}

///|
/// Matchable query failure; the original message remains available in each case.
pub(all) enum QueryError {
  Syntax(String)
  Compilation(String)
  Evaluation(String)
  Unsupported(String)
}

///|
/// Classifies parser failures without requiring hosts to match message text.
/// The legacy `error` field retains its original text for existing callers.
pub fn QueryResult::failure(self : QueryResult) -> QueryError? {
  match self.error {
    None => None
    Some(message) =>
      if message.has_prefix("compilation ") {
        Some(Compilation(message))
      } else if message.has_prefix("query.oracle_type_error") {
        Some(Evaluation(message))
      } else if message.has_prefix("unsupported ") ||
        message.has_prefix("unknown function ") ||
        message.has_prefix("unknown FROM source ") ||
        message.has_prefix("BALANCES AT supports ") {
        Some(Unsupported(message))
      } else {
        Some(Syntax(message))
      }
  }
}