#!/usr/bin/env python3
"""Generate data/consumables.json -- the complete FIFA 17 consumable (cardtype 6)
enum, derived from the binary and cross-checked against the client's own tables.

SOURCES, all verified 2026-08-05 (slug `consumables`):
  * FUN_1800d8330 (714 chars, full)   cardsubtypeid -> cardtype
  * FUN_18013f4d0 (8354 chars, full)  cardsubtypeid -> category(+0xb8), +0xbc, +0xbe,
                                      +0xbf, +0xc0.  Two callees only:
                                      FUN_180136480 (playstyle index) and
                                      FUN_1800d7ad0 (int16 clamp).  No DB handle.
  * FUN_1801bfac0 (42813 chars, full) category -> FUT_CONSUMABLE_* loc keys + the
                                      fixed 5000xxx artwork ids.
  * FUN_1801aa230                     (6, category) -> fcc_myclubscategories id
  * FUN_180048780 (3455 chars, full)  that id -> the UI bucket name
  * data/tables/fcc_trainingcards.json  (143 rows) and fcc_healingcards.json (27),
    fcc_contractcards.json (13): EA's own authored amount/rating per subtype.

NOTHING here needs the live game. Run it any time; it only reads files.
"""
import json, os, collections

HERE = os.path.dirname(os.path.abspath(__file__))
TABLES = os.path.join(HERE, "..", "data", "tables")
OUT = os.path.join(HERE, "..", "data", "consumables.json")

POS = ["GK","SW","RWB","RB","RCB","CB","LCB","LB","LWB","RDM","CDM","LDM","RM","RCM",
       "CM","LCM","LM","RAM","CAM","LAM","RF","CF","LF","RW","RS","ST","LS","LW"]

# FUN_18013f4d0, arm by arm.  value = (category, bc, bf, c0_is_single)
# bf/bc of None means "taken from the `amount` atom at parse time".
GK_ATTR   = ["FUT_UC_DIVING","FUT_UC_HANDLING","FUT_UC_KICKING","FUT_UC_REFLEXES",
             "FUT_UC_SPEED","FUT_UC_POSITIONING","FUT_FITNESS_UC"]
PL_ATTR   = ["FUT_MC_PACE","FUT_MC_SHOOTING","FUT_MC_PASSING","FUT_MC_DRIBBLING",
             "FUT_MC_DEFENDING","FUT_MC_HEADING","FUT_FITNESS_MC"]
HEAL_AREA = ["Injury_Head","Injury_UpperBody","Injury_Arm","Injury_Back","Injury_Knee",
             "Injury_Leg","Injury_Foot","Injury_All"]
HEAL_LOC  = ["FUT_HEAD_HEALING","FUT_UPPERBODY_HEALING","FUT_ARM_HEALING",
             "FUT_BACK_HEALING","FUT_KNEE_HEALING","FUT_LEG_HEALING",
             "FUT_FOOT_HEALING","FUT_PLAYER_HEALING"]

GK_TRAIN  = {51:0, 52:1, 53:2, 54:4, 55:5, 56:3, 57:6}          # 0x33..0x39
PL_TRAIN  = {61:0, 62:1, 63:2, 64:3, 65:5, 66:4, 67:6}          # 0x3d..0x43
FORMATION = [23,24,25,27,14,3,13,6,7,8,19,16,21,29,30,31]        # identical in both arms
POSMOD    = {                                                    # 0x5b..0x6e  (bc=from, bf=to)
    91:(8,7),   92:(7,8),   93:(2,3),   94:(3,2),   95:(16,27), 96:(12,23),
    97:(27,16), 98:(23,12), 99:(27,22), 100:(23,20),101:(22,27),102:(20,23),
    103:(14,18),104:(18,14),105:(10,14),106:(14,10),107:(18,21),108:(21,18),
    109:(21,25),110:(25,21),
}

