"""Source-bound Collection wheel axes. This module never opens the game."""
from functools import lru_cache
import hashlib,json,math,struct
from .paths import root
from .vendor.map_memory import MapReadPending
SOURCE_REF='data/r7-scroll-source/axis.json'
SOURCE_HASH='23f44490e9c42a30e281764259d3bc3384ddb07a1fb55cc9aa9dca0b88273014'
@lru_cache(maxsize=1)
def axis_source():
    raw=(root()/SOURCE_REF).read_bytes()
    if hashlib.sha256(raw).hexdigest()!=SOURCE_HASH:raise ValueError('collection wheel axis source changed')
    value=json.loads(raw)
    if hashlib.sha256((root()/value['source_ref']).read_bytes()).hexdigest()!=value['source_sha256']:
        raise ValueError('collection wheel receiver source changed')
    return value

def source_axis(constraint,direction):
    rows=[r for r in axis_source()['contract'].values() if r['constraint']==constraint and r['direction']==direction]
    if len(rows)!=1:raise MapReadPending('current collection compound axis needs its own source route')
    return dict(rows[0])

def projected_axis(corners,binding):
    """Actual native basis, with the source wheel sign. No pixel inference."""
    rule=source_axis(binding['scroll_constraint'],binding['scroll_direction'])
    if len(corners)!=4 or any(len(p)!=3 or any(not math.isfinite(v) for v in p) for p in corners):
        raise MapReadPending('current collection native scroll basis unavailable')
    x=[corners[1][i]-corners[0][i] for i in range(3)]
    y=[corners[3][i]-corners[0][i] for i in range(3)]
    if abs(x[0])<1e-6 or abs(y[1])<1e-6 or max(abs(x[1]),abs(x[2]),abs(y[0]),abs(y[2]))>1e-5:
        raise MapReadPending('current collection native scroll axes rotated or degenerate')
    index=rule['axis_index'];physical=x[0] if index==0 else -y[1]
    sign=1 if physical>0 else -1
    return dict(axis_index=index,screen_axis='x' if index==0 else 'y',layout_to_screen_sign=sign,
        wheel_to_screen_sign=binding['wheel_parent_screen_sign']*rule['native_wheel_sign']*binding['adapter_position_sign'],scroll_constraint=rule['constraint'],
        scroll_direction=rule['direction'],compound_pointer=binding['compound'],handler_pointer=binding['handler'],
        control_pointer=binding['control'],adapter_type=binding['adapter_type'],adapter_position_sign=binding['adapter_position_sign'],source_ref=SOURCE_REF,source_revision=SOURCE_HASH)

def scroll_direction(axis,target,visible):
    """One normal wheel notch toward a strictly outside cached target center."""
    sign=axis['wheel_to_screen_sign']
    if sign not in (-1,1) or not visible or any(not math.isfinite(v) for v in [target,*visible]):
        raise ValueError('current source scroll direction unavailable')
    if target<min(visible)-1e-4:return 120*sign
    if target>max(visible)+1e-4:return -120*sign
    return None


def parent_screen_sign(session,native,managed_content,axis_index):
    """anchoredPosition moves in parent space, not the child's scaled X.

    rect_world verifies pinned source, every clean TRS ancestor and activity
    separately at the existing receiver boundary. Reuse that exact current
    native layout; do not infer this sign from one/two rendered item pixels.
    """
    from .quest_ui_actions import _apply_trs
    geometry=native.rect_world(managed_content)
    pointer=geometry.get('native_pointer');expected=geometry.get('hierarchy')
    if not pointer or not expected:raise MapReadPending('current scroll parent native basis unavailable')
    hierarchy,index=struct.unpack('<Qi',session.watch(pointer+0x38,12))
    if hierarchy!=expected:raise ValueError('current scroll native hierarchy owner changed')
    arrays=struct.unpack('<2Q',session.watch(hierarchy+0x18,16))
    if not all(arrays) or not 0<=index<=100000:raise MapReadPending('current scroll native parent arrays unavailable')
    parent=struct.unpack('<i',session.watch(arrays[1]+4*index,4))[0]
    if parent<0:raise MapReadPending('current scroll content has no parent rect basis')
    vector=(1.,0.,0.) if axis_index==0 else (0.,1.,0.);seen=set()
    while parent>=0:
        if parent in seen or parent>100000 or len(seen)>=127:raise ValueError('current scroll parent hierarchy invalid')
        seen.add(parent);v=struct.unpack('<12f',session.watch(arrays[0]+48*parent,48))
        if any(not math.isfinite(x) for x in v):raise MapReadPending('current scroll parent TRS invalid')
        vector=_apply_trs(vector,(0.,0.,0.),v[4:8],v[8:11])
        parent=struct.unpack('<i',session.watch(arrays[1]+4*parent,4))[0]
    if abs(vector[axis_index])<1e-6 or max(abs(vector[1-axis_index]),abs(vector[2]))>1e-5:
        raise MapReadPending('current scroll parent axis rotated or degenerate')
    physical=vector[0] if axis_index==0 else -vector[1]
    return 1 if physical>0 else -1
