// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;

namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
    public class OsuLegacySkinTransformer : LegacySkinTransformer
    {
        public override bool IsProvidingLegacyResources => base.IsProvidingLegacyResources || hasHitCircle.Value;

        private readonly Lazy<bool> hasHitCircle;

        /// <summary>
        /// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
        /// Their hittable area is 128px, but the actual circle portion is 118px.
        /// We must account for some gameplay elements such as slider bodies, where this padding is not present.
        /// </summary>
        public const float LEGACY_CIRCLE_RADIUS = OsuHitObject.OBJECT_RADIUS - 5;

        /// <summary>
        /// The maximum allowed size of sprites that reside in the follow circle area of a slider.
        /// </summary>
        /// <remarks>
        /// The reason this is extracted out to a constant, rather than be inlined in the follow circle sprite retrieval,
        /// is that some skins will use `sliderb` elements to emulate a slider follow circle with slightly different visual effects applied
        /// (`sliderb` is always shown and doesn't pulsate; `sliderfollowcircle` isn't always shown and pulsates).
        /// </remarks>
        public static readonly Vector2 MAX_FOLLOW_CIRCLE_AREA_SIZE = OsuHitObject.OBJECT_DIMENSIONS * 3;

        public OsuLegacySkinTransformer(ISkin skin)
            : base(skin)
        {
            hasHitCircle = new Lazy<bool>(() => GetTexture("hitcircle") != null);
        }

        public override Drawable? GetDrawableComponent(ISkinComponentLookup lookup)
        {
            switch (lookup)
            {
                case GlobalSkinnableContainerLookup containerLookup:
                    // Only handle per ruleset defaults here.
                    if (containerLookup.Ruleset == null)
                        return base.GetDrawableComponent(lookup);

                    // we don't have enough assets to display these components (this is especially the case on a "beatmap" skin).
                    if (!IsProvidingLegacyResources)
                        return null;

                    // Our own ruleset components default.
                    switch (containerLookup.Lookup)
                    {
                        case GlobalSkinnableContainers.MainHUDComponents:
                            return new DefaultSkinComponentsContainer(container =>
                            {
                                var keyCounter = container.OfType<LegacyKeyCounterDisplay>().FirstOrDefault();

                                if (keyCounter != null)
                                {
                                    // set the anchor to top right so that it won't squash to the return button to the top
                                    keyCounter.Anchor = Anchor.CentreRight;
                                    keyCounter.Origin = Anchor.TopRight;
                                    keyCounter.Position = new Vector2(0, -40) * 1.6f;
                                }

                                var combo = container.OfType<LegacyDefaultComboCounter>().FirstOrDefault();
                                var spectatorList = container.OfType<SpectatorList>().FirstOrDefault();
                                var leaderboard = container.OfType<DrawableGameplayLeaderboard>().FirstOrDefault();

                                Vector2 pos = new Vector2();

                                if (combo != null)
                                {
                                    combo.Anchor = Anchor.BottomLeft;
                                    combo.Origin = Anchor.BottomLeft;
                                    combo.Scale = new Vector2(1.28f);

                                    pos += new Vector2(10, -(combo.DrawHeight * 1.56f + 20) * combo.Scale.X);
                                }

                                if (spectatorList != null)
                                {
                                    spectatorList.Anchor = Anchor.BottomLeft;
                                    spectatorList.Origin = Anchor.BottomLeft;
                                    spectatorList.Position = pos;

                                    // maximum height of the spectator list is around ~172 units
                                    pos += new Vector2(0, -185);
                                }

                                if (leaderboard != null)
                                {
                                    leaderboard.Anchor = Anchor.BottomLeft;
                                    leaderboard.Origin = Anchor.BottomLeft;
                                    leaderboard.Position = pos;
                                }

                                foreach (var d in container.OfType<ISerialisableDrawable>())
                                    d.UsesFixedAnchor = true;
                            })
                            {
                                Children = new Drawable[]
                                {
                                    new LegacyDefaultComboCounter(),
                                    new LegacyKeyCounterDisplay(),
                                    new SpectatorList(),
                                    new DrawableGameplayLeaderboard(),
                                }
                            };
                    }

                    return null;

                case SkinComponentLookup<HitResult> resultComponent:
                    switch (resultComponent.Component)
                    {
                        case HitResult.LargeTickHit:
                        case HitResult.SliderTailHit:
                            if (getSliderPointTexture(resultComponent.Component) is Texture texture)
                                return new LegacyJudgementPieceSliderTickHit { Texture = texture };

                            break;

                        // If the corresponding hit result displays a judgement and the miss texture isn't provided by this skin, don't look up the miss texture from any further skins.
                        case HitResult.LargeTickMiss:
                        case HitResult.IgnoreMiss:
                            if (getSliderPointTexture(resultComponent.Component == HitResult.LargeTickMiss
                                    ? HitResult.LargeTickHit
                                    : HitResult.SliderTailHit) != null)
                                return base.GetDrawableComponent(lookup) ?? Drawable.Empty();

                            break;
                    }

                    return base.GetDrawableComponent(lookup);

                    Texture? getSliderPointTexture(HitResult result)
                    {
                        // https://github.com/peppy/osu-stable-reference/blob/0e91e49bc83fe8b21c3ba5f1eb2d5d06456eae84/osu!/GameModes/Play/Rulesets/Ruleset.cs#L799
                        if (GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version)?.Value < 2m)
                            // Note that osu!stable used sliderpoint30 for heads and repeats, and sliderpoint10 for ticks, but the mapping is intentionally changed here so that each texture represents one type of HitResult.
                            return GetTexture(result == HitResult.LargeTickHit ? "sliderpoint30" : "sliderpoint10");

                        return null;
                    }

                case OsuSkinComponentLookup osuComponent:
                    switch (osuComponent.Component)
                    {
                        case OsuSkinComponents.FollowPoint:
                            return this.GetAnimation("followpoint", true, true, true, startAtCurrentTime: false,
                                maxSize: new Vector2(OsuHitObject.OBJECT_RADIUS * 2, OsuHitObject.OBJECT_RADIUS));

                        case OsuSkinComponents.SliderScorePoint:
                            return this.GetAnimation("sliderscorepoint", false, false, maxSize: OsuHitObject.OBJECT_DIMENSIONS);

                        case OsuSkinComponents.SliderFollowCircle:
                            var followCircleContent = this.GetAnimation("sliderfollowcircle", true, true, true, maxSize: MAX_FOLLOW_CIRCLE_AREA_SIZE);
                            if (followCircleContent != null)
                                return new LegacyFollowCircle(followCircleContent);

                            return null;

                        case OsuSkinComponents.SliderBall:
                            if (GetTexture("sliderb") != null || GetTexture("sliderb0") != null)
                                return new LegacySliderBall(this);

                            return null;

                        case OsuSkinComponents.SliderBody:
                            if (hasHitCircle.Value)
                                return new LegacySliderBody();

                            return null;

                        case OsuSkinComponents.SliderTailHitCircle:
                            if (hasHitCircle.Value)
                                return new LegacyMainCirclePiece("sliderendcircle", false);

                            return null;

                        case OsuSkinComponents.SliderHeadHitCircle:
                            if (hasHitCircle.Value)
                                return new LegacySliderHeadHitCircle();

                            return null;

                        case OsuSkinComponents.ReverseArrow:
                            if (hasHitCircle.Value)
                                return new LegacyReverseArrow();

                            return null;

                        case OsuSkinComponents.HitCircle:
                            if (hasHitCircle.Value)
                                return new LegacyMainCirclePiece();

                            return null;

                        case OsuSkinComponents.Cursor:
                            if (GetTexture("cursor") != null)
                                return new LegacyCursor(this);

                            return null;

                        case OsuSkinComponents.CursorTrail:
                            if (GetTexture("cursortrail") != null)
                                return new LegacyCursorTrail(this);

                            return null;

                        case OsuSkinComponents.CursorRipple:
                            if (GetTexture("cursor-ripple") != null)
                            {
                                var ripple = this.GetAnimation("cursor-ripple", false, false);

                                // In stable this element was scaled down to 50% and opacity 20%, but this makes the elements WAY too big and inflexible.
                                // If anyone complains about these not being applied, this can be uncommented.
                                //
                                // But if no one complains I'd rather fix this in lazer. Wiki documentation doesn't mention size,
                                // so we might be okay.
                                //
                                // if (ripple != null)
                                // {
                                //     ripple.Scale = new Vector2(0.5f);
                                //     ripple.Alpha = 0.2f;
                                // }

                                return ripple;
                            }

                            return null;

                        case OsuSkinComponents.CursorParticles:
                            if (GetTexture("star2") != null)
                                return new LegacyCursorParticles();

                            return null;

                        case OsuSkinComponents.CursorSmoke:
                            if (GetTexture("cursor-smoke") != null)
                                return new LegacySmokeSegment();

                            return null;

                        case OsuSkinComponents.HitCircleText:
                            if (!this.HasFont(LegacyFont.HitCircle))
                                return null;

                            const float hitcircle_text_scale = 0.8f;
                            return new LegacySpriteText(LegacyFont.HitCircle)
                            {
                                // stable applies a blanket 0.8x scale to hitcircle fonts
                                Scale = new Vector2(hitcircle_text_scale),
                                MaxSizePerGlyph = OsuHitObject.OBJECT_DIMENSIONS * 2 / hitcircle_text_scale,
                            };

                        case OsuSkinComponents.SpinnerBody:
                            bool hasBackground = GetTexture("spinner-background") != null;

                            if (GetTexture("spinner-top") != null && !hasBackground)
                                return new LegacyNewStyleSpinner();
                            else if (hasBackground)
                                return new LegacyOldStyleSpinner();

                            return null;

                        case OsuSkinComponents.ApproachCircle:
                            if (GetTexture(@"approachcircle") != null)
                                return new LegacyApproachCircle();

                            return null;

                        default:
                            throw new UnsupportedSkinComponentException(lookup);
                    }

                default:
                    return base.GetDrawableComponent(lookup);
            }
        }

        public override IBindable<TValue>? GetConfig<TLookup, TValue>(TLookup lookup)
        {
            switch (lookup)
            {
                case OsuSkinColour colour:
                    return base.GetConfig<SkinCustomColourLookup, TValue>(new SkinCustomColourLookup(colour));

                case OsuSkinConfiguration osuLookup:
                    switch (osuLookup)
                    {
                        case OsuSkinConfiguration.SliderPathRadius:
                            if (hasHitCircle.Value)
                                return SkinUtils.As<TValue>(new BindableFloat(LEGACY_CIRCLE_RADIUS));

                            break;

                        case OsuSkinConfiguration.HitCircleOverlayAboveNumber:
                            // See https://osu.ppy.sh/help/wiki/Skinning/skin.ini#%5Bgeneral%5D
                            // HitCircleOverlayAboveNumer (with typo) should still be supported for now.
                            return base.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ??
                                   base.GetConfig<OsuSkinConfiguration, TValue>(OsuSkinConfiguration.HitCircleOverlayAboveNumer);
                    }

                    break;
            }

            return base.GetConfig<TLookup, TValue>(lookup);
        }
    }
}
