///|
pub struct BarStyle {
  left : String
  right : String
  fill : String
  head : String
  tail : String
  empty : String
  part1 : String
  part2 : String
  part3 : String
  width : Int
  is_prefix : Bool
  is_gradient : Bool
}

///|
pub let tqdmoon_classic : BarStyle = BarStyle::{
  left: "|",
  right: "|",
  fill: "█",
  head: "",
  tail: "",
  empty: " ",
  part1: " ",
  part2: " ",
  part3: " ",
  width: 20,
  is_prefix: false,
  is_gradient: false,
}

///|
pub let tqdmoon_ascii : BarStyle = BarStyle::{
  left: "[",
  right: "]",
  fill: "=",
  head: ">",
  tail: "",
  empty: " ",
  part1: " ",
  part2: " ",
  part3: " ",
  width: 20,
  is_prefix: false,
  is_gradient: false,
}

///|
pub let tqdmoon_moon : BarStyle = BarStyle::{
  left: "|",
  right: "|",
  fill: "🌕",
  head: "",
  tail: "",
  empty: "🌑",
  part1: "🌒",
  part2: "🌓",
  part3: "🌔",
  width: 10,
  is_prefix: false,
  is_gradient: false,
}

///|
pub let tqdmoon_google : BarStyle = BarStyle::{
  left: "",
  right: "",
  fill: "●",
  head: "tqdm",
  tail: "on",
  empty: "o",
  part1: "o",
  part2: "o",
  part3: "o",
  width: 20,
  is_prefix: true,
  is_gradient: true,
}

///|
pub fn make_bar_style(
  left? : String = "|",
  right? : String = "|",
  fill? : String = "█",
  head? : String = "",
  tail? : String = "",
  empty? : String = " ",
  part1? : String = " ",
  part2? : String = " ",
  part3? : String = " ",
  width? : Int = 20,
  is_prefix? : Bool = false,
  is_gradient? : Bool = false,
) -> BarStyle {
  BarStyle::{
    left,
    right,
    fill,
    head,
    tail,
    empty,
    part1,
    part2,
    part3,
    width,
    is_prefix,
    is_gradient,
  }
}

///|
pub struct Tqdm[T] {
  iterable : Iter[T]
  total : Int?
  start_instant : @bench.Timestamp
  bar_style : BarStyle
  desc : String
  unit : String
  current : Int
  last_render_time : Double
  disabled : Bool
}

///|
pub fn[T] tqdm(
  iterable : Iter[T],
  total? : Int? = None,
  style? : BarStyle = tqdmoon_classic,
  desc? : String = "",
  unit? : String = "items",
  disabled? : Bool = false,
) -> Tqdm[T] {
  Tqdm::{
    iterable,
    total,
    start_instant: @bench.monotonic_clock_start(),
    bar_style: style,
    desc,
    unit,
    current: 0,
    last_render_time: 0.0,
    disabled,
  }
}

///|
pub fn[T] Tqdm::set_style(self : Tqdm[T], style : BarStyle) -> Tqdm[T] {
  Tqdm::{ ..self, bar_style: style }
}

///|
pub fn[T] Tqdm::set_disabled(self : Tqdm[T], disabled : Bool) -> Tqdm[T] {
  Tqdm::{ ..self, disabled, }
}

///|
pub fn[T] Tqdm::set_desc(self : Tqdm[T], desc : String) -> Tqdm[T] {
  Tqdm::{ ..self, desc, }
}

///|
pub fn[T] Tqdm::set_unit(self : Tqdm[T], unit : String) -> Tqdm[T] {
  Tqdm::{ ..self, unit, }
}

///|
fn repeat_string(s : String, n : Int) -> String {
  if n <= 0 {
    ""
  } else {
    s + repeat_string(s, n - 1)
  }
}

///|
fn progress_fill_char(style : BarStyle, index : Int, filled : Int) -> String {
  if index < filled {
    style.empty
  } else {
    style.fill
  }
}

///|
fn build_progressive_fill(style : BarStyle, filled : Int) -> String {
  build_progressive_fill_loop(style, filled, style.width, 0, "")
}

///|
fn build_progressive_fill_loop(
  style : BarStyle,
  filled : Int,
  width : Int,
  index : Int,
  acc : String,
) -> String {
  if index >= width {
    acc
  } else {
    build_progressive_fill_loop(
      style,
      filled,
      width,
      index + 1,
      acc + progress_fill_char(style, index, filled),
    )
  }
}

///|
fn build_bar_body(style : BarStyle, progress : Int) -> String {
  if style.is_prefix {
    if style.is_gradient {
      let suffix = if progress >= style.width { style.tail } else { "n" }
      style.head + build_progressive_fill(style, progress) + suffix
    } else {
      style.head + repeat_string(style.fill, progress) + style.tail
    }
  } else {
    let n = progress
    let w = style.width
    if n >= w {
      repeat_string(style.fill, w)
    } else {
      let filled = repeat_string(style.fill, n) + style.head
      filled + repeat_string(style.empty, w - n - style.head.length())
    }
  }
}

