// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
pub suberror OtLayoutError {
  UnexpectedEof
  InvalidFormat
} derive(Eq, Show, ToJson)

///|
/// Layout table header (GSUB/GPOS shared).
pub struct LayoutHeader {
  version : UInt
  script_list_offset : Int
  feature_list_offset : Int
  lookup_list_offset : Int
  feature_variations_offset : Int?
} derive(Eq, Show, ToJson)

///|
/// Script record entry (offset from table start).
pub struct ScriptRecord {
  tag : @common.Tag
  offset : Int
} derive(Eq, Show, ToJson)

///|
/// Script list table.
pub struct ScriptList {
  records : Array[ScriptRecord]
} derive(Eq, Show, ToJson)

///|
pub fn ScriptList::records(self : ScriptList) -> ArrayView[ScriptRecord] {
  self.records[:]
}

///|
/// Feature record entry (offset from table start).
pub struct FeatureRecord {
  tag : @common.Tag
  offset : Int
} derive(Eq, Show, ToJson)

///|
/// Feature list table.
pub struct FeatureList {
  records : Array[FeatureRecord]
} derive(Eq, Show, ToJson)

///|
pub fn FeatureList::records(self : FeatureList) -> ArrayView[FeatureRecord] {
  self.records[:]
}

///|
/// Lookup list table (offsets from table start).
pub struct LookupList {
  offsets : Array[Int]
} derive(Eq, Show, ToJson)

///|
pub fn LookupList::offsets(self : LookupList) -> ArrayView[Int] {
  self.offsets[:]
}

///|
/// Parsed layout table (GSUB/GPOS shared).
pub struct LayoutTable {
  data : BytesView
  header : LayoutHeader
  script_list : ScriptList
  feature_list : FeatureList
  lookup_list : LookupList
} derive(Show, ToJson)

///|
pub fn LayoutTable::header(self : LayoutTable) -> LayoutHeader {
  self.header
}

///|
pub fn LayoutTable::script_list(self : LayoutTable) -> ScriptList {
  self.script_list
}

///|
pub fn LayoutTable::feature_list(self : LayoutTable) -> FeatureList {
  self.feature_list
}

///|
pub fn LayoutTable::lookup_list(self : LayoutTable) -> LookupList {
  self.lookup_list
}

///|
/// Select lookup offsets for a script/language pair using a feature allowlist.
pub fn LayoutTable::select_lookup_offsets_with_features(
  self : LayoutTable,
  script_tag : @common.Tag,
  language_tag? : @common.Tag = @common.tag_none,
  feature_tags? : Array[@common.Tag] = [],
) -> Result[Array[Int], OtLayoutError] {
  select_lookup_offsets_internal(self, script_tag, language_tag, feature_tags)
}

///|
/// Select lookup offsets for a single feature tag within a script/language pair.
pub fn LayoutTable::select_lookup_offsets_for_feature(
  self : LayoutTable,
  script_tag : @common.Tag,
  feature_tag : @common.Tag,
  language_tag? : @common.Tag = @common.tag_none,
  include_required? : Bool = true,
) -> Result[Array[Int], OtLayoutError] {
  select_lookup_offsets_for_feature_internal(
    self,
    script_tag,
    language_tag,
    feature_tag,
    include_required,
  )
}

///|
/// Select lookup offsets for a script/language pair using the default LangSys.
pub fn LayoutTable::select_lookup_offsets(
  self : LayoutTable,
  script_tag : @common.Tag,
  language_tag? : @common.Tag = @common.tag_none,
) -> Result[Array[Int], OtLayoutError] {
  self.select_lookup_offsets_with_features(script_tag, language_tag=language_tag)
}

