///|
// Prawn's transformations and transparency (`Prawn::Graphics::
// Transformation`, `Transparency`): a block whose content is drawn under a
// changed coordinate system or opacity. Like Prawn's `q … cm … Q`, what the
// block draws on the page is wrapped in one pagelayout graphic group.

///|
/// pagelayout's page space is y down from the top of a page `height` tall;
/// Prawn's is y up. `flip` maps one to the other (it is its own inverse).
fn flip(height : Double) -> @pagelayout.Matrix {
  { a: 1.0, b: 0.0, c: 0.0, d: -1.0, e: 0.0, f: height, }
}

///|
/// A page item as ops of a graphic group in page space; None for those
/// that draw nothing (links and anchors stay page items).
fn item_ops(item : @pagelayout.PageItem) -> Array[@pagelayout.GraphicOp]? {
  match item {
    Text(run) => Some([Text(run)])
    Image(image) => Some([Image(image)])
    // kept in a save and restore of its own, as pagelayout draws each graphic
    Graphic(g) if g.x_pt == 0.0 && g.y_pt == 0.0 =>
      Some([Save, ..g.ops, Restore])
    Graphic(g) =>
      Some(
        [
          Save,
          Transform({ ..@pagelayout.identity, e: g.x_pt, f: g.y_pt, }),
          ..g.ops,
          Restore,
        ],
      )
    Rect(r) =>
      Some([
        Path({
          segments: [Rectangle(r.x_pt, r.y_pt, r.w_pt, r.h_pt)],
          fill: r.fill.map(c => Solid(c)),
          fill_rule: NonZero,
          stroke: r.stroke.map(c => Solid(c)),
          stroke_style: {
            ..@pagelayout.StrokeStyle::default(),
            width: Width(r.stroke_w_pt),
          },
        }),
      ])
    Link(_) | Anchor(_) => None
  }
}

///|
/// `body`, with what it draws on the current page wrapped in a group that
/// starts with `setup` (a transform, an opacity) inside save and restore,
/// which also save and restore the graphics state (colours, line style).
/// What lands on other pages is drawn as it is: a new page starts without
/// the block's state, and the page it started on stays as the block set it
/// up when it ends on another (Prawn writes its Q there).
fn Document::group(
  self : Document,
  setup : Array[@pagelayout.GraphicOp],
  body : () -> Unit raise,
) -> Unit raise {
  let page = self.page()
  // q: a copy of the graphics state goes on the page's stack (pdf-core's
  // GraphicStateStack), and the page's content saves it
  page.stack.push(
    match page.stack.last() {
      Some(state) => state.copy()
      // none to copy (a stack restored past its bottom): pdf-core pushes
      // the defaults, one state
      None => GraphicState::new()
    },
  )
  page.save(setup)
  // a block that raises writes no Q: its page stays as it set it up
  body()
  // Q, on the page current now (Prawn's is, wherever the block ended):
  // its stack pops, one with nothing left to restore (a new page holds one
  // state, so two blocks around a page break empty it) being pdf-core's
  // EmptyGraphicStateStack, and its content restores
  let current = self.page()
  if current.stack.is_empty() {
    raise EmptyGraphicStateStack
  }
  current.stack.pop() |> ignore
  current.restore()
}

///|
/// Prawn's `transformation_matrix` with a block: `body` drawn under the
/// matrix `[a, b, c, d, e, f]` in Prawn's page space (y up).
pub fn Document::transformation_matrix(
  self : Document,
  matrix : (Double, Double, Double, Double, Double, Double),
  body : () -> Unit raise,
) -> Unit raise {
  let (a, b, c, d, e, f) = matrix
  let f_ = flip(self.page_h())
  let m : @pagelayout.Matrix = { a, b, c, d, e, f, }
  self.group([Transform(f_.after(m.after(f_)))], body)
}

///|
/// Prawn's `translate` with a block.
pub fn Document::translate(
  self : Document,
  x : Double,
  y : Double,
  body : () -> Unit raise,
) -> Unit raise {
  self.transformation_matrix((1.0, 0.0, 0.0, 1.0, x, y), body)
}

///|
/// The origin `(x, y)` of the bounds on the page (Prawn's
/// `bounds.absolute_left/bottom` plus the point).
fn Document::absolute(
  self : Document,
  point : (Double, Double),
) -> (Double, Double) {
  let box = self.current_box()
  (box.left + point.0, box.top - box.height_at(self.y()) + point.1)
}

