package com.n3plugins.guardiansrift;

import com.n3plugins.InteractionApi.actions.DialogActions;
import com.n3plugins.InteractionApi.actions.InventoryActions;
import com.n3plugins.InteractionApi.actions.MagicActions;
import com.n3plugins.InteractionApi.actions.NPCActions;
import com.n3plugins.InteractionApi.actions.ObjectActions;
import com.n3plugins.InteractionApi.actions.Spell;
import com.n3plugins.InteractionApi.common.InteractionResult;
import com.n3plugins.InteractionApi.common.InteractionStatus;
import com.n3plugins.sdk.client.GameVars;
import com.n3plugins.sdk.client.N3Client;
import com.n3plugins.sdk.query.Equipment;
import com.n3plugins.sdk.query.Inventory;
import com.n3plugins.sdk.query.NPCs;
import com.n3plugins.sdk.query.TileObjects;
import com.n3plugins.sdk.query.Widgets;
import com.n3plugins.sdk.widgets.RunePouchApi;
import net.runelite.api.Client;
import net.runelite.api.DynamicObject;
import net.runelite.api.GameObject;
import net.runelite.api.gameval.ItemID;
import net.runelite.api.ObjectComposition;
import net.runelite.api.Player;
import net.runelite.api.Quest;
import net.runelite.api.QuestState;
import net.runelite.api.Skill;
import net.runelite.api.TileObject;
import net.runelite.api.WorldView;
import net.runelite.api.gameval.InterfaceID;
import net.runelite.api.widgets.Widget;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.BooleanSupplier;
import java.util.stream.Collectors;

final class RuneLiteGuardiansRiftRuntime implements GuardiansRiftRuntime
{
    private static final int OUTSIDE_BARRIER_Y = 9482;
    private static final int LARGE_MINE_X = 3637;
    private static final int HUGE_MINE_X = 3592;
    private static final int GOTR_WIDGET_PARENT = InterfaceID.GotrHud.DODGER;
    private static final int GOTR_PORTAL_WIDGET = InterfaceID.GotrHud.PORTAL_POSITION;
    private static final int GOTR_POWER_WIDGET = InterfaceID.GotrHud.ENERGY_TITLE;
    private static final int BARRIER_BUSY_ID = 43849;
    private static final int ACTIVE_ALTAR_ANIMATION = 9363;
    private static final int CRAFTING_ESSENCE_ANIMATION = 9365;
    private static final int SPELLBOOK_VARBIT = 4070;
    private static final int LUNAR_SPELLBOOK = 2;

    private static final List<Integer> PICKAXE_IDS = Arrays.asList(
            ItemID.BRONZE_PICKAXE,
            ItemID.IRON_PICKAXE,
            ItemID.STEEL_PICKAXE,
            ItemID.BLACK_PICKAXE,
            ItemID.MITHRIL_PICKAXE,
            ItemID.ADAMANT_PICKAXE,
            ItemID.RUNE_PICKAXE,
            ItemID.TRAIL_GILDED_PICKAXE,
            ItemID.DRAGON_PICKAXE,
            ItemID.ZALCANO_PICKAXE,
            ItemID.TRAILBLAZER_PICKAXE_NO_INFERNAL,
            ItemID.DRAGON_PICKAXE_PRETTY,
            ItemID.INFERNAL_PICKAXE,
            ItemID._3A_PICKAXE,
            ItemID.CRYSTAL_PICKAXE,
            ItemID.TRAILBLAZER_PICKAXE);

    private static final Set<Integer> RUNE_IDS = new HashSet<>(Arrays.asList(
            ItemID.AIRRUNE,
            ItemID.WATERRUNE,
            ItemID.EARTHRUNE,
            ItemID.FIRERUNE,
            ItemID.MINDRUNE,
            ItemID.CHAOSRUNE,
            ItemID.DEATHRUNE,
            ItemID.BLOODRUNE,
            ItemID.COSMICRUNE,
            ItemID.NATURERUNE,
            ItemID.LAWRUNE,
            ItemID.BODYRUNE,
            ItemID.SOULRUNE,
            ItemID.ASTRALRUNE,
            ItemID.MISTRUNE,
            ItemID.MUDRUNE,
            ItemID.DUSTRUNE,
            ItemID.LAVARUNE,
            ItemID.STEAMRUNE,
            ItemID.SMOKERUNE,
            ItemID.WRATHRUNE));

