// Copyright (c) Wojciech Figat. All rights reserved.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using FlaxEditor.GUI.Timeline.Undo;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Json;
using FlaxEngine.Utilities;

namespace FlaxEditor.GUI.Timeline.Tracks
{
    /// <summary>
    /// The timeline track for invoking events on a certain points in the time.
    /// </summary>
    /// <seealso cref="MemberTrack" />
    public class EventTrack : MemberTrack, IKeyframesEditorContext
    {
        /// <summary>
        /// Gets the archetype.
        /// </summary>
        /// <returns>The archetype.</returns>
        public static TrackArchetype GetArchetype()
        {
            return new TrackArchetype
            {
                TypeId = 15,
                Name = "Event",
                DisableSpawnViaGUI = true,
                Create = options => new EventTrack(ref options),
                Load = LoadTrack,
                Save = SaveTrack,
            };
        }

        private static void LoadTrack(int version, Track track, BinaryReader stream)
        {
            var e = (EventTrack)track;

            var paramsCount = stream.ReadInt32();
            e.EventParamsSizes = new int[paramsCount];
            e.EventParamsTypes = new Type[paramsCount];
            int eventsCount = stream.ReadInt32();
            e.MemberName = LoadName(stream);

            bool isInvalid = false;
            for (int i = 0; i < paramsCount; i++)
            {
                e.EventParamsSizes[i] = stream.ReadInt32();
                var paramTypeName = LoadName(stream);
                e.EventParamsTypes[i] = TypeUtils.GetManagedType(paramTypeName);
                if (e.EventParamsTypes[i] == null)
                {
                    Editor.LogError($"Unknown type {paramTypeName}.");
                    isInvalid = true;
                }
            }

            if (isInvalid)
            {
                e.Events.ResetKeyframes();
                stream.ReadBytes(eventsCount * (sizeof(float) + e.EventParamsSizes.Sum()));
                Editor.LogError("Cannot load track " + e.MemberName + " of type " + e.MemberTypeName + ". Failed to find the value type information.");
                return;
            }

            e.OnEventParamsChanged();

            var events = new KeyframesEditor.Keyframe[eventsCount];
            var dataBuffer = paramsCount != 0 ? new byte[e.EventParamsSizes.Max()] : null;
            GCHandle handle = paramsCount != 0 ? GCHandle.Alloc(dataBuffer, GCHandleType.Pinned) : new GCHandle();
            for (int i = 0; i < eventsCount; i++)
            {
                var time = stream.ReadSingle();

                var key = new EventKey
                {
                    Parameters = new object[paramsCount]
                };

                for (int j = 0; j < paramsCount; j++)
                {
                    stream.Read(dataBuffer, 0, e.EventParamsSizes[j]);
                    key.Parameters[j] = Utilities.Utils.ByteArrayToStructure(handle.AddrOfPinnedObject(), e.EventParamsTypes[j], e.EventParamsSizes[j]);
                }

                events[i] = new KeyframesEditor.Keyframe
                {
                    Time = time,
                    Value = key,
                };
            }
            if (handle.IsAllocated)
                handle.Free();

            e.Events.SetKeyframes(events);
        }

        private static void SaveTrack(Track track, BinaryWriter stream)
        {
            var e = (EventTrack)track;

            var events = e.Events.Keyframes;
            var paramsCount = e.EventParamsTypes.Length;

            stream.Write(paramsCount);
            stream.Write(events.Count);
            SaveName(stream, e.MemberName);

            for (int i = 0; i < e.EventParamsTypes.Length; i++)
            {
                stream.Write(e.EventParamsSizes[i]);
                SaveName(stream, e.EventParamsTypes[i]?.FullName);
            }

            var dataBuffer = paramsCount != 0 ? new byte[e.EventParamsSizes.Max()] : null;
            IntPtr ptr = paramsCount != 0 ? Marshal.AllocHGlobal(dataBuffer.Length) : IntPtr.Zero;
            for (int i = 0; i < events.Count; i++)
            {
                var keyframe = events[i];
                var key = (EventKey)keyframe.Value;

                stream.Write(keyframe.Time);

                for (int j = 0; j < paramsCount; j++)
                {
                    Utilities.Utils.StructureToByteArray(key.Parameters[j], e.EventParamsSizes[j], ptr, dataBuffer);
                    stream.Write(dataBuffer, 0, e.EventParamsSizes[j]);
                }
            }
            Marshal.FreeHGlobal(ptr);
        }

        private byte[] _eventsEditingStartData;

        /// <summary>
        /// The event keyframes editor.
        /// </summary>
        public KeyframesEditor Events;

        /// <summary>
        /// The event parameters data sizes collection.
        /// </summary>
        public int[] EventParamsSizes = Utils.GetEmptyArray<int>();

        /// <summary>
        /// The event parameters types collection.
        /// </summary>
        public Type[] EventParamsTypes = Utils.GetEmptyArray<Type>();

