using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Timeline;

namespace UnityEditor.Timeline
{
    [Flags]
    enum InputEvent
    {
        None = 0,
        DragEnter = 1,
        DragExit = 2,
        Drag = 4,
        KeyboardInput = 8
    }

    static class InputEventMethods
    {
        public static bool InputHasBegun(this InputEvent evt)
        {
            return evt == InputEvent.DragEnter || evt == InputEvent.KeyboardInput;
        }
    }

    static class TimelineInspectorUtility
    {
        internal static class Styles
        {
            public static readonly GUIContent SecondsPrefix = L10n.TextContent("s", "Seconds");
            public static readonly GUIContent FramesPrefix = L10n.TextContent("f", "Frames");
        }

        public static void TimeField(SerializedProperty property, GUIContent label, bool readOnly, double frameRate, double minValue, double maxValue, ref InputEvent inputEvent)
        {
            var rect = EditorGUILayout.GetControlRect();
            TimeField(rect, property, label, readOnly, frameRate, minValue, maxValue, ref inputEvent);
        }

        // Display Time related properties in frames and seconds
        public static void TimeField(Rect rect, SerializedProperty property, GUIContent label, bool readOnly, double frameRate, double minValue, double maxValue, ref InputEvent inputEvent)
        {
            using (var propertyScope = new PropertyScope(rect, label, property))
            {
                GUIContent title = propertyScope.content;
                rect = EditorGUI.PrefixLabel(rect, title);

                using (new IndentLevelScope(0))
                using (new LabelWidthScope((int)EditorGUI.kMiniLabelW))
                using (new GUIMixedValueScope(property.hasMultipleDifferentValues))
                {
                    var secondsRect = new Rect(rect.xMin, rect.yMin, rect.width / 2 - EditorGUI.kSpacingSubLabel, rect.height);
                    var framesRect = new Rect(rect.xMin + rect.width / 2, rect.yMin, rect.width / 2, rect.height);

                    if (readOnly)
                    {
                        EditorGUI.FloatField(secondsRect, Styles.SecondsPrefix, (float)property.doubleValue, EditorStyles.label);
                    }
                    else
                    {
                        EditorGUI.BeginChangeCheck();
                        DelayedAndDraggableDoubleField(secondsRect, Styles.SecondsPrefix, property, ref inputEvent);
                        if (EditorGUI.EndChangeCheck())
                        {
                            property.doubleValue = Clamp(property.doubleValue, minValue, maxValue);
                        }
                    }

                    if (frameRate > TimeUtility.kTimeEpsilon)
                    {
                        EditorGUI.BeginChangeCheck();

                        double time = property.doubleValue;
                        int frames = TimeUtility.ToFrames(time, frameRate);
                        double exactFrames = TimeUtility.ToExactFrames(time, frameRate);
                        bool useIntField = TimeUtility.OnFrameBoundary(time, frameRate);

                        if (readOnly)
                        {
                            if (useIntField)
                                EditorGUI.IntField(framesRect, Styles.FramesPrefix, frames, EditorStyles.label);
                            else
                                EditorGUI.DoubleField(framesRect, Styles.FramesPrefix, exactFrames, EditorStyles.label);
                        }
                        else
                        {
                            if (useIntField)
                            {
                                int newFrames = DelayedAndDraggableIntField(framesRect, Styles.FramesPrefix, frames, ref inputEvent);
                                time = Math.Max(0, TimeUtility.FromFrames(newFrames, frameRate));
                            }
                            else
                            {
                                double newExactFrames = DelayedAndDraggableDoubleField(framesRect, Styles.FramesPrefix, exactFrames, ref inputEvent);
                                time = Math.Max(0, TimeUtility.FromFrames((int)Math.Floor(newExactFrames), frameRate));
                            }
                        }

                        if (EditorGUI.EndChangeCheck())
                        {
                            property.doubleValue = Clamp(time, minValue, maxValue);
                        }
                    }
                }
            }
        }

        public static double TimeFieldUsingTimeReference(
            GUIContent label, double time, bool readOnly, bool showMixed, double frameRate, double minValue,
            double maxValue, ref InputEvent inputEvent)
        {
            var state = TimelineWindow.instance.state;
            var needsTimeConversion = state != null && state.timeReferenceMode == TimeReferenceMode.Global;

            if (needsTimeConversion)
                time = state.editSequence.ToGlobalTime(time);

            var t = TimeField(label, time, readOnly, showMixed, frameRate, minValue, maxValue, ref inputEvent);

            if (needsTimeConversion)
                t = state.editSequence.ToLocalTime(t);

            return t;
        }

