package thb.jeanluc.adventure.save;

import thb.jeanluc.adventure.game.GameSession;
import thb.jeanluc.adventure.game.QuestLog;
import thb.jeanluc.adventure.model.Player;
import thb.jeanluc.adventure.model.Room;
import thb.jeanluc.adventure.model.World;
import thb.jeanluc.adventure.model.item.Item;
import thb.jeanluc.adventure.model.item.SwitchableItem;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * Converts between a live {@link GameSession} and a {@link SaveData} snapshot.
 * Pure (no disk): {@link #capture} reads a session; {@link #apply} overlays a
 * snapshot onto a session built from a freshly loaded world.
 */
public final class SaveCodec {

    private SaveCodec() {
    }

    /** Reads the mutable state of {@code session} into a {@link SaveData}. */
    public static SaveData capture(GameSession session) {
        World w = session.getWorld();
        Player p = session.getPlayer();

        Map<String, List<String>> roomItems = new LinkedHashMap<>();
        for (Room room : w.getRooms().values()) {
            if (!room.getItems().isEmpty()) {
                roomItems.put(room.getId(), new ArrayList<>(room.getItems().keySet()));
            }
        }
        Map<String, Boolean> switches = new LinkedHashMap<>();
        for (Item item : w.getItems().values()) {
            if (item instanceof SwitchableItem sw) {
                switches.put(sw.getId(), sw.isOn());
            }
        }
        Map<String, Integer> stages = new LinkedHashMap<>();
        QuestLog log = session.getQuestLog();
        for (String id : log.active()) {
            stages.put(id, log.stageIndex(id));
        }

        return new SaveData(
                SaveData.CURRENT_VERSION,
                w.getTitle(),
                session.getSlotName(),
                System.currentTimeMillis(),
                session.getTurn(),
                p.getCurrentRoom().getId(),
                new ArrayList<>(p.getVisitedRoomIds()),
                p.getGold(),
                new ArrayList<>(p.getInventory().keySet()),
                new ArrayList<>(session.getState().all()),
                stages,
                new ArrayList<>(log.completed()),
                roomItems,
                switches);
    }

    /**
     * Overlays {@code data} onto {@code session} (whose world was just loaded
     * from YAML). Throws {@link SaveException} if the save references ids that
     * no longer exist in the current world.
     */
    public static void apply(SaveData data, GameSession session) {
        World w = session.getWorld();
        Player p = session.getPlayer();

        // 1. Clear all item placements, then redistribute from the save.
        for (Room room : w.getRooms().values()) {
            room.getItems().clear();
        }
        p.getInventory().clear();
        for (Map.Entry<String, List<String>> e : data.roomItemIds().entrySet()) {
            Room room = requireRoom(w, e.getKey());
            for (String itemId : e.getValue()) {
                room.addItem(requireItem(w, itemId));
            }
        }
        for (String itemId : data.inventoryItemIds()) {
            p.addItem(requireItem(w, itemId));
        }

        // 2. Switch states.
        for (Map.Entry<String, Boolean> e : data.switchStates().entrySet()) {
            if (requireItem(w, e.getKey()) instanceof SwitchableItem sw) {
                sw.setState(e.getValue());
            }
        }

        // 3. Player position + visited + gold.
        p.getVisitedRoomIds().clear();
        p.getVisitedRoomIds().addAll(data.visitedRoomIds());
        p.setCurrentRoom(requireRoom(w, data.currentRoomId()));
        p.setGold(data.gold());

        // 4. Flags, quests, turn.
        session.getState().restore(data.flags());
        session.getQuestLog().restore(data.questStages(), data.questCompleted());
        session.setTurn(data.turn());
    }

    private static Room requireRoom(World w, String id) {
        Room r = w.getRooms().get(id);
        if (r == null) {
            throw new SaveException("Save refers to unknown room '" + id
                    + "'. The game content may have changed.");
        }
        return r;
    }

    private static Item requireItem(World w, String id) {
        Item i = w.getItems().get(id);
        if (i == null) {
            throw new SaveException("Save refers to unknown item '" + id
                    + "'. The game content may have changed.");
        }
        return i;
    }
}
