using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input.Touch;
using MLEM.Extensions;
using MLEM.Misc;
using MLEM.Startup;
using MLEM.Textures;
using TouchyTickets.Attractions;
using TouchyTickets.Upgrades;

namespace TouchyTickets; 

[DataContract]
public class ParkMap {

    private const int AdditionalRadius = 15;
    private const int AutoBuyIntervalSecs = 30;

    [DataMember]
    public readonly int Width;
    [DataMember]
    public readonly int Height;
    [DataMember]
    private readonly List<(Point, Attraction)> attractions = new();
    private readonly Dictionary<Point, int> treePositions = new();
    private readonly Dictionary<Point, int> fencePositions = new();
    private readonly Attraction[,] attractionGrid;

    [DataMember]
    public double TicketsPerSecond { get; private set; }
    public Attraction PlacingAttraction;
    public AttractionModifier PlacingModifier;
    public Point PlacingPosition;
    public Point? SelectedPosition;
    private bool draggingAttraction;
    private double autoBuyCounter;

    public ParkMap(int width, int height) {
        this.Width = width;
        this.Height = height;
        this.attractionGrid = new Attraction[width, height];

        // set up trees
        var random = new Random();
        for (var x = -ParkMap.AdditionalRadius; x < this.Width + ParkMap.AdditionalRadius; x++) {
            for (var y = -ParkMap.AdditionalRadius; y < this.Height + ParkMap.AdditionalRadius; y++) {
                var pos = new Point(x, y);
                if (this.IsInBounds(pos))
                    continue;
                if (random.Next(15) != 0)
                    continue;
                var type = random.Next(3);
                this.treePositions[pos] = type;
            }
        }

        // set up fences
        this.fencePositions[new Point(-1, -1)] = 2;
        this.fencePositions[new Point(this.Width, -1)] = 3;
        this.fencePositions[new Point(-1, this.Height)] = 4;
        this.fencePositions[new Point(this.Width, this.Height)] = 5;
        for (var x = 0; x < this.Width; x++) {
            this.fencePositions[new Point(x, -1)] = 0;
            this.fencePositions[new Point(x, this.Height)] = 0;
        }
        for (var y = 0; y < this.Height; y++) {
            this.fencePositions[new Point(-1, y)] = 1;
            this.fencePositions[new Point(this.Width, y)] = 1;
        }
    }

    public void Update(TimeSpan passed, bool wasAway) {
        var toSimulate = wasAway ? new TimeSpan(passed.Ticks / 2) : passed;

        // handle auto-buying
        this.autoBuyCounter += toSimulate.TotalSeconds;
        this.TryAutoBuy();
        var autoBuysPerAttraction = ((float) this.autoBuyCounter / ParkMap.AutoBuyIntervalSecs / this.attractions.Count).Ceil();

        // update tickets
        this.TicketsPerSecond = 0;
        foreach (var (pos, attraction) in this.attractions) {
            var genPerSecond = attraction.Update(toSimulate, this, pos);
            this.TicketsPerSecond += genPerSecond;

            // if we were away, we have to catch up with auto-buys while also taking into account
            // the amount of tickets that each ride generates. The easiest way we can do this is
            // to progress, between updating each ride, by a percentage of the total update amount
            if (wasAway) {
                for (var i = autoBuysPerAttraction; i > 0; i--) {
                    if (!this.TryAutoBuy())
                        break;
                }
            }
        }

        // map movement
        if (GameImpl.Instance.DrawMap && GameImpl.Instance.UiSystem.Controls.HandleTouch) {
            var camera = GameImpl.Instance.Camera;
            if (MlemGame.Input.GetGesture(GestureType.Pinch, out var pinch)) {
                // pinch zoom
                var center = (pinch.Position + pinch.Position2) / 2;
                var newDist = Vector2.Distance(pinch.Position + pinch.Delta, pinch.Position2 + pinch.Delta2);
                var oldDist = Vector2.Distance(pinch.Position, pinch.Position2);
                var newScale = newDist / oldDist * camera.Scale;
                camera.Zoom(newScale - camera.Scale, center);
            } else if (MlemGame.Input.GetGesture(GestureType.FreeDrag, out var drag)) {
                if (this.draggingAttraction) {
                    // move the current placing position
                    var nextPos = (camera.ToWorldPos(drag.Position + drag.Delta) / Assets.TileSize).ToPoint();
                    // drag the center of the attraction
                    nextPos -= new Point(this.PlacingAttraction.Type.Width / 2, this.PlacingAttraction.Type.Height / 2);
                    if (this.PlacingAttraction.Type.GetCoveredTiles().Select(p => nextPos + p).All(this.IsInBounds))
                        this.PlacingPosition = nextPos;
                } else {
                    // move the camera
                    camera.Position -= drag.Delta / camera.ActualScale;
                }
            } else if (this.PlacingAttraction != null) {
                foreach (var touch in MlemGame.Input.TouchState) {
                    if (touch.State != TouchLocationState.Pressed)
                        continue;
                    // when first pressing down, go into attraction drag mode if we're touching the place location
                    var offset = (camera.ToWorldPos(touch.Position) / Assets.TileSize).ToPoint();
                    this.draggingAttraction = this.PlacingAttraction.Type.GetCoveredTiles()
                        .Any(p => this.PlacingPosition + p == offset);
                }
            } else {
                // we're not placing an attraction, so we're in remove and move mode
                if (MlemGame.Input.GetGesture(GestureType.Tap, out var tap) && GameImpl.Instance.UiSystem.Controls.GetElementUnderPos(tap.Position) == null) {
                    var pos = (camera.ToWorldPos(tap.Position) / Assets.TileSize).ToPoint();
                    var attraction = this.GetAttractionAt(pos);
                    if (attraction != null && (this.PlacingModifier == null || this.PlacingModifier.IsAffected(attraction))) {
                        // actually select the top left for easy usage later
                        this.SelectedPosition = this.attractions.First(kv => kv.Item2 == attraction).Item1;
                    } else {
                        this.SelectedPosition = null;
                    }
                }
            }
            camera.ConstrainWorldBounds(new Vector2(-ParkMap.AdditionalRadius) * Assets.TileSize, new Vector2(this.Width + ParkMap.AdditionalRadius, this.Height + ParkMap.AdditionalRadius) * Assets.TileSize);
        }
    }

