// Port of rough.js 4.6.6 src/renderer.ts.
//
// Evaluation order: rough.js draws its random numbers inside expressions and
// argument lists, which JavaScript evaluates left to right. Here every call
// that consumes a random number is a statement of its own, in the order the
// JavaScript code evaluates them; drawing one number more, less or earlier
// would shift every later coordinate.

///|
using @geometry {type Point}

///|
priv struct EllipseParams {
  increment : Double
  rx : Double
  ry : Double
}

///|
priv struct EllipseResult {
  estimated_points : Array[Point]
  opset : OpSet
}

///|
fn line(
  x1 : Double,
  y1 : Double,
  x2 : Double,
  y2 : Double,
  o : ResolvedOptions,
) -> OpSet {
  { kind: Path, ops: double_line(x1, y1, x2, y2, o), }
}

///|
fn linear_path(
  points : Array[Point],
  close : Bool,
  o : ResolvedOptions,
) -> OpSet {
  let len = points.length()
  if len > 2 {
    let ops = []
    for i in 0..<(len - 1) {
      ops.append(
        double_line(
          points[i].x,
          points[i].y,
          points[i + 1].x,
          points[i + 1].y,
          o,
        ),
      )
    }
    if close {
      ops.append(
        double_line(
          points[len - 1].x,
          points[len - 1].y,
          points[0].x,
          points[0].y,
          o,
        ),
      )
    }
    { kind: Path, ops, }
  } else if len == 2 {
    line(points[0].x, points[0].y, points[1].x, points[1].y, o)
  } else {
    { kind: Path, ops: [], }
  }
}

///|
fn polygon(points : Array[Point], o : ResolvedOptions) -> OpSet {
  linear_path(points, true, o)
}

///|
fn rectangle_points(
  x : Double,
  y : Double,
  width : Double,
  height : Double,
) -> Array[Point] {
  [
    Point::new(x, y),
    Point::new(x + width, y),
    Point::new(x + width, y + height),
    Point::new(x, y + height),
  ]
}

///|
fn rectangle(
  x : Double,
  y : Double,
  width : Double,
  height : Double,
  o : ResolvedOptions,
) -> OpSet {
  polygon(rectangle_points(x, y, width, height), o)
}

///|
fn curve(points_list : Array[Array[Point]], o : ResolvedOptions) -> OpSet {
  if points_list.length() == 0 {
    return { kind: Path, ops: [], }
  }
  let o1 = curve_with_offset(points_list[0], 1.0 * (1.0 + o.roughness * 0.2), o)
  let o2 = if o.disable_multi_stroke {
    []
  } else {
    curve_with_offset(
      points_list[0],
      1.5 * (1.0 + o.roughness * 0.22),
      clone_options_alter_seed(o),
    )
  }
  for i in 1.. 0 {
      let underlay = curve_with_offset(
        points,
        1.0 * (1.0 + o.roughness * 0.2),
        o,
      )
      let overlay = if o.disable_multi_stroke {
        []
      } else {
        curve_with_offset(
          points,
          1.5 * (1.0 + o.roughness * 0.22),
          clone_options_alter_seed(o),
        )
      }
      for item in underlay {
        if !(item is Move(_, _)) {
          o1.push(item)
        }
      }
      for item in overlay {
        if !(item is Move(_, _)) {
          o2.push(item)
        }
      }
    }
  }
  { kind: Path, ops: [..o1, ..o2], }
}

///|
fn ellipse(
  x : Double,
  y : Double,
  width : Double,
  height : Double,
  o : ResolvedOptions,
) -> OpSet {
  let params = generate_ellipse_params(width, height, o)
  ellipse_with_params(x, y, o, params).opset
}

///|
fn generate_ellipse_params(
  width : Double,
  height : Double,
  o : ResolvedOptions,
) -> EllipseParams {
  let hw = width / 2.0
  let hh = height / 2.0
  let psq = (@jsmath.PI * 2.0 * ((hw * hw + hh * hh) / 2.0).sqrt()).sqrt()
  let step_count = @jsmath.max(
    o.curve_step_count,
    o.curve_step_count / (200.0 : Double).sqrt() * psq,
  ).ceil()
  let increment = @jsmath.PI * 2.0 / step_count
  let mut rx = (width / 2.0).abs()
  let mut ry = (height / 2.0).abs()
  let curve_fit_randomness = 1.0 - o.curve_fitting
  rx += offset_opt(rx * curve_fit_randomness, o)
  ry += offset_opt(ry * curve_fit_randomness, o)
  { increment, rx, ry, }
}

