"""Source-pinned current MMML parent supplier. Read-only and disabled by default.

The transport is the existing bounded QUERY_INFORMATION|VM_READ implementation.
Only current UI instance dictionaries are traversed: no heap scans, writes,
game calls, injected code, privilege changes or screen input. Import is inert.
"""
from __future__ import annotations
from mini_masukja.native_type_reference import type_definition_index
import math
import struct
import hashlib
from pathlib import Path
from datetime import datetime,timezone
from .arbeit_targets import ui_map
from .vendor.map_memory import MapMemorySession,MapReadPending
from .vendor.map_character_context import _class
from .vendor.map_nullable import read_nullable_int32

NS='MM.Client.Presentation.UI.Instances.StageScene'
TARGET_FOR_OP={'dialogue.accept':'dialogue.accept','dialogue.report':'dialogue.report',
               'objective.deliver':'delivery.submit'}
FACT_FOR_TARGET={'dialogue.accept':'accept_caption','dialogue.report':'report_caption',
                 'delivery.submit':'delivery_submit_caption','delivery.item':'delivery_item'}


class XForestView:
    """Bounded current MMML model reader. Never evaluates the UI VM.

    The retained XForest.Uncompact/FromElement/AsString and ToBool file bytes
    establish CompactValue's inline layout and string/element interpretation.
    Only source-selected attribute names and children of this model root are read.
    """
    def __init__(self,session,forest):
        self.s=session;self.r=session.reader;self.forest=forest
        self.fields=session.source_fields(forest,'XForest')

    def dynamic_fields(self,klass,name):
        source=self.s.source_shape(name)
        if self.r.class_name(klass)!=(name,source['namespace']):raise ValueError('Unexpected UI collection type: '+name)
        return {f['name']:f for f in self.r.layout(klass,[f['name'] for f in source['fields']])['fields']}

    def dense(self,pointer,*,key_name,key_stride,value_name,value_stride,limit=4096):
        info=self.r.dictionary(pointer)
        if info['kind']!='manual_dense' or not 0<=info['count']<=limit:raise ValueError('Unsupported UI model dictionary')
        self.s.watch(pointer+info['version_offset'],4);self.s.watch(pointer+info['count_offset'],4)
        keys,values=info['arrays']['keys'],info['arrays']['values']
        if (keys['name'],keys['stride'],values['name'],values['stride'])!=(key_name,key_stride,value_name,value_stride):
            raise ValueError('UI model dictionary element types changed')
        count=info['count'];ka=int(keys['pointer'],16)+32;va=int(values['pointer'],16)+32
        if count:
            self.s.watch(ka,count*key_stride);self.s.watch(va,count*value_stride)
        return info,[(ka+i*key_stride,va+i*value_stride) for i in range(count)]

    def list_entries(self,field,element_id,*,attributes=False):
        collection=self.s.ref(self.forest,self.fields,field)
        klass=self.r.u64(collection)
        if self.r.class_name(klass)!=('MMDictionaryOfList`2','Silvervine.ManualMemory'):
            raise ValueError('Unexpected current XForest list dictionary')
        parent=self.r.u64(klass+0x58);f=self.dynamic_fields(parent,'MMReadOnlyDictionaryOfList`2')
        self.s.watch(collection+f['version']['offset'],4)
        table=self.r.u64(collection+f['table']['offset']);array=self.r.u64(collection+f['array']['offset'])
        info,rows=self.dense(table,key_name='ElementId[]',key_stride=4,value_name='List[]',value_stride=8)
        matches=[v for k,v in rows if self.r.i32(k)==element_id]
        if not matches:return []
        if len(matches)!=1:raise ValueError('Duplicate current UI element ID')
        layout=info['arrays']['values']['element_layout']
        source=self.s.source_shape('List','')
        actual=layout['fields']
        if [(f['name'],f['offset'],f['type_code']) for f in actual]!=[(f['name'],f['offset'],f['type']['type_code']) for f in source['fields']]:
            raise ValueError('Current UI list span type changed')
        offset,capacity,count=struct.unpack('<iHH',self.r.read(matches[0],8))
        if offset<0 or not 0<=count<=capacity or count>128:raise ValueError('Current UI list span exceeds bounds')
        if attributes:
            length,stride=self.s.array(array,name='ValueTuple`2[]',limit=20000)
            if not 12<=stride<=32:raise ValueError('Current UI attribute tuple size invalid')
            element_class=self.r.u64(self.r.u64(array)+0x40)
            shape=self.r.layout(element_class,['Item1','Item2']);fs=shape['fields']
            if [f['type_code'] for f in fs]!=[17,17]:raise ValueError('Current UI attribute tuple types changed')
            offsets=[f['offset']-16 for f in fs]
            if offsets[0]<0 or offsets[1]<offsets[0]+4 or offsets[1]+8>stride:raise ValueError('UI attribute fields exceed tuple')
        else:
            length,stride=self.s.array(array,name='ElementId[]',stride=4,limit=20000);offsets=None
        if offset+count>length:raise ValueError('Current UI list span exceeds array')
        start=array+32+offset*stride
        if count:self.s.watch(start,count*stride)
        return [(start+i*stride,offsets) for i in range(count)]

    def string_value(self,element_id,name_id):
        pointer=self.s.ref(self.forest,self.fields,'xvalueObjects')
        info,rows=self.dense(pointer,key_name='ValueTuple`2[]',key_stride=8,value_name='Object[]',value_stride=8)
        shape=info['arrays']['keys']['element_layout']
        if [(f['name'],f['offset'],f['type_code']) for f in shape['fields']]!=[('Item1',16,17),('Item2',20,17)]:
            raise ValueError('Current UI string key tuple changed')
        matches=[self.r.u64(v) for k,v in rows if struct.unpack('<ii',self.r.read(k,8))==(element_id,name_id)]
        if len(matches)!=1:raise MapReadPending('Current UI string source unavailable or ambiguous')
        pointer=matches[0]
        if not pointer or self.r.class_name(self.r.u64(pointer))!=('String','System'):
            raise ValueError('Current UI string source is not a String')
        return self.r.string(pointer)

    def attributes(self,element_id,names):
        enums=self.s.source_shape('Types','')['enum_constants'];out={}
        for address,offsets in self.list_entries('attributes',element_id,attributes=True):
            name_id=self.r.i32(address+offsets[0]);name=self.s.name_text(name_id)
            if name not in names:continue
            if name in out:raise ValueError('Duplicate current UI attribute '+name)
            kind,aux_high,aux_low,value=struct.unpack('<BBHi',self.r.read(address+offsets[1],8))
            if kind==enums['Boolean']:
                values=ui_map()['runtime_type_shapes']['xvalue_boolean_payloads']
                if value not in values.values():raise ValueError('Unsupported UI Boolean payload')
                out[name]=value==values['I32_TRUE']
            elif kind==enums['String']:out[name]=self.string_value(element_id,name_id)
            elif kind==enums['Name']:out[name]=self.s.name_text(value)
            elif kind==enums['Int32']:out[name]=value
            elif kind==enums['Element']:out[name]={'element_id':value,'forest':self.forest}
            elif kind==enums['Event']:out[name]={'event_present':True,'event_id':value}
            elif kind==enums['Null']:out[name]=None
            else:raise ValueError('Unsupported selected UI attribute type: '+name)
        return out

    def children(self,element_id):
        return [self.r.i32(address) for address,_ in self.list_entries('childrenInfo',element_id)]

    def selected_tree(self,root_id,names,*,limit=128,max_depth=6):
        queue=[(root_id,0)];seen=set();rows=[]
        while queue:
            element,depth=queue.pop(0)
            if element in seen:continue
            if len(seen)>=limit:raise ValueError('Current UI model exceeds selected traversal bound')
            seen.add(element);values=self.attributes(element,names)
            if values:rows.append({'element_id':element,'values':values})
            children=self.children(element)
            children+= [value['element_id'] for value in values.values() if isinstance(value,dict) and 'element_id' in value]
            children=list(dict.fromkeys(child for child in children if child not in seen))
            if children and depth>=max_depth:raise ValueError('Current UI model exceeds selected depth')
            queue.extend((child,depth+1) for child in children)
        return rows


