// d3-shape 3.2 curves used by line and area marks: linear, step,
// step-before, step-after and monotone (X for vertical, Y for horizontal).

///|
pub(all) enum CurveKind {
  Linear
  Step
  StepBefore
  StepAfter
  MonotoneX
  MonotoneY
}

///|
/// Look up an interpolation method by its Vega name.
pub fn curve_kind(name : String, orient : String) -> CurveKind? {
  match name {
    "linear" => Some(Linear)
    "step" => Some(Step)
    "step-before" => Some(StepBefore)
    "step-after" => Some(StepAfter)
    "monotone" =>
      Some(if orient == "horizontal" { MonotoneY } else { MonotoneX })
    _ => None
  }
}

///|
/// Mutable curve state, mirroring d3's curve objects.
pub struct Curve {
  kind : CurveKind
  mut line : Double
  mut point : Int32
  mut x0 : Double
  mut y0 : Double
  mut x1 : Double
  mut y1 : Double
  mut t0 : Double
  mut t : Double
}

///|
pub fn Curve::new(kind : CurveKind) -> Curve {
  let nan = @double.not_a_number
  {
    kind,
    line: nan,
    point: 0,
    x0: nan,
    y0: nan,
    x1: nan,
    y1: nan,
    t0: nan,
    t: match kind {
      Step => 0.5
      StepBefore => 0.0
      StepAfter => 1.0
      _ => 0.0
    },
  }
}

///|
pub fn Curve::area_start(self : Curve) -> Unit {
  self.line = 0.0
}

///|
pub fn Curve::area_end(self : Curve) -> Unit {
  self.line = @double.not_a_number
}

///|
pub fn Curve::line_start(self : Curve, _ctx : Ctx) -> Unit {
  self.point = 0
  match self.kind {
    Step | StepBefore | StepAfter => {
      self.x1 = @double.not_a_number
      self.y1 = @double.not_a_number
    }
    MonotoneX | MonotoneY => {
      self.x0 = @double.not_a_number
      self.x1 = @double.not_a_number
      self.y0 = @double.not_a_number
      self.y1 = @double.not_a_number
      self.t0 = @double.not_a_number
    }
    Linear => ()
  }
}

// MonotoneY draws through a context that swaps x and y.

///|
fn mono_move(ctx : Ctx, swap : Bool, x : Double, y : Double) -> Unit {
  if swap {
    ctx.move_to(y, x)
  } else {
    ctx.move_to(x, y)
  }
}

///|
fn mono_line(ctx : Ctx, swap : Bool, x : Double, y : Double) -> Unit {
  if swap {
    ctx.line_to(y, x)
  } else {
    ctx.line_to(x, y)
  }
}

///|
fn sign(x : Double) -> Double {
  if x < 0.0 {
    -1.0
  } else {
    1.0
  }
}

///|
fn slope3(c : Curve, x2 : Double, y2 : Double) -> Double {
  let h0 = c.x1 - c.x0
  let h1 = x2 - c.x1
  // (h0 || h1 < 0 && -0): a zero step takes the sign of the other step
  let d0 = if h0 != 0.0 && !h0.is_nan() {
    h0
  } else if h1 < 0.0 {
    -0.0
  } else {
    0.0
  }
  let d1 = if h1 != 0.0 && !h1.is_nan() {
    h1
  } else if h0 < 0.0 {
    -0.0
  } else {
    0.0
  }
  let s0 = (c.y1 - c.y0) / d0
  let s1 = (y2 - c.y1) / d1
  let p = (s0 * h1 + s1 * h0) / (h0 + h1)
  let m = @util.js_min(@util.js_min(s0.abs(), s1.abs()), 0.5 * p.abs())
  let r = (sign(s0) + sign(s1)) * m
  if r == 0.0 || r.is_nan() {
    0.0
  } else {
    r
  }
}

///|
fn slope2(c : Curve, t : Double) -> Double {
  let h = c.x1 - c.x0
  if h != 0.0 && !h.is_nan() {
    (3.0 * (c.y1 - c.y0) / h - t) / 2.0
  } else {
    t
  }
}