        public static double DurationFieldUsingTimeReference(
            GUIContent label, double start, double end, bool readOnly, bool showMixed, double frameRate,
            double minValue, double maxValue, ref InputEvent inputEvent)
        {
            var state = TimelineWindow.instance.state;
            var needsTimeConversion = state != null && state.timeReferenceMode == TimeReferenceMode.Global;

            if (needsTimeConversion)
            {
                start = state.editSequence.ToGlobalTime(start);
                end = state.editSequence.ToGlobalTime(end);
            }

            var duration = end - start;

            var t = TimeField(label, duration, readOnly, showMixed, frameRate, minValue, maxValue, ref inputEvent);

            end = start + t;

            if (needsTimeConversion)
            {
                start = state.editSequence.ToLocalTime(start);
                end = state.editSequence.ToLocalTime(end);
            }

            return end - start;
        }

        public static double TimeField(Rect rect, GUIContent label, double time, bool readOnly, bool showMixed, double frameRate, double minValue, double maxValue, ref InputEvent inputEvent)
        {
            using (new HorizontalScope(label, GUIStyle.none))
            {
                rect = EditorGUI.PrefixLabel(rect, label);

                using (new IndentLevelScope(0))
                using (new LabelWidthScope((int)EditorGUI.kMiniLabelW))
                using (new GUIMixedValueScope(showMixed))
                {
                    var secondsRect = new Rect(rect.xMin, rect.yMin, rect.width / 2 - EditorGUI.kSpacingSubLabel, rect.height);
                    var framesRect = new Rect(rect.xMin + rect.width / 2, rect.yMin, rect.width / 2, rect.height);

                    if (readOnly)
                    {
                        EditorGUI.FloatField(secondsRect, Styles.SecondsPrefix, (float)time, EditorStyles.label);
                    }
                    else
                    {
                        time = DelayedAndDraggableDoubleField(secondsRect, Styles.SecondsPrefix, time, ref inputEvent);
                    }

                    if (frameRate > TimeUtility.kTimeEpsilon)
                    {
                        int frames = TimeUtility.ToFrames(time, frameRate);
                        double exactFrames = TimeUtility.ToExactFrames(time, frameRate);
                        bool useIntField = TimeUtility.OnFrameBoundary(time, frameRate);
                        if (readOnly)
                        {
                            if (useIntField)
                                EditorGUI.IntField(framesRect, Styles.FramesPrefix, frames, EditorStyles.label);
                            else
                                EditorGUI.FloatField(framesRect, Styles.FramesPrefix, (float)exactFrames, EditorStyles.label);
                        }
                        else
                        {
                            double newTime;
                            EditorGUI.BeginChangeCheck();
                            if (useIntField)
                            {
                                int newFrames = DelayedAndDraggableIntField(framesRect, Styles.FramesPrefix, frames, ref inputEvent);
                                newTime = Math.Max(0, TimeUtility.FromFrames(newFrames, frameRate));
                            }
                            else
                            {
                                double newExactFrames = DelayedAndDraggableDoubleField(framesRect, Styles.FramesPrefix, exactFrames, ref inputEvent);
                                newTime = Math.Max(0, TimeUtility.FromFrames((int)Math.Floor(newExactFrames), frameRate));
                            }

                            if (EditorGUI.EndChangeCheck())
                            {
                                time = newTime;
                            }
                        }
                    }
                }
            }

            return Clamp(time, minValue, maxValue);
        }

        public static double TimeField(GUIContent label, double time, bool readOnly, bool showMixed, double frameRate, double minValue, double maxValue, ref InputEvent inputEvent)
        {
            var rect = EditorGUILayout.GetControlRect();
            return TimeField(rect, label, time, readOnly, showMixed, frameRate, minValue, maxValue, ref inputEvent);
        }

        static InputEvent InputEventType(Rect rect, int id)
        {
            var evt = Event.current;
            switch (evt.GetTypeForControl(id))
            {
                case EventType.MouseDown:
                    if (rect.Contains(evt.mousePosition) && evt.button == 0)
                    {
                        return InputEvent.DragEnter;
                    }
                    break;
                case EventType.MouseUp:
                    if (GUIUtility.hotControl == id)
                    {
                        return InputEvent.DragExit;
                    }
                    break;
                case EventType.MouseDrag:
                    if (GUIUtility.hotControl == id)
                    {
                        return InputEvent.Drag;
                    }
                    break;
                case EventType.KeyDown:
                    if (GUIUtility.hotControl == id && evt.keyCode == KeyCode.Escape)
                    {
                        return InputEvent.DragExit;
                    }
                    break;
            }
            return InputEvent.None;
        }

        static double DelayedAndDraggableDoubleField(Rect rect, GUIContent label, double value, ref InputEvent inputEvent, double dragSensitivity)
        {
            var id = GUIUtility.GetControlID(FocusType.Keyboard);
            var fieldRect = EditorGUI.PrefixLabel(rect, id, label);
            rect.xMax = fieldRect.x;

            double refValue = value;
            long dummy = 0;

            inputEvent |= InputEventType(rect, id);

            EditorGUI.DragNumberValue(rect, id, true, ref refValue, ref dummy, dragSensitivity);

            EditorGUI.BeginChangeCheck();
            var result = EditorGUI.DelayedDoubleFieldInternal(fieldRect, GUIContent.none, refValue, EditorStyles.numberField);
            if (EditorGUI.EndChangeCheck())
                inputEvent |= InputEvent.KeyboardInput;

            return result;
        }

        static int DelayedAndDraggableIntField(Rect rect, GUIContent label, int value, ref InputEvent inputEvent, long dragSensitivity)
        {
            var id = GUIUtility.GetControlID(FocusType.Keyboard);
            var fieldRect = EditorGUI.PrefixLabel(rect, id, label);
            rect.xMax = fieldRect.x;

            double dummy = 0.0;
            long refValue = value;

            inputEvent |= InputEventType(rect, id);

            EditorGUI.DragNumberValue(rect, id, false, ref dummy, ref refValue, dragSensitivity);

            EditorGUI.BeginChangeCheck();
            var result = EditorGUI.DelayedIntFieldInternal(fieldRect, GUIContent.none, (int)refValue, EditorStyles.numberField);
            if (EditorGUI.EndChangeCheck())
                inputEvent |= InputEvent.KeyboardInput;

            return result;
        }

        internal static double DelayedAndDraggableDoubleField(GUIContent label, double value, ref InputEvent action, double dragSensitivity)
        {
            var r = EditorGUILayout.s_LastRect = EditorGUILayout.GetControlRect(false, EditorGUI.kSingleLineHeight);
            return DelayedAndDraggableDoubleField(r, label, value, ref action, dragSensitivity);
        }

        static void DelayedAndDraggableDoubleField(Rect rect, GUIContent label, SerializedProperty property, ref InputEvent inputEvent)
        {
            EditorGUI.BeginChangeCheck();
            var newValue = DelayedAndDraggableDoubleField(rect, label, property.doubleValue, ref inputEvent);
            if (EditorGUI.EndChangeCheck())
                property.doubleValue = newValue;
        }

        static double DelayedAndDraggableDoubleField(Rect rect, GUIContent label, double value, ref InputEvent inputEvent)
        {
            var dragSensitivity = NumericFieldDraggerUtility.CalculateFloatDragSensitivity(value);
            return DelayedAndDraggableDoubleField(rect, label, value, ref inputEvent, dragSensitivity);
        }

        static int DelayedAndDraggableIntField(Rect rect, GUIContent label, int value, ref InputEvent inputEvent)
        {
            var dragSensitivity = NumericFieldDraggerUtility.CalculateIntDragSensitivity(value);
            return DelayedAndDraggableIntField(rect, label, value, ref inputEvent, dragSensitivity);
        }

        internal static T Clamp<T>(this T val, T min, T max) where T : IComparable<T>
        {
            if (val.CompareTo(min) < 0) return min;
            if (val.CompareTo(max) > 0) return max;
            return val;
        }

        public static Editor GetInspectorForObjects(UnityEngine.Object[] objects, Editor previousEditor)
        {
            // create cached editor throws on assembly reload...
            try
            {
                if (objects.Any(x => x != null))
                {
                    var director = TimelineWindow.instance.state.editSequence.director;
                    Editor.CreateCachedEditorWithContext(objects, director, null, ref previousEditor);
                    return previousEditor;
                }
            }
            catch (Exception)
            { }

            return null;
        }
    }
}
