<commit_before>﻿// Copyright (c) ppy Pty Ltd <<EMAIL>>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK.Graphics;

namespace osu.Game.Rulesets.Objects.Drawables
{
    [Cached(typeof(DrawableHitObject))]
    public abstract class DrawableHitObject : SkinReloadableDrawable
    {
        public readonly HitObject HitObject;

        /// <summary>
        /// The colour used for various elements of this DrawableHitObject.
        /// </summary>
        public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(Color4.Gray);

        protected SkinnableSound Samples { get; private set; }

        protected virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples;

        private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>();
        public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList<DrawableHitObject>)Array.Empty<DrawableHitObject>();

        /// <summary>
        /// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
        /// </summary>
        public event Action<DrawableHitObject, JudgementResult> OnNewResult;

        /// <summary>
        /// Invoked when a <see cref="JudgementResult"/> is being reverted by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
        /// </summary>
        public event Action<DrawableHitObject, JudgementResult> OnRevertResult;

        /// <summary>
        /// Whether a visual indicator should be displayed when a scoring result occurs.
        /// </summary>
        public virtual bool DisplayResult => true;

        /// <summary>
        /// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
        /// </summary>
        public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged);

        /// <summary>
        /// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result"/> is hit.
        /// Note: This does NOT include nested hitobjects.
        /// </summary>
        public bool IsHit => Result?.IsHit ?? false;

        /// <summary>
        /// Whether this <see cref="DrawableHitObject"/> has been judged.
        /// Note: This does NOT include nested hitobjects.
        /// </summary>
        public bool Judged => Result?.HasResult ?? true;

        /// <summary>
        /// The scoring result of this <see cref="DrawableHitObject"/>.
        /// </summary>
        public JudgementResult Result { get; private set; }

        private BindableList<HitSampleInfo> samplesBindable;
        private Bindable<double> startTimeBindable;
        private Bindable<int> comboIndexBindable;

        public override bool RemoveWhenNotAlive => false;
        public override bool RemoveCompletedTransforms => false;
        protected override bool RequiresChildrenUpdate => true;

        public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart);

        private readonly Bindable<ArmedState> state = new Bindable<ArmedState>();

        public IBindable<ArmedState> State => state;

        protected DrawableHitObject([NotNull] HitObject hitObject)
        {
            HitObject = hitObject ?? throw new ArgumentNullException(nameof(hitObject));
        }

        [BackgroundDependencyLoader]
        private void load()
        {
            var judgement = HitObject.CreateJudgement();

            Result = CreateResult(judgement);
            if (Result == null)
                throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");

            loadSamples();
        }

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

            HitObject.DefaultsApplied += onDefaultsApplied;

            startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
            startTimeBindable.BindValueChanged(_ => updateState(State.Value, true));

            if (HitObject is IHasComboInformation combo)
            {
                comboIndexBindable = combo.ComboIndexBindable.GetBoundCopy();
                comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
            }

            samplesBindable = HitObject.SamplesBindable.GetBoundCopy();
            samplesBindable.CollectionChanged += (_, __) => loadSamples();

            updateState(ArmedState.Idle, true);
            onDefaultsApplied();
        }

        private void loadSamples()
        {
            if (Samples != null)
            {
                RemoveInternal(Samples);
                Samples = null;
            }

            var samples = GetSamples().ToArray();

            if (samples.Length <= 0)
                return;

            if (HitObject.SampleControlPoint == null)
            {
                throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
                                                    + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
            }

            AddInternal(Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))));
        }

        private void onDefaultsApplied() => apply(HitObject);

        private void apply(HitObject hitObject)
        {
#pragma warning disable 618 // can be removed 20200417
            if (GetType().GetMethod(nameof(AddNested), BindingFlags.NonPublic | BindingFlags.Instance)?.DeclaringType != typeof(DrawableHitObject))
                return;
#pragma warning restore 618

            if (nestedHitObjects.IsValueCreated)
            {
                nestedHitObjects.Value.Clear();
                ClearNestedHitObjects();
            }

            foreach (var h in hitObject.NestedHitObjects)
            {
                var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");

                addNested(drawableNested);
                AddNestedHitObject(drawableNested);
            }
        }

        /// <summary>
        /// Invoked by the base <see cref="DrawableHitObject"/> to add nested <see cref="DrawableHitObject"/>s to the hierarchy.
        /// </summary>
        /// <param name="hitObject">The <see cref="DrawableHitObject"/> to be added.</param>
        protected virtual void AddNestedHitObject(DrawableHitObject hitObject)
        {
        }

        /// <summary>
        /// Adds a nested <see cref="DrawableHitObject"/>. This should not be used except for legacy nested <see cref="DrawableHitObject"/> usages.
        /// </summary>
        /// <param name="h"></param>
        [Obsolete("Use AddNestedHitObject() / ClearNestedHitObjects() / CreateNestedHitObject() instead.")] // can be removed 20200417
        protected virtual void AddNested(DrawableHitObject h) => addNested(h);

        /// <summary>
        /// Invoked by the base <see cref="DrawableHitObject"/> to remove all previously-added nested <see cref="DrawableHitObject"/>s.
        /// </summary>
        protected virtual void ClearNestedHitObjects()
        {
        }

        /// <summary>
        /// Creates the drawable representation for a nested <see cref="HitObject"/>.
        /// </summary>
        /// <param name="hitObject">The <see cref="HitObject"/>.</param>
        /// <returns>The drawable representation for <paramref name="hitObject"/>.</returns>
        protected virtual DrawableHitObject CreateNestedHitObject(HitObject hitObject) => null;

        private void addNested(DrawableHitObject hitObject)
        {
            // Todo: Exists for legacy purposes, can be removed 20200417

            hitObject.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r);
            hitObject.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r);
            hitObject.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j);

            nestedHitObjects.Value.Add(hitObject);
        }

        #region State / Transform Management

        /// <summary>
        /// Bind to apply a custom state which can override the default implementation.
        /// </summary>
        public event Action<DrawableHitObject, ArmedState> ApplyCustomUpdateState;

