// Parser builder helpers of sqlglot/dialects/dialect.py (`FUNCTIONS` entries).
//
// Conventions:
//   * Python `def f(args)` -> `pub fn f(args : Array[Expr]) -> Expr` (wrap it as
//     `fn(args, _p) { f(args) }` to use it as a `FuncBuilder`).
//   * Python `def f(args, dialect)` -> `pub fn f(args : Array[Expr], dialect : Dialect)`.
//   * Python factories returning builders return a `FuncBuilder` (whose `Parser` argument
//     provides the dialect).

///|
/// Python `build_formatted_time(exp_class, dialect_override, default)(args, dialect)`.
/// `default=true` uses the dialect's TIME_FORMAT; `default_format` is a string default.
pub fn build_formatted_time_expr(
  kind : Kind,
  args : Array[Expr],
  dialect : Dialect,
  dialect_override? : String,
  default? : Bool = false,
  default_format? : String,
) -> Expr raise SqlglotError {
  let target_dialect = match dialect_override {
    Some(name) => Dialect::get_or_raise(name)
    None => dialect
  }
  // Python passes the raw (quoted) TIME_FORMAT / default string to `format_time`.
  let format = match arg(args, 1) {
    Some(f) => target_dialect.format_time(Some(f))
    None =>
      if default {
        Some(target_dialect.format_time_str(target_dialect.cfg.time_format))
      } else {
        match default_format {
          Some(s) if s != "" => Some(target_dialect.format_time_str(s))
          _ => None
        }
      }
  }
  mk(kind, [("this", arg(args, 0)), ("format", format)])
}

///|
/// Python `build_formatted_time(exp_class, dialect_override=None, default=None)`.
pub fn build_formatted_time(
  kind : Kind,
  dialect_override? : String,
  default? : Bool = false,
  default_format? : String,
) -> FuncBuilder {
  fn(args, p) raise SqlglotError {
    build_formatted_time_expr(
      kind,
      args,
      p.dialect,
      dialect_override?,
      default~,
      default_format?,
    )
  }
}

///|
/// Python `build_date_delta(exp_class, unit_mapping=None, default_unit="DAY",
/// supports_timezone=False)`. Pass `default_unit=""` for Python `None`.
pub fn build_date_delta(
  kind : Kind,
  unit_mapping? : Map[String, String],
  default_unit? : String = "DAY",
  supports_timezone? : Bool = false,
) -> FuncBuilder {
  fn(args, _p) {
    let unit_based = args.length() >= 3
    let has_timezone = args.length() == 4
    let this = if unit_based { Some(args[2]) } else { arg(args, 0) }
    let mut unit : Expr? = None
    if unit_based || default_unit != "" {
      let u = if unit_based { args[0] } else { literal_string(default_unit) }
      unit = Some(
        match unit_mapping {
          Some(m) if !m.is_empty() =>
            var_(
              match m.get(py_lower(u.name())) {
                Some(v) => v
                None => u.name()
              },
            )
          _ => u
        },
      )
    }
    let expression = mk(kind, [
      ("this", this),
      ("expression", arg(args, 1)),
      ("unit", unit),
    ])
    if supports_timezone && has_timezone {
      expression.set("zone", args[args.length() - 1])
    }
    expression
  }
}

///|
/// Python `build_date_delta_with_interval(expression_class, default_unit=None)`.
/// Python returns `None` for fewer than 2 arguments; this raises a ParseError instead.
pub fn build_date_delta_with_interval(
  kind : Kind,
  default_unit? : String,
) -> FuncBuilder {
  fn(args, _p) raise SqlglotError {
    if args.length() < 2 {
      raise ParseError(
        "\{kind.name()} expects at least 2 arguments, got \{args.length()}",
        [],
      )
    }
    let interval = args[1]
    if !interval.kind.is_a(Interval) {
      match default_unit {
        None =>
          raise ParseError(
            "INTERVAL expression expected but got '\{expr_to_sql(interval)}'",
            [],
          )
        Some(u) =>
          return mk(kind, [
            ("this", args[0]),
            ("expression", interval),
            ("unit", literal_string(u)),
          ])
      }
    }
    mk(kind, [
      ("this", Some(args[0])),
      ("expression", interval.this()),
      ("unit", unit_to_str(interval)),
    ])
  }
}

