#!/usr/bin/env python3
"""Reproduce SAND walker hashes offline.

All walker hashes = MD5(UTF8(JsonConvert.SerializeObject(obj))).hexUPPER, where the
default JsonConvert settings use a global StringEnumConverter (enums -> NAME strings),
NullValueHandling.Include, compact formatting, member declaration order.

Verified against all local walkers:
  CompartmentHash (per part)  = md5_json(placement from CompartmentsDatabase)   [compartment_hashes.py]
  CompartmentsHash (walker)   = md5_json( Compartments list )                   <- here
  ConnectionsHash  (walker)   = md5_json( Connections list )                    <- here
  DefinitionsHash  (walker)   = md5_json( Compartments.Select(->CompartmentDefinitionDto) )
                                ^ needs the server-sourced CompartmentDefinitionDto objects;
                                  not reproducible offline. Copy from source walker (it only
                                  changes if the set/order of part definitions changes).
  DefinitionHash  (per part)  = md5_json(CompartmentDefinitionDto)  -> also server-sourced.

Enum name tables (from dump.cs):
  ConnectionSlotType: 0 DOOR,1 HATCH,2 STRUCTURE,3 BALCONY,4 DECK
  ConnectionState(MasterserverDtos): 0 DEFAULT,1 DOOR,2 OPEN
  ConnectionsCount: 0 FULL,1 PARTIAL,2 ERROR

DecorationsInfo nesting is serialized as KeyValuePair ARRAYS (not JSON objects):
  Sockets: Dict<CellCoordinate, Dict<ConnectionSlotType, {state,count}>>
  -> {"Sockets":[{"Key":{x,y,z},"Value":[{"Key":"<slot>","Value":{"state":"<st>","count":"<cnt>"}}]}]}
"""
import json, hashlib

SLOT = {0: 'DOOR', 1: 'HATCH', 2: 'STRUCTURE', 3: 'BALCONY', 4: 'DECK'}
STATE = {0: 'DEFAULT', 1: 'DOOR', 2: 'OPEN'}
COUNT = {0: 'FULL', 1: 'PARTIAL', 2: 'ERROR'}

def _md5(s): return hashlib.md5(s.encode()).hexdigest().upper()
def _coord(v): return '{"x":%d,"y":%d,"z":%d}' % (v['x'], v['y'], v['z'])
def _dbl(x): return f'{int(x)}.0' if float(x) == int(x) else repr(float(x))
def _stinfo(d): return '{"state":"%s","count":"%s"}' % (STATE[d['state']], COUNT[d['count']])

def _deco(d):
    if d is None:
        return 'null'
    socks = ','.join(
        '{"Key":%s,"Value":[%s]}' % (
            _coord(kv['Key']),
            ','.join('{"Key":"%s","Value":%s}' % (SLOT[iv['Key']], _stinfo(iv['Value']))
                     for iv in kv['Value']))
        for kv in d['Sockets'])
    return '{"Sockets":[%s]}' % socks

def compartment_json(c):
    p = [('Id', str(c['Id'])), ('EpbId', json.dumps(c['EpbId'])),
         ('CellCoordinate', _coord(c['CellCoordinate'])),
         ('DecorationsInfo', _deco(c['DecorationsInfo'])),
         ('Rotation', _dbl(c['Rotation'])),
         ('CompartmentHash', json.dumps(c['CompartmentHash'])),
         ('DefinitionHash', json.dumps(c['DefinitionHash']))]
    return '{' + ','.join(f'"{k}":{v}' for k, v in p) + '}'

def connection_json(c):
    p = [('Id', str(c['Id'])),
         ('EpbId', json.dumps(c['EpbId']) if c['EpbId'] is not None else 'null'),
         ('ActualDirection', _coord(c['ActualDirection'])),
         ('GridCoordinate', _coord(c['GridCoordinate'])),
         ('SlotType', '"%s"' % SLOT[c['SlotType']]),
         ('State', '"%s"' % STATE[c['State']])]
    return '{' + ','.join(f'"{k}":{v}' for k, v in p) + '}'

def compartments_hash(compartments):
    """CompartmentsHash from walker['Compartments'] (the BSON-decoded list)."""
    return _md5('[' + ','.join(compartment_json(c) for c in compartments) + ']')

def connections_hash(connections):
    """ConnectionsHash from walker['Connections']."""
    return _md5('[' + ','.join(connection_json(c) for c in connections) + ']')

if __name__ == '__main__':
    import sys, gzip, glob
    from bson import decode
    KEY = bytes.fromhex('70dd1f2a0b4a'); CHUNK = 0xA000
    files = sys.argv[1:] or glob.glob(
        '/home/downloadpizza/sand_tools/Walkers/*.wbt')
    for f in sorted(files):
        raw = gzip.decompress(open(f, 'rb').read())
        w = decode(bytes(raw[i] ^ KEY[(i % CHUNK) % 6] for i in range(len(raw))))['walker']
        okC = compartments_hash(w['Compartments']) == w['CompartmentsHash']
        okN = connections_hash(w['Connections']) == w['ConnectionsHash']
        print(f"{f.split('/')[-1][:8]}  CompartmentsHash={'OK' if okC else 'FAIL'}  "
              f"ConnectionsHash={'OK' if okN else 'FAIL'}")
