// Core machinery of the SQL generator (port of sqlglot/generator.py `Generator`).
//
// Dispatch (mirrors Python `_build_dispatch`):
//   `sql(e)` -> `fns.transforms[kind]` (exact kind) -> `fns.methods[kind]` (dialect
//   `_sql` overrides) -> `base_dispatch` (base `_sql` methods) -> `Func`
//   fallback -> `Property` fallback -> error.
//
// Conventions for dialect overrides:
//   * `Generator::_sql` is always the BASE implementation (call it as `super()`).
//   * A Python direct call `self._sql(e)` (virtual) is `self.call_method(Kind, e)`.
//   * `_sql` methods with extra parameters have a typed hook in `GeneratorHooks` and a
//     virtual wrapper `Generator::_sql_v(...)`.
//   * Overridable helper methods (e.g. `column_parts`) have a typed hook, a virtual
//     wrapper `Generator::` and the base implementation `Generator::_base`.

///|
pub type GenFn = (Generator, Expr) -> String raise SqlglotError

///|
/// Result of `locate_properties`: properties grouped by their location.
pub type PropLocs = Map[PropertiesLocation, Array[Expr]]

///|
/// Typed hooks for Generator methods that dialects override and that are called
/// virtually from base code (or have extra parameters).
pub(all) struct GeneratorHooks {
  // Helper methods: virtual wrapper `Generator::`, base `Generator::_base`.
  mut add_column_sql : GenFn?
  mut after_limit_modifiers : ((Generator, Expr) -> Array[String] raise SqlglotError)?
  mut column_parts : GenFn?
  mut createable_sql : ((Generator, Expr, PropLocs) -> String raise SqlglotError)?
  mut format_time : ((Generator, Expr, Map[String, String]?, Trie?) -> String? raise SqlglotError)?
  mut generate : ((Generator, Expr, Bool) -> String raise SqlglotError)?
  mut in_unnest_op : GenFn?
  mut lateral_op : GenFn?
  mut locate_properties : ((Generator, Expr) -> PropLocs raise SqlglotError)?
  mut offset_limit_modifiers : ((Generator, Expr, Bool, Expr?) -> Array[String] raise SqlglotError)?
  mut options_modifier : GenFn?
  mut scope_resolution : ((Generator, String, String) -> String raise SqlglotError)?
  mut set_operations : GenFn?
  mut table_parts : GenFn?
  mut with_properties : GenFn?
  mut preprocess : GenExprFn?
  // `_sql` methods with extra parameters: virtual wrapper `Generator::_v`,
  // base `Generator::`. These hooks are also used by `sql()` dispatch.
  mut alterrename_sql : ((Generator, Expr, Bool) -> String raise SqlglotError)?
  mut arrayinsert_sql : ((Generator, Expr, Int) -> String raise SqlglotError)?
  mut cast_sql : ((Generator, Expr, String?) -> String raise SqlglotError)?
  mut chr_sql : ((Generator, Expr, String) -> String raise SqlglotError)?
  mut columndef_sql : ((Generator, Expr, String) -> String raise SqlglotError)?
  mut hexstring_sql : ((Generator, Expr, String?) -> String raise SqlglotError)?
  mut lambda_sql : ((Generator, Expr, String, Bool) -> String raise SqlglotError)?
  mut table_sql : ((Generator, Expr, String) -> String raise SqlglotError)?
  mut tablesample_sql : ((Generator, Expr, String?) -> String raise SqlglotError)?
  mut uniquekeyproperty_sql : ((Generator, Expr, String) -> String raise SqlglotError)?
  mut values_sql : ((Generator, Expr, Bool) -> String raise SqlglotError)?
  // Callable class attributes.
  /// `UNICODE_SUBSTITUTE`: receives the escaped code (regex group 1).
  mut unicode_substitute : ((String) -> String)?
  /// `SAFE_JSON_PATH_KEY_RE.match`; `None` means `SAFE_IDENTIFIER_RE`.
  mut safe_json_path_key_re : ((String) -> Bool)?
}

