// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.Rooms;
using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;

namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
{
    public partial class RankedPlayUserDisplay : CompositeDrawable
    {
        public readonly BindableInt Health = new BindableInt
        {
            MaxValue = 1_000_000,
            MinValue = 0,
            Value = 1_000_000,
        };

        public HealthBar HealthDisplay { get; private set; } = null!;

        private readonly APIUser user;
        private readonly Anchor contentAnchor;
        private readonly RankedPlayColourScheme colourScheme;

        private BufferedContainer grayScaleContainer = null!;
        private OsuSpriteText beatmapState = null!;
        private OsuSpriteText damageMultiplierText = null!;
        private OsuSpriteText lastStandText = null!;

        private BeatmapAvailability availability = BeatmapAvailability.Unknown();

        [Resolved]
        private MultiplayerClient client { get; set; } = null!;

        [Resolved]
        private RankedPlayCornerPiece? cornerPiece { get; set; }

        public RankedPlayUserDisplay(APIUser user, Anchor contentAnchor, RankedPlayColourScheme colourScheme)
        {
            this.user = user;
            this.contentAnchor = contentAnchor;
            this.colourScheme = colourScheme;
        }

        [BackgroundDependencyLoader]
        private void load()
        {
            var shear = contentAnchor == Anchor.TopLeft || contentAnchor == Anchor.BottomRight
                ? -OsuGame.SHEAR
                : OsuGame.SHEAR;

            var beatmapStateAnchor = (contentAnchor & Anchor.x0) != 0
                ? Anchor.CentreLeft
                : Anchor.CentreRight;

            var damageMultiplierAnchor = (contentAnchor & Anchor.x0) != 0
                ? Anchor.CentreRight
                : Anchor.CentreLeft;

            var lastStandAnchor = (contentAnchor & Anchor.x0) != 0
                ? Anchor.CentreLeft
                : Anchor.CentreRight;

            InternalChildren =
            [
                new CircularContainer
                {
                    Name = "Avatar",
                    Size = new Vector2(72),
                    Masking = true,
                    Anchor = contentAnchor,
                    Origin = contentAnchor,
                    Children =
                    [
                        new Box
                        {
                            RelativeSizeAxes = Axes.Both,
                            Colour = colourScheme.Surface,
                            Alpha = 0.5f,
                        },
                        grayScaleContainer = new BufferedContainer(cachedFrameBuffer: false, pixelSnapping: true)
                        {
                            RelativeSizeAxes = Axes.Both,
                            Child = new UpdateableAvatar(user)
                            {
                                RelativeSizeAxes = Axes.Both,
                            }
                        }
                    ]
                },
                new FillFlowContainer
                {
                    RelativeSizeAxes = Axes.Both,
                    Padding = (contentAnchor & Anchor.x0) != 0 ? new MarginPadding { Left = 72 } : new MarginPadding { Right = 72 },
                    Direction = FillDirection.Vertical,
                    Children =
                    [
                        new Container
                        {
                            Anchor = contentAnchor,
                            Origin = contentAnchor,
                            RelativeSizeAxes = Axes.X,
                            Height = 16,
                            Padding = new MarginPadding { Horizontal = 4, Vertical = 6 },
                            Children =
                            [
                                damageMultiplierText = new OsuSpriteText
                                {
                                    Anchor = damageMultiplierAnchor,
                                    Origin = damageMultiplierAnchor,
                                    Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
                                    UseFullGlyphHeight = false,
                                },
                                lastStandText = new OsuSpriteText
                                {
                                    Anchor = lastStandAnchor,
                                    Origin = lastStandAnchor,
                                    Text = "Last Stand!",
                                    Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
                                    Alpha = 0,
                                }
                            ]
                        },
                        HealthDisplay = new HealthBar(colourScheme, (contentAnchor & Anchor.x0) != 0, shear)
                        {
                            Health = { BindTarget = Health },
                            RelativeSizeAxes = Axes.X,
                            Height = 22,
                            Anchor = contentAnchor,
                            Origin = contentAnchor,
                        },
                        new FillFlowContainer
                        {
                            Name = "Username/beatmap state container",
                            AutoSizeAxes = Axes.Both,
                            Anchor = contentAnchor,
                            Origin = contentAnchor,
                            Direction = FillDirection.Horizontal,
                            Padding = new MarginPadding { Horizontal = 4, Vertical = 6 },
                            Spacing = new Vector2(5, 0),
                            Children =
                            [
                                new OsuSpriteText
                                {
                                    Name = "Username",
                                    Text = user.Username,
                                    Anchor = contentAnchor,
                                    Origin = contentAnchor,
                                    Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold),
                                    UseFullGlyphHeight = false,
                                },
                                beatmapState = new OsuSpriteText
                                {
                                    Anchor = beatmapStateAnchor,
                                    Origin = beatmapStateAnchor,
                                    Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
                                },
                            ],
                        },
                    ]
                }
            ];
        }

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

            Health.BindValueChanged(e =>
            {
                grayScaleContainer.GrayscaleTo(e.NewValue <= 0 ? 1 : 0, 300);
                cornerPiece?.OnHealthChanged(e.NewValue);

                if (e.NewValue == 1)
                    lastStandText.FadeIn(100).Delay(3000).FadeOut(400);
            });

            client.RoomUpdated += onRoomUpdated;
            onRoomUpdated();
        }

        private void onRoomUpdated()
        {
            updateBeatmapState();
            updateDamageMultiplier();
        }

        private void updateBeatmapState()
        {
            var multiplayerUser = client.Room?.Users.SingleOrDefault(u => u.UserID == user.Id);

            if (multiplayerUser == null || availability == multiplayerUser.BeatmapAvailability)
                return;

            availability = multiplayerUser.BeatmapAvailability;

            if (availability.State is DownloadState.NotDownloaded or DownloadState.Downloading or DownloadState.Importing)
                beatmapState.FadeIn(50);
            else
                beatmapState.FadeOut(50);

            switch (availability.State)
            {
                case DownloadState.NotDownloaded:
                    beatmapState.Text = "Missing Beatmap";
                    break;

                case DownloadState.Downloading:
                    double progress = Math.Clamp(availability.DownloadProgress ?? 0, 0, 1);
                    beatmapState.Text = $"Downloading... ({progress:P0})";
                    break;

                case DownloadState.Importing:
                    beatmapState.Text = "Importing...";
                    break;
            }
        }

        private void updateDamageMultiplier()
        {
            if (client.Room?.MatchState is not RankedPlayRoomState roomState)
                return;

            if (!roomState.Users.TryGetValue(user.Id, out var userInfo))
                return;

            double totalMultiplier = roomState.DamageMultiplier + userInfo.DamageMultiplier;
            damageMultiplierText.Text = $"{totalMultiplier.ToStandardFormattedString(maxDecimalDigits: 1)}x damage";
        }

        protected override void Dispose(bool isDisposing)
        {
            base.Dispose(isDisposing);

            if (client.IsNotNull())
                client.RoomUpdated -= onRoomUpdated;
        }

        public partial class HealthBar : CompositeDrawable
        {
            private readonly bool leftToRight;

            public readonly BindableInt Health = new BindableInt
            {
                MaxValue = 1_000_000,
                MinValue = 0,
                Value = 1_000_000,
            };

            private readonly BindableInt healthTextValue = new BindableInt();

            /// <summary>
            /// relative health threshold below which the health bar starts flashing red
            /// </summary>
            public float HealthFlashThreshold { get; set; } = 0.3f;

            private readonly ColourInfo healthBarColour;

            private readonly Container healthBar;
            private readonly Box healthBarBackground;
            private readonly Container damageIndicator;
            private readonly TrianglesV2 triangles;
            private readonly SpriteIcon heartIcon;
            private readonly OsuSpriteText healthText;

            /// <summary>
            /// Impact position for damage animation
            /// </summary>
            public Vector2 ScreenSpaceImpactPosition
            {
                get
                {
                    var rect = healthBar.ScreenSpaceDrawQuad.AABBFloat;

                    return leftToRight ? new Vector2(rect.Right, rect.Centre.Y) : new Vector2(rect.Left, rect.Centre.Y);
                }
            }

            public HealthBar(RankedPlayColourScheme colourScheme, bool leftToRight, Vector2 shear)
            {
                this.leftToRight = leftToRight;

                Shear = shear;

                Anchor contentAnchor = leftToRight ? Anchor.CentreLeft : Anchor.CentreRight;

                BufferedContainer content;

                InternalChildren =
                [
                    new Container
                    {
                        RelativeSizeAxes = Axes.Both,
                        Masking = true,
                        CornerRadius = 3,
                        BorderThickness = 1f,
                        BorderColour = ColourInfo.GradientVertical(colourScheme.Surface, colourScheme.SurfaceBorder),
                        Child = new Box
                        {
                            RelativeSizeAxes = Axes.Both,
                            Colour = colourScheme.Surface,
                            Alpha = 0.8f,
                        }
                    },
                    new Container
                    {
                        RelativeSizeAxes = Axes.Both,
                        Padding = new MarginPadding { Horizontal = 2.2f, Vertical = 2 }, // slightly different ratio to account for shear
                        Children =
                        [
                            healthBar = new Container
                            {
                                RelativeSizeAxes = Axes.Both,
                                Masking = true,
                                CornerRadius = 2,
                                Anchor = contentAnchor,
                                Origin = contentAnchor,
                                Children =
                                [
                                    healthBarBackground = new Box
                                    {
                                        RelativeSizeAxes = Axes.Both,
                                        Alpha = 0.8f,
                                        Colour = healthBarColour = leftToRight
                                            ? ColourInfo.GradientHorizontal(colourScheme.PrimaryDarker, colourScheme.Primary)
                                            : ColourInfo.GradientHorizontal(colourScheme.Primary, colourScheme.PrimaryDarker),
                                    },
                                    triangles = new TrianglesV2
                                    {
                                        RelativeSizeAxes = Axes.Y,
                                        Anchor = contentAnchor,
                                        Origin = contentAnchor,
                                        SpawnRatio = 0.5f,
                                        ScaleAdjust = 0.75f,
                                        Alpha = 0.1f,
                                        Blending = BlendingParameters.Additive,
                                        Colour = leftToRight
                                            ? ColourInfo.GradientHorizontal(Color4.Transparent, Color4.White)
                                            : ColourInfo.GradientHorizontal(Color4.White, Color4.Transparent),
                                    },
                                ],
                            },
                        ]
                    },
                    content = new BufferedContainer(pixelSnapping: true)
                    {
                        RelativeSizeAxes = Axes.Both,
                        Shear = -shear,
                        BackgroundColour = Color4.White.Opacity(0), // workaround for non-premultiplied alpha blending of white content on transparent background
                        Child = new FillFlowContainer
                        {
                            RelativeSizeAxes = Axes.Both,
                            Direction = FillDirection.Horizontal,
                            Anchor = Anchor.Centre,
                            Origin = Anchor.Centre,
                            Spacing = new Vector2(3),
                            Padding = new MarginPadding { Horizontal = 10 },
                            Children =
                            [
                                new Container
                                {
                                    Size = new Vector2(10),
                                    Anchor = contentAnchor,
                                    Origin = contentAnchor,
                                    Child = heartIcon = new SpriteIcon
                                    {
                                        Icon = FontAwesome.Solid.Heart,
                                        RelativeSizeAxes = Axes.Both,
                                        Anchor = Anchor.Centre,
                                        Origin = Anchor.Centre,
                                    }
                                },
                                healthText = new OsuSpriteText
                                {
                                    Text = "1,000,000",
                                    Anchor = contentAnchor,
                                    Origin = contentAnchor,
                                    Font = OsuFont.GetFont(size: 14, weight: FontWeight.Medium, fixedWidth: true),
                                    Spacing = new Vector2(-1, 0),
                                    UseFullGlyphHeight = false,
                                    Padding = new MarginPadding { Top = 1 },
                                    Shadow = false,
                                }
                            ]
                        }
                    },
                    new Container
                    {
                        RelativeSizeAxes = Axes.Both,
                        Padding = new MarginPadding { Horizontal = 2.2f, Vertical = 2 }, // slightly different ratio to account for shear
                        Children =
                        [
                            damageIndicator = new Container
                            {
                                RelativeSizeAxes = Axes.Both,
                                RelativePositionAxes = Axes.X,
                                Anchor = contentAnchor,
                                Origin = contentAnchor,
                                Masking = true,
                                CornerRadius = 2,
                                Alpha = 0,
                                EdgeEffect = new EdgeEffectParameters
                                {
                                    Type = EdgeEffectType.Glow,
                                    Radius = 25,
                                    Colour = Color4Extensions.FromHex("FF171B").Opacity(0.5f),
                                    Roundness = 10,
                                    Hollow = true,
                                },
                                Children =
                                [
                                    new Box
                                    {
                                        RelativeSizeAxes = Axes.Both,
                                    },
                                    content.CreateView().With(d =>
                                    {
                                        d.SynchronisedDrawQuad = true;
                                        d.Colour = Color4.Red;
                                    })
                                ],
                            },
                        ]
                    },
                ];
            }

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

                Health.BindValueChanged(onHealthChanged, true);

                healthTextValue.BindValueChanged(e => healthText.Text = FormattableString.Invariant($"{e.NewValue:N0}"), true);

                FinishTransforms(true);

                Scheduler.AddDelayed(flashHealth, 1000, true);
            }