CAT_LOC = {   # category -> (name loc key, desc loc key, artwork base, myclub cat, ui bucket)
    0:  ("FUT_CONSUMABLE_NAME_PLAYERTRAINING", "FUT_CONSUMABLE_PLAYERTRAINING", "5000000_<idx>", 0,  "training"),
    2:  ("FUT_CONSUMABLE_NAME_PLAYERCONTRACT", "FUT_CONSUMABLE_PLAYERCONTRACT", "5000007",       1,  "contracts"),
    3:  ("FUT_CONSUMABLE_NAME_MANAGERCONTRACT","FUT_CONSUMABLE_MANAGERCONTRACT","5000008",       2,  "contracts"),
    4:  ("FUT_CONSUMABLE_NAME_PLAYERHEALING",  "FUT_CONSUMABLE_PLAYERHEALING",  "5000009_<idx>", 3,  "healing"),
    5:  ("FUT_CONSUMABLE_NAME_PLAYERFITNESS",  "FUT_CONSUMABLE_PLAYERFITNESS",  "5000010",       4,  "fitness"),
    6:  ("FUT_CONSUMABLE_NAME_FORMATIONMOD",   "FUT_CONSUMABLE_FORMATIONMOD",   "-1",            15, "training"),
    7:  ("FUT_CONSUMABLE_NAME_FORMATIONMOD",   "FUT_CONSUMABLE_FORMATIONMOD",   "-1",            16, "formation"),
    8:  ("FUT_CONSUMABLE_NAME_POSITIONMOD",    "FUT_CONSUMABLE_POSITIONMOD",    "5000013",       17, "position"),
    9:  ("FUT_CONSUMABLE_NAME_PLAYERSTYLE",    "FUT_CONSUMABLE_PLAYERSTYLE",    "5000015_<amt>", 23, "playStyle"),
    10: ("FUT_CONSUMABLE_NAME_MANAGERLEAGUE",  "FUT_CONSUMABLE_MANAGERLEAGUE",  "5000016",       24, "managerLeagueModifier"),
}


def cardtype(s):
    """FUN_1800d8330, verbatim."""
    if 0 <= s <= 3:  return 1
    if s == 4:  return 2
    if s == 5:  return 3
    if s == 6:  return 10
    if s == 7:  return 5
    if s == 8:  return 4
    if s in (9, 10, 11): return 7
    if s in (30, 31, 145,146,147,148,149,150, 231,232,233, 236): return 9
    if 0 <= s - 0x33 <= 0x55: return 6      # 51..136
    if 0 <= s - 0xc9 <= 0x13: return 6      # 201..220
    if 0 <= s - 0xfa <= 0x17: return 6      # 250..273
    if 0 <= s - 300  <  0x2a: return 6      # 300..341
    return 0