///|
fn select_lookup_offsets_internal(
  layout : LayoutTable,
  script_tag : @common.Tag,
  language_tag : @common.Tag,
  feature_tags : Array[@common.Tag],
) -> Result[Array[Int], OtLayoutError] {
  let (required_feature, feature_indices) = match resolve_lang_sys_features(
    layout,
    script_tag,
    language_tag,
  ) {
    Err(err) => return Err(err)
    Ok(value) => value
  }
  let has_filter = !feature_tags.is_empty()
  let selected_features : Array[Int] = []
  match required_feature {
    None => ()
    Some(feature) =>
      if feature >= 0 && feature < layout.feature_list.records.length() {
        push_unique_int(selected_features, feature)
      }
  }
  for feature in feature_indices {
    if !has_filter {
      if feature >= 0 && feature < layout.feature_list.records.length() {
        push_unique_int(selected_features, feature)
      }
    } else {
      if feature < 0 || feature >= layout.feature_list.records.length() {
        continue
      }
      let record = layout.feature_list.records[feature]
      if tag_in_list(record.tag, feature_tags) {
        push_unique_int(selected_features, feature)
      }
    }
  }
  let lookup_indices : Array[Int] = []
  for feature_index in selected_features {
    if feature_index < 0 || feature_index >= layout.feature_list.records.length() {
      continue
    }
    let record = layout.feature_list.records[feature_index]
    let lookup_list = match parse_feature_table(layout.data, record.offset) {
      Err(err) => return Err(err)
      Ok(value) => value
    }
    for lookup_index in lookup_list {
      push_unique_int(lookup_indices, lookup_index)
    }
  }
  let lookup_offsets : Array[Int] = []
  for lookup_index in lookup_indices {
    if lookup_index < 0 || lookup_index >= layout.lookup_list.offsets.length() {
      continue
    }
    lookup_offsets.push(layout.lookup_list.offsets[lookup_index])
  }
  Ok(lookup_offsets)
}

///|
fn select_lookup_offsets_for_feature_internal(
  layout : LayoutTable,
  script_tag : @common.Tag,
  language_tag : @common.Tag,
  feature_tag : @common.Tag,
  include_required : Bool,
) -> Result[Array[Int], OtLayoutError] {
  let (required_feature, feature_indices) = match resolve_lang_sys_features(
    layout,
    script_tag,
    language_tag,
  ) {
    Err(err) => return Err(err)
    Ok(value) => value
  }
  let selected_features : Array[Int] = []
  if include_required {
    match required_feature {
      None => ()
      Some(feature) =>
        if feature >= 0 && feature < layout.feature_list.records.length() {
          push_unique_int(selected_features, feature)
        }
    }
  }
  for feature_index in feature_indices {
    if feature_index < 0 || feature_index >= layout.feature_list.records.length() {
      continue
    }
    let record = layout.feature_list.records[feature_index]
    if record.tag == feature_tag {
      push_unique_int(selected_features, feature_index)
    }
  }
  let lookup_indices : Array[Int] = []
  for feature_index in selected_features {
    if feature_index < 0 || feature_index >= layout.feature_list.records.length() {
      continue
    }
    let record = layout.feature_list.records[feature_index]
    let lookup_list = match parse_feature_table(layout.data, record.offset) {
      Err(err) => return Err(err)
      Ok(value) => value
    }
    for lookup_index in lookup_list {
      push_unique_int(lookup_indices, lookup_index)
    }
  }
  let lookup_offsets : Array[Int] = []
  for lookup_index in lookup_indices {
    if lookup_index < 0 || lookup_index >= layout.lookup_list.offsets.length() {
      continue
    }
    lookup_offsets.push(layout.lookup_list.offsets[lookup_index])
  }
  Ok(lookup_offsets)
}

///|
fn resolve_lang_sys_features(
  layout : LayoutTable,
  script_tag : @common.Tag,
  language_tag : @common.Tag,
) -> Result[(Int?, Array[Int]), OtLayoutError] {
  let normalized_script = lowercase_tag(script_tag)
  let script_offset = match find_script_offset(layout.script_list.records, normalized_script) {
    Some(offset) => Some(offset)
    None => if normalized_script != script_tag {
        find_script_offset(layout.script_list.records, script_tag)
      } else {
        None
      }
  }
  let script_offset = match script_offset {
    Some(offset) => Some(offset)
    None => find_script_offset(
      layout.script_list.records,
      @common.Tag::from_chars('D', 'F', 'L', 'T'),
    )
  }
  let resolved_script_offset = match script_offset {
    Some(offset) => offset
    None =>
      if layout.script_list.records.is_empty() {
        return Ok((None, []))
      } else {
        layout.script_list.records[0].offset
      }
  }
  let (default_lang, lang_records) = match parse_script_table(
    layout.data,
    resolved_script_offset,
  ) {
    Err(err) => return Err(err)
    Ok(value) => value
  }
  let lang_offset = if language_tag != @common.tag_none {
    find_lang_sys_offset(lang_records, language_tag)
  } else {
    None
  }
  let resolved_lang_offset = match lang_offset {
    Some(offset) => Some(offset)
    None => default_lang
  }
  let final_lang_offset = match resolved_lang_offset {
    Some(offset) => offset
    None =>
      if lang_records.is_empty() {
        return Ok((None, []))
      } else {
        lang_records[0].1
      }
  }
  parse_lang_sys_table(layout.data, final_lang_offset)
}

