///|
/// ICC profile 头部(128 字节)与 tag 表的解析。
///
/// 布局依据 ICC.1:2022 / ISO 15076-1 第 7 章。全部字段为大端序。
///
/// 头部偏移表(字节):
///   0   size            u32     profile 总字节数
///   4   cmmType         u32     preferred CMM(0 表示未指定,无约束)
///   8   version         u32     (major.4)(minor.4)(bug.4)(reserved.4)
///   12  deviceClass     4 字节  'scnr'/'mntr'/'prtr'/'link'/'spac'/'abst'/'nmcl'
///   16  colorSpace      4 字节  输入色空间,如 'RGB '/'CMYK'/'GRAY'/'Lab '
///   20  pcs             4 字节  'XYZ ' 或 'Lab '
///   24  dateTime        12 字节 6×u16: 年/月/日/时/分/秒
///   36  signature       4 字节  必须是 'acsp'
///   40  platform        4 字节
///   44  flags           u32
///   48  manufacturer    u32
///   52  model           u32
///   56  attributes      u64
///   64  renderingIntent u32
///   68  illuminant      12 字节 3×s15Fixed16 (PCS 白点, 通常 D50)
///   80  creator         u32
///   84  profileId       16 字节 (MD5, 可为全 0)
///   100 reserved        28 字节
///   128 tagCount        u32
///   132 tagTable        tagCount × 12 字节 {sig:4, offset:u32, size:u32}

///|
/// 头部固定长度。
pub let header_size : Int32 = 128

///|
/// 每个 tag 表项固定长度。
pub let tag_entry_size : Int32 = 12

///|
/// `acsp` 文件签名。
pub let icc_signature : String = "acsp"

///|
/// profile 的日期时间字段(6 个 u16)。
pub struct IccDateTime {
  year : Int32
  month : Int32
  day : Int32
  hour : Int32
  minute : Int32
  second : Int32
}

///|
/// 用 ISO 8601 风格格式化日期时间。
pub fn IccDateTime::to_iso_string(self : IccDateTime) -> String {
  let two = fn(n : Int32) -> String {
    if n < 10 {
      "0" + n.to_string()
    } else {
      n.to_string()
    }
  }
  self.year.to_string() +
  "-" +
  two(self.month) +
  "-" +
  two(self.day) +
  "T" +
  two(self.hour) +
  ":" +
  two(self.minute) +
  ":" +
  two(self.second)
}

///|
/// 三刺激值(XYZ 或 Lab 的三分量)。
pub struct XyzNumber {
  x : Double
  y : Double
  z : Double
}

///|
/// 解析后的 profile 头部。
pub struct IccHeader {
  /// 声明的 profile 总字节数。
  size : Int32
  /// preferred CMM 类型(0 表示未指定)。
  cmm_type : Int32
  /// 主版本号。
  major_version : Int32
  /// 次版本号。
  minor_version : Int32
  /// profile 类别,如 `mntr`。
  device_class : String
  /// 输入色空间,如 `RGB `。
  color_space : String
  /// PCS,`XYZ ` 或 `Lab `。
  pcs : String
  /// 创建时间。
  date_time : IccDateTime
  /// 平台签名。
  platform : String
  /// 标志位。
  flags : Int32
  /// 制造商签名。
  manufacturer : Int32
  /// 设备型号。
  model : Int32
  /// 设备属性(u64,见 ICC.1 表 21)。
  attributes : Int32
  /// 默认渲染意图。
  rendering_intent : Int32
  /// PCS 照明体(通常 D50)。
  illuminant : XyzNumber
  /// 创建者签名。
  creator : Int32
  /// profile ID(16 字节 MD5,可为全 0)。
  profile_id : Bytes
}

///|
/// tag 表中的一个条目。
pub struct IccTagEntry {
  /// tag 签名,如 `rXYZ`。
  signature : String
  /// 相对文件起始的偏移。
  offset : Int32
  /// 数据字节数(含 4 字节类型签名与 4 字节保留位)。
  size : Int32
}

///|
/// 渲染意图的可读名称。未知值返回 `"unknown"`。
pub fn rendering_intent_name(intent : Int32) -> String {
  match intent {
    0 => "perceptual"
    1 => "relative-colorimetric"
    2 => "saturation"
    3 => "absolute-colorimetric"
    _ => "unknown"
  }
}