class CurrentUILayoutSession(MapMemorySession):
    """Extend the already pinned map reader to two declared active UI types."""
    def declarations(self):
        """Subclasses may supply their own explicit metadata-hashed allowlist."""
        return ui_map()['memory_declarations']

    def window_id(self,handle):
        value=read_nullable_int32(self,handle,self.schema('MMMLUIHandle'),'WindowId')
        if value is None:raise MapReadPending('Active MMML handle has no WindowId')
        return value

    def source_shape(self,name,namespace=None):
        source=ui_map()['runtime_type_shapes']
        if any(source[k]!=self.plan[k] for k in ('metadata_sha256','game_assembly_sha256')):
            raise ValueError('Typed UI reader source versions differ')
        matches=[x for x in source['classes'] if x['name']==name and (namespace is None or x['namespace']==namespace)]
        if len(matches)!=1:raise ValueError('UI source type ambiguous or absent: '+name)
        return matches[0]

    def source_fields(self,pointer,name):
        return self.fields(self.reader.u64(pointer),self.source_shape(name))

    def inline_value(self,pointer,owner,field_name,struct_name,value_name):
        """Exact file-derived inline value with independently checked class shape."""
        shape=self.source_shape(owner);self.fields(self.reader.u64(pointer),shape)
        field=next(f for f in shape['fields'] if f['name']==field_name)
        if field['type'].get('name')!=struct_name or field['type']['type_code']!=17:
            raise ValueError('Unexpected inline UI identity type')
        value_shape=self.source_shape(struct_name)
        klass=_class(self.reader,self.base,value_shape);offsets=self.fields(klass,value_shape)
        part=next(f for f in value_shape['fields'] if f['name']==value_name)
        width={8:4,10:8}.get(part['type']['type_code'])
        if not width or offsets[value_name]!=16 or value_shape['type_definition_size_raw'][1]!=width:
            raise ValueError('Inline identity source shape changed')
        raw=self.watch(pointer+field['offset'],width)
        return struct.unpack('<i' if width==4 else '<q',raw)[0]

    def current_model(self,ui,fields):
        model=self.declared_ref(ui,fields,'viewModel')
        f=self.declared_fields(model,'MMMLUIViewModel`2')
        root=f['<ViewModelRoot>k__BackingField']
        if root['type_code']!=17:raise ValueError('Current UI model root is not XElem')
        shape=self.source_shape('XElem');klass=_class(self.reader,self.base,shape)
        parts=self.fields(klass,shape)
        if parts!={'Forest':16,'Id':24}:raise ValueError('XElem source layout changed')
        address=model+root['offset'];self.watch(address,16)
        forest=self.reader.u64(address);ident=self.reader.i32(address+8)
        if ident<0 or not forest:raise MapReadPending('Current UI model root unavailable')
        names={'uiData','generalButtons','specialGroups','buttons','buttonDesc','keywordId','displayName','isEnable','onClickEvent',
               'isDeliverButtonEnabled','deliverButtonText','deliverClicked','selectedDeliverContextIndex','targetDeliverItems',
               'deliverableItems','itemData'}
        view=XForestView(self,forest)
        # DeliverUI's source has one uiData descriptor. Do not traverse every
        # inventory icon just to read its submit state.
        root_values=view.attributes(ident,{'uiData'})
        if isinstance(root_values.get('uiData'),dict) and 'element_id' in root_values['uiData']:
            child=root_values['uiData']['element_id']
            values=view.attributes(child,{'isDeliverButtonEnabled','deliverButtonText','deliverClicked','selectedDeliverContextIndex'})
            rows=[{'element_id':ident,'values':root_values},{'element_id':child,'values':values}]
        else:
            rows=view.selected_tree(ident,names-{'targetDeliverItems','deliverableItems','itemData'})
        return {'root_element_id':ident,'rows':rows,'source':'current typed UI viewModel XElem/XForest attributes'}

    def declared_fields(self,pointer,name):
        if not pointer:raise MapReadPending('UI object unavailable: '+name)
        declarations=self.declarations()
        if declarations['metadata_sha256']!=self.plan['metadata_sha256']:
            raise ValueError('UI and map reader metadata revisions differ')
        matches=[x for x in declarations['types'] if x['name']==name]
        if len(matches)!=1:raise ValueError('UI declaration ambiguous or absent: '+name)
        decl=matches[0];klass=self.reader.u64(pointer)
        if self.reader.class_name(klass)!=(name,decl['namespace']):raise ValueError('UI class identity changed')
        shape=self.reader.layout(klass,[f['name'] for f in decl['fields']])
        return {f['name']:f for f in shape['fields']}

    def declared_ref(self,pointer,fields,name):
        field=fields[name]
        if field['type_code'] not in (0x12,0x15,0x1c,0x1d):
            raise ValueError('Expected reference field: '+name)
        self.watch(pointer+field['offset'],8)
        return self.reader.u64(pointer+field['offset'])

    def declared_scalar(self,pointer,fields,name,*,codes):
        field=fields[name];code=field['type_code'];address=pointer+field['offset']
        if code not in codes:raise ValueError('Unexpected source primitive type: '+name)
        formats={2:'<?',8:'<i',9:'<I',10:'<q',11:'<Q'}
        if code==14:
            self.watch(address,8);ref=self.reader.u64(address)
            return self.reader.string(ref) if ref else None
        if code not in formats:raise ValueError('Unsupported source scalar: '+name)
        if code==2:
            self.watch(address,1);return self.bool(address)
        size=struct.calcsize(formats[code]);raw=self.watch(address,size)
        return struct.unpack(formats[code],raw)[0]

    def delivery_contexts(self,component,fields):
        """Current requirements only. Selected item identities remain unbound."""
        pointer=self.declared_ref(component,fields,'deliverContexts')
        contexts=[]
        for index,entry in enumerate(self.refs(pointer,element='DeliverContext',limit=64)):
            f=self.declared_fields(entry,'DeliverContext')
            amount=self.declared_scalar(entry,f,'TargetAmount',codes=(10,11))
            if amount<0:raise ValueError('Invalid current delivery target amount')
            contexts.append({'index':index,'target_amount':amount,
                'target_display_name':self.declared_scalar(entry,f,'TargetDisplayName',codes=(14,)),
                'selected_item_ids':None,'selected_amount':None,
                'source':'current DeliverUIViewModelComponent.deliverContexts'})
        return contexts

    def enum_value(self,pointer,owner,field_name,enum_name):
        # These source enum definitions have no cached-class slots. Validate
        # the actual owning FieldInfo's exact definition instead of requiring
        # a boxed enum class that may never be initialized by the game.
        r=self.reader;shape=self.source_shape(owner);klass=r.u64(pointer)
        offsets=self.fields(klass,shape);field=next(f for f in shape['fields'] if f['name']==field_name)
        enum=self.source_shape(enum_name);underlying=next(f for f in enum['fields'] if f['name']=='value__')
        if (field['type'].get('definition_index')!=enum['type_index'] or field['type']['type_code']!=17
                or underlying['type']['type_code']!=8 or underlying['offset']!=16
                or enum['type_definition_size_raw'][1]!=4):
            raise ValueError('Unexpected source dialogue enum shape')
        type_pointer=r.u64(r.u64(klass+0x80)+32*field['index']+8)
        if type_definition_index(self, type_pointer)!=enum['type_index']:
            raise ValueError('Current dialogue enum definition changed')
        value=struct.unpack('<i',self.watch(pointer+offsets[field_name],4))[0]
        choices=enum['enum_constants']
        names=[name for name,number in choices.items() if number==value]
        if len(names)!=1:raise ValueError('Unknown current dialogue enum: '+enum_name)
        return {'value':value,'name':names[0]}

    def dialogue_context(self,component,helper,helper_fields):
        """Current speaker and ordered UI selection records, not quest ownership.

        The source UI component is owned by the same active UI instance as the
        keyword helper. Its model pointer must equal that helper's model. A
        speaker can be an actor in a dialogue, so it is not called the giver.
        """
        owner='DialogueInteractionUIComponent'
        f=self.declared_fields(component,owner)
        model=self.declared_ref(component,f,'dialogueKeywordUIViewModel')
        if not model or model!=self.declared_ref(helper,helper_fields,'viewModel'):
            raise MapReadPending('Dialogue component/helper current model differs')
        result={'source':'current DialogueInteractionUIInstance-owned component',
                'speaker_npc_id':self.inline_value(component,owner,'currentSpeakerId','NPCSheetId','Id'),
                'prompt_type':self.enum_value(component,owner,'promptType','DialoguePromptType'),
                'is_keyword_displaying':self.declared_scalar(component,f,'isKeywordDisplaying',codes=(2,)),
                'keywords_applied_this_frame':self.declared_scalar(component,f,'keywordsAppliedThisFrame',codes=(2,)),
                'keyword_rows':[], 'quest_binding':None,
                'quest_binding_gap':'DialoguePromptSelectionKeyword has no QuestInstanceId/QuestStaticDataId; current DialogueId-to-owned-quest relation is unresolved'}
        pointer=self.declared_ref(component,f,'promptSelectionKeywords')
        for index,entry in enumerate(self.refs(pointer,element='DialoguePromptSelectionKeyword',limit=128)):
            row_owner='DialoguePromptSelectionKeyword';fields=self.declared_fields(entry,row_owner)
            result['keyword_rows'].append({'selection_index':index,
                'type':self.enum_value(entry,row_owner,'Type','DialoguePromptSelectionType'),
                'text':self.declared_scalar(entry,fields,'Text',codes=(14,)),
                'enabled':self.declared_scalar(entry,fields,'IsEnable',codes=(2,)),
                'disabled_reason':self.declared_scalar(entry,fields,'DisabledReasonText',codes=(14,))})
        # Retain the actual index -> Name relation separately from OCR text and
        # from the list order. There is no string-based keyword-ID parser.
        names=self.declared_ref(component,f,'keywordNames')
        if names:
            mapping={}
            for index,name_id in self.keyword_name_entries(names):
                if index in mapping or not 0<=index<len(result['keyword_rows']):
                    raise MapReadPending('Current dialogue keyword index map is stale or ambiguous')
                mapping[index]=self.name_text(name_id)
            for row in result['keyword_rows']:row['keyword_id']=mapping.get(row['selection_index'])
        return result

    def keyword_name_entries(self,pointer):
        """Decode only MMDerivedDictionary<int,Name>, with closed generic fields.

        The old shared dictionary reader accepts MMDictionary, not this derived
        collection. Do not pretend these runtime class names are interchangeable.
        """
        r=self.reader;klass=r.u64(pointer);common=None
        if r.class_name(klass)!=('MMDerivedDictionary`2','Silvervine.ManualMemory'):
            raise ValueError('Current keyword name map is not MMDerivedDictionary')
        allowed={'MMDerivedDictionary`2','MMReadOnlyDictionary`2','CommonDictionary`2'}
        for _ in range(4):
            name,namespace=r.class_name(klass)
            if name not in allowed:raise ValueError('Current keyword map base changed')
            source=self.source_shape(name,namespace)
            shape=r.layout(klass,[f['name'] for f in source['fields']])
            if name=='CommonDictionary`2':
                common={f['name']:f for f in shape['fields']};break
            klass=r.u64(klass+0x58)
        if common is None or set(common)!={'keys','values','hashTable','version','count'}:
            raise ValueError('Current keyword map common fields unavailable')
        for name in ('version','count'):
            if common[name]['type_code']!=8:raise ValueError('Current keyword map count type changed')
        count=struct.unpack('<i',self.watch(pointer+common['count']['offset'],4))[0]
        self.watch(pointer+common['version']['offset'],4)
        if not 0<=count<=128:raise ValueError('Current keyword map exceeds bound')
        arrays=[]
        for field,name,namespace in (('keys','Int32','System'),('values','Name','Silvervine.Namoo')):
            if common[field]['type_code']!=29:raise ValueError('Current keyword map array type changed')
            array=struct.unpack('<Q',self.watch(pointer+common[field]['offset'],8))[0]
            capacity,stride=self.array(array,name=name+'[]',stride=4,limit=30000)
            if count>capacity:raise ValueError('Current keyword map array is truncated')
            if r.class_name(r.u64(r.u64(array)+0x40))!=(name,namespace):raise ValueError('Current keyword map element type changed')
            self.watch(array+24,8)
            arrays.append(struct.unpack('<'+'i'*count,self.watch(array+32,count*4)) if count else ())
        return list(zip(*arrays))

    def current_layout(self,target_id):
        """Read actual layout rectangles; missing semantic ownership stays absent."""
        self._guards=[];r=self.reader
        target=ui_map()['targets'][target_id]
        if not target.get('runtime_layout_tags'):raise MapReadPending('No typed current MMML supplier for '+target_id)
        source_range=ui_map()['current_mmml_source_range']
        path=Path(r.image).parent/Path(source_range['relative_path'])
        with path.open('rb') as stream:
            stream.seek(source_range['offset']);raw_source=stream.read(source_range['length'])
        if len(raw_source)!=source_range['length'] or hashlib.sha256(raw_source).hexdigest()!=source_range['raw_sha256']:
            raise ValueError('Current MMML source range changed; source layout must be rebuilt')
        wanted=('DeliverUIInstance',NS) if target_id.startswith('delivery.') else ('DialogueInteractionUIInstance',NS)
        entry=self.schema('PresentationSingleton');klass=_class(r,self.base,entry)
        fields=self.fields(klass,entry);instance=r.u64(r.u64(klass+0xb8)+fields['instance'])
        if r.u64(instance)!=klass:raise MapReadPending('Presentation singleton unavailable')
        service=r.u64(instance+fields['<UIInstanceService>k__BackingField'])
        f=self.object_fields(service,'UIInstanceService');table=self.ref(service,f,'handleTable')
        f=self.object_fields(table,'UIInstanceHandleTable');dictionary=self.ref(table,f,'handleTable')
        panels=[]
        for _,handle in self.dictionary_values(dictionary,limit=self.plan['limits']['instances']):
            if not handle or r.class_name(r.u64(handle))!=('UIInstanceHandle',''):continue
            shape=r.layout(r.u64(handle));h={x['name']:x['offset'] for x in shape['fields']}
            if not {'<Status>k__BackingField','<Instance>k__BackingField'}<=h.keys():raise ValueError('UI handle layout changed')
            self.watch(handle+h['<Status>k__BackingField'],4)
            if r.i32(handle+h['<Status>k__BackingField'])!=2:continue
            self.watch(handle+h['<Instance>k__BackingField'],8)
            ui=r.u64(handle+h['<Instance>k__BackingField'])
            if ui and r.class_name(r.u64(ui))==wanted:panels.append(ui)
        if len(panels)!=1:raise MapReadPending('Exact active UI count: '+str(len(panels)))
        ui=panels[0];f=self.declared_fields(ui,wanted[0]);dialogue_component=None
        if wanted[0]=='DialogueInteractionUIInstance':
            # DialogueKeywordUI is an owned helper, not a registered UIInstance.
            dialogue_component=self.declared_ref(ui,f,'dialogueInteractionUIComponent')
            ui=self.declared_ref(ui,f,'dialogueKeywordUI');f=self.declared_fields(ui,'DialogueKeywordUI')
        controller=self.declared_ref(ui,f,'controller')
        controller_name='DeliverUIController' if wanted[0]=='DeliverUIInstance' else 'DialogueKeywordUIController'
        cf=self.declared_fields(controller,controller_name)
        parent=r.u64(r.u64(controller)+0x58)
        base=self.fields(parent,self.schema('MMMLUIControllerBase'))
        self.watch(controller+base['handle'],8)
        handle=r.u64(controller+base['handle']);h=self.object_fields(handle,'MMMLUIHandle')
        self.watch(handle+h['shouldActive'],1)
        if not self.bool(handle+h['shouldActive']):raise MapReadPending('MMML handle inactive')
        window_id=self.window_id(handle);service=self.ref(handle,h,'service')
        sf=self.object_fields(service,'MMMLService');entries=self.ref(service,sf,'entryMap')
        windows=[ptr for key,ptr in self.dictionary_values(entries,key_type='Int32',value_type='UnityMMMLWindow',limit=4096) if key==window_id]
        if len(windows)!=1:raise MapReadPending('Current MMML window unavailable or ambiguous')
        window=windows[0];w=self.object_fields(window,'UnityMMMLWindow')
        if r.i32(window+w['windowId'])!=window_id:raise MapReadPending('Window identity changed')
        parent=r.u64(r.u64(window)+0x58);b=self.fields(parent,self.schema('MMMLWindow'))
        for field in ('closing','needCheckLayoutDirty'):self.watch(window+b[field],1)
        if self.bool(window+b['closing']) or self.bool(window+b['needCheckLayoutDirty']):raise MapReadPending('Current layout closing or dirty')
        context=r.u64(window+b['mainLayoutContext']);c=self.object_fields(context,'LayoutVMContext')
        vm=self.ref(context,c,'VM');v=self.object_fields(vm,'LayoutVM')
        program=self.ref(vm,v,'Program');p=self.object_fields(program,'LayoutProgram')
        infos=self.ref(program,p,'ElementBasicInfos');values=self.ref(vm,v,'values')
        count,_=self.array(infos,name='ElementBasicInfo[]',stride=8,limit=self.plan['limits']['layout_elements'])
        value_count,_=self.array(values,name='Single[]',stride=4,limit=self.plan['limits']['layout_elements']*8+10000)
        if count*8>value_count:raise ValueError('Current layout value array truncated')
        wanted_tags=set(target['runtime_layout_tags']);candidates=[]
        for i in range(count):
            tag=self.name_text(r.i32(infos+32+i*8))
            if tag not in wanted_tags:continue
            self.watch(infos+32+i*8,8)
            raw=self.watch(values+32+i*32,32);floats=struct.unpack('<8f',raw)
            rect=[floats[self.plan['layout_values'][k]] for k in ('left','top','width','height')]
            if not all(math.isfinite(x) for x in rect) or rect[2]<=0 or rect[3]<=0:continue
            # ElementBasicInfo.name is a source name ID, not a parent index.
            name=self.name_text(r.i32(infos+36+i*8))
            candidates.append({'tag':tag,'name':name,'rect_ui':rect,'index':i})
        for field,size in (('lastReferenceResolutionScale',4),('global_ScreenSize',8)):
            self.watch(service+sf[field],size)
        scale=self.float(service+sf['lastReferenceResolutionScale']);screen=self.pair(service+sf['global_ScreenSize'])
        if scale<=0:raise ValueError('Invalid current MMML scale')
        missing=['current exact quest/node ownership','current raycast occlusion/active modal ownership']
        details={'model':self.current_model(ui,f)}
        if wanted[0]=='DeliverUIInstance':
            component=self.declared_ref(controller,cf,'viewModelComponent')
            df=self.declared_fields(component,'DeliverUIViewModelComponent')
            details['component_pointer']=hex(component)
            details['declared_current_identity_fields']={k:df[k] for k in ('targetNPCSheetId','targetQuestInstanceId','targetStepId','targetObjectiveIndex','selectedDeliverContextIndex')}
            # Read only confirmed primitive types; inline structs are never
            # treated as integer sheet IDs or object pointers by assumption.
            for name in ('targetStepId','targetObjectiveIndex','selectedDeliverContextIndex'):
                field=df[name]
                if field['type_code']==8:
                    self.watch(component+field['offset'],4);details[name]=r.i32(component+field['offset'])
            field=df['<IsDelivering>k__BackingField']
            if field['type_code']==2:
                self.watch(component+field['offset'],1);details['is_delivering']=self.bool(component+field['offset'])
            details['is_delivered']=self.declared_scalar(controller,cf,'isDelivered',codes=(2,))
            details['button_text']=self.declared_scalar(component,df,'buttonText',codes=(14,))
            details['delivery_contexts']=self.delivery_contexts(component,df)
            details['target_npc_id']=self.inline_value(component,'DeliverUIViewModelComponent','targetNPCSheetId','NPCSheetId','Id')
            details['target_quest_instance_id']=self.inline_value(component,'DeliverUIViewModelComponent','targetQuestInstanceId','QuestInstanceId','int64Value')
            missing += ['join current accepted quest instance and current NPC','typed deliverable item ID and current reservation linkage']
        else:
            details['dialogue_context']=self.dialogue_context(dialogue_component,ui,f)
            missing += [details['dialogue_context']['quest_binding_gap'],
                        'current dialogue owner NPC differs conceptually from current speaker; giver ownership requires exact dialogue context']
        if self.ref(vm,v,'Program')!=program or self.ref(vm,v,'values')!=values:
            raise MapReadPending('Current MMML program changed during read')
        if any(r.read(address,len(raw))!=raw for address,raw in self._guards):
            raise MapReadPending('Current UI changed during read')
        return {'target_id':target_id,'ui_type':wanted[0],'window_id':window_id,'scale':scale,'screen_ui':list(screen),
                'candidates':candidates,'source_hash':target['evidence'][0]['payload_sha256'],
                'captured_at':datetime.now(timezone.utc).isoformat(),'layout_stable':True,
                'source':'current typed MMML LayoutVM outputs; no VM execution','source_range_verified':True,'missing':missing,'details':details,
                'memory_writes':0,'game_inputs':0}