    private static final Set<Integer> CELL_IDS = new HashSet<>(Arrays.asList(
            ItemID.GOTR_CELL_TIER1,
            ItemID.GOTR_CELL_TIER2,
            ItemID.GOTR_CELL_TIER3,
            ItemID.GOTR_CELL_TIER4));

    private final Client client;
    private final GuardiansRiftConfig config;
    private final BooleanSupplier shouldBreak;
    private final BooleanSupplier breakActive;
    private int gameEndCountdown = 10;
    private boolean gameStarted;
    private boolean firstPortalSpawned;
    private boolean gameEnding;

    RuneLiteGuardiansRiftRuntime(Client client, GuardiansRiftConfig config,
                                 BooleanSupplier shouldBreak,
                                 BooleanSupplier breakActive)
    {
        this.client = client;
        this.config = config;
        this.shouldBreak = shouldBreak;
        this.breakActive = breakActive;
    }

    @Override
    public GuardiansRiftSnapshot snapshot()
    {
        Player player = client == null ? null : client.getLocalPlayer();
        boolean loggedIn = N3Client.loggedIn();
        boolean inAltar = loggedIn && isInAltar();
        boolean widgetVisible = visibleWidget(GOTR_WIDGET_PARENT);
        if (widgetVisible)
        {
            gameStarted = true;
            gameEndCountdown = 10;
        }
        else if (!inAltar && gameStarted && --gameEndCountdown <= 0)
        {
            gameStarted = false;
            firstPortalSpawned = false;
            gameEnding = false;
            gameEndCountdown = 10;
        }
        if (!firstPortalSpawned && (portalSpawned() || guardianPower() > 15
                || Inventory.getItemAmount(ItemID.GOTR_GUARDIAN_FRAGMENT) >= 250))
        {
            firstPortalSpawned = true;
        }

        int runecraftLevel = client == null ? 1 : client.getRealSkillLevel(Skill.RUNECRAFT);
        Set<Integer> equipmentIds = Equipment.search().result().stream()
                .map(item -> item.getId())
                .collect(Collectors.toSet());
        boolean usePouches = config.usePouches();
        boolean repairReady = !usePouches || GuardiansRiftPouchRules.hasDegradationPrevention(equipmentIds)
                || GuardiansRiftPouchRules.chooseRepairRoute(config.pouchRepairPolicy(), repairContext()) != GuardiansRiftRepairRoute.NONE;

        return GuardiansRiftSnapshot.builder()
                .loggedIn(loggedIn)
                .hasPickaxe(hasPickaxe())
                .usePouches(usePouches)
                .pouchRepairReady(repairReady)
                .breakDue(shouldBreak.getAsBoolean())
                .breakActive(breakActive.getAsBoolean())
                .gameBusy(isGameBusy())
                .outsideBarrier(player != null && player.getWorldLocation().getY() <= OUTSIDE_BARRIER_Y && !inAltar)
                .gameStarted(gameStarted)
                .gameEnding(gameEnding)
                .inAltar(inAltar)
                .inLargeMine(player != null && !inAltar && player.getWorldLocation().getX() >= LARGE_MINE_X)
                .inHugeMine(player != null && !inAltar && player.getWorldLocation().getX() <= HUGE_MINE_X)
                .firstPortalSpawned(firstPortalSpawned)
                .portalSpawned(portalSpawned())
                .playerMoving(N3Client.isMoving())
                .mining(player != null && GuardiansRiftMiningAnimations.isMining(player.getAnimation()))
                .craftingEssence(player != null && player.getAnimation() == CRAFTING_ESSENCE_ANIMATION)
                .inventoryFull(Inventory.full())
                .inventoryFreeSlots(Inventory.getEmptySlots())
                .cellCount(Inventory.getItemAmount(ItemID.GOTR_CELL_UNCHARGED))
                .fragmentCount(Inventory.getItemAmount(ItemID.GOTR_GUARDIAN_FRAGMENT))
                .neededFragments(Inventory.getEmptySlots() + GuardiansRiftPouchRules.availableSpace(inventoryPouchIds(), runecraftLevel) + 5)
                .guardianEssence(Inventory.getItemAmount(ItemID.GOTR_GUARDIAN_ESSENCE))
                .pouchEssence(0)
                .emptyPouchAvailable(usePouches && GuardiansRiftPouchRules.availableSpace(inventoryPouchIds(), runecraftLevel) > 0)
                .fullPouchAvailable(false)
                .degradedPouchPresent(degradedPouchPresent())
                .powerEssencePresent(Inventory.getItemAmount(ItemID.GOTR_GUARDIAN_STONE_CATALYTIC) > 0
                        || Inventory.getItemAmount(ItemID.GOTR_GUARDIAN_STONE_ELEMENTAL) > 0)
                .powerCellPresent(Inventory.search().idIn(CELL_IDS).first().isPresent())
                .inventoryRunesPresent(Inventory.search().idIn(RUNE_IDS).first().isPresent())
                .droppableRunesPresent(droppableRunesPresent())
                .dropRunes(config.dropRunes())
                .nextAltarAvailable(nextAltar().isPresent())
                .startingFragments(config.startingFragments())
                .build();
    }

