#!/usr/bin/env python3
"""Generate a MediaWiki Loot/Drop-tables page from extracted/loot_tables.json.

Source = the two Odin-binary LootTablesConfig assets (Storm + Voyage), decoded by
odin_read.py. Both regions share the same 193 table ids and the same item set per
table; only the counts differ (Voyage is leaner). So each (table,item) is one row
with separate Storm and Voyage count columns. Item names are the in-game I2 names.
"""
import json, os, re

EX = '/home/downloadpizza/sand_tools/extracted'
WIKI = '/home/downloadpizza/sand_tools/wiki'
SRC = os.path.join(EX, 'loot_tables.json')
NAMES = os.path.join(EX, 'item_names.json')
OUT = os.path.join(WIKI, 'Loot.mediawiki')

_NAMES = json.load(open(NAMES))['items'] if os.path.exists(NAMES) else {}

def camel_split(s):
    return re.findall(r'[A-Za-z][a-z]*\d*', s)

def words(tok):
    return ' '.join(w.upper() if re.match(r'^[A-Za-z]\d+$', w) else w[:1].upper() + w[1:]
                    for w in camel_split(tok))

def humanize(item_id):
    s = item_id
    is_item = s.startswith('item_')
    if is_item: s = s[5:]
    toks = s.split('_')
    if toks and toks[0].startswith('resource'): toks[0] = toks[0][len('resource'):]
    if is_item and len(toks) > 1:
        return words(toks[0]) + ' (' + ', '.join(words(v) for v in toks[1:]) + ')'
    return ' '.join(words(t) for t in toks)

def display(item_id):
    n = _NAMES.get(item_id, {}).get('name')
    return n if n else humanize(item_id)

CAT_RULES = [
    ('weapons_container_resupply', 'Weapon resupply'),
    ('weapons_container',          'Weapons container'),
    ('shells_container',           'Shells container'),
    ('valuables_container',        'Valuables container'),
    ('resource_container',         'Resource container'),
    ('food_container',             'Food container'),
    ('med_container',              'Medical container'),
    ('buriedTreasure',            'Buried treasure'),
    ('mobLoot_ghoulRange',         'Mob drop — Ghoul (ranged)'),
    ('mobLoot_ghoulMeleeShovel',   'Mob drop — Ghoul (shovel)'),
    ('mobLoot_ghoulMelee',         'Mob drop — Ghoul (melee)'),
    ('ironcladLoot_packedTurret',  'Ironclad — packed turret'),
    ('ironcladLoot_lootBoxEntity', 'Ironclad — loot box (entity)'),
    ('ironcladLoot_lootBox_mandat','Ironclad — mandatory alloy'),
    ('ironcladLoot_lootBox',       'Ironclad — loot box'),
    ('ironcladLoot_repairKit',     'Ironclad — repair kit'),
    ('ironcladLoot',               'Ironclad'),
    ('navalMine',                  'Naval mine'),
    ('aurogenCrystal',             'Aurogen crystal'),
]
def category(tid):
    for pre, lab in CAT_RULES:
        if tid.startswith(pre):
            return lab
    return 'Other'

def cnt(rows_by_id, item):
    r = rows_by_id.get(item)
    if r is None:
        return '—'
    lo, hi = r['countMin'], r['countMax']
    return str(lo) if lo == hi else f'{lo}&ndash;{hi}'

def esc(s):
    return (s or '').replace('|', '&#124;')

def main():
    d = json.load(open(SRC))
    storm, voyage = d['Storm'], d['Voyage']
    tids = sorted(storm.keys(), key=lambda t: (category(t), t))

    # build per-table id->row maps for fast count lookup
    smap = {t: {r['itemBlueprint']: r for r in storm[t]} for t in storm}
    vmap = {t: {r['itemBlueprint']: r for r in voyage.get(t, [])} for t in storm}

    nrows = sum(len(storm[t]) for t in tids)
    out = []
    out.append("'''Loot tables''' for '''SAND''' — what each lootable source (containers, buried "
               "treasure, naval mines, ironclad cargo, and mob kills) can drop, and in what "
               "quantity. Extracted directly from the game's drop-table configuration.")
    out.append('')
    out.append("The game ships two regional drop-table sets, '''Storm''' and '''Voyage'''. They "
               "contain the same loot tables with the same items, but the drop ''amounts'' differ "
               "&mdash; Voyage is the leaner region (152 of 193 tables give smaller stacks). Both "
               "amounts are shown side by side below; a count like ''20&ndash;25'' is a random "
               "range, a single number is fixed.")
    out.append('')
    out.append(f"{len(tids)} loot tables, {nrows} item drops. The table is sortable &mdash; click a "
               "header to group by category, table, or item.")
    out.append('')
    out.append('__TOC__')
    out.append('')
    out.append('== Drop tables ==')
    out.append('{| class="wikitable sortable"')
    out.append('! Category !! Loot table !! Item !! data-sort-type="number" | Storm '
               '!! data-sort-type="number" | Voyage')
    for t in tids:
        cat = category(t)
        for r in storm[t]:
            iid = r['itemBlueprint']
            out.append('|-')
            out.append(f"| {esc(cat)} || <code>{t}</code> || [[{esc(display(iid))}]] "
                       f"|| {cnt(smap[t], iid)} || {cnt(vmap[t], iid)}")
    out.append('|}')
    out.append('')

    # reverse index: item -> which tables drop it
    out.append('== Where each item drops ==')
    out.append('For each lootable item, the loot tables that can drop it.')
    out.append('')
    item_to_tables = {}
    for t in tids:
        for r in storm[t]:
            item_to_tables.setdefault(r['itemBlueprint'], []).append(t)
    out.append('{| class="wikitable sortable"')
    out.append('! Item !! Item ID !! data-sort-type="number" | # tables !! Dropped by')
    for iid in sorted(item_to_tables, key=lambda i: display(i).lower()):
        ts = item_to_tables[iid]
        shown = ', '.join(f'<code>{x}</code>' for x in ts[:12])
        if len(ts) > 12:
            shown += f' … (+{len(ts)-12} more)'
        out.append('|-')
        out.append(f"| [[{esc(display(iid))}]] || <code>{iid}</code> || {len(ts)} || {shown}")
    out.append('|}')
    out.append('')
    out.append('== Notes ==')
    out.append('* Source: the game\'s <code>LootTablesConfig</code> assets '
               '(<code>conf_worldLootTablesStormConfig</code> / <code>…VoyageConfig</code>), '
               'Odin-serialized; decoded from the binary payload.')
    out.append('* Each drop entry is <code>{item, countMin, countMax}</code>; the count shown is '
               '<code>min&ndash;max</code> (a single value means min == max).')
    out.append('* A few drop entries are drop-only blueprint variants (e.g. mob/mine-drop resource '
               'variants and world treasure piles) that are not separate carriable items; they are '
               'shown under their in-game display name where one exists.')
    out.append("* This lists ''what'' a table can drop. The ''chance'' of a given table rolling, and "
               "how many POI containers use each table, are decided elsewhere in game logic.")
    out.append('')
    out.append('[[Category:Loot]]')

    text = '\n'.join(out)
    os.makedirs(WIKI, exist_ok=True)
    open(OUT, 'w').write(text)
    print(f"wrote {OUT} ({len(text)} chars, {len(tids)} tables, {nrows} rows, "
          f"{len(item_to_tables)} distinct items)")

if __name__ == '__main__':
    main()