///|
/// GSUB table wrapper.
pub struct GsubTable {
  layout : LayoutTable
} derive(Show, ToJson)

///|
pub fn GsubTable::parse(blob : @blob.Blob) -> Result[GsubTable, OtLayoutError] {
  match LayoutTable::parse(blob) {
    Err(err) => Err(err)
    Ok(layout) => Ok(GsubTable::{ layout })
  }
}

///|
pub fn GsubTable::layout(self : GsubTable) -> LayoutTable {
  self.layout
}

///|
/// GPOS table wrapper.
pub struct GposTable {
  layout : LayoutTable
} derive(Show, ToJson)

///|
pub fn GposTable::parse(blob : @blob.Blob) -> Result[GposTable, OtLayoutError] {
  match LayoutTable::parse(blob) {
    Err(err) => Err(err)
    Ok(layout) => Ok(GposTable::{ layout })
  }
}

///|
pub fn GposTable::layout(self : GposTable) -> LayoutTable {
  self.layout
}

///|
/// Parse a GSUB/GPOS layout table.
pub fn LayoutTable::parse(
  blob : @blob.Blob,
) -> Result[LayoutTable, OtLayoutError] {
  let data = blob.as_view()
  let version = read_u32(data, 0)
  let script_offset = read_u16_int(data, 4)
  let feature_offset = read_u16_int(data, 6)
  let lookup_offset = read_u16_int(data, 8)
  match (version, script_offset, feature_offset, lookup_offset) {
    (Err(err), _, _, _) => Err(err)
    (_, Err(err), _, _) => Err(err)
    (_, _, Err(err), _) => Err(err)
    (_, _, _, Err(err)) => Err(err)
    (Ok(version), Ok(script_offset), Ok(feature_offset), Ok(lookup_offset)) => {
      if version < 0x00010000U {
        return Err(InvalidFormat)
      }
      if script_offset == 0 || feature_offset == 0 || lookup_offset == 0 {
        return Err(InvalidFormat)
      }
      let feature_variations_offset =
        if version >= 0x00010001U {
          match read_u32_int(data, 10) {
            Err(err) => return Err(err)
            Ok(value) => if value == 0 { None } else { Some(value) }
          }
        } else {
          None
        }
      let script_list = match parse_script_list(data, script_offset) {
        Err(err) => return Err(err)
        Ok(value) => value
      }
      let feature_list = match parse_feature_list(data, feature_offset) {
        Err(err) => return Err(err)
        Ok(value) => value
      }
      let lookup_list = match parse_lookup_list(data, lookup_offset) {
        Err(err) => return Err(err)
        Ok(value) => value
      }
      let header = LayoutHeader::{
        version,
        script_list_offset: script_offset,
        feature_list_offset: feature_offset,
        lookup_list_offset: lookup_offset,
        feature_variations_offset,
      }
      Ok(LayoutTable::{ data, header, script_list, feature_list, lookup_list })
    }
  }
}

///|
fn parse_script_list(
  data : BytesView,
  base : Int,
) -> Result[ScriptList, OtLayoutError] {
  if base < 0 || base + 2 > data.length() {
    return Err(UnexpectedEof)
  }
  let count = read_u16_int(data, base)
  let records : Array[ScriptRecord] = []
  let record_base = base + 2
  match count {
    Err(err) => Err(err)
    Ok(count) => {
      if record_base < 0 || record_base + count * 6 > data.length() {
        return Err(UnexpectedEof)
      }
      for i in 0.. return Err(err)
          Ok(value) => value
        }
        let offset = read_u16_int(data, record_offset + 4)
        match offset {
          Err(err) => return Err(err)
          Ok(offset) => {
            if offset == 0 {
              return Err(InvalidFormat)
            }
            let abs_offset = base + offset
            if abs_offset < 0 || abs_offset >= data.length() {
              return Err(UnexpectedEof)
            }
            records.push(ScriptRecord::{ tag, offset: abs_offset })
          }
        }
      }
      Ok(ScriptList::{ records })
    }
  }
}

