"""Actual owned popup dimmer close, using source registration and current geometry.

No game functions are invoked. Every observation uses the caller's existing
read-only session. The exposed rectangle only selects a probe; it is never an
occlusion-clear claim. The shared final-target supplier authorizes a click.
"""
from mini_masukja.native_type_reference import type_definition_index
from copy import deepcopy
from functools import lru_cache
import hashlib
import json
import math
import struct

from .paths import root
from .material_escape import EscapeView
from .material_return import MMML_REVISION
from .vendor.map_memory import MapReadPending
from .quest_ui_actions import QuestUIActionSession,NativeUnityUI,prepare_pointer_proposal

SOURCE_REF='data/arbeit-material-return/dimmer-source.json'
TARGET_ID='material_return.close.detail'


@lru_cache(maxsize=1)
def dimmer_source():
    return json.loads((root()/SOURCE_REF).read_text(encoding='utf8'))


def exposed_rectangle(dimmer,popup,safe):
    """Largest axis-aligned visible band after subtracting the current panel."""
    if any(not isinstance(r,(list,tuple)) or len(r)!=4 for r in (dimmer,popup,safe)):
        raise ValueError('current dimmer/popup/safe rectangles required')
    if any(type(v) not in (int,float) or not math.isfinite(v) for r in (dimmer,popup,safe) for v in r):
        raise ValueError('nonfinite current dimmer geometry')
    if min(*dimmer[2:],*popup[2:])<=0:raise ValueError('empty current dimmer/popup geometry')
    l,t=max(dimmer[0],safe[0]),max(dimmer[1],safe[1])
    r,b=min(dimmer[0]+dimmer[2],safe[2]),min(dimmer[1]+dimmer[3],safe[3])
    if l>=r or t>=b:raise MapReadPending('current dimmer has no safe visible area')
    pl,pt=max(l,popup[0]),max(t,popup[1]);pr,pb=min(r,popup[0]+popup[2]),min(b,popup[1]+popup[3])
    bands=([(l,t,r-l,b-t)] if pl>=pr or pt>=pb else
           [(l,t,r-l,pt-t),(l,pb,r-l,b-pb),(l,pt,pl-l,pb-pt),(pr,pt,r-pr,pb-pt)])
    # One physical pixel inset retains the shared probe's strict interior rule.
    bands=[[x+1,y+1,w-2,h-2] for x,y,w,h in bands if w>=6 and h>=6]
    if not bands:raise MapReadPending('current popup covers all safely probeable dimmer pixels')
    return max(bands,key=lambda r:r[2]*r[3])


class DimmerSession(QuestUIActionSession):
    def root_game_object(self,pointer,native):
        if self.reader.class_name(self.reader.u64(pointer))==('UIGameObject','MM.Client.Framework.UI.Object'):
            managed=DimmerView(self).value(pointer,'UIGameObject','gameObject')
            return native.native_pointer(managed,{'GameObject'})
        return super().root_game_object(pointer,native)


def dimmer_session(session):
    proxy=DimmerSession.__new__(DimmerSession);proxy.__dict__=session.__dict__;return proxy


