///|
/// PDF object serialization functions.

///|
fn trim_trailing_zeros(value : String) -> String {
  let dot = value.find(".")
  match dot {
    None => value
    Some(_) => {
      let chars = value[:]
      let mut end = chars.length() - 1
      while end >= 0 && chars[end] == '0' {
        end = end - 1
      }
      if end >= 0 && chars[end] == '.' {
        end = end - 1
      }
      if end < 0 {
        "0"
      } else {
        slice_range(value, 0, end + 1)
      }
    }
  }
}

///|
/// Slice a string from `start` to its end. Returns "" if `start` is
/// out of bounds. This used to wrap the slice in `try … catch`, but
/// MoonBit's native backend aborts instead of raising on
/// out-of-bounds indices, so the catch was dead code.
fn slice_from(value : String, start : Int) -> String {
  if start < 0 || start > value.length() {
    return ""
  }
  value[start:].to_owned()
}

///|
/// Slice `value[start:end]` with an explicit bounds check. Returns ""
/// if either end of the range is out of bounds. See `slice_from` for
/// why we guard explicitly rather than relying on `try … catch`.
fn slice_range(value : String, start : Int, end : Int) -> String {
  let len = value.length()
  if start < 0 || start > len || end < start || end > len {
    return ""
  }
  value[start:end].to_owned()
}

///|
fn parse_signed_int(value : String) -> Int {
  let chars = value[:]
  if chars.length() == 0 {
    return 0
  }
  let mut sign = 1
  let mut i = 0
  let first = chars[0]
  if first is '-' {
    sign = -1
    i = 1
  } else if first is '+' {
    i = 1
  }
  let mut result = 0
  while i < chars.length() {
    let c = chars[i]
    if c < '0' || c > '9' {
      break
    }
    result = result * 10 + (c.to_int() - '0')
    i = i + 1
  }
  sign * result
}

