package com.openosrs.client.core;

import com.openosrs.client.api.types.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * WorldState - Manages the game world state including NPCs, objects, and items.
 * 
 * This class provides agents with access to:
 * - NPCs in the local area
 * - Interactive objects (doors, trees, rocks, etc.)
 * - Ground items
 * - Other players
 * - Regional information
 */
public class WorldState {
    private static final Logger logger = LoggerFactory.getLogger(WorldState.class);
    
    private final EventSystem eventSystem;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    
    // World entities - Using our modernized API types
    private final ConcurrentHashMap<Integer, NPC> npcs;
    private final ConcurrentHashMap<Integer, GameObject> objects;
    private final ConcurrentHashMap<Integer, GroundItem> groundItems;
    private final ConcurrentHashMap<Integer, OtherPlayer> otherPlayers;
    
    // Region information
    private final AtomicInteger currentRegionId = new AtomicInteger(-1);
    private final AtomicInteger[] regionIds = new AtomicInteger[4]; // Current 2x2 region area
    
    // Cache for frequently accessed data
    private volatile List<NPC> npcCache = new CopyOnWriteArrayList<>();
    private volatile List<GameObject> objectCache = new CopyOnWriteArrayList<>();
    private volatile List<GroundItem> groundItemCache = new CopyOnWriteArrayList<>();
    private volatile boolean cacheNeedsRefresh = true;
    
    public WorldState(EventSystem eventSystem) {
        this.eventSystem = eventSystem;
        this.npcs = new ConcurrentHashMap<>();
        this.objects = new ConcurrentHashMap<>();
        this.groundItems = new ConcurrentHashMap<>();
        this.otherPlayers = new ConcurrentHashMap<>();
        
        for (int i = 0; i < regionIds.length; i++) {
            regionIds[i] = new AtomicInteger(-1);
        }
        
        logger.debug("WorldState initialized");
    }
    
    public void initialize() {
        logger.debug("Initializing WorldState");
        // Clear any existing state
        npcs.clear();
        objects.clear();
        groundItems.clear();
        otherPlayers.clear();
    }
    
    public void shutdown() {
        logger.debug("Shutting down WorldState");
        npcs.clear();
        objects.clear();
        groundItems.clear();
        otherPlayers.clear();
    }
    
    public void tick() {
        // Update cache if needed
        if (cacheNeedsRefresh) {
            refreshCaches();
        }
        
        // Update NPC states - ticking individual entities if they have state
        // (In a real implementation, NPCs might have timers, animations, etc.)
        npcs.values().forEach(npc -> {
            // Update NPC-specific state if needed
            // For now, NPCs are mostly immutable data structures
        });
        
        // Update ground items (remove expired ones)
        List<Integer> expiredItems = groundItems.entrySet().stream()
            .filter(entry -> {
                GroundItem item = entry.getValue();
                // Check if item has expired (5 minutes = 300,000ms)
                return System.currentTimeMillis() - item.getSpawnTime() > 300000;
            })
            .map(Map.Entry::getKey)
            .collect(Collectors.toList());
            
        for (Integer key : expiredItems) {
            GroundItem item = groundItems.remove(key);
            if (item != null) {
                eventSystem.fireEvent(EventSystem.EventType.ITEM_DESPAWNED, 
                    new ItemDespawnedEvent(item.getItemId(), item.getPosition().getX(), item.getPosition().getY()));
                logger.debug("Ground item {} expired at {}", item.getItemId(), item.getPosition());
                cacheNeedsRefresh = true;
            }
        }
        
        // Update other players (similar to NPCs, mostly immutable for now)
        otherPlayers.values().forEach(player -> {
            // Update player-specific state if needed
        });
    }
    