#pragma warning disable 618 // (legacy state management) - can be removed 20200227

        /// <summary>
        /// Enables automatic transform management of this hitobject. Implementation of transforms should be done in <see cref="UpdateInitialTransforms"/> and <see cref="UpdateStateTransforms"/> only. Rewinding and removing previous states is done automatically.
        /// </summary>
        /// <remarks>
        /// Going forward, this is the preferred way of implementing <see cref="DrawableHitObject"/>s. Previous functionality
        /// is offered as a compatibility layer until all rulesets have been migrated across.
        /// </remarks>
        [Obsolete("Use UpdateInitialTransforms()/UpdateStateTransforms() instead")] // can be removed 20200227
        protected virtual bool UseTransformStateManagement => true;

        protected override void ClearInternal(bool disposeChildren = true) => throw new InvalidOperationException($"Should never clear a {nameof(DrawableHitObject)}");

        private void updateState(ArmedState newState, bool force = false)
        {
            if (State.Value == newState && !force)
                return;

            if (UseTransformStateManagement)
            {
                LifetimeEnd = double.MaxValue;

                double transformTime = HitObject.StartTime - InitialLifetimeOffset;

                base.ApplyTransformsAt(double.MinValue, true);
                base.ClearTransformsAfter(double.MinValue, true);

                using (BeginAbsoluteSequence(transformTime, true))
                {
                    UpdateInitialTransforms();

                    var judgementOffset = Result?.TimeOffset ?? 0;

                    using (BeginDelayedSequence(InitialLifetimeOffset + judgementOffset, true))
                    {
                        UpdateStateTransforms(newState);
                        state.Value = newState;
                    }
                }

                if (state.Value != ArmedState.Idle && LifetimeEnd == double.MaxValue || HitObject.HitWindows == null)
                    Expire();
            }
            else
                state.Value = newState;

            UpdateState(newState);

            // apply any custom state overrides
            ApplyCustomUpdateState?.Invoke(this, newState);

            if (newState == ArmedState.Hit)
                PlaySamples();
        }

        /// <summary>
        /// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.
        /// The local drawable hierarchy is recursively delayed to <see cref="LifetimeStart"/> for convenience.
        ///
        /// By default this will fade in the object from zero with no duration.
        /// </summary>
        /// <remarks>
        /// This is called once before every <see cref="UpdateStateTransforms"/>. This is to ensure a good state in the case
        /// the <see cref="JudgementResult.TimeOffset"/> was negative and potentially altered the pre-hit transforms.
        /// </remarks>
        protected virtual void UpdateInitialTransforms()
        {
            this.FadeInFromZero();
        }

        /// <summary>
        /// Apply transforms based on the current <see cref="ArmedState"/>. Previous states are automatically cleared.
        /// In the case of a non-idle <see cref="ArmedState"/>, and if <see cref="Drawable.LifetimeEnd"/> was not set during this call, <see cref="Drawable.Expire"/> will be invoked.
        /// </summary>
        /// <param name="state">The new armed state.</param>
        protected virtual void UpdateStateTransforms(ArmedState state)
        {
        }

        public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null)
        {
            // When we are using automatic state management, parent calls to this should be blocked for safety.
            if (!UseTransformStateManagement)
                base.ClearTransformsAfter(time, propagateChildren, targetMember);
        }

        public override void ApplyTransformsAt(double time, bool propagateChildren = false)
        {
            // When we are using automatic state management, parent calls to this should be blocked for safety.
            if (!UseTransformStateManagement)
                base.ApplyTransformsAt(time, propagateChildren);
        }

        /// <summary>
        /// Legacy method to handle state changes.
        /// Should generally not be used when <see cref="UseTransformStateManagement"/> is true; use <see cref="UpdateStateTransforms"/> instead.
        /// </summary>
        /// <param name="state">The new armed state.</param>
        [Obsolete("Use UpdateInitialTransforms()/UpdateStateTransforms() instead")] // can be removed 20200227
        protected virtual void UpdateState(ArmedState state)
        {
        }

