// 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.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
using osu.Game.Utils;
using osuTK;

namespace osu.Game.Overlays.BeatmapSet
{
    public partial class BeatmapPicker : Container
    {
        private const float tile_icon_padding = 7;
        private const float tile_spacing = 2;

        private readonly OsuSpriteText version, starRating, starRatingText;
        private readonly LinkFlowContainer guestMapperContainer;
        private readonly FillFlowContainer starRatingContainer;
        private readonly Statistic plays, favourites;

        public readonly DifficultiesContainer Difficulties;

        public readonly Bindable<APIBeatmap?> Beatmap = new Bindable<APIBeatmap?>();
        private APIBeatmapSet? beatmapSet;

        public APIBeatmapSet? BeatmapSet
        {
            get => beatmapSet;
            set
            {
                if (value == beatmapSet) return;

                beatmapSet = value;
                updateDisplay();
            }
        }

        public BeatmapPicker()
        {
            RelativeSizeAxes = Axes.X;
            AutoSizeAxes = Axes.Y;

            Children = new Drawable[]
            {
                new FillFlowContainer
                {
                    AutoSizeAxes = Axes.Y,
                    RelativeSizeAxes = Axes.X,
                    Direction = FillDirection.Vertical,
                    Children = new Drawable[]
                    {
                        Difficulties = new DifficultiesContainer
                        {
                            RelativeSizeAxes = Axes.X,
                            AutoSizeAxes = Axes.Y,
                            Margin = new MarginPadding { Left = -(tile_icon_padding + tile_spacing / 2), Bottom = 10 },
                            OnLostHover = () =>
                            {
                                showBeatmap(Beatmap.Value);
                                starRatingContainer.FadeOut(100);
                            },
                        },
                        new FillFlowContainer
                        {
                            AutoSizeAxes = Axes.Both,
                            Spacing = new Vector2(5f),
                            Children = new Drawable[]
                            {
                                version = new OsuSpriteText
                                {
                                    Anchor = Anchor.BottomLeft,
                                    Origin = Anchor.BottomLeft,
                                    Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold)
                                },
                                guestMapperContainer = new LinkFlowContainer(s =>
                                    s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11))
                                {
                                    AutoSizeAxes = Axes.Both,
                                    Anchor = Anchor.BottomLeft,
                                    Origin = Anchor.BottomLeft,
                                    Margin = new MarginPadding { Bottom = 1 },
                                },
                                starRatingContainer = new FillFlowContainer
                                {
                                    Anchor = Anchor.BottomLeft,
                                    Origin = Anchor.BottomLeft,
                                    Alpha = 0,
                                    Direction = FillDirection.Horizontal,
                                    Spacing = new Vector2(2f, 0),
                                    Margin = new MarginPadding { Bottom = 1 },
                                    Children = new[]
                                    {
                                        starRatingText = new OsuSpriteText
                                        {
                                            Anchor = Anchor.BottomLeft,
                                            Origin = Anchor.BottomLeft,
                                            Font = OsuFont.GetFont(size: 11, weight: FontWeight.Bold),
                                            Text = BeatmapsetsStrings.ShowStatsStars,
                                        },
                                        starRating = new OsuSpriteText
                                        {
                                            Anchor = Anchor.BottomLeft,
                                            Origin = Anchor.BottomLeft,
                                            Font = OsuFont.GetFont(size: 11, weight: FontWeight.Bold),
                                            Text = string.Empty,
                                        },
                                    }
                                },
                            },
                        },
                        new FillFlowContainer
                        {
                            RelativeSizeAxes = Axes.X,
                            AutoSizeAxes = Axes.Y,
                            Spacing = new Vector2(10f),
                            Margin = new MarginPadding { Top = 5 },
                            Children = new[]
                            {
                                plays = new Statistic(FontAwesome.Solid.PlayCircle),
                                favourites = new Statistic(FontAwesome.Solid.Heart),
                            },
                        },
                    },
                },
            };