///|
fn expand_scientific(value : String) -> String {
  let (sign, body) = match value.get_char(0) {
    Some('-') => ("-", slice_from(value, 1))
    _ => ("", value)
  }
  let exp_index = match body.find("e") {
    Some(i) => Some(i)
    None => body.find("E")
  }
  match exp_index {
    None => sign + trim_trailing_zeros(body)
    Some(idx) => {
      let mantissa = slice_range(body, 0, idx)
      let exp_str = slice_from(body, idx + 1)
      let exp = parse_signed_int(exp_str)
      let digits = []
      let mut int_len = 0
      let mut saw_dot = false
      for ch in mantissa {
        if ch == '.' {
          saw_dot = true
        } else {
          if !saw_dot {
            int_len = int_len + 1
          }
          digits.push(ch)
        }
      }
      if digits.length() == 0 {
        sign + "0"
      } else {
        let new_pos = int_len + exp
        let sb = StringBuilder()
        sb.write_string(sign)
        if new_pos <= 0 {
          sb.write_string("0.")
          for _ in 0..<-new_pos {
            sb.write_char('0')
          }
          for ch in digits {
            sb.write_char(ch)
          }
        } else if new_pos >= digits.length() {
          for ch in digits {
            sb.write_char(ch)
          }
          for _ in 0..<(new_pos - digits.length()) {
            sb.write_char('0')
          }
        } else {
          for i in 0.. String {
  if value == 0.0 {
    return "0"
  }
  let raw = value.to_string()
  if raw.contains("e") || raw.contains("E") {
    expand_scientific(raw)
  } else {
    trim_trailing_zeros(raw)
  }
}

///|
fn is_delimiter_byte(b : Int) -> Bool {
  match b {
    b if b == '(' => true
    b if b == ')' => true
    b if b == '<' => true
    b if b == '>' => true
    b if b == '[' => true
    b if b == ']' => true
    b if b == '{' => true
    b if b == '}' => true
    b if b == '/' => true
    b if b == '%' => true
    _ => false
  }
}

///|
fn hex_digit(value : Int) -> Char {
  match value {
    0 => '0'
    1 => '1'
    2 => '2'
    3 => '3'
    4 => '4'
    5 => '5'
    6 => '6'
    7 => '7'
    8 => '8'
    9 => '9'
    10 => 'A'
    11 => 'B'
    12 => 'C'
    13 => 'D'
    14 => 'E'
    15 => 'F'
    _ => '0'
  }
}

///|
fn needs_processing_name(bytes : Array[Byte]) -> Bool {
  if bytes.length() < 2 {
    return false
  }
  for i in 1.. 126 || is_delimiter_byte(b) || b == '#' {
      return true
    }
  }
  false
}

///|
fn make_pdf_name(name : String) -> String {
  let normalized = match name.get_char(0) {
    Some('/') => name
    _ => {
      @pdfe.warn("warning: bad name |\{name}|")
      "/" + name
    }
  }
  let bytes = @pdfio.bytes_of_string(normalized)
  if !needs_processing_name(bytes) {
    return normalized
  }
  let sb = StringBuilder()
  sb.write_char('/')
  for i in 1.. 126 || is_delimiter_byte(b) || b == '#' {
      sb.write_char('#')
      sb.write_char(hex_digit(b / 16))
      sb.write_char(hex_digit(b % 16))
    } else {
      sb.write_char(b.unsafe_to_char())
    }
  }
  sb.to_string()
}

///|
fn make_pdf_string(value : String) -> String {
  let sb = StringBuilder()
  sb.write_char('(')
  for ch in value {
    match ch {
      '(' | ')' | '\\' => {
        sb.write_char('\\')
        sb.write_char(ch)
      }
      '\n' => sb.write_string("\\n")
      '\r' => sb.write_string("\\r")
      '\t' => sb.write_string("\\t")
      '\b' => sb.write_string("\\b")
      '\u000C' => sb.write_string("\\f")
      _ => sb.write_char(ch)
    }
  }
  sb.write_char(')')
  sb.to_string()
}

///|
fn write_pdf_object(
  obj : PdfObject,
  include_stream_data : Bool,
  warn_on_toget : Bool,
  sb : StringBuilder,
) -> Unit {
  match obj {
    Null => sb.write_string("null")
    Boolean(b) => sb.write_string(if b { "true" } else { "false" })
    Integer(i) => sb.write_string(i.to_string())
    Real(r) => sb.write_string(format_real(r))
    PdfString(s) => sb.write_string(make_pdf_string(s))
    Name(n) => sb.write_string(make_pdf_name(n.to_string_bytes()))
    PdfArray(values) => {
      sb.write_char('[')
      for i in 0.. 0 {
          sb.write_char(' ')
        }
        write_pdf_object(values[i], include_stream_data, warn_on_toget, sb)
      }
      sb.write_char(']')
    }
    Dictionary(entries) => {
      sb.write_string("<<")
      for i in 0.. 0 {
          sb.write_char(' ')
        }
        sb.write_string(make_pdf_name(key))
        sb.write_char(' ')
        write_pdf_object(value, include_stream_data, warn_on_toget, sb)
      }
      sb.write_string(">>")
    }
    Stream(r) => {
      let (dict, _) = r.val
      write_pdf_object(dict, include_stream_data, warn_on_toget, sb)
      sb.write_string("\nstream\n")
      if include_stream_data {
        let _ = obj.getstream() catch { _ => () }
        match r.val {
          (_, Got(bytes)) => sb.write_string(@pdfio.string_of_bytes(bytes))
          _ =>
            if warn_on_toget {
              @pdfe.warn("WARNING: toget in string_of_pdfobj_including_data")
            }
        }
      }
      sb.write_string("\nendstream")
    }
    Indirect(n) => sb.write_string("\{n} 0 R")
  }
}

///|
/// Convert a PDF object to its string representation.
pub fn PdfObject::to_string(self : PdfObject) -> String {
  let sb = StringBuilder()
  write_pdf_object(self, false, false, sb)
  sb.to_string()
}

///|
/// Convert a PDF object to string including stream data.
pub fn PdfObject::to_string_including_data(self : PdfObject) -> String {
  let sb = StringBuilder()
  write_pdf_object(self, true, true, sb)
  sb.to_string()
}

///|
test "serialize helpers handle trimming and slicing" {
  assert_eq(trim_trailing_zeros("123"), "123")
  assert_eq(trim_trailing_zeros("1.23000"), "1.23")
  assert_eq(trim_trailing_zeros("0.000"), "0")
  assert_eq(slice_from("x", 2), "")
  assert_eq(slice_range("x", 0, 5), "")
  assert_eq(parse_signed_int(""), 0)
  assert_eq(parse_signed_int("+12x"), 12)
  assert_eq(parse_signed_int("-3"), -3)
}

///|
test "expand_scientific formats exponent strings" {
  assert_eq(expand_scientific("-1.2300"), "-1.23")
  assert_eq(expand_scientific("e1"), "0")
  assert_eq(expand_scientific("1e-3"), "0.001")
  assert_eq(expand_scientific("1e3"), "1000")
  assert_eq(expand_scientific("12.34e1"), "123.4")
}