using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Coroutine;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
using MLEM.Cameras;
using MLEM.Extended.Extensions;
using MLEM.Extensions;
using MLEM.Font;
using MLEM.Formatting.Codes;
using MLEM.Misc;
using MLEM.Startup;
using MLEM.Textures;
using MLEM.Ui;
using MLEM.Ui.Elements;
using MonoGame.Extended;
using MonoGame.Extended.Tiled;
using Newtonsoft.Json;
using ColorHelper = MLEM.Extensions.ColorHelper;

namespace GreatSpringGameJam {
    public class GameImpl : MlemGame {

        public static GameImpl Instance { get; private set; }
        private static readonly JsonSerializer Serializer = new();

        public Map Map { get; private set; }
        public Player Player { get; private set; }
        public Camera Camera { get; private set; }
        public bool IsInCutscene { get; private set; }
        public Dictionary<string, int> Scores { get; private set; } = new();
        private ContentManager mapLoader;

        public GameImpl() {
            Instance = this;
            this.IsMouseVisible = true;
        }

        protected override void LoadContent() {
            this.GraphicsDeviceManager.PreferredBackBufferWidth = 1280;
            this.GraphicsDeviceManager.PreferredBackBufferHeight = 720;
            this.GraphicsDeviceManager.ApplyChanges();
            base.LoadContent();

            this.UiSystem.Style.Font = new GenericSpriteFont(LoadContent<SpriteFont>("Fonts/Regular"));
            this.UiSystem.Style.TextScale = 0.075F;
            this.UiSystem.Style.TextColor = ColorHelper.FromHexRgb(0xebd5bd);
            this.UiSystem.Style.ButtonTexture = new NinePatch(this.SpriteBatch.GenerateSquareTexture(ColorHelper.FromHexRgb(0x594e6f) * 0.35F), 0);
            this.UiSystem.Style.ActionSound = new SoundEffectInfo(LoadContent<SoundEffect>("Sounds/ButtonPress"));
            this.UiSystem.AutoScaleWithScreen = true;
            this.UiSystem.Controls.HandleKeyboard = false;
            this.UiSystem.Controls.HandleGamepad = false;
            this.UiSystem.Controls.HandleTouch = false;
            this.UiSystem.TextFormatter.AddImage("Flower", Entity.StuffTexture[0, 1]);
            this.UiSystem.TextFormatter.AddImage("Gnome", Entity.StuffTexture[1, 1]);
            this.UiSystem.TextFormatter.AddImage("Snow", Entity.StuffTexture[2, 1]);
            this.InputHandler.HandleKeyboardRepeats = false;

            this.mapLoader = new ContentManager(this.Services, this.Content.RootDirectory);
            var scores = GetScoreFile();
            if (scores.Exists) {
                using var reader = new JsonTextReader(scores.OpenText());
                this.Scores = Serializer.Deserialize<Dictionary<string, int>>(reader);
            }
            this.Camera = new Camera(this.GraphicsDevice) {
                AutoScaleWithScreen = true,
                Scale = 4
            };

            // open the main menu
            var group = new Group(Anchor.TopLeft, Vector2.One, false);
            group.AddChild(new Image(Anchor.TopCenter, new Vector2(0.45F, -0.5F), Entity.StuffTexture[0, 2, 4, 2]) {
                OnUpdated = (e, time) => e.PositionOffset = new Vector2(0, 50 + MathF.Sin((float) time.TotalGameTime.TotalSeconds * 2) * 5)
            });
            group.AddChild(new Paragraph(Anchor.BottomCenter, 1, "Created by <l https://ellpeck.de>Ellpeck</l> for the <l https://itch.io/jam/great-spring-game-jam-2021>Great Spring Game Jam: 2021</l>", true) {
                TextScale = 0.06F
            });
            var buttons = group.AddChild(new Group(Anchor.BottomCenter, new Vector2(500)) {PositionOffset = new Vector2(0, 125)});
            buttons.AddChild(new Button(Anchor.AutoCenter, new Vector2(1, 60), "<i Gnome> Play <i Flower>") {
                OnPressed = e => {
                    if (!this.IsInCutscene)
                        this.StartLevel("Overworld", Color.White);
                }
            });
            buttons.AddChild(new VerticalSpace(10));
            buttons.AddChild(new Button(Anchor.AutoLeft, new Vector2(245, 50), "View on itch"));
            buttons.AddChild(new Button(Anchor.AutoInlineIgnoreOverflow, new Vector2(245, 50), "View source") {
                PositionOffset = new Vector2(10, 0),
                OnPressed = e => Process.Start(new ProcessStartInfo("https://git.ellpeck.de/Ellpeck/GreatSpringGameJam") {UseShellExecute = true})
            });
            this.UiSystem.Add("MainMenu", group);
        }

        protected override void DoUpdate(GameTime gameTime) {
            base.DoUpdate(gameTime);
            if (this.Map != null) {
                this.Map.Update(gameTime);
                if (this.Player != null && !this.IsInCutscene)
                    this.FocusCameraOnPlayer();
            }
        }

        protected override void DoDraw(GameTime gameTime) {
            this.GraphicsDevice.Clear(ColorHelper.FromHexRgb(0x729cd4));
            base.DoDraw(gameTime);
            if (this.Map != null)
                this.Map.Draw(gameTime, this.SpriteBatch, this.Camera);
        }