///|
/// Python `date_trunc_to_time(args)`.
pub fn date_trunc_to_time(args : Array[Expr]) -> Expr {
  let unit = arg(args, 0)
  let this = arg(args, 1)
  match this {
    Some(t) if t.kind.is_a(Cast) && t.is_type([DType::DATE]) =>
      mk(DateTrunc, [("unit", unit), ("this", this)])
    _ => mk(TimestampTrunc, [("this", this), ("unit", unit)])
  }
}

///|
/// Python `binary_from_function(expr_type)`.
pub fn binary_from_function(kind : Kind) -> FuncBuilder {
  fn(args, _p) {
    mk(kind, [("this", arg(args, 0)), ("expression", arg(args, 1))])
  }
}

///|
/// Python `build_timestamp_trunc(args)` (DATE_TRUNC in Doris, Postgres and StarRocks).
pub fn build_timestamp_trunc(args : Array[Expr]) -> Expr {
  mk(TimestampTrunc, [
    ("this", arg(args, 1)),
    ("unit", arg(args, 0)),
    ("zone", arg(args, 2)),
  ])
}

///|
/// Python `build_trunc(args, dialect, ...)`: builder for dialects with an overloaded
/// TRUNC (Oracle, Snowflake, etc). Uses type annotation to distinguish date vs numeric
/// truncation and returns Anonymous if the type cannot be determined.
pub fn build_trunc(
  args : Array[Expr],
  dialect : Dialect,
  date_trunc_unabbreviate? : Bool = true,
  default_date_trunc_unit? : String,
  date_trunc_requires_part? : Bool = true,
  fractions_supported? : Bool = false,
) -> Expr raise SqlglotError {
  let mut this = arg(args, 0)
  let mut second = arg(args, 1)
  match this {
    Some(t) if t.get_type() is None =>
      this = Some(gen_annotate_types(t, dialect))
    _ => ()
  }
  match second {
    Some(s) if s.get_type() is None =>
      second = Some(gen_annotate_types(s, dialect))
    _ => ()
  }
  let default_unit = match default_date_trunc_unit {
    Some(u) if u != "" => Some(u)
    _ => None
  }
  let is_type = fn(e : Expr?, types : Array[DType]) {
    match e {
      Some(x) => x.is_type(types)
      None => false
    }
  }
  if (
      is_type(this, dtype_temporal_types) &&
      (second is Some(_) || default_unit is Some(_))
    ) ||
    is_type(second, dtype_text_types) {
    let unit = match second {
      Some(s) => s
      None => literal_string(default_unit.unwrap_or(""))
    }
    return mk(DateTrunc, [
      ("this", this),
      ("unit", Some(unit)),
      ("unabbreviate", Some(date_trunc_unabbreviate)),
    ])
  }
  if is_type(this, dtype_numeric_types) ||
    is_type(second, dtype_numeric_types) ||
    (!date_trunc_requires_part && second is None) {
    return mk(Trunc, [
      ("this", this),
      ("decimals", second),
      ("fractions_supported", Some(fractions_supported)),
    ])
  }
  mk(Anonymous, [("this", "TRUNC"), ("expressions", args)])
}

///|
/// Python `isnull_to_is_null(args)`.
pub fn isnull_to_is_null(args : Array[Expr]) -> Expr {
  mk1(Paren, mk(Is, [("this", arg(args, 0)), ("expression", Some(null_()))]))
}

///|
/// Python `build_json_extract_path(expr_type, zero_based_indexing=True,
/// arrow_req_json_type=False, json_type=None)`. Like Python, it truncates `args` to 2
/// elements when the path is built.
pub fn build_json_extract_path(
  kind : Kind,
  zero_based_indexing? : Bool = true,
  arrow_req_json_type? : Bool = false,
  json_type? : String,
) -> FuncBuilder {
  fn(args, _p) {
    let segments : Array[Expr] = [mk0(JSONPathRoot)]
    for i in 1.. 2 {
          args[2:].to_owned()
        } else {
          []
        }
        return mk(kind, [
          ("this", arg(args, 0)),
          ("expression", arg(args, 1)),
          (
            "expressions",
            if rest_args.is_empty() {
              None
            } else {
              Some(rest_args)
            },
          ),
        ])
      }
      let text = a.name()
      match
        (if is_int_str(text) && (!arrow_req_json_type || !a.is_string()) {
          parse_int_checked(text)
        } else {
          None
        }) {
        Some(index) => {
          if !zero_based_indexing && index == -9223372036854775807L - 1L {
            raise int64_range_error("\{text} - 1")
          }
          segments.push(
            mk1(
              JSONPathSubscript,
              Value::Int(if zero_based_indexing { index } else { index - 1L }),
            ),
          )
        }
        _ => segments.push(mk1(JSONPathKey, text))
      }
    }
    // This is done to avoid failing in the expression validator due to the arg count
    args.truncate(2)
    let expression = mk(kind, [
      ("this", arg(args, 0)),
      ("expression", Some(mk(JSONPath, [("expressions", segments)]))),
    ])
    if !kind.is_any([JSONBExtract, JSONBExtractScalar]) {
      expression.set("only_json_types", arrow_req_json_type)
    }
    match json_type {
      Some(t) => expression.set("json_type", t)
      None => ()
    }
    expression
  }
}