class CurrentUILayoutReader:
    """Production adapter; live memory access requires an explicit opt-in."""
    def __init__(self,*,enabled=False,session_factory=CurrentUILayoutSession):
        self.enabled=enabled;self.session_factory=session_factory

    def read(self,*,frame,node,semantic):
        if not self.enabled:
            return {'evidence_kind':'unavailable','missing':['read-only UI layout access disabled'],'game_inputs':0}
        target_id=TARGET_FOR_OP.get(node.get('op'))
        reservation=semantic.get('facts',{}).get('delivery_reservation')
        if target_id=='delivery.submit' and reservation and reservation['selected']<reservation['required']:target_id='delivery.item'
        if not target_id:return {'evidence_kind':'unavailable','missing':['typed parent supplier for '+str(node.get('op'))],'game_inputs':0}
        window=frame.get('window',{})
        pid=window.get('pid',frame.get('pid'))
        if type(pid) is not int or pid<=0:return {'evidence_kind':'unavailable','missing':['bound game PID'],'game_inputs':0}
        try:
            with self.session_factory(pid) as session:
                snapshot=session.current_layout(target_id)
                geometry=getattr(session,'attach_current_geometry',None)
                if callable(geometry):
                    try:geometry(snapshot,dict(frame,frame_id=frame.get('frame_id') or frame.get('observation_id')))
                    except (OSError,ValueError,RuntimeError,KeyError) as exc:
                        snapshot.setdefault('missing',[]).append('current control geometry: '+str(exc))
        except (OSError,ValueError,RuntimeError) as exc:
            return {'evidence_kind':'unavailable','missing':[str(exc)],'game_inputs':0}
        # A read-only layout snapshot is evidence even when remaining descriptor
        # fields prevent input. Preserve the concrete candidates for diagnosis.
        width=semantic.get('frame',{}).get('width');height=semantic.get('frame',{}).get('height')
        scale=snapshot['scale'];screen=snapshot['screen_ui']
        if width is None or height is None or abs(screen[0]*scale-width)>2 or abs(screen[1]*scale-height)>2:
            return {'evidence_kind':'unavailable','missing':['captured viewport differs from current MMML screen'],'layout_snapshot':snapshot,'game_inputs':0}
        fact=semantic.get('facts',{}).get(FACT_FOR_TARGET[target_id]);matches=[]
        if fact:
            box=fact['box']
            for candidate in snapshot['candidates']:
                x,y,w,h=(v*scale for v in candidate['rect_ui'])
                if x<=box[0] and y<=box[1] and x+w>=box[2] and y+h>=box[3]:matches.append(candidate)
        frame_id=frame.get('frame_id') or frame.get('observation_id')
        physical_rect=frame.get('physical_client_rect',[0,0,width,height])
        origin=window.get('origin',[window.get('client_left',physical_rect[0]),window.get('client_top',physical_rect[1])])
        context=frame.get('current_context',{})
        safe_rect=[0,0,width,height]
        if context.get('ok'):
            left,top,right,bottom=context['safe_rect'];safe_rect=[left,top,right-left,bottom-top]
        result={'evidence_kind':'current-frame','frame_id':frame_id,'observed_at':frame.get('captured_at'),
                'session_id':frame.get('session_id'),'window_signature':frame.get('window_signature'),
                'viewport':{'width':width,'height':height,'origin':origin},'layout_scale':scale,
                'safe_rect':safe_rect,'safe_rect_source':context.get('source','client content bounds only; inset/occlusion unknown'),
                'source_hashes':[snapshot['source_hash']],
                'bindings':{},'layout_snapshot':snapshot,'missing':list(snapshot['missing']),
                'current_action_binding':{},'game_inputs':0}
        if len(matches)==1:
            candidate=matches[0]
            enabled,descriptor,descriptor_missing=model_enabled(snapshot,target_id,fact['observed_text'])
            result['missing'].extend(descriptor_missing)
            result['bindings'][target_id]={**candidate,'frame_id':frame_id,'screen':ui_map()['targets'][target_id]['screen'],
                'label':fact['observed_text'],'source_bound':True,'layout_stable':True,'semantic_unique':True,
                'enabled':enabled,'occluded':None,'current_identity_bound':False,'descriptor':descriptor,
                'rect_source':'current-mmml-output',
                'layout_source':snapshot['source'],'layout_revision':snapshot['source_hash']}
            if target_id.startswith('delivery.'):
                ownership,missing=delivery_ownership(snapshot,frame,node)
                result['missing'].extend(missing)
                if ownership:
                    result['goal']={'quest_id':ownership['quest_id'],'npc':ownership['npc']}
                    result['bindings'][target_id].update(ownership,ownership_verified=True,current_identity_bound=True,
                        is_deliver_button_enabled=enabled)
                    result['current_action_binding']={**ownership,'node_id':node['id'],'source':'current typed delivery UI joined to current accepted quest',
                        'observed_at':frame.get('captured_at')}
                    result['missing']=[m for m in result['missing'] if m not in ('current exact quest/node ownership','join current accepted quest instance and current NPC')]
        else:result['missing'].append('semantic glyph requires one source rectangle; candidates='+str(len(matches)))
        return result


