// 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()
  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\{pdf_num(c.r)} \{pdf_num(c.g)} \{pdf_num(c.b)} RG\n\{pdf_num(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 \{pdf_num(width)} \{pdf_num(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\{pdf_num(x)} \{pdf_num(yb)} \{pdf_num(w)} \{pdf_num(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 \{pdf_num(size)} Tf\n\{pdf_num(color.r)} \{pdf_num(color.g)} \{pdf_num(color.b)} rg\n\{pdf_num(tx)} \{pdf_num(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 {
  "\{pdf_num(c.r)} \{pdf_num(c.g)} \{pdf_num(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("\{pdf_num(p.x)} \{pdf_num(height - p.y)} m")
        cur = p
        start = p
      }
      LineTo(p) => {
        parts.push("\{pdf_num(p.x)} \{pdf_num(height - p.y)} l")
        cur = p
      }
      CurveTo(c1, c2, e) => {
        parts.push(
          "\{pdf_num(c1.x)} \{pdf_num(height - c1.y)} \{pdf_num(c2.x)} \{pdf_num(height - c2.y)} \{pdf_num(e.x)} \{pdf_num(height - e.y)} c",
        )
        cur = e
      }
      // quadratic -> cubic by repeating the control point (approximation)
      QCurveTo(c, e) => {
        parts.push(
          "\{pdf_num(c.x)} \{pdf_num(height - c.y)} \{pdf_num(c.x)} \{pdf_num(height - c.y)} \{pdf_num(e.x)} \{pdf_num(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("\{pdf_num(pt.x)} \{pdf_num(height - pt.y)} l")
        }
        cur = e
      }
      Close => {
        parts.push("h")
        cur = start
      }
    }
  }
  parts.join("\n") + "\n"
}

///|
/// Format a number for a PDF content stream. PDF has no exponent notation, so
/// values that `Double::to_string` prints as e.g. `2.842170943040401e-14` (a
/// rounding residue) are written as plain decimals; magnitudes below 1e-9 are
/// written as `0`.
fn pdf_num(x : Double) -> String {
  let s = x.to_string()
  guard s.contains("e") else { s }
  if x.abs() < 1.0e-9 {
    return "0"
  }
  // split "[-]d[.ddd]e[+|-]k" into its digits, integer length and exponent
  let chars = s.to_array()
  let digits = StringBuilder()
  let mut ndigits = 0
  let mut int_len = 0
  let mut seen_point = false
  let mut i = if chars[0] == '-' { 1 } else { 0 }
  while chars[i] != 'e' {
    if chars[i] == '.' {
      seen_point = true
    } else {
      digits.write_char(chars[i])
      ndigits += 1
      if !seen_point {
        int_len += 1
      }
    }
    i += 1
  }
  i += 1
  let exp_neg = chars[i] == '-'
  if chars[i] == '-' || chars[i] == '+' {
    i += 1
  }
  let mut exp = 0
  while i < chars.length() {
    exp = exp * 10 + (chars[i].to_int() - '0'.to_int())
    i += 1
  }
  let point = int_len + (if exp_neg { -exp } else { exp })
  let d = digits.to_string()
  let sign = if x < 0.0 { "-" } else { "" }
  if point <= 0 {
    sign + "0." + "0".repeat(-point) + d
  } else if point >= ndigits {
    sign + d + "0".repeat(point - ndigits)
  } else {
    sign +
    d.view(end_offset=point).to_owned() +
    "." +
    d.view(start_offset=point).to_owned()
  }
}