///|
/// Deterministic GLB construction and container verification.
///
/// Importers often need a normalized GLB artifact after validation. This
/// writer keeps JSON/BIN chunk alignment explicit, uses UTF-8 encoding without
/// host dependencies, and validates the generated container with the same
/// boundary rules as the reader.

///|
pub(all) suberror BinaryWriteError {
  EmptyJson
  InvalidJsonPadding
  ChunkTooLarge(length~ : Int)
  InvalidGlbLength
  Utf8Encoding
} derive(Debug, Eq)

///|
pub impl Show for BinaryWriteError with fn output(self, logger) {
  match self {
    EmptyJson => logger.write_string("BinaryWriteError: JSON chunk is empty")
    InvalidJsonPadding =>
      logger.write_string("BinaryWriteError: JSON chunk has invalid padding")
    ChunkTooLarge(length~) =>
      logger.write_string("BinaryWriteError: chunk is too large: \{length}")
    InvalidGlbLength =>
      logger.write_string("BinaryWriteError: GLB length is invalid")
    Utf8Encoding =>
      logger.write_string("BinaryWriteError: UTF-8 encoding failed")
  }
}

///|
pub(all) struct GlbBuildOptions {
  json_padding : Char
  bin_padding : Byte
  include_empty_bin : Bool
} derive(Debug, ToJson, Eq)

///|
pub(all) struct GlbBuildReport {
  bytes : Bytes
  json_bytes : Int
  bin_bytes : Int
  json_padding_bytes : Int
  bin_padding_bytes : Int
  total_bytes : Int
  has_bin : Bool
  checksum : UInt
  valid : Bool
  errors : Array[String]
} derive(Debug, ToJson, Eq)

///|
pub(all) struct GlbChunkInfo {
  index : Int
  kind : String
  offset : Int
  length : Int
  valid : Bool
} derive(Debug, ToJson, Eq)

///|
pub fn default_glb_build_options() -> GlbBuildOptions {
  { json_padding: ' ', bin_padding: 0, include_empty_bin: false }
}

///|
pub fn glb_padding_length(length : Int) -> Int {
  if length < 0 {
    0
  } else {
    (4 - length % 4) % 4
  }
}

///|
pub fn glb_padded_length(length : Int) -> Int {
  length + glb_padding_length(length)
}

///|
pub fn glb_json_padding(json : String, padding : Char) -> String {
  let count = glb_padding_length(json.length())
  if count == 0 {
    json
  } else {
    json + String::make(count, padding)
  }
}

///|
pub fn glb_binary_padding(bin : Bytes, padding : Byte) -> Bytes {
  let count = glb_padding_length(bin.length())
  if count == 0 {
    bin
  } else {
    Bytes::add(bin, Bytes::make(count, padding))
  }
}

///|
pub fn utf8_encode_string(value : String) -> Bytes {
  let bytes : Array[Byte] = []
  for character in value {
    let code = character.to_int()
    if code < 0x80 {
      bytes.push(code.to_byte())
    } else if code < 0x800 {
      bytes.push((0xC0 | (code >> 6)).to_byte())
      bytes.push((0x80 | (code & 0x3F)).to_byte())
    } else {
      bytes.push((0xE0 | (code >> 12)).to_byte())
      bytes.push((0x80 | ((code >> 6) & 0x3F)).to_byte())
      bytes.push((0x80 | (code & 0x3F)).to_byte())
    }
  }
  Bytes::from_array(bytes)
}

///|
pub fn u32_to_le_bytes(value : UInt) -> Bytes {
  Bytes::from_array([
    (value & 0xFF).to_byte(),
    ((value >> 8) & 0xFF).to_byte(),
    ((value >> 16) & 0xFF).to_byte(),
    ((value >> 24) & 0xFF).to_byte(),
  ])
}

///|
pub fn glb_header_bytes(total_length : Int) -> Bytes {
  Bytes::add(
    Bytes::add(u32_to_le_bytes(0x46546C67), u32_to_le_bytes(2)),
    u32_to_le_bytes(total_length.reinterpret_as_uint()),
  )
}

///|
pub fn glb_chunk_bytes(chunk_type : UInt, data : Bytes) -> Bytes {
  Bytes::add(
    u32_to_le_bytes(data.length().reinterpret_as_uint()),
    Bytes::add(u32_to_le_bytes(chunk_type), data),
  )
}

///|
pub fn glb_json_chunk(json : String, padding : Char) -> Bytes {
  glb_chunk_bytes(
    0x4E4F534A,
    utf8_encode_string(glb_json_padding(json, padding)),
  )
}

///|
pub fn glb_bin_chunk(bin : Bytes, padding : Byte) -> Bytes {
  glb_chunk_bytes(0x004E4942, glb_binary_padding(bin, padding))
}

///|
pub fn build_glb(
  json : String,
  bin : Bytes,
  options? : GlbBuildOptions = default_glb_build_options(),
) -> Bytes raise BinaryWriteError {
  if json == "" {
    raise BinaryWriteError::EmptyJson
  }
  let json_chunk = glb_json_chunk(json, options.json_padding)
  let has_bin = bin.length() > 0 || options.include_empty_bin
  let bin_chunk = if has_bin {
    glb_bin_chunk(bin, options.bin_padding)
  } else {
    Bytes::from_array([])
  }
  let total = 12 + json_chunk.length() + bin_chunk.length()
  if total < 12 {
    raise BinaryWriteError::InvalidGlbLength
  }
  Bytes::add(glb_header_bytes(total), Bytes::add(json_chunk, bin_chunk))
}