///|
pub type GenExprFn = (Generator, Expr) -> Expr raise SqlglotError

///|
pub fn GeneratorHooks::new() -> GeneratorHooks {
  {
    add_column_sql: None,
    after_limit_modifiers: None,
    column_parts: None,
    createable_sql: None,
    format_time: None,
    generate: None,
    in_unnest_op: None,
    lateral_op: None,
    locate_properties: None,
    offset_limit_modifiers: None,
    options_modifier: None,
    scope_resolution: None,
    set_operations: None,
    table_parts: None,
    with_properties: None,
    preprocess: None,
    alterrename_sql: None,
    arrayinsert_sql: None,
    cast_sql: None,
    chr_sql: None,
    columndef_sql: None,
    hexstring_sql: None,
    lambda_sql: None,
    table_sql: None,
    tablesample_sql: None,
    uniquekeyproperty_sql: None,
    values_sql: None,
    unicode_substitute: None,
    safe_json_path_key_re: None,
  }
}

///|
pub fn GeneratorHooks::copy(self : GeneratorHooks) -> GeneratorHooks {
  { ..self, }
}

///|
/// Callable class attributes of the Python `Generator` plus method override hooks.
pub(all) struct GeneratorFns {
  /// `TRANSFORMS`: exact kind -> generator function. Takes precedence over methods
  /// (mirrors Python `_build_dispatch`).
  mut transforms : Map[Kind, GenFn]
  /// Dialect `_sql` method overrides. Used by `sql()` dispatch after
  /// `transforms`, and by virtual calls `Generator::call_method`.
  mut methods : Map[Kind, GenFn]
  /// Typed overrides of helper methods / methods with extra parameters.
  mut hooks : GeneratorHooks
  /// `TYPE_PARAM_SETTINGS`: dtype -> (defaults, bounds)
  mut type_param_settings : Map[DType, (Array[Int], Array[Int?])]
  /// `AFTER_HAVING_MODIFIER_TRANSFORMS`
  mut after_having_modifier_transforms : Array[(String, GenFn)]
}

///|
pub fn GeneratorFns::copy(self : GeneratorFns) -> GeneratorFns {
  {
    transforms: Map::from_iter(self.transforms.iter()),
    methods: Map::from_iter(self.methods.iter()),
    hooks: self.hooks.copy(),
    type_param_settings: Map::from_iter(self.type_param_settings.iter()),
    after_having_modifier_transforms: self.after_having_modifier_transforms.copy(),
  }
}

///|
/// Converts a given syntax tree to the corresponding SQL string.
pub(all) struct Generator {
  dialect : Dialect
  cfg : GeneratorConfig
  fns : GeneratorFns
  pretty : Bool
  /// Python `identify`: "" (False), "always" (True), "safe" or "unsafe".
  mut identify : String
  normalize : Bool
  pad : Int
  indent_size : Int
  /// Python `normalize_functions`: "upper", "lower" or "" (False).
  normalize_functions : String
  unsupported_level : ErrorLevel
  max_unsupported : Int
  leading_comma : Bool
  max_text_width : Int
  comments : Bool
  mut unsupported_messages : Array[String]
  escaped_quote_end : String
  escaped_byte_quote_end : String
  escaped_identifier_end : String
  next_name : () -> String
  identifier_start : String
  identifier_end : String
  mut quote_json_path_key_using_brackets : Bool
}

///|
/// Python's module-level `sqlglot.pretty`: the default for generators created without
/// an explicit `pretty` option.
pub let default_pretty : Ref[Bool] = Ref(false)

