using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.U2D;

namespace UnityEditor.U2D.SpriteShape
{
    public interface IAngleRangeCache
    {
        List<AngleRange> angleRanges { get; }
        int selectedIndex { get; set; }
        float previewAngle { get; set; }
        void RegisterUndo(string name);
    }

    public class AngleRangeController
    {
        public event Action selectionChanged = () => { };
        public IAngleRangeCache cache { get; set; }
        public IAngleRangeView view { get; set; }
        public float angleOffset { get; set; }
        public float radius { get; set; }
        public Rect rect { get; set; }
        public bool snap { get; set; }
        public Color gradientMin { get; set; }
        public Color gradientMid { get; set; }
        public Color gradientMax { get; set; }

        public AngleRange selectedAngleRange
        {
            get
            {
                Debug.Assert(cache != null);

                AngleRange angleRange = null;

                if (cache.selectedIndex >= 0 && cache.selectedIndex < cache.angleRanges.Count)
                    return cache.angleRanges[cache.selectedIndex];

                return angleRange;
            }
        }

        private AngleRange hoveredAngleRange
        {
            get
            {
                Debug.Assert(cache != null);
                Debug.Assert(view != null);

                AngleRange angleRange = null;

                if (view.hoveredRangeIndex >= 0 && view.hoveredRangeIndex < cache.angleRanges.Count)
                    return cache.angleRanges[view.hoveredRangeIndex];

                return angleRange;
            }
        }

        public void OnGUI()
        {
            Debug.Assert(cache != null);

            view.SetupLayout(rect, angleOffset, radius);

            DoAngleRanges();
            HandleSelectAngleRange();
            HandleCreateRange();
            HandlePreviewSelector();
            HandleRemoveRange();
        }

        private void DoAngleRanges()
        {
            var removeInvalid = false;

            if (view.IsActionTriggering(AngleRangeAction.ModifyRange))
                cache.RegisterUndo("Modify Range");

            if (view.IsActionFinishing(AngleRangeAction.ModifyRange))
                removeInvalid = true;

            var index = 0;
            foreach (var angleRange in cache.angleRanges)
            {
                var start = angleRange.start;
                var end = angleRange.end;
                var isSelected = selectedAngleRange != null && selectedAngleRange == angleRange;

                if (view.DoAngleRange(index, rect, radius, angleOffset, ref start, ref end, snap, !isSelected, gradientMin, gradientMid, gradientMax))
                    SetRange(angleRange, start, end);

                ++index;
            }

            if (removeInvalid)
                RemoveInvalidRanges();
        }

        public void RemoveInvalidRanges()
        {
            var toDelete = new List<AngleRange>();

            foreach (var angleRange in cache.angleRanges)
            {
                var start = angleRange.start;
                var end = angleRange.end;

                if (start >= end)
                    toDelete.Add(angleRange);
            }

            foreach (var angleRange in toDelete)
                cache.angleRanges.Remove(angleRange);

            if (toDelete.Count > 0)
            {
                SetSelectedIndexFromPreviewAngle();
                view.Repaint();
            }
        }

        private void HandleSelectAngleRange()
        {
            int newSelected;
            if (view.DoSelectAngleRange(cache.selectedIndex, out newSelected))
            {
                cache.RegisterUndo("Select Angle Range");
                cache.previewAngle = view.GetAngleFromPosition(rect, angleOffset);
                SelectIndex(newSelected);
            }

            if (view.IsActionActive(AngleRangeAction.SelectRange))
            {
                if (hoveredAngleRange == null)
                    return;

                view.DrawAngleRangeOutline(rect, hoveredAngleRange.start, hoveredAngleRange.end, angleOffset, radius);
            }
        }

        private void HandleCreateRange()
        {
            if (!view.IsActionActive(AngleRangeAction.CreateRange))
                return;

            var angle = view.GetAngleFromPosition(rect, angleOffset);
            var start = 0f;
            var end = 0f;
            var canCreate = GetNewRangeBounds(angle, out start, out end);

            if (canCreate && view.DoCreateRange(rect, radius, angleOffset, start, end))
            {
                CreateRangeAtAngle(angle);
                cache.previewAngle = angle;
                SetSelectedIndexFromPreviewAngle();
            }
        }

        private void HandlePreviewSelector()
        {
            if (view.IsActionTriggering(AngleRangeAction.ModifySelector))
                cache.RegisterUndo("Set Preview Angle");

            float newAngle;
            if (view.DoSelector(rect, angleOffset, radius, cache.previewAngle, out newAngle))
            {
                if (selectedAngleRange == null)
                    newAngle = Mathf.Repeat(newAngle + 180f, 360f) - 180f;

                cache.previewAngle = newAngle;
                SetSelectedIndexFromPreviewAngle();
            }
        }

        private void SetSelectedIndexFromPreviewAngle()
        {
            var index = SpriteShapeEditorUtility.GetRangeIndexFromAngle(cache.angleRanges, cache.previewAngle);
            SelectIndex(index);
        }

        private void SelectIndex(int index)
        {
            view.RequestFocusIndex(index);

            if (cache.selectedIndex == index)
                return;

            cache.selectedIndex = index;
            selectionChanged();
        }