///|
/// Python `build_default_decimal_type(precision=None, scale=None)`: a DataType
/// converter that adds default DECIMAL parameters.
pub fn build_default_decimal_type(
  precision? : Int,
  scale? : Int,
) -> (Expr) -> Expr {
  fn(dtype) {
    match precision {
      Some(p) if dtype.expressions().is_empty() => {
        let params = [mk1(DataTypeParam, literal_int(p))]
        match scale {
          Some(s) => params.push(mk1(DataTypeParam, literal_int(s)))
          None => ()
        }
        mk(DataType, [
          ("this", DType::DECIMAL),
          ("expressions", params),
          ("nested", false),
        ])
      }
      _ => dtype
    }
  }
}

///|
/// Python `build_timestamp_from_parts(args)`.
pub fn build_timestamp_from_parts(args : Array[Expr]) -> Expr {
  if args.length() == 2 {
    // Other dialects don't have the TIMESTAMP_FROM_PARTS(date, time) concept, so we
    // parse this into Anonymous for now instead of introducing complexity
    return mk(Anonymous, [
      ("this", "TIMESTAMP_FROM_PARTS"),
      ("expressions", args),
    ])
  }
  from_arg_list(TimestampFromParts, args)
}

///|
/// Python `build_like(expr_type, not_like=False)` from dialects/dialect.py (named
/// `build_like_fn` because the parser's `build_like(args)` already exists).
pub fn build_like_fn(kind : Kind, not_like? : Bool = false) -> FuncBuilder {
  fn(args, _p) {
    let mut like_expr = mk(kind, [
      ("this", arg(args, 0)),
      ("expression", arg(args, 1)),
    ])
    match arg(args, 2) {
      Some(escape) =>
        like_expr = mk(Escape, [("this", like_expr), ("expression", escape)])
      None => ()
    }
    if not_like {
      like_expr = mk1(Not, like_expr)
    }
    like_expr
  }
}

///|
/// Python `build_regexp_extract(expr_type)`.
pub fn build_regexp_extract(kind : Kind) -> FuncBuilder {
  fn(args, p) {
    // The "position" argument specifies the index of the string character to start
    // matching from. `null_if_pos_overflow` reflects the dialect's behavior when position
    // is greater than the string length: NULL if true, else an empty string. It's only
    // needed for RegexpExtract - RegexpExtractAll always returns an empty array.
    let group = match arg(args, 2) {
      Some(g) => g
      None => literal_int(p.dialect.cfg.regexp_extract_default_group)
    }
    let e = mk(kind, [
      ("this", arg(args, 0)),
      ("expression", arg(args, 1)),
      ("group", Some(group)),
      ("parameters", arg(args, 3)),
    ])
    if kind == RegexpExtract {
      e.set(
        "null_if_pos_overflow",
        p.dialect.cfg.regexp_extract_position_overflow_returns_null,
      )
    }
    e
  }
}

///|
/// Python `build_timetostr_or_tochar(args, dialect)`.
pub fn build_timetostr_or_tochar(
  args : Array[Expr],
  dialect : Dialect,
) -> Expr raise SqlglotError {
  if args.length() == 2 {
    let this = args[0]
    if this.get_type() is None {
      gen_annotate_types(this, dialect) |> ignore
    }
    if this.is_type(dtype_temporal_types) {
      return build_formatted_time_expr(TimeToStr, args, dialect, default=true)
    }
  }
  from_arg_list(ToChar, args)
}

///|
/// Python `build_replace_with_optional_replacement(args)`.
pub fn build_replace_with_optional_replacement(args : Array[Expr]) -> Expr {
  mk(Replace, [
    ("this", arg(args, 0)),
    ("expression", arg(args, 1)),
    (
      "replacement",
      Some(
        match arg(args, 2) {
          Some(r) => r
          None => literal_string("")
        },
      ),
    ),
  ])
}