// 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 osu.Framework.Bindables;
using osu.Game.Beatmaps;

namespace osu.Game.Rulesets.Mods
{
    public class DifficultyBindable : Bindable<float?>
    {
        /// <summary>
        /// Whether the extended limits should be applied to this bindable.
        /// </summary>
        public readonly BindableBool ExtendedLimits = new BindableBool();

        /// <summary>
        /// An internal numeric bindable to hold and propagate min/max/precision.
        /// The value of this bindable should not be set.
        /// </summary>
        internal readonly BindableFloat CurrentNumber = new BindableFloat
        {
            MinValue = 0,
            MaxValue = 10,
        };

        /// <summary>
        /// A function that can extract the current value of this setting from a beatmap difficulty for display purposes.
        /// </summary>
        public Func<BeatmapDifficulty, float> ReadCurrentFromDifficulty;

        public float Precision
        {
            set => CurrentNumber.Precision = value;
        }

        public float MinValue
        {
            set => CurrentNumber.MinValue = value;
        }

        private float maxValue;

        public float MaxValue
        {
            set
            {
                if (value == maxValue)
                    return;

                maxValue = value;
                updateMaxValue();
            }
        }

        private float? extendedMaxValue;

        /// <summary>
        /// The maximum value to be used when extended limits are applied.
        /// </summary>
        public float? ExtendedMaxValue
        {
            set
            {
                if (value == extendedMaxValue)
                    return;

                extendedMaxValue = value;
                updateMaxValue();
            }
        }

        public DifficultyBindable()
            : this(null)
        {
        }

        public DifficultyBindable(float? defaultValue = null)
            : base(defaultValue)
        {
            ExtendedLimits.BindValueChanged(_ => updateMaxValue());
        }

        public override float? Value
        {
            get => base.Value;
            set
            {
                // Ensure that in the case serialisation runs in the wrong order (and limit extensions aren't applied yet) the deserialised value is still propagated.
                if (value != null)
                    CurrentNumber.MaxValue = MathF.Max(CurrentNumber.MaxValue, value.Value);

                base.Value = value;
            }
        }

        private void updateMaxValue()
        {
            CurrentNumber.MaxValue = ExtendedLimits.Value && extendedMaxValue != null ? extendedMaxValue.Value : maxValue;
        }

        public override void BindTo(Bindable<float?> them)
        {
            if (!(them is DifficultyBindable otherDifficultyBindable))
                throw new InvalidOperationException($"Cannot bind to a non-{nameof(DifficultyBindable)}.");

            ReadCurrentFromDifficulty = otherDifficultyBindable.ReadCurrentFromDifficulty;

            // the following max value copies are only safe as long as these values are effectively constants.
            MaxValue = otherDifficultyBindable.maxValue;
            ExtendedMaxValue = otherDifficultyBindable.extendedMaxValue;

            ExtendedLimits.BindTarget = otherDifficultyBindable.ExtendedLimits;

            // the actual values need to be copied after the max value constraints.
            CurrentNumber.BindTarget = otherDifficultyBindable.CurrentNumber;
            base.BindTo(them);
        }

        public override void UnbindFrom(IUnbindable them)
        {
            if (!(them is DifficultyBindable otherDifficultyBindable))
                throw new InvalidOperationException($"Cannot unbind from a non-{nameof(DifficultyBindable)}.");

            base.UnbindFrom(them);

            CurrentNumber.UnbindFrom(otherDifficultyBindable.CurrentNumber);
            ExtendedLimits.UnbindFrom(otherDifficultyBindable.ExtendedLimits);
        }

        protected override Bindable<float?> CreateInstance() => new DifficultyBindable();
    }
}