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

///|
/// Rewrite moondb's dialect-neutral `?` positional placeholders into
/// PostgreSQL's numbered `$1`, `$2`, … form. moondb fixes the *calling
/// convention* (an ordered params array) but leaves the placeholder spelling to
/// the driver; PostgreSQL's extended-query protocol requires `$n`, so a query
/// layer that emits `?` for portability is translated here before Parse.
///
/// A `?` is only a placeholder in SQL text — never inside a single-quoted string
/// literal, a dollar-quoted string, a `"`-quoted identifier, or a `--` / `/* */`
/// comment. Those spans are scanned through verbatim so a literal `?` in data or
/// a comment is left untouched and does not shift the parameter numbering. A
/// literal `?` an application genuinely needs in output can be written `??`,
/// which collapses to a single `?` (mirroring JDBC-style escaping).
pub fn translate_placeholders(sql : String) -> String {
  scan_placeholders(sql).0
}

///|
/// The single scan both `translate_placeholders` and `count_placeholders` use:
/// rewrite each `?` outside a quoted span or comment to `$n`, and report how many
/// were rewritten.
fn scan_placeholders(sql : String) -> (String, Int32) {
  let sb = StringBuilder()
  let n = sql.length()
  let mut i = 0
  let mut param = 0
  while i < n {
    let c = sql[i]
    if c == '\'' {
      // Single-quoted string literal: `''` is an escaped quote, and so is a
      // backslash escape in an E'' string or under standard_conforming_strings=off.
      // Missing the backslash form ends the literal early, after which a `?` in the
      // *data* is renumbered as a placeholder and the real one is left untranslated.
      sb.write_char('\'')
      i += 1
      while i < n {
        let d = sql[i]
        sb.write_char(d.unsafe_to_char())
        if d == '\\' && i + 1 < n {
          sb.write_char(sql[i + 1].unsafe_to_char())
          i += 2
          continue
        }
        if d == '\'' {
          if i + 1 < n && sql[i + 1] == '\'' {
            sb.write_char('\'')
            i += 2
            continue
          }
          i += 1
          break
        }
        i += 1
      }
    } else if c == '"' {
      // double-quoted identifier: "" is an escaped quote
      sb.write_char('"')
      i += 1
      while i < n {
        let d = sql[i]
        sb.write_char(d.unsafe_to_char())
        if d == '"' {
          if i + 1 < n && sql[i + 1] == '"' {
            sb.write_char('"')
            i += 2
            continue
          }
          i += 1
          break
        }
        i += 1
      }
    } else if c == '$' && i + 1 < n && is_dollar_tag_char(sql[i + 1], true) {
      // dollar-quoted string: $tag$ ... $tag$
      let tag_end = dollar_tag_end(sql, i)
      if tag_end < 0 {
        sb.write_char('$')
        i += 1
      } else {
        let tag = sql[i:tag_end + 1].to_owned()
        push_str(sb, tag)
        i = tag_end + 1
        // scan body until the matching closing tag
        let close = find_substring(sql, tag, i)
        if close < 0 {
          while i < n {
            sb.write_char(sql[i].unsafe_to_char())
            i += 1
          }
        } else {
          let body = sql[i:close + tag.length()].to_owned()
          push_str(sb, body)
          i = close + tag.length()
        }
      }
    } else if c == '-' && i + 1 < n && sql[i + 1] == '-' {
      // line comment to end of line
      while i < n && sql[i] != '\n' {
        sb.write_char(sql[i].unsafe_to_char())
        i += 1
      }
    } else if c == '/' && i + 1 < n && sql[i + 1] == '*' {
      // block comment (PostgreSQL nests them)
      sb.write_char('/')
      sb.write_char('*')
      i += 2
      let mut depth = 1
      while i < n && depth > 0 {
        if i + 1 < n && sql[i] == '/' && sql[i + 1] == '*' {
          depth += 1
          sb.write_char('/')
          sb.write_char('*')
          i += 2
        } else if i + 1 < n && sql[i] == '*' && sql[i + 1] == '/' {
          depth -= 1
          sb.write_char('*')
          sb.write_char('/')
          i += 2
        } else {
          sb.write_char(sql[i].unsafe_to_char())
          i += 1
        }
      }
    } else if c == '?' {
      if i + 1 < n && (sql[i + 1] == '|' || sql[i + 1] == '&') {
        // `?|` and `?&` are JSONB's "any"/"all key exists" operators. They are two
        // characters with no space between, so they are never a placeholder.
        sb.write_char('?')
        sb.write_char(sql[i + 1].unsafe_to_char())
        i += 2
      } else if i + 1 < n && sql[i + 1] == '?' {
        sb.write_char('?')
        i += 2
      } else if is_jsonb_exists_operator(sql, i) {
        // A bare `?` between two expressions is JSONB's "key exists" operator;
        // rewriting it to `$n` produces `data $1 'key'` and a syntax error.
        sb.write_char('?')
        i += 1
      } else {
        param += 1
        sb.write_char('$')
        push_str(sb, param.to_string())
        i += 1
      }
    } else {
      sb.write_char(c.unsafe_to_char())
      i += 1
    }
  }
  (sb.to_string(), param)
}

///|
/// The number of `?` placeholders [`translate_placeholders`] would consume — the
/// count of parameters a statement expects. Shares the same scanner discipline
/// so `??` escapes and quoted/comment spans do not count.
pub fn count_placeholders(sql : String) -> Int32 {
  // Counted during the scan, not by looking for `$n` in the output: an output `$9`
  // may have been copied through from a string literal or a comment, which made
  // this report a parameter count no statement ever had.
  scan_placeholders(sql).1
}