///|
fn partial_char(style : BarStyle, remainder : Int) -> String {
  match remainder {
    1 => style.part1
    2 => style.part2
    3 => style.part3
    _ => style.empty
  }
}

///|
fn build_bar_body_scaled(style : BarStyle, scaled : Int, scale : Int) -> String {
  if style.is_prefix {
    build_bar_body(style, scaled / scale)
  } else {
    let w = style.width
    if scaled >= w * scale {
      repeat_string(style.fill, w)
    } else {
      let full = scaled / scale
      let rem = scaled % scale
      let partial = if rem > 0 { partial_char(style, rem) } else { style.head }
      let used = full + partial.length()
      repeat_string(style.fill, full) +
      partial +
      repeat_string(style.empty, w - used)
    }
  }
}

///|
fn format_time(secs : Double) -> String {
  let total = secs.to_int()
  let hours = total / 3600
  let mins = total % 3600 / 60
  let sec = total % 60
  let pad = fn(x : Int) -> String {
    if x < 10 {
      "0" + x.to_string()
    } else {
      x.to_string()
    }
  }
  if hours > 0 {
    pad(hours) + ":" + pad(mins) + ":" + pad(sec)
  } else {
    pad(mins) + ":" + pad(sec)
  }
}

///|
fn format_rate(rate : Double, unit : String) -> String {
  let suffix = " " + unit + "/s"
  let int_part = rate.to_int()
  if int_part.to_double() == rate {
    int_part.to_string() + suffix
  } else {
    let tenth = (rate * 10.0).to_int()
    let whole = tenth / 10
    let frac = tenth % 10
    whole.to_string() + "." + frac.to_string() + suffix
  }
}

///|
fn apply_desc_prefix(desc : String, bar : String) -> String {
  if desc.is_empty() {
    bar
  } else {
    desc + ": " + bar
  }
}

///|
fn render_bar(
  total : Int?,
  current : Int,
  style : BarStyle,
  elapsed : Double,
  desc : String,
  unit : String,
) -> String {
  match total {
    None => {
      let rate = if elapsed > 0.0 { current.to_double() / elapsed } else { 0.0 }
      apply_desc_prefix(
        desc,
        current.to_string() +
        " " +
        unit +
        " [" +
        format_time(elapsed) +
        ", " +
        format_rate(rate, unit) +
        "]",
      )
    }
    Some(n) =>
      if n == 0 {
        apply_desc_prefix(desc, "0/0")
      } else {
        let pct = current * 100 / n
        let scale = 4
        let scaled = current * style.width * scale / n
        let body = build_bar_body_scaled(style, scaled, scale)
        let rate = if elapsed > 0.0 {
          current.to_double() / elapsed
        } else {
          0.0
        }
        let eta = if rate > 0.0 {
          (n - current).to_double() / rate
        } else {
          0.0
        }
        apply_desc_prefix(
          desc,
          style.left +
          body +
          style.right +
          " " +
          pct.to_string() +
          "% " +
          current.to_string() +
          "/" +
          n.to_string() +
          " [" +
          format_time(elapsed) +
          "<" +
          format_time(eta) +
          ", " +
          format_rate(rate, unit) +
          "]",
        )
      }
  }
}

///|
fn elapsed_secs(start : @bench.Timestamp) -> Double {
  @bench.monotonic_clock_end(start) / 1_000_000.0
}

///|
pub fn[T] Tqdm::iter(self : Tqdm[T]) -> Iter[T] {
  let iterable = self.iterable
  let total = self.total
  let start_instant = self.start_instant
  let bar_style = self.bar_style
  let desc = self.desc
  let unit = self.unit
  let disabled = self.disabled
  let mut current = self.current
  let mut last_render_time = self.last_render_time
  let mut first_render = true
  let size_hint = match total {
    None => -1
    Some(n) => n
  }

  Iter::new(
    fn() {
      let item = iterable.next()
      match item {
        None => {
          if disabled == false {
            let elapsed = elapsed_secs(start_instant)
            let bar = render_bar(total, current, bar_style, elapsed, desc, unit)
            if first_render {
              println(bar)
            } else {
              println("\u001b[1A\u001b[K" + bar)
            }
          }
          None
        }
        Some(x) => {
          current = current + 1
          if disabled == false {
            let elapsed = elapsed_secs(start_instant)
            if elapsed - last_render_time >= 0.05 {
              let bar = render_bar(
                total, current, bar_style, elapsed, desc, unit,
              )
              if first_render {
                println(bar)
                first_render = false
              } else {
                println("\u001b[1A\u001b[K" + bar)
              }
              last_render_time = elapsed
            }
          }
          Some(x)
        }
      }
    },
    size_hint~,
  )
}