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

///|
const SAFE_INTEGER_LIMIT : Int64 = 9007199254740991L

///|
const MAX_MANTISSA_FAST_PATH : UInt64 = 9007199254740992UL

///|
const MIN_EXPONENT_FAST_PATH : Int64 = -22L

///|
const MAX_EXPONENT_FAST_PATH : Int64 = 22L

///|
const MAX_EXPONENT_DISGUISED_FAST_PATH : Int64 = 37L

///|
const EXPONENT_CAP : Int64 = 100000L

///|
const MAX_UINT64 : UInt64 = 0xffffffffffffffffUL

///|
let pow10_table : ReadOnlyArray[Double] = [
  1.0, 10.0, 100.0, 1000.0, 10000.0, 100000.0, 1000000.0, 10000000.0, 100000000.0,
  1000000000.0, 10000000000.0, 100000000000.0, 1000000000000.0, 10000000000000.0,
  100000000000000.0, 1000000000000000.0, 10000000000000000.0, 100000000000000000.0,
  1000000000000000000.0, 10000000000000000000.0, 100000000000000000000.0, 1000000000000000000000.0,
  10000000000000000000000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
]

///|
let int_pow10_table : ReadOnlyArray[UInt64] = [
  1UL, 10UL, 100UL, 1000UL, 10000UL, 100000UL, 1000000UL, 10000000UL, 100000000UL,
  1000000000UL, 10000000000UL, 100000000000UL, 1000000000000UL, 10000000000000UL,
  100000000000000UL, 1000000000000000UL,
]

///|
// `#valtype` keeps this struct stack-allocated on the native target.
// `scan_json_number` builds + returns one of these on every JSON
// number, and without the annotation each call boxes a ~32-byte heap
// object — measured as ~50 % of total alloc bytes in the all-integer
// json bench (300 k numbers → 9.16 MB / 300 k allocs eliminated).
#valtype
priv struct JsonNumberScan {
  negative : Bool
  is_integer : Bool
  mantissa : UInt64
  exponent : Int64
  many_digits : Bool
}

///|
fn json_pow10_fast_path(exponent : Int) -> Double {
  pow10_table[exponent & 31]
}

///|
fn checked_mul(a : UInt64, b : UInt64) -> UInt64? {
  if a == 0UL || b == 0UL {
    return Some(0UL)
  }
  if a == 1UL {
    return Some(b)
  }
  if b == 1UL {
    return Some(a)
  }
  if b.clz() == 0 || a.clz() == 0 {
    return None
  }
  let quotient = MAX_UINT64 / b
  if a > quotient {
    None
  } else {
    Some(a * b)
  }
}

///|
// Returns NaN to mean "fast path didn't apply; caller should fall
// back to strconv". The fast path itself can only produce 0 or a
// finite Double (the `checked_mul` guard rules out infinity), so NaN
// is a free sentinel — and dodging the `Double?` return means no
// per-number `Some` allocation on the json_parse hot path (one less
// ~16-byte boxed `Option` per number).
fn JsonNumberScan::try_fast_double(self : JsonNumberScan) -> Double {
  if self.mantissa == 0UL {
    let value = 0.0
    return if self.negative { -value } else { value }
  }
  if self.many_digits ||
    self.exponent < MIN_EXPONENT_FAST_PATH ||
    self.exponent > MAX_EXPONENT_DISGUISED_FAST_PATH ||
    self.mantissa > MAX_MANTISSA_FAST_PATH {
    return @double.not_a_number
  }
  let value = if self.exponent <= MAX_EXPONENT_FAST_PATH {
    let value = self.mantissa.to_double()
    if self.exponent < 0L {
      value / json_pow10_fast_path(-self.exponent.to_int())
    } else {
      value * json_pow10_fast_path(self.exponent.to_int())
    }
  } else {
    let shift = self.exponent - MAX_EXPONENT_FAST_PATH
    let mantissa = match
      checked_mul(self.mantissa, int_pow10_table[shift.to_int()]) {
      Some(m) => m
      None => return @double.not_a_number
    }
    if mantissa > MAX_MANTISSA_FAST_PATH {
      return @double.not_a_number
    }
    mantissa.to_double() * json_pow10_fast_path(MAX_EXPONENT_FAST_PATH.to_int())
  }
  if self.negative {
    -value
  } else {
    value
  }
}