    public void Draw(GameTime time, SpriteBatch batch, Vector2 position, float scale, float alpha, bool showSurroundings, RectangleF visibleArea) {
        var tileSize = Assets.TileSize * scale;
        // draw ground
        var additionalRadius = showSurroundings ? ParkMap.AdditionalRadius : 0;
        var minX = Math.Max(-additionalRadius, visibleArea.Left / tileSize.X).Floor();
        var minY = Math.Max(-additionalRadius, visibleArea.Top / tileSize.Y).Floor();
        var maxX = Math.Min(this.Width + additionalRadius, visibleArea.Right / tileSize.X).Ceil();
        var maxY = Math.Min(this.Height + additionalRadius, visibleArea.Bottom / tileSize.Y).Ceil();
        for (var x = minX; x < maxX; x++) {
            for (var y = minY; y < maxY; y++) {
                var pos = new Vector2(x, y);
                var drawPos = position + pos * tileSize;
                batch.Draw(Assets.TilesTexture[0, 0], drawPos, Color.White * alpha, 0, Vector2.Zero, scale, SpriteEffects.None, 0);

                if (this.fencePositions.TryGetValue(pos.ToPoint(), out var fenceType)) {
                    batch.Draw(Assets.TilesTexture[fenceType, 1], drawPos, Color.White * alpha, 0, Vector2.Zero, scale, SpriteEffects.None, 0);
                } else if (this.treePositions.TryGetValue(pos.ToPoint(), out var treeType)) {
                    batch.Draw(Assets.TilesTexture[1 + treeType, 0], drawPos, Color.White * alpha, 0, Vector2.Zero, scale, SpriteEffects.None, 0);
                }
            }
        }
        // selected attraction
        if (this.SelectedPosition != null) {
            var selected = this.SelectedPosition.Value;
            var attr = this.GetAttractionAt(selected);
            foreach (var pos in attr.Type.GetCoveredTiles())
                batch.Draw(batch.GetBlankTexture(), new RectangleF(position + (selected + pos).ToVector2() * tileSize, tileSize), Color.Black * 0.25F * alpha);
        }
        // draw attractions
        foreach (var (pos, attraction) in this.attractions) {
            if (this.PlacingModifier != null && this.PlacingModifier.IsAffected(attraction)) {
                var color = GameImpl.Instance.Tickets >= attraction.GetModifierPrice(this.PlacingModifier) ? Color.Yellow : Color.Red;
                foreach (var offset in attraction.Type.GetCoveredTiles())
                    batch.Draw(batch.GetBlankTexture(), new RectangleF(position + (pos + offset).ToVector2() * tileSize, tileSize), color * 0.25F * alpha);
            }
            attraction.Draw(batch, position + pos.ToVector2() * tileSize, alpha, scale);
        }
        // placing attraction
        if (this.PlacingAttraction != null) {
            var placingPos = position + this.PlacingPosition.ToVector2() * tileSize;
            var color = this.CanPlace(this.PlacingPosition, this.PlacingAttraction) ? Color.Yellow : Color.Red;
            foreach (var pos in this.PlacingAttraction.Type.GetCoveredTiles())
                batch.Draw(batch.GetBlankTexture(), new RectangleF(placingPos + pos.ToVector2() * tileSize, tileSize), color * 0.25F * alpha);
            this.PlacingAttraction.Draw(batch, placingPos, alpha * 0.5F, scale);
        }
    }

