using System;
using System.Collections.Generic;
using System.Linq;
namespace Spectre.Console
{
///
/// Represents a progress task.
///
public sealed class ProgressTask : IProgress
{
private readonly List _samples;
private readonly object _lock;
private double _maxValue;
private string _description;
private double _value;
///
/// Gets the task ID.
///
public int Id { get; }
///
/// Gets or sets the task description.
///
public string Description
{
get => _description;
set => Update(description: value);
}
///
/// Gets or sets the max value of the task.
///
public double MaxValue
{
get => _maxValue;
set => Update(maxValue: value);
}
///
/// Gets or sets the value of the task.
///
public double Value
{
get => _value;
set => Update(value: value);
}
///
/// Gets the start time of the task.
///
public DateTime? StartTime { get; private set; }
///
/// Gets the stop time of the task.
///
public DateTime? StopTime { get; private set; }
///
/// Gets the task state.
///
public ProgressTaskState State { get; }
///
/// Gets a value indicating whether or not the task has started.
///
public bool IsStarted => StartTime != null;
///
/// Gets a value indicating whether or not the task has finished.
///
public bool IsFinished => StopTime != null || Value >= MaxValue;
///
/// Gets the percentage done of the task.
///
public double Percentage => GetPercentage();
///
/// Gets the speed measured in steps/second.
///
public double? Speed => GetSpeed();
///
/// Gets the elapsed time.
///
public TimeSpan? ElapsedTime => GetElapsedTime();
///
/// Gets the remaining time.
///
public TimeSpan? RemainingTime => GetRemainingTime();
///
/// Gets or sets a value indicating whether the ProgressBar shows
/// actual values or generic, continuous progress feedback.
///
public bool IsIndeterminate { get; set; }
///
/// Initializes a new instance of the class.
///
/// The task ID.
/// The task description.
/// The task max value.
/// Whether or not the task should start automatically.
public ProgressTask(int id, string description, double maxValue, bool autoStart = true)
{
_samples = new List();
_lock = new object();
_maxValue = maxValue;
_value = 0;
_description = description?.RemoveNewLines()?.Trim() ??
throw new ArgumentNullException(nameof(description));
if (string.IsNullOrWhiteSpace(_description))
{
throw new ArgumentException("Task name cannot be empty", nameof(description));
}
Id = id;
State = new ProgressTaskState();
StartTime = autoStart ? DateTime.Now : null;
}
///
/// Starts the task.
///
public void StartTask()
{
lock (_lock)
{
if (StopTime != null)
{
throw new InvalidOperationException("Stopped tasks cannot be restarted");
}
StartTime = DateTime.Now;
StopTime = null;
}
}
///
/// Stops and marks the task as finished.
///
public void StopTask()
{
lock (_lock)
{
var now = DateTime.Now;
StartTime ??= now;
StopTime = now;
}
}
///
/// Increments the task's value.
///
/// The value to increment with.
public void Increment(double value)
{
Update(increment: value);
}
private void Update(
string? description = null,
double? maxValue = null,
double? increment = null,
double? value = null)
{
lock (_lock)
{
var startValue = Value;
if (description != null)
{
description = description?.RemoveNewLines()?.Trim();
if (string.IsNullOrWhiteSpace(description))
{
throw new InvalidOperationException("Task name cannot be empty.");
}
_description = description;
}
if (maxValue != null)
{
_maxValue = maxValue.Value;
}
if (increment != null)
{
_value += increment.Value;
}
if (value != null)
{
_value = value.Value;
}
// Need to cap the max value?
if (_value > _maxValue)
{
_value = _maxValue;
}
var timestamp = DateTime.Now;
var threshold = timestamp - TimeSpan.FromSeconds(30);
// Remove samples that's too old
while (_samples.Count > 0 && _samples[0].Timestamp < threshold)
{
_samples.RemoveAt(0);
}
// Keep maximum of 1000 samples
while (_samples.Count > 1000)
{
_samples.RemoveAt(0);
}
_samples.Add(new ProgressSample(timestamp, Value - startValue));
}
}
private double GetPercentage()
{
var percentage = (Value / MaxValue) * 100;
percentage = Math.Min(100, Math.Max(0, percentage));
return percentage;
}
private double? GetSpeed()
{
lock (_lock)
{
if (StartTime == null)
{
return null;
}
if (_samples.Count == 0)
{
return null;
}
var totalTime = _samples.Last().Timestamp - _samples[0].Timestamp;
if (totalTime == TimeSpan.Zero)
{
return null;
}
var totalCompleted = _samples.Sum(x => x.Value);
return totalCompleted / totalTime.TotalSeconds;
}
}
private TimeSpan? GetElapsedTime()
{
lock (_lock)
{
if (StartTime == null)
{
return null;
}
if (StopTime != null)
{
return StopTime - StartTime;
}
return DateTime.Now - StartTime;
}
}
private TimeSpan? GetRemainingTime()
{
lock (_lock)
{
if (IsFinished)
{
return TimeSpan.Zero;
}
var speed = GetSpeed();
if (speed == null || speed == 0)
{
return null;
}
// If the speed is near zero, the estimate below causes the
// TimeSpan creation to throw an OverflowException. Just return
// the maximum possible remaining time instead of overflowing.
var estimate = (MaxValue - Value) / speed.Value;
if (estimate > TimeSpan.MaxValue.TotalSeconds)
{
return TimeSpan.MaxValue;
}
return TimeSpan.FromSeconds(estimate);
}
}
///
void IProgress.Report(double value)
{
Update(increment: value - Value);
}
}
}