// 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.

///|
extern "js" fn decode_utf8_js(
  bytes : Bytes,
  start : Int,
  len : Int,
  preserve_bom : Bool,
) -> Array[String] =
  #| ((preserveBOMDecoder, dropBOMDecoder) => function(bytes, start, len, preserveBOM) {
  #|   try {
  #|     const end = start + len;
  #|     const slice = bytes.subarray(start, end);
  #|     const decoder = preserveBOM ? preserveBOMDecoder : dropBOMDecoder;
  #|     return [decoder.decode(slice)];
  #|   } catch (_) {
  #|     return [];
  #|   }
  #| })(
  #|   new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }),
  #|   new TextDecoder("utf-8", { fatal: true, ignoreBOM: false }),
  #| )

///|
extern "js" fn decode_utf8_lossy_js(
  bytes : Bytes,
  start : Int,
  len : Int,
  preserve_bom : Bool,
) -> String =
  #| ((preserveBOMDecoder, dropBOMDecoder) => function(bytes, start, len, preserveBOM) {
  #|   const end = start + len;
  #|   const slice = bytes.subarray(start, end);
  #|   const decoder = preserveBOM ? preserveBOMDecoder : dropBOMDecoder;
  #|   return decoder.decode(slice);
  #| })(
  #|   new TextDecoder("utf-8", { ignoreBOM: true }),
  #|   new TextDecoder("utf-8", { ignoreBOM: false }),
  #| )

///|
fn utf8_find_malformed(src : Bytes, src_offset : Int, src_length : Int) -> Int {
  let view = src[src_offset:src_offset + src_length]
  for bytes = view {
    match bytes {
      [] => break -1
      [
        _..=0x7F,
        _..=0x7F,
        _..=0x7F,
        _..=0x7F,
        _..=0x7F,
        _..=0x7F,
        _..=0x7F,
        _..=0x7F,
        .. rest,
      ] => continue rest
      [0..=0x7F, .. rest] => continue rest
      [0xC2..=0xDF, 0x80..=0xBF, .. rest] => continue rest
      [0xE0, 0xA0..=0xBF, 0x80..=0xBF, .. rest]
      | [0xE1..=0xEC, 0x80..=0xBF, 0x80..=0xBF, .. rest]
      | [0xED, 0x80..=0x9F, 0x80..=0xBF, .. rest]
      | [0xEE..=0xEF, 0x80..=0xBF, 0x80..=0xBF, .. rest] => continue rest
      [0xF0, 0x90..=0xBF, 0x80..=0xBF, 0x80..=0xBF, .. rest]
      | [0xF1..=0xF3, 0x80..=0xBF, 0x80..=0xBF, 0x80..=0xBF, .. rest]
      | [0xF4, 0x80..=0x8F, 0x80..=0xBF, 0x80..=0xBF, .. rest] => continue rest
      malformed => break malformed.start_offset() - src_offset
    }
  }
}

///|
fn strict_malformed_suffix(bytes : BytesView) -> BytesView {
  let input = bytes.data()
  let src_offset = bytes.start_offset()
  let src_length = bytes.length()
  let malformed_offset = utf8_find_malformed(input, src_offset, src_length)
  bytes[malformed_offset:]
}

///|
/// Decode input bytes/text into structured output.
pub fn decode(
  bytes : BytesView,
  ignore_bom? : Bool = false,
) -> String raise Malformed {
  let result = decode_utf8_js(
    bytes.data(),
    bytes.start_offset(),
    bytes.length(),
    !ignore_bom,
  )
  if result.length() == 1 {
    result[0]
  } else {
    let bytes = drop_utf8_bom(bytes, ignore_bom)
    raise Malformed(strict_malformed_suffix(bytes))
  }
}

///|
///
/// References :
/// - https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G66453
/// - https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-5/#G40630
pub fn decode_lossy(bytes : BytesView, ignore_bom? : Bool = false) -> String {
  decode_utf8_lossy_js(
    bytes.data(),
    bytes.start_offset(),
    bytes.length(),
    !ignore_bom,
  )
}