"""Source-pinned native facts for the current Escape receiver selector.

No transport is opened and no client function is called here. The caller owns
the already-enabled, watched read session. New ICalls must already be resolved;
an unresolved cache remains pending. Native pointers are never managed objects.
The scale formula includes every parent; it is not local scale or column length.
Floating-point bounds, rather than bitwise SSE equivalence, guard its selector.
"""
from datetime import datetime, timezone
import hashlib
import json
import math
import struct

from .paths import root
from .quest_ui_actions import NativeUnityUI, SOURCE_VERSION
from .vendor.map_memory import MapReadPending

SOURCE_REF='data/arbeit-material-escape-native/source-pins.json'
_raw=(root()/SOURCE_REF).read_bytes()
if hashlib.sha256(_raw).hexdigest()!='9044b397c37f8106ce2027b9405a369b3fc786d8ed97c517678a8b6093767cf6':
    raise ValueError('Escape native source pins changed')
PINS=json.loads(_raw)
del _raw
SCALE_THRESHOLD=struct.unpack('<f',struct.pack('<f',1e-10))[0]


# Each tuple is (real-valued center, absolute error bound). The selected SSE
# bodies use <128 scalar arithmetic operations per algebraic component. Widen
# at every conceptual operation by gamma(128), covering their different finite
# float32 associations as well as Python evaluation. This intentionally loses
# precision around the selector boundary instead of guessing its Boolean.
_GAMMA=(128*2**-24)/(1-128*2**-24)
def _ball(value,error=0.):return float(value),float(error)
def _plus(a,b):
    v=a[0]+b[0];e=a[1]+b[1]
    return v,e+_GAMMA*(abs(a[0])+abs(b[0])+e)+2**-149
def _neg(a):return -a[0],a[1]
def _times(a,b):
    v=a[0]*b[0];e=abs(a[0])*b[1]+abs(b[0])*a[1]+a[1]*b[1]
    return v,e+_GAMMA*(abs(v)+e)+2**-149
def _sum(values):
    result=_ball(0)
    for value in values:result=_plus(result,value)
    return result
def _quat_mul(a,b):
    ax,ay,az,aw=a;bx,by,bz,bw=b
    return [_sum([_times(aw,bx),_times(ax,bw),_times(ay,bz),_neg(_times(az,by))]),
            _sum([_times(aw,by),_neg(_times(ax,bz)),_times(ay,bw),_times(az,bx)]),
            _sum([_times(aw,bz),_times(ax,by),_neg(_times(ay,bx)),_times(az,bw)]),
            _sum([_times(aw,bw),_neg(_times(ax,bx)),_neg(_times(ay,by)),_neg(_times(az,bz))])]
def _signed_ancestor_rotation(quaternion,scale):
    """0xfc1b0 +0x82..+0xcf: XOR accumulated quaternion by scale sign pairs.

    The packed sign masks include negative zero. The w lane is cleared before
    XOR. No scale magnitudes, determinant guess, or quaternion normalization.
    """
    sx,sy,sz=(math.copysign(1,v)<0 for v in scale)
    flips=(sy!=sz,sx!=sz,sx!=sy,False)
    return [_neg(v) if flip else v for v,flip in zip(quaternion,flips)]
def _rotation(q):
    x,y,z,w=q;two=_ball(2);one=_ball(1)
    def diagonal(a,b):return _plus(one,_neg(_times(two,_plus(_times(a,a),_times(b,b)))))
    def cross(a,b,c,d,sign=1):return _times(two,_plus(_times(a,b),_times(_ball(sign),_times(c,d))))
    return [[diagonal(y,z),cross(x,y,z,w,-1),cross(x,z,y,w)],
            [cross(x,y,z,w),diagonal(x,z),cross(y,z,x,w,-1)],
            [cross(x,z,y,w,-1),cross(y,z,x,w),diagonal(x,y)]]
def _matmul(a,b):return [[_sum([_times(a[i][k],b[k][j]) for k in range(3)]) for j in range(3)] for i in range(3)]


