///|
// Prawn's graphics (`Prawn::Graphics`): the graphics state, paths built in
// Prawn's coordinates (y up, relative to the bounds' bottom left), and
// painting them, drawn as pagelayout graphics in page space.

///|

///|
/// The graphics state Prawn keeps (`PDF::Core::GraphicState`).
priv struct GraphicState {
  mut fill : Color
  mut stroke : Color
  mut line_width : Double
  mut cap : @pagelayout.LineCap
  mut join : @pagelayout.LineJoin
  mut dash : Array[Double]
  mut dash_phase : Double
}

///|
fn GraphicState::new() -> GraphicState {
  {
    fill: Color::rgb(0, 0, 0),
    stroke: Color::rgb(0, 0, 0),
    line_width: 1.0,
    cap: ButtCap,
    join: MiterJoin,
    dash: [],
    dash_phase: 0.0,
  }
}

///|
fn GraphicState::copy(self : GraphicState) -> GraphicState {
  { ..self, dash: self.dash.copy(), }
}

///|
/// The current page's graphics state (each page has its own; see
/// `Document::new`).
fn Document::gs(self : Document) -> GraphicState {
  let stack = self.page().stack
  // a stack restored past its bottom (a block that started on another
  // page) starts again from the defaults
  if stack.is_empty() {
    stack.push(GraphicState::new())
  }
  stack[stack.length() - 1]
}

///|
/// The current page's path being built.
fn Document::path(self : Document) -> Array[@pagelayout.PathSegment] {
  self.page().path
}

///|
/// Adds a segment to the current page's path (a close with nothing to
/// close does nothing).
fn Document::add_segment(
  self : Document,
  segment : @pagelayout.PathSegment,
) -> Unit {
  // content too (pdf-core's `add_content` makes a graphics state)
  self.gs() |> ignore
  let path = self.path()
  if segment is Close && path.is_empty() {
    return
  }
  // kept as drawn on the page: Prawn's path is in the coordinates of the
  // transformation it is built under
  path.append(map_segment(segment, self.page().ctm))
}

///|
/// Prawn's `KAPPA`: the control point distance, as a fraction of the
/// radius, of the four Bézier curves that make a circle.
let kappa : Double = 4.0 * ((2.0.sqrt() - 1.0) / 3.0)

///|
/// A colour Prawn names in hex (`"FFCC00"`: six digits, no `#`).
pub fn color_of_hex(hex : String) -> Color raise PrawnError {
  let bad = ArgumentError("Unknown type of color: \"\{hex}\"")
  guard hex.length() == 6 else { raise bad }
  let digit = fn(c : Char) -> Int? {
    match c {
      '0'..='9' => Some(c.to_int() - '0'.to_int())
      'a'..='f' => Some(c.to_int() - 'a'.to_int() + 10)
      'A'..='F' => Some(c.to_int() - 'A'.to_int() + 10)
      _ => None
    }
  }
  let parts : Array[Int] = []
  for i in 0..<3 {
    guard digit(hex[2 * i].unsafe_to_char()) is Some(hi) &&
      digit(hex[2 * i + 1].unsafe_to_char()) is Some(lo) else {
      raise bad
    }
    parts.push(hi * 16 + lo)
  }
  Color::rgb(parts[0], parts[1], parts[2])
}

///|
/// Prawn's `fill_color=`: what fills and text are painted with.
pub fn Document::set_fill_color(self : Document, color : Color) -> Unit {
  self.gs().fill = color
}

///|
/// Prawn's `stroke_color=`.
pub fn Document::set_stroke_color(self : Document, color : Color) -> Unit {
  self.gs().stroke = color
}

///|
/// Prawn's `line_width=`.
pub fn Document::set_line_width(self : Document, width : Double) -> Unit {
  self.gs().line_width = width
}

///|
/// Prawn's `cap_style=`.
pub fn Document::set_cap_style(
  self : Document,
  cap : @pagelayout.LineCap,
) -> Unit {
  self.gs().cap = cap
}

///|
/// Prawn's `join_style=`.
pub fn Document::set_join_style(
  self : Document,
  join : @pagelayout.LineJoin,
) -> Unit {
  self.gs().join = join
}

///|
/// Prawn's `dash`: dashes `lengths` long (one length: dashes that long with
/// gaps `space`, by default as long), starting `phase` into the pattern.
pub fn Document::dash(
  self : Document,
  lengths : Array[Double],
  space? : Double,
  phase? : Double = 0.0,
) -> Unit raise PrawnError {
  if lengths.iter().all(x => x == 0.0) {
    raise ArgumentError(
      "Zero length dashes are invalid. Call #undash to disable dashes.",
    )
  }
  if lengths.iter().any(x => x < 0.0) {
    raise ArgumentError("Negative numbers are not allowed for dash lengths.")
  }
  self.gs().dash = match lengths {
    [length] => [length, space.unwrap_or(length)]
    _ => lengths
  }
  self.gs().dash_phase = phase
}

