using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using StardewModdingAPI.Framework.StateTracking.Comparers;
using StardewModdingAPI.Framework.StateTracking.FieldWatchers;
using StardewValley;
using StardewValley.Buildings;
using StardewValley.Locations;

namespace StardewModdingAPI.Framework.StateTracking
{
    /// <summary>Detects changes to the game's locations.</summary>
    internal class WorldLocationsTracker : IWatcher
    {
        /*********
        ** Fields
        *********/
        /// <summary>Tracks changes to the location list.</summary>
        private readonly ICollectionWatcher<GameLocation> LocationListWatcher;

        /// <summary>Tracks changes to the list of active mine locations.</summary>
        private readonly ICollectionWatcher<MineShaft> MineLocationListWatcher;

        /// <summary>Tracks changes to the list of active volcano locations.</summary>
        private readonly ICollectionWatcher<GameLocation> VolcanoLocationListWatcher;

        /// <summary>A lookup of the tracked locations.</summary>
        private IDictionary<GameLocation, LocationTracker> LocationDict { get; } = new Dictionary<GameLocation, LocationTracker>(new ObjectReferenceComparer<GameLocation>());

        /// <summary>A lookup of registered buildings and their indoor location.</summary>
        private readonly IDictionary<Building, GameLocation> BuildingIndoors = new Dictionary<Building, GameLocation>(new ObjectReferenceComparer<Building>());


        /*********
        ** Accessors
        *********/
        /// <summary>Whether locations were added or removed since the last reset.</summary>
        public bool IsLocationListChanged => this.Added.Any() || this.Removed.Any();

        /// <summary>Whether any tracked location data changed since the last reset.</summary>
        public bool IsChanged => this.IsLocationListChanged || this.Locations.Any(p => p.IsChanged);

        /// <summary>The tracked locations.</summary>
        public IEnumerable<LocationTracker> Locations => this.LocationDict.Values;

        /// <summary>The locations removed since the last update.</summary>
        public ICollection<GameLocation> Added { get; } = new HashSet<GameLocation>(new ObjectReferenceComparer<GameLocation>());

        /// <summary>The locations added since the last update.</summary>
        public ICollection<GameLocation> Removed { get; } = new HashSet<GameLocation>(new ObjectReferenceComparer<GameLocation>());


        /*********
        ** Public methods
        *********/
        /// <summary>Construct an instance.</summary>
        /// <param name="locations">The game's list of locations.</param>
        /// <param name="activeMineLocations">The game's list of active mine locations.</param>
        /// <param name="activeVolcanoLocations">The game's list of active volcano locations.</param>
        public WorldLocationsTracker(ObservableCollection<GameLocation> locations, IList<MineShaft> activeMineLocations, IList<VolcanoDungeon> activeVolcanoLocations)
        {
            this.LocationListWatcher = WatcherFactory.ForObservableCollection(locations);
            this.MineLocationListWatcher = WatcherFactory.ForReferenceList(activeMineLocations);
            this.VolcanoLocationListWatcher = WatcherFactory.ForReferenceList(activeVolcanoLocations);
        }

        /// <summary>Update the current value if needed.</summary>
        public void Update()
        {
            // update watchers
            this.LocationListWatcher.Update();
            this.MineLocationListWatcher.Update();
            this.VolcanoLocationListWatcher.Update();
            foreach (LocationTracker watcher in this.Locations)
                watcher.Update();

            // detect added/removed locations
            if (this.LocationListWatcher.IsChanged)
            {
                this.Remove(this.LocationListWatcher.Removed);
                this.Add(this.LocationListWatcher.Added);
            }
            if (this.MineLocationListWatcher.IsChanged)
            {
                this.Remove(this.MineLocationListWatcher.Removed);
                this.Add(this.MineLocationListWatcher.Added);
            }
            if (this.VolcanoLocationListWatcher.IsChanged)
            {
                this.Remove(this.VolcanoLocationListWatcher.Removed);
                this.Add(this.VolcanoLocationListWatcher.Added);
            }

            // detect building changed
            foreach (LocationTracker watcher in this.Locations.Where(p => p.BuildingsWatcher.IsChanged).ToArray())
            {
                this.Remove(watcher.BuildingsWatcher.Removed);
                this.Add(watcher.BuildingsWatcher.Added);
            }

            // detect building interiors changed (e.g. construction completed)
            foreach (KeyValuePair<Building, GameLocation> pair in this.BuildingIndoors.Where(p => !object.Equals(p.Key.indoors.Value, p.Value)))
            {
                GameLocation oldIndoors = pair.Value;
                GameLocation newIndoors = pair.Key.indoors.Value;

                if (oldIndoors != null)
                    this.Added.Add(oldIndoors);
                if (newIndoors != null)
                    this.Removed.Add(newIndoors);
            }
        }