///|
pub fn Generator::new(
  dialect : Dialect,
  pretty? : Bool,
  identify? : String = "",
  normalize? : Bool = false,
  pad? : Int = 2,
  indent? : Int = 2,
  normalize_functions? : String,
  unsupported_level? : ErrorLevel = Warn,
  max_unsupported? : Int = 3,
  leading_comma? : Bool = false,
  max_text_width? : Int = 80,
  comments? : Bool = true,
) -> Generator {
  let normalize_functions = match normalize_functions {
    Some(n) => n
    None =>
      match dialect.cfg.normalize_functions {
        S(s) => s
        B(true) => "upper"
        B(false) => ""
      }
  }
  let pretty = match pretty {
    Some(p) => p
    None => default_pretty.val
  }
  let identify = match identify {
    "true" | "True" => "always"
    "false" | "False" => ""
    s => s
  }
  let string_escapes = dialect.tokenizer.string_escapes
  let first_escape = if string_escapes.is_empty() {
    ""
  } else {
    string_escapes[0]
  }
  {
    dialect,
    cfg: dialect.gen_cfg,
    fns: dialect.gen_fns,
    pretty,
    identify,
    normalize,
    pad,
    indent_size: indent,
    normalize_functions,
    unsupported_level,
    max_unsupported,
    leading_comma,
    max_text_width,
    comments,
    unsupported_messages: [],
    escaped_quote_end: first_escape + dialect.cfg.quote_end,
    escaped_byte_quote_end: match dialect.cfg.byte_end {
      Some(e) if e != "" => first_escape + e
      _ => ""
    },
    escaped_identifier_end: dialect.cfg.identifier_end +
    dialect.cfg.identifier_end,
    next_name: name_sequence("_t"),
    identifier_start: dialect.cfg.identifier_start,
    identifier_end: dialect.cfg.identifier_end,
    quote_json_path_key_using_brackets: true,
  }
}

///|
/// Generates SQL for `expression` with the base dialect and default settings
/// (Python `str(expression)` / `expression.sql()`).
pub fn expr_to_sql(expression : Expr) -> String raise SqlglotError {
  Generator::new(base_dialect()).generate(expression)
}

///|
/// Generates the SQL string corresponding to the given syntax tree (virtual).
pub fn Generator::generate(
  self : Generator,
  expression : Expr,
  copy? : Bool = true,
) -> String raise SqlglotError {
  match self.fns.hooks.generate {
    Some(f) => f(self, expression, copy)
    None => self.generate_base(expression, copy~)
  }
}

///|
pub fn Generator::generate_base(
  self : Generator,
  expression : Expr,
  copy? : Bool = true,
) -> String raise SqlglotError {
  let expression = if copy { expression.copy() } else { expression }
  let expression = self.preprocess(expression)
  self.unsupported_messages = []
  let mut sql = py_strip(self.sql(Some(expression)))
  if self.pretty {
    sql = sql.replace_all(old=self.cfg.sentinel_line_break, new="\n")
  }
  match self.unsupported_level {
    Ignore => return sql
    Warn =>
      for msg in self.unsupported_messages {
        log_warning(msg)
      }
    Raise =>
      if !self.unsupported_messages.is_empty() {
        raise UnsupportedError(
          concat_messages(self.unsupported_messages, self.max_unsupported),
        )
      }
    Immediate => ()
  }
  sql
}

///|
/// Apply generic preprocessing transformations to a given expression.
pub fn Generator::preprocess(
  self : Generator,
  expression : Expr,
) -> Expr raise SqlglotError {
  match self.fns.hooks.preprocess {
    Some(f) => f(self, expression)
    None => self.preprocess_base(expression)
  }
}

///|
pub fn Generator::preprocess_base(
  self : Generator,
  expression : Expr,
) -> Expr raise SqlglotError {
  let mut expression = self.move_ctes_to_top_level(expression)
  if self.cfg.ensure_bools {
    expression = ensure_bools(expression)
  }
  expression
}