///|
fn parse_feature_list(
  data : BytesView,
  base : Int,
) -> Result[FeatureList, OtLayoutError] {
  if base < 0 || base + 2 > data.length() {
    return Err(UnexpectedEof)
  }
  let count = read_u16_int(data, base)
  let records : Array[FeatureRecord] = []
  let record_base = base + 2
  match count {
    Err(err) => Err(err)
    Ok(count) => {
      if record_base < 0 || record_base + count * 6 > data.length() {
        return Err(UnexpectedEof)
      }
      for i in 0.. return Err(err)
          Ok(value) => value
        }
        let offset = read_u16_int(data, record_offset + 4)
        match offset {
          Err(err) => return Err(err)
          Ok(offset) => {
            if offset == 0 {
              return Err(InvalidFormat)
            }
            let abs_offset = base + offset
            if abs_offset < 0 || abs_offset >= data.length() {
              return Err(UnexpectedEof)
            }
            records.push(FeatureRecord::{ tag, offset: abs_offset })
          }
        }
      }
      Ok(FeatureList::{ records })
    }
  }
}

///|
fn parse_lookup_list(
  data : BytesView,
  base : Int,
) -> Result[LookupList, OtLayoutError] {
  if base < 0 || base + 2 > data.length() {
    return Err(UnexpectedEof)
  }
  let count = read_u16_int(data, base)
  let offsets : Array[Int] = []
  let record_base = base + 2
  match count {
    Err(err) => Err(err)
    Ok(count) => {
      if record_base < 0 || record_base + count * 2 > data.length() {
        return Err(UnexpectedEof)
      }
      for i in 0.. return Err(err)
          Ok(offset) => {
            if offset == 0 {
              return Err(InvalidFormat)
            }
            let abs_offset = base + offset
            if abs_offset < 0 || abs_offset >= data.length() {
              return Err(UnexpectedEof)
            }
            offsets.push(abs_offset)
          }
        }
      }
      Ok(LookupList::{ offsets })
    }
  }
}

///|
fn find_script_offset(
  records : Array[ScriptRecord],
  tag : @common.Tag,
) -> Int? {
  for record in records {
    if record.tag == tag {
      return Some(record.offset)
    }
  }
  None
}

///|
fn find_lang_sys_offset(
  records : Array[(@common.Tag, Int)],
  tag : @common.Tag,
) -> Int? {
  for record in records {
    let (record_tag, offset) = record
    if record_tag == tag {
      return Some(offset)
    }
  }
  None
}

///|
fn parse_script_table(
  data : BytesView,
  offset : Int,
) -> Result[(Int?, Array[(@common.Tag, Int)]), OtLayoutError] {
  if offset < 0 || offset + 4 > data.length() {
    return Err(UnexpectedEof)
  }
  let default_offset = read_u16_int(data, offset)
  let lang_count = read_u16_int(data, offset + 2)
  match (default_offset, lang_count) {
    (Err(err), _) => Err(err)
    (_, Err(err)) => Err(err)
    (Ok(default_offset), Ok(lang_count)) => {
      let record_base = offset + 4
      if record_base < 0 || record_base + lang_count * 6 > data.length() {
        return Err(UnexpectedEof)
      }
      let records : Array[(@common.Tag, Int)] = []
      for i in 0.. return Err(err)
          Ok(value) => value
        }
        let lang_offset = read_u16_int(data, record_offset + 4)
        match lang_offset {
          Err(err) => return Err(err)
          Ok(lang_offset) => {
            if lang_offset == 0 {
              return Err(InvalidFormat)
            }
            let abs_offset = offset + lang_offset
            if abs_offset < 0 || abs_offset >= data.length() {
              return Err(UnexpectedEof)
            }
            records.push((tag, abs_offset))
          }
        }
      }
      let default_lang = if default_offset == 0 {
        None
      } else {
        let abs_offset = offset + default_offset
        if abs_offset < 0 || abs_offset >= data.length() {
          return Err(UnexpectedEof)
        }
        Some(abs_offset)
      }
      Ok((default_lang, records))
    }
  }
}

///|
fn parse_lang_sys_table(
  data : BytesView,
  offset : Int,
) -> Result[(Int?, Array[Int]), OtLayoutError] {
  if offset < 0 || offset + 6 > data.length() {
    return Err(UnexpectedEof)
  }
  let lookup_order = read_u16_int(data, offset)
  let req_feature = read_u16_int(data, offset + 2)
  let feature_count = read_u16_int(data, offset + 4)
  match (lookup_order, req_feature, feature_count) {
    (Err(err), _, _) => Err(err)
    (_, Err(err), _) => Err(err)
    (_, _, Err(err)) => Err(err)
    (Ok(_), Ok(req_feature), Ok(feature_count)) => {
      let list_base = offset + 6
      if list_base < 0 || list_base + feature_count * 2 > data.length() {
        return Err(UnexpectedEof)
      }
      let indices : Array[Int] = []
      for i in 0.. return Err(err)
          Ok(value) => indices.push(value)
        }
      }
      let required = if req_feature == 0xffff { None } else { Some(req_feature) }
      Ok((required, indices))
    }
  }
}

