// Date/time/unit helpers of sqlglot/dialects/dialect.py used by dialect generators.

///|
/// Python `date_add_interval_sql(data_type, kind)`: `_(this, INTERVAL ...)`.
pub fn date_add_interval_sql(data_type : String, kind : String) -> GenFn {
  (g, e) => {
    let this = g.sql_key(e, "this")
    let interval = mk(Interval, [
      ("this", e.expression()),
      ("unit", unit_to_var(e)),
    ])
    data_type + "_" + kind + "(" + this + ", " + g.sql(Some(interval)) + ")"
  }
}

///|
/// Python `timestamptrunc_sql(func="DATE_TRUNC", zone=False)`.
pub fn timestamptrunc_sql(
  func? : String = "DATE_TRUNC",
  zone? : Bool = false,
) -> GenFn {
  (g, e) => {
    let args : Array[&SqlArg] = [weekstart_unit_to_str(g, e), e.this()]
    if zone {
      args.push(e.get("zone"))
    }
    g.func(func, args)
  }
}

///|
/// Python `no_timestamp_sql`.
pub fn no_timestamp_sql(g : Generator, e : Expr) -> String raise SqlglotError {
  match e.arg("zone") {
    None => {
      let target_type = match gen_annotate_types(e, g.dialect).get_type() {
        Some(t) => t
        None => datatype_of(DType::TIMESTAMP)
      }
      g.sql(Some(exp_cast_to(e.this_(), target_type)))
    }
    Some(zone) =>
      if timezones.contains(py_lower(zone.name())) {
        g.sql(
          Some(
            mk(AtTimeZone, [
              ("this", exp_cast(e.this_(), DType::TIMESTAMP)),
              ("zone", zone),
            ]),
          ),
        )
      } else {
        g.func("TIMESTAMP", [e.this(), Some(zone)])
      }
  }
}

///|
/// Python `no_time_sql`: transpiles BigQuery's TIME(timestamp, zone) to
/// `CAST(CAST( AS TIMESTAMPTZ) AT TIME ZONE  AS TIME)`.
pub fn no_time_sql(g : Generator, e : Expr) -> String raise SqlglotError {
  let this = exp_cast(e.this_(), DType::TIMESTAMPTZ)
  let expr = exp_cast(
    mk(AtTimeZone, [("this", Some(this)), ("zone", e.arg("zone"))]),
    DType::TIME,
  )
  g.sql(Some(expr))
}

///|
/// Python `no_datetime_sql`.
pub fn no_datetime_sql(g : Generator, e : Expr) -> String raise SqlglotError {
  let this = e.this_()
  // Python raises AttributeError here when the expression is missing; raise
  // instead of aborting the whole process
  guard e.expression() is Some(expr) else {
    raise ValueError("'NoneType' object has no attribute 'name'")
  }
  if timezones.contains(py_lower(expr.name())) {
    // Transpile BigQuery's DATETIME(timestamp, zone) to
    // CAST(CAST( AS TIMESTAMPTZ) AT TIME ZONE  AS TIMESTAMP)
    let this = exp_cast(this, DType::TIMESTAMPTZ)
    let this = exp_cast(
      mk(AtTimeZone, [("this", this), ("zone", expr)]),
      DType::TIMESTAMP,
    )
    return g.sql(Some(this))
  }
  let this = exp_cast(this, DType::DATE)
  let expr = exp_cast(expr, DType::TIME)
  g.sql(Some(exp_cast(mk2(Add, this, expr), DType::TIMESTAMP)))
}

///|
/// Python `timestrtotime_sql(self, expression, include_precision=False)`.
pub fn timestrtotime_sql(
  g : Generator,
  e : Expr,
  include_precision? : Bool = false,
) -> String raise SqlglotError {
  let builder = if e.has("zone") {
    DType::TIMESTAMPTZ
  } else {
    DType::TIMESTAMP
  }
  let mut datatype = datatype_of(builder)
  match e.this() {
    Some(this) if this.kind.is_a(Literal) && include_precision => {
      let precision = subsecond_precision(this.name())
      if precision > 0 {
        datatype = datatype_build(builder, kwargs=[
          ("expressions", [mk1(DataTypeParam, literal_int(precision))]),
        ])
      }
    }
    _ => ()
  }
  g.sql(Some(exp_cast_to(e.this_(), datatype, dialect=g.dialect)))
}