///|
fn ellipse_with_params(
  x : Double,
  y : Double,
  o : ResolvedOptions,
  params : EllipseParams,
) -> EllipseResult {
  // `increment * _offset(0.1, _offset(0.4, 1, o), o)`: inner call first.
  let inner = offset(0.4, 1.0, o)
  let overlap = params.increment * offset(0.1, inner, o)
  let (ap1, cp1) = compute_ellipse_points(
    params.increment,
    x,
    y,
    params.rx,
    params.ry,
    1.0,
    overlap,
    o,
  )
  let o1 = curve_ops(ap1, o)
  if !o.disable_multi_stroke && o.roughness != 0.0 {
    let (ap2, _) = compute_ellipse_points(
      params.increment,
      x,
      y,
      params.rx,
      params.ry,
      1.5,
      0.0,
      o,
    )
    o1.append(curve_ops(ap2, o))
  }
  { estimated_points: cp1, opset: { kind: Path, ops: o1, }, }
}

///|
fn arc(
  x : Double,
  y : Double,
  width : Double,
  height : Double,
  start : Double,
  stop : Double,
  closed : Bool,
  rough_closure : Bool,
  o : ResolvedOptions,
) -> OpSet {
  let cx = x
  let cy = y
  let mut rx = (width / 2.0).abs()
  let mut ry = (height / 2.0).abs()
  rx += offset_opt(rx * 0.01, o)
  ry += offset_opt(ry * 0.01, o)
  let (strt, stp) = normalize_arc_angles(start, stop)
  let ellipse_inc = @jsmath.PI * 2.0 / o.curve_step_count
  let arc_inc = @jsmath.min(ellipse_inc / 2.0, (stp - strt) / 2.0)
  let ops = arc_ops(arc_inc, cx, cy, rx, ry, strt, stp, 1.0, o)
  if !o.disable_multi_stroke {
    let o2 = arc_ops(arc_inc, cx, cy, rx, ry, strt, stp, 1.5, o)
    ops.append(o2)
  }
  if closed {
    if rough_closure {
      ops.append(
        double_line(
          cx,
          cy,
          cx + rx * @jsmath.cos(strt),
          cy + ry * @jsmath.sin(strt),
          o,
        ),
      )
      ops.append(
        double_line(
          cx,
          cy,
          cx + rx * @jsmath.cos(stp),
          cy + ry * @jsmath.sin(stp),
          o,
        ),
      )
    } else {
      ops.push(LineTo(cx, cy))
      ops.push(LineTo(cx + rx * @jsmath.cos(strt), cy + ry * @jsmath.sin(strt)))
    }
  }
  { kind: Path, ops, }
}

///|
/// The start / stop normalization shared by `arc` and `patternFillArc`.
fn normalize_arc_angles(start : Double, stop : Double) -> (Double, Double) {
  let mut strt = start
  let mut stp = stop
  while strt < 0.0 {
    strt += @jsmath.PI * 2.0
    stp += @jsmath.PI * 2.0
  }
  if stp - strt > @jsmath.PI * 2.0 {
    strt = 0.0
    stp = @jsmath.PI * 2.0
  }
  (strt, stp)
}

///|
fn svg_path(
  path : StringView,
  o : ResolvedOptions,
) -> OpSet raise @path_data.PathError {
  let segments = @path_data.normalize(
    @path_data.absolutize(@path_data.parse_path(path)),
  )
  let ops = []
  let mut first = (0.0, 0.0)
  let mut current = (0.0, 0.0)
  for segment in segments {
    let data = segment.data
    match segment.key {
      'M' => {
        current = (data[0], data[1])
        first = (data[0], data[1])
      }
      'L' => {
        ops.append(double_line(current.0, current.1, data[0], data[1], o))
        current = (data[0], data[1])
      }
      'C' => {
        let x1 = data[0]
        let y1 = data[1]
        let x2 = data[2]
        let y2 = data[3]
        let x = data[4]
        let y = data[5]
        ops.append(bezier_to(x1, y1, x2, y2, x, y, current, o))
        current = (x, y)
      }
      'Z' => {
        ops.append(double_line(current.0, current.1, first.0, first.1, o))
        current = (first.0, first.1)
      }
      _ => ()
    }
  }
  { kind: Path, ops, }
}

// Fills