def lossy_scale_from_chain(chain):
    """Pure source formula, child first, each row (quaternion xyzw, scale xyz).

    Return approximate values *with conservative error bounds*. Only the proven
    threshold classification is usable by the selector, never the approximation
    alone. Negative leaf/ancestor scales and signed zero use the source sign-bit
    correction. No quaternion is normalized.
    """
    if not chain or len(chain)>128:raise ValueError('Escape transform chain outside bound')
    matrix=[[_ball(int(i==j)) for j in range(3)] for i in range(3)]
    world_q=[_ball(0),_ball(0),_ball(0),_ball(1)]
    for ancestor_index,(q,scale) in enumerate(chain):
        if len(q)!=4 or len(scale)!=3 or not all(math.isfinite(x) for x in (*q,*scale)):
            raise ValueError('Current Escape transform contains invalid TRS values')
        if abs(sum(x*x for x in q)-1)>1e-4:raise MapReadPending('Current Escape quaternion is not a supported unit rotation')
        qb=list(map(_ball,q));local=_rotation(qb)
        local=[[_times(local[i][j],_ball(scale[j])) for j in range(3)] for i in range(3)]
        matrix=_matmul(local,matrix)
        world_q=qb if ancestor_index==0 else _quat_mul(qb,_signed_ancestor_rotation(world_q,scale))
    inverse_q=[_neg(v) if i<3 else v for i,v in enumerate(world_q)]
    residual=_matmul(_rotation(inverse_q),matrix)
    values=[residual[i][i][0] for i in range(3)]
    errors=[residual[i][i][1] for i in range(3)]
    if not all(math.isfinite(v) for v in (*values,*errors)):
        raise MapReadPending('Current Escape scale arithmetic exceeds finite bounds')
    lower=sum(max(0,abs(v)-e)**2 for v,e in zip(values,errors))
    upper=sum((abs(v)+e)**2 for v,e in zip(values,errors))
    # The selector itself performs three float32 multiplies and two additions.
    lower=max(0,lower*(1-_GAMMA));upper=upper*(1+_GAMMA)+2**-149
    qualifies=True if lower>=SCALE_THRESHOLD else False if upper<SCALE_THRESHOLD else None
    return {'lossy_scale':values,'lossy_scale_error_bounds':errors,
            'scale_squared_bounds':[lower,upper],'scale_threshold':SCALE_THRESHOLD,
            'lossy_scale_threshold_verified':qualifies is not None,
            'scale_qualifies':qualifies,
            'scale_basis':'source inverse world rotation × complete current world TRS matrix diagonal',
            'native_float32_bit_exact':False}