#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value
            private float normalizedHealth;
            private float normalizedHealthWithDamage;
#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value

            private void onHealthChanged(ValueChangedEvent<int> e)
            {
                this.TransformBindableTo(healthTextValue, e.NewValue, 500, Easing.OutExpo);

                bool isHealthDecrease = e.NewValue < e.OldValue;

                if (isHealthDecrease)
                {
                    damageIndicator.FadeIn(50)
                                   .Then(delay: 1100)
                                   .FadeOut(200);

                    healthBarBackground.FadeColour(Color4.Red, 100)
                                       .Then()
                                       .FadeColour(healthBarColour, 1000);

                    this.TransformTo(nameof(normalizedHealthWithDamage), Health.NormalizedValue, 400, Easing.OutExpo)
                        .Then(500)
                        .TransformTo(nameof(normalizedHealth), Health.NormalizedValue, 800, Easing.OutExpo);
                }

                else
                {
                    this.TransformTo(nameof(normalizedHealthWithDamage), Health.NormalizedValue, 800, Easing.OutExpo)
                        .TransformTo(nameof(normalizedHealth), Health.NormalizedValue, 800, Easing.OutExpo);
                }
            }

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

                triangles.Width = DrawWidth;
                healthBar.Width = normalizedHealth;

                damageIndicator.X = leftToRight ? normalizedHealthWithDamage : -normalizedHealthWithDamage;
                damageIndicator.Width = float.Clamp(normalizedHealth - normalizedHealthWithDamage, 0, 1);
            }

            private void flashHealth()
            {
                if (Health.NormalizedValue > HealthFlashThreshold)
                    return;

                var almostRed = Interpolation.ValueAt(0.75, healthBarColour, ColourInfo.SingleColour(Color4.Red), 0.0, 1.0);

                healthBarBackground.FadeColour(almostRed, 150)
                                   .Then()
                                   .FadeColour(healthBarColour, 800);

                heartIcon
                    .ScaleTo(0.8f, 150, Easing.Out)
                    .Then()
                    .ScaleTo(1f, 400, Easing.OutElasticHalf);
            }
        }
    }
}