    private void refreshCaches() {
        lock.writeLock().lock();
        try {
            npcCache = new CopyOnWriteArrayList<>(npcs.values());
            objectCache = new CopyOnWriteArrayList<>(objects.values());
            groundItemCache = new CopyOnWriteArrayList<>(groundItems.values());
            cacheNeedsRefresh = false;
            logger.debug("World state caches refreshed: {} NPCs, {} objects, {} ground items", 
                npcCache.size(), objectCache.size(), groundItemCache.size());
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    // === BULK UPDATE METHODS FOR BRIDGE INTEGRATION ===
    
    /**
     * Update all NPCs from bridge data (called by BridgeAdapter)
     */
    public void updateNPCs(List<NPC> newNPCs) {
        if (newNPCs == null) return;
        
        lock.writeLock().lock();
        try {
            // Clear existing NPCs
            npcs.clear();
            
            // Add new NPCs
            for (NPC npc : newNPCs) {
                if (npc != null && npc.getIndex() >= 0) {
                    npcs.put(npc.getIndex(), npc);
                }
            }
            
            cacheNeedsRefresh = true;
            logger.debug("Updated {} NPCs from bridge", newNPCs.size());
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    /**
     * Update all game objects from bridge data
     */
    public void updateGameObjects(List<GameObject> newObjects) {
        if (newObjects == null) return;
        
        lock.writeLock().lock();
        try {
            // Clear existing objects
            objects.clear();
            
            // Add new objects (use hash of position as key)
            for (GameObject obj : newObjects) {
                if (obj != null && obj.getPosition() != null) {
                    int key = generateObjectKey(obj.getPosition());
                    objects.put(key, obj);
                }
            }
            
            cacheNeedsRefresh = true;
            logger.debug("Updated {} game objects from bridge", newObjects.size());
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    /**
     * Update all ground items from bridge data
     */
    public void updateGroundItems(List<GroundItem> newItems) {
        if (newItems == null) return;
        
        lock.writeLock().lock();
        try {
            // Clear existing ground items
            groundItems.clear();
            
            // Add new ground items
            for (GroundItem item : newItems) {
                if (item != null && item.getPosition() != null) {
                    int key = generateItemKey(item.getPosition(), item.getItemId());
                    groundItems.put(key, item);
                }
            }
            
            cacheNeedsRefresh = true;
            logger.debug("Updated {} ground items from bridge", newItems.size());
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    // === QUERY METHODS FOR AGENTS ===
    
    public NPC getNPC(int index) {
        return npcs.get(index);
    }
    
    public List<NPC> getAllNPCs() {
        return new CopyOnWriteArrayList<>(npcCache);
    }
    
    public List<NPC> getNPCsById(int id) {
        return npcCache.stream()
            .filter(npc -> npc.getId() == id)
            .collect(Collectors.toList());
    }
    
    public List<NPC> getNPCsByName(String name) {
        if (name == null) return List.of();
        
        return npcCache.stream()
            .filter(npc -> name.equalsIgnoreCase(npc.getName()))
            .collect(Collectors.toList());
    }
    
    public List<NPC> getNPCsInRadius(Position center, int radius) {
        if (center == null) return List.of();
        
        return npcCache.stream()
            .filter(npc -> {
                Position npcPos = npc.getPosition();
                if (npcPos == null || npcPos.getPlane() != center.getPlane()) {
                    return false;
                }
                return npcPos.distanceTo(center) <= radius;
            })
            .collect(Collectors.toList());
    }
    
    public NPC getClosestNPC(Position playerPos, int npcId) {
        if (playerPos == null) return null;
        
        return npcCache.stream()
            .filter(npc -> npc.getId() == npcId)
            .filter(npc -> npc.getPosition() != null && npc.getPosition().getPlane() == playerPos.getPlane())
            .min((npc1, npc2) -> Double.compare(
                npc1.getPosition().distanceTo(playerPos),
                npc2.getPosition().distanceTo(playerPos)
            ))
            .orElse(null);
    }
    
    // === GAME OBJECT METHODS ===
    
    public GameObject getObjectAt(Position position) {
        if (position == null) return null;
        return objects.get(generateObjectKey(position));
    }
    
    public List<GameObject> getAllGameObjects() {
        return new CopyOnWriteArrayList<>(objectCache);
    }
    
    public List<GameObject> getObjectsById(int id) {
        return objectCache.stream()
            .filter(obj -> obj.getId() == id)
            .collect(Collectors.toList());
    }
    
    public List<GameObject> getObjectsByName(String name) {
        if (name == null) return List.of();
        
        return objectCache.stream()
            .filter(obj -> name.equalsIgnoreCase(obj.getName()))
            .collect(Collectors.toList());
    }
    
    public List<GameObject> getObjectsWithAction(String action) {
        if (action == null) return List.of();
        
        return objectCache.stream()
            .filter(obj -> obj.hasAction(action))
            .collect(Collectors.toList());
    }
    
    public GameObject getClosestGameObject(Position playerPos, int objectId) {
        if (playerPos == null) return null;
        
        return objectCache.stream()
            .filter(obj -> obj.getId() == objectId)
            .filter(obj -> obj.getPosition() != null && obj.getPosition().getPlane() == playerPos.getPlane())
            .min((obj1, obj2) -> Double.compare(
                obj1.getPosition().distanceTo(playerPos),
                obj2.getPosition().distanceTo(playerPos)
            ))
            .orElse(null);
    }
    
    private int generateObjectKey(Position position) {
        return (position.getPlane() << 24) | (position.getX() << 12) | position.getY();
    }
    
    // === GROUND ITEM METHODS ===
    
    public GroundItem getGroundItemAt(Position position, int itemId) {
        if (position == null) return null;
        return groundItems.get(generateItemKey(position, itemId));
    }
    
    public List<GroundItem> getGroundItemsAt(Position position) {
        if (position == null) return List.of();
        
        return groundItemCache.stream()
            .filter(item -> position.equals(item.getPosition()))
            .collect(Collectors.toList());
    }
    
    public List<GroundItem> getAllGroundItems() {
        return new CopyOnWriteArrayList<>(groundItemCache);
    }
    
    public List<GroundItem> getGroundItemsById(int itemId) {
        return groundItemCache.stream()
            .filter(item -> item.getItemId() == itemId)
            .collect(Collectors.toList());
    }
    
    public GroundItem getClosestGroundItem(Position playerPos, int itemId) {
        if (playerPos == null) return null;
        
        return groundItemCache.stream()
            .filter(item -> item.getItemId() == itemId)
            .filter(item -> item.getPosition() != null && item.getPosition().getPlane() == playerPos.getPlane())
            .min((item1, item2) -> Double.compare(
                item1.getPosition().distanceTo(playerPos),
                item2.getPosition().distanceTo(playerPos)
            ))
            .orElse(null);
    }
    
    private int generateItemKey(Position position, int itemId) {
        return (position.getPlane() << 26) | (itemId << 14) | (position.getX() << 7) | position.getY();
    }
    
    // === OTHER PLAYER METHODS ===
    
    public void updateOtherPlayers(List<OtherPlayer> newPlayers) {
        if (newPlayers == null) return;
        
        lock.writeLock().lock();
        try {
            otherPlayers.clear();
            for (OtherPlayer player : newPlayers) {
                if (player != null && player.getIndex() >= 0) {
                    otherPlayers.put(player.getIndex(), player);
                }
            }
            logger.debug("Updated {} other players from bridge", newPlayers.size());
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public OtherPlayer getOtherPlayer(int index) {
        return otherPlayers.get(index);
    }
    
    public List<OtherPlayer> getAllOtherPlayers() {
        return new CopyOnWriteArrayList<>(otherPlayers.values());
    }
    
    // === REGION MANAGEMENT ===
    
    public void updateRegion(int regionId) {
        int oldRegion = currentRegionId.getAndSet(regionId);
        if (oldRegion != regionId && regionId != -1) {
            logger.info("Region changed from {} to {}", oldRegion, regionId);
            
            // When changing regions, clear all world entities
            // They will be repopulated by the bridge on the next update
            clearAllEntities();
            
            eventSystem.fireEvent(EventSystem.EventType.REGION_CHANGED, 
                new RegionChangedEvent(oldRegion, regionId));
        }
    }
    
    private void clearAllEntities() {
        lock.writeLock().lock();
        try {
            npcs.clear();
            objects.clear();
            groundItems.clear();
            otherPlayers.clear();
            cacheNeedsRefresh = true;
            logger.debug("Cleared all world entities due to region change");
        } finally {
            lock.writeLock().unlock();
        }
    }
    
    public int getCurrentRegionId() {
        return currentRegionId.get();
    }
    
    // === EVENT CLASSES ===
    
    public static class RegionChangedEvent extends EventSystem.GameEvent {
        private final int oldRegionId, newRegionId;
        
        public RegionChangedEvent(int oldRegionId, int newRegionId) {
            super(EventSystem.EventType.REGION_CHANGED);
            this.oldRegionId = oldRegionId;
            this.newRegionId = newRegionId;
        }
        
        public int getOldRegionId() { return oldRegionId; }
        public int getNewRegionId() { return newRegionId; }
    }
    
    public static class ItemDespawnedEvent extends EventSystem.GameEvent {
        private final int itemId, x, y;
        
        public ItemDespawnedEvent(int itemId, int x, int y) {
            super(EventSystem.EventType.ITEM_DESPAWNED);
            this.itemId = itemId;
            this.x = x;
            this.y = y;
        }
        
        public int getItemId() { return itemId; }
        public int getX() { return x; }
        public int getY() { return y; }
    }
}