///|
/// Prawn's `rotate` with a block: `angle` degrees counterclockwise, about
/// the page's origin or `origin` (a point of the bounds).
pub fn Document::rotate(
  self : Document,
  angle : Double,
  origin? : (Double, Double),
  body : () -> Unit raise,
) -> Unit raise {
  let rad = angle * @math.PI / 180.0
  let cos = @math.cos(rad)
  let sin = @math.sin(rad)
  match origin {
    None => self.transformation_matrix((cos, sin, -sin, cos, 0.0, 0.0), body)
    Some(o) => {
      let (x, y) = self.absolute(o)
      let x_prime = x * cos - y * sin
      let y_prime = x * sin + y * cos
      self.translate(x - x_prime, y - y_prime, () => {
        self.transformation_matrix((cos, sin, -sin, cos, 0.0, 0.0), body)
      })
    }
  }
}

///|
/// Prawn's `scale` with a block, about the page's origin or `origin`.
pub fn Document::scale(
  self : Document,
  factor : Double,
  origin? : (Double, Double),
  body : () -> Unit raise,
) -> Unit raise {
  match origin {
    None =>
      self.transformation_matrix((factor, 0.0, 0.0, factor, 0.0, 0.0), body)
    Some(o) => {
      let (x, y) = self.absolute(o)
      self.translate(x - factor * x, y - factor * y, () => {
        self.transformation_matrix((factor, 0.0, 0.0, factor, 0.0, 0.0), body)
      })
    }
  }
}

///|
/// Prawn's `transparent`: `body` drawn with fill opacity `opacity` and
/// stroke opacity `stroke_opacity` (by default the same), each clamped to
/// 0 to 1.
pub fn Document::transparent(
  self : Document,
  opacity : Double,
  stroke_opacity? : Double,
  body : () -> Unit raise,
) -> Unit raise {
  let clamp = fn(x : Double) {
    if x < 0.0 {
      0.0
    } else if x > 1.0 {
      1.0
    } else {
      x
    }
  }
  let fill = clamp(opacity)
  let stroke = clamp(stroke_opacity.unwrap_or(opacity))
  self.group([Alpha(fill, stroke)], body)
}

///|
/// Prawn's `save_graphics_state` with a block: what the block changes in
/// the graphics state is undone after it.
pub fn Document::save_graphics_state(
  self : Document,
  body : () -> Unit raise,
) -> Unit raise {
  self.group([], body)
}

///|
fn is_identity(m : @pagelayout.Matrix) -> Bool {
  m == @pagelayout.identity
}

///|
/// The inverse of an affine matrix.
fn inverse(m : @pagelayout.Matrix) -> @pagelayout.Matrix {
  let det = m.a * m.d - m.b * m.c
  let a = m.d / det
  let b = -m.b / det
  let c = -m.c / det
  let d = m.a / det
  { a, b, c, d, e: -(a * m.e + c * m.f), f: -(b * m.e + d * m.f), }
}

///|
/// A path segment mapped by `m` (a rectangle becomes its four sides, in the
/// order pagelayout draws it, unless `m` is the identity).
fn map_segment(
  segment : @pagelayout.PathSegment,
  m : @pagelayout.Matrix,
) -> Array[@pagelayout.PathSegment] {
  if is_identity(m) {
    return [segment]
  }
  match segment {
    MoveTo(x, y) => {
      let (x, y) = m.apply(x, y)
      [MoveTo(x, y)]
    }
    LineTo(x, y) => {
      let (x, y) = m.apply(x, y)
      [LineTo(x, y)]
    }
    CurveTo(x1, y1, x2, y2, x3, y3) => {
      let (x1, y1) = m.apply(x1, y1)
      let (x2, y2) = m.apply(x2, y2)
      let (x3, y3) = m.apply(x3, y3)
      [CurveTo(x1, y1, x2, y2, x3, y3)]
    }
    Rectangle(x, y, w, h) => {
      // from the bottom left (x, y + h) counterclockwise on the page
      let (x0, y0) = m.apply(x, y + h)
      let (x1, y1) = m.apply(x + w, y + h)
      let (x2, y2) = m.apply(x + w, y)
      let (x3, y3) = m.apply(x, y)
      [MoveTo(x0, y0), LineTo(x1, y1), LineTo(x2, y2), LineTo(x3, y3), Close]
    }
    Close => [Close]
  }
}