        /// <summary>
        /// The event key data.
        /// </summary>
        public struct EventKey : ICloneable
        {
            /// <summary>
            /// The parameters values.
            /// </summary>
            [EditorDisplay("Parameters", EditorDisplayAttribute.InlineStyle), ExpandGroups]
            [Collection(CanReorderItems = false, CanResize = true)]
            public object[] Parameters;

            /// <inheritdoc />
            public override string ToString()
            {
                if (Parameters == null || Parameters.Length == 0)
                    return "()";
                var sb = new StringBuilder();
                sb.Append('(');
                for (int i = 0; i < Parameters.Length; i++)
                {
                    if (i != 0)
                        sb.Append(", ");
                    sb.Append(Parameters[i] ?? "<null>");
                }
                sb.Append(')');
                return sb.ToString();
            }

            /// <inheritdoc />
            public object Clone()
            {
                if (Parameters == null)
                    return new EventKey();

                // Deep clone parameter values (especially boxed value types need to be duplicated to avoid referencing the same ones)
                var parameters = new object[Parameters.Length];
                for (int i = 0; i < parameters.Length; i++)
                {
                    var p = Parameters[i];
                    if (p == null || p is FlaxEngine.Object)
                        parameters[i] = Parameters[i];
                    else
                        parameters[i] = JsonSerializer.Deserialize(JsonSerializer.Serialize(p), p.GetType());
                }

                return new EventKey { Parameters = parameters };
            }
        }

        /// <inheritdoc />
        public EventTrack(ref TrackCreateOptions options)
        : base(ref options, true, false)
        {
            Height = 20.0f;

            // Events editor
            Events = new KeyframesEditor
            {
                EnableZoom = false,
                EnablePanning = false,
                ScrollBars = ScrollBars.None,
            };
            Events.Edited += OnEventsEdited;
            Events.EditingStart += OnEventsEditingStart;
            Events.EditingEnd += OnEventsEditingEnd;
            Events.UnlockChildrenRecursive();

            _addKey.Clicked += OnAddKeyClicked;
            _leftKey.Clicked += OnLeftKeyClicked;
            _rightKey.Clicked += OnRightKeyClicked;
        }

        private void OnRightKeyClicked(Image image, MouseButton button)
        {
            if (button == MouseButton.Left && GetNextKeyframeFrame(Timeline.CurrentTime, out var frame))
            {
                Timeline.OnSeek(frame);
            }
        }

        /// <inheritdoc />
        public override bool GetNextKeyframeFrame(float time, out int result)
        {
            for (int i = 0; i < Events.Keyframes.Count; i++)
            {
                var k = Events.Keyframes[i];
                if (k.Time > time)
                {
                    result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond);
                    return true;
                }
            }
            return base.GetNextKeyframeFrame(time, out result);
        }

