#!/usr/bin/env python3
"""Extract SAND world 'Factory Production Line' (conveyor) recipes.

These are the fixed single-recipe world structures (e.g. 1 Raw Aurogen Crystal ->
10 Energy Rods), DISTINCT from the player workbench recipes in
extracted/crafting_recipes.json. Same data shape, different home:

  Mechanic .... a "Factory Production Line": an item conveyor that runs ONE recipe.
  ECS ......... Hologryph.Sand.Shared.Game.Features.Crafting
                  ProductionLineComponent        (input/output entity indices)
                  ProductionLineRecipeComponent  -> CraftingRecipe recipe
                  ProductionLine{Input,Output}DataComponent (isLargeItem flags)
                CraftingRecipe = { CraftingIngredient[] inputIngredients,
                                   CraftingIngredient[] outputIngredients,
                                   float craftingTimeSeconds }
                CraftingIngredient = { string itemId, int amount }
  Where ....... epb_assets_all.bundle, prefabs named  game_conveyor_*_epb.prefab
                (Assets/Content/Game/game_conveyor/...). Each prefab GameObject has
                an EntityBlueprint (Sirenix Odin SerializedMonoBehaviour) component;
                the recipe lives in its Odin-serialized `serializationData`.
  Islands ..... energy_grid_*_epb.prefab (Wunderinsel, Kaiserplatz, DeusExMachine,
                LittleFactory1-3, LittleFactoryArmory*) place/group conveyors per
                location. They reference conveyors by entity ref, NOT item-id string,
                so recipe->island mapping needs the island prefab placements
                (islands_assets_all.bundle) -- not done here.

Extraction path: UnityPy (typetree via IL2CPP generator) reads each EntityBlueprint
MonoBehaviour -> serializationData.SerializedBytes -> odin_read.parse -> walk for
the CraftingRecipe node. Mirrors extract_loot.py (loot tables are Odin too).

Writes extracted/production_lines.json.
"""
import os, sys, json, UnityPy
from UnityPy.helpers.TypeTreeGenerator import TypeTreeGenerator
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import odin_read

GAME = "/mnt/d/SteamLibrary/steamapps/common/Sand Playtest"
META = os.path.join(GAME, "Sand_Data/il2cpp_data/Metadata/global-metadata.dat")
DLL = os.path.join(GAME, "GameAssembly.dll")
BUNDLES = "/home/downloadpizza/sand_tools/bundles"          # symlink -> StreamingAssets/aa/...
OUT = "/home/downloadpizza/sand_tools/extracted/production_lines.json"
UNITY = "6000.0.40f1"


def recipes_in(node, out):
    """Collect every CraftingRecipe-shaped dict in an odin_read tree."""
    if isinstance(node, dict):
        if "inputIngredients" in node or "outputIngredients" in node:
            out.append(node)
        for v in node.values():
            recipes_in(v, out)
    elif isinstance(node, list):
        for v in node:
            recipes_in(v, out)


def ingredients(arr):
    """Flatten an Odin CraftingIngredient[] (possibly nested in $items) -> [(itemId, amount)]."""
    if isinstance(arr, dict):
        arr = arr.get("$items", arr)
    out = []
    for x in (arr or []):
        rows = x if isinstance(x, list) else [x]
        for y in rows:
            if isinstance(y, dict) and "itemId" in y:
                out.append((y.get("itemId"), y.get("amount")))
    return out


def entity_blueprint(go):
    """Return the EntityBlueprint MonoBehaviour object_reader on a prefab GameObject, or None."""
    for c in go.m_Components:
        co = c.read()
        r = co.object_reader
        if r.type.name == "MonoBehaviour" and co.m_Script.read().m_ClassName == "EntityBlueprint":
            return r
    return None


def main():
    gen = TypeTreeGenerator(UNITY)
    gen.load_il2cpp(open(DLL, "rb").read(), open(META, "rb").read())
    env = UnityPy.load(os.path.join(BUNDLES, "epb_assets_all.bundle"),
                       os.path.join(BUNDLES, "sand_monoscripts.bundle"))

    result = {}
    for path, obj in env.container.items():
        base = path.split("/")[-1].replace("_epb.prefab", "")
        if not base.startswith("game_conveyor_") or obj.type.name != "GameObject":
            continue
        eb = entity_blueprint(obj.read())
        if eb is None:
            continue
        script = eb.read().m_Script.read()
        full = (script.m_Namespace + "." if script.m_Namespace else "") + script.m_ClassName
        nodes = json.loads(gen.get_nodes_as_json(script.m_AssemblyName, full))
        sb = eb.read_typetree(nodes).get("serializationData", {}).get("SerializedBytes")
        if not sb:
            continue
        parsed = odin_read.parse(bytes(sb))
        found = []
        recipes_in(parsed["roots"], found)
        recipes_in(parsed["items"], found)
        for r in found:
            result[base] = {
                "inputs": [{"itemId": i, "amount": a} for i, a in ingredients(r.get("inputIngredients"))],
                "outputs": [{"itemId": i, "amount": a} for i, a in ingredients(r.get("outputIngredients"))],
                "craftTimeSeconds": r.get("craftingTimeSeconds"),
            }

    out = {
        "_source": "epb_assets_all.bundle game_conveyor_*_epb EntityBlueprint "
                   "(Odin) -> ProductionLineRecipeComponent.recipe (CraftingRecipe)",
        "_count": len(result),
        "production_lines": dict(sorted(result.items())),
    }
    json.dump(out, open(OUT, "w"), indent=1)
    print(f"wrote {OUT}  ({len(result)} conveyor recipes)")
    for k, r in out["production_lines"].items():
        inp = " + ".join(f"{i['amount']}x {i['itemId']}" for i in r["inputs"]) or "?"
        outp = " + ".join(f"{i['amount']}x {i['itemId']}" for i in r["outputs"]) or "?"
        print(f"  {k.replace('game_conveyor_',''):24s} {inp:48s} -> {outp}  ({r['craftTimeSeconds']}s)")


if __name__ == "__main__":
    main()
