"""JavaScript semantics the port depends on.

doctor.js (the website engine) is the reference. Wherever Python and JavaScript disagree on a
primitive (number to string, rounding, string trimming, object key order, string ordering,
division by zero), the helpers here reproduce what JavaScript does, so that both engines print
the same text and make the same decisions.
"""
import json
import math
import re
from decimal import ROUND_HALF_UP, Context, Decimal, localcontext

# Characters removed by String.prototype.trim and matched by the regex class \s in JavaScript.
# (Python's str.strip() also removes \x1c-\x1f and \x85 but keeps ﻿.)
JS_WS = "\t\n\v\f\r                  　﻿"
# The same set as a regex character-class body (use inside [...]).
WS = r"\t\n\v\f\r    -     　﻿"

_INT_KEY = re.compile(r"(?:0|[1-9][0-9]*)\Z")
_HUNDRED = Context(prec=200)


def trim(s):
    """String.prototype.trim()."""
    return s.strip(JS_WS)


def u16len(s):
    """String length in UTF-16 code units, like JavaScript's .length."""
    return len(s) + sum(1 for ch in s if ord(ch) > 0xFFFF) if not s.isascii() else len(s)


def u16key(s):
    """Sort key giving JavaScript's `a < b` order for strings (UTF-16 code-unit order)."""
    return s.encode("utf-16-be", "surrogatepass")


def is_array_index(k):
    """True for keys JavaScript treats as array indices (they come first in Object.keys)."""
    return bool(_INT_KEY.match(k)) and int(k) <= 4294967294


def js_keys(d):
    """Object.keys(obj) order: integer-like keys ascending, then the rest in insertion order."""
    ints, others = [], []
    for k in d:
        (ints if is_array_index(k) else others).append(k)
    ints.sort(key=int)
    return ints + others


def div(a, b):
    """a / b with IEEE semantics (JavaScript never raises on division by zero)."""
    if b == 0:
        if a == 0 or a != a:
            return math.nan
        neg = (a < 0) != (math.copysign(1.0, b) < 0)
        return -math.inf if neg else math.inf
    return a / b


def js_max(*xs):
    """Math.max: NaN if any argument is NaN."""
    for x in xs:
        if x != x:
            return math.nan
    return max(xs)


def js_round(x):
    """Math.round: round half toward +infinity."""
    if x != x or x in (math.inf, -math.inf):
        return x
    r = math.floor(x)
    return r + 1 if x - r >= 0.5 else r


def js_str(x):
    """String(number) / number + "" in JavaScript."""
    if isinstance(x, bool):
        return "true" if x else "false"
    if isinstance(x, int):
        if abs(x) < 10 ** 21:
            return str(x)
        x = float(x)
    if x != x:
        return "NaN"
    if x == math.inf:
        return "Infinity"
    if x == -math.inf:
        return "-Infinity"
    if x == 0:
        return "0"
    sign = "-" if x < 0 else ""
    t = Decimal(repr(abs(x))).as_tuple()          # repr gives the shortest round-trip digits, like JS
    digits = list(t.digits)
    exp = t.exponent
    while len(digits) > 1 and digits[-1] == 0:
        digits.pop()
        exp += 1
    k = len(digits)
    n = exp + k
    ds = "".join(map(str, digits))
    if k <= n <= 21:
        out = ds + "0" * (n - k)
    elif 0 < n <= 21:
        out = ds[:n] + "." + ds[n:]
    elif -6 < n <= 0:
        out = "0." + "0" * (-n) + ds
    else:
        e = n - 1
        es = ("+" if e >= 0 else "-") + str(abs(e))
        out = ds + "e" + es if k == 1 else ds[0] + "." + ds[1:] + "e" + es
    return sign + out


def to_fixed(x, f):
    """Number.prototype.toFixed(f): exact decimal value, ties rounded away from zero."""
    if x != x or x in (math.inf, -math.inf) or abs(x) >= 1e21:
        return js_str(x)
    sign = "-" if x < 0 else ""
    with localcontext(_HUNDRED):
        q = Decimal(abs(x)).quantize(Decimal(1).scaleb(-f), rounding=ROUND_HALF_UP)
    return sign + format(q, "f")


def to_exponential(x, f):
    """Number.prototype.toExponential(f)."""
    if x != x or x in (math.inf, -math.inf):
        return js_str(x)
    sign = "-" if x < 0 else ""
    if x == 0:
        return sign + "0" + ("." + "0" * f if f else "") + "e+0"
    with localcontext(_HUNDRED):
        d = Decimal(abs(x))
        e = d.adjusted()
        q = d.scaleb(-e).quantize(Decimal(1).scaleb(-f), rounding=ROUND_HALF_UP)
        if q >= 10:
            e += 1
            q = d.scaleb(-e).quantize(Decimal(1).scaleb(-f), rounding=ROUND_HALF_UP)
    return sign + format(q, "f") + "e" + ("+" if e >= 0 else "-") + str(abs(e))


def to_locale_int(x):
    """Math.round(x).toLocaleString("en-US") for the integers fmtNum produces."""
    return "{:,}".format(int(x))


def py_str(s):
    """JSON.stringify(string) - used to write Python string literals in generated code."""
    return json.dumps(s, ensure_ascii=False)


def js_json(v):
    """JSON.stringify(v) without spaces (lists of ints in generated code)."""
    return json.dumps(v, separators=(",", ":"), ensure_ascii=False)