    @Override
    public InteractionResult gameBusy()
    {
        Optional<TileObject> barrier = TileObjects.search().withId(BARRIER_BUSY_ID).walkable().nearestByPath();
        return barrier.map(tileObject -> ObjectActions.interact(tileObject, "Check", "Peek"))
                .orElseGet(() -> InteractionResult.fail(InteractionStatus.TARGET_NOT_FOUND, "No busy barrier"));
    }

    @Override
    public InteractionResult enterGame()
    {
        Optional<Widget> guarded = Widgets.search()
                .withTextContainsIgnoreCase("already being guarded")
                .hiddenState(false)
                .single();
        if (guarded.isPresent())
        {
            return DialogActions.continueSpace();
        }
        return ObjectActions.interact("Barrier", "Quick-pass");
    }

    @Override
    public InteractionResult repairPouch()
    {
        GuardiansRiftRepairRoute route = GuardiansRiftPouchRules.chooseRepairRoute(config.pouchRepairPolicy(), repairContext());
        switch (route)
        {
            case CORDELIA:
                return NPCActions.interact("Apprentice Cordelia", "Repair", "Repair-pouches");
            case NPC_CONTACT_DARK_MAGE:
                return MagicActions.cast(Spell.NPC_CONTACT);
            case DARK_MAGE:
                return NPCActions.interact("Dark Mage", "Repair-pouches", "Talk-to");
            case NONE:
            default:
                return InteractionResult.fail(InteractionStatus.TARGET_NOT_FOUND, "No pouch repair route");
        }
    }

    @Override
    public InteractionResult waitForGame()
    {
        return InteractionResult.confirmed("Waiting for game");
    }

    @Override
    public InteractionResult getCells()
    {
        return ObjectActions.interact("Uncharged cells", "Take-10");
    }

    @Override
    public InteractionResult moveToEastMine()
    {
        return ObjectActions.interact(object -> objectNameContains(object, "Rubble"), "Climb");
    }

    @Override
    public InteractionResult leaveEastMine()
    {
        return ObjectActions.interact(object -> objectNameContains(object, "Rubble"), "Climb");
    }

    @Override
    public InteractionResult enterPortal()
    {
        Optional<TileObject> portal = TileObjects.search()
                .nameContainsIgnoreCase("Portal")
                .walkable()
                .nearestByPath();
        return portal.map(tileObject -> ObjectActions.interact(tileObject, "Enter", "Exit", "Use"))
                .orElseGet(() -> InteractionResult.fail(InteractionStatus.TARGET_NOT_FOUND, "No portal"));
    }

    @Override
    public InteractionResult mineHugeGuardians()
    {
        return ObjectActions.interact(object -> objectNameContains(object, "Huge guardian remains"), "Mine");
    }

    @Override
    public InteractionResult mineLargeGuardians()
    {
        return ObjectActions.interact(object -> objectNameContains(object, "Large guardian remains"), "Mine");
    }

    @Override
    public InteractionResult mineRegularGuardians()
    {
        return ObjectActions.interact(object -> objectNameContains(object, "Guardian parts"), "Mine");
    }

    @Override
    public InteractionResult craftEssence()
    {
        return ObjectActions.interact(object -> objectNameContains(object, "Workbench"), "Work-at");
    }

