#!/usr/bin/env python3
"""Convert a master-server WalkerDto (e.g. ExpeditionDto.Trampler, from GetExpedition /
GetExpeditionWalker) into a loadable WalkerBlueprintDto, recomputing the 3 top-level hashes.

WHY a transform is needed: the WS channel serializes enums as INTEGERS and omits null EpbId on
connections; the blueprint/hash ("storage") form uses enum NAME strings and includes EpbId:null.
ObjectToByteArray hashes the storage form (verified in trampler_hashes.py). So WS-form -> storage-form
before hashing.

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

Self-verifies offline via round-trip on the known sample (storage->WS->storage is identity and
reproduces the stored CompartmentsHash/ConnectionsHash).
"""
import json, os, sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from trampler_hashes import compartments_hash, connections_hash, definitions_hash

SLOT = ["DOOR", "HATCH", "STRUCTURE", "BALCONY", "DECK"]            # ConnectionSlotType
STATE = ["DEFAULT", "DOOR", "OPEN"]                                  # ConnectionState
COUNT = ["FULL", "PARTIAL", "ERROR"]                                # ConnectionsCount
SLOT_I = {n: i for i, n in enumerate(SLOT)}
STATE_I = {n: i for i, n in enumerate(STATE)}
COUNT_I = {n: i for i, n in enumerate(COUNT)}


def _od(*pairs):
    """build a dict in explicit (declaration) order."""
    d = {}
    for k, v in pairs:
        d[k] = v
    return d


def _decor_i2s(decor):
    """DecorationsInfo: socket enums int->name (storage form)."""
    if decor is None:
        return None
    socks = []
    for s in decor.get("Sockets", []):
        vals = []
        for kv in s.get("Value", []):
            v = kv.get("Value") or {}
            vals.append(_od(("Key", SLOT[kv["Key"]]),
                            ("Value", _od(("state", STATE[v["state"]]), ("count", COUNT[v["count"]])))))
        socks.append(_od(("Key", s["Key"]), ("Value", vals)))
    return _od(("Sockets", socks))


def _decor_s2i(decor):
    """inverse, for round-trip self-test only."""
    if decor is None:
        return None
    socks = []
    for s in decor.get("Sockets", []):
        vals = []
        for kv in s.get("Value", []):
            v = kv.get("Value") or {}
            vals.append(_od(("Key", SLOT_I[kv["Key"]]),
                            ("Value", _od(("state", STATE_I[v["state"]]), ("count", COUNT_I[v["count"]])))))
        socks.append(_od(("Key", s["Key"]), ("Value", vals)))
    return _od(("Sockets", socks))


def comp_i2s(bp):
    """one CompartmentBlueprintDto WS->storage (CompartmentBlueprintDto field order)."""
    return _od(("Id", bp["Id"]), ("EpbId", bp.get("EpbId")), ("CellCoordinate", bp["CellCoordinate"]),
               ("DecorationsInfo", _decor_i2s(bp.get("DecorationsInfo"))), ("Rotation", bp["Rotation"]),
               ("CompartmentHash", bp.get("CompartmentHash")), ("DefinitionHash", bp.get("DefinitionHash")))


def comp_s2i(bp):
    return _od(("Id", bp["Id"]), ("EpbId", bp.get("EpbId")), ("CellCoordinate", bp["CellCoordinate"]),
               ("DecorationsInfo", _decor_s2i(bp.get("DecorationsInfo"))), ("Rotation", bp["Rotation"]),
               ("CompartmentHash", bp.get("CompartmentHash")), ("DefinitionHash", bp.get("DefinitionHash")))


def conn_i2s(bp):
    """one ConnectionBlueprintDto WS->storage: add EpbId:null, enums int->name (declaration order)."""
    return _od(("Id", bp["Id"]), ("EpbId", bp.get("EpbId")), ("ActualDirection", bp["ActualDirection"]),
               ("GridCoordinate", bp["GridCoordinate"]), ("SlotType", SLOT[bp["SlotType"]]),
               ("State", STATE[bp["State"]]))


def conn_s2i(c):
    """inverse: drop EpbId, enums name->int (round-trip self-test only)."""
    return _od(("Id", c["Id"]), ("ActualDirection", c["ActualDirection"]),
               ("GridCoordinate", c["GridCoordinate"]), ("SlotType", SLOT_I[c["SlotType"]]),
               ("State", STATE_I[c["State"]]))


def walkerdto_to_blueprint(walker, definitions=None, version=1):
    """ExpeditionDto.Trampler / WalkerDto -> WalkerBlueprintDto with recomputed hashes."""
    chassis = comp_i2s(walker["Chassis"]["Blueprint"])
    comps = [comp_i2s(c["Blueprint"]) for c in walker["Compartments"]]
    conns = [conn_i2s(c["Blueprint"]) for c in walker["Connections"]]
    bp = _od(("Id", walker.get("BlueprintId")), ("UniqueId", walker.get("BlueprintUniqueId")),
             ("Version", version), ("Chassis", chassis), ("Compartments", comps), ("Connections", conns),
             ("CompartmentsHash", compartments_hash(comps)), ("DefinitionsHash", ""),
             ("ConnectionsHash", connections_hash(conns)))
    if definitions is not None:
        bp["DefinitionsHash"] = definitions_hash(definitions)
    return bp


def _verify_roundtrip():
    """storage -> WS -> storage must be identity (and reproduce stored hashes) on the sample."""
    p = os.path.join(os.path.dirname(__file__), "..", "extracted", "playfab_titledata_TramplerBlueprint3.json")
    s = json.load(open(p))
    comps_ws = [comp_s2i(c) for c in s["Compartments"]]
    conns_ws = [conn_s2i(c) for c in s["Connections"]]
    comps_back = [comp_i2s(c) for c in comps_ws]
    conns_back = [conn_i2s(c) for c in conns_ws]
    cj = lambda o: json.dumps(o, separators=(",", ":"))
    assert cj(comps_back) == cj(s["Compartments"]), "compartment round-trip not identity"
    assert cj(conns_back) == cj(s["Connections"]), "connection round-trip not identity"
    assert compartments_hash(comps_back) == s["CompartmentsHash"], "CompartmentsHash mismatch"
    assert connections_hash(conns_back) == s["ConnectionsHash"], "ConnectionsHash mismatch"
    print("round-trip OK: storage->WS->storage is byte-identical; #1 & #3 hashes reproduce.")


if __name__ == "__main__":
    _verify_roundtrip()
    exp = os.path.join(os.path.dirname(__file__), "..", "extracted", "master_GetExpedition.json")
    if os.path.exists(exp):
        tr = json.load(open(exp)).get("Trampler")
        if tr:
            bp = walkerdto_to_blueprint(tr)
            out = os.path.join(os.path.dirname(__file__), "..", "extracted", "host_trampler_blueprint.json")
            json.dump(bp, open(out, "w"), indent=1, ensure_ascii=False)
            print("converted host trampler -> %s" % out)
            print("  chassis=%s comps=%d conns=%d" % (bp["Chassis"]["EpbId"], len(bp["Compartments"]), len(bp["Connections"])))
            print("  CompartmentsHash=%s" % bp["CompartmentsHash"])
            print("  ConnectionsHash =%s" % bp["ConnectionsHash"])
            print("  DefinitionsHash =%s (provisional - needs live verify)" % (bp["DefinitionsHash"] or "<empty>"))