///|
fn mono_point(
  ctx : Ctx,
  swap : Bool,
  c : Curve,
  t0 : Double,
  t1 : Double,
) -> Unit {
  let x0 = c.x0
  let y0 = c.y0
  let x1 = c.x1
  let y1 = c.y1
  let dx = (x1 - x0) / 3.0
  if swap {
    ctx.bezier_to(y0 + dx * t0, x0 + dx, y1 - dx * t1, x1 - dx, y1, x1)
  } else {
    ctx.bezier_to(x0 + dx, y0 + dx * t0, x1 - dx, y1 - dx * t1, x1, y1)
  }
}

///|
pub fn Curve::line_end(self : Curve, ctx : Ctx) -> Unit {
  match self.kind {
    Linear => {
      if (self.line != 0.0 && !self.line.is_nan()) ||
        (self.line != 0.0 && self.point == 1) {
        ctx.close_path()
      }
      self.line = 1.0 - self.line
    }
    Step | StepBefore | StepAfter => {
      if 0.0 < self.t && self.t < 1.0 && self.point == 2 {
        ctx.line_to(self.x1, self.y1)
      }
      if (self.line != 0.0 && !self.line.is_nan()) ||
        (self.line != 0.0 && self.point == 1) {
        ctx.close_path()
      }
      if self.line >= 0.0 {
        self.t = 1.0 - self.t
        self.line = 1.0 - self.line
      }
    }
    MonotoneX | MonotoneY => {
      let swap = self.kind is MonotoneY
      if self.point == 2 {
        mono_line(ctx, swap, self.x1, self.y1)
      } else if self.point == 3 {
        mono_point(ctx, swap, self, self.t0, slope2(self, self.t0))
      }
      if (self.line != 0.0 && !self.line.is_nan()) ||
        (self.line != 0.0 && self.point == 1) {
        ctx.close_path()
      }
      self.line = 1.0 - self.line
    }
  }
}

///|
pub fn Curve::point(self : Curve, ctx : Ctx, x0 : Double, y0 : Double) -> Unit {
  match self.kind {
    Linear =>
      if self.point == 0 {
        self.point = 1
        if self.line != 0.0 && !self.line.is_nan() {
          ctx.line_to(x0, y0)
        } else {
          ctx.move_to(x0, y0)
        }
      } else {
        self.point = 2
        ctx.line_to(x0, y0)
      }
    Step | StepBefore | StepAfter => {
      if self.point == 0 {
        self.point = 1
        if self.line != 0.0 && !self.line.is_nan() {
          ctx.line_to(x0, y0)
        } else {
          ctx.move_to(x0, y0)
        }
      } else {
        self.point = 2
        if self.t <= 0.0 {
          ctx.line_to(self.x1, y0)
          ctx.line_to(x0, y0)
        } else {
          let xm = self.x1 * (1.0 - self.t) + x0 * self.t
          ctx.line_to(xm, self.y1)
          ctx.line_to(xm, y0)
        }
      }
      self.x1 = x0
      self.y1 = y0
    }
    MonotoneX | MonotoneY => {
      let swap = self.kind is MonotoneY
      // MonotoneY feeds (y, x) into the MonotoneX algorithm
      let x = if swap { y0 } else { x0 }
      let y = if swap { x0 } else { y0 }
      if x == self.x1 && y == self.y1 {
        return
      }
      let mut t1 = @double.not_a_number
      match self.point {
        0 => {
          self.point = 1
          if self.line != 0.0 && !self.line.is_nan() {
            mono_line(ctx, swap, x, y)
          } else {
            mono_move(ctx, swap, x, y)
          }
        }
        1 => self.point = 2
        2 => {
          self.point = 3
          t1 = slope3(self, x, y)
          mono_point(ctx, swap, self, slope2(self, t1), t1)
        }
        _ => {
          t1 = slope3(self, x, y)
          mono_point(ctx, swap, self, self.t0, t1)
        }
      }
      self.x0 = self.x1
      self.x1 = x
      self.y0 = self.y1
      self.y1 = y
      self.t0 = t1
    }
  }
}