///|
/// Python `_move_ctes_to_top_level`.
pub fn Generator::move_ctes_to_top_level(
  self : Generator,
  expression : Expr,
) -> Expr {
  if expression.parent is None &&
    self.cfg.expressions_without_nested_ctes.contains(expression.kind) &&
    expression
    .find_all([With])
    .any(node => {
      match node.parent {
        Some(p) => !physical_equal(p, expression)
        None => true
      }
    }) {
    move_ctes_to_top_level(expression)
  } else {
    expression
  }
}

///|
pub fn Generator::unsupported(
  self : Generator,
  message : String,
) -> Unit raise SqlglotError {
  if self.unsupported_level == Immediate {
    raise UnsupportedError(message)
  }
  self.unsupported_messages.push(message)
}

///|
pub fn Generator::sep(self : Generator, sep? : String = " ") -> String {
  if self.pretty {
    py_strip(sep) + "\n"
  } else {
    sep
  }
}

///|
pub fn Generator::seg(
  self : Generator,
  sql : String,
  sep? : String = " ",
) -> String {
  self.sep(sep~) + sql
}

///|
pub fn Generator::sanitize_comment(
  self : Generator,
  comment : String,
) -> String {
  ignore(self)
  let mut comment = comment
  match g_first_char(comment) {
    Some(c) if !is_space(c) => comment = " " + comment
    _ => ()
  }
  match g_last_char(comment) {
    Some(c) if !is_space(c) => comment = comment + " "
    _ => ()
  }
  comment.replace_all(old="*/", new="* /").replace_all(old="/*", new="/ *")
}

///|
pub fn Generator::maybe_comment(
  self : Generator,
  sql : String,
  expression? : Expr,
  comments? : Array[String],
  separated? : Bool = false,
) -> String {
  let comments = if self.comments {
    match comments {
      Some(c) => Some(c)
      None =>
        match expression {
          Some(e) => e.comments
          None => None
        }
    }
  } else {
    None
  }
  let comments = match comments {
    Some(c) if !c.is_empty() => c
    _ => return sql
  }
  match expression {
    Some(e) => if e.kind.is_any(self.cfg.exclude_comments) { return sql }
    None => ()
  }
  let comments_list = []
  for comment in comments {
    if comment != "" {
      comments_list.push(
        "/*" + self.replace_line_breaks(self.sanitize_comment(comment)) + "*/",
      )
    }
  }
  if comments_list.is_empty() {
    return sql
  }
  let sep_comments = separated ||
    (match expression {
      Some(e) => e.kind.is_any(self.cfg.with_separated_comments)
      None => false
    })
  if sep_comments {
    let comments_sql = comments_list.join(self.sep())
    match g_first_char(sql) {
      None => self.sep() + comments_sql + sql
      Some(c) if is_space(c) => self.sep() + comments_sql + sql
      _ => comments_sql + self.sep() + sql
    }
  } else {
    sql + " " + comments_list.join(" ")
  }
}

///|
/// `wrap(expression)` for an expression.
pub fn Generator::wrap(
  self : Generator,
  expression : Expr,
) -> String raise SqlglotError {
  let this_sql = if expression.kind.is_any([Select, SetOperation]) {
    self.sql(Some(expression))
  } else {
    self.sql_key(expression, "this")
  }
  self.wrap_sql(this_sql)
}

///|
/// `wrap(sql)` for an already generated SQL string.
pub fn Generator::wrap_str(self : Generator, sql : String) -> String {
  self.wrap_sql(sql)
}

///|
pub fn Generator::wrap_sql(self : Generator, this_sql : String) -> String {
  if this_sql == "" {
    return "()"
  }
  let this_sql = self.indent(this_sql, level=1, pad=0)
  "(" + self.sep(sep="") + this_sql + self.seg(")", sep="")
}

///|
/// Runs `f` with `identify` disabled.
pub fn[T] Generator::no_identify(
  self : Generator,
  f : () -> T raise SqlglotError,
) -> T raise SqlglotError {
  let original = self.identify
  self.identify = ""
  let result = f()
  self.identify = original
  result
}

