"""Bounded native hierarchy reads for scroll routing; never invokes a getter.

The Transform component range is read from the same globals used by the pinned
GameObject.activeInHierarchy helper. A component's array position is not a type.
"""
import hashlib
import struct

from .vendor.map_memory import MapReadPending


class ScrollNativeHierarchy:
    def __init__(self,native,source):
        self.native=native;self.s=native.s;self.source=source
        from .quest_ui_actions import NATIVE_SOURCE
        if source['unity_player_sha256']!=NATIVE_SOURCE['unity_player_sha256']:
            raise ValueError('Scroll native hierarchy/geometry source versions differ')
        raw=bytes.fromhex(source['raw_hex'])
        if hashlib.sha256(raw).hexdigest()!=source['sha256']:
            raise ValueError('Scroll native hierarchy bundled source changed')
        native.pinned_entry('GameObject','get_activeInHierarchy',0xef540,298)
        if native.file_bytes(source['rva'],len(raw))!=raw or native.r.read(native.base+source['rva'],len(raw))!=raw:
            raise ValueError('Scroll native hierarchy loaded source changed')
        native.pinned_entry('Transform','get_position_Injected',0x8c3a0,329)
        for rva,size in ((0x5469b0,63),(0x546750,420),(0x54c680,422)):
            if native.r.read(native.base+rva,size)!=native.file_bytes(rva,size):
                raise ValueError('Scroll native hierarchy Transform source changed')
        self.lower=self.unpack('<I',native.base+source['transform_type_base_rva'])
        self.width=self.unpack('<I',native.base+source['transform_type_width_rva'])
        if not 0<self.width<=65536:raise MapReadPending('Current Transform type range unavailable')

    def unpack(self,fmt,address):
        return struct.unpack(fmt,self.s.watch(address,struct.calcsize(fmt)))[0]

    def transform(self,game_object):
        entries=self.unpack('<Q',game_object+0x30);count=self.unpack('<Q',game_object+0x40)
        if not entries or not 0<count<=512:raise MapReadPending('Current GameObject component array unavailable')
        matches=[]
        for i in range(count):
            kind=self.unpack('<I',entries+16*i)
            # Source opcode 72 (JB), exclusive upper bound and uint32 wrapping.
            if ((kind-self.lower)&0xffffffff)<self.width:
                matches.append(self.unpack('<Q',entries+16*i+8))
        if len(matches)!=1 or not matches[0]:raise MapReadPending('Current unique native Transform component unavailable')
        transform=matches[0]
        if self.unpack('<Q',transform+0x30)!=game_object:
            raise ValueError('Current native Transform/GameObject identity changed')
        return transform

    def chain(self,game_object):
        """Child first native GO chain, cross-checked against clean TRS indices."""
        out=[];seen=set();transform=self.transform(game_object);expected=None
        bit=self.unpack('<i',self.native.base+0x1a914a0)
        if not 0<=bit<64:raise MapReadPending('Current Transform dirty bit unavailable')
        while transform:
            if transform in seen or len(seen)>=128:raise ValueError('Current scroll native ancestry exceeds bound')
            seen.add(transform);go=self.unpack('<Q',transform+0x30)
            if not go or (out and self.transform(go)!=transform):
                raise ValueError('Current native parent Transform identity differs')
            hierarchy,index=struct.unpack('<Qi',self.s.watch(transform+0x38,12))
            if not hierarchy or not 0<=index<=100000 or (expected and (hierarchy,index)!=expected):
                raise ValueError('Current native parent hierarchy/index differs')
            parents=self.unpack('<Q',hierarchy+0x20);dirty=self.unpack('<Q',hierarchy+0x48)
            if not parents or not dirty:raise MapReadPending('Current native hierarchy arrays unavailable')
            if self.unpack('<Q',dirty+8*index)&(1<<bit):raise MapReadPending('Current native scroll ancestor dirty')
            parent_index=self.unpack('<i',parents+4*index)
            parent=self.unpack('<Q',transform+0x90)
            if bool(parent)!=(parent_index>=0):raise ValueError('Current native parent pointer/index differs')
            out.append(go);expected=(hierarchy,parent_index);transform=parent
        if not out or out[0]!=game_object:raise ValueError('Current native scroll target identity differs')
        return out


def select_scroll_route(hits,registered,target,*,chain,active,penetrable):
    """Mirror the source's orthogonal filtering/dispatch-before-block ordering.

    A foreign registered receiver is never skipped, even if penetrable. This
    planner owns one list; it cannot authorize a wheel that also moves another.
    """
    previous=None;trace=[];selected=None
    for hit in hits:
        go=hit['native_game_object'];ancestry=chain(go)
        if previous and go in previous[1:]:
            trace.append(dict(target=go,skipped='strict_ancestor_of_previous'))
            continue
        previous=ancestry
        receivers=[p for p in ancestry if p in registered]
        if receivers:
            receiver=receivers[0]
            if receiver!=target or selected:return None
            if not active(go):return None
            selected=dict(first_target=go,ancestor_prefix=ancestry[:ancestry.index(target)+1],prior_hits=list(trace))
        is_active=active(go)
        passage=penetrable(hit['game_object']) if is_active else None
        trace.append(dict(target=go,active=is_active,penetration=passage))
        if is_active and not (passage and passage['is_penetrable'] is True):break
    if selected:selected['route_trace']=trace
    return selected