class MaterialEscapeNative:
    """Extends existing NativeUnityUI pins without changing its global allowlist."""
    def __init__(self,session,native=None):
        self.s=session;self.r=session.reader
        self.n=native or getattr(session,'_native_ui',None) or NativeUnityUI(session)
        if any(session.plan.get(k)!=v for k,v in SOURCE_VERSION.items()):
            raise ValueError('Escape native source version differs from current reader')
        self.base=self.n.base

    def _body(self,name):
        rows=[r for r in PINS['native_bodies'] if r['name']==name]
        if len(rows)!=1:raise ValueError('Escape native body missing')
        row=rows[0];raw=bytes.fromhex(row['raw_hex'])
        if self.n.file_bytes(row['rva'],len(raw))!=raw or self.r.read(self.base+row['rva'],len(raw))!=raw:
            raise ValueError('Current Escape native source body changed: '+name)
        return row

    def _resolved(self,owner,method,body):
        rows=[r for r in PINS['wrappers'] if (r['owner'],r['method'])==(owner,method)]
        if len(rows)!=1 or 'cache_rva' not in rows[0]:raise ValueError('Escape named getter unavailable')
        row=rows[0];code=bytes.fromhex(row['raw_hex'])
        if self.r.read(self.s.base+row['wrapper_rva'],len(code))!=code:
            raise ValueError('Current Escape named wrapper changed')
        name=(row['icall_name']+'\0').encode()
        if self.r.read(self.s.base+row['name_rva'],len(name))!=name:
            raise ValueError('Current Escape named resolver string changed')
        pointer=self._u64(self.s.base+row['cache_rva'])
        if not pointer:raise MapReadPending('Current Escape named getter cache unresolved; no resolver call is allowed')
        expected=self._body(body)
        if pointer!=self.base+expected['rva']:
            raise MapReadPending('Current Escape named native getter differs from inspected source entry')

    def _u64(self,address):return struct.unpack('<Q',self.s.watch(address,8))[0]
    def _i32(self,address):return struct.unpack('<i',self.s.watch(address,4))[0]

    def instance_id(self,native_object):
        self._resolved('Object','GetOffsetOfInstanceIDInCPlusPlusObject','instance_offset_bridge')
        self._body('instance_offset_leaf')
        # The exact pinned bridge is jmp 0x291d10; the complete leaf is
        # mov eax,8; ret. A native object is not a managed Object wrapper.
        value=self._i32(native_object+8)
        if value==0:raise MapReadPending('Current Escape native object instance ID unavailable')
        return value

    def tag(self,native_game_object):
        self._resolved('GameObject','get_tag','tag_bridge')
        for name in ('tag_manager','manager_registry','tag_lookup'):self._body(name)
        key=struct.unpack('<H',self.s.watch(native_game_object+0x54,2))[0]
        manager=self._u64(self.base+0x1ae5620+2*8)
        if not manager:raise MapReadPending('Current TagManager registry slot unavailable')
        tree=self._u64(manager+0x38)
        if not tree:raise MapReadPending('Current tag tree container unavailable')
        # 4c8b4938; 4d8b01; 498bc8; 498b4008:
        # manager+0x38 -> container -> head; head+8 -> root.
        head=self._u64(tree)
        if not head:raise MapReadPending('Current tag tree head unavailable')
        if self.s.watch(head+0x19,1)!=b'\1':raise ValueError('Current tag tree sentinel invalid')
        pointer=self._u64(head+8);candidate=head;seen=set()
        while True:
            if not pointer or pointer in seen or len(seen)>=128:
                raise ValueError('Current tag tree traversal invalid or exceeds bound')
            seen.add(pointer);sentinel=self.s.watch(pointer+0x19,1)[0]
            if sentinel==1:break
            if sentinel!=0:raise ValueError('Current tag node sentinel invalid')
            current=struct.unpack('<I',self.s.watch(pointer+0x20,4))[0]
            if current>=key:candidate=pointer;pointer=self._u64(pointer)
            else:pointer=self._u64(pointer+0x10)
        if candidate==head or struct.unpack('<I',self.s.watch(candidate+0x20,4))[0]!=key:
            raise MapReadPending('Current native tag ID has no exact TagManager string')
        address=candidate+0x28;layout=self.s.watch(address,0x28);kind=layout[0x20]
        if kind==1:
            remaining=struct.unpack_from('<b',layout,0x18)[0];size=24-remaining
            if not 0<=size<=24:raise ValueError('Current native inline tag string length invalid')
            value=layout[:size]
        elif kind in (0,2):
            pointer=struct.unpack_from('<Q',layout)[0];size=struct.unpack_from('<Q',layout,0x10)[0]
            if size>4096 or (size and not pointer):raise ValueError('Current native tag string outside bound')
            value=self.s.watch(pointer,size) if size else b''
        else:raise MapReadPending('Current native tag string storage kind unsupported')
        return value.decode('utf8',errors='strict')

    def lossy_scale(self,native_transform):
        self._resolved('Transform','get_lossyScale_Injected','lossy_scale_bridge')
        for name in ('lossy_scale','world_rotation','world_matrix','inverse_rotation_times_world_matrix'):self._body(name)
        for row in PINS['constants']:
            expected=bytes.fromhex(row['raw_hex'])
            if self.n.file_bytes(row['rva'],len(expected),executable=False)!=expected or self.r.read(self.base+row['rva'],len(expected))!=expected:
                raise ValueError('Current Escape transform arithmetic constants changed')
        hierarchy,index=struct.unpack('<Qi',self.s.watch(native_transform+0x38,12))
        if not hierarchy:raise MapReadPending('Current Escape transform hierarchy unavailable')
        # Getter would synchronize/clear outstanding hierarchy work. Read-only
        # observation cannot perform those calls or assume their effects.
        if self._u64(hierarchy)!=0:raise MapReadPending('Current Escape hierarchy has pending synchronization work')
        array,parents=struct.unpack('<2Q',self.s.watch(hierarchy+0x18,16))
        if not array or not parents or index<0:raise MapReadPending('Current Escape transform arrays/index unavailable')
        chain=[];seen=set()
        while index>=0:
            if index in seen or index>100000 or len(chain)>=128:
                raise ValueError('Current Escape transform ancestry exceeds bound or cycles')
            seen.add(index);trs=struct.unpack('<12f',self.s.watch(array+48*index,48))
            chain.append((trs[4:8],trs[8:11]));index=self._i32(parents+4*index)
        result=lossy_scale_from_chain(chain)
        result.update(hierarchy=hierarchy,ancestor_count=len(chain),native_transform=native_transform)
        if not result['lossy_scale_threshold_verified']:
            raise MapReadPending('Current Escape scale lies within float32 selector uncertainty bounds')
        return result