def rows():
    out = []
    for s in range(0, 400):
        if cardtype(s) != 6:
            continue
        r = {"cardsubtypeid": s}
        if s in GK_TRAIN:
            i = GK_TRAIN[s]
            r.update(kind="gk_training", category=0, bc=i, bf="amount",
                     c0=int(s != 57), detail=GK_ATTR[i],
                     loc_name="FUT_CONSUMABLE_NAME_KEEPERTRAINING",
                     loc_desc="FUT_CONSUMABLE_KEEPERTRAINING",
                     artwork="5000001_%d" % i, needs=["amount"])
        elif s in PL_TRAIN:
            i = PL_TRAIN[s]
            r.update(kind="player_training", category=0, bc=i, bf="amount",
                     c0=int(s != 67), detail=PL_ATTR[i],
                     loc_name="FUT_CONSUMABLE_NAME_PLAYERTRAINING",
                     loc_desc="FUT_CONSUMABLE_PLAYERTRAINING",
                     artwork="5000000_%d" % i, needs=["amount"])
        elif 71 <= s <= 86:
            f = FORMATION[s - 71]
            r.update(kind="manager_formation_mod", category=6, bc=f, bf=0, c0=None,
                     detail="formationid=%d" % f, artwork="-1", needs=[])
        elif s in POSMOD:
            a, b = POSMOD[s]
            r.update(kind="position_mod", category=8, bc=a, bf=b, c0=None,
                     detail="%s >> %s" % (POS[a], POS[b]),
                     artwork="5000013", needs=[])
        elif 121 <= s <= 136:
            f = FORMATION[s - 121]
            r.update(kind="formation_mod", category=7, bc=f, bf=0, c0=None,
                     detail="formationid=%d" % f, artwork="-1", needs=[])
        elif s == 201:
            r.update(kind="player_contract", category=2, bc=None, bf=None, c0=None,
                     detail="games from atom `contract` (0xb8) -> record+0x8c, unit fcc_matches",
                     artwork="5000007", needs=["contract"])
        elif s == 202:
            r.update(kind="manager_contract", category=3, bc=None, bf=None, c0=None,
                     detail="games from atom `contract` (0xb8) -> record+0x8c, unit fcc_matches",
                     artwork="5000008", needs=["contract"])
        elif 211 <= s <= 218:
            i = s - 211
            r.update(kind="healing", category=4, bc=i, bf="amount", c0=int(s != 218),
                     detail=HEAL_AREA[i], loc_detail=HEAL_LOC[i],
                     artwork="5000009_%d" % i, needs=["amount"])
        elif s in (219, 220):
            r.update(kind="player_fitness" if s == 219 else "squad_fitness",
                     category=5, bc=0, bf="amount", c0=int(s != 220),
                     detail="RAREFLAG TRAP: rareflag==1 renders 219 as SQUAD fitness"
                            if s == 219 else "always squad fitness",
                     artwork="5000010" if s == 219 else "5000011", needs=["amount"])
            if s == 220:
                # 0xdc == 220 is the FIRST half of the squad-fitness test in
                # FUN_1801bfac0 case 5, so 220 ALWAYS takes that branch -- and that
                # branch writes FUT_CONSUMABLE_NAME_SQUADTRAINING. There is no
                # FUT_CONSUMABLE_NAME_SQUADFITNESS string in the binary at all
                # (24-string census); squad fitness reuses the squad-training name.
                r.update(loc_name="FUT_CONSUMABLE_NAME_SQUADTRAINING",
                         loc_desc="FUT_CONSUMABLE_SQUADTRAINING")
        elif 250 <= s <= 273:
            i = s - 250
            r.update(kind="gk_playstyle" if s >= 269 else "player_playstyle",
                     category=9, bc=i, be="amount", bf=None, c0=None,
                     detail="FUT_PLAYSTYLE_%d" % i,
                     loc_name=("FUT_CONSUMABLE_NAME_GK_PLAYERSTYLE" if s >= 269
                               else "FUT_CONSUMABLE_NAME_PLAYERSTYLE"),
                     artwork="5000015_<amount>", needs=["amount"])
        elif 300 <= s <= 341:
            r.update(kind="manager_league", category=10, bc="clamp_i16(amount)",
                     bf=None, c0=None, detail="ML: <leagueid from amount>",
                     artwork="5000016", needs=["amount"])
        else:
            # FUN_1801bfac0 case 0 tests `subtype - 0x33 < 7` (GK training) then
            # `subtype - 0x3d < 7` (player training) and otherwise falls to the
            # SQUAD-training branch. No dead-zone subtype satisfies either test, so
            # the name is SQUADTRAINING, not PLAYERTRAINING (which is what CAT_LOC[0]
            # would otherwise have stamped here).
            r.update(kind="DEAD_ZONE", category=0, bc=0, bf=0, c0=None,
                     detail="falls through to the bottom default of FUN_18013f4d0; "
                            "renders as SQUAD TRAINING / attribute index 0, amount 0. "
                            "NOT a loud error -- do not ship these.",
                     loc_name="FUT_CONSUMABLE_NAME_SQUADTRAINING",
                     loc_desc="FUT_CONSUMABLE_SQUADTRAINING",
                     artwork="5000000_0", needs=[])
        c = r["category"]
        if "loc_name" not in r: r["loc_name"] = CAT_LOC[c][0]
        if "loc_desc" not in r: r["loc_desc"] = CAT_LOC[c][1]
        r["myclub_category"] = CAT_LOC[c][3]
        r["ui_bucket"] = CAT_LOC[c][4]
        out.append(r)
    return out


