using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Storage.Entities.Profile;
namespace Artemis.Core;
///
/// Represents a timeline used by profile elements
///
public class Timeline : CorePropertyChanged, IStorageModel
{
private readonly object _lock = new();
///
/// Creates a new instance of the class
///
public Timeline()
{
Entity = new TimelineEntity();
MainSegmentLength = TimeSpan.FromSeconds(5);
Save();
}
internal Timeline(TimelineEntity entity)
{
Entity = entity;
Load();
}
///
public override string ToString()
{
return $"Progress: {Position}/{Length} - delta: {Delta}";
}
#region Properties
private TimeSpan _position;
private TimeSpan _lastDelta;
private TimelinePlayMode _playMode;
private TimelineStopMode _stopMode;
private TimeSpan _startSegmentLength;
private TimeSpan _mainSegmentLength;
private TimeSpan _endSegmentLength;
private TimeSpan _lastOverride;
///
/// Gets the current position of the timeline
///
public TimeSpan Position
{
get => _position;
private set => SetAndNotify(ref _position, value);
}
///
/// Gets the cumulative delta of all calls to that took place after the last call to
///
///
public TimeSpan Delta
{
get => _lastDelta;
private set => SetAndNotify(ref _lastDelta, value);
}
///
/// Gets or sets the mode in which the render element starts its timeline when display conditions are met
///
public TimelinePlayMode PlayMode
{
get => _playMode;
set => SetAndNotify(ref _playMode, value);
}
///
/// Gets or sets the mode in which the render element stops its timeline when display conditions are no longer met
///
public TimelineStopMode StopMode
{
get => _stopMode;
set => SetAndNotify(ref _stopMode, value);
}
///
/// Gets a boolean indicating whether the timeline has finished its run
///
public bool IsFinished => Position > Length;
///
/// Gets a boolean indicating whether the timeline progress has been overridden
///
public bool IsOverridden { get; private set; }
#region Segments
///
/// Gets the total length of this timeline
///
public TimeSpan Length => StartSegmentLength + MainSegmentLength + EndSegmentLength;
///
/// Gets or sets the length of the start segment
///
public TimeSpan StartSegmentLength
{
get => _startSegmentLength;
set
{
if (SetAndNotify(ref _startSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
///
/// Gets or sets the length of the main segment
///
public TimeSpan MainSegmentLength
{
get => _mainSegmentLength;
set
{
if (SetAndNotify(ref _mainSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
///
/// Gets or sets the length of the end segment
///
public TimeSpan EndSegmentLength
{
get => _endSegmentLength;
set
{
if (SetAndNotify(ref _endSegmentLength, value))
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
///
/// Gets or sets the start position of the main segment
///
public TimeSpan MainSegmentStartPosition
{
get => StartSegmentEndPosition;
set
{
StartSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.Main, true);
}
}
///
/// Gets or sets the end position of the end segment
///
public TimeSpan EndSegmentStartPosition
{
get => MainSegmentEndPosition;
set
{
MainSegmentEndPosition = value;
NotifySegmentShiftAt(TimelineSegment.End, true);
}
}
///
/// Gets or sets the end position of the start segment
///
public TimeSpan StartSegmentEndPosition
{
get => StartSegmentLength;
set
{
StartSegmentLength = value;
NotifySegmentShiftAt(TimelineSegment.Start, false);
}
}
///
/// Gets or sets the end position of the main segment
///
public TimeSpan MainSegmentEndPosition
{
get => StartSegmentEndPosition + MainSegmentLength;
set
{
MainSegmentLength = value - StartSegmentEndPosition >= TimeSpan.Zero ? value - StartSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.Main, false);
}
}
///
/// Gets or sets the end position of the end segment
///
public TimeSpan EndSegmentEndPosition
{
get => MainSegmentEndPosition + EndSegmentLength;
set
{
EndSegmentLength = value - MainSegmentEndPosition >= TimeSpan.Zero ? value - MainSegmentEndPosition : TimeSpan.Zero;
NotifySegmentShiftAt(TimelineSegment.End, false);
}
}
internal TimelineEntity Entity { get; set; }
///
/// Notifies the right segments in a way that I don't have to think about it
///
/// The segment that was updated
/// Whether the start point of the was updated
private void NotifySegmentShiftAt(TimelineSegment segment, bool startUpdated)
{
if (segment <= TimelineSegment.End)
{
if (startUpdated || segment < TimelineSegment.End)
OnPropertyChanged(nameof(EndSegmentStartPosition));
OnPropertyChanged(nameof(EndSegmentEndPosition));
}
if (segment <= TimelineSegment.Main)
{
if (startUpdated || segment < TimelineSegment.Main)
OnPropertyChanged(nameof(MainSegmentStartPosition));
OnPropertyChanged(nameof(MainSegmentEndPosition));
}
if (segment <= TimelineSegment.Start)
OnPropertyChanged(nameof(StartSegmentEndPosition));
OnPropertyChanged(nameof(Length));
OnTimelineChanged();
}
///
/// Occurs when changes have been made to any of the segments of the timeline.
///
public event EventHandler? TimelineChanged;
private void OnTimelineChanged()
{
TimelineChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
#endregion
#region Updating
///
/// Updates the timeline, applying the provided to the
///
/// The amount of time to apply to the position
/// Whether to stick to the main segment, wrapping around if needed
public void Update(TimeSpan delta, bool stickToMainSegment)
{
lock (_lock)
{
if (IsOverridden)
throw new ArtemisCoreException("Can't update an overridden timeline, call ClearOverride first.");
Delta += delta;
Position += delta;
if (!stickToMainSegment || Position <= MainSegmentEndPosition)
return;
// If the main segment has no length, simply stick to the start of the segment
if (MainSegmentLength == TimeSpan.Zero)
Position = MainSegmentStartPosition;
// Ensure wrapping back around retains the delta time
else
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(delta.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
}
}
///
/// Moves the position of the timeline backwards to the very start of the timeline
///
public void JumpToStart()
{
lock (_lock)
{
if (Position == TimeSpan.Zero)
return;
Delta = TimeSpan.Zero - Position;
Position = TimeSpan.Zero;
}
}
///
/// Moves the position of the timeline forwards to the beginning of the end segment
///
public void JumpToEndSegment()
{
lock (_lock)
{
if (Position >= EndSegmentStartPosition)
return;
Delta = EndSegmentStartPosition - Position;
Position = EndSegmentStartPosition;
}
}
///
/// Moves the position of the timeline forwards to the very end of the timeline
///
public void JumpToEnd()
{
lock (_lock)
{
if (Position >= EndSegmentEndPosition)
return;
Delta = EndSegmentEndPosition - Position;
Position = EndSegmentEndPosition;
}
}
///
/// Overrides the to the specified time
///
/// The position to set the timeline to
/// Whether to stick to the main segment, wrapping around if needed
internal void Override(TimeSpan position, bool stickToMainSegment)
{
lock (_lock)
{
if (_lastOverride == TimeSpan.Zero)
Delta = Position - position;
else
Delta = position - _lastOverride;
Position = position;
IsOverridden = true;
_lastOverride = position;
if (!stickToMainSegment || Position < MainSegmentStartPosition)
return;
bool atSegmentStart = Position >= MainSegmentStartPosition;
if (MainSegmentLength > TimeSpan.Zero)
{
Position = MainSegmentStartPosition + TimeSpan.FromMilliseconds(Position.TotalMilliseconds % MainSegmentLength.TotalMilliseconds);
// If the cursor is at the end of the timeline we don't want to wrap back around yet so only allow going to the start if the cursor
// is actually at the start of the segment
if (Position == MainSegmentStartPosition && !atSegmentStart)
Position = MainSegmentEndPosition;
}
else
{
Position = MainSegmentStartPosition;
}
}
}
internal void ClearOverride()
{
IsOverridden = false;
_lastOverride = TimeSpan.Zero;
}
///
/// Sets the to
///
public void ClearDelta()
{
lock (_lock)
{
Delta = TimeSpan.Zero;
}
}
#endregion
#region Storage
///
public void Load()
{
StartSegmentLength = Entity.StartSegmentLength;
MainSegmentLength = Entity.MainSegmentLength;
EndSegmentLength = Entity.EndSegmentLength;
PlayMode = (TimelinePlayMode) Entity.PlayMode;
StopMode = (TimelineStopMode) Entity.StopMode;
JumpToEnd();
}
///
public void Save()
{
Entity.StartSegmentLength = StartSegmentLength;
Entity.MainSegmentLength = MainSegmentLength;
Entity.EndSegmentLength = EndSegmentLength;
Entity.PlayMode = (int) PlayMode;
Entity.StopMode = (int) StopMode;
}
#endregion
}
internal enum TimelineSegment
{
Start,
Main,
End
}
///
/// Represents a mode for render elements to start their timeline when display conditions are met
///
public enum TimelinePlayMode
{
///
/// Continue repeating the main segment of the timeline while the condition is met
///
Repeat,
///
/// Only play the timeline once when the condition is met
///
Once
}
///
/// Represents a mode for render elements to stop their timeline when display conditions are no longer met
///
public enum TimelineStopMode
{
///
/// When conditions are no longer met, finish the the current run of the main timeline
///
Finish,
///
/// When conditions are no longer met, skip to the end segment of the timeline
///
SkipToEnd
}
///
/// Represents a mode for render elements to start their timeline when display conditions events are fired
///
public enum TimeLineEventOverlapMode
{
///
/// Stop the current run and restart the timeline
///
Restart,
///
/// Ignore subsequent event fires until the timeline finishes
///
Ignore,
///
/// Play another copy of the timeline on top of the current run
///
Copy,
///
/// Repeat the timeline until the event fires again
///
Toggle
}