#pragma warning restore 618
<commit_msg>Remove legacy DrawableHitObject state management<commit_after>﻿// Copyright (c) p<NAME> <<EMAIL>>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using osuTK.Graphics;

namespace osu.Game.Rulesets.Objects.Drawables
{
    [Cached(typeof(DrawableHitObject))]
    public abstract class DrawableHitObject : SkinReloadableDrawable
    {
        public readonly HitObject HitObject;

        /// <summary>
        /// The colour used for various elements of this DrawableHitObject.
        /// </summary>
        public readonly Bindable<Color4> AccentColour = new Bindable<Color4>(Color4.Gray);

        protected SkinnableSound Samples { get; private set; }

        protected virtual IEnumerable<HitSampleInfo> GetSamples() => HitObject.Samples;

        private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>();
        public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects.IsValueCreated ? nestedHitObjects.Value : (IReadOnlyList<DrawableHitObject>)Array.Empty<DrawableHitObject>();

        /// <summary>
        /// Invoked when a <see cref="JudgementResult"/> has been applied by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
        /// </summary>
        public event Action<DrawableHitObject, JudgementResult> OnNewResult;

        /// <summary>
        /// Invoked when a <see cref="JudgementResult"/> is being reverted by this <see cref="DrawableHitObject"/> or a nested <see cref="DrawableHitObject"/>.
        /// </summary>
        public event Action<DrawableHitObject, JudgementResult> OnRevertResult;

        /// <summary>
        /// Whether a visual indicator should be displayed when a scoring result occurs.
        /// </summary>
        public virtual bool DisplayResult => true;

        /// <summary>
        /// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
        /// </summary>
        public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged);

