// 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.Text;
using DiffPlex;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
using osu.Game.IO;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;

namespace osu.Game.Screens.Edit
{
    /// <summary>
    /// Patches an <see cref="EditorBeatmap"/> based on the difference between two legacy (.osu) states.
    /// </summary>
    public class LegacyEditorBeatmapPatcher
    {
        private readonly EditorBeatmap editorBeatmap;

        public LegacyEditorBeatmapPatcher(EditorBeatmap editorBeatmap)
        {
            this.editorBeatmap = editorBeatmap;
        }

        public void Patch(byte[] currentState, byte[] newState)
        {
            // Diff the beatmaps
            var result = new Differ().CreateLineDiffs(readString(currentState), readString(newState), true, false);

            // Find the index of [HitObject] sections. Lines changed prior to this index are ignored.
            int oldHitObjectsIndex = Array.IndexOf(result.PiecesOld, "[HitObjects]");
            int newHitObjectsIndex = Array.IndexOf(result.PiecesNew, "[HitObjects]");

            var toRemove = new List<int>();
            var toAdd = new List<int>();

            foreach (var block in result.DiffBlocks)
            {
                // Removed hitobjects
                for (int i = 0; i < block.DeleteCountA; i++)
                {
                    int hoIndex = block.DeleteStartA + i - oldHitObjectsIndex - 1;

                    if (hoIndex < 0)
                        continue;

                    toRemove.Add(hoIndex);
                }

                // Added hitobjects
                for (int i = 0; i < block.InsertCountB; i++)
                {
                    int hoIndex = block.InsertStartB + i - newHitObjectsIndex - 1;

                    if (hoIndex < 0)
                        continue;

                    toAdd.Add(hoIndex);
                }
            }

            // Sort the indices to ensure that removal + insertion indices don't get jumbled up post-removal or post-insertion.
            // This isn't strictly required, but the differ makes no guarantees about order.
            toRemove.Sort();
            toAdd.Sort();

            // Apply the changes.
            for (int i = toRemove.Count - 1; i >= 0; i--)
                editorBeatmap.RemoveAt(toRemove[i]);

            if (toAdd.Count > 0)
            {
                IBeatmap newBeatmap = readBeatmap(newState);
                foreach (var i in toAdd)
                    editorBeatmap.Insert(i, newBeatmap.HitObjects[i]);
            }
        }

        private string readString(byte[] state) => Encoding.UTF8.GetString(state);

        private IBeatmap readBeatmap(byte[] state)
        {
            using (var stream = new MemoryStream(state))
            using (var reader = new LineBufferedReader(stream, true))
            {
                var decoded = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
                decoded.BeatmapInfo.Ruleset = editorBeatmap.BeatmapInfo.Ruleset;
                return new PassThroughWorkingBeatmap(decoded).GetPlayableBeatmap(editorBeatmap.BeatmapInfo.Ruleset);
            }
        }

        private class PassThroughWorkingBeatmap : WorkingBeatmap
        {
            private readonly IBeatmap beatmap;

            public PassThroughWorkingBeatmap(IBeatmap beatmap)
                : base(beatmap.BeatmapInfo, null)
            {
                this.beatmap = beatmap;
            }

            protected override IBeatmap GetBeatmap() => beatmap;

            protected override Texture GetBackground() => throw new NotImplementedException();

            protected override Track GetTrack() => throw new NotImplementedException();
        }
    }
}