        public void Finish() {
            IEnumerable<Wait> Impl() {
                // scroll up
                var offset = 0F;
                while (offset < 1) {
                    this.Camera.LookingPosition -= new Vector2(0, offset) * this.Map.TileSize;
                    offset += 0.01F;
                    yield return new Wait(CoroutineEvents.Update);
                }

                // calculate score
                var snowRemoved = this.Map.TotalSnow - this.Map.GetTotalTiles("Snow") / 2;
                var plantsGrown = this.Map.TotalSeeds - this.Map.GetTotalTiles("Seed");
                var gnomesCollected = this.Map.TotalGnomes - this.Map.GetEntities<Gnome>().Count();
                // snow removal is less important, gnomes are more important
                var completion = (snowRemoved / 3F + plantsGrown + gnomesCollected * 10F) / (this.Map.TotalSnow / 3F + this.Map.TotalSeeds + this.Map.TotalGnomes * 10F);

                // display end of level info
                var group = new Group(Anchor.Center, new Vector2(0.5F), false);
                this.UiSystem.Add("EndCard", group);
                group.AddChild(new Paragraph(Anchor.AutoCenter, 1, "Level Completed!", true) {TextScale = 0.125F});
                group.AddChild(new VerticalSpace(15));
                yield return new Wait(1);
                group.AddChild(new Paragraph(Anchor.AutoCenter, 1, $"<i Snow> Snow Removed: {snowRemoved} / {this.Map.TotalSnow}", true));
                yield return new Wait(1);
                group.AddChild(new Paragraph(Anchor.AutoCenter, 1, $"<i Flower> Plants Watered: {plantsGrown} / {this.Map.TotalSeeds}", true));
                yield return new Wait(1);
                group.AddChild(new Paragraph(Anchor.AutoCenter, 1, $"<i Gnome> Gnomes Collected: {gnomesCollected} / {this.Map.TotalGnomes}", true));
                yield return new Wait(1);

                // display score
                var completionCounter = 0;
                group.AddChild(new VerticalSpace(10));
                const string scoreText = "Score: {0}%";
                var score = group.AddChild(new Paragraph(Anchor.AutoCenter, 1, string.Format(scoreText, 0), true) {TextScale = 0.1F});
                while (completionCounter < completion * 100) {
                    completionCounter++;
                    score.Text = string.Format(scoreText, completionCounter);
                    yield return new Wait(0.03F);
                }

                // update highest score data
                if (!this.Scores.TryGetValue(this.Map.Name, out var lastCompletion) || lastCompletion < completionCounter)
                    this.Scores[this.Map.Name] = completionCounter;
                using (var writer = GetScoreFile().CreateText())
                    Serializer.Serialize(writer, this.Scores);

                yield return new Wait(3);

                // fade out and move to overworld
                foreach (var wait in this.FadeAndStartLevel("Overworld", Color.White))
                    yield return wait;
            }

            this.IsInCutscene = true;
            CoroutineHandler.Start(Impl());
        }

        public void StartLevel(string name, Color fadeColor) {
            this.IsInCutscene = true;
            CoroutineHandler.Start(this.FadeAndStartLevel(name, fadeColor));
        }

        private IEnumerable<Wait> FadeAndStartLevel(string mapName, Color fadeColor) {
            var fadeOverlay = new Group(Anchor.TopLeft, Vector2.One, false) {
                OnDrawn = (e, time, batch, a) => batch.FillRectangle(e.DisplayArea.ToExtended(), fadeColor * a),
                DrawAlpha = 0
            };
            this.UiSystem.Add("Fade", fadeOverlay);
            while (fadeOverlay.DrawAlpha < 1) {
                fadeOverlay.DrawAlpha += 0.04F;
                yield return new Wait(CoroutineEvents.Update);
            }
            yield return new Wait(0.25F);

            this.UiSystem.Remove("EndCard");
            this.UiSystem.Remove("MainMenu");

            // load next map
            this.mapLoader.Unload();
            var map = new Map(this.mapLoader.Load<TiledMap>($"Maps/{mapName}"), mapName);
            var spawnPoint = map.SpawnPoint.ToVector2();
            if (mapName == "Overworld" && this.Map != null && this.Map.Name != mapName)
                spawnPoint = (map.GetLevelEntrances().First(e => e.Name == this.Map.Name).Position / map.TileSize).FloorCopy();
            this.Player = new Player(map, spawnPoint);
            this.Map = map;
            this.Map.AddEntity(this.Player);
            this.FocusCameraOnPlayer(1);

            // fade back in
            yield return new Wait(0.25F);
            while (fadeOverlay.DrawAlpha > 0) {
                fadeOverlay.DrawAlpha -= 0.04F;
                yield return new Wait(CoroutineEvents.Update);
            }
            this.UiSystem.Remove(fadeOverlay.Root.Name);

            this.IsInCutscene = false;
        }

        private void FocusCameraOnPlayer(float speed = 0.25F) {
            this.Camera.LookingPosition = Vector2.Lerp(this.Camera.LookingPosition, (this.Player.Position + Vector2.One / 2) * this.Map.TileSize, speed);
            this.Camera.ConstrainWorldBounds(Vector2.Zero, this.Map.SizeInPixels.ToVector2());
        }

        private static FileInfo GetScoreFile() {
            var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            return new FileInfo(Path.Combine(appData, $"{Assembly.GetExecutingAssembly().GetName().Name}.dat"));
        }

    }
}