        /// <summary>
        /// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result"/> is hit.
        /// Note: This does NOT include nested hitobjects.
        /// </summary>
        public bool IsHit => Result?.IsHit ?? false;

        /// <summary>
        /// Whether this <see cref="DrawableHitObject"/> has been judged.
        /// Note: This does NOT include nested hitobjects.
        /// </summary>
        public bool Judged => Result?.HasResult ?? true;

        /// <summary>
        /// The scoring result of this <see cref="DrawableHitObject"/>.
        /// </summary>
        public JudgementResult Result { get; private set; }

        private BindableList<HitSampleInfo> samplesBindable;
        private Bindable<double> startTimeBindable;
        private Bindable<int> comboIndexBindable;

        public override bool RemoveWhenNotAlive => false;
        public override bool RemoveCompletedTransforms => false;
        protected override bool RequiresChildrenUpdate => true;

        public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart);

        private readonly Bindable<ArmedState> state = new Bindable<ArmedState>();

        public IBindable<ArmedState> State => state;

        protected DrawableHitObject([NotNull] HitObject hitObject)
        {
            HitObject = hitObject ?? throw new ArgumentNullException(nameof(hitObject));
        }

        [BackgroundDependencyLoader]
        private void load()
        {
            var judgement = HitObject.CreateJudgement();

            Result = CreateResult(judgement);
            if (Result == null)
                throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");

            loadSamples();
        }

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

            HitObject.DefaultsApplied += onDefaultsApplied;

            startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
            startTimeBindable.BindValueChanged(_ => updateState(State.Value, true));

            if (HitObject is IHasComboInformation combo)
            {
                comboIndexBindable = combo.ComboIndexBindable.GetBoundCopy();
                comboIndexBindable.BindValueChanged(_ => updateComboColour(), true);
            }

            samplesBindable = HitObject.SamplesBindable.GetBoundCopy();
            samplesBindable.CollectionChanged += (_, __) => loadSamples();

            updateState(ArmedState.Idle, true);
            onDefaultsApplied();
        }

        private void loadSamples()
        {
            if (Samples != null)
            {
                RemoveInternal(Samples);
                Samples = null;
            }

            var samples = GetSamples().ToArray();

            if (samples.Length <= 0)
                return;

            if (HitObject.SampleControlPoint == null)
            {
                throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
                                                    + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
            }

            AddInternal(Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))));
        }

        private void onDefaultsApplied() => apply(HitObject);

        private void apply(HitObject hitObject)
        {
#pragma warning disable 618 // can be removed 20200417
            if (GetType().GetMethod(nameof(AddNested), BindingFlags.NonPublic | BindingFlags.Instance)?.DeclaringType != typeof(DrawableHitObject))
                return;
#pragma warning restore 618

            if (nestedHitObjects.IsValueCreated)
            {
                nestedHitObjects.Value.Clear();
                ClearNestedHitObjects();
            }

            foreach (var h in hitObject.NestedHitObjects)
            {
                var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}.");

                addNested(drawableNested);
                AddNestedHitObject(drawableNested);
            }
        }

        /// <summary>
        /// Invoked by the base <see cref="DrawableHitObject"/> to add nested <see cref="DrawableHitObject"/>s to the hierarchy.
        /// </summary>
        /// <param name="hitObject">The <see cref="DrawableHitObject"/> to be added.</param>
        protected virtual void AddNestedHitObject(DrawableHitObject hitObject)
        {
        }

        /// <summary>
        /// Adds a nested <see cref="DrawableHitObject"/>. This should not be used except for legacy nested <see cref="DrawableHitObject"/> usages.
        /// </summary>
        /// <param name="h"></param>
        [Obsolete("Use AddNestedHitObject() / ClearNestedHitObjects() / CreateNestedHitObject() instead.")] // can be removed 20200417
        protected virtual void AddNested(DrawableHitObject h) => addNested(h);

        /// <summary>
        /// Invoked by the base <see cref="DrawableHitObject"/> to remove all previously-added nested <see cref="DrawableHitObject"/>s.
        /// </summary>
        protected virtual void ClearNestedHitObjects()
        {
        }

        /// <summary>
        /// Creates the drawable representation for a nested <see cref="HitObject"/>.
        /// </summary>
        /// <param name="hitObject">The <see cref="HitObject"/>.</param>
        /// <returns>The drawable representation for <paramref name="hitObject"/>.</returns>
        protected virtual DrawableHitObject CreateNestedHitObject(HitObject hitObject) => null;

        private void addNested(DrawableHitObject hitObject)
        {
            // Todo: Exists for legacy purposes, can be removed 20200417

            hitObject.OnNewResult += (d, r) => OnNewResult?.Invoke(d, r);
            hitObject.OnRevertResult += (d, r) => OnRevertResult?.Invoke(d, r);
            hitObject.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j);

            nestedHitObjects.Value.Add(hitObject);
        }

        #region State / Transform Management

        /// <summary>
        /// Bind to apply a custom state which can override the default implementation.
        /// </summary>
        public event Action<DrawableHitObject, ArmedState> ApplyCustomUpdateState;

        protected override void ClearInternal(bool disposeChildren = true) => throw new InvalidOperationException($"Should never clear a {nameof(DrawableHitObject)}");

        private void updateState(ArmedState newState, bool force = false)
        {
            if (State.Value == newState && !force)
                return;

            LifetimeEnd = double.MaxValue;

            double transformTime = HitObject.StartTime - InitialLifetimeOffset;

            base.ApplyTransformsAt(double.MinValue, true);
            base.ClearTransformsAfter(double.MinValue, true);

            using (BeginAbsoluteSequence(transformTime, true))
            {
                UpdateInitialTransforms();

                var judgementOffset = Result?.TimeOffset ?? 0;

                using (BeginDelayedSequence(InitialLifetimeOffset + judgementOffset, true))
                {
                    UpdateStateTransforms(newState);
                    state.Value = newState;
                }
            }

            if (state.Value != ArmedState.Idle && LifetimeEnd == double.MaxValue || HitObject.HitWindows == null)
                Expire();

            // apply any custom state overrides
            ApplyCustomUpdateState?.Invoke(this, newState);

            if (newState == ArmedState.Hit)
                PlaySamples();
        }

        /// <summary>
        /// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.
        /// The local drawable hierarchy is recursively delayed to <see cref="LifetimeStart"/> for convenience.
        ///
        /// By default this will fade in the object from zero with no duration.
        /// </summary>
        /// <remarks>
        /// This is called once before every <see cref="UpdateStateTransforms"/>. This is to ensure a good state in the case
        /// the <see cref="JudgementResult.TimeOffset"/> was negative and potentially altered the pre-hit transforms.
        /// </remarks>
        protected virtual void UpdateInitialTransforms()
        {
            this.FadeInFromZero();
        }

        /// <summary>
        /// Apply transforms based on the current <see cref="ArmedState"/>. Previous states are automatically cleared.
        /// In the case of a non-idle <see cref="ArmedState"/>, and if <see cref="Drawable.LifetimeEnd"/> was not set during this call, <see cref="Drawable.Expire"/> will be invoked.
        /// </summary>
        /// <param name="state">The new armed state.</param>
        protected virtual void UpdateStateTransforms(ArmedState state)
        {
        }

        public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null)
        {
            // Parent calls to this should be blocked for safety, as we are manually handling this in updateState.
        }

        public override void ApplyTransformsAt(double time, bool propagateChildren = false)
        {
            // Parent calls to this should be blocked for safety, as we are manually handling this in updateState.
        }

{'fraction_non_alphanumeric': 0.10329920364050058, 'fraction_numerical': 0.0037921880925293893, 'mean_word_length': 2.3713883917156737, 'pattern_counts': {'":': 0, '<': 205, '<?xml version=': 0, '>': 252, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3105241', 'n_tokens_mistral': 6743, 'n_tokens_neox': 6423, 'n_words': 1865}