            Beatmap.ValueChanged += b =>
            {
                showBeatmap(b.NewValue);
                updateDifficultyButtons();
            };
        }

        [Resolved]
        private IBindable<RulesetInfo> ruleset { get; set; } = null!;

        [BackgroundDependencyLoader]
        private void load(OsuColour colours)
        {
            starRating.Colour = colours.Yellow;
            starRatingText.Colour = colours.Yellow;
            updateDisplay();
        }

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

            ruleset.ValueChanged += _ => updateDisplay();

            // done here so everything can bind in intialization and get the first trigger
            Beatmap.TriggerChange();
        }

        private void updateDisplay()
        {
            Difficulties.Clear();

            if (BeatmapSet != null)
            {
                Difficulties.ChildrenEnumerable = BeatmapSet.Beatmaps.Concat(BeatmapSet.Converts ?? Array.Empty<APIBeatmap>())
                                                            .Where(b => b.Ruleset.MatchesOnlineID(ruleset.Value))
                                                            .OrderBy(b => !b.Convert)
                                                            .ThenBy(b => b.StarRating)
                                                            .Select(b => new DifficultySelectorButton(b, b.Convert ? new RulesetInfo { OnlineID = 0 } : null)
                                                            {
                                                                State = DifficultySelectorState.NotSelected,
                                                                OnHovered = beatmap =>
                                                                {
                                                                    showBeatmap(beatmap);
                                                                    starRating.Text = beatmap.StarRating.FormatStarRating();
                                                                    starRatingContainer.FadeIn(100);
                                                                },
                                                                OnClicked = beatmap => { Beatmap.Value = beatmap; },
                                                            });
            }

            starRatingContainer.FadeOut(100);

            // If a selection is already made, try and maintain it.
            if (Beatmap.Value != null)
                Beatmap.Value = Difficulties.FirstOrDefault(b => b.Beatmap.OnlineID == Beatmap.Value.OnlineID)?.Beatmap;

            // Else just choose the first available difficulty for now.
            Beatmap.Value ??= Difficulties.FirstOrDefault()?.Beatmap;

            plays.Value = BeatmapSet?.PlayCount ?? 0;
            favourites.Value = BeatmapSet?.FavouriteCount ?? 0;

            updateDifficultyButtons();
        }

        private void showBeatmap(APIBeatmap? beatmapInfo)
        {
            guestMapperContainer.Clear();

            if (beatmapInfo?.AuthorID != BeatmapSet?.AuthorID)
            {
                APIUser? user = BeatmapSet?.RelatedUsers?.SingleOrDefault(u => u.OnlineID == beatmapInfo?.AuthorID);

                if (user != null)
                {
                    guestMapperContainer.AddText("mapped by ");
                    guestMapperContainer.AddUserLink(user);
                }
            }

            version.Text = beatmapInfo?.DifficultyName ?? string.Empty;
        }

        private void updateDifficultyButtons()
        {
            Difficulties.Children.ToList().ForEach(diff => diff.State = diff.Beatmap == Beatmap.Value ? DifficultySelectorState.Selected : DifficultySelectorState.NotSelected);
        }

        public partial class DifficultiesContainer : FillFlowContainer<DifficultySelectorButton>
        {
            public Action? OnLostHover;

            protected override void OnHoverLost(HoverLostEvent e)
            {
                base.OnHoverLost(e);
                OnLostHover?.Invoke();
            }
        }

        public partial class DifficultySelectorButton : OsuClickableContainer, IStateful<DifficultySelectorState>
        {
            private const float transition_duration = 100;
            private const float size = 54;
            private const float background_size = size - 2;

            private readonly Container background;
            private readonly Box backgroundBox;
            private readonly DifficultyIcon icon;

            public readonly APIBeatmap Beatmap;

            public Action<APIBeatmap>? OnHovered;
            public Action<APIBeatmap>? OnClicked;
            public event Action<DifficultySelectorState>? StateChanged;

            private DifficultySelectorState state;

            public DifficultySelectorState State
            {
                get => state;
                set
                {
                    if (value == state) return;

                    state = value;

                    StateChanged?.Invoke(State);
                    if (value == DifficultySelectorState.Selected)
                        fadeIn();
                    else
                        fadeOut();
                }
            }

            public DifficultySelectorButton(APIBeatmap beatmapInfo, IRulesetInfo? ruleset)
            {
                Beatmap = beatmapInfo;
                Size = new Vector2(size);
                Margin = new MarginPadding { Horizontal = tile_spacing / 2 };

                Children = new Drawable[]
                {
                    background = new Container
                    {
                        Size = new Vector2(background_size),
                        Masking = true,
                        CornerRadius = 4,
                        Child = backgroundBox = new Box
                        {
                            RelativeSizeAxes = Axes.Both,
                            Alpha = 0.5f
                        }
                    },
                    icon = new DifficultyIcon(beatmapInfo, ruleset)
                    {
                        TooltipType = DifficultyIconTooltipType.None,
                        Current = { Value = new StarDifficulty(beatmapInfo.StarRating, 0) },
                        Anchor = Anchor.Centre,
                        Origin = Anchor.Centre,
                        Size = new Vector2(size - tile_icon_padding * 2),
                        Margin = new MarginPadding { Bottom = 1 },
                    },
                };
            }

            protected override bool OnHover(HoverEvent e)
            {
                fadeIn();
                OnHovered?.Invoke(Beatmap);
                return base.OnHover(e);
            }

            protected override void OnHoverLost(HoverLostEvent e)
            {
                if (State == DifficultySelectorState.NotSelected)
                    fadeOut();
                base.OnHoverLost(e);
            }

            protected override bool OnClick(ClickEvent e)
            {
                OnClicked?.Invoke(Beatmap);
                return base.OnClick(e);
            }

            private void fadeIn()
            {
                background.FadeIn(transition_duration);
                icon.FadeIn(transition_duration);
            }

            private void fadeOut()
            {
                background.FadeOut();
                icon.FadeTo(0.7f, transition_duration);
            }

            [BackgroundDependencyLoader]
            private void load(OverlayColourProvider colourProvider)
            {
                backgroundBox.Colour = colourProvider.Background6;
            }
        }

        private partial class Statistic : FillFlowContainer
        {
            private readonly OsuSpriteText text;

            private int value;

            public int Value
            {
                get => value;
                set
                {
                    this.value = value;
                    text.Text = Value.ToLocalisableString(@"N0");
                }
            }

            public Statistic(IconUsage icon)
            {
                AutoSizeAxes = Axes.Both;
                Direction = FillDirection.Horizontal;
                Spacing = new Vector2(2f);

                Children = new Drawable[]
                {
                    new SpriteIcon
                    {
                        Anchor = Anchor.CentreLeft,
                        Origin = Anchor.CentreLeft,
                        Icon = icon,
                        Shadow = true,
                        Size = new Vector2(12),
                    },
                    text = new OsuSpriteText
                    {
                        Anchor = Anchor.CentreLeft,
                        Origin = Anchor.CentreLeft,
                        Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold, italics: true),
                    },
                };
            }
        }

        public enum DifficultySelectorState
        {
            Selected,
            NotSelected,
        }
    }
}