    @Override
    public InteractionResult fillPouches()
    {
        Optional<Widget> pouch = Inventory.search()
                .idIn(new HashSet<>(Arrays.asList(ItemID.RCU_POUCH_SMALL, ItemID.RCU_POUCH_MEDIUM, ItemID.RCU_POUCH_LARGE,
                        ItemID.RCU_POUCH_GIANT, ItemID.RCU_POUCH_COLOSSAL)))
                .first();
        return pouch.map(widget -> InventoryActions.use(widget, "Fill"))
                .orElseGet(() -> InteractionResult.fail(InteractionStatus.TARGET_NOT_FOUND, "No pouch to fill"));
    }

    @Override
    public InteractionResult powerGuardian()
    {
        return NPCActions.interact("The Great Guardian", "Power-up");
    }

    @Override
    public InteractionResult useCell()
    {
        Optional<Widget> cell = Inventory.search().idIn(CELL_IDS).first();
        Optional<TileObject> tile = TileObjects.search()
                .nameContainsIgnoreCase("cell tile")
                .walkable()
                .nearestByPath();
        if (cell.isEmpty() || tile.isEmpty())
        {
            return InteractionResult.fail(InteractionStatus.TARGET_NOT_FOUND, "No cell or cell tile");
        }
        return ObjectActions.interact(tile.get(), "Place-cell");
    }

    @Override
    public InteractionResult enterAltar()
    {
        Optional<TileObject> altar = nextAltar();
        return altar.map(tileObject -> ObjectActions.interact(tileObject, "Enter"))
                .orElseGet(() -> InteractionResult.fail(InteractionStatus.TARGET_NOT_FOUND, "No altar"));
    }

    @Override
    public InteractionResult craftRunes()
    {
        Optional<Widget> fullPouch = Inventory.search()
                .idIn(new HashSet<>(Arrays.asList(ItemID.RCU_POUCH_SMALL, ItemID.RCU_POUCH_MEDIUM, ItemID.RCU_POUCH_LARGE,
                        ItemID.RCU_POUCH_GIANT, ItemID.RCU_POUCH_COLOSSAL)))
                .first();
        if (fullPouch.isPresent() && Inventory.getEmptySlots() > 0)
        {
            InventoryActions.use(fullPouch.get(), "Empty");
        }
        return ObjectActions.interact(object -> objectHasAction(object, "Craft-rune"), "Craft-rune");
    }

    @Override
    public InteractionResult leaveAltar()
    {
        return ObjectActions.interact(object -> objectNameContains(object, "Portal"), "Use");
    }

    @Override
    public InteractionResult depositRunes()
    {
        return ObjectActions.interact(object -> objectHasAction(object, "Deposit-runes"), "Deposit-runes");
    }

    @Override
    public InteractionResult dropRunes()
    {
        Optional<Widget> filtered = filteredRune();
        if (filtered.isPresent())
        {
            return InventoryActions.use(filtered.get(), "Drop");
        }
        Optional<Widget> anyRune = Inventory.search().idIn(RUNE_IDS).first();
        return anyRune.map(widget -> InventoryActions.use(widget, "Drop"))
                .orElseGet(() -> InteractionResult.fail(InteractionStatus.TARGET_NOT_FOUND, "No rune to drop"));
    }

    private boolean hasPickaxe()
    {
        return Inventory.search().idInList(PICKAXE_IDS).first().isPresent()
                || Equipment.search().idInList(PICKAXE_IDS).first().isPresent();
    }

    private boolean degradedPouchPresent()
    {
        return Inventory.search().result().stream()
                .anyMatch(widget -> widget != null && GuardiansRiftPouchRules.isDegradedPouch(widget.getItemId()));
    }

    private Set<Integer> inventoryPouchIds()
    {
        return Inventory.search().result().stream()
                .filter(widget -> widget != null)
                .map(Widget::getItemId)
                .filter(id -> id == ItemID.RCU_POUCH_SMALL || id == ItemID.RCU_POUCH_MEDIUM || id == ItemID.RCU_POUCH_LARGE
                        || id == ItemID.RCU_POUCH_GIANT || id == ItemID.RCU_POUCH_COLOSSAL)
                .collect(Collectors.toSet());
    }

