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

///|
fn ParseContext::lex_string(ctx : ParseContext) -> String raise ParseError {
  let string_start = ctx.offset
  // Fast path for ordinary strings: scan raw UTF-16 code units and materialize
  // the slice directly when there are no escapes or control characters. On
  // SIMD targets, this loop checks `LEX_STRING_SCALAR_UNITS` code units, so a
  // shorter key or value is lexed without leaving it; the rest of a longer
  // string is scanned by `lex_string_long`. Scalar targets keep the full loop.
  let end = ctx.end_offset
  let scalar_end = if LEX_STRING_USE_SIMD &&
    end - string_start > LEX_STRING_SCALAR_UNITS {
    string_start + LEX_STRING_SCALAR_UNITS
  } else {
    end
  }
  for i in string_start.. String raise ParseError {
  let base = ctx.input.start_offset()
  let i = find_string_special_long(
      ctx.input.data(),
      base + from,
      base + ctx.end_offset,
    ) -
    base
  guard i < ctx.end_offset else { raise InvalidEof }
  let c = ctx.input.unsafe_get(i)
  if c == '"' {
    ctx.offset = i + 1
    ctx.input.data().unsafe_substring(start=base + string_start, end=base + i)
  } else if c == '\\' {
    ctx.offset = i
    ctx.lex_string_slow(string_start)
  } else {
    ctx.offset = i + 1
    ctx.invalid_char(shift=-1)
  }
}

///|
/// Lexes the rest of a string that contains escapes. `ctx.offset` is at a
/// backslash, and `run_start..ctx.offset` is literal content not yet copied.
fn ParseContext::lex_string_slow(
  ctx : ParseContext,
  run_start : Int,
) -> String raise ParseError {
  let buf = StringBuilder()
  // Keep the run cursor local. A closure capturing it would allocate a
  // mutable box and a closure environment on the native backend.
  let mut start = run_start

  let end = ctx.end_offset
  // Everything before the next `"`, `\` or control character is literal
  // content, and none of those code units is a surrogate, so scanning code
  // units stops at the same place as decoding one character at a time. On
  // wasm, after an escape this loop checks units up to `scalar_limit`,
  // and `find_string_special_long` scans the rest of the run. (The first unit
  // is the backslash, which sets `scalar_limit`. If the addition wraps, the
  // switch just happens sooner; either scan finds the same unit.)
  let mut scalar_limit = 0
  for i = ctx.offset {
    guard i < end else { raise InvalidEof }
    let c = ctx.input.unsafe_get(i)
    if c == '"' {
      ctx.offset = i + 1
      if start > 0 && i > start {
        buf.write_view(ctx.input[start:i])
      }
      break
    } else if c == '\\' {
      ctx.offset = i + 1
      if start > 0 && i > start {
        buf.write_view(ctx.input[start:i])
      }
      match ctx.read_char() {
        Some('b') => buf.write_char('\b')
        Some('f') => buf.write_char('\u{0C}')
        Some('n') => buf.write_char('\n')
        Some('r') => buf.write_char('\r')
        Some('t') => buf.write_char('\t')
        Some('"') => buf.write_char('"')
        Some('\\') => buf.write_char('\\')
        Some('/') => buf.write_char('/')
        Some('u') => {
          // The backslash that opened this escape. `ctx.offset` is just
          // past the `u`, and `\` and `u` are one code unit each.
          let escape_start = ctx.offset - 2
          let c = ctx.lex_hex_digits(4)
          if c is (0xD800..=0xDBFF) {
            // A leading-surrogate escape is only meaningful as the first
            // half of an escaped surrogate pair; combine it with the
            // immediately following trailing-surrogate escape into one
            // Unicode scalar value. Anything else would manufacture a
            // string containing an unpaired surrogate, which MoonBit
            // strings disallow (RFC 8259 calls the behavior for such
            // escapes unpredictable; I-JSON forbids them).
            match ctx.read_char() {
              Some('\\') => ()
              Some(_) => ctx.unpaired_surrogate(escape_start)
              None => raise InvalidEof
            }
            match ctx.read_char() {
              Some('u') => ()
              Some(_) => ctx.unpaired_surrogate(escape_start)
              None => raise InvalidEof
            }
            let c2 = ctx.lex_hex_digits(4)
            if c2 is (0xDC00..=0xDFFF) {
              let combined = (c << 10) + c2 - 0x35fdc00
              buf.write_char(combined.unsafe_to_char())
            } else {
              ctx.unpaired_surrogate(escape_start)
            }
          } else if c is (0xDC00..=0xDFFF) {
            // A bare trailing-surrogate escape can never form a scalar
            // value.
            ctx.unpaired_surrogate(escape_start)
          } else {
            buf.write_char(c.unsafe_to_char())
          }
        }
        Some(c) => ctx.invalid_char(shift=-c.utf16_len())
        None => raise InvalidEof
      }
      start = ctx.offset
      if LEX_STRING_ESCAPE_SIMD {
        scalar_limit = ctx.offset + LEX_STRING_SCALAR_UNITS
      }
      continue ctx.offset
    } else if c < ' ' {
      // An unescaped control character.
      ctx.offset = i + 1
      ctx.invalid_char(shift=-1)
    } else if !LEX_STRING_ESCAPE_SIMD || i < scalar_limit {
      continue i + 1
    } else {
      // Lands on the next `"`, `\` or control character, or on `end`.
      let base = ctx.input.start_offset()
      continue find_string_special_long(
          ctx.input.data(),
          base + i + 1,
          base + end,
        ) -
        base
    }
  }
  buf.to_string()
}

///|
/// Scalar reference for `find_string_special_long`: the first index in
/// `start.. Int {
  for i in start.. Int {
  find_string_special_scalar(str, start, end)
}

///|
/// Checks eight UTF-16 code units at a time on linear-memory backends, then
/// the remaining tail one code unit at a time. Callers pass
/// `0 <= start <= end <= str.length()`; every load covers `pos..= 8` (written as a difference so it
/// cannot overflow near `Int::max_value`).
#cfg(any(target="native", target="wasm"))
fn find_string_special_long(str : String, start : Int, end : Int) -> Int {
  let control_limit = @v128.i16x8_splat(' ')
  let quote = @v128.i16x8_splat('"')
  let backslash = @v128.i16x8_splat('\\')
  let tail_start = for pos = start; end - pos >= 8; {
    let block = @v128.v128_load_i16x8(str, pos)
    let special = @v128.v128_or_(
      @v128.i16x8_lt_u(block, control_limit),
      @v128.v128_or_(
        @v128.i16x8_eq(block, quote),
        @v128.i16x8_eq(block, backslash),
      ),
    )
    if @v128.v128_any_true(special) {
      // Bit `k` of the mask is lane `k`, the code unit at `pos + k`.
      return pos + @v128.i16x8_bitmask(special).ctz()
    }
    continue pos + 8
  } nobreak {
    pos
  }
  find_string_special_scalar(str, tail_start, end)
}

///|
/// Reports the `\uXXXX` escape that begins at `escape_start` as invalid.
///
/// Every rejection of an unpaired surrogate points here — at the backslash
/// opening the offending escape — rather than at whichever character the
/// scan happened to stop on. Blaming the stopping point would name a
/// perfectly valid hex digit for `"\uDC00"`, and for a leading surrogate
/// followed by a non-BMP character it would name a position inside that
/// character.
fn[T] ParseContext::unpaired_surrogate(
  ctx : ParseContext,
  escape_start : Int,
) -> T raise ParseError {
  ctx.invalid_char(shift=escape_start - ctx.offset)
}

///|
fn ParseContext::lex_hex_digits(
  ctx : ParseContext,
  n : Int,
) -> Int raise ParseError {
  for _ in 0.. c.to_int() - '0'
      Some('A'..='F' as c) => c.to_int() - 'A' + 10
      Some('a'..='f' as c) => c.to_int() - 'a' + 10
      // `-1` would land inside the character when it is not in the BMP,
      // reporting a broken half at the wrong column.
      Some(c) => ctx.invalid_char(shift=-c.utf16_len())
      None => raise InvalidEof
    }
    continue (r << 4) | d
  } nobreak {
    r
  }
}