///|
/// Python `datestrtodate_sql`.
pub fn datestrtodate_sql(g : Generator, e : Expr) -> String raise SqlglotError {
  g.sql(Some(exp_cast(e.this_(), DType::DATE)))
}

///|
/// Python `TsOrDsAdd.return_type`: `DataType.build(return_type or DATE)`.
pub fn ts_or_ds_add_return_type(e : Expr) -> Expr {
  match e.arg("return_type") {
    Some(t) => t.copy()
    None =>
      match e.get("return_type") {
        Some(DT(d)) => datatype_of(d)
        _ => datatype_of(DType::DATE)
      }
  }
}

///|
/// Python `ts_or_ds_add_cast`.
pub fn ts_or_ds_add_cast(e : Expr) -> Expr {
  let mut this = e.this_().copy()
  let return_type = ts_or_ds_add_return_type(e)
  if return_type.is_type([DType::DATE]) {
    // If we need to cast to a DATE, we cast to TIMESTAMP first to make sure we can
    // truncate timestamp strings, because some dialects can't cast them to DATE
    this = exp_cast(this, DType::TIMESTAMP)
  }
  e.this_().replace(Some(exp_cast_to(this, return_type))) |> ignore
  e
}

///|
/// Python `remove_ts_or_ds_to_date(to_sql=None, args=("this",))`.
pub fn remove_ts_or_ds_to_date(
  to_sql? : GenFn,
  args? : Array[String] = ["this"],
) -> GenFn {
  (g, e) => {
    for arg_key in args {
      match e.arg(arg_key) {
        Some(a) if a.kind.is_any([TsOrDsToDate, TsOrDsToTimestamp]) &&
          !a.has("format") => e.set(arg_key, a.this())
        _ => ()
      }
    }
    match to_sql {
      Some(f) => f(g, e)
      None => g.function_fallback_sql(e)
    }
  }
}

///|
/// Python `date_delta_sql(name, cast=False)`: `name(unit, expression, this)`.
pub fn date_delta_sql(name : String, cast? : Bool = false) -> GenFn {
  (g, e) => {
    let e = if cast && e.kind.is_a(TsOrDsAdd) {
      ts_or_ds_add_cast(e)
    } else {
      e
    }
    g.func(name, [unit_to_var(e), e.expression(), e.this()])
  }
}

///|
/// Python `date_delta_to_binary_interval_op(cast=True)`: `this +/- INTERVAL ...`.
pub fn date_delta_to_binary_interval_op(cast? : Bool = true) -> GenFn {
  (g, e) => {
    let mut this = e.this_()
    let unit = unit_to_var(e)
    let op = if e.kind.is_any([
        DateAdd,
        TimeAdd,
        DatetimeAdd,
        TsOrDsAdd,
        TimestampAdd,
      ]) {
      "+"
    } else {
      "-"
    }
    let mut to_type : Expr? = None
    if cast {
      if e.kind.is_a(TsOrDsAdd) {
        to_type = Some(ts_or_ds_add_return_type(e))
      } else if this.is_string() {
        // Cast string literals (i.e function parameters) to the appropriate type for
        // +/- interval to work
        to_type = Some(
          datatype_of(
            if e.kind.is_any([DatetimeAdd, DatetimeSub]) {
              DType::DATETIME
            } else {
              DType::DATE
            },
          ),
        )
      }
    }
    match to_type {
      Some(t) => this = exp_cast_to(this, t)
      None => ()
    }
    let expr = e.expression_()
    let interval = if expr.kind.is_a(Interval) {
      expr
    } else {
      mk(Interval, [("this", Some(expr)), ("unit", unit)])
    }
    g.sql(Some(this)) + " " + op + " " + g.sql(Some(interval))
  }
}