///|
pub fn glb_chunk_infos(data : Bytes) -> Array[GlbChunkInfo] {
  let result : Array[GlbChunkInfo] = []
  if data.length() < 12 {
    result
  } else {
    let mut offset = 12
    let mut index = 0
    while offset + 8 <= data.length() {
      let length = read_u32_le(data, offset).reinterpret_as_int() catch {
        _ => 0
      }
      let chunk_type = read_u32_le(data, offset + 4) catch { _ => 0 }
      let kind = if chunk_type == 0x4E4F534A {
        "JSON"
      } else if chunk_type == 0x004E4942 {
        "BIN"
      } else {
        "UNKNOWN"
      }
      let valid = length >= 0 && offset + 8 + length <= data.length()
      result.push({ index, kind, offset, length, valid })
      if length <= 0 {
        break
      }
      offset += 8 + length
      index += 1
    }
    result
  }
}

///|
pub fn glb_has_json_chunk(data : Bytes) -> Bool {
  glb_chunk_infos(data).fold(init=false, (found, chunk) => {
    found || chunk.kind == "JSON"
  })
}

///|
pub fn glb_has_bin_chunk(data : Bytes) -> Bool {
  glb_chunk_infos(data).fold(init=false, (found, chunk) => {
    found || chunk.kind == "BIN"
  })
}

///|
pub fn glb_chunk_count(data : Bytes) -> Int {
  glb_chunk_infos(data).length()
}

///|
pub fn validate_glb_container(data : Bytes) -> Array[String] {
  let errors : Array[String] = []
  if data.length() < 12 {
    errors.push("GLB header is shorter than 12 bytes")
    errors
  } else {
    let magic = read_u32_le(data, 0) catch { _ => 0 }
    let version = read_u32_le(data, 4).reinterpret_as_int() catch { _ => 0 }
    let total = read_u32_le(data, 8).reinterpret_as_int() catch { _ => 0 }
    if magic != 0x46546C67 {
      errors.push("GLB magic is invalid")
    }
    if version != 2 {
      errors.push("GLB version is not 2")
    }
    if total != data.length() {
      errors.push("GLB declared length does not match input length")
    }
    let chunks = glb_chunk_infos(data)
    if chunks.length() == 0 {
      errors.push("GLB contains no complete chunks")
    }
    if !glb_has_json_chunk(data) {
      errors.push("GLB has no JSON chunk")
    }
    let mut seen_bin = false
    for chunk in chunks {
      if !chunk.valid {
        errors.push("GLB chunk exceeds input bounds")
      }
      if chunk.kind == "BIN" {
        if seen_bin {
          errors.push("GLB contains multiple BIN chunks")
        }
        seen_bin = true
      }
    }
    errors
  }
}

///|
pub fn build_glb_report(
  json : String,
  bin : Bytes,
  options? : GlbBuildOptions = default_glb_build_options(),
) -> GlbBuildReport {
  try {
    let bytes = build_glb(json, bin, options~)
    let json_bytes = utf8_encode_string(json).length()
    let padded_json = glb_padded_length(json_bytes)
    let padded_bin = glb_padded_length(bin.length())
    let errors = validate_glb_container(bytes)
    {
      bytes,
      json_bytes,
      bin_bytes: bin.length(),
      json_padding_bytes: padded_json - json_bytes,
      bin_padding_bytes: padded_bin - bin.length(),
      total_bytes: bytes.length(),
      has_bin: glb_has_bin_chunk(bytes),
      checksum: binary_checksum(bytes),
      valid: errors.length() == 0,
      errors,
    }
  } catch {
    error =>
      {
        bytes: Bytes::from_array([]),
        json_bytes: 0,
        bin_bytes: 0,
        json_padding_bytes: 0,
        bin_padding_bytes: 0,
        total_bytes: 0,
        has_bin: false,
        checksum: 0,
        valid: false,
        errors: ["\{error}"],
      }
  }
}

///|
pub fn glb_json_byte_count(data : Bytes) -> Int {
  glb_chunk_infos(data).fold(init=0, (sum, chunk) => {
    if chunk.kind == "JSON" {
      sum + chunk.length
    } else {
      sum
    }
  })
}

///|
pub fn glb_bin_byte_count(data : Bytes) -> Int {
  glb_chunk_infos(data).fold(init=0, (sum, chunk) => {
    if chunk.kind == "BIN" {
      sum + chunk.length
    } else {
      sum
    }
  })
}

///|
pub fn glb_alignment_overhead(data : Bytes) -> Int {
  glb_chunk_infos(data).fold(init=0, (sum, chunk) => {
    sum + glb_padding_length(chunk.length)
  })
}

///|
pub fn glb_report_summary_lines(report : GlbBuildReport) -> Array[String] {
  [
    "valid=\{report.valid}, total_bytes=\{report.total_bytes}",
    "json_bytes=\{report.json_bytes}, bin_bytes=\{report.bin_bytes}",
    "json_padding=\{report.json_padding_bytes}, bin_padding=\{report.bin_padding_bytes}",
    "checksum=\{report.checksum}, has_bin=\{report.has_bin}",
  ]
}