// 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.

#nullable disable

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;

namespace osu.Game.Tests.Visual.Gameplay
{
    public class TestSceneSkinnableDrawable : OsuTestScene
    {
        [Test]
        public void TestConfineScaleDown()
        {
            FillFlowContainer<ExposedSkinnableDrawable> fill = null;

            AddStep("setup layout larger source", () =>
            {
                Child = new SkinProvidingContainer(new SizedSource(50))
                {
                    RelativeSizeAxes = Axes.Both,
                    Child = fill = new FillFlowContainer<ExposedSkinnableDrawable>
                    {
                        Size = new Vector2(30),
                        Anchor = Anchor.Centre,
                        Origin = Anchor.Centre,
                        Spacing = new Vector2(10),
                        Children = new[]
                        {
                            new ExposedSkinnableDrawable("default", _ => new DefaultBox()),
                            new ExposedSkinnableDrawable("available", _ => new DefaultBox()),
                            new ExposedSkinnableDrawable("available", _ => new DefaultBox(), ConfineMode.NoScaling)
                        }
                    },
                };
            });

            AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 50 }));
            AddStep("adjust scale", () => fill.Scale = new Vector2(2));
            AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 30, 30, 50 }));
        }

        [Test]
        public void TestConfineScaleUp()
        {
            FillFlowContainer<ExposedSkinnableDrawable> fill = null;

            AddStep("setup layout larger source", () =>
            {
                Child = new SkinProvidingContainer(new SizedSource(30))
                {
                    RelativeSizeAxes = Axes.Both,
                    Child = fill = new FillFlowContainer<ExposedSkinnableDrawable>
                    {
                        Size = new Vector2(50),
                        Anchor = Anchor.Centre,
                        Origin = Anchor.Centre,
                        Spacing = new Vector2(10),
                        Children = new[]
                        {
                            new ExposedSkinnableDrawable("default", _ => new DefaultBox()),
                            new ExposedSkinnableDrawable("available", _ => new DefaultBox()),
                            new ExposedSkinnableDrawable("available", _ => new DefaultBox(), ConfineMode.NoScaling)
                        }
                    },
                };
            });

            AddAssert("check sizes", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 50, 30 }));
            AddStep("adjust scale", () => fill.Scale = new Vector2(2));
            AddAssert("check sizes unchanged by scale", () => fill.Children.Select(c => c.Drawable.DrawWidth).SequenceEqual(new float[] { 50, 50, 30 }));
        }

        [Test]
        public void TestInitialLoad()
        {
            var secondarySource = new SecondarySource();
            SkinConsumer consumer = null;

            AddStep("setup layout", () =>
            {
                Child = new SkinSourceContainer
                {
                    RelativeSizeAxes = Axes.Both,
                    Child = new SkinProvidingContainer(secondarySource)
                    {
                        RelativeSizeAxes = Axes.Both,
                        Child = consumer = new SkinConsumer("test", _ => new NamedBox("Default Implementation"))
                    }
                };
            });

            AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox);
            AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1);
        }

        [Test]
        public void TestOverride()
        {
            var secondarySource = new SecondarySource();

            SkinConsumer consumer = null;
            Container target = null;

            AddStep("setup layout", () =>
            {
                Child = new SkinSourceContainer
                {
                    RelativeSizeAxes = Axes.Both,
                    Child = target = new SkinProvidingContainer(secondarySource)
                    {
                        RelativeSizeAxes = Axes.Both,
                    }
                };
            });

            AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", _ => new NamedBox("Default Implementation"))));
            AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox);
            AddAssert("skinchanged only called once", () => consumer.SkinChangedCount == 1);
        }

        [Test]
        public void TestSwitchOff()
        {
            SkinConsumer consumer = null;
            SwitchableSkinProvidingContainer target = null;

            AddStep("setup layout", () =>
            {
                Child = new SkinSourceContainer
                {
                    RelativeSizeAxes = Axes.Both,
                    Child = target = new SwitchableSkinProvidingContainer(new SecondarySource())
                    {
                        RelativeSizeAxes = Axes.Both,
                    }
                };
            });

            AddStep("add permissive", () => target.Add(consumer = new SkinConsumer("test", _ => new NamedBox("Default Implementation"))));
            AddAssert("consumer using override source", () => consumer.Drawable is SecondarySourceBox);
            AddStep("disable", () => target.Disable());
            AddAssert("consumer using base source", () => consumer.Drawable is BaseSourceBox);
        }

        private class SwitchableSkinProvidingContainer : SkinProvidingContainer
        {
            private bool allow = true;

            protected override bool AllowDrawableLookup(ISkinComponent component) => allow;

            public void Disable()
            {
                allow = false;
                TriggerSourceChanged();
            }

            public SwitchableSkinProvidingContainer(ISkin skin)
                : base(skin)
            {
            }
        }

        private class ExposedSkinnableDrawable : SkinnableDrawable
        {
            public new Drawable Drawable => base.Drawable;

            public ExposedSkinnableDrawable(string name, Func<ISkinComponent, Drawable> defaultImplementation, ConfineMode confineMode = ConfineMode.ScaleToFit)
                : base(new TestSkinComponent(name), defaultImplementation, confineMode)
            {
            }
        }

        private class DefaultBox : DrawWidthBox
        {
            public DefaultBox()
            {
                RelativeSizeAxes = Axes.Both;
            }
        }

        private class DrawWidthBox : Container
        {
            private readonly OsuSpriteText text;

            public DrawWidthBox()
            {
                Children = new Drawable[]
                {
                    new Box
                    {
                        Colour = Color4.Gray,
                        RelativeSizeAxes = Axes.Both,
                    },
                    text = new OsuSpriteText
                    {
                        Anchor = Anchor.Centre,
                        Origin = Anchor.Centre,
                    }
                };
            }

            protected override void UpdateAfterChildren()
            {
                base.UpdateAfterChildren();
                text.Text = DrawWidth.ToString(CultureInfo.InvariantCulture);
            }
        }

        private class NamedBox : Container
        {
            public NamedBox(string name)
            {
                Children = new Drawable[]
                {
                    new Box
                    {
                        Colour = Color4.Black,
                        RelativeSizeAxes = Axes.Both,
                    },
                    new OsuSpriteText
                    {
                        Font = OsuFont.Default.With(size: 40),
                        Anchor = Anchor.Centre,
                        Origin = Anchor.Centre,
                        Text = name
                    }
                };
            }
        }

        private class SkinConsumer : SkinnableDrawable
        {
            public new Drawable Drawable => base.Drawable;
            public int SkinChangedCount { get; private set; }

            public SkinConsumer(string name, Func<ISkinComponent, Drawable> defaultImplementation)
                : base(new TestSkinComponent(name), defaultImplementation)
            {
            }

            protected override void SkinChanged(ISkinSource skin)
            {
                base.SkinChanged(skin);
                SkinChangedCount++;
            }
        }

        private class BaseSourceBox : NamedBox
        {
            public BaseSourceBox()
                : base("Base Source")
            {
            }
        }

        private class SecondarySourceBox : NamedBox
        {
            public SecondarySourceBox()
                : base("Secondary Source")
            {
            }
        }

        private class SizedSource : ISkin
        {
            private readonly float size;

            public SizedSource(float size)
            {
                this.size = size;
            }

            public Drawable GetDrawableComponent(ISkinComponent componentName) =>
                componentName.LookupName == "available"
                    ? new DrawWidthBox
                    {
                        Colour = Color4.Yellow,
                        Size = new Vector2(size)
                    }
                    : null;

            public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();

            public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();

            public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
        }

        private class SecondarySource : ISkin
        {
            public Drawable GetDrawableComponent(ISkinComponent componentName) => new SecondarySourceBox();

            public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();

            public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();

            public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
        }

        [Cached(typeof(ISkinSource))]
        private class SkinSourceContainer : Container, ISkinSource
        {
            public Drawable GetDrawableComponent(ISkinComponent componentName) => new BaseSourceBox();

            public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();

            public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();

            public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();

            public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => throw new NotImplementedException();

            public IEnumerable<ISkin> AllSources => throw new NotImplementedException();

            public event Action SourceChanged
            {
                add { }
                remove { }
            }
        }

        private class TestSkinComponent : ISkinComponent
        {
            public TestSkinComponent(string name)
            {
                LookupName = name;
            }

            public string ComponentGroup => string.Empty;

            public string LookupName { get; }
        }
    }
}