    private boolean portalSpawned()
    {
        return visibleWidget(GOTR_PORTAL_WIDGET);
    }

    private boolean visibleWidget(int widgetId)
    {
        Widget widget = client == null ? null : client.getWidget(widgetId);
        return widget != null && !widget.isHidden();
    }

    private int guardianPower()
    {
        Widget widget = client == null ? null : client.getWidget(GOTR_POWER_WIDGET);
        if (widget == null || widget.getText() == null)
        {
            return 0;
        }
        String digits = widget.getText().replaceAll("[^0-9]", "");
        return digits.isEmpty() ? 0 : Integer.parseInt(digits);
    }

    private boolean isInAltar()
    {
        WorldView worldView = client == null ? null : client.getTopLevelWorldView();
        if (worldView == null || worldView.getMapRegions() == null)
        {
            return false;
        }
        for (int region : worldView.getMapRegions())
        {
            if (region == 14483 || region == 14484)
            {
                return false;
            }
        }
        return true;
    }

    private boolean isGameBusy()
    {
        Player player = client == null ? null : client.getLocalPlayer();
        return player != null
                && player.getWorldLocation().getY() <= OUTSIDE_BARRIER_Y
                && !isInAltar()
                && TileObjects.search().withId(BARRIER_BUSY_ID).nearestToPlayer().isPresent();
    }

    private GuardiansRiftRepairContext repairContext()
    {
        return new GuardiansRiftRepairContext(
                NPCs.search().withName("Apprentice Cordelia").walkable().nearestByPath().isPresent(),
                hasNpcContactRunes(),
                GameVars.getVarbit(SPELLBOOK_VARBIT) == LUNAR_SPELLBOOK,
                NPCs.search().withName("Dark Mage").walkable().nearestByPath().isPresent());
    }

    private boolean hasNpcContactRunes()
    {
        return RunePouchApi.hasAmount(ItemID.AIRRUNE, 2)
                && RunePouchApi.hasAmount(ItemID.ASTRALRUNE, 1)
                && RunePouchApi.hasAmount(ItemID.COSMICRUNE, 1);
    }

    private Optional<TileObject> nextAltar()
    {
        List<TileObject> objects = TileObjects.search()
                .nameContainsIgnoreCase("Guardian of")
                .walkable()
                .result();
        List<GuardiansRiftAltarCandidate> candidates = new ArrayList<>();
        for (TileObject object : objects)
        {
            GuardiansRiftAltar altar = altarFor(object.getId());
            if (altar == null)
            {
                continue;
            }
            boolean active = objectActive(object);
            boolean talisman = Inventory.getItemAmount(altar.getTalismanId()) > 0;
            candidates.add(active ? GuardiansRiftAltarCandidate.active(altar)
                    : GuardiansRiftAltarCandidate.inactive(altar, talisman));
        }

        GuardiansRiftAltarContext context = GuardiansRiftAltarContext.builder()
                .runecraftLevel(client == null ? 1 : client.getRealSkillLevel(Skill.RUNECRAFT))
                .completedQuests(completedQuests())
                .ignoredAltars(ignoredAltars())
                .elementalPoints(GameVars.getVarbit(13686))
                .catalyticPoints(GameVars.getVarbit(13685))
                .prioritizeBloodDeath(config.prioritizeBloodDeath())
                .build();
        Optional<GuardiansRiftAltar> selected = GuardiansRiftAltarSelector.choose(candidates, context);
        if (selected.isEmpty())
        {
            return Optional.empty();
        }
        return objects.stream()
                .filter(object -> object.getId() == selected.get().getObjectId())
                .findFirst();
    }

    private Set<GuardiansRiftQuest> completedQuests()
    {
        EnumSet<GuardiansRiftQuest> quests = EnumSet.noneOf(GuardiansRiftQuest.class);
        addQuest(quests, GuardiansRiftQuest.LOST_CITY, Quest.LOST_CITY);
        addQuest(quests, GuardiansRiftQuest.TROLL_STRONGHOLD, Quest.TROLL_STRONGHOLD);
        addQuest(quests, GuardiansRiftQuest.MOURNINGS_END_PART_II, Quest.MOURNINGS_END_PART_II);
        addQuest(quests, GuardiansRiftQuest.SINS_OF_THE_FATHER, Quest.SINS_OF_THE_FATHER);
        return quests;
    }

    private void addQuest(Set<GuardiansRiftQuest> quests, GuardiansRiftQuest localQuest, Quest quest)
    {
        if (client != null && quest.getState(client) == QuestState.FINISHED)
        {
            quests.add(localQuest);
        }
    }

    private Set<GuardiansRiftAltar> ignoredAltars()
    {
        EnumSet<GuardiansRiftAltar> ignored = EnumSet.noneOf(GuardiansRiftAltar.class);
        if (config.ignoreAir()) ignored.add(GuardiansRiftAltar.AIR);
        if (config.ignoreMind()) ignored.add(GuardiansRiftAltar.MIND);
        if (config.ignoreWater()) ignored.add(GuardiansRiftAltar.WATER);
        if (config.ignoreEarth()) ignored.add(GuardiansRiftAltar.EARTH);
        if (config.ignoreFire()) ignored.add(GuardiansRiftAltar.FIRE);
        if (config.ignoreBody()) ignored.add(GuardiansRiftAltar.BODY);
        if (config.ignoreCosmic()) ignored.add(GuardiansRiftAltar.COSMIC);
        if (config.ignoreChaos()) ignored.add(GuardiansRiftAltar.CHAOS);
        if (config.ignoreNature()) ignored.add(GuardiansRiftAltar.NATURE);
        if (config.ignoreLaw()) ignored.add(GuardiansRiftAltar.LAW);
        if (config.ignoreDeath()) ignored.add(GuardiansRiftAltar.DEATH);
        if (config.ignoreBlood()) ignored.add(GuardiansRiftAltar.BLOOD);
        return ignored;
    }

    private static GuardiansRiftAltar altarFor(int objectId)
    {
        for (GuardiansRiftAltar altar : GuardiansRiftAltar.values())
        {
            if (altar.getObjectId() == objectId)
            {
                return altar;
            }
        }
        return null;
    }

    private boolean objectActive(TileObject object)
    {
        if (!(object instanceof GameObject))
        {
            return false;
        }
        GameObject gameObject = (GameObject) object;
        return gameObject.getRenderable() instanceof DynamicObject
                && ((DynamicObject) gameObject.getRenderable()).getAnimation() != null
                && ((DynamicObject) gameObject.getRenderable()).getAnimation().getId() == ACTIVE_ALTAR_ANIMATION;
    }

    private boolean droppableRunesPresent()
    {
        return filteredRune().isPresent();
    }

    private Optional<Widget> filteredRune()
    {
        String raw = config.dropRunesFilter();
        if (raw == null || raw.trim().isEmpty())
        {
            return Optional.empty();
        }
        for (String token : raw.split(","))
        {
            String name = token.trim();
            if (!name.isEmpty())
            {
                Optional<Widget> widget = Inventory.search().nameContainsIgnoreCase(name).first();
                if (widget.isPresent())
                {
                    return widget;
                }
            }
        }
        return Optional.empty();
    }

    private boolean objectNameContains(TileObject object, String text)
    {
        ObjectComposition composition = objectComposition(object);
        return composition != null && composition.getName() != null
                && composition.getName().toLowerCase().contains(text.toLowerCase());
    }

    private boolean objectHasAction(TileObject object, String action)
    {
        ObjectComposition composition = objectComposition(object);
        if (composition == null || composition.getActions() == null)
        {
            return false;
        }
        for (String candidate : composition.getActions())
        {
            if (action.equals(candidate))
            {
                return true;
            }
        }
        return false;
    }

    private ObjectComposition objectComposition(TileObject object)
    {
        if (client == null || object == null)
        {
            return null;
        }
        return client.getObjectDefinition(object.getId());
    }

    void onGameMessage(String message)
    {
        if (message == null)
        {
            return;
        }
        if (message.contains("Creatures from the Abyss will attack in 120 seconds."))
        {
            gameStarted = true;
        }
        if (message.contains("is fully charged"))
        {
            gameEnding = true;
        }
        if (message.contains("successfully closed the rift") || message.contains("Guardian was defeated"))
        {
            gameStarted = false;
            firstPortalSpawned = false;
            gameEnding = false;
        }
    }
}
