///|
// Prawn's `image` (`images.rb`): a PNG or JPEG file placed at a point, or
// in the flow of the page (at the position, moving it down, to the next
// page when it does not fit), sized as given or by its pixels as points.
// pagelayout embeds the file (pdflite reads it).

///|
/// What Prawn knows of an image: its type and its size in pixels (Prawn's
/// `Prawn::Images::PNG` / `JPG`).
pub(all) struct ImageInfo {
  /// "png" or "jpeg"
  kind : String
  width : Int
  height : Int
}

///|
/// Where an image goes across the bounds, in the flow (Prawn's
/// `:position`).
pub(all) enum ImagePosition {
  ImageLeft
  ImageCenter
  ImageRight
  /// this far from the bounds' left
  ImageAt(Double)
}

///|
/// Where an image goes down the bounds (Prawn's `:vposition`): by default
/// at the position, in the flow of the page.
pub(all) enum ImageVPosition {
  ImageTop
  ImageMiddle
  ImageBottom
  /// this far below the bounds' top
  ImageDown(Double)
}

///|
fn be16(data : Bytes, at : Int) -> Int {
  (data[at].to_int() << 8) | data[at + 1].to_int()
}

///|
fn be32(data : Bytes, at : Int) -> Int {
  (be16(data, at) << 16) | be16(data, at + 2)
}

///|
/// What Prawn knows of the image in `data` (from `file`), checked as Prawn
/// checks it (UnsupportedImageType) and as far as pdflite can embed it
/// faithfully (Unsupported otherwise, rather than drawing it wrongly or
/// not at all).
///
/// Where Prawn itself draws an image wrongly, it is drawn right, not as
/// Prawn draws it: a palette's transparency (Prawn takes the rows' filtered
/// bytes, one a pixel, for the indices), a truecolour PNG with a suggested
/// palette (Prawn draws it as indexed), and a JPEG of components named R, G
/// and B without a JFIF or Adobe marker (Prawn leaves them to be taken for
/// YCbCr).
fn read_image(file : String, data : Bytes) -> ImageInfo raise PrawnError {
  let n = data.length()
  let png_signature = b"\x89PNG\r\n\x1a\n"
  if n >= 8 && data[0:8] == png_signature[:] {
    // the IHDR chunk: size, bit depth, colour type, compression, filter,
    // interlace
    if n < 33 {
      raise UnsupportedImageType("the PNG \{file} is truncated")
    }
    let colour_type = data[25].to_int()
    if data[26] != b'\x00' {
      raise UnsupportedImageType("PNG uses an unsupported compression method")
    }
    if data[27] != b'\x00' {
      raise UnsupportedImageType("PNG uses an unsupported filter method")
    }
    if data[28] != b'\x00' {
      raise UnsupportedImageType("PNG uses unsupported interlace method")
    }
    if ![0, 2, 3, 4, 6].contains(colour_type) {
      raise UnsupportedImageType("PNG uses an unsupported number of colors")
    }
    // embedded as rendering will, its data inflated (a corrupt one
    // refused now, before the position moves)
    try
      @pdflite.pdf_image_object_of_png_data(
        @pdflite.pdf_document_empty(),
        data[:],
      )
    catch {
      _ => raise Unsupported("the image \{file} (a PNG pdflite cannot read)")
    } noraise {
      _ => ()
    }
    return { kind: "png", width: be32(data, 16), height: be32(data, 20), }
  }
  // JPEG: markers up to a start of frame (SOF0–SOF15 but DHT, JPG, DAC)
  guard n >= 4 && data[0] == b'\xFF' && data[1] == b'\xD8' else {
    raise UnsupportedImageType("image file is an unrecognised format")
  }
  let mut i = 2
  // whether Adobe's marker (APP14) has come: Adobe writes CMYK inverted
  let mut adobe = false
  while i + 9 < n {
    if data[i] != b'\xFF' {
      i += 1
      continue
    }
    let marker = data[i + 1].to_int()
    if marker == 0xFF {
      i += 1
      continue
    }
    if marker == 0xEE &&
      be16(data, i + 2) >= 14 &&
      data[i + 4:i + 9] == b"Adobe" {
      adobe = true
    }
    if marker >= 0xC0 &&
      marker <= 0xCF &&
      marker != 0xC4 &&
      marker != 0xC8 &&
      marker != 0xCC {
      // Prawn inverts every CMYK JPEG; pdflite, Adobe's (which are the
      // inverted ones). Another would be drawn in other colours than
      // Prawn's.
      if data[i + 9] == b'\x04' && !adobe {
        raise Unsupported(
          "the image \{file} (a CMYK JPEG without Adobe's marker)",
        )
      }
      // embedded as rendering will (one pdflite cannot embed refused now)
      try @pdflite.pdf_image_object_of_jpeg_data(data[:]) catch {
        _ => raise Unsupported("the image \{file} (a JPEG pdflite cannot read)")
      } noraise {
        _ => ()
      }
      return {
        kind: "jpeg",
        height: be16(data, i + 5),
        width: be16(data, i + 7),
      }
    }
    i += 2 + be16(data, i + 2)
  }
  raise UnsupportedImageType("the JPEG \{file} has no frame")
}

