// PDF backend: interpret the backend-neutral `DrawCmd` list (see draw.mbt) into
// a PDF content stream. Shares the AST fold with the SVG/canvas backends.
//
// PDF has a y-up coordinate system, so every y is flipped to `height - y`.
// Limitations of this basic backend: gradients are approximated by a flat
// representative colour, group opacity is ignored, and quadratic curves / arcs
// are approximated (matching the existing PDF primitives).

///|
/// Render the image to a single-page PDF document of the given size.
pub fn Image::to_pdf(self : Image, width : Double, height : Double) -> String {
  let cmds = self.to_draw_list(width, height)
  let sb = StringBuilder::new()
  for cmd in cmds {
    pdf_cmd(cmd, width, height, sb)
  }
  @pdf.PdfDocument::PdfDocument(width, height)
  .add_raw(sb.to_string())
  .to_string()
}

///|
fn pdf_cmd(
  cmd : DrawCmd,
  width : Double,
  height : Double,
  sb : StringBuilder,
) -> Unit {
  match cmd {
    // a fully transparent fill paints nothing (this basic backend renders
    // partial alpha as opaque)
    FillPath(path, paint, area) => {
      let c = paint_color(paint)
      if c.a > 0.0 {
        match area {
          // stroke the outline with width w (round caps/joins)
          Outline(w) => {
            sb.write_string("q\n\{c.r} \{c.g} \{c.b} RG\n\{w} w\n1 J\n1 j\n")
            sb.write_string(pdf_path(path, height))
            sb.write_string("S\nQ\n")
          }
          _ => {
            sb.write_string("q\n\{pdf_color(c)}\n")
            sb.write_string(pdf_path(path, height))
            sb.write_string("\{pdf_fill_op(area)}\nQ\n")
          }
        }
      }
    }
    FillViewport(paint) =>
      // an infinite colour field fills the whole page
      if paint_color(paint).a > 0.0 {
        sb.write_string(
          "q\n\{pdf_fill_color(paint)}\n0 0 \{width} \{height} re\nf\nQ\n",
        )
      }
    RasterCell(x, y, w, h, c) =>
      if c.a > 0.0 {
        // re takes the lower-left corner in PDF's y-up space
        let yb = height - (y + h)
        sb.write_string("q\n\{pdf_color(c)}\n\{x} \{yb} \{w} \{h} re\nf\nQ\n")
      }
    // clip the following commands to the path's area (W / W*), then `n`
    PushClip(path, area) => {
      let clip = match area {
        Anz => "W n"
        Aeo => "W* n"
        Outline(_) => "W n" // outline cuts are stroked/rasterised, not clipped
      }
      sb.write_string("q\n\{pdf_path(path, height)}\{clip}\n")
    }
    PopClip => sb.write_string("Q\n")
    // group opacity is not expressed by this basic backend; keep q/Q balanced
    PushOpacity(_) => sb.write_string("q\n")
    PopOpacity => sb.write_string("Q\n")
    DrawText(content, x, y, size, color) => {
      // approximate centre alignment to match SVG/canvas (Helvetica is ~0.5em
      // per glyph; vertical centre ~0.35em above the baseline)
      let tx = x - content.length().to_double() * size * 0.5 / 2.0
      let ty = height - y - size * 0.35
      sb.write_string(
        "q\nBT\n/F1 \{size} Tf\n\{color.r} \{color.g} \{color.b} rg\n\{tx} \{ty} Td\n(\{pdf_text_escape(content)}) Tj\nET\nQ\n",
      )
    }
  }
}

///|
/// Escape a string for a PDF literal string `( ... )`.
fn pdf_text_escape(s : String) -> String {
  let parts : Array[String] = []
  for ch in s {
    parts.push(
      match ch {
        '\\' => "\\\\"
        '(' => "\\("
        ')' => "\\)"
        _ => ch.to_string()
      },
    )
  }
  parts.join("")
}

///|
fn pdf_fill_op(area : Area) -> String {
  match area {
    Anz => "f"
    Aeo => "f*"
    Outline(_) => "f" // strokes are handled before pdf_fill_op
  }
}

///|
fn pdf_color(c : Color) -> String {
  "\{c.r} \{c.g} \{c.b} rg"
}

///|
/// A flat representative colour for a paint (gradients are approximated).
fn paint_color(paint : Paint) -> Color {
  match paint {
    Solid(c) => c
    Linear(stops, _, _) => avg_stops(stops)
    Radial(stops, _, _) => avg_stops(stops)
  }
}

///|
fn pdf_fill_color(paint : Paint) -> String {
  pdf_color(paint_color(paint))
}

///|
fn avg_stops(stops : Array[Stop]) -> Color {
  guard stops.length() > 0 else { @color.black() }
  @color.lerp_color(stops[0].color, stops[stops.length() - 1].color, 0.5)
}

///|
/// A (baked) path as PDF path-construction operators, with y flipped.
fn pdf_path(path : Path, height : Double) -> String {
  let parts : Array[String] = []
  let mut cur = Point(0.0, 0.0)
  let mut start = Point(0.0, 0.0)
  for seg in path.0 {
    match seg {
      MoveTo(p) => {
        parts.push("\{p.x} \{height - p.y} m")
        cur = p
        start = p
      }
      LineTo(p) => {
        parts.push("\{p.x} \{height - p.y} l")
        cur = p
      }
      CurveTo(c1, c2, e) => {
        parts.push(
          "\{c1.x} \{height - c1.y} \{c2.x} \{height - c2.y} \{e.x} \{height - e.y} c",
        )
        cur = e
      }
      // quadratic -> cubic by repeating the control point (approximation)
      QCurveTo(c, e) => {
        parts.push(
          "\{c.x} \{height - c.y} \{c.x} \{height - c.y} \{e.x} \{height - e.y} c",
        )
        cur = e
      }
      EArcTo(rx, ry, rot, la, sw, e) => {
        for pt in flatten_arc(cur, rx, ry, rot, la, sw, e) {
          parts.push("\{pt.x} \{height - pt.y} l")
        }
        cur = e
      }
      Close => {
        parts.push("h")
        cur = start
      }
    }
  }
  parts.join("\n") + "\n"
}