///|
/// Prawn's `undash`.
pub fn Document::undash(self : Document) -> Unit {
  self.gs().dash = []
  self.gs().dash_phase = 0.0
}

///|
/// A point of the bounds (Prawn's `map_to_absolute`) on the page, in
/// pagelayout's space (y down from the page's top).
fn Document::page_point(
  self : Document,
  x : Double,
  y : Double,
) -> (Double, Double) {
  let box = self.current_box()
  let bottom = box.top - box.height_at(self.y())
  (box.left + x, self.page_h() - (bottom + y))
}

///|
/// Prawn's `move_to`.
pub fn Document::move_to(self : Document, x : Double, y : Double) -> Unit {
  let (px, py) = self.page_point(x, y)
  self.add_segment(MoveTo(px, py))
}

///|
/// Prawn's `line_to`.
pub fn Document::line_to(self : Document, x : Double, y : Double) -> Unit {
  let (px, py) = self.page_point(x, y)
  self.add_segment(LineTo(px, py))
}

///|
/// Prawn's `curve_to`: a Bézier curve to `dest` with control points
/// `bounds`.
pub fn Document::curve_to(
  self : Document,
  dest : (Double, Double),
  bounds : ((Double, Double), (Double, Double)),
) -> Unit {
  let (x1, y1) = self.page_point(bounds.0.0, bounds.0.1)
  let (x2, y2) = self.page_point(bounds.1.0, bounds.1.1)
  let (x3, y3) = self.page_point(dest.0, dest.1)
  self.add_segment(CurveTo(x1, y1, x2, y2, x3, y3))
}

///|
/// Prawn's `rectangle`: `point` is the top left corner.
pub fn Document::rectangle(
  self : Document,
  point : (Double, Double),
  width : Double,
  height : Double,
) -> Unit {
  let (x, y) = self.page_point(point.0, point.1)
  self.add_segment(Rectangle(x, y, width, height))
}

///|
/// Prawn's `line`.
pub fn Document::line(
  self : Document,
  from : (Double, Double),
  to : (Double, Double),
) -> Unit {
  self.move_to(from.0, from.1)
  self.line_to(to.0, to.1)
}

///|
/// Prawn's `horizontal_line`: from `x1` to `x2` at `at` (by default the
/// cursor).
pub fn Document::horizontal_line(
  self : Document,
  x1 : Double,
  x2 : Double,
  at? : Double,
) -> Unit {
  let y = at.unwrap_or(self.cursor())
  self.line((x1, y), (x2, y))
}

///|
/// Prawn's `horizontal_rule`: across the bounds at the cursor.
pub fn Document::horizontal_rule(self : Document) -> Unit {
  self.horizontal_line(0.0, self.bounds().width)
}

///|
/// Prawn's `vertical_line`.
pub fn Document::vertical_line(
  self : Document,
  y1 : Double,
  y2 : Double,
  at~ : Double,
) -> Unit {
  self.line((at, y1), (at, y2))
}

///|
/// Prawn's `curve`.
pub fn Document::curve(
  self : Document,
  origin : (Double, Double),
  dest : (Double, Double),
  bounds : ((Double, Double), (Double, Double)),
) -> Unit {
  self.move_to(origin.0, origin.1)
  self.curve_to(dest, bounds)
}

///|
/// Prawn's `ellipse`: four Bézier curves around `center`, then a move to
/// the center.
pub fn Document::ellipse(
  self : Document,
  center : (Double, Double),
  r1 : Double,
  r2? : Double,
) -> Unit {
  let r2 = r2.unwrap_or(r1)
  let (x, y) = center
  let l1 = r1 * kappa
  let l2 = r2 * kappa
  self.move_to(x + r1, y)
  self.curve_to((x, y + r2), ((x + r1, y + l2), (x + l1, y + r2)))
  self.curve_to((x - r1, y), ((x - l1, y + r2), (x - r1, y + l2)))
  self.curve_to((x, y - r2), ((x - r1, y - l2), (x - l1, y - r2)))
  self.curve_to((x + r1, y), ((x + l1, y - r2), (x + r1, y - l2)))
  self.move_to(x, y)
}

///|
/// Prawn's `circle`.
pub fn Document::circle(
  self : Document,
  center : (Double, Double),
  radius : Double,
) -> Unit {
  self.ellipse(center, radius, r2=radius)
}