def read_back_graphic_native(session,graphic,frame,*,back_tag=None,native=None):
    """Actual current Graphic facts; caller still resolves receiver/root ownership.

    `back_tag` must be the observed UIBackButtonExecuteBinder static BackButtonTag.
    Missing caches are not populated by invoking Unity. This result does not prove
    dictionary ownership, lack of competing tagged GameObjects, or a selected
    handler. No physical point or input command is generated.
    """
    if not isinstance(back_tag,str) or not back_tag:raise MapReadPending('Current Escape binder BackButtonTag unavailable')
    reader=MaterialEscapeNative(session,native);ui=reader.n
    fields=session.base_fields(graphic,'Graphic','UnityEngine.UI')
    read=lambda name:reader._u64(graphic+fields[name])
    rect=read('m_RectTransform');canvas=read('m_Canvas');renderer=read('m_CanvasRenderer')
    if not all((rect,canvas,renderer)):raise MapReadPending('Current Escape graphic render caches unavailable')
    game_object=ui.component_game_object(graphic)
    if game_object!=ui.component_game_object(rect):raise ValueError('Current Escape graphic/transform owner differs')
    native_transform=ui.native_pointer(rect,{'RectTransform'})
    # Graphic's current RectTransform is checked against its actual component owner.
    # This avoids requiring Unity to create a managed Transform wrapper on demand.
    active=ui.native_active_cached(game_object)
    tag=reader.tag(game_object);instance=reader.instance_id(game_object)
    scale=reader.lossy_scale(native_transform)
    sorting=ui.scalar(canvas,'Canvas','get_sortingOrder')
    depth=ui.scalar(renderer,'CanvasRenderer','get_absoluteDepth')
    session.assert_stable()
    result=dict(scale,game_object=game_object,game_object_native=game_object,
        game_object_managed=None,graphic_managed=graphic,rect_transform_managed=rect,
        instance_id=instance,tag=tag,back_tag=back_tag,back_tag_verified=tag==back_tag,
        tag_comparison_verified=True,tag_matches=tag==back_tag,
        active_in_hierarchy=active,sorting_order=sorting,depth=depth,confirmed=True,
        source=SOURCE_REF,source_version=SOURCE_VERSION,
        source_hashes=[x['sha256'] for x in PINS['source_files']],
        observed_at=datetime.now(timezone.utc).isoformat(),live_verified=False)
    for key in ('frame_id','window_signature','session_id','character_id','process_started_at','input_epoch'):
        if key in frame:result[key]=frame[key]
    return result