    public bool CanPlace(Point position, Attraction attraction) {
        foreach (var offset in attraction.Type.GetCoveredTiles()) {
            if (!this.IsInBounds(position + offset))
                return false;
            if (this.GetAttractionAt(position + offset) != null)
                return false;
        }
        return true;
    }

    public void Place(Point position, Attraction attraction) {
        foreach (var (x, y) in attraction.Type.GetCoveredTiles())
            this.attractionGrid[position.X + x, position.Y + y] = attraction;
        this.attractions.Add((position, attraction));
    }

    public Attraction Remove(Point position) {
        var attraction = this.GetAttractionAt(position);
        if (attraction != null) {
            foreach (var (x, y) in attraction.Type.GetCoveredTiles())
                this.attractionGrid[position.X + x, position.Y + y] = null;
            this.attractions.Remove((position, attraction));
        }
        return attraction;
    }

    public Attraction GetAttractionAt(Point position) {
        return !this.IsInBounds(position) ? null : this.attractionGrid[position.X, position.Y];
    }

    public int GetAttractionAmount(AttractionType type) {
        return this.attractions.Count(a => type == null || a.Item2.Type == type);
    }

    public int GetModifierAmount(AttractionModifier modifier) {
        return this.attractions.Sum(a => a.Item2.GetModifierAmount(modifier));
    }

    public bool IsAnyAttractionAffected(AttractionModifier modifier) {
        return this.attractions.Any(a => modifier.IsAffected(a.Item2));
    }

    public IEnumerable<(Point, Attraction)> GetAttractions() {
        foreach (var attraction in this.attractions)
            yield return attraction;
    }

    public bool IsInBounds(Point pos) {
        return pos.X >= 0 && pos.Y >= 0 && pos.X < this.Width && pos.Y < this.Height;
    }

    public ParkMap Copy(int? newWidth = null, int? newHeight = null) {
        var newMap = new ParkMap(newWidth ?? this.Width, newHeight ?? this.Height);
        foreach (var (pos, attraction) in this.attractions) {
            if (newMap.CanPlace(pos, attraction))
                newMap.Place(pos, attraction);
        }
        newMap.TicketsPerSecond = this.TicketsPerSecond;
        return newMap;
    }

    private bool TryAutoBuy() {
        if (!Options.Instance.AutoBuyEnabled)
            return false;
        if (GameImpl.Instance.Tickets < Options.Instance.MinTicketsForAutoBuy)
            return false;
        if (this.autoBuyCounter < ParkMap.AutoBuyIntervalSecs)
            return false;
        this.autoBuyCounter -= ParkMap.AutoBuyIntervalSecs;

        var success = false;
        // auto-buy modifiers
        if (Upgrade.AutoPlaceModifiers[0].IsActive()) {
            // loop through all attractions, but look at attractions with fewer applied modifiers first
            foreach (var attraction in this.attractions.Select(kv => kv.Item2).OrderBy(a => a.GetModifierAmount(null))) {
                var match = AttractionModifier.Modifiers.Values.Where(m => m.IsAffected(attraction));
                // if we don't have level 2, we only want to increase existing modifiers
                if (!Upgrade.AutoPlaceModifiers[1].IsActive())
                    match = match.Where(m => attraction.GetModifierAmount(m) > 0);
                // we want to apply the least applied modifier on this attraction
                var modifier = match.OrderBy(m => attraction.GetModifierAmount(m)).FirstOrDefault();
                if (modifier != null && modifier.Buy(attraction))
                    success = true;
            }
        }
        return success;
    }

}