// 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.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Objects;
using osuTK;

namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
    public partial class TimelineBreak : CompositeDrawable, IHasContextMenu
    {
        public Bindable<BreakPeriod> Break { get; } = new Bindable<BreakPeriod>();

        public Action<BreakPeriod>? OnDeleted { get; init; }

        public TimelineBreak(BreakPeriod b)
        {
            Break.Value = b;
        }

        [BackgroundDependencyLoader]
        private void load(OsuColour colours)
        {
            RelativePositionAxes = Axes.X;
            RelativeSizeAxes = Axes.Both;
            Origin = Anchor.TopLeft;
            Padding = new MarginPadding { Horizontal = -5 };

            InternalChildren = new Drawable[]
            {
                new Container
                {
                    RelativeSizeAxes = Axes.Both,
                    Padding = new MarginPadding { Horizontal = 5 },
                    Child = new Box
                    {
                        RelativeSizeAxes = Axes.Both,
                        Colour = colours.Gray5,
                        Alpha = 0.9f,
                    },
                },
                new DragHandle(isStartHandle: true)
                {
                    Break = { BindTarget = Break },
                    Anchor = Anchor.TopLeft,
                    Origin = Anchor.TopLeft,
                    Action = (time, breakPeriod) => new ManualBreakPeriod(time, breakPeriod.EndTime),
                },
                new DragHandle(isStartHandle: false)
                {
                    Break = { BindTarget = Break },
                    Anchor = Anchor.TopRight,
                    Origin = Anchor.TopRight,
                    Action = (time, breakPeriod) => new ManualBreakPeriod(breakPeriod.StartTime, time),
                },
            };
        }

        protected override void LoadComplete()
        {
            base.LoadComplete();

            Break.BindValueChanged(_ =>
            {
                X = (float)Break.Value.StartTime;
                Width = (float)Break.Value.Duration;
            }, true);
        }

        public MenuItem[] ContextMenuItems => new MenuItem[]
        {
            new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => OnDeleted?.Invoke(Break.Value)),
        };

        private partial class DragHandle : FillFlowContainer
        {
            public Bindable<BreakPeriod> Break { get; } = new Bindable<BreakPeriod>();

            public new Anchor Anchor
            {
                get => base.Anchor;
                init => base.Anchor = value;
            }

            public Func<double, BreakPeriod, BreakPeriod>? Action { get; init; }

            private readonly bool isStartHandle;

            private Container handle = null!;
            private (double min, double max)? allowedDragRange;

            [Resolved]
            private EditorBeatmap beatmap { get; set; } = null!;

            [Resolved]
            private Timeline timeline { get; set; } = null!;

            [Resolved]
            private IEditorChangeHandler? changeHandler { get; set; }

            [Resolved]
            private OsuColour colours { get; set; } = null!;

            public DragHandle(bool isStartHandle)
            {
                this.isStartHandle = isStartHandle;
            }

            [BackgroundDependencyLoader]
            private void load()
            {
                AutoSizeAxes = Axes.X;
                RelativeSizeAxes = Axes.Y;
                Direction = FillDirection.Horizontal;
                Spacing = new Vector2(5);

                Children = new Drawable[]
                {
                    handle = new Container
                    {
                        Anchor = Anchor,
                        Origin = Anchor,
                        RelativeSizeAxes = Axes.Y,
                        CornerRadius = 4,
                        Masking = true,
                        Child = new Box
                        {
                            RelativeSizeAxes = Axes.Both,
                            Colour = Colour4.White,
                        },
                    },
                    new OsuSpriteText
                    {
                        BypassAutoSizeAxes = Axes.X,
                        Anchor = Anchor,
                        Origin = Anchor,
                        Text = "Break",
                        Margin = new MarginPadding { Top = 2, },
                    },
                };
            }

            protected override void LoadComplete()
            {
                base.LoadComplete();

                updateState();
                FinishTransforms(true);
            }

            protected override bool OnHover(HoverEvent e)
            {
                updateState();
                return true;
            }

            protected override void OnHoverLost(HoverLostEvent e)
            {
                updateState();
                base.OnHoverLost(e);
            }

            protected override bool OnDragStart(DragStartEvent e)
            {
                changeHandler?.BeginChange();
                updateState();

                double min = beatmap.HitObjects.LastOrDefault(ho => ho.GetEndTime() <= Break.Value.StartTime)?.GetEndTime() ?? double.NegativeInfinity;
                double max = beatmap.HitObjects.FirstOrDefault(ho => ho.StartTime >= Break.Value.EndTime)?.StartTime ?? double.PositiveInfinity;

                if (isStartHandle)
                    max = Math.Min(max, Break.Value.EndTime - BreakPeriod.MIN_BREAK_DURATION);
                else
                    min = Math.Max(min, Break.Value.StartTime + BreakPeriod.MIN_BREAK_DURATION);

                allowedDragRange = (min, max);

                return true;
            }

            protected override void OnDrag(DragEvent e)
            {
                base.OnDrag(e);

                Debug.Assert(allowedDragRange != null);

                if (Action != null
                    && timeline.FindSnappedPositionAndTime(e.ScreenSpaceMousePosition).Time is double time
                    && time > allowedDragRange.Value.min
                    && time < allowedDragRange.Value.max)
                {
                    int index = beatmap.Breaks.IndexOf(Break.Value);
                    beatmap.Breaks[index] = Break.Value = Action.Invoke(time, Break.Value);
                }

                updateState();
            }

            protected override void OnDragEnd(DragEndEvent e)
            {
                changeHandler?.EndChange();
                updateState();
                base.OnDragEnd(e);
            }

            private void updateState()
            {
                bool active = IsHovered || IsDragged;

                var colour = colours.Gray8;
                if (active)
                    colour = colour.Lighten(0.3f);

                handle.FadeColour(colour, 400, Easing.OutQuint);
                handle.ResizeWidthTo(active ? 10 : 8, 400, Easing.OutElasticHalf);
            }
        }
    }
}