// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Screens.Edit;
using osu.Game.Skinning;

namespace osu.Game.Overlays.SkinEditor
{
    public partial class SkinEditorChangeHandler : EditorChangeHandler
    {
        private readonly ISerialisableDrawableContainer? firstTarget;

        // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
        private readonly BindableList<ISerialisableDrawable>? components;

        public SkinEditorChangeHandler(Drawable targetScreen)
        {
            // To keep things simple, we are currently only handling the current target screen for undo / redo.
            // In the future we'll want this to cover all changes, even to skin's `InstantiationInfo`.
            // We'll also need to consider cases where multiple targets are on screen at the same time.

            firstTarget = targetScreen.ChildrenOfType<ISerialisableDrawableContainer>().FirstOrDefault();

            if (firstTarget == null)
                return;

            components = new BindableList<ISerialisableDrawable> { BindTarget = firstTarget.Components };
            components.BindCollectionChanged((_, _) => SaveState(), true);
        }

        protected override void WriteCurrentStateToStream(MemoryStream stream)
        {
            if (firstTarget == null)
                return;

            var skinnableInfos = firstTarget.CreateSerialisedInfo().ToArray();
            string json = JsonConvert.SerializeObject(skinnableInfos, new JsonSerializerSettings { Formatting = Formatting.Indented });
            stream.Write(Encoding.UTF8.GetBytes(json));
        }

        protected override void ApplyStateChange(byte[] previousState, byte[] newState)
        {
            if (firstTarget == null)
                return;

            var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SerialisedDrawableInfo>>(Encoding.UTF8.GetString(newState));

            if (deserializedContent == null)
                return;

            SerialisedDrawableInfo[] skinnableInfos = deserializedContent.ToArray();
            ISerialisableDrawable[] targetComponents = firstTarget.Components.ToArray();

            // Store components based on type for later reuse
            var componentsPerTypeLookup = new Dictionary<Type, Queue<Drawable>>();

            foreach (ISerialisableDrawable component in targetComponents)
            {
                Type lookup = component.GetType();

                if (!componentsPerTypeLookup.TryGetValue(lookup, out Queue<Drawable>? componentsOfSameType))
                    componentsPerTypeLookup.Add(lookup, componentsOfSameType = new Queue<Drawable>());

                componentsOfSameType.Enqueue((Drawable)component);
            }

            for (int i = targetComponents.Length - 1; i >= 0; i--)
                firstTarget.Remove(targetComponents[i], false);

            foreach (var skinnableInfo in skinnableInfos)
            {
                Type lookup = skinnableInfo.Type;

                if (!componentsPerTypeLookup.TryGetValue(lookup, out Queue<Drawable>? componentsOfSameType))
                {
                    firstTarget.Add((ISerialisableDrawable)skinnableInfo.CreateInstance());
                    continue;
                }

                // Wherever possible, attempt to reuse existing component instances.
                if (componentsOfSameType.TryDequeue(out Drawable? component))
                {
                    component.ApplySerialisedInfo(skinnableInfo);
                }
                else
                {
                    component = skinnableInfo.CreateInstance();
                }

                firstTarget.Add((ISerialisableDrawable)component);
            }

            // Dispose components which were not reused.
            foreach ((Type _, Queue<Drawable> typeComponents) in componentsPerTypeLookup)
            {
                foreach (var component in typeComponents)
                    component.Dispose();
            }
        }
    }
}