        /// <summary>Set the current location list as the baseline.</summary>
        public void ResetLocationList()
        {
            this.Removed.Clear();
            this.Added.Clear();
            this.LocationListWatcher.Reset();
            this.MineLocationListWatcher.Reset();
            this.VolcanoLocationListWatcher.Reset();
        }

        /// <summary>Set the current value as the baseline.</summary>
        public void Reset()
        {
            this.ResetLocationList();
            foreach (IWatcher watcher in this.GetWatchers())
                watcher.Reset();
        }

        /// <summary>Get whether the given location is tracked.</summary>
        /// <param name="location">The location to check.</param>
        public bool HasLocationTracker(GameLocation location)
        {
            return this.LocationDict.ContainsKey(location);
        }

        /// <summary>Stop watching the player fields and release all references.</summary>
        public void Dispose()
        {
            foreach (IWatcher watcher in this.GetWatchers())
                watcher.Dispose();
        }


        /*********
        ** Private methods
        *********/
        /****
        ** Enumerable wrappers
        ****/
        /// <summary>Add the given buildings.</summary>
        /// <param name="buildings">The buildings to add.</param>
        public void Add(IEnumerable<Building> buildings)
        {
            foreach (Building building in buildings)
                this.Add(building);
        }

        /// <summary>Add the given locations.</summary>
        /// <param name="locations">The locations to add.</param>
        public void Add(IEnumerable<GameLocation> locations)
        {
            foreach (GameLocation location in locations)
                this.Add(location);
        }

        /// <summary>Remove the given buildings.</summary>
        /// <param name="buildings">The buildings to remove.</param>
        public void Remove(IEnumerable<Building> buildings)
        {
            foreach (Building building in buildings)
                this.Remove(building);
        }

        /// <summary>Remove the given locations.</summary>
        /// <param name="locations">The locations to remove.</param>
        public void Remove(IEnumerable<GameLocation> locations)
        {
            foreach (GameLocation location in locations)
                this.Remove(location);
        }

        /****
        ** Main add/remove logic
        ****/
        /// <summary>Add the given building.</summary>
        /// <param name="building">The building to add.</param>
        public void Add(Building building)
        {
            if (building == null)
                return;

            GameLocation indoors = building.indoors.Value;
            this.BuildingIndoors[building] = indoors;
            this.Add(indoors);
        }

        /// <summary>Add the given location.</summary>
        /// <param name="location">The location to add.</param>
        public void Add(GameLocation location)
        {
            if (location == null)
                return;

            // remove old location if needed
            this.Remove(location);

            // add location
            this.Added.Add(location);
            this.LocationDict[location] = new LocationTracker(location);

            // add buildings
            if (location is BuildableGameLocation buildableLocation)
                this.Add(buildableLocation.buildings);
        }

        /// <summary>Remove the given building.</summary>
        /// <param name="building">The building to remove.</param>
        public void Remove(Building building)
        {
            if (building == null)
                return;

            this.BuildingIndoors.Remove(building);
            this.Remove(building.indoors.Value);
        }

        /// <summary>Remove the given location.</summary>
        /// <param name="location">The location to remove.</param>
        public void Remove(GameLocation location)
        {
            if (location == null)
                return;

            if (this.LocationDict.TryGetValue(location, out LocationTracker watcher))
            {
                // track change
                this.Removed.Add(location);

                // remove
                this.LocationDict.Remove(location);
                watcher.Dispose();
                if (location is BuildableGameLocation buildableLocation)
                    this.Remove(buildableLocation.buildings);
            }
        }

        /****
        ** Helpers
        ****/
        /// <summary>The underlying watchers.</summary>
        private IEnumerable<IWatcher> GetWatchers()
        {
            yield return this.LocationListWatcher;
            yield return this.MineLocationListWatcher;
            yield return this.VolcanoLocationListWatcher;
            foreach (LocationTracker watcher in this.Locations)
                yield return watcher;
        }
    }
}