///|
pub fn Generator::normalize_func(self : Generator, name : String) -> String {
  if self.normalize_functions == "upper" {
    py_upper(name)
  } else if self.normalize_functions == "lower" {
    py_lower(name)
  } else {
    name
  }
}

///|
pub fn Generator::indent(
  self : Generator,
  sql : String,
  level? : Int = 0,
  pad? : Int,
  skip_first? : Bool = false,
  skip_last? : Bool = false,
) -> String {
  if !self.pretty || sql == "" {
    return sql
  }
  let pad = match pad {
    Some(p) => p
    None => self.pad
  }
  let lines = py_split(sql, "\n")
  let n = lines.length()
  let prefix = " ".repeat(level * self.indent_size + pad)
  let out = []
  for i, line in lines {
    if (skip_first && i == 0) || (skip_last && i == n - 1) {
      out.push(line)
    } else {
      out.push(prefix + line)
    }
  }
  out.join("\n")
}

///|
/// Generates SQL for an expression (Python `self.sql(expression)`).
pub fn Generator::sql(
  self : Generator,
  expression : Expr?,
  comment? : Bool = true,
) -> String raise SqlglotError {
  guard expression is Some(e) else { return "" }
  let kind = e.kind
  let sql = match self.fns.transforms.get(kind) {
    Some(f) => f(self, e)
    None =>
      match self.fns.methods.get(kind) {
        Some(f) => f(self, e)
        None => self.default_sql(kind, e)
      }
  }
  if self.comments && comment {
    self.maybe_comment(sql, expression=e)
  } else {
    sql
  }
}

///|
/// The base generation of `e` (no dialect transform or method override applies).
fn Generator::default_sql(
  self : Generator,
  kind : Kind,
  e : Expr,
) -> String raise SqlglotError {
  if is_unary_chain_kind(kind) {
    match self.unary_chain_sql(e) {
      Some(s) => return s
      None => ()
    }
  }
  match self.base_dispatch(kind, e) {
    Some(s) => s
    None =>
      if kind.is_a(Func) {
        self.function_fallback_sql(e)
      } else if kind.is_a(Property) {
        self.call_method(Property, e)
      } else {
        raise ValueError("Unsupported expression type \{kind.name()}")
      }
  }
}

///|
fn is_unary_chain_kind(kind : Kind) -> Bool {
  kind is (Neg | Not | Paren | BitwiseNot)
}

///|
/// Chains shorter than this are generated recursively.
let unary_chain_min_length : Int = 64

///|
/// Generates a long chain of NOT / unary minus / ~ / parentheses (e.g. `NOT NOT ... x`
/// or `((((x))))`) without one level of recursion per link: the innermost operand is
/// generated, then each link's base `*_sql` formatting (and comment) is applied from the
/// inside out, producing exactly what the recursive calls produce. Links whose
/// generation a dialect overrides end the chain. Returns `None` for short chains.
fn Generator::unary_chain_sql(
  self : Generator,
  e : Expr,
) -> String? raise SqlglotError {
  let chain = [e]
  let mut cur = e
  for ;; {
    match cur.args.get("this") {
      Some(Node(c)) if is_unary_chain_kind(c.kind) &&
        self.fns.transforms.get(c.kind) is None &&
        self.fns.methods.get(c.kind) is None => {
        chain.push(c)
        cur = c
      }
      _ => break
    }
  }
  if chain.length() < unary_chain_min_length {
    return None
  }
  // The text is `prefixes (outermost last) + inner + suffixes (innermost first)`, kept
  // apart so that each link costs O(1) instead of copying the text built so far;
  // pretty parentheses (which re-indent their contents) and comments materialize it.
  let mut inner = self.sql_key(cur, "this")
  let prefixes : Array[String] = []
  let suffixes : Array[String] = []
  let materialize = () => {
    if prefixes.is_empty() && suffixes.is_empty() {
      return inner
    }
    let sb = StringBuilder()
    for i = prefixes.length() - 1; i >= 0; i = i - 1 {
      sb.write_string(prefixes[i])
    }
    sb.write_string(inner)
    for x in suffixes {
      sb.write_string(x)
    }
    prefixes.clear()
    suffixes.clear()
    sb.to_string()
  }
  for i = chain.length() - 1; i >= 0; i = i - 1 {
    let node = chain[i]
    match node.kind {
      Neg => {
        let starts_with_minus = match prefixes.last() {
          Some(p) => p.has_prefix("-")
          None => inner.has_prefix("-")
        }
        prefixes.push(if starts_with_minus { "- " } else { "-" })
      }
      Not => prefixes.push("NOT ")
      BitwiseNot => prefixes.push("~")
      _ =>
        if self.pretty {
          inner = "(" +
            self.seg(self.indent(materialize()), sep="") +
            self.seg(")", sep="")
        } else {
          prefixes.push("(")
          suffixes.push(")")
        }
    }
    // the inner links are generated through `sql_key`, i.e. with their comments
    if i > 0 &&
      self.comments &&
      (match node.comments {
        Some(c) => !c.is_empty()
        None => false
      }) {
      inner = self.maybe_comment(materialize(), expression=node)
    }
  }
  Some(materialize())
}