///|
fn ParseContext::scan_json_number(
  ctx : ParseContext,
  start : Int,
  end : Int,
) -> JsonNumberScan {
  let negative = ctx.input.unsafe_get(start) == '-'
  let mut has_decimal = false
  let mut has_exponent = false
  let mut exponent_negative = false
  let mut exponent_part = 0L
  let mut fractional_digits = 0
  let mut mantissa = 0UL
  let mut significant_digits = 0
  let mut seen_nonzero = false
  for i in (if negative { start + 1 } else { start }).. {
        let digit = c.to_int() - '0'
        if has_exponent {
          if exponent_part < EXPONENT_CAP {
            let next_exponent = exponent_part * 10L + digit.to_int64()
            exponent_part = if next_exponent > EXPONENT_CAP {
              EXPONENT_CAP
            } else {
              next_exponent
            }
          }
        } else {
          if has_decimal {
            fractional_digits += 1
          }
          if digit != 0 || seen_nonzero {
            seen_nonzero = true
            significant_digits += 1
            if significant_digits <= 19 {
              mantissa = mantissa * 10UL +
                UInt64::extend_uint(digit.reinterpret_as_uint())
            }
          }
        }
      }
      '.' => has_decimal = true
      'e' | 'E' => {
        has_exponent = true
        if i + 1 < end {
          let next = ctx.input.unsafe_get(i + 1)
          if next == '-' {
            exponent_negative = true
          }
        }
      }
      _ => ()
    }
  }
  let exponent_part = if exponent_negative {
    -exponent_part
  } else {
    exponent_part
  }
  {
    negative,
    is_integer: !has_decimal && !has_exponent,
    mantissa,
    exponent: exponent_part - fractional_digits.to_int64(),
    many_digits: significant_digits > 19,
  }
}

///|
// Shared result type for the number lexing helpers. `#valtype` keeps it
// stack-allocated on the native target, so returning it does not heap-allocate
// the way the previous `(Double, StringView?)` tuple did (one box per parsed
// number).
// `repr` is `Some` only for the rare out-of-range values that fall back to a
// string-preserving representation; it is `None` in the common case.
// Field order matters: the reference field (`repr`) must precede the `Double`
// (`value`). With `value` first, the Wasm backend currently mis-compiles the
// destructuring in `lex_value` (`i32.wrap_i64 expected i64, found f64`), so keep
// `repr` first.
#valtype
priv struct LexedNumber {
  repr : StringView?
  value : Double
}

///|
fn ParseContext::lex_integer_end(
  ctx : ParseContext,
  start : Int,
  end : Int,
) -> LexedNumber {
  let negative = ctx.input.unsafe_get(start) == '-'
  let number_start = if negative { start + 1 } else { start }
  for i = number_start, acc = 0L {
    if i >= end {
      let value = if negative { -acc } else { acc }
      break { value: value.to_double(), repr: None }
    }
    let digit = (ctx.input.unsafe_get(i).to_int() - '0').to_int64()
    if acc > (SAFE_INTEGER_LIMIT - digit) / 10L {
      // The literal exceeds the exact-integer range of a double (2^53 - 1).
      // Fall back to strconv for the correctly rounded value, preserving the
      // exact source text in `repr` so `stringify` stays lossless. Only a
      // literal strconv itself rejects (beyond double range) keeps the
      // infinity sentinel.
      let s = ctx.input.view(start_offset=start, end_offset=end)
      try {
        let value = @internal/strconv.parse_double(s)
        return { value, repr: Some(s) }
      } catch {
        _ =>
          return if negative {
            { value: @double.neg_infinity, repr: Some(s) }
          } else {
            { value: @double.infinity, repr: Some(s) }
          }
      }
    }
    continue i + 1, acc * 10L + digit
  }
}

///|
fn ParseContext::lex_decimal_integer(
  ctx : ParseContext,
  start~ : Int,
) -> LexedNumber raise ParseError {
  for ;; {
    match ctx.read_char() {
      Some('.') => return ctx.lex_decimal_point(start~)
      Some('e' | 'E') => return ctx.lex_decimal_exponent(start~)
      Some('0'..='9') => continue
      Some(_) => {
        ctx.offset -= 1
        return ctx.lex_number_end(start, ctx.offset)
      }
      None => return ctx.lex_number_end(start, ctx.offset)
    }
  }
}

///|
fn ParseContext::lex_decimal_point(
  ctx : ParseContext,
  start~ : Int,
) -> LexedNumber raise ParseError {
  match ctx.read_char() {
    Some('0'..='9') => ctx.lex_decimal_fraction(start~)
    Some(_) => ctx.invalid_char(shift=-1)
    None => raise InvalidEof
  }
}