class DimmerView(EscapeView):
    def __init__(self,session):
        self.s=dimmer_session(session);self.r=session.reader;self.source=dimmer_source()
        if any(session.plan.get(k)!=self.source[k] for k in ('game_assembly_sha256','metadata_sha256')):
            raise ValueError('current dimmer source/transport version mismatch')

    def delegate(self,pointer):
        if self.value(pointer,'MulticastDelegate','delegates'):
            raise MapReadPending('current dimmer callback is multicast')
        return dict(pointer=pointer,method=self.value(pointer,'Delegate','method_ptr'),
                    target=self.value(pointer,'Delegate','m_target'))

    def current_registration(self,root_object):
        """Implemented with the exact ClickUIEventSpot table, never a heap scan."""
        system=self.s.ui_system_root()
        dispatcher=self.value(system,'UISystem','clickEventDispatcher')
        if self.value(dispatcher,'ClickEventDispatcher','anyActionCalledThisFrame'):
            raise MapReadPending('current click dispatcher has already consumed this frame')
        events=self.value(system,'UISystem','<EventSystem>k__BackingField')
        if events!=self.value(dispatcher,'ClickEventDispatcher','eventSystem'):
            raise ValueError('current dimmer click event system root differs')
        picker=self.value(events,'UnityUIEventSystem','eventSpotPicker')
        group=self.value(picker,'UIEventSpotPicker','eventSpotTables')
        return self.registered_click(group,root_object,dispatcher)

    def registered_click(self,group,root_object,dispatcher):
        tables=self.value(group,'UIEventSpotTableGroup','tables');matches=[]
        managed_go=self.value(root_object,'UIGameObject','gameObject')
        for _,table in self.s.reference_dictionary(tables,limit=64):
            klass=self.r.u64(table);shape=self.shape('UIEventSpotTable`1')
            if self.r.class_name(klass)!=(shape['name'],shape['namespace']):
                raise ValueError('current event spot table implementation changed')
            rows={f['name']:f for f in self.r.layout(klass,[f['name'] for f in shape['fields']])['fields']}
            for declared in shape['fields']:
                actual=rows[declared['name']]
                if actual['type_code']!=declared['type']['type_code'] or not 16<=actual['offset']<=2048:
                    raise ValueError('current closed event table field changed')
            # Verify the instantiated spotTable Dictionary<GameObject,IClickUIEventSpot>
            # through its actual FieldInfo type. Open generic offsets are never used.
            field_table=self.r.u64(klass+0x80)
            type_pointer=self.r.u64(field_table+8) # source spotTable field index 0
            if ((self.r.u32(type_pointer+8)>>16)&255)!=21:raise ValueError('current spot table field is not generic')
            generic=self.r.u64(type_pointer);inst=self.r.u64(generic+8)
            count=self.r.u64(inst);arguments=self.r.u64(inst+8)
            if count!=2:raise ValueError('current spot dictionary generic argument count changed')
            types=[self.r.u64(arguments+8*i) for i in range(2)]
            args=[(type_definition_index(self.s, p),(self.r.u32(p+8)>>16)&255) for p in types]
            if args!=[(73587,18),(54976,18)]:continue
            pointers={key:struct.unpack('<Q',self.s.watch(table+rows[key]['offset'],8))[0]
                      for key in ('spotTable','lifeCycleTable','slotTable')}
            spots=self.s.reference_dictionary(pointers['spotTable'],limit=512)
            lifecycle=self.s.reference_dictionary(pointers['lifeCycleTable'],limit=512)
            slots=self.s.reference_dictionary(pointers['slotTable'],limit=512)
            selected=[spot for obj,spot in spots if obj==managed_go]
            if len(selected)>1:raise ValueError('current dimmer has duplicate click spots')
            if not selected:continue
            spot=selected[0]
            if self.value(spot,'ClickUIEventSpot','target')!=root_object:
                raise MapReadPending('current click spot target differs from dimmer root')
            handler=self.value(spot,'ClickUIEventSpot','handler')
            action=self.delegate(self.value(handler,'DelegateClickEventHandler','action'))
            if action['method']!=self.s.base+0x743cbf0:raise MapReadPending('current dimmer dispatcher wrapper changed')
            closure=action['target']
            if self.value(closure,'<>c__DisplayClass3_0','<>4__this')!=dispatcher:
                raise MapReadPending('current dimmer wrapper owns another dispatcher')
            wrapped=self.delegate(self.value(closure,'<>c__DisplayClass3_0','action'))
            if wrapped['method']!=self.s.base+0x16a8d60:raise MapReadPending('current SetOnClick action wrapper changed')
            callback=self.delegate(self.value(wrapped['target'],'<>c__DisplayClass8_0','action'))
            matches.append(dict(table_pointer=table,spot_pointer=spot,handler_pointer=handler,
                root_object_pointer=root_object,game_object_managed=managed_go,dispatcher_pointer=dispatcher,
                dispatcher_action=action,set_onclick_action=wrapped,callback=callback,
                lifecycle_rows=[(o,g) for o,g in lifecycle if o==root_object],
                slot_rows=[(g,o) for g,o in slots if g==managed_go]))
        if len(matches)!=1:raise MapReadPending('current exact dimmer click registration not unique')
        return matches[0]

    def current_window(self,panel,binding):
        service=self.value(panel['handle'],'MMMLUIHandle','service')
        table=self.value(service,'MMMLService','entryMap')
        rows=[p for k,p in self.s.dictionary_values(table,key_type='Int32',value_type='UnityMMMLWindow',limit=4096) if k==binding['window_id']]
        if len(rows)!=1:raise MapReadPending('current owned popup window not unique')
        window=rows[0]
        if self.value(window,'UnityMMMLWindow','windowId')!=binding['window_id']:
            raise ValueError('current popup window ID changed')
        if any(self.value(window,'MMMLWindow',key) for key in ('closing','needCheckLayoutDirty')):
            raise MapReadPending('current popup closing or layout dirty')
        return window

    def popup_rectangle(self,window,canvas,frame,native):
        table=self.value(window,'UnityMMMLWindow','elements')
        rows=self.s.dictionary_values(table,key_type='ElementId',value_type='MMMLElementBase',limit=4096)
        selected=[(key,p) for key,p in rows if self.r.class_name(self.r.u64(p))==
                  ('MMMLBottomSheetElement','MM.Client.Presentation.UI.Service.MMML.Elements')]
        if len(selected)!=1:raise MapReadPending('current exact BottomSheet element not unique')
        key,element=selected[0]
        if not self.value(element,'MMMLBottomSheetElement','closable'):
            raise MapReadPending('current detail BottomSheet not closable')
        if self.value(element,'MMMLBottomSheetElement','needSizeAdjustment'):
            raise MapReadPending('current detail BottomSheet geometry still adjusting')
        control=self.value(element,'MMMLBottomSheetElement','control')
        draggable=self.value(control,'BottomSheetControl','draggablePanelControl')
        rect=self.value(draggable,'DraggablePanelControl','target')
        if not native.native_active_cached(native.component_game_object(rect)):
            raise MapReadPending('current detail native parent inactive')
        physical=frame['physical_client_rect'];viewport=(physical[2]-physical[0],physical[3]-physical[1])
        geometry=native.overlay_rect(rect,canvas,viewport)
        return dict(element_id=key,element_pointer=element,bottom_sheet_control=control,
                    draggable_control=draggable,rect_transform=rect,rect_client=geometry['rect_client'],
                    source_hashes=geometry['source_hashes'])

    def observe(self,binding,frame):
        self.verify_methods()
        panel=self.owned_panel(binding,require_back=False)
        player=self.value(panel['pointer'],'MMMLPopupUIInstanceBase','player')
        if self.value(player,'UIStackingPlayer','instance')!=panel['pointer']:
            raise MapReadPending('current dimmer player owns another popup')
        dimmer=self.value(player,'UIStackingPlayer','blackDimmer')
        root_object=self.value(dimmer,'BlackDimmerUI','<UIObject>k__BackingField')
        if self.disposed(root_object):raise MapReadPending('current dimmer root disposed')
        graphic=self.value(dimmer,'BlackDimmerUI','image')
        registration=self.current_registration(root_object)
        callback=registration['callback']
        if callback['target']!=panel['pointer'] or callback['method']!=self.s.base+0xae7d5d0:
            raise MapReadPending('current dimmer callback does not close the exact owned popup')
        window=self.current_window(panel,binding)
        geometry=self.s.read_graphic_geometry(graphic,root_object,registration['handler_pointer'],panel['pointer'],frame)
        native=self.s._native_ui
        gf=self.s.base_fields(graphic,'Graphic','UnityEngine.UI')
        canvas=struct.unpack('<Q',self.s.watch(graphic+gf['m_Canvas'],8))[0]
        popup=self.popup_rectangle(window,canvas,frame,native)
        safe=(frame.get('current_context') or {}).get('safe_rect')
        rect=exposed_rectangle(geometry['rect_client'],popup['rect_client'],safe)
        candidate=dict(geometry,rect_client=rect,full_dimmer_rect=geometry['rect_client'],
            index=popup['element_id'],tag='CurrentOwnedPopup.Dimmer',handler_pointer=registration['handler_pointer'],
            model_event_id=panel['event_id'],window_id=binding['window_id'],window_kind='owned-popup-dimmer',
            event_kind='registered-click-popup-close')
        candidate['native_geometry_proof']=dict(candidate['native_geometry_proof'],
            clipped_to_exposed_dimmer=True,full_dimmer_rect=geometry['rect_client'],popup_rect=popup['rect_client'])
        semantics=dict(source_family=binding['source_family'],recipe_id=binding['recipe_id'],raw_recipe_id=panel['recipe'],
            panel=panel,player_pointer=player,dimmer_pointer=dimmer,registration=registration,popup=popup,
            full_dimmer_rect=geometry['rect_client'],safe_rect=safe,source_revision=MMML_REVISION,
            close_effect='current registered IStackableUIInstance.Close on this popup')
        snapshot=dict(target_id=TARGET_ID,ui_type='CurrentOwnedPopup.Dimmer',source_hash=MMML_REVISION,
            window_id=binding['window_id'],ui_pointer=panel['pointer'],pointer_semantics=semantics,
            candidates=[candidate],pointer_state=self.s.current_pointer_state())
        self.s.assert_stable()
        return candidate,snapshot


