// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// Render one bound value as a MySQL SQL literal (UTF-8 bytes). Strings are
/// single-quoted with every metacharacter backslash-escaped; blobs use the
/// `x'…'` hex form; NULL/booleans/numbers are their plain literals.
fn render_value(v : @moondb.Value, mode : QuoteMode) -> Bytes {
  match v {
    Null => b"NULL"
    Bool(true) => b"1"
    Bool(false) => b"0"
    Int(i) => string_to_bytes(i.to_string())
    Int64(i) => string_to_bytes(i.to_string())
    Double(d) => string_to_bytes(d.to_string())
    Text(s) => {
      let buf = Buffer()
      buf.write_byte(b'\'')
      escape_into(buf, string_to_bytes(s), mode)
      buf.write_byte(b'\'')
      buf.to_bytes()
    }
    Blob(bytes) => {
      let buf = Buffer()
      buf.write_byte(b'x')
      buf.write_byte(b'\'')
      let digits = "0123456789abcdef"
      for i in 0..> 4].to_int().to_byte())
        buf.write_byte(digits[c & 0xF].to_int().to_byte())
      }
      buf.write_byte(b'\'')
      buf.to_bytes()
    }
  }
}

///|
/// How the server reads a string literal, which decides how a value has to be
/// escaped. `NO_BACKSLASH_ESCAPES` is part of `ANSI` and of several stock
/// `sql_mode` combinations: there, a backslash is an ordinary character and the
/// only escape is a doubled quote — so a backslash-escaping writer leaves the
/// value's own quote live and the statement is injectable.
pub(all) enum QuoteMode {
  /// The default: backslash escapes are honoured.
  Backslash
  /// `NO_BACKSLASH_ESCAPES` is set: `''` is the only escape.
  DoubleQuote
} derive(Eq)

///|
/// Append `bytes` escaped for `mode`, so the value cannot end the literal it sits
/// in. Escaping happens on UTF-8 bytes and every escaped character is ASCII, so
/// multibyte text passes through untouched.
///
/// Under `DoubleQuote` a backslash is written as itself — doubling it there would
/// corrupt the value — and the control bytes stay raw, which a quoted literal
/// accepts.
fn escape_into(buf : Buffer, bytes : Bytes, mode : QuoteMode) -> Unit {
  if mode is DoubleQuote {
    for i in 0.. Bytes raise MysqlError {
  let src = string_to_bytes(sql)
  let n = src.length()
  let out = Buffer()
  let mut i = 0
  let mut pi = 0
  let mut in_single = false
  let mut in_double = false
  let mut in_back = false
  while i < n {
    let b = src[i].to_int()
    if in_single {
      out.write_byte(src[i])
      if b == 0x5C && i + 1 < n {
        i += 1
        out.write_byte(src[i])
      } else if b == 0x27 {
        in_single = false
      }
      i += 1
    } else if in_double {
      out.write_byte(src[i])
      if b == 0x5C && i + 1 < n {
        i += 1
        out.write_byte(src[i])
      } else if b == 0x22 {
        in_double = false
      }
      i += 1
    } else if in_back {
      out.write_byte(src[i])
      if b == 0x60 {
        in_back = false
      }
      i += 1
    } else if b == 0x27 {
      in_single = true
      out.write_byte(src[i])
      i += 1
    } else if b == 0x22 {
      in_double = true
      out.write_byte(src[i])
      i += 1
    } else if b == 0x60 {
      in_back = true
      out.write_byte(src[i])
      i += 1
    } else if b == 0x3F {
      if pi >= params.length() {
        raise ProtocolError(
          "too many ? placeholders for the " +
          params.length().to_string() +
          " parameter(s) supplied",
        )
      }
      out.write_bytes(render_value(params[pi], mode)[:])
      pi += 1
      i += 1
    } else {
      out.write_byte(src[i])
      i += 1
    }
  }
  if pi != params.length() {
    raise ProtocolError(
      "parameter count mismatch: bound " +
      pi.to_string() +
      " placeholder(s) but " +
      params.length().to_string() +
      " parameter(s) were supplied",
    )
  }
  out.to_bytes()
}