using RhythmBase.Exceptions; using RhythmBase.Utils; using System.Diagnostics.CodeAnalysis; using System.Numerics; namespace RhythmBase.Components { /// /// A beat. /// public struct RDBeat : IComparable, IEquatable, IComparisonOperators { internal readonly RDLevel? BaseLevel => _calculator?.Collection; /// /// Whether this beat cannot be calculated. /// [MemberNotNullWhen(false, nameof(_calculator))] public readonly bool IsEmpty => _calculator == null || (!_isBeatLoaded && !_isBarBeatLoaded && !_isTimeSpanLoaded); /// /// The total number of beats from this moment to the beginning of the level. /// public float BeatOnly { get { IfNullThrowException(); if (!_isBeatLoaded) { if (_isBarBeatLoaded) _beat = _calculator!.BarBeatToBeatOnly(_BarBeat.Bar, _BarBeat.Beat) - 1f; else if (_isTimeSpanLoaded) _beat = _calculator!.TimeSpanToBeatOnly(_TimeSpan) - 1f; _isBeatLoaded = true; } return _beat + 1f; } } /// /// The actual bar and beat of this moment. /// public (uint bar, float beat) BarBeat { get { IfNullThrowException(); if (!_isBarBeatLoaded) { if (_isBeatLoaded) _BarBeat = _calculator!.BeatOnlyToBarBeat(_beat + 1f); else if (_isTimeSpanLoaded) { _beat = _calculator!.TimeSpanToBeatOnly(_TimeSpan) - 1f; _isBeatLoaded = true; _BarBeat = _calculator.BeatOnlyToBarBeat(_beat + 1f); } _isBarBeatLoaded = true; } return _BarBeat; } } /// /// The total amount of time from the beginning of the level to this beat. /// public TimeSpan TimeSpan { get { IfNullThrowException(); if (!_isTimeSpanLoaded) { if (_isBeatLoaded) _TimeSpan = _calculator!.BeatOnlyToTimeSpan(_beat + 1f); else { if (_isBarBeatLoaded) { _beat = _calculator!.BarBeatToBeatOnly(_BarBeat.Bar, _BarBeat.Beat) - 1f; _isBeatLoaded = true; _TimeSpan = _calculator.BeatOnlyToTimeSpan(_beat + 1f); } } _isTimeSpanLoaded = true; } return _TimeSpan; } } /// /// The number of beats per minute followed at this moment. /// public float BPM { get { if (!_isBPMLoaded) { _BPM = _calculator?.BeatsPerMinuteOf(this) ?? throw new InvalidRDBeatException(); _isBPMLoaded = true; } return _BPM; } } /// /// The number of beats per bar followed at this moment. /// public float CPB { get { if (!_isCPBLoaded) { _CPB = (uint)Math.Round(_calculator?.CrotchetsPerBarOf(this) ?? throw new InvalidRDBeatException()); _isCPBLoaded = true; } return _CPB; } } /// /// Construct an instance without specifying a calculator. /// /// The total number of beats from this moment to the beginning of the level. public RDBeat(float beatOnly) { this = default; if (beatOnly < 1) throw new OverflowException(string.Format("The beat must not be less than 1, but {0} is given", beatOnly)); _beat = beatOnly - 1f; _isBeatLoaded = true; } /// /// Constructs an instance of RDBeat with the specified bar and beat. /// /// The actual bar of this moment. Must be greater than or equal to 1. /// The actual beat of this moment. Must be greater than or equal to 1. /// Thrown when the bar or beat is less than 1. public RDBeat(uint bar, float beat) { this = default; if (bar < 1) throw new OverflowException(string.Format("The bar must not be less than 1, but {0} is given", bar)); if (beat < 1) throw new OverflowException(string.Format("The beat must not be less than 1, but {0} is given", beat)); _BarBeat = new ValueTuple(bar, beat); _isBarBeatLoaded = true; } /// /// Constructs an instance of RDBeat with the specified time span. /// /// The total amount of time from the start of the level to the moment. /// Thrown when the time span is less than zero. public RDBeat(TimeSpan timeSpan) { this = default; if (timeSpan < TimeSpan.Zero) throw new OverflowException(string.Format("The time must not be less than zero, but {0} is given", timeSpan)); _TimeSpan = timeSpan; _isTimeSpanLoaded = true; } /// /// Construct an instance with specifying a calculator. /// /// Specified calculator. /// The total number of beats from this moment to the beginning of the level. public RDBeat(BeatCalculator? calculator, float beatOnly) { this = new RDBeat(beatOnly); _calculator = calculator; } /// /// Construct an instance with specifying a calculator. /// /// Specified calculator. /// The actual bar of this moment. /// The actual beat of this moment. public RDBeat(BeatCalculator calculator, uint bar, float beat) { this = new RDBeat(bar, beat); _calculator = calculator; _beat = _calculator.BarBeatToBeatOnly(bar, beat) - 1f; } /// /// Construct an instance with specifying a calculator. /// /// Specified calculator. /// The total amount of time from the start of the level to the moment public RDBeat(BeatCalculator calculator, TimeSpan timeSpan) { this = new RDBeat(timeSpan); _calculator = calculator; _beat = _calculator.TimeSpanToBeatOnly(timeSpan) - 1f; } /// /// Construct an instance with specifying a calculator. /// /// Specified calculator. /// Another instance. public RDBeat(BeatCalculator calculator, RDBeat beat) { this = default; if (beat._isBeatLoaded) { if (beat._beat < 0f) throw new OverflowException(string.Format("The beat must not be less than 1, but {0} is given", beat._beat)); _beat = beat._beat; _isBeatLoaded = true; _calculator = calculator; } else if (beat._isBarBeatLoaded) { if (beat._BarBeat.Bar < 1) throw new OverflowException(string.Format("The bar must not be less than 1, but {0} is given", beat._BarBeat.Bar)); if (beat._BarBeat.Beat < 1) throw new OverflowException(string.Format("The beat must not be less than 1, but {0} is given", beat._BarBeat.Beat)); _BarBeat = beat._BarBeat; _isBarBeatLoaded = true; _calculator = calculator; _beat = _calculator.BarBeatToBeatOnly(beat._BarBeat.Bar, beat._BarBeat.Beat) - 1f; } else if (beat._isTimeSpanLoaded) { if (beat._TimeSpan < TimeSpan.Zero) throw new OverflowException(string.Format("The time must not be less than zero, but {0} is given", beat._TimeSpan)); _TimeSpan = beat._TimeSpan; _isTimeSpanLoaded = true; _calculator = calculator; _beat = _calculator.TimeSpanToBeatOnly(TimeSpan) - 1f; } } /// /// Construct a beat of the 1st beat from the calculator /// /// Specified calculator. /// The first beat tied to the level. public static RDBeat Default(BeatCalculator calculator) { RDBeat Default = new(calculator, 1f); return Default; } /// /// Determine if two beats come from the same level /// /// A beat. /// Another beat. /// If true, an exception will be thrown when two beats do not come from the same level. /// public static bool FromSameLevel(RDBeat a, RDBeat b, bool @throw = false) => (a._calculator?.Equals(b._calculator) ?? true) || (@throw ? throw new RhythmBaseException("Beats must come from the same RDLevel.") : false); /// /// Determine if two beats are from the same level. ///
/// If any of them does not come from any level, it will also return true. ///
/// A beat. /// Another beat. /// If true, an exception will be thrown when two beats do not come from the same level. /// public static bool FromSameLevelOrNull(RDBeat a, RDBeat b, bool @throw = false) => a._calculator == null || b._calculator == null || FromSameLevel(a, b, @throw); /// /// Determine if two beats are from the same level. /// /// Another beat. /// If true, an exception will be thrown when two beats do not come from the same level. /// [MemberNotNullWhen(true)] public readonly bool FromSameLevel(RDBeat b, bool @throw = false) => FromSameLevel(this, b, @throw); /// /// Determine if two beats are from the same level. ///
/// If any of them does not come from any level, it will also return true. ///
/// Another beat. /// If true, an exception will be thrown when two beats do not come from the same level. /// public readonly bool FromSameLevelOrNull(RDBeat b, bool @throw = false) => BaseLevel == null || b.BaseLevel == null || FromSameLevel(b, @throw); /// /// Returns a new instance of unbinding the level. /// /// A new instance of unbinding the level. public readonly RDBeat WithoutBinding() { RDBeat result = this; if (result._calculator != null) result.Cache(); result._calculator = null; return result; } /// /// 校验这个实例是否缺少在转换单位时必需的参数并抛出异常 /// /// internal readonly void IfNullThrowException() { if (IsEmpty) { throw new InvalidRDBeatException(); } } /// /// Refresh the cache. /// public void ResetCache() { object __ = BeatOnly; _isBarBeatLoaded = false; _isTimeSpanLoaded = false; } /// /// Caches the current state of the beat by accessing its properties. /// /// Thrown when the beat cannot be calculated. public void Cache() { IfNullThrowException(); object __ = BeatOnly; __ = BarBeat; __ = TimeSpan; } /// /// /// internal void ResetBPM() { if (!_isBeatLoaded) _beat = (_calculator?.TimeSpanToBeatOnly(_TimeSpan) - 1f) ?? throw new InvalidRDBeatException(); _isBeatLoaded = true; _isTimeSpanLoaded = false; _isBPMLoaded = false; } internal void ResetCPB() { if (!_isBeatLoaded) _beat = (_calculator?.BarBeatToBeatOnly(_BarBeat.Bar, _BarBeat.Beat) - 1f) ?? throw new InvalidRDBeatException(); _isBeatLoaded = true; _isBarBeatLoaded = false; _isCPBLoaded = false; } /// public static RDBeat operator +(RDBeat a, float b) { RDBeat result; if (!a.IsEmpty) result = new RDBeat(a._calculator, a.BeatOnly + b); else { if (!a._isBeatLoaded) throw new ArgumentNullException(nameof(a), "The beat cannot be calculate."); result = new RDBeat(a._beat + b); } return result; } /// public static RDBeat operator +(RDBeat a, TimeSpan b) { RDBeat result; if (!a.IsEmpty) result = new RDBeat(a._calculator, a.TimeSpan + b); else { if (!a._isBeatLoaded) throw new ArgumentNullException(nameof(a), "The beat cannot be calculate."); result = new RDBeat(a._TimeSpan + b); } return result; } /// public static RDBeat operator -(RDBeat a, float b) { RDBeat result; if (!a.IsEmpty) result = new RDBeat(a._calculator, a.BeatOnly - b); else { if (!a._isBeatLoaded) throw new ArgumentNullException(nameof(a), "The beat cannot be calculate."); result = new RDBeat(a._beat - b); } return result; } /// public static RDBeat operator -(RDBeat a, TimeSpan b) { RDBeat result; if (!a.IsEmpty) result = new RDBeat(a._calculator, a.TimeSpan - b); else { if (!a._isBeatLoaded) throw new ArgumentNullException(nameof(a), "The beat cannot be calculate."); result = new RDBeat(a._TimeSpan - b); } return result; } /// public static bool operator >(RDBeat a, RDBeat b) => FromSameLevel(a, b, true) && a.BeatOnly > b.BeatOnly; /// public static bool operator <(RDBeat a, RDBeat b) => FromSameLevel(a, b, true) && a.BeatOnly < b.BeatOnly; /// public static bool operator >=(RDBeat a, RDBeat b) => FromSameLevel(a, b, true) && a.BeatOnly >= b.BeatOnly; /// public static bool operator <=(RDBeat a, RDBeat b) => FromSameLevel(a, b, true) && a.BeatOnly <= b.BeatOnly; /// public static bool operator ==(RDBeat a, RDBeat b) => (FromSameLevel(a, b, true) && a._beat == b._beat) || (a._isBarBeatLoaded && b._isBarBeatLoaded && a._BarBeat.Bar == b._BarBeat.Bar && a._BarBeat.Beat == b._BarBeat.Beat) || (a._isTimeSpanLoaded && b._isTimeSpanLoaded && a._TimeSpan == b._TimeSpan) || a.BeatOnly == b.BeatOnly; /// public static bool operator !=(RDBeat a, RDBeat b) => !(a == b); /// public override string ToString() { string ToString; if (IsEmpty) ToString = string.Format("[{0},{1},{2}]", _isBeatLoaded ? _beat.ToString() : "?", _isBarBeatLoaded ? _BarBeat.ToString() : "?", _isTimeSpanLoaded ? _TimeSpan.ToString() : "?"); else ToString = string.Format("[{0},{1}]", BarBeat.bar, BarBeat.beat); return ToString; } /// public readonly override bool Equals([NotNullWhen(true)] object? obj) => obj is RDBeat e && Equals(e); /// public readonly bool Equals(RDBeat other) => this == other; /// public override int GetHashCode() => HashCode.Combine(BeatOnly, BaseLevel); /// public int CompareTo(RDBeat other) { float result = BeatOnly - other.BeatOnly; int CompareTo; if (result > 0f) CompareTo = 1; else if (result < 0f) CompareTo = -1; else CompareTo = 0; return CompareTo; } internal BeatCalculator? _calculator; private bool _isBeatLoaded; private bool _isBarBeatLoaded; private bool _isTimeSpanLoaded; private bool _isBPMLoaded; private bool _isCPBLoaded; private float _beat; private (uint Bar, float Beat) _BarBeat; private TimeSpan _TimeSpan; private float _BPM; private uint _CPB; } }