"""Current inventory entry for QuestFakeItem cards; never a use/quest effect.

Reads existing UI, delegates and visible collection models. No native invocation.
"""
import struct
from .story_ui_observer import StoryView
from .vendor.map_memory import MapReadPending

class InventoryEntryView(StoryView):
    def delegate(self,pointer,target,rva):
        if self.value(pointer,'MulticastDelegate','delegates'):
            raise MapReadPending('inventory entry multicast callback unsupported')
        if self.value(pointer,'Delegate','m_target')!=target or self.value(pointer,'Delegate','method_ptr')!=self.s.base+rva:
            raise MapReadPending('current inventory entry callback owner changed')
        return pointer

    def closed(self,pointer,name,member,code):
        shape=self.shape(name);klass=self.r.u64(pointer)
        if self.r.class_name(klass)!=(name,shape['namespace']):raise ValueError('current inventory closed generic class changed')
        fields={f['name']:f for f in self.r.layout(klass)['fields']};field=fields[member]
        expected=next(x for x in shape['fields'] if x['name']==member)
        if not 16<=field['offset']<=256 or field['type_code'] not in code:raise ValueError('current inventory closed generic field changed')
        fmt='<?' if field['type_code']==2 else '<Q'
        return struct.unpack(fmt,self.s.watch(pointer+field['offset'],struct.calcsize(fmt)))[0]

    def reference_pairs(self,pointer):
        # The legacy integer-key dictionary helper truncates reference keys.
        # Read this concrete reference/reference entry layout independently.
        d=self.r.dictionary(pointer)
        if d.get('kind')=='manual_dense' or d['count']>4096:raise ValueError('inventory model reference dictionary shape changed')
        self.s.watch(pointer+d['version_offset'],4);self.s.watch(pointer+d['count_offset'],4)
        rows={f['name'].casefold():f for f in d['entry_layout']['fields']}
        if not {'hashcode','key','value'}<=rows.keys():raise ValueError('inventory model dictionary fields missing')
        if any(rows[k]['type_code'] not in (18,19,21,28) for k in ('key','value')):raise ValueError('inventory dictionary reference type changed')
        offsets={k:v['offset']-16 for k,v in rows.items()}
        if d['stride']!=24:raise ValueError('inventory reference dictionary stride changed')
        entries=int(d['entries'],16);result=[]
        for i in range(d['used']):
            raw=self.s.watch(entries+32+i*d['stride'],d['stride'])
            if struct.unpack_from('<i',raw,offsets['hashcode'])[0]>=0:
                result.append(tuple(struct.unpack_from('<Q',raw,offsets[k])[0] for k in ('key','value')))
        if len(result)!=d['count']:raise MapReadPending('inventory model dictionary changed during read')
        return result

    def exact_item(self,data,item_id):
        self.fields(data,'InventoryItemIconData')
        if self.value(data,'InventoryItemIconData','<IsInvalidated>k__BackingField'):return False
        ident=self.value(data,'InventoryItemIconData','<ItemId>k__BackingField')
        shape=self.shape('ItemCollectionFakeItemId')
        if not ident or self.r.class_name(self.r.u64(ident))!=(shape['name'],shape['namespace']):return False
        return self.value(ident,'ItemCollectionFakeItemId','<QuestFakeItemSheetId>k__BackingField','<i')==item_id

    def inventory(self,ui,frame,item_id):
        s=self.s;component=self.value(ui,'EquipmentInventoryUIInstance','component')
        if not self.value(component,'EquipmentInventoryModularUIComponent','<IsActive>k__BackingField'):raise MapReadPending('current inventory component inactive')
        panel=self.value(ui,'EquipmentInventoryUIInstance','inventoryPanelUI')
        if self.value(panel,'InventoryPanelUI','<IsClosed>k__BackingField'):raise MapReadPending('current inventory panel closed')
        model=self.value(ui,'EquipmentInventoryUIInstance','inventoryPanelUIComponent')
        if not self.value(model,'InventoryPanelUIComponent','isInitialized') or self.value(model,'InventoryPanelUIComponent','pauseInventoryRefresh'):
            raise MapReadPending('current inventory model not stable')
        data=self.value(model,'InventoryPanelUIComponent','inventoryItemListData')
        listing=self.value(data,'InventoryItemListData','IconDataList');icons=set(s.refs(listing,element='InventoryItemIconData',limit=4096))
        scroll=self.value(panel,'InventoryPanelUI','inventoryScrollListControl')
        if not self.value(scroll,'InventoryScrollListControl','isOpened'):raise MapReadPending('current inventory collection not open')
        proxies=self.value(scroll,'InventoryScrollListControl','proxies')
        targets=[]
        for proxy in s.refs(proxies,element='ICollectionItemProxy`1',limit=4096):
            if self.closed(proxy,'CollectionItemProxy`1','<IsDestroyed>k__BackingField',(2,)):continue
            element=self.closed(proxy,'CollectionItemProxy`1','modelElement',(18,21))
            row=self.closed(element,'CollectionModelElement`1','<CurrentData>k__BackingField',(18,19,21))
            if row in icons and self.exact_item(row,item_id):targets.append((element,row))
        # An open inventory itself is an effect even when this filter/viewport
        # does not currently contain the requested item's control.
        base=dict(present=True,ui_pointer=ui,stage='open_card',candidate=None,window_id=ui,
            pointer_semantics=dict(ui_kind='quest_fake_item_inventory',ui_pointer=ui,fake_item_id=item_id),
            missing=['current_exact_fake_item_visible_row_required'])
        from .quest_item_inventory_navigation import navigate_inventory
        def navigate():return navigate_inventory(self,base,ui,panel,icons,frame,item_id)
        if not targets:return navigate()
        if len(targets)!=1:raise MapReadPending('current exact fake item inventory row ambiguous')
        element,row=targets[0];collection=self.value(scroll,'InventoryScrollListControl','collectionControl')
        alignment=self.value(collection,'CollectionControl','alignmentModule')
        for flag in ('needDataUpdate','needLayoutUpdate'):
            if self.value(alignment,'CollectionControlItemAlignmentModule',flag):raise MapReadPending('current inventory layout updating')
        entries=self.value(alignment,'CollectionControlItemAlignmentModule','collectionEntries');controls=[]
        for _,entry in self.reference_pairs(entries):
            table=self.value(entry,'CollectionControlAlignmentModuleCollectionEntry','modelToControlTable')
            table=self.value(table,'CollectionControlModelToControlTable','table')
            for key,value in self.reference_pairs(table):
                if key==element:
                    wrapper=self.value(value,'Entry','ControlWrapper');controls.append(self.value(wrapper,'CollectionItemControlWrapper','control'))
        if len(controls)!=1:return navigate()
        control=controls[0]
        if self.value(control,'InventoryItemIconControl','data')!=row:raise MapReadPending('current inventory control data changed')
        if self.value(control,'InventoryItemIconControl','currentContext'):raise MapReadPending('current inventory custom item context requires its own callback binding')
        click=self.delegate(self.value(control,'InventoryItemIconControl','onClick'),control,0x99441f0)
        button=self.value(control,'InventoryItemIconControl','buttonControl')
        for flag in ('enabled','interactable'):
            if not self.value(button,'ButtonControl',flag):raise MapReadPending('current inventory item control disabled')
        selection=self.value(button,'ButtonControl','<SelectionControl>k__BackingField')
        for field in ('ClickedWithTouchInputMethod','ClickedWithTouchDeviceType'):
            if self.value(selection,'DefaultButtonSelectionControl',field):raise MapReadPending('inventory item has additional click callbacks')
        callback=self.delegate(self.value(selection,'DefaultButtonSelectionControl','ClickedWithMouseButtonType'),control,0x9944000)
        geometry=s.read_control_geometry(button,ui,frame)
        geometry.update(index=self.value(row,'InventoryItemIconData','<Id>k__BackingField'),window_id=ui,window_kind='native-ui-root',
            event_kind='native-inventory-item-click',model_event_id=callback,button_pointer=button)
        base['pointer_semantics'].update(data_pointer=row,model_pointer=element,control_pointer=control,callback_pointer=callback,
            default_callback_pointer=click,fake_id_domain='QuestFakeItemSheet',callback_method_rva=0x9944000)
        base.update(candidate=geometry,pointer_state=s.current_pointer_state(),missing=[])
        return navigate()

    def hud(self,instance,frame,item_id):
        s=self.s;ui=self.value(instance,'InventoryButtonUIInstance','ui')
        main=self.value(ui,'InventoryButtonUI','buttonControl')
        if self.value(main,'MainHudButtonControl','currentState','<i')!=1:raise MapReadPending('current inventory HUD button is not normal state')
        tween=self.value(main,'MainHudButtonControl','tweenShowHelper')
        if self.value(tween,'TweenShowHelper','hasBeenPlayed'):
            show=self.value(tween,'TweenShowHelper','tweenShow');player=self.value(show,'TweenShow','player')
            if not self.value(player,'TweenShowProgramPlayer','<HasProgramReachedToEnd>k__BackingField'):
                raise MapReadPending('current inventory HUD button tween running')
        outer=self.delegate(self.value(main,'MainHudButtonControl','clickCallback'),ui,0x98e2730)
        button=self.value(main,'MainHudButtonControl','buttonControl')
        for flag in ('enabled','interactable'):
            if not self.value(button,'ButtonControl',flag):raise MapReadPending('current inventory HUD button disabled')
        selection=self.value(button,'ButtonControl','<SelectionControl>k__BackingField')
        for field in ('ClickedWithTouchDeviceType','ClickedWithMouseButtonType'):
            if self.value(selection,'DefaultButtonSelectionControl',field):raise MapReadPending('current inventory HUD button additional callbacks')
        callback=self.delegate(self.value(selection,'DefaultButtonSelectionControl','ClickedWithTouchInputMethod'),main,0x98e33d0)
        geometry=s.read_control_geometry(button,instance,frame)
        geometry.update(index=0,window_id=instance,window_kind='native-ui-root',event_kind='native-inventory-hud-click',model_event_id=callback,button_pointer=button)
        sem=dict(ui_kind='inventory_hud_entry',ui_pointer=instance,hud_control_pointer=main,inventory_ui_pointer=ui,fake_item_id=item_id,
            callback_pointer=callback,outer_callback_pointer=outer,callback_method_rva=0x98e33d0)
        return dict(present=True,ui_pointer=instance,stage='open_inventory',candidate=geometry,pointer_semantics=sem,
            window_id=instance,pointer_state=s.current_pointer_state(),missing=[])