///|
/// Python `self.sql(expression, key)`.
pub fn Generator::sql_key(
  self : Generator,
  expression : Expr,
  key : String,
) -> String raise SqlglotError {
  match expression.args.get(key) {
    Some(v) if v.truthy() => self.sql_value(v)
    _ => ""
  }
}

///|
/// Python `self.sql(value)` for an argument value.
pub fn Generator::sql_value(
  self : Generator,
  value : Value,
  comment? : Bool = true,
) -> String raise SqlglotError {
  match value {
    Node(e) => self.sql(Some(e), comment~)
    Str(s) => s
    List(l) =>
      if l.is_empty() {
        ""
      } else {
        raise ValueError("Unsupported expression type list")
      }
    Bool(b) =>
      if b {
        raise ValueError("Unsupported expression type bool")
      } else {
        ""
      }
    Int(i) =>
      if i == 0 {
        ""
      } else {
        raise ValueError("Unsupported expression type int")
      }
    DT(d) => raise ValueError("Unsupported expression type \{d}")
  }
}

///|
/// Virtual call of a `_sql` method: a dialect override if present, else the
/// base implementation (Python `self._sql(expression)`).
pub fn Generator::call_method(
  self : Generator,
  kind : Kind,
  expression : Expr,
) -> String raise SqlglotError {
  match self.fns.methods.get(kind) {
    Some(f) => f(self, expression)
    None =>
      match self.base_dispatch(kind, expression) {
        Some(s) => s
        None =>
          if kind == Property || kind.is_a(Property) {
            self.property_sql(expression)
          } else {
            raise ValueError("No generator method for \{kind.name()}")
          }
      }
  }
}

///|
/// An argument of `func` / `format_args`: an expression, a SQL string or `None`.
pub trait SqlArg {
  fn arg_sql(Self, Generator) -> String? raise SqlglotError
}

///|
pub impl SqlArg for Expr with fn arg_sql(self, g) {
  Some(g.sql(Some(self)))
}

///|
pub impl SqlArg for String with fn arg_sql(self, _g) {
  Some(self)
}

///|
pub impl SqlArg for Value with fn arg_sql(self, g) {
  match self {
    Bool(_) => None
    v => Some(g.sql_value(v))
  }
}

///|
pub impl[T : SqlArg] SqlArg for T? with fn arg_sql(self, g) {
  match self {
    Some(x) => x.arg_sql(g)
    None => None
  }
}

///|
pub fn Generator::func(
  self : Generator,
  name : String,
  args : Array[&SqlArg],
  prefix? : String = "(",
  suffix? : String = ")",
  normalize? : Bool = true,
) -> String raise SqlglotError {
  let name = if normalize { self.normalize_func(name) } else { name }
  name + prefix + self.format_args(args) + suffix
}