        private void OnAddKeyClicked(Image image, MouseButton button)
        {
            if (button == MouseButton.Left)
            {
                // Evaluate a value
                var time = Timeline.CurrentTime;
                if (!TryGetValue(out var value))
                    value = Events.Evaluate(time);
                value = ((ICloneable)value).Clone();

                // Find event at the current location
                for (int i = Events.Keyframes.Count - 1; i >= 0; i--)
                {
                    var k = Events.Keyframes[i];
                    var frame = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond);
                    if (frame == Timeline.CurrentFrame)
                    {
                        // Skip if value is the same
                        if (k.Value == value)
                            return;

                        // Update existing event value
                        Events.SetKeyframe(i, value);
                        return;
                    }
                }

                // Add a new key
                using (new TrackUndoBlock(this))
                    Events.AddKeyframe(new KeyframesEditor.Keyframe(time, value));
            }
        }

        private void OnLeftKeyClicked(Image image, MouseButton button)
        {
            if (button == MouseButton.Left && GetPreviousKeyframeFrame(Timeline.CurrentTime, out var frame))
            {
                Timeline.OnSeek(frame);
            }
        }

        /// <inheritdoc />
        public override bool GetPreviousKeyframeFrame(float time, out int result)
        {
            for (int i = Events.Keyframes.Count - 1; i >= 0; i--)
            {
                var k = Events.Keyframes[i];
                if (k.Time < time)
                {
                    result = Mathf.FloorToInt(k.Time * Timeline.FramesPerSecond);
                    return true;
                }
            }
            return base.GetPreviousKeyframeFrame(time, out result);
        }

        private void UpdateEvents()
        {
            if (Events == null || Timeline == null)
                return;
            bool wasVisible = Events.Visible;
            Events.Visible = Visible;
            if (!Visible)
            {
                if (wasVisible)
                    Events.ClearSelection();
                return;
            }
            Events.KeyframesEditorContext = Timeline;
            Events.CustomViewPanning = Timeline.OnKeyframesViewPanning;
            Events.Bounds = new Rectangle(Timeline.StartOffset, Y + 1.0f, Timeline.Duration * Timeline.UnitsPerSecond * Timeline.Zoom, Height - 2.0f);
            Events.ViewScale = new Float2(Timeline.Zoom, 1.0f);
            Events.Visible = Visible;
            Events.UpdateKeyframes();
        }

        private void OnEventsEdited()
        {
            Timeline.MarkAsEdited();
        }

        private void OnEventsEditingStart()
        {
            _eventsEditingStartData = EditTrackAction.CaptureData(this);
        }

        private void OnEventsEditingEnd()
        {
            var after = EditTrackAction.CaptureData(this);
            if (!Utils.ArraysEqual(_eventsEditingStartData, after))
                Timeline.AddBatchedUndoAction(new EditTrackAction(Timeline, this, _eventsEditingStartData, after));
            _eventsEditingStartData = null;
        }

        private void OnEventParamsChanged()
        {
            // Calculate data size
            EventParamsSizes = new int[EventParamsTypes.Length];
            for (int i = 0; i < EventParamsTypes.Length; i++)
            {
                var type = EventParamsTypes[i];
                EventParamsSizes[i] = Marshal.SizeOf(type.IsEnum ? Enum.GetUnderlyingType(type) : type);
            }

            // Build default value
            var defaultValue = new EventKey
            {
                Parameters = new object[EventParamsTypes.Length]
            };

            for (int i = 0; i < EventParamsTypes.Length; i++)
                defaultValue.Parameters[i] = Activator.CreateInstance(EventParamsTypes[i]);
            Events.DefaultValue = defaultValue;
        }

        /// <inheritdoc />
        protected override MemberTypes MemberTypes => MemberTypes.Method;

        /// <inheritdoc />
        protected override void OnMemberChanged(MemberInfo value, Type type)
        {
            base.OnMemberChanged(value, type);

            Events.ResetKeyframes();

            if (value is MethodInfo m)
            {
                EventParamsTypes = m.GetParameters().Select(x => x.ParameterType).ToArray();
            }
            else
            {
                EventParamsTypes = Utils.GetEmptyArray<Type>();
            }
            OnEventParamsChanged();
        }

        /// <inheritdoc />
        protected override void OnVisibleChanged()
        {
            base.OnVisibleChanged();

            Events.Visible = Visible;
        }

        /// <inheritdoc />
        public override void OnTimelineChanged(Timeline timeline)
        {
            base.OnTimelineChanged(timeline);

            Events.Parent = timeline?.MediaPanel;
            Events.FPS = timeline?.FramesPerSecond;

            UpdateEvents();
        }

        /// <inheritdoc />
        public override void OnTimelineZoomChanged()
        {
            base.OnTimelineZoomChanged();

            UpdateEvents();
        }

        /// <inheritdoc />
        public override void OnTimelineArrange()
        {
            base.OnTimelineArrange();

            UpdateEvents();
        }

        /// <inheritdoc />
        public override void OnTimelineFpsChanged(float before, float after)
        {
            base.OnTimelineFpsChanged(before, after);

            Events.FPS = after;
        }

        /// <inheritdoc />
        public override void OnDestroy()
        {
            if (Events != null)
            {
                Events.Dispose();
                Events = null;
            }

            base.OnDestroy();
        }

        /// <inheritdoc />
        public new void OnKeyframesDeselect(IKeyframesEditor editor)
        {
            if (Events != null && Events.Visible)
                Events.OnKeyframesDeselect(editor);
        }

        /// <inheritdoc />
        public new void OnKeyframesSelection(IKeyframesEditor editor, ContainerControl control, Rectangle selection)
        {
            if (Events != null && Events.Visible)
                Events.OnKeyframesSelection(editor, control, selection);
        }

        /// <inheritdoc />
        public new int OnKeyframesSelectionCount()
        {
            return Events != null && Events.Visible ? Events.OnKeyframesSelectionCount() : 0;
        }

        /// <inheritdoc />
        public new void OnKeyframesDelete(IKeyframesEditor editor)
        {
            if (Events != null && Events.Visible)
                Events.OnKeyframesDelete(editor);
        }

        /// <inheritdoc />
        public new void OnKeyframesMove(IKeyframesEditor editor, ContainerControl control, Float2 location, bool start, bool end)
        {
            if (Events != null && Events.Visible)
                Events.OnKeyframesMove(editor, control, location, start, end);
        }

        /// <inheritdoc />
        public new void OnKeyframesCopy(IKeyframesEditor editor, float? timeOffset, StringBuilder data)
        {
            if (Events != null && Events.Visible)
                Events.OnKeyframesCopy(editor, timeOffset, data);
        }

        /// <inheritdoc />
        public new void OnKeyframesPaste(IKeyframesEditor editor, float? timeOffset, string[] datas, ref int index)
        {
            if (Events != null && Events.Visible)
                Events.OnKeyframesPaste(editor, timeOffset, datas, ref index);
        }

        /// <inheritdoc />
        public new void OnKeyframesGet(Action<string, float, object> get)
        {
            Events?.OnKeyframesGet(Name, get);
        }

        /// <inheritdoc />
        public new void OnKeyframesSet(List<KeyValuePair<float, object>> keyframes)
        {
            Events?.OnKeyframesSet(keyframes);
        }
    }
}