///|
/// Python `unit_to_str(expression, default="DAY")`. Pass `default=""` for Python `None`.
pub fn unit_to_str(e : Expr, default? : String = "DAY") -> Expr? {
  match e.get("unit") {
    Some(Node(unit)) =>
      if unit.kind.is_a(WeekStart) {
        // WEEK() is BigQuery-only syntax, so it degrades to the plain WEEK unit.
        // Callers that need a warning about a changed week start should use
        // weekstart_unit_to_str
        Some(literal_string("WEEK"))
      } else if unit.kind.is_a(Placeholder) ||
        (unit.kind != Var && unit.kind != Literal) {
        Some(unit)
      } else {
        Some(literal_string(unit.name()))
      }
    Some(v) if v.truthy() => Some(literal_string(value_name(v)))
    _ => if default != "" { Some(literal_string(default)) } else { None }
  }
}

///|
/// Python `.name` of a non-expression argument value.
fn value_name(v : Value) -> String {
  match v {
    Str(s) => s
    Int(i) => i.to_string()
    _ => ""
  }
}

///|
/// Python `weekstart_unit_to_str(self, expression, default="DAY")`.
pub fn weekstart_unit_to_str(
  g : Generator,
  e : Expr,
  default? : String = "DAY",
) -> Expr? raise SqlglotError {
  match e.arg("unit") {
    Some(unit) if unit.kind.is_a(WeekStart) =>
      Some(literal_string(g.weekstart_name(unit)))
    _ => unit_to_str(e, default~)
  }
}

///|
/// Python `unit_to_var(expression, default="DAY")`. Pass `default=""` for Python `None`.
pub fn unit_to_var(e : Expr, default? : String = "DAY") -> Expr? {
  let value = match e.get("unit") {
    Some(Node(unit)) => {
      if unit.kind.is_any([Var, Placeholder, WeekStart, Column]) {
        return Some(unit)
      }
      unit.name()
    }
    Some(v) if v.truthy() => value_name(v)
    _ => default
  }
  if value != "" {
    Some(var_(value))
  } else {
    None
  }
}

///|
/// Python `WEEK_START_DAY_TO_DOW`: days of week to ISO 8601 day-of-week numbers
/// (Monday=1, ..., Sunday=7).
pub let week_start_day_to_dow : Map[String, Int] = {
  "MONDAY": 1,
  "TUESDAY": 2,
  "WEDNESDAY": 3,
  "THURSDAY": 4,
  "FRIDAY": 5,
  "SATURDAY": 6,
  "SUNDAY": 7,
}

///|
/// Python `week_offset_to_dow(offset)`: converts a dialect's WEEK_OFFSET (days relative
/// to Monday) to the ISO day number of its week start.
pub fn week_offset_to_dow(offset : Int) -> Int {
  let m = offset % 7
  (if m < 0 { m + 7 } else { m }) + 1
}

///|
/// Python `week_unit_to_dow(unit)`: the ISO 8601 week start day (Monday=1 .. Sunday=7) of
/// a week-ish unit, e.g. BigQuery's WEEK() or ISOWEEK, or None if not a week unit or
/// if the day is dynamic.
pub fn week_unit_to_dow(unit : Expr?) -> Int? {
  match unit {
    Some(u) if u.kind.is_a(Var) => {
      let n = py_upper(u.name())
      if n == "WEEK" || n == "ISOWEEK" {
        Some(1)
      } else {
        None
      }
    }
    Some(u) if u.kind.is_a(WeekStart) =>
      week_start_day_to_dow.get(py_upper(u.name()))
    _ => None
  }
}

///|
/// Python `map_date_part(part, dialect)`. `dialect` defaults to the base dialect.
pub fn map_date_part(part : Expr?, dialect? : Dialect) -> Expr? {
  guard part is Some(p) else { return None }
  if p.kind.is_a(Column) && p.parts().length() != 1 {
    return part
  }
  let d = match dialect {
    Some(d) => d
    None => base_dialect()
  }
  match d.cfg.date_part_mapping.get(py_upper(p.name())) {
    Some(mapped) if mapped != "" =>
      Some(if p.is_string() { literal_string(mapped) } else { var_(mapped) })
    _ => part
  }
}

///|
/// Python `map_date_part(part, dialect)` for a non-optional part.
pub fn map_date_part_expr(part : Expr, dialect? : Dialect) -> Expr {
  match map_date_part(Some(part), dialect?) {
    Some(p) => p
    None => part
  }
}