///|
pub fn Generator::format_args(
  self : Generator,
  args : Array[&SqlArg],
  sep? : String = ", ",
) -> String raise SqlglotError {
  let arg_sqls = []
  for arg in args {
    match arg.arg_sql(self) {
      Some(s) => arg_sqls.push(s)
      None => ()
    }
  }
  if self.pretty && self.too_wide(arg_sqls) {
    return self.indent(
      "\n" + arg_sqls.join(py_strip(sep) + "\n") + "\n",
      skip_first=true,
      skip_last=true,
    )
  }
  arg_sqls.join(sep)
}

///|
pub fn Generator::too_wide(self : Generator, args : Array[String]) -> Bool {
  let mut total = 0
  for a in args {
    total += py_len(a)
  }
  total > self.max_text_width
}

///|
/// Virtual `format_time`.
pub fn Generator::format_time(
  self : Generator,
  expression : Expr,
  inverse_time_mapping? : Map[String, String],
  inverse_time_trie? : Trie,
) -> String? raise SqlglotError {
  match self.fns.hooks.format_time {
    Some(f) => f(self, expression, inverse_time_mapping, inverse_time_trie)
    None =>
      self.format_time_base(
        expression,
        inverse_time_mapping?,
        inverse_time_trie?,
      )
  }
}

///|
pub fn Generator::format_time_base(
  self : Generator,
  expression : Expr,
  inverse_time_mapping? : Map[String, String],
  inverse_time_trie? : Trie,
) -> String? raise SqlglotError {
  let mapping = match inverse_time_mapping {
    Some(m) if !m.is_empty() => m
    _ => self.dialect.cfg.inverse_time_mapping
  }
  let trie = match inverse_time_trie {
    Some(t) if !t.children.is_empty() || t.terminal => t
    _ => self.dialect.inverse_time_trie
  }
  format_time_opt(self.sql_key(expression, "format"), mapping, trie~)
}

///|
/// Python `self.expressions(...)`.
pub fn Generator::expressions(
  self : Generator,
  expression? : Expr,
  key? : String,
  sqls? : Array[Value],
  flat? : Bool = false,
  indent? : Bool = true,
  skip_first? : Bool = false,
  skip_last? : Bool = false,
  sep? : String = ", ",
  prefix? : String = "",
  dynamic? : Bool = false,
  new_line? : Bool = false,
) -> String raise SqlglotError {
  let items : Array[Value] = match expression {
    Some(e) => {
      let k = match key {
        Some(k) if k != "" => k
        _ => "expressions"
      }
      match e.args.get(k) {
        Some(List(l)) => l
        Some(v) if v.truthy() => [v]
        _ => []
      }
    }
    None =>
      match sqls {
        Some(s) => s
        None => []
      }
  }
  if items.is_empty() {
    return ""
  }
  if flat {
    let out = []
    for v in items {
      let s = self.sql_value(v)
      if s != "" {
        out.push(s)
      }
    }
    return out.join(sep)
  }
  let num_sqls = items.length()
  let result_sqls : Array[String] = []
  for i, v in items {
    let sql = self.sql_value(v, comment=false)
    if sql == "" {
      continue
    }
    let comments = match v {
      Node(e) => self.maybe_comment("", expression=e)
      _ => ""
    }
    if self.pretty {
      if self.leading_comma {
        result_sqls.push(
          (if i > 0 { sep } else { "" }) + prefix + sql + comments,
        )
      } else {
        let tail = if i + 1 < num_sqls {
          if comments != "" {
            py_rstrip(sep)
          } else {
            sep
          }
        } else {
          ""
        }
        result_sqls.push(prefix + sql + tail + comments)
      }
    } else {
      result_sqls.push(
        prefix + sql + comments + (if i + 1 < num_sqls { sep } else { "" }),
      )
    }
  }
  let result_sql = if self.pretty && (!dynamic || self.too_wide(result_sqls)) {
    if new_line {
      result_sqls.insert(0, "")
      result_sqls.push("")
    }
    result_sqls.map(s => py_rstrip(s)).join("\n")
  } else {
    result_sqls.join("")
  }
  if indent {
    self.indent(result_sql, skip_first~, skip_last~)
  } else {
    result_sql
  }
}

