"""Current cached Rune rows and actual instance predicates; never execute."""
from copy import deepcopy
from functools import lru_cache
import hashlib,json,struct
from .paths import root
from .vendor.map_character_context import _class
from .vendor.map_memory import MapReadPending


@lru_cache(maxsize=1)
def rune_rules():
    return json.loads((root()/'data/objective-inventory/rune-semantic-rules.json').read_text('utf8'))


def generic_fields(session,klass,name):
    """Use closed runtime offsets; open generic source offsets are zero."""
    schema=session.stock_shape(name);r=session.reader
    if r.class_name(klass)!=(schema['name'],schema['namespace']):raise ValueError('current_rune_generic_owner_changed:'+name)
    fields=r.layout(klass)['fields']
    if [f['name'] for f in fields]!=[f['name'] for f in schema['fields']]:raise ValueError('current_rune_generic_members_changed:'+name)
    if any(type(f.get('offset')) is not int or not 16<=f['offset']<=4096 for f in fields):
        raise ValueError('current_rune_generic_field_offset_invalid:'+name)
    return {f['name']:f for f in fields}


def _ref(session,pointer,fields,name,codes):
    field=fields[name]
    if field.get('type_code') not in codes:raise ValueError('current_rune_reference_type_changed:'+name)
    value=struct.unpack('<Q',session.watch(pointer+field['offset'],8))[0]
    if not value:raise MapReadPending('current_rune_reference_unavailable:'+name)
    return value


def current_rune_row(session,rune_id):
    r=session.reader;schema=session.stock_shape('ClientStaticDataHolder');klass=_class(r,session.base,schema)
    fields=session.fields(klass,schema)
    statics=struct.unpack('<Q',session.watch(klass+0xb8,8))[0]
    if not statics:raise MapReadPending('current_rune_static_holder_uninitialized')
    holder=struct.unpack('<Q',session.watch(statics+fields['<Runes>k__BackingField'],8))[0]
    if not holder:raise MapReadPending('current_Runes_holder_unavailable')
    f=generic_fields(session,r.u64(holder),'RuneSheetDataHolder_Common`2')
    loader=_ref(session,holder,f,'Sheet',(18,19,21))
    session.stock_fields(loader,'RuneSheetLoader')
    parent=r.u64(r.u64(loader)+0x58);f=generic_fields(session,parent,'MapSheetLoaderBase`3')
    cache=_ref(session,loader,f,'rowCache',(21,))
    d=r.dictionary(cache)
    if d.get('kind')!='manual_dense' or not 0<=d['count']<=8192:raise ValueError('current_rune_cache_dictionary_layout_or_bound')
    common={f['name']:f for f in d['layout']['fields']}
    session.watch(cache+d['count_offset'],4);session.watch(cache+d['version_offset'],4)
    keys,values=d['arrays']['keys'],d['arrays']['values']
    if keys['name']!='RuneSheetId[]' or keys['stride']!=4 or values['name']!='CacheEntry[]':
        raise ValueError('current_rune_cache_typed_arrays_changed')
    kp,vp=int(keys['pointer'],16),int(values['pointer'],16)
    if (struct.unpack('<Q',session.watch(cache+common['keys']['offset'],8))[0]!=kp
            or struct.unpack('<Q',session.watch(cache+common['values']['offset'],8))[0]!=vp):
        raise MapReadPending('current_rune_cache_arrays_changed')
    session.watch(kp+24,8);session.watch(vp+24,8)
    element=r.u64(r.u64(vp)+0x40);entry=generic_fields(session,element,'CacheEntry')
    if (entry['IsLoaded'].get('type_code')!=2 or entry['Row'].get('type_code') not in (18,19,21)
            or not 1<=values['stride']<=256 or any(entry[k]['offset']-16+n>values['stride'] for k,n in [('IsLoaded',1),('Row',8)])):
        raise ValueError('current_rune_cache_entry_types_changed')
    key_bytes=session.watch(kp+32,d['count']*4)
    matches=[i for i in range(d['count']) if struct.unpack_from('<i',key_bytes,i*4)[0]==rune_id]
    if len(matches)!=1:raise MapReadPending('current_exact_rune_row_not_cached_or_ambiguous')
    address=vp+32+matches[0]*values['stride']
    loaded=session.watch(address+entry['IsLoaded']['offset']-16,1)[0]
    if loaded!=1:raise MapReadPending('current_exact_rune_cache_entry_not_loaded')
    pointer=struct.unpack('<Q',session.watch(address+entry['Row']['offset']-16,8))[0]
    if not pointer:raise MapReadPending('current_exact_rune_cache_row_missing')
    identity=session.value(pointer,'RuneSheetData','<Id>k__BackingField')
    if identity!=rune_id:raise ValueError('current_rune_cache_key_and_row_ID_disagree')
    rarity=session.value(pointer,'RuneSheetData','<Rarity>k__BackingField')
    season=session.value(pointer,'RuneSheetData','<Season>k__BackingField')
    if rarity not in session.stock_shape('Rarity')['enum_constants'].values() or season not in session.stock_shape('RuneSeasonType')['enum_constants'].values():
        raise ValueError('current_rune_attribute_enum_unrecognized')
    return dict(rune_sheet_id=identity,rarity=rarity,season=season,source_pointer=hex(pointer),
        holder_pointer=hex(holder),loader_pointer=hex(loader),cache_pointer=hex(cache),entry_loaded=True,
        source_path=rune_rules()['current_row_path'],loader_called=False)


def nonzero_equipment_attributes(session,rune_id,equipment_pointer):
    rules=rune_rules()
    for check in rules['code_checks']:
        raw=bytes.fromhex(check['bytes_hex'])
        if session.watch(session.base+check['rva'],len(raw))!=raw:raise ValueError('current_nonzero_rune_predicate_code_changed:'+check['name'])
    state=session.value(equipment_pointer,'Equipment','<TranscendentState>k__BackingField')
    if str(state) not in rules['override_by_state']:raise ValueError('current_equipment_transcendent_state_unrecognized')
    row=current_rune_row(session,rune_id);override=rules['override_by_state'][str(state)]
    return dict(rarity=row['rarity'] if override is None else override,season=row['season'],constraints_status='observed',missing=[],
        transcendent_state=state,current_rune_row=deepcopy(row),rarity_overridden_by_current_transcendent_state=override is not None,
        attribute_basis='actual current instance rune + loaded exact RuneSheetData + source TranscendentState predicate',
        attribute_source='data/objective-inventory/rune-semantic-rules.json',
        attribute_source_sha256=hashlib.sha256((root()/'data/objective-inventory/rune-semantic-rules.json').read_bytes()).hexdigest(),
        future_output_guaranteed=False)