///|
/// Prawn's `polygon`: closed.
pub fn Document::polygon(
  self : Document,
  points : Array[(Double, Double)],
) -> Unit {
  guard points.length() > 0 else { return }
  self.move_to(points[0].0, points[0].1)
  for p in points[1:] {
    self.line_to(p.0, p.1)
  }
  self.line_to(points[0].0, points[0].1)
  self.add_segment(Close)
}

///|
/// Prawn's `point_on_line`: the point `distance` short of the end of the
/// line from `p0` to `p1`.
fn point_on_line(
  distance : Double,
  p0 : (Double, Double),
  p1 : (Double, Double),
) -> (Double, Double) {
  let (x0, y0) = p0
  let (x1, y1) = p1
  let length = ((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)).sqrt()
  let p = (length - distance) / length
  (x0 + p * (x1 - x0), y0 + p * (y1 - y0))
}

///|
/// Prawn's `rounded_vertex`.
fn Document::rounded_vertex(
  self : Document,
  radius : Double,
  p0 : (Double, Double),
  p1 : (Double, Double),
  p2 : (Double, Double),
) -> Unit {
  let radial1 = point_on_line(radius, p0, p1)
  let bezier1 = point_on_line(radius - radius * kappa, p0, p1)
  let radial2 = point_on_line(radius, p2, p1)
  let bezier2 = point_on_line(radius - radius * kappa, p2, p1)
  self.line_to(radial1.0, radial1.1)
  self.curve_to(radial2, (bezier1, bezier2))
}

///|
/// Prawn's `rounded_polygon`: corners rounded with `radius`, closed.
pub fn Document::rounded_polygon(
  self : Document,
  radius : Double,
  points : Array[(Double, Double)],
) -> Unit {
  guard points.length() >= 2 else { return }
  let start = point_on_line(radius, points[1], points[0])
  self.move_to(start.0, start.1)
  let ring = [..points, points[0], points[1]]
  for i in 0.. Unit {
  let (x, y) = point
  self.rounded_polygon(radius, [
    point,
    (x + width, y),
    (x + width, y - height),
    (x, y - height),
  ])
}

///|
/// Prawn's `close_path`.
pub fn Document::close_path(self : Document) -> Unit {
  self.add_segment(Close)
}

///|
/// Paints the path built so far and starts a new one.
fn Document::paint(
  self : Document,
  fill? : Bool = false,
  stroke? : Bool = false,
  even_odd? : Bool = false,
) -> Unit {
  // the paint operator is content: pdf-core makes a graphics state for it
  // when the stack has none (`add_content`), even for an empty path
  self.gs() |> ignore
  // in the coordinates of the transformation it is painted under (the
  // group around it applies that)
  let back = inverse(self.page().ctm)
  let segments = self
    .path()
    .iter()
    .flat_map(s => map_segment(s, back).iter())
    .to_array()
  self.path().clear()
  guard segments.length() > 0 else { return }
  let gs = self.gs()
  self.page().items.push(
    Graphic({
      x_pt: 0.0,
      y_pt: 0.0,
      ops: [
        Path({
          segments,
          fill: if fill {
            Some(Solid(gs.fill))
          } else {
            None
          },
          fill_rule: if even_odd {
            EvenOdd
          } else {
            NonZero
          },
          stroke: if stroke {
            Some(Solid(gs.stroke))
          } else {
            None
          },
          stroke_style: {
            width: if gs.line_width == 0.0 {
              Hairline
            } else {
              Width(gs.line_width)
            },
            cap: gs.cap,
            join: gs.join,
            miter_limit: 10.0,
            dash: gs.dash.copy(),
            dash_phase: gs.dash_phase,
          },
        }),
      ],
    }),
  )
}

///|
/// Prawn's `stroke`.
pub fn Document::stroke(self : Document) -> Unit {
  self.paint(stroke=true)
}

///|
/// Prawn's `close_and_stroke`.
pub fn Document::close_and_stroke(self : Document) -> Unit {
  self.add_segment(Close)
  self.paint(stroke=true)
}

///|
/// Prawn's `fill`.
pub fn Document::fill(self : Document, even_odd? : Bool = false) -> Unit {
  self.paint(fill=true, even_odd~)
}

///|
/// Prawn's `fill_and_stroke`: PDF's `b`, which closes the current
/// subpath before it fills and strokes.
pub fn Document::fill_and_stroke(
  self : Document,
  even_odd? : Bool = false,
) -> Unit {
  if !(self.path().last() is Some(Close)) {
    self.add_segment(Close)
  }
  self.paint(fill=true, stroke=true, even_odd~)
}

///|
/// Prawn's `stroke_bounds`.
pub fn Document::stroke_bounds(self : Document) -> Unit {
  let b = self.bounds()
  self.rectangle((0.0, b.height), b.width, b.height)
  self.stroke()
}