///|
pub fn Generator::op_expressions(
  self : Generator,
  op : String,
  expression : Expr,
  flat? : Bool = false,
) -> String raise SqlglotError {
  let flat = flat ||
    (match expression.parent {
      Some(p) => p.kind.is_a(Properties)
      None => false
    })
  let expressions_sql = self.expressions(expression~, flat~)
  if flat {
    return op + " " + expressions_sql
  }
  self.seg(op) +
  (if expressions_sql != "" { self.sep() } else { "" }) +
  expressions_sql
}

///|
priv enum BinItem {
  BNode(Expr?)
  BStr(String)
}

///|
/// An operand of a binary node; Python's `self.sql(str)` returns raw strings as-is.
fn bin_item(node : Expr, key : String) -> BinItem {
  match node.args.get(key) {
    Some(Str(s)) => BStr(s)
    _ => BNode(node.arg(key))
  }
}

///|
pub fn Generator::binary(
  self : Generator,
  expression : Expr,
  op : String,
) -> String raise SqlglotError {
  let sqls : Array[String] = []
  let stack : Array[BinItem] = [BNode(Some(expression))]
  let binary_type = expression.kind
  let mut op = op
  while stack.pop() is Some(item) {
    match item {
      BNode(Some(node)) if node.kind == binary_type => {
        match node.args.get("operator") {
          Some(op_func) if op_func.truthy() =>
            op = "OPERATOR(" + self.sql_value(op_func) + ")"
          _ => ()
        }
        stack.push(bin_item(node, "expression"))
        stack.push(
          BStr(" " + self.maybe_comment(op, comments?=node.comments) + " "),
        )
        stack.push(bin_item(node, "this"))
      }
      BNode(n) => sqls.push(self.sql(n))
      BStr(s) => sqls.push(s)
    }
  }
  sqls.join("")
}

///|
pub fn Generator::connector_sql(
  self : Generator,
  expression : Expr,
  op : String,
) -> String raise SqlglotError {
  ignore(op)
  let stack : Array[BinItem] = [BNode(Some(expression))]
  let sqls : Array[String] = []
  let ops : @set.Set[String] = @set.Set([])
  while stack.pop() is Some(item) {
    match item {
      BNode(Some(node)) if node.kind.is_a(Connector) => {
        let mut node_op = match node.kind {
          And => "AND"
          Or => "OR"
          Xor => "XOR"
          k => py_upper(k.key())
        }
        stack.push(BNode(node.arg("expression")))
        match node.comments {
          Some(c) if !c.is_empty() && self.comments =>
            node_op = self.maybe_comment(node_op, comments=c)
          _ => ()
        }
        stack.push(BStr(node_op))
        stack.push(BNode(node.arg("this")))
        ops.add(node_op)
      }
      other => {
        let sql = match other {
          BNode(n) => self.sql(n)
          BStr(s) => s
        }
        let n = sqls.length()
        if n > 0 && ops.contains(sqls[n - 1]) {
          sqls[n - 1] = sqls[n - 1] + " " + sql
        } else {
          sqls.push(sql)
        }
      }
    }
  }
  let sep = if self.pretty && self.too_wide(sqls) { "\n" } else { " " }
  sqls.join(sep)
}

///|
/// Python `_replace_line_breaks`.
pub fn Generator::replace_line_breaks(
  self : Generator,
  string : String,
) -> String {
  if self.pretty {
    string.replace_all(old="\n", new=self.cfg.sentinel_line_break)
  } else {
    string
  }
}