def _binding(ui):
    pointer=ui.get('ui_pointer',ui.get('panel_id'));pointer=int(pointer,16) if isinstance(pointer,str) else pointer
    return dict(target_id=TARGET_ID,target_ui_pointer=pointer,source_family=ui['source_family'],
        recipe_id=ui['recipe_id'],window_id=ui['window_id'],event_id=ui['close']['callback']['event_id'])


def attach_material_dimmer(session,ui,frame):
    if ui.get('surface')!='detail':return ui
    ui.pop('dimmer_close',None)
    try:
        candidate,snapshot=DimmerView(session).observe(_binding(ui),frame)
        ui['dimmer_close']=dict(candidate=candidate,snapshot=snapshot)
    except (OSError,RuntimeError,ValueError,KeyError,TypeError,AttributeError) as exc:
        ui.setdefault('close_missing',[]).append('current owned dimmer close: '+str(exc))
    return ui


def bind_dimmer_target(ui,snapshot):
    from .arbeit_operations import _fresh
    view=ui.get('dimmer_close') or {}
    if not view:raise ValueError('current_detail_popup_close_control_not_yet_source_joined')
    candidate=view['candidate'];typed=view['snapshot'];frame=ui['frame']
    process=snapshot.get('process_started_at') or snapshot.get('window_preflight',{}).get('selected',{}).get('process_started_at')
    for key in ('session_id','character_id','process_started_at'):
        current=process if key=='process_started_at' else snapshot.get(key)
        if not frame.get(key) or frame[key]!=ui.get(key) or frame[key]!=current:
            raise ValueError('current dimmer frame owner changed')
    if not _fresh(dict(frame,source='current bound frame',observed_at=frame.get('captured_at')),snapshot):
        raise ValueError('current dimmer frame stale')
    if (typed['ui_pointer']!=_binding(ui)['target_ui_pointer'] or typed['window_id']!=ui['window_id'] or
        candidate['model_event_id']!=ui['close']['callback']['event_id']):
        raise ValueError('current dimmer popup/model changed')
    pointer=prepare_pointer_proposal(candidate,typed,dict(frame,input_owner=snapshot.get('input_owner')))
    pointer['probe_context'].update(source_family=ui['source_family'],recipe_id=ui['recipe_id'])
    physical=frame['physical_client_rect'];point=pointer['point_client']
    x,y,w,h=candidate['rect_client']
    return dict(pointer,status='ready',occlusion=pointer.get('occlusion'),point_desktop=[point[0]+physical[0],point[1]+physical[1]],
        rect_client=deepcopy(candidate['rect_client']),source_rect=[x,y,x+w,y+h],
        safe_rect=list(typed['pointer_semantics']['safe_rect']),
        source='current owned popup dimmer registered Close + exposed native rect + final pointer target',
        source_refs=[SOURCE_REF+'#sha256='+hashlib.sha256((root()/SOURCE_REF).read_bytes()).hexdigest()],
        candidate_index=candidate['index'],event_id=candidate['model_event_id'],tag=candidate['tag'],
        frame_id=frame['frame_id'],window_signature=frame['window_signature'],observed_at=frame['captured_at'],
        pointer_observed_at=typed['pointer_state']['observed_at'],runtime_input_authorized=False,live_verified=False)


def dimmer_scene_reader(session,context,frame):
    if frame.get('material_return_kind',context.get('source_family'))!=context.get('source_family'):
        raise MapReadPending('current dimmer return source family changed')
    binding=dict(target_id=TARGET_ID,target_ui_pointer=context['target_ui_pointer'],source_family=context['source_family'],
        recipe_id=context['recipe_id'],window_id=context['window_id'],event_id=context['model_event_id'])
    return DimmerView(session).observe(binding,frame)