///|
/// 设备类别为显示器时返回 true。
pub fn IccHeader::is_display(self : IccHeader) -> Bool {
  self.device_class == "mntr"
}

///|
/// 解析 128 字节头部。
///
/// 会校验 `acsp` 签名;`size` 与实际字节数不一致时报 `SizeMismatch`
/// (某些第三方 profile 会留空洞,故仅对"声明值小于实际值"严格报错,
/// 见下方注释)。
pub fn parse_header(data : Bytes) -> IccHeader raise IccError {
  if data.length() < header_size {
    raise Truncated(expected=header_size, offset=0, actual=data.length())
  }
  let r = BigEndianReader::new(data)
  let size = r.read_u32()
  // 声明尺寸可以小于实际文件(尾部填充),但不可大于实际数据,
  // 否则后续 tag 偏移都不可信。
  if size > data.length() {
    raise SizeMismatch(declared=size, actual=data.length())
  }
  let cmm_type = r.read_u32()
  let version = r.read_u32()
  let major_version = (version >> 24) & 0xFF
  let minor_version = (version >> 20) & 0x0F
  let device_class = r.read_signature()
  let color_space = r.read_signature()
  let pcs = r.read_signature()
  let date_time = {
    let year = r.read_u16()
    let month = r.read_u16()
    let day = r.read_u16()
    let hour = r.read_u16()
    let minute = r.read_u16()
    let second = r.read_u16()
    { year, month, day, hour, minute, second, }
  }
  let signature = r.read_signature()
  if signature != icc_signature {
    raise BadSignature(found=signature)
  }
  let platform = r.read_signature()
  let flags = r.read_u32()
  let manufacturer = r.read_u32()
  let model = r.read_u32()
  let attributes = r.read_u64()
  let rendering_intent = r.read_u32()
  let illuminant = {
    let x = r.read_s15f16()
    let y = r.read_s15f16()
    let z = r.read_s15f16()
    { x, y, z, }
  }
  let creator = r.read_u32()
  let profile_id = r.read_bytes(16)
  // 100..128 为 28 字节保留区,按规范必须为 0;此处不强制校验,
  // 因为存在未清零的历史 profile,强制校验会拒掉合法文件。
  {
    size,
    cmm_type,
    major_version,
    minor_version,
    device_class,
    color_space,
    pcs,
    date_time,
    platform,
    flags,
    manufacturer,
    model,
    attributes,
    rendering_intent,
    illuminant,
    creator,
    profile_id,
  }
}

///|
/// 解析 tag 表。
///
/// 每个条目的 `offset + size` 必须落在 profile 数据范围内,否则报
/// `TagOutOfBounds`。返回的条目顺序保持文件中的原始顺序。
pub fn parse_tag_table(
  data : Bytes,
  header : IccHeader,
) -> Array[IccTagEntry] raise IccError {
  let table_start = header_size
  if data.length() < table_start + 4 {
    raise Truncated(
      expected=4,
      offset=table_start,
      actual=data.length() - table_start,
    )
  }
  let r = BigEndianReader::new(data)
  r.seek(table_start)
  let count = r.read_u32()
  // 每个条目 12 字节,条目数必须能装进剩余空间。
  let need = count * tag_entry_size
  if count < 0 || need / tag_entry_size != count {
    raise OutOfRange(what="tag_count", value=count)
  }
  let available = data.length() - (table_start + 4)
  if need > available {
    raise BadTagTable(count~, offset=table_start + 4)
  }
  let entries : Array[IccTagEntry] = []
  for i = 0; i < count; i = i + 1 {
    let signature = r.read_signature()
    let offset = r.read_u32()
    let size = r.read_u32()
    // 越界检查以"声明尺寸"为准:某些 profile 的 size 字段会大于实际文件,
    // 此时用实际可用长度做上界,避免把合法文件误判为损坏。
    let limit = if header.size < data.length() {
      header.size
    } else {
      data.length()
    }
    if offset < 0 || size < 0 || offset + size > limit {
      raise TagOutOfBounds(tag=signature, offset~, size~)
    }
    entries.push({ signature, offset, size, })
  }
  entries
}