using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using MonoGame.FourEngine.Attributes;
using MonoGame.FourEngine.Collections;
using MonoGame.FourEngine.Components;
using MonoGame.FourEngine.Exceptions;
using MonoGame.FourEngine.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace MonoGame.FourEngine
{
    public abstract class DrawingSurfaceBase : Entity
    {
        public Scene Scene => CurrentScene ?? (CurrentScene = CreateNewScene());
        public int Width => Target?.Width ?? FourEngineGame.Current.GraphicsDevice.Viewport.Width;
        public int Height => Target?.Height ?? FourEngineGame.Current.GraphicsDevice.Viewport.Height;
        public RenderTarget2D Target { get; }

        public Point Position
        {
            get => Component.Position;
            set => Component.Position = value;
        }

        public ICollection<SpriteDrawHandler> Transformations { get; private set; }
        
        public DrawingSurfaceComponent Component { get; }

        protected Type NextSceneType;
        protected Scene CurrentScene;

        public DrawingSurfaceBase(int width, int height) : this(new RenderTarget2D(FourEngineGame.Current.GraphicsDevice, width, height))
        {
        }

        public DrawingSurfaceBase(Point size) : this(size.X, size.Y)
        {
        }

        public DrawingSurfaceBase(RenderTarget2D target)
        {
            Target = target;
            var sceneTypes = ServiceLocator.Types.Values
                .Where(t => typeof(Scene).GetTypeInfo().IsAssignableFrom(t) && !t.IsAbstract && !t.IsInterface)
                .ToList();
            var defaultSceneType = sceneTypes.FirstOrDefault(t => t.HasAttribute<DefaultAttribute>())
                ?? sceneTypes.FirstOrDefault();
            NextSceneType = defaultSceneType?.AsType() ?? throw new ImplementationNotFoundException(typeof(Scene));
            Components.Add(Component = new DrawingSurfaceComponent(this));
            Transformations = new SimpleObservableCollection<SpriteDrawHandler>(OnTransformationAdd, OnTransformationRemove);
        }

        public void Update(UpdateEventArgs args)
            => Scene.Update(args);

        public void Update(GameTime time) 
            => Scene.Update(time);

        public void Draw(GameTime time)
        {
            var surfaces = Scene.Entities.Where(x => x is DrawingSurfaceBase).Cast<DrawingSurfaceBase>();
            foreach (var surface in surfaces)
            {
                surface.Draw(time);
            }
            FourEngineGame.Current.GraphicsDevice.SetRenderTarget(Target);
            Scene.Draw(time);
        }

        private void OnTransformationAdd(SpriteDrawHandler handler)
        {
            Component.BeforeDraw += handler;
        }

        private void OnTransformationRemove(SpriteDrawHandler handler)
        {
            Component.BeforeDraw -= handler;
        }

        private Scene CreateNewScene()
        {
            var scene = (Scene)Activator.CreateInstance(NextSceneType);
            scene.DrawingSurface = this;
            return scene;
        }

        public override void Dispose()
        {
            base.Dispose();
            Transformations.Clear();
        }
    }

    public class DrawingSurface : DrawingSurfaceBase
    {
        public DrawingSurface(RenderTarget2D target) : base(target)
        {
        }

        public DrawingSurface(Point size) : base(size)
        {
        }

        public DrawingSurface(int width, int height) : base(width, height)
        {
        }

        public T NavigateTo<T>() where T : Scene
        {
            CurrentScene?.OnDetach(this);
            CurrentScene?.Dispose();
            NextSceneType = typeof(T);
            CurrentScene = null;
            return (T)Scene;
        }

        public Scene NavigateTo(Type type)
        {
            if (CurrentScene != null)
            {
                CurrentScene.OnDetach(this);
                CurrentScene.Dispose();
            }
            NextSceneType = type;
            CurrentScene = null;
            return Scene;
        }
    }

    public class DrawingSurface<T> : DrawingSurfaceBase where T : Scene
    {
        public new T Scene => (T)base.Scene;

        public DrawingSurface(RenderTarget2D target) : base(target)
        {
            Init();
        }

        public DrawingSurface(Point size) : base(size)
        {
            Init();
        }

        public DrawingSurface(int width, int height) : base(width, height)
        {
            Init();
        }

        private void Init()
        {
            NextSceneType = typeof(T);
            CurrentScene = null;
        }
    }
}