def merge_ea():
    """EA's authored (amount, rating) per subtype, straight out of the resident DB."""
    ea = collections.defaultdict(list)
    for f in ("fcc_trainingcards", "fcc_healingcards"):
        d = json.load(open(os.path.join(TABLES, f + ".json")))
        for row in d["rows"]:
            ea[row["cardsubtype"]].append(
                {"carddbid": row["carddbid"], "amount": row["amount"],
                 "rating": row["rating"], "weightrare": row["weightrare"],
                 "table": f})
    d = json.load(open(os.path.join(TABLES, "fcc_contractcards.json")))
    for row in d["rows"]:
        ea[row["cardsubtype"]].append(
            {"carddbid": row["carddbid"], "rating": row["rating"],
             "weightrare": row["weightrare"], "table": "fcc_contractcards",
             "gold": row["gold"], "silver": row["silver"], "bronze": row["bronze"]})
    return ea


def main():
    rs = rows()
    ea = merge_ea()
    for r in rs:
        r["ea_variants"] = ea.get(r["cardsubtypeid"], [])
    doc = {
        "generated_by": "tools/build_consumables.py",
        "derived_from": ["FUN_1800d8330", "FUN_18013f4d0", "FUN_1801bfac0",
                         "FUN_1801aa230", "FUN_180048780",
                         "data/tables/fcc_trainingcards.json",
                         "data/tables/fcc_healingcards.json",
                         "data/tables/fcc_contractcards.json"],
        "record_offsets": {
            "0x4c": "cardtype  (FUN_1800d8330(cardsubtypeid))",
            "0x50": "cardsubtypeid (atom 0x6c). default 342 -> cardtype 0",
            "0x54": "level: 1 if rating<65, 2 if <75, else 3 (shared tail of FUN_180141660)",
            "0x58": "rareflag (atom 0x271, u32). THE TRAP: ==1 flips subtype 219 to squad fitness",
            "0x18": "resourceId (atom 0x287) -- overwritten by FUN_18013f4d0 param_2",
            "0x88": "playStyle (atom 0x23f, mapped 250..273 -> 0..23)",
            "0x8c": "contract (atom 0xb8) -- the contract card's games count",
            "0xb4": "rating (atom 0x274)",
            "0xb8": "consumable CATEGORY (written by FUN_18013f4d0)",
            "0xbc": "int16 sub-selector: attribute / injury area / formationid / from-position / playstyle index / leagueid",
            "0xbe": "int8 playstyle artwork variant = (byte)amount",
            "0xbf": "int8 amount, or to-position for position mods",
            "0xc0": "bool single-target",
        },
        "club_type_values_for_consumables": {
            "contract": "FUN_18012ec50 arm 24 -> atom 0xb8",
            "training": "arm 25 -> atom 0x337",
            "healing":  "arm 23 -> atom 0x156",
            "development": "arm 6 -> atom 0xd3 (best candidate for position/formation/playStyle/managerLeague)",
            "STATUS": "ALL FOUR UNOBSERVED ON THE WIRE. Only type=player, type=manager and "
                      "type=custom have ever been seen from this client.",
        },
        "subtypes": rs,
    }
    with open(OUT, "w") as f:
        json.dump(doc, f, indent=1)
    live = [r for r in rs if r["kind"] != "DEAD_ZONE"]
    dead = [r for r in rs if r["kind"] == "DEAD_ZONE"]
    print("cardtype-6 subtypes: %d  (live %d, dead-zone %d)" % (len(rs), len(live), len(dead)))
    print("dead zones:", sorted(r["cardsubtypeid"] for r in dead))
    print("with EA variants:", len([r for r in rs if r["ea_variants"]]))
    print("live but NO EA variant:",
          sorted(r["cardsubtypeid"] for r in live if not r["ea_variants"]))
    print("wrote", os.path.normpath(OUT))


if __name__ == "__main__":
    main()