///|
fn solid_fill_polygon(
  polygon_list : Array[Array[Point]],
  o : ResolvedOptions,
) -> OpSet {
  let ops = []
  for points in polygon_list {
    if points.length() > 0 {
      let offset = js_or(o.max_randomness_offset, 0.0)
      let len = points.length()
      if len > 2 {
        let mx = points[0].x + offset_opt(offset, o)
        let my = points[0].y + offset_opt(offset, o)
        ops.push(Move(mx, my))
        for i in 1.. OpSet {
  fill_polygons(polygon_list, o)
}

///|
fn pattern_fill_arc(
  x : Double,
  y : Double,
  width : Double,
  height : Double,
  start : Double,
  stop : Double,
  o : ResolvedOptions,
) -> OpSet {
  let cx = x
  let cy = y
  let mut rx = (width / 2.0).abs()
  let mut ry = (height / 2.0).abs()
  rx += offset_opt(rx * 0.01, o)
  ry += offset_opt(ry * 0.01, o)
  let (strt, stp) = normalize_arc_angles(start, stop)
  let increment = (stp - strt) / o.curve_step_count
  let points = []
  // rough.js loops forever when the increment is zero or negative.
  if increment.is_nan() || increment > 0.0 {
    for angle = strt; angle <= stp; angle = angle + increment {
      points.push(
        Point::new(cx + rx * @jsmath.cos(angle), cy + ry * @jsmath.sin(angle)),
      )
    }
  }
  points.push(
    Point::new(cx + rx * @jsmath.cos(stp), cy + ry * @jsmath.sin(stp)),
  )
  points.push(Point::new(cx, cy))
  pattern_fill_polygons([points], o)
}

///|
/// `doubleLineOps` of the filler helper: a double line using
/// `disable_multi_stroke_fill`.
fn double_line_fill_ops(
  x1 : Double,
  y1 : Double,
  x2 : Double,
  y2 : Double,
  o : ResolvedOptions,
) -> Array[Op] {
  double_line(x1, y1, x2, y2, o, filling=true)
}

// Private helpers

///|
/// JavaScript `x || fallback` for a number.
fn js_or(x : Double, fallback : Double) -> Double {
  if truthy(x) {
    x
  } else {
    fallback
  }
}

///|
/// `cloneOptionsAlterSeed`: a copy without randomizer, seeded with seed + 1.
fn clone_options_alter_seed(ops : ResolvedOptions) -> ResolvedOptions {
  {
    ..ops,
    randomizer: None,
    seed: if ops.seed != 0 {
      ops.seed + 1
    } else {
      ops.seed
    },
  }
}

///|
/// `random(ops)`: creates the options' randomizer on first use.
fn random(ops : ResolvedOptions) -> Double {
  match ops.randomizer {
    Some(r) => r.next()
    None => {
      let r = @random.Random::new(ops.seed, source=ops.source)
      ops.randomizer = Some(r)
      r.next()
    }
  }
}

///|
fn offset(
  min : Double,
  max : Double,
  ops : ResolvedOptions,
  roughness_gain~ : Double = 1.0,
) -> Double {
  let r = random(ops)
  ops.roughness * roughness_gain * (r * (max - min) + min)
}

///|
fn offset_opt(
  x : Double,
  ops : ResolvedOptions,
  roughness_gain~ : Double = 1.0,
) -> Double {
  offset(-x, x, ops, roughness_gain~)
}

///|
fn double_line(
  x1 : Double,
  y1 : Double,
  x2 : Double,
  y2 : Double,
  o : ResolvedOptions,
  filling~ : Bool = false,
) -> Array[Op] {
  let single_stroke = if filling {
    o.disable_multi_stroke_fill
  } else {
    o.disable_multi_stroke
  }
  let o1 = line_ops(x1, y1, x2, y2, o, true, false)
  if single_stroke {
    return o1
  }
  let o2 = line_ops(x1, y1, x2, y2, o, true, true)
  [..o1, ..o2]
}

///|
fn line_ops(
  x1 : Double,
  y1 : Double,
  x2 : Double,
  y2 : Double,
  o : ResolvedOptions,
  with_move : Bool,
  overlay : Bool,
) -> Array[Op] {
  let dx = x1 - x2
  let dy = y1 - y2
  let length_sq = dx * dx + dy * dy
  let length = length_sq.sqrt()
  let roughness_gain = if length < 200.0 {
    1.0
  } else if length > 500.0 {
    0.4
  } else {
    -0.0016668 * length + 1.233334
  }
  let mut offset = js_or(o.max_randomness_offset, 0.0)
  if offset * offset * 100.0 > length_sq {
    offset = length / 10.0
  }
  let half_offset = offset / 2.0
  let diverge_point = 0.2 + random(o) * 0.2
  let mut mid_disp_x = o.bowing * o.max_randomness_offset * (y2 - y1) / 200.0
  let mut mid_disp_y = o.bowing * o.max_randomness_offset * (x1 - x2) / 200.0
  mid_disp_x = offset_opt(mid_disp_x, o, roughness_gain~)
  mid_disp_y = offset_opt(mid_disp_y, o, roughness_gain~)
  let ops = []
  let random_half = fn() { offset_opt(half_offset, o, roughness_gain~) }
  let random_full = fn() { offset_opt(offset, o, roughness_gain~) }
  let preserve_vertices = o.preserve_vertices
  if with_move {
    if overlay {
      let mx = x1 + (if preserve_vertices { 0.0 } else { random_half() })
      let my = y1 + (if preserve_vertices { 0.0 } else { random_half() })
      ops.push(Move(mx, my))
    } else {
      let mx = x1 + (if preserve_vertices { 0.0 } else { random_full() })
      let my = y1 + (if preserve_vertices { 0.0 } else { random_full() })
      ops.push(Move(mx, my))
    }
  }
  let rnd = if overlay { random_half } else { random_full }
  let c1x = mid_disp_x + x1 + (x2 - x1) * diverge_point + rnd()
  let c1y = mid_disp_y + y1 + (y2 - y1) * diverge_point + rnd()
  let c2x = mid_disp_x + x1 + 2.0 * (x2 - x1) * diverge_point + rnd()
  let c2y = mid_disp_y + y1 + 2.0 * (y2 - y1) * diverge_point + rnd()
  let ex = x2 + (if preserve_vertices { 0.0 } else { rnd() })
  let ey = y2 + (if preserve_vertices { 0.0 } else { rnd() })
  ops.push(BCurveTo(c1x, c1y, c2x, c2y, ex, ey))
  ops
}

///|
fn curve_with_offset(
  points : Array[Point],
  offset : Double,
  o : ResolvedOptions,
) -> Array[Op] {
  if points.length() == 0 {
    return []
  }
  let ps = []
  // The first point twice, then every point, the last one twice.
  for _ in 0..<2 {
    let x = points[0].x + offset_opt(offset, o)
    let y = points[0].y + offset_opt(offset, o)
    ps.push(Point::new(x, y))
  }
  for i in 1.. Array[Op] {
  let len = points.length()
  let ops = []
  if len > 3 {
    let s = 1.0 - o.curve_tightness
    ops.push(Move(points[1].x, points[1].y))
    for i = 1; i + 2 < len; i = i + 1 {
      let cached = points[i]
      let b1x = cached.x + (s * points[i + 1].x - s * points[i - 1].x) / 6.0
      let b1y = cached.y + (s * points[i + 1].y - s * points[i - 1].y) / 6.0
      let b2x = points[i + 1].x + (s * points[i].x - s * points[i + 2].x) / 6.0
      let b2y = points[i + 1].y + (s * points[i].y - s * points[i + 2].y) / 6.0
      ops.push(BCurveTo(b1x, b1y, b2x, b2y, points[i + 1].x, points[i + 1].y))
    }
  } else if len == 3 {
    ops.push(Move(points[1].x, points[1].y))
    ops.push(
      BCurveTo(
        points[1].x,
        points[1].y,
        points[2].x,
        points[2].y,
        points[2].x,
        points[2].y,
      ),
    )
  } else if len == 2 {
    ops.append(
      line_ops(
        points[0].x,
        points[0].y,
        points[1].x,
        points[1].y,
        o,
        true,
        true,
      ),
    )
  }
  ops
}

///|
fn compute_ellipse_points(
  increment : Double,
  cx : Double,
  cy : Double,
  rx : Double,
  ry : Double,
  offset : Double,
  overlap : Double,
  o : ResolvedOptions,
) -> (Array[Point], Array[Point]) {
  let core_only = o.roughness == 0.0
  let core_points = []
  let all_points = []
  if core_only {
    let increment = increment / 4.0
    all_points.push(
      Point::new(
        cx + rx * @jsmath.cos(-increment),
        cy + ry * @jsmath.sin(-increment),
      ),
    )
    // rough.js loops forever when the increment is zero or negative.
    if increment.is_nan() || increment > 0.0 {
      for angle = 0.0; angle <= @jsmath.PI * 2.0; angle = angle + increment {
        let p = Point::new(
          cx + rx * @jsmath.cos(angle),
          cy + ry * @jsmath.sin(angle),
        )
        core_points.push(p)
        all_points.push(p)
      }
    }
    all_points.push(
      Point::new(cx + rx * @jsmath.cos(0.0), cy + ry * @jsmath.sin(0.0)),
    )
    all_points.push(
      Point::new(
        cx + rx * @jsmath.cos(increment),
        cy + ry * @jsmath.sin(increment),
      ),
    )
  } else {
    let rad_offset = offset_opt(0.5, o) - @jsmath.PI / 2.0
    let x0 = offset_opt(offset, o) +
      cx +
      0.9 * rx * @jsmath.cos(rad_offset - increment)
    let y0 = offset_opt(offset, o) +
      cy +
      0.9 * ry * @jsmath.sin(rad_offset - increment)
    all_points.push(Point::new(x0, y0))
    let end_angle = @jsmath.PI * 2.0 + rad_offset - 0.01
    if increment.is_nan() || increment > 0.0 {
      for angle = rad_offset; angle < end_angle; angle = angle + increment {
        let x = offset_opt(offset, o) + cx + rx * @jsmath.cos(angle)
        let y = offset_opt(offset, o) + cy + ry * @jsmath.sin(angle)
        let p = Point::new(x, y)
        core_points.push(p)
        all_points.push(p)
      }
    }
    let x1 = offset_opt(offset, o) +
      cx +
      rx * @jsmath.cos(rad_offset + @jsmath.PI * 2.0 + overlap * 0.5)
    let y1 = offset_opt(offset, o) +
      cy +
      ry * @jsmath.sin(rad_offset + @jsmath.PI * 2.0 + overlap * 0.5)
    all_points.push(Point::new(x1, y1))
    let x2 = offset_opt(offset, o) +
      cx +
      0.98 * rx * @jsmath.cos(rad_offset + overlap)
    let y2 = offset_opt(offset, o) +
      cy +
      0.98 * ry * @jsmath.sin(rad_offset + overlap)
    all_points.push(Point::new(x2, y2))
    let x3 = offset_opt(offset, o) +
      cx +
      0.9 * rx * @jsmath.cos(rad_offset + overlap * 0.5)
    let y3 = offset_opt(offset, o) +
      cy +
      0.9 * ry * @jsmath.sin(rad_offset + overlap * 0.5)
    all_points.push(Point::new(x3, y3))
  }
  (all_points, core_points)
}

///|
fn arc_ops(
  increment : Double,
  cx : Double,
  cy : Double,
  rx : Double,
  ry : Double,
  strt : Double,
  stp : Double,
  offset : Double,
  o : ResolvedOptions,
) -> Array[Op] {
  let rad_offset = strt + offset_opt(0.1, o)
  let points = []
  let x0 = offset_opt(offset, o) +
    cx +
    0.9 * rx * @jsmath.cos(rad_offset - increment)
  let y0 = offset_opt(offset, o) +
    cy +
    0.9 * ry * @jsmath.sin(rad_offset - increment)
  points.push(Point::new(x0, y0))
  // rough.js loops forever when the increment is zero or negative.
  if increment.is_nan() || increment > 0.0 {
    for angle = rad_offset; angle <= stp; angle = angle + increment {
      let x = offset_opt(offset, o) + cx + rx * @jsmath.cos(angle)
      let y = offset_opt(offset, o) + cy + ry * @jsmath.sin(angle)
      points.push(Point::new(x, y))
    }
  }
  points.push(
    Point::new(cx + rx * @jsmath.cos(stp), cy + ry * @jsmath.sin(stp)),
  )
  points.push(
    Point::new(cx + rx * @jsmath.cos(stp), cy + ry * @jsmath.sin(stp)),
  )
  curve_ops(points, o)
}

///|
fn bezier_to(
  x1 : Double,
  y1 : Double,
  x2 : Double,
  y2 : Double,
  x : Double,
  y : Double,
  current : (Double, Double),
  o : ResolvedOptions,
) -> Array[Op] {
  let ops = []
  let ros = [
    js_or(o.max_randomness_offset, 1.0),
    js_or(o.max_randomness_offset, 1.0) + 0.3,
  ]
  let iterations = if o.disable_multi_stroke { 1 } else { 2 }
  let preserve_vertices = o.preserve_vertices
  for i in 0..