///|
fn ParseContext::lex_decimal_fraction(
  ctx : ParseContext,
  start~ : Int,
) -> LexedNumber raise ParseError {
  for ;; {
    match ctx.read_char() {
      Some('e' | 'E') => return ctx.lex_decimal_exponent(start~)
      Some('0'..='9') => continue
      Some(_) => {
        ctx.offset -= 1
        return ctx.lex_number_end(start, ctx.offset)
      }
      None => return ctx.lex_number_end(start, ctx.offset)
    }
  }
}

///|
fn ParseContext::lex_decimal_exponent(
  ctx : ParseContext,
  start~ : Int,
) -> LexedNumber raise ParseError {
  match ctx.read_char() {
    Some('+' | '-') => return ctx.lex_decimal_exponent_sign(start~)
    Some('0'..='9') => return ctx.lex_decimal_exponent_integer(start~)
    Some(_) => {
      ctx.offset -= 1
      ctx.invalid_char()
    }
    None => raise InvalidEof
  }
}

///|
fn ParseContext::lex_decimal_exponent_sign(
  ctx : ParseContext,
  start~ : Int,
) -> LexedNumber raise ParseError {
  match ctx.read_char() {
    Some('0'..='9') => return ctx.lex_decimal_exponent_integer(start~)
    Some(_) => {
      ctx.offset -= 1
      ctx.invalid_char()
    }
    None => raise InvalidEof
  }
}

///|
fn ParseContext::lex_decimal_exponent_integer(
  ctx : ParseContext,
  start~ : Int,
) -> LexedNumber {
  for ;; {
    match ctx.read_char() {
      Some('0'..='9') => continue
      Some(_) => {
        ctx.offset -= 1
        return ctx.lex_number_end(start, ctx.offset)
      }
      None => return ctx.lex_number_end(start, ctx.offset)
    }
  }
}

///|
fn ParseContext::lex_zero(
  ctx : ParseContext,
  start~ : Int,
) -> LexedNumber raise ParseError {
  match ctx.read_char() {
    Some('.') => ctx.lex_decimal_point(start~)
    Some('e' | 'E') => ctx.lex_decimal_exponent(start~)
    Some('0'..='9') => {
      ctx.offset -= 1
      ctx.invalid_char()
    }
    Some(_) => {
      ctx.offset -= 1
      return ctx.lex_number_end(start, ctx.offset)
    }
    None => return ctx.lex_number_end(start, ctx.offset)
  }
}

///|
fn ParseContext::lex_number_end(
  ctx : ParseContext,
  start : Int,
  end : Int,
) -> LexedNumber {
  // Fast path for JSON numbers: the lexer has already validated the grammar,
  // so scan raw UTF-16 digits once and bypass the general strconv parser for
  // safe integers and Clinger-style fast-path doubles. Fall back to strconv for
  // large or precision-sensitive numbers so existing rounding behavior is kept.
  let scan = ctx.scan_json_number(start, end)
  if scan.is_integer {
    // `is_integer` is set by `scan_json_number` only when no `.` and no `e/E`
    // are seen, so for that branch `scan.exponent` is structurally 0 and
    // `scan.mantissa` is exactly the unsigned absolute value of the integer
    // literal. The `scan.exponent == 0L` guard restates that invariant
    // locally so a future relaxation of `is_integer` cannot silently make
    // this branch return an unscaled value; in that case we just fall
    // through to `lex_integer_end`.
    //
    // The mantissa <= 2^53 - 1 check (`SAFE_INTEGER_LIMIT`) keeps the
    // returned Double lossless. `reinterpret_as_uint64` / `reinterpret_as_int64`
    // are value-preserving here because both operands sit in [0, 2^53), well
    // inside the overlap of Int64+ and UInt64.
    if !scan.many_digits &&
      scan.exponent == 0L &&
      scan.mantissa <= SAFE_INTEGER_LIMIT.reinterpret_as_uint64() {
      let v = scan.mantissa.reinterpret_as_int64()
      let signed = if scan.negative { -v } else { v }
      return { value: signed.to_double(), repr: None }
    }
    return ctx.lex_integer_end(start, end)
  }
  let fast = scan.try_fast_double()
  if !fast.is_nan() {
    return { value: fast, repr: None }
  }
  let s = ctx.input.view(start_offset=start, end_offset=end)
  try {
    let d = @internal/strconv.parse_double(s)
    // For normal values, return without string representation
    { value: d, repr: None }
  } catch {
    // If parsing fails as a double, treat it as infinity and preserve the string
    _ =>
      if scan.negative {
        { value: @double.neg_infinity, repr: Some(s) }
      } else {
        { value: @double.infinity, repr: Some(s) }
      }
  }
}