///|
fn parse_feature_table(
  data : BytesView,
  offset : Int,
) -> Result[Array[Int], OtLayoutError] {
  if offset < 0 || offset + 4 > data.length() {
    return Err(UnexpectedEof)
  }
  let params_offset = read_u16_int(data, offset)
  let count = read_u16_int(data, offset + 2)
  match (params_offset, count) {
    (Err(err), _) => Err(err)
    (_, Err(err)) => Err(err)
    (Ok(_), Ok(count)) => {
      let list_base = offset + 4
      if list_base < 0 || list_base + count * 2 > data.length() {
        return Err(UnexpectedEof)
      }
      let indices : Array[Int] = []
      for i in 0.. return Err(err)
          Ok(value) => indices.push(value)
        }
      }
      Ok(indices)
    }
  }
}

///|
fn push_unique_int(values : Array[Int], value : Int) -> Unit {
  for entry in values {
    if entry == value {
      return
    }
  }
  values.push(value)
}

///|
fn tag_in_list(tag : @common.Tag, tags : Array[@common.Tag]) -> Bool {
  for entry in tags {
    if entry == tag {
      return true
    }
  }
  false
}

///|
fn lowercase_tag(tag : @common.Tag) -> @common.Tag {
  let (b1, b2, b3, b4) = tag.bytes()
  @common.Tag::from_bytes(
    ascii_lowercase_byte(b1),
    ascii_lowercase_byte(b2),
    ascii_lowercase_byte(b3),
    ascii_lowercase_byte(b4),
  )
}

///|
fn ascii_lowercase_byte(b : Byte) -> Byte {
  let v = b.to_uint()
  if v >= 0x41U && v <= 0x5aU {
    (v + 0x20U).to_byte()
  } else {
    b
  }
}

///|
fn check_range(
  data : BytesView,
  offset : Int,
  size : Int,
) -> Result[Unit, OtLayoutError] {
  if offset < 0 || size < 0 || offset + size > data.length() {
    Err(UnexpectedEof)
  } else {
    Ok(())
  }
}

///|
fn read_u16(data : BytesView, offset : Int) -> Result[UInt, OtLayoutError] {
  match check_range(data, offset, 2) {
    Err(err) => return Err(err)
    Ok(_) => ()
  }
  let b0 = data[offset].to_uint()
  let b1 = data[offset + 1].to_uint()
  Ok((b0 << 8) | b1)
}

///|
fn read_u32(data : BytesView, offset : Int) -> Result[UInt, OtLayoutError] {
  match check_range(data, offset, 4) {
    Err(err) => return Err(err)
    Ok(_) => ()
  }
  let b0 = data[offset].to_uint()
  let b1 = data[offset + 1].to_uint()
  let b2 = data[offset + 2].to_uint()
  let b3 = data[offset + 3].to_uint()
  Ok((b0 << 24) | (b1 << 16) | (b2 << 8) | b3)
}

///|
fn u16_to_int(value : UInt) -> Int {
  let v = value.reinterpret_as_int()
  if v < 0 {
    v + 0x10000
  } else {
    v
  }
}

///|
fn read_u16_int(data : BytesView, offset : Int) -> Result[Int, OtLayoutError] {
  match read_u16(data, offset) {
    Err(err) => Err(err)
    Ok(value) => Ok(u16_to_int(value))
  }
}

///|
fn read_u32_int(data : BytesView, offset : Int) -> Result[Int, OtLayoutError] {
  match read_u32(data, offset) {
    Err(err) => Err(err)
    Ok(value) => {
      let v = value.reinterpret_as_int()
      if v < 0 {
        Err(InvalidFormat)
      } else {
        Ok(v)
      }
    }
  }
}

///|
fn read_i16(data : BytesView, offset : Int) -> Result[Int, OtLayoutError] {
  match read_u16_int(data, offset) {
    Err(err) => Err(err)
    Ok(value) => if value >= 0x8000 { Ok(value - 0x10000) } else { Ok(value) }
  }
}

///|
fn read_tag(data : BytesView, offset : Int) -> Result[@common.Tag, OtLayoutError] {
  match check_range(data, offset, 4) {
    Err(err) => return Err(err)
    Ok(_) => ()
  }
  Ok(
    @common.Tag::from_bytes(
      data[offset],
      data[offset + 1],
      data[offset + 2],
      data[offset + 3],
    ),
  )
}