using Microsoft.Xna.Framework;
using MonoGame.FourEngine.Resources;
using MonoGame.FourEngine.Tiled;
using System;
using System.Collections.Generic;
using System.Linq;

namespace MonoGame.FourEngine.Raycast.Tiled
{
    public class RaycastTiledMap : RaycastMap
    {
        public IRaycastCamera Camera { get; private set; }
        public List<IRaycastObject> Objects { get; private set; } = new List<IRaycastObject>();
        public List<Entity> OtherEntites { get; private set; } = new List<Entity>();

        private Point _tileSize;

        public RaycastTiledMap(Scene scene, MapDefinition def) : this(scene, def.FileName)
        { }

        public RaycastTiledMap(Scene scene, string fileName)
        {
            LoadMap(fileName);
            if (Camera is Entity cameraEntity)
                scene.AddEntity(cameraEntity);
            foreach (var obj in Objects)
                if (obj is Entity entity)
                    scene.AddEntity(entity);
            foreach (var entity in OtherEntites)
                scene.AddEntity(entity);
        }

        private void LoadMap(string fileName, bool hasFloor = true)
        {
            var map = new Map(fileName);
            var tileLayersDictionary = new Dictionary<int, List<TiledEntity>>();
            foreach (var entity in map.GetEntities())
            {
                switch (entity)
                {
                    case TiledEntity tiledEntity:
                        AddTiledEntity(tileLayersDictionary, tiledEntity);
                        break;
                    case IRaycastCamera camera:
                        AddMap(camera);
                        Camera = camera;
                        break;
                    case IRaycastObject obj:
                        AddMap(obj);
                        Objects.Add(obj);
                        break;
                    default:
                        OtherEntites.Add(entity);
                        break;
                }
            }

            Layers = new List<RaycastLayer>(tileLayersDictionary.Count);
            var tileLayers = tileLayersDictionary
                .OrderBy(x => x.Key)
                .Select(x => x.Value)
                .ToArray();
            var tiledEntites = tileLayersDictionary.SelectMany(x => x.Value).ToList();
            var mapSize = new Point(
                tiledEntites.Max(x => x.Position.X / x.Width) + 1,
                tiledEntites.Max(x => x.Position.Y / x.Height) + 1);

            for (var i = 0; i < tileLayers.Length; i++)
            {
                if (i == 0 && hasFloor)
                    Floor = CreateLayer(tileLayers[i], mapSize);
                else
                    Layers.Add(CreateLayer(tileLayers[i], mapSize));
            }

            if (Floor == null)
                throw new ArgumentException("Map has no floor! Consider setting hasFloor to false.");

            if (!Layers.Any())
                throw new ArgumentException("Map has no layers! Please add a layer to the map.");

            if (Camera == null)
                throw new ArgumentException("Map has no camera! Please add IRaycastCamera to the map.");
            Camera.Position = Camera.Position / _tileSize.ToVector2();

            foreach (var obj in Objects)
            {
                obj.Position = obj.Position / _tileSize.ToVector2();
            }
        }

        private static void AddTiledEntity(Dictionary<int, List<TiledEntity>> tileLayers, TiledEntity tiledEntity)
        {
            var layer = tiledEntity.Layer;
            if (!tileLayers.ContainsKey(layer))
                tileLayers.Add(layer, new List<TiledEntity>());
            tileLayers[layer].Add(tiledEntity);
        }

        private RaycastLayer CreateLayer(List<TiledEntity> entities, Point mapSize)
        {
            Sprite tileSet = null;
            if (entities.Any())
            {
                tileSet = entities.First().SpriteComponent.Sprite;
                _tileSize = tileSet.Size;
            }
            var pairs = entities.Select(x => new
            {
                Position = x.Position / x.SpriteComponent.Sprite.Size,
                x.Frame
            }).ToList();
            var array = new int[mapSize.X, mapSize.Y];
            foreach (var pair in pairs)
            {
                // TODO: Allow 2 dimensional indexes
                if (pair.Frame.Y != 0)
                    throw new NotImplementedException("Sorry, only 1 dimensional tilemaps yet");
                array[pair.Position.X, pair.Position.Y]
                    = pair.Frame.X + 1; // Compensate tilesetGid
            }
            return new RaycastLayer(array, tileSet);
        }

        private void AddMap(object obj)
        {
            if (obj is INeedMap needsMap)
                needsMap.Map = this;
        }
    }
}