///|
/// Keywords after which a `?` introduces a value binding, not the JSONB `?`
/// operator. Without this, `SELECT ? FROM t` would read the `?` as sitting between
/// two operands (the trailing `T` of `SELECT` and the `F` of `FROM`) and leave it
/// untranslated.
let placeholder_intro_keywords : Array[String] = [
  "select", "where", "and", "or", "not", "in", "values", "set", "returning", "on",
  "using", "by", "limit", "offset", "between", "like", "ilike", "is", "then", "else",
  "when", "case", "having", "as", "distinct", "from", "into", "update", "insert",
  "delete", "all", "any", "some", "exists", "with", "over", "partition", "cast",
  "filter", "group", "order", "union", "except", "intersect", "lateral", "conflict",
  "do", "nothing", "null", "true", "false",
]

///|
/// The last index at or before `from` whose character is not ASCII whitespace,
/// or `-1` when the run is all whitespace / past the start.
fn prev_significant(sql : String, from : Int32) -> Int32 {
  let mut i = from
  while i >= 0 {
    let c = sql[i].to_int()
    if c != 32 && c != 9 && c != 10 && c != 13 {
      return i
    }
    i -= 1
  }
  -1
}

///|
/// The first index at or after `from` whose character is not ASCII whitespace,
/// or `-1` when the rest is all whitespace / past the end.
fn next_significant(sql : String, from : Int32) -> Int32 {
  let n = sql.length()
  let mut i = from
  while i < n {
    let c = sql[i].to_int()
    if c != 32 && c != 9 && c != 10 && c != 13 {
      return i
    }
    i += 1
  }
  -1
}

///|
/// Whether `ch` can appear in an unquoted SQL identifier or number.
fn is_identifier_char(ch : UInt16) -> Bool {
  let c = ch.to_int()
  (c >= 65 && c <= 90) ||
  (c >= 97 && c <= 122) ||
  (c >= 48 && c <= 57) ||
  c == 95 ||
  c == 36
}

///|
/// Whether `ch` can open the right operand of the JSONB `?` operator: a string
/// or identifier, a nested expression, an array constructor, or a number.
fn starts_operand(ch : UInt16) -> Bool {
  let c = ch.to_int()
  // A `?` here is the *next* placeholder (`data ? ?` binds the key), not the
  // two-character escape — `??` is consumed before this predicate is reached.
  c == 39 || c == 34 || c == 40 || c == 91 || c == 63 || is_identifier_char(ch)
}

///|
/// Whether `ch` can end the left operand of the JSONB `?` operator: an
/// identifier character, a closing paren/bracket, or a closing quote.
fn ends_expression(ch : UInt16) -> Bool {
  let c = ch.to_int()
  is_identifier_char(ch) || c == 41 || c == 93 || c == 39 || c == 34
}

///|
/// The identifier ending at `end` (inclusive), lowercased for keyword lookup.
fn word_ending_at(sql : String, end : Int32) -> String {
  let mut start = end
  while start > 0 && is_identifier_char(sql[start - 1]) {
    start -= 1
  }
  sql[start:end + 1].to_owned().to_lower()
}

///|
/// Whether the bare `?` at `at` is PostgreSQL's JSONB "key exists" operator. It
/// is one only when it sits *between* two expressions: an expression-ending token
/// on the left and an operand-starting token on the right, with the left token not
/// being a keyword that precedes a value. Every other bare `?` is a placeholder.
fn is_jsonb_exists_operator(sql : String, at : Int32) -> Bool {
  let next = next_significant(sql, at + 1)
  if next < 0 || !starts_operand(sql[next]) {
    return false
  }
  let prev = prev_significant(sql, at - 1)
  if prev < 0 || !ends_expression(sql[prev]) {
    return false
  }
  if is_identifier_char(sql[prev]) {
    return !placeholder_intro_keywords.contains(word_ending_at(sql, prev))
  }
  true
}

///|
fn push_str(sb : StringBuilder, s : String) -> Unit {
  for i in 0.. Bool {
  let ci = c.to_int()
  if ci == 36 {
    return true // '$' closes the tag
  }
  let is_alpha = (ci >= 65 && ci <= 90) || (ci >= 97 && ci <= 122) || ci == 95
  if first {
    is_alpha
  } else {
    is_alpha || (ci >= 48 && ci <= 57)
  }
}

///|
/// Index of the `$` that closes a dollar-quote tag beginning at `start`, or `-1`
/// if the run from `start` is not a valid opening tag.
fn dollar_tag_end(sql : String, start : Int32) -> Int32 {
  let n = sql.length()
  let mut i = start + 1
  while i < n {
    let c = sql[i]
    if c == '$' {
      return i
    }
    if !is_dollar_tag_char(c, false) {
      return -1
    }
    i += 1
  }
  -1
}

///|
/// First index ≥ `from` at which `needle` occurs in `hay`, or `-1`.
fn find_substring(hay : String, needle : String, from : Int32) -> Int32 {
  let hn = hay.length()
  let nn = needle.length()
  if nn == 0 {
    return from
  }
  let mut i = from
  while i + nn <= hn {
    let mut j = 0
    while j < nn && hay[i + j] == needle[j] {
      j += 1
    }
    if j == nn {
      return i
    }
    i += 1
  }
  -1
}