def model_enabled(snapshot,target_id,label):
    """Select a current source descriptor, never expected metadata as state."""
    rows=[r['values'] for r in snapshot.get('details',{}).get('model',{}).get('rows',[])]
    if target_id.startswith('dialogue.'):
        key='isEnable';text='displayName';event='onClickEvent'
        rows=[r for r in rows if 'keywordId' in r]
    elif target_id=='delivery.submit':key='isDeliverButtonEnabled';text='deliverButtonText';event='deliverClicked'
    else:return None,None,['current deliverable item descriptor binding not implemented']
    normalize=lambda s:''.join(str(s).split())
    matches=[r for r in rows if text in r and normalize(r[text])==normalize(label) and key in r]
    if len(matches)!=1:return None,None,['current descriptor label is absent or ambiguous']
    desc=matches[0]
    if type(desc[key]) is not bool:return None,desc,['current enabled field is not Boolean']
    if not isinstance(desc.get(event),dict) or desc[event].get('event_present') is not True:
        return None,desc,['current authored callback event is missing']
    return desc[key],desc,[]


def delivery_ownership(snapshot,frame,node):
    """Join a current DeliverUI identity to this session's accepted instance."""
    current=frame.get('current_quest') or {};details=snapshot.get('details',{})
    required={'confirmed':True,'source':'current-root-typed-quest-collections',
        'session_id':frame.get('session_id'),'process_started_at':frame.get('process_started_at')}
    if any(current.get(k)!=v or v is None for k,v in required.items()) or str(current.get('character_id'))!=str(frame.get('character_id')):
        return None,['current accepted quest observation for the same character/process/session']
    if not current.get('observed_at') or not current.get('accept_time'):
        return None,['current accepted quest observation time and accepted occurrence']
    checks=[(details.get('target_quest_instance_id'),current.get('instance_id')),
        (current.get('quest_id'),node.get('quest_id')),(details.get('targetStepId'),current.get('step_id')),
        (details.get('targetStepId'),node.get('step_id')),(details.get('targetObjectiveIndex'),node.get('objective_index'))]
    if any(a is None or b is None or a!=b for a,b in checks):return None,['current delivery quest/step/objective differs from selected goal']
    params=node.get('params',{});targets=params.get('targets') or params.get('objective',{}).get('targets',[])
    npcs=[t['table_data'] for t in targets if t.get('kind')=='npc' and t.get('table_data',{}).get('raw_id') is not None]
    if len(npcs)!=1 or details.get('target_npc_id') is None or (details['target_npc_id']&0xffffffff)!=(npcs[0]['raw_id']&0xffffffff):
        return None,['current delivery NPC differs from exact source target']
    npc_identity={'table_domain':'NPCSheet','id':details['target_npc_id']&0xffffffff}
    accepted=[p for p in frame.get('quest_state_proofs',[]) if p.get('confirmed') is True and p.get('state')=='accepted'
        and p.get('source')==required['source'] and p.get('session_id')==required['session_id']
        and str(p.get('character_id'))==str(current.get('character_id'))
        and all(p.get(k)==current[k] for k in ('quest_id','instance_id','accept_time'))]
    if len(accepted)!=1:return None,['exact current accepted occurrence proof unavailable or ambiguous']
    return {'quest_id':current['quest_id'],'instance_id':current['instance_id'],'accept_time':current['accept_time'],
        'step_id':current['step_id'],'objective_index':details['targetObjectiveIndex'],'npc_id':details['target_npc_id'],
        'npc':npcs[0].get('display_name') or npc_identity,'npc_label':npcs[0].get('display_name'),
        'npc_identity':npc_identity,'ownership_basis':'typed DeliverUIViewModelComponent IDs matched current accepted quest and exact graph NPC'},[]
