using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace BallanceTasEditor.Backend {

    /// <summary>
    /// TAS操作接口。所有TAS操作均需要支持此接口。
    /// </summary>
    public interface ITasOperation {
        /// <summary>
        /// 执行对应的TAS操作。
        /// </summary>
        /// <param name="seq">所要操作的TAS存储容器。</param>
        void Execute(ITasSequence seq);
        /// <summary>
        /// 检查该操作是否已经被执行过。
        /// </summary>
        /// <remarks>
        /// 所有Tas操作类创建后只能执行一次或者不执行，
        /// 因此有此函数用于获取是否已经执行过。
        /// </remarks>
        /// <returns>如果已经执行过，返回true，否则返回false。</returns>
        bool IsExecuted();
    }

    /// <summary>
    /// 可撤销的TAS操作接口，所有可撤销的TAS操作均需支持此接口。
    /// </summary>
    public interface ITasRevocableOperation : ITasOperation {
        /// <summary>
        /// 撤销对应TAS操作。
        /// </summary>
        /// <param name="seq">所要撤销操作的TAS存储容器。</param>
        void Revoke(ITasSequence seq);
        /// <summary>
        /// 返回该TAS操作占用的内存大小。
        /// </summary>
        /// <remarks>
        /// 可撤销的TAS操作会在内存中存储一定数据，用于撤销对应操作。
        /// 该函数返回的占用用于衡量该操作的开销。
        /// 我们应当基于大小，而非写死的个数决定撤销栈中的最大操作次数，
        /// 例如对于小型操作我们可以存储100个，对于大型操作则只能存储5个等。
        /// 用于解决编辑者目前认为撤销栈大小不足的情况。
        /// <para/>
        /// 该函数返回的大小可以不是特别精确，但要准确反映空间复杂度。
        /// </remarks>
        /// <returns>占用的内存大小（以byte为单位）。</returns>
        int Occupation();
    }

    internal static class OperationUtils {
        internal const int SIZEOF_DELTA_TIME = sizeof(float);
        internal const int SIZEOF_KEYS = sizeof(uint);
        internal const int SIZEOF_FRAME = SIZEOF_DELTA_TIME + SIZEOF_KEYS;

        internal static readonly InvalidOperationException ExecutionEnvironment = new InvalidOperationException("Can not execute one TAS operation multiple times.");
        internal static readonly InvalidOperationException RevokeEnvironment = new InvalidOperationException("Can not revoke an not executed TAS operation.");
    }

    public enum CellKeysOperationKind {
        Set, Unset, Flip
    }

    public class CellKeysOperation : ITasRevocableOperation {
        public static CellKeysOperation FromSingleCell(CellKeysOperationKind kind, int index, TasKey key) {
            return new CellKeysOperation(kind, index, index, key, key);
        }

        public static CellKeysOperation FromCellRange(CellKeysOperationKind kind, int startIndex, int endIndex, TasKey startKey, TasKey endKey) {
            return new CellKeysOperation(kind, startIndex, endIndex, startKey, endKey);
        }

        private CellKeysOperation(CellKeysOperationKind kind, int startIndex, int endIndex, TasKey startKey, TasKey endKey) {
            // Check arguments.
            ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
            ArgumentOutOfRangeException.ThrowIfGreaterThan(startKey.ToIndex(), endKey.ToIndex());
            // Setup members.
            m_Kind = kind;
            m_StartIndex = startIndex;
            m_EndIndex = endIndex;
            m_StartKey = startKey;
            m_EndKey = endKey;
            m_FramesBackup = null;
        }

        private CellKeysOperationKind m_Kind;
        private int m_StartIndex, m_EndIndex;
        private TasKey m_StartKey, m_EndKey;
        private RawTasFrame[]? m_FramesBackup;

        [MemberNotNullWhen(true, nameof(m_FramesBackup))]
        public bool IsExecuted() {
            return m_FramesBackup is not null;
        }

        public void Execute(ITasSequence seq) {
            if (IsExecuted()) {
                throw OperationUtils.ExecutionEnvironment;
            }

            // Check index range.
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_EndIndex, seq.GetCount());
            ArgumentOutOfRangeException.ThrowIfLessThan(m_StartIndex, 0);

            // Do backup and set values at the same time
            int backupIndex = 0;
            var backups = new RawTasFrame[m_EndIndex - m_StartIndex + 1];
            // Pre-build key list for fast fetching.
            var keys = Enumerable.Range(m_StartKey.ToIndex(), m_EndKey.ToIndex() - m_StartKey.ToIndex() + 1).Select((i) => TasKey.FromIndex(i)).ToArray();
            foreach (var frame in seq.BatchlyVisit(m_StartIndex, m_EndIndex)) {
                // Do backup
                frame.ToRawImplace(ref backups[backupIndex++]);
                // Modify keys
                foreach (var key in keys) {
                    switch (m_Kind) {
                        case CellKeysOperationKind.Set:
                            frame.SetKeyPressed(key, true);
                            break;
                        case CellKeysOperationKind.Unset:
                            frame.SetKeyPressed(key, false);
                            break;
                        case CellKeysOperationKind.Flip:
                            frame.FlipKeyPressed(key);
                            break;
                    }
                }
            }

            // Assign backups
            m_FramesBackup = backups;
        }

        public void Revoke(ITasSequence seq) {
            if (!IsExecuted()) {
                throw OperationUtils.RevokeEnvironment;
            }

            // Index range is checked,
            // so we directly restore backup.
            int backupIndex = 0;
            foreach (var frame in seq.BatchlyVisit(m_StartIndex, m_EndIndex)) {
                frame.FromRawImplace(m_FramesBackup[backupIndex++]);
            }

            // Clear backups
            m_FramesBackup = null;
        }

        public int Occupation() {
            return (m_EndIndex - m_StartIndex) * OperationUtils.SIZEOF_FRAME;
        }

    }

    public class FrameFpsOperation : ITasRevocableOperation {
        public static FrameFpsOperation FromSingleFrame(int index, uint fps) {
            return new FrameFpsOperation(index, index, fps);
        }

        public static FrameFpsOperation FromFrameRange(int startIndex, int endIndex, uint fps) {
            return new FrameFpsOperation(startIndex, endIndex, fps);
        }

        private FrameFpsOperation(int startIndex, int endIndex, uint fps) {
            // Check arguments
            if (!FpsConverter.IsValidFps(fps)) {
                throw new ArgumentOutOfRangeException(nameof(fps));
            }
            ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
            // Assign arguments
            m_StartIndex = startIndex;
            m_EndIndex = endIndex;
            m_DeltaTime = FpsConverter.ToDelta(fps);
        }

        private int m_StartIndex, m_EndIndex;
        private float m_DeltaTime;
        private float[]? m_DeltaTimesBackup;

        [MemberNotNullWhen(true, nameof(m_DeltaTimesBackup))]
        public bool IsExecuted() {
            return m_DeltaTimesBackup is not null;
        }

        public void Execute(ITasSequence seq) {
            if (IsExecuted()) {
                throw OperationUtils.ExecutionEnvironment;
            }

            // Check index range
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_EndIndex, seq.GetCount());
            ArgumentOutOfRangeException.ThrowIfLessThan(m_StartIndex, 0);

            // Do backup and set values at the same time
            int backupIndex = 0;
            var backups = new float[m_EndIndex - m_StartIndex + 1];
            foreach (var frame in seq.BatchlyVisit(m_StartIndex, m_EndIndex)) {
                // Do backup
                backups[backupIndex++] = frame.GetTimeDelta();
                // Modify delta time
                frame.SetTimeDelta(m_DeltaTime);
            }

            // Assign backups
            m_DeltaTimesBackup = backups;
        }

        public void Revoke(ITasSequence seq) {
            if (!IsExecuted()) {
                throw OperationUtils.RevokeEnvironment;
            }

            // Index range is checked,
            // so we directly restore backup.
            int backupIndex = 0;
            foreach (var frame in seq.BatchlyVisit(m_StartIndex, m_EndIndex)) {
                frame.SetTimeDelta(m_DeltaTimesBackup[backupIndex++]);
            }

            // Clear backups
            m_DeltaTimesBackup = null;
        }

        public int Occupation() {
            return (m_EndIndex - m_StartIndex) * OperationUtils.SIZEOF_DELTA_TIME;
        }

    }

    public class RemoveFrameOperation : ITasRevocableOperation {
        public RemoveFrameOperation(int startIndex, int endIndex) {
            // Check arguments
            ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
            // Assign arguments
            m_StartIndex = startIndex;
            m_EndIndex = endIndex;
        }

        private int m_StartIndex, m_EndIndex;
        private RawTasFrame[]? m_FramesBackup;

        [MemberNotNullWhen(true, nameof(m_FramesBackup))]
        public bool IsExecuted() {
            return m_FramesBackup is not null;
        }

        public void Execute(ITasSequence seq) {
            if (IsExecuted()) {
                throw OperationUtils.ExecutionEnvironment;
            }

            // Check index range
            ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_EndIndex, seq.GetCount());
            ArgumentOutOfRangeException.ThrowIfLessThan(m_StartIndex, 0);

            // Do backups
            int backupIndex = 0;
            var backups = new RawTasFrame[m_EndIndex - m_StartIndex + 1];
            foreach (var frame in seq.BatchlyVisit(m_StartIndex, m_EndIndex)) {
                frame.ToRawImplace(ref backups[backupIndex++]);
            }
            // Do remove
            seq.Remove(m_StartIndex, m_EndIndex);

            // Assign backups
            m_FramesBackup = backups;
        }

        public void Revoke(ITasSequence seq) {
            if (!IsExecuted()) {
                throw OperationUtils.RevokeEnvironment;
            }

            // Index range is checked,
            // so we directly restore backup.
            // Build iterator first
            var iter = m_FramesBackup.Select((frame) => TasFrame.FromRaw(frame));
            var exactSizedIter = new ExactSizeEnumerableAdapter<TasFrame>(iter, m_EndIndex - m_StartIndex + 1);
            // Insert at start index
            seq.Insert(m_StartIndex, exactSizedIter);

            // Clear backups
            m_FramesBackup = null;
        }

        public int Occupation() {
            return (m_EndIndex - m_StartIndex) * OperationUtils.SIZEOF_FRAME;
        }
    }

    public enum AddFrameOperationKind {
        Before, After
    }

    public class AddFrameOperation : ITasRevocableOperation {
        public AddFrameOperation(AddFrameOperationKind kind, int index, uint fps, int count) {
            // Check argument
            if (!FpsConverter.IsValidFps(fps)) {
                throw new ArgumentOutOfRangeException(nameof(fps));
            }
            ArgumentOutOfRangeException.ThrowIfNegative(count);
            // Assign argument
            m_Kind = kind;
            m_Index = index;
            m_Fps = fps;
            m_Count = count;
            m_IsExecuted = false;
        }

        private AddFrameOperationKind m_Kind;
        private int m_Index;
        private uint m_Fps;
        private int m_Count;
        private bool m_IsExecuted;

        public bool IsExecuted() {
            return m_IsExecuted;
        }

        public void Execute(ITasSequence seq) {
            if (IsExecuted()) {
                throw OperationUtils.ExecutionEnvironment;
            }

            // Check arguments
            // If we add before some frame, the valid index can be [0, count],
            // however, if we add after some frame, the valid index is [0, count),
            switch (m_Kind) {
                case AddFrameOperationKind.Before:
                    ArgumentOutOfRangeException.ThrowIfGreaterThan(m_Index, seq.GetCount());
                    break;
                case AddFrameOperationKind.After:
                    ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_Index, seq.GetCount());
                    break;
                default:
                    throw new UnreachableException("Unknown AddFrameOperationKind");
            }

            // Skip if count is zero.
            if (m_Count != 0) {
                // Prepare data builder.
                var iter = Enumerable.Range(0, m_Count).Select((_) => TasFrame.FromFps(m_Fps));
                var exactSizedIter = new ExactSizeEnumerableAdapter<TasFrame>(iter, m_Count);
                // Compute the insert index
                var index = m_Kind switch {
                    AddFrameOperationKind.Before => m_Index,
                    AddFrameOperationKind.After => m_Index + 1,
                    _ => throw new UnreachableException("Unknown AddFrameOperationKind"),
                };
                seq.Insert(index, exactSizedIter);
            }

            // Set status
            m_IsExecuted = true;
        }

        public void Revoke(ITasSequence seq) {
            if (!IsExecuted()) {
                throw OperationUtils.RevokeEnvironment;
            }

            // Arguments were checked so we directly resotre them.
            // If we inserted count is not zero, remove inserted frames, otherwise do nothing.
            if (m_Count != 0) {
                // Compute the index for removing
                var index = m_Kind switch {
                    AddFrameOperationKind.Before => m_Index,
                    AddFrameOperationKind.After => m_Index + 1,
                    _ => throw new UnreachableException("Unknown AddFrameOperationKind"),
                };
                // Execute removing.
                seq.Remove(index, index + m_Count - 1);
            }

            // Modify execution status
            m_IsExecuted = false;
        }

        public int Occupation() {
            return OperationUtils.SIZEOF_FRAME;
        }
    }

    public enum InsertFrameOperationKind {
        Before, After
    }

    public class InsertFrameOperation : ITasRevocableOperation {
        public InsertFrameOperation(InsertFrameOperationKind kind, int index, IExactSizeEnumerable<TasFrame> frames) {
            m_Kind = kind;
            m_Index = index;
            m_InsertedFrames = frames.Select((frame) => frame.ToRaw()).ToArray();
            m_IsExecuted = false;
        }

        private InsertFrameOperationKind m_Kind;
        private int m_Index;
        private RawTasFrame[] m_InsertedFrames;
        private bool m_IsExecuted;

        public bool IsExecuted() {
            return m_IsExecuted;
        }

        public void Execute(ITasSequence seq) {
            if (IsExecuted()) {
                throw OperationUtils.ExecutionEnvironment;
            }

            // Check arguments
            // If we insert before some frame, the valid index can be [0, count],
            // however, if we insert after some frame, the valid index is [0, count),
            switch (m_Kind) {
                case InsertFrameOperationKind.Before:
                    ArgumentOutOfRangeException.ThrowIfGreaterThan(m_Index, seq.GetCount());
                    break;
                case InsertFrameOperationKind.After:
                    ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(m_Index, seq.GetCount());
                    break;
                default:
                    throw new UnreachableException("Unknown InsertFrameOperationKind");
            }

            // Skip if count is zero
            var count = m_InsertedFrames.Length;
            if (count != 0) {
                // Prepare iterator
                var iter = m_InsertedFrames.Select((frame) => TasFrame.FromRaw(frame));
                var exactSizedIter = new ExactSizeEnumerableAdapter<TasFrame>(iter, count);
                // Compute the insert index
                var index = m_Kind switch {
                    InsertFrameOperationKind.Before => m_Index,
                    InsertFrameOperationKind.After => m_Index + 1,
                    _ => throw new UnreachableException("Unknown InsertFrameOperationKind"),
                };
                // Execute inserting.
                seq.Insert(index, exactSizedIter);
            }

            // Set execution status
            m_IsExecuted = true;
        }

        public void Revoke(ITasSequence seq) {
            if (!IsExecuted()) {
                throw OperationUtils.RevokeEnvironment;
            }

            // Arguments were checked so we directly restore them.
            var count = m_InsertedFrames.Length;
            if (count != 0) {
                // Compute the index for removing
                var index = m_Kind switch {
                    InsertFrameOperationKind.Before => m_Index,
                    InsertFrameOperationKind.After => m_Index + 1,
                    _ => throw new UnreachableException("Unknown InsertFrameOperationKind"),
                };
                // Execute removing.
                seq.Remove(index, index + count - 1);
            }

            // Modify execution status
            m_IsExecuted = false;
        }

        public int Occupation() {
            return m_InsertedFrames.Length * OperationUtils.SIZEOF_FRAME;
        }
    }

    public class ClearKeysOperation : ITasOperation {
        public ClearKeysOperation() {
            m_IsExecuted = false;
        }

        private bool m_IsExecuted;

        public void Execute(ITasSequence seq) {
            // Check execution status first.
            if (IsExecuted()) {
                throw OperationUtils.ExecutionEnvironment;
            }
            // Execute operation
            foreach (var frame in seq) {
                frame.ClearKeyPressed();
            }
            m_IsExecuted = true;
        }

        public bool IsExecuted() {
            return m_IsExecuted;
        }
    }

    public class UniformFpsOperation : ITasOperation {
        public UniformFpsOperation(uint fps) {
            // Check arguments
            if (!FpsConverter.IsValidFps(fps)) {
                throw new ArgumentOutOfRangeException(nameof(fps));
            }
            // Assign arguments
            m_DeltaTime = FpsConverter.ToDelta(fps);
            m_IsExecuted = false;
        }

        private float m_DeltaTime;
        private bool m_IsExecuted;

        public void Execute(ITasSequence seq) {
            // Check execution status first.
            if (IsExecuted()) {
                throw OperationUtils.ExecutionEnvironment;
            }
            // Execute operation
            foreach (var frame in seq) {
                frame.SetTimeDelta(m_DeltaTime);
            }
            m_IsExecuted = true;
        }

        public bool IsExecuted() {
            return m_IsExecuted;
        }
    }

}