///|
/// Prawn's `calc_image_dimensions`: the size an image is drawn at, in
/// points.
fn ImageInfo::dimensions(
  self : ImageInfo,
  width : Double?,
  height : Double?,
  scale : Double?,
  fit : (Double, Double)?,
) -> (Double, Double) {
  let iw = self.width.to_double()
  let ih = self.height.to_double()
  // in Prawn's order: a width or a height alone, then a scale, then a fit
  // (with both a width and a height given too), then as given
  match (width, height, scale, fit) {
    (Some(w), None, _, _) => (iw * (w / iw), ih * (w / iw))
    (None, Some(h), _, _) => (iw * (h / ih), ih * (h / ih))
    (_, _, Some(s), _) => (iw * s, ih * s)
    (_, _, None, Some((bw, bh))) => {
      let bp = bw / bh
      let ip = iw / ih
      if ip > bp {
        (bw, bw / ip)
      } else {
        (bh * ip, bh)
      }
    }
    (w, h, _, _) => (w.unwrap_or(iw), h.unwrap_or(ih))
  }
}

///|
/// Prawn's `image`: the image in `file` (read through the document's
/// assets) drawn with its top left at `at` (a point of the bounds), or,
/// without `at`, in the flow of the page at `position` across the bounds
/// and `vposition` down them (by default at the position, which moves down
/// past it, to the next page when it does not fit); `width` and `height`
/// (either keeps the proportions), `scale` or `fit` size it, else its
/// pixels are points. What Prawn knows of it is returned.
pub fn Document::image(
  self : Document,
  file : String,
  at? : (Double, Double),
  width? : Double,
  height? : Double,
  scale? : Double,
  fit? : (Double, Double),
  position? : ImagePosition = ImageLeft,
  vposition? : ImageVPosition,
) -> ImageInfo raise PrawnError {
  guard (self.assets)(file) is Some(data) else {
    raise ArgumentError("\{file} not found")
  }
  let info = read_image(file, data)
  let (w, h) = info.dimensions(width, height, scale, fit)
  let (x, y) = match at {
    // a point of the bounds (Prawn's `map_to_absolute`)
    Some((ax, ay)) => {
      let b = self.bounds()
      (b.absolute_left + ax, b.absolute_top - b.height + ay)
    }
    None => {
      let (x, y) = self.image_position(w, h, position, vposition)
      self.move_text_position(h)
      (x, y)
    }
  }
  self.page().items.push(
    Image({
      x_pt: x,
      y_pt: self.page_h() - y,
      w_pt: w,
      h_pt: h,
      data,
      mime: if info.kind == "png" {
        "image/png"
      } else {
        "image/jpeg"
      },
    }),
  )
  info
}

///|
/// Prawn's `image_position`: where an image `width` by `height` placed in
/// the flow has its top left, on the page.
fn Document::image_position(
  self : Document,
  width : Double,
  height : Double,
  position : ImagePosition,
  vposition : ImageVPosition?,
) -> (Double, Double) {
  let box = self.bbox
  let top = box.top
  let bounds_height = box.height_at(self.y())
  let y = match vposition {
    Some(ImageTop) => top
    Some(ImageMiddle) => top - (bounds_height - height) / 2.0
    Some(ImageBottom) => top - bounds_height + height
    Some(ImageDown(down)) => top - down
    None => {
      // Prawn's `determine_y_with_page_flow`: past the bottom of the
      // reference bounds (the nearest fixed box), on to the next column or
      // page
      if self.y() - height < self.reference_bottom() {
        self.move_past_bottom()
      }
      self.y()
    }
  }
  // across the bounds as they are now (a page break may have moved them)
  let (side, bounds_width) = self.bbox.side()
  let x = match position {
    ImageLeft => side
    ImageCenter => side + (bounds_width - width) / 2.0
    ImageRight => self.bbox.right_side() - width
    ImageAt(dx) => side + dx
  }
  (x, y)
}

///|
/// The bottom of Prawn's `reference_bounds`: of the current box, or of the
/// nearest fixed box it is in when it is stretchy.
fn Document::reference_bottom(self : Document) -> Double {
  let mut box = self.bbox
  while box.height is None {
    match box.parent {
      Some(parent) => box = parent
      None => break
    }
  }
  box.top - box.height_at(self.y())
}

///|
/// Prawn's `move_text_position`: down by `amount`, past the bottom of the
/// bounds (of the margin box, in a stretchy box) on to the next column or
/// page first when it does not fit.
fn Document::move_text_position(self : Document, amount : Double) -> Unit {
  let bottom = if self.bbox.height is None {
    self.margin.top - self.margin.height_at(self.y())
  } else {
    self.bbox.top - self.bbox.height_at(self.y())
  }
  if self.y() - amount < bottom {
    self.move_past_bottom()
  }
  self.set_y(self.y() - amount)
}