        private void ClampPreviewAngle(float start, float end, float prevStart, float prevEnd)
        {
            var angle = cache.previewAngle;

            if (prevStart < start)
            {
                var a1 = Mathf.Repeat(angle - prevStart, 360f);
                var a2 = Mathf.Repeat(angle - start, 360f);

                if (a1 < a2)
                    angle = Mathf.Min(start, end);
            }
            else if (prevEnd > end)
            {
                var a1 = Mathf.Repeat(prevEnd - angle, 360f);
                var a2 = Mathf.Repeat(end - angle, 360f);

                if (a1 < a2)
                    angle = Mathf.Max(start, end);
            }

            cache.previewAngle = angle;
        }

        private void HandleRemoveRange()
        {
            if (view.DoRemoveRange())
            {
                cache.RegisterUndo("Remove Range");
                cache.angleRanges.RemoveAt(cache.selectedIndex);
                SelectIndex(-1);
            }
        }

        public void CreateRange()
        {
            CreateRangeAtAngle(cache.previewAngle);
        }

        private void CreateRangeAtAngle(float angle)
        {
            var start = 0f;
            var end = 0f;

            if (GetNewRangeBounds(angle, out start, out end))
            {
                cache.RegisterUndo("Create Range");

                var angleRange = new AngleRange();
                angleRange.start = start;
                angleRange.end = end;

                cache.angleRanges.Add(angleRange);

                ValidateRange(angleRange);
                SetSelectedIndexFromPreviewAngle();
            }
        }

        public void SetRange(AngleRange angleRange, float start, float end)
        {
            var prevStart = angleRange.start;
            var prevEnd = angleRange.end;

            angleRange.start = start;
            angleRange.end = end;

            ValidateRange(angleRange, prevStart, prevEnd);

            if (angleRange == selectedAngleRange)
                ClampPreviewAngle(start, end, prevStart, prevEnd);
        }

        private bool GetNewRangeBounds(float angle, out float emptyRangeStart, out float emptyRangeEnd)
        {
            angle = Mathf.Repeat(angle + 180f, 360f) - 180f;

            emptyRangeStart = float.MinValue;
            emptyRangeEnd = float.MaxValue;

            if (GetAngleRangeAt(angle) != null)
                return false;

            FindMinMax(out emptyRangeEnd, out emptyRangeStart);

            if (angle < emptyRangeStart)
                emptyRangeStart -= 360f;

            if (angle > emptyRangeEnd)
                emptyRangeEnd += 360f;

            foreach (var angleRange in cache.angleRanges)
            {
                var start = angleRange.start;
                var end = angleRange.end;

                if (angle > end)
                    emptyRangeStart = Mathf.Max(emptyRangeStart, end);

                if (angle < start)
                    emptyRangeEnd = Mathf.Min(emptyRangeEnd, start);
            }

            var rangeLength = emptyRangeEnd - emptyRangeStart;

            if (rangeLength > 90f)
            {
                emptyRangeStart = Mathf.Max(angle - 45f, emptyRangeStart);
                emptyRangeEnd = Mathf.Min(angle + 45f, emptyRangeEnd);
            }

            return true;
        }

        private AngleRange GetAngleRangeAt(float angle)
        {
            foreach (var angleRange in cache.angleRanges)
            {
                var start = angleRange.start;
                var end = angleRange.end;
                var range = end - start;

                var angle2 = Mathf.Repeat(angle - start, 360f);

                if (angle2 >= 0f && angle2 <= range)
                    return angleRange;
            }

            return null;
        }

        private void FindMinMax(out float min, out float max)
        {
            min = float.MaxValue;
            max = float.MinValue;

            foreach (var angleRange in cache.angleRanges)
            {
                min = Mathf.Min(angleRange.start, min);
                max = Mathf.Max(angleRange.end, max);
            }
        }

        private void ValidateRange(AngleRange range)
        {
            ValidateRange(range, range.start, range.end);
        }

        private void ValidateRange(AngleRange angleRange, float prevStart, float prevEnd)
        {
            var start = angleRange.start;
            var end = angleRange.end;

            foreach (var otherRange in cache.angleRanges)
            {
                var otherStart = otherRange.start;
                var otherEnd = otherRange.end;

                if (otherRange == angleRange)
                {
                    if ((start > 180f && end > 180f) || (start < -180f && end < -180f))
                    {
                        start = Mathf.Repeat(start + 180f, 360f) - 180f;
                        end = Mathf.Repeat(end + 180f, 360f) - 180f;
                    }

                    otherStart = start + 360f;
                    otherEnd = end - 360f;
                }

                ValidateRangeStartEnd(ref start, ref end, prevStart, prevEnd, otherStart, otherEnd);
            }

            angleRange.start = start;
            angleRange.end = end;
        }

        private void ValidateRangeStartEnd(ref float start, ref float end, float prevStart, float prevEnd, float otherStart, float otherEnd)
        {
            var min = Mathf.Min(start, otherStart);
            var max = Mathf.Max(end, otherEnd);

            start -= min;
            end -= min;
            otherStart -= min;
            otherEnd -= min;
            prevStart -= min;
            prevEnd -= min;

            if (prevEnd != end)
                end = Mathf.Clamp(end, start, otherStart >= start ? otherStart : 360f);

            start += min - max;
            end += min - max;
            otherStart += min - max;
            otherEnd += min - max;
            prevStart += min - max;
            prevEnd += min - max;

            if (prevStart != start)
                start = Mathf.Clamp(start, otherEnd <= end ? otherEnd : -360f, end);

            start += max;
            end += max;
        }
    }
}