package net.runelite.client.plugins.microbot.api.tileobject;

import com.google.inject.Inject;
import com.google.inject.Singleton;
import net.runelite.api.Client;
import net.runelite.api.GameObject;
import net.runelite.api.Player;
import net.runelite.api.Tile;
import net.runelite.api.WorldEntity;
import net.runelite.api.WorldView;
import net.runelite.api.gameval.VarbitID;
import net.runelite.client.callback.ClientThread;
import net.runelite.client.plugins.microbot.Microbot;
import net.runelite.client.plugins.microbot.api.boat.Rs2BoatCache;
import net.runelite.client.plugins.microbot.api.tileobject.models.Rs2TileObjectModel;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

@Singleton
public final class Rs2TileObjectCache {

    private final Client client;
    private final ClientThread clientThread;

    private int lastUpdateObjects = 0;
    private List<Rs2TileObjectModel> tileObjects = new ArrayList<>();

    @Inject
    public Rs2TileObjectCache(Client client, ClientThread clientThread) {
        this.client = client;
        this.clientThread = clientThread;
    }

    public Rs2TileObjectQueryable query() {
        return new Rs2TileObjectQueryable();
    }

    /**
     * Get all tile objects in the current scene
     *
     * @return Stream of Rs2TileObjectModel
     */
    public Stream<Rs2TileObjectModel> getStream() {
        return clientThread.invoke(this::getSnapshot).stream();
    }

    /**
     * RuneLite's scene and WorldView accessors are client-thread-only.  Keep
     * both the freshness check and the complete scene copy inside the same
     * client-thread invocation so callers can safely consume the resulting
     * immutable snapshot from script threads.
     */
    private List<Rs2TileObjectModel> getSnapshot() {
        if (lastUpdateObjects >= client.getTickCount()) {
            return tileObjects;
        }

        Player player = client.getLocalPlayer();
        if (player == null) return List.of();

        List<Rs2TileObjectModel> result = new ArrayList<>();

        /*
         * WorldViewLoaded can fire before MicrobotPlugin subscribes during a
         * client restart inside an existing instance (notably a docked player
         * boat).  Seed every refresh with the views that are authoritative now,
         * so facilities such as the helm, cargo hold and gangplank do not vanish
         * from the cache until the player leaves and re-enters the instance.
         */
        WorldView playerWorldView = player.getWorldView();
        WorldView topLevelWorldView = client.getTopLevelWorldView();
        Integer playerLocalWorldViewId = player.getLocalLocation() == null
                ? null : player.getLocalLocation().getWorldView();
        Set<Integer> worldViewIds = activeWorldViewIds(Microbot.getWorldViewIds(),
                playerLocalWorldViewId,
                topLevelWorldView == null ? null : topLevelWorldView.getId());

        Map<Integer, WorldView> worldViews = new LinkedHashMap<>();
        for (var id : worldViewIds) {
            addWorldView(worldViews, client.getWorldView(id));
        }
        // Sub-world views are not always returned by Client#getWorldView after
        // a restart.  The actor and WorldEntity references remain authoritative.
        addWorldView(worldViews, playerWorldView);
        addWorldView(worldViews, topLevelWorldView);
        WorldView localBoatWorldView = null;
        if (topLevelWorldView != null && playerLocalWorldViewId != null) {
            WorldEntity localWorldEntity = playerLocalWorldViewId != topLevelWorldView.getId()
                    ? topLevelWorldView.worldEntities().byIndex(playerLocalWorldViewId)
                    : null;
            boolean boarded = client.getVarbitValue(VarbitID.SAILING_PLAYER_IS_ON_PLAYER_BOAT) == 1
                    || client.getVarbitValue(VarbitID.SAILING_BOARDED_BOAT) == 1;
            if (localWorldEntity == null) {
                localWorldEntity = boarded
                        ? Rs2BoatCache.selfOwnedBoat(topLevelWorldView.worldEntities())
                        : null;
            }
            if (localWorldEntity != null) {
                localBoatWorldView = localWorldEntity.getWorldView();
                addWorldView(worldViews, localBoatWorldView);
            }
        }

        for (WorldView worldView : worldViews.values()) {
            Tile[][][] sceneTiles = worldView.getScene().getTiles();
            boolean localSubView = !worldView.isTopLevel()
                    && ((localBoatWorldView != null && worldView.getId() == localBoatWorldView.getId())
                    || (playerWorldView != null && worldView.getId() == playerWorldView.getId()));
            int firstPlane = localSubView ? 0 : worldView.getPlane();
            int lastPlane = localSubView ? sceneTiles.length - 1 : firstPlane;
            for (int plane = firstPlane; plane <= lastPlane; plane++) {
                for (Tile[] tileValue : sceneTiles[plane]) {
                    for (Tile tile : tileValue) {
                        if (tile == null) continue;

                        if (tile.getGameObjects() != null) {
                            for (GameObject gameObject : tile.getGameObjects()) {
                                if (gameObject == null) continue;
                                if (gameObject.getSceneMinLocation().equals(tile.getSceneLocation())) {
                                    result.add(new Rs2TileObjectModel(gameObject));
                                }
                            }
                        }
                        if (tile.getGroundObject() != null) {
                            result.add(new Rs2TileObjectModel(tile.getGroundObject()));
                        }
                        if (tile.getWallObject() != null) {
                            result.add(new Rs2TileObjectModel(tile.getWallObject()));
                        }
                        if (tile.getDecorativeObject() != null) {
                            result.add(new Rs2TileObjectModel(tile.getDecorativeObject()));
                        }
                    }
                }
            }
        }
        tileObjects = List.copyOf(result);
        lastUpdateObjects = client.getTickCount();
        return tileObjects;
    }

    private static void addWorldView(Map<Integer, WorldView> worldViews, WorldView worldView) {
        if (worldView != null) worldViews.putIfAbsent(worldView.getId(), worldView);
    }

    static Set<Integer> activeWorldViewIds(Set<Integer> announcedIds,
                                           Integer playerWorldViewId,
                                           Integer topLevelWorldViewId) {
        Set<Integer> ids = announcedIds == null
                ? new HashSet<>() : new HashSet<>(announcedIds);
        if (playerWorldViewId != null) ids.add(playerWorldViewId);
        if (topLevelWorldViewId != null) ids.add(topLevelWorldViewId);
        return ids;
    }

    /**
     * @deprecated Use {@link Microbot#getRs2TileObjectCache()}.getStream() instead
     */
    @Deprecated(since = "2.1.8", forRemoval = true)
    public static Stream<Rs2TileObjectModel> getObjectsStream() {
        return Microbot.getRs2TileObjectCache().getStream();
    }
}
