/*
    Author: Lorenzo Lotti
    Name: Timeline (DataStructures.Timeline<TValue>)
    Type: Data structure (class)
    Description: A collection of dates/times and values sorted by dates/times easy to query.
    Usage:
        this data structure can be used to represent an ordered series of dates or times with which to associate values.
        An example is a chronology of events:
            306: Constantine is the new emperor,
            312: Battle of the Milvian Bridge,
            313: Edict of Milan,
            330: Constantine move the capital to Constantinople.
*/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

namespace DataStructures
{
    /// <summary>
    ///     A collection of <see cref="DateTime" /> and <see cref="TValue" />
    ///     sorted by <see cref="DateTime" /> field.
    /// </summary>
    /// <typeparam name="TValue">Value associated with a <see cref="DateTime" />.</typeparam>
    public class Timeline<TValue> : ICollection<(DateTime Time, TValue Value)>, IEquatable<Timeline<TValue>>
    {
        /// <summary>
        ///     Inner collection storing the timeline events as key-tuples.
        /// </summary>
        private readonly List<(DateTime Time, TValue Value)> timeline = new();

        /// <summary>
        ///     Initializes a new instance of the <see cref="Timeline{TValue}"/> class.
        /// </summary>
        public Timeline()
        {
        }

        /// <summary>
        ///     Initializes a new instance of the <see cref="Timeline{TValue}"/> class populated with an initial event.
        /// </summary>
        /// <param name="time">The time at which the given event occurred.</param>
        /// <param name="value">The event's content.</param>
        public Timeline(DateTime time, TValue value)
            => timeline = new List<(DateTime, TValue)>
            {
                (time, value),
            };

        /// <summary>
        ///     Initializes a new instance of the <see cref="Timeline{TValue}"/> class containing the provided events
        ///     ordered chronologically.
        /// </summary>
        /// <param name="timeline">The timeline to represent.</param>
        public Timeline(params (DateTime, TValue)[] timeline)
            => this.timeline = timeline
                .OrderBy(pair => pair.Item1)
                .ToList();

        /// <summary>
        /// Gets he number of unique times within this timeline.
        /// </summary>
        public int TimesCount
            => GetAllTimes().Length;

        /// <summary>
        ///     Gets all events that has occurred in this timeline.
        /// </summary>
        public int ValuesCount
            => GetAllValues().Length;

        /// <summary>
        ///     Get all values associated with <paramref name="time" />.
        /// </summary>
        /// <param name="time">Time to get values for.</param>
        /// <returns>Values associated with <paramref name="time" />.</returns>
        public TValue[] this[DateTime time]
        {
            get => GetValuesByTime(time);
            set
            {
                var overridenEvents = timeline.Where(@event => @event.Time == time).ToList();
                foreach (var @event in overridenEvents)
                {
                    timeline.Remove(@event);
                }

                foreach (var v in value)
                {
                    Add(time, v);
                }
            }
        }

        /// <inheritdoc />
        bool ICollection<(DateTime Time, TValue Value)>.IsReadOnly
            => false;

        /// <summary>
        ///     Gets the count of pairs.
        /// </summary>
        public int Count
            => timeline.Count;

        /// <summary>
        ///     Clear the timeline, removing all events.
        /// </summary>
        public void Clear()
            => timeline.Clear();

        /// <summary>
        ///     Copy a value to an array.
        /// </summary>
        /// <param name="array">Destination array.</param>
        /// <param name="arrayIndex">The start index.</param>
        public void CopyTo((DateTime, TValue)[] array, int arrayIndex)
            => timeline.CopyTo(array, arrayIndex);

        /// <summary>
        ///     Add an event at a given time.
        /// </summary>
        /// <param name="item">The tuple containing the event date and value.</param>
        void ICollection<(DateTime Time, TValue Value)>.Add((DateTime Time, TValue Value) item)
            => Add(item.Time, item.Value);

        /// <summary>
        ///     Check whether or not a event exists at a specific date in the timeline.
        /// </summary>
        /// <param name="item">The tuple containing the event date and value.</param>
        /// <returns>True if this event exists at the given date, false otherwise.</returns>
        bool ICollection<(DateTime Time, TValue Value)>.Contains((DateTime Time, TValue Value) item)
            => Contains(item.Time, item.Value);

        /// <summary>
        ///     Remove an event at a specific date.
        /// </summary>
        /// <param name="item">The tuple containing the event date and value.</param>
        /// <returns>True if the event was removed, false otherwise.</returns>
        bool ICollection<(DateTime Time, TValue Value)>.Remove((DateTime Time, TValue Value) item)
            => Remove(item.Time, item.Value);

        /// <inheritdoc />
        IEnumerator IEnumerable.GetEnumerator()
            => timeline.GetEnumerator();

        /// <inheritdoc />
        IEnumerator<(DateTime Time, TValue Value)> IEnumerable<(DateTime Time, TValue Value)>.GetEnumerator()
            => timeline.GetEnumerator();

        /// <inheritdoc />
        public bool Equals(Timeline<TValue>? other)
            => other is not null && this == other;

        /// <summary>
        ///     Checks whether or not two <see cref="Timeline{TValue}"/> are equals.
        /// </summary>
        /// <param name="left">The first timeline.</param>
        /// <param name="right">The other timeline to be checked against the <paramref name="left"/> one.</param>
        /// <returns>True if both timelines are similar, false otherwise.</returns>
        public static bool operator ==(Timeline<TValue> left, Timeline<TValue> right)
        {
            var leftArray = left.ToArray();
            var rightArray = right.ToArray();

            if (left.Count != rightArray.Length)
            {
                return false;
            }

            for (var i = 0; i < leftArray.Length; i++)
            {
                if (leftArray[i].Time != rightArray[i].Time
                    && !leftArray[i].Value!.Equals(rightArray[i].Value))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        ///     Checks whether or not two <see cref="Timeline{TValue}"/> are not equals.
        /// </summary>
        /// <param name="left">The first timeline.</param>
        /// <param name="right">The other timeline to be checked against the <paramref name="left"/> one.</param>
        /// <returns>False if both timelines are similar, true otherwise.</returns>
        public static bool operator !=(Timeline<TValue> left, Timeline<TValue> right)
            => !(left == right);

        /// <summary>
        ///     Get all <see cref="DateTime" /> of the timeline.
        /// </summary>
        public DateTime[] GetAllTimes()
            => timeline.Select(t => t.Time)
                .Distinct()
                .ToArray();

        /// <summary>
        ///     Get <see cref="DateTime" /> values of the timeline that have this <paramref name="value" />.
        /// </summary>
        public DateTime[] GetTimesByValue(TValue value)
            => timeline.Where(pair => pair.Value!.Equals(value))
                .Select(pair => pair.Time)
                .ToArray();

        /// <summary>
        ///     Get all <see cref="DateTime" /> before <paramref name="time" />.
        /// </summary>
        public DateTime[] GetTimesBefore(DateTime time)
            => GetAllTimes()
                .Where(t => t < time)
                .OrderBy(t => t)
                .ToArray();

        /// <summary>
        ///     Get all <see cref="DateTime" /> after <paramref name="time" />.
        /// </summary>
        public DateTime[] GetTimesAfter(DateTime time)
            => GetAllTimes()
                .Where(t => t > time)
                .OrderBy(t => t)
                .ToArray();

        /// <summary>
        ///     Get all <see cref="TValue" /> of the timeline.
        /// </summary>
        public TValue[] GetAllValues()
            => timeline.Select(pair => pair.Value)
                .ToArray();

        /// <summary>
        ///     Get all <see cref="TValue" /> associated with <paramref name="time" />.
        /// </summary>
        public TValue[] GetValuesByTime(DateTime time)
            => timeline.Where(pair => pair.Time == time)
                .Select(pair => pair.Value)
                .ToArray();

        /// <summary>
        ///     Get all <see cref="TValue" /> before <paramref name="time" />.
        /// </summary>
        public Timeline<TValue> GetValuesBefore(DateTime time)
            => new(this.Where(pair => pair.Time < time).ToArray());

        /// <summary>
        ///     Get all <see cref="TValue" /> before <paramref name="time" />.
        /// </summary>
        public Timeline<TValue> GetValuesAfter(DateTime time)
            => new(this.Where(pair => pair.Time > time).ToArray());

        /// <summary>
        ///     Gets all values that happened at specified millisecond.
        /// </summary>
        /// <param name="millisecond">Value to look for.</param>
        /// <returns>Array of values.</returns>
        public Timeline<TValue> GetValuesByMillisecond(int millisecond)
            => new(timeline.Where(pair => pair.Time.Millisecond == millisecond).ToArray());

        /// <summary>
        ///     Gets all values that happened at specified second.
        /// </summary>
        /// <param name="second">Value to look for.</param>
        /// <returns>Array of values.</returns>
        public Timeline<TValue> GetValuesBySecond(int second)
            => new(timeline.Where(pair => pair.Time.Second == second).ToArray());

        /// <summary>
        ///     Gets all values that happened at specified minute.
        /// </summary>
        /// <param name="minute">Value to look for.</param>
        /// <returns>Array of values.</returns>
        public Timeline<TValue> GetValuesByMinute(int minute)
            => new(timeline.Where(pair => pair.Time.Minute == minute).ToArray());

        /// <summary>
        ///     Gets all values that happened at specified hour.
        /// </summary>
        /// <param name="hour">Value to look for.</param>
        /// <returns>Array of values.</returns>
        public Timeline<TValue> GetValuesByHour(int hour)
            => new(timeline.Where(pair => pair.Time.Hour == hour).ToArray());

        /// <summary>
        ///     Gets all values that happened at specified day.
        /// </summary>
        /// <param name="day">Value to look for.</param>
        /// <returns>Array of values.</returns>
        public Timeline<TValue> GetValuesByDay(int day)
            => new(timeline.Where(pair => pair.Time.Day == day).ToArray());

        /// <summary>
        ///     Gets all values that happened at specified time of the day.
        /// </summary>
        /// <param name="timeOfDay">Value to look for.</param>
        /// <returns>Array of values.</returns>
        public Timeline<TValue> GetValuesByTimeOfDay(TimeSpan timeOfDay)
            => new(timeline.Where(pair => pair.Time.TimeOfDay == timeOfDay).ToArray());

        /// <summary>
        ///     Gets all values that happened at specified day of the week.
        /// </summary>
        /// <param name="dayOfWeek">Value to look for.</param>
        /// <returns>Array of values.</returns>
        public Timeline<TValue> GetValuesByDayOfWeek(DayOfWeek dayOfWeek)
            => new(timeline.Where(pair => pair.Time.DayOfWeek == dayOfWeek).ToArray());

        /// <summary>
        ///     Gets all values that happened at specified day of the year.
        /// </summary>
        /// <param name="dayOfYear">Value to look for.</param>
        /// <returns>Array of values.</returns>
        public Timeline<TValue> GetValuesByDayOfYear(int dayOfYear)
            => new(timeline.Where(pair => pair.Time.DayOfYear == dayOfYear).ToArray());

        /// <summary>
        ///     Gets all values that happened at specified month.
        /// </summary>
        /// <param name="month">Value to look for.</param>
        /// <returns>Array of values.</returns>
        public Timeline<TValue> GetValuesByMonth(int month)
            => new(timeline.Where(pair => pair.Time.Month == month).ToArray());

        /// <summary>
        ///     Gets all values that happened at specified year.
        /// </summary>
        /// <param name="year">Value to look for.</param>
        /// <returns>Array of values.</returns>
        public Timeline<TValue> GetValuesByYear(int year)
            => new(timeline.Where(pair => pair.Time.Year == year).ToArray());

        /// <summary>
        ///     Add an event at a given <paramref name="time"/>.
        /// </summary>
        /// <param name="time">The date at which the event occurred.</param>
        /// <param name="value">The event value.</param>
        public void Add(DateTime time, TValue value)
        {
            timeline.Add((time, value));
        }

        /// <summary>
        ///     Add a set of <see cref="DateTime" /> and <see cref="TValue" /> to the timeline.
        /// </summary>
        public void Add(params (DateTime, TValue)[] timeline)
        {
            this.timeline.AddRange(timeline);
        }

        /// <summary>
        ///     Append an existing timeline to this one.
        /// </summary>
        public void Add(Timeline<TValue> timeline)
            => Add(timeline.ToArray());

        /// <summary>
        ///     Add a <paramref name="value" /> associated with <see cref="DateTime.Now" /> to the timeline.
        /// </summary>
        public void AddNow(params TValue[] value)
        {
            var now = DateTime.Now;
            foreach (var v in value)
            {
                Add(now, v);
            }
        }

        /// <summary>
        ///     Check whether or not a event exists at a specific date in the timeline.
        /// </summary>
        /// <param name="time">The date at which the event occurred.</param>
        /// <param name="value">The event value.</param>
        /// <returns>True if this event exists at the given date, false otherwise.</returns>
        public bool Contains(DateTime time, TValue value)
            => timeline.Contains((time, value));

        /// <summary>
        ///     Check if timeline contains this set of value pairs.
        /// </summary>
        /// <param name="timeline">The events to checks.</param>
        /// <returns>True if any of the events has occurred in the timeline.</returns>
        public bool Contains(params (DateTime, TValue)[] timeline)
            => timeline.Any(@event => Contains(@event.Item1, @event.Item2));

        /// <summary>
        ///     Check if timeline contains any of the event of the provided <paramref name="timeline"/>.
        /// </summary>
        /// <param name="timeline">The events to checks.</param>
        /// <returns>True if any of the events has occurred in the timeline.</returns>
        public bool Contains(Timeline<TValue> timeline)
            => Contains(timeline.ToArray());

        /// <summary>
        ///     Check if timeline contains any of the time of the provided <paramref name="times"/>.
        /// </summary>
        /// <param name="times">The times to checks.</param>
        /// <returns>True if any of the times is stored in the timeline.</returns>
        public bool ContainsTime(params DateTime[] times)
        {
            var storedTimes = GetAllTimes();
            return times.Any(value => storedTimes.Contains(value));
        }

        /// <summary>
        ///     Check if timeline contains any of the event of the provided <paramref name="values"/>.
        /// </summary>
        /// <param name="values">The events to checks.</param>
        /// <returns>True if any of the events has occurred in the timeline.</returns>
        public bool ContainsValue(params TValue[] values)
        {
            var storedValues = GetAllValues();
            return values.Any(value => storedValues.Contains(value));
        }

        /// <summary>
        ///     Remove an event at a specific date.
        /// </summary>
        /// <param name="time">The date at which the event occurred.</param>
        /// <param name="value">The event value.</param>
        /// <returns>True if the event was removed, false otherwise.</returns>
        public bool Remove(DateTime time, TValue value)
            => timeline.Remove((time, value));

        /// <summary>
        ///     Remove a set of value pairs from the timeline.
        /// </summary>
        /// <param name="timeline">An collection of all events to remove.</param>
        /// <returns>Returns true if the operation completed successfully.</returns>
        public bool Remove(params (DateTime, TValue)[] timeline)
        {
            var result = false;
            foreach (var (time, value) in timeline)
            {
                result |= this.timeline.Remove((time, value));
            }

            return result;
        }

        /// <summary>
        ///     Remove an existing timeline from this timeline.
        /// </summary>
        /// <param name="timeline">An collection of all events to remove.</param>
        /// <returns>Returns true if the operation completed successfully.</returns>
        public bool Remove(Timeline<TValue> timeline)
            => Remove(timeline.ToArray());

        /// <summary>
        ///     Remove a value pair from the timeline if the time is equal to <paramref name="times" />.
        /// </summary>
        /// <returns>Returns true if the operation completed successfully.</returns>
        public bool RemoveTimes(params DateTime[] times)
        {
            var isTimeContainedInTheTimeline = times.Any(time => GetAllTimes().Contains(time));

            if (!isTimeContainedInTheTimeline)
            {
                return false;
            }

            var eventsToRemove = times.SelectMany(time =>
                timeline.Where(@event => @event.Time == time))
                .ToList();

            foreach (var @event in eventsToRemove)
            {
                timeline.Remove(@event);
            }

            return true;
        }

        /// <summary>
        ///     Remove a value pair from the timeline if the value is equal to <paramref name="values" />.
        /// </summary>
        /// <returns>Returns true if the operation completed successfully.</returns>
        public bool RemoveValues(params TValue[] values)
        {
            var isValueContainedInTheTimeline = values.Any(v => GetAllValues().Contains(v));

            if (!isValueContainedInTheTimeline)
            {
                return false;
            }

            var eventsToRemove = values.SelectMany(value =>
                timeline.Where(@event => EqualityComparer<TValue>.Default.Equals(@event.Value, value)))
                .ToList();

            foreach (var @event in eventsToRemove)
            {
                timeline.Remove(@event);
            }

            return true;
        }

        /// <summary>
        ///     Convert the timeline to an array.
        /// </summary>
        /// <returns>
        /// The timeline as an array of tuples of (<see cref="DateTime"/>, <typeparamref name="TValue"/>).
        /// </returns>
        public (DateTime Time, TValue Value)[] ToArray()
            => timeline.ToArray();

        /// <summary>
        ///     Convert the timeline to a list.
        /// </summary>
        /// <returns>
        /// The timeline as a list of tuples of (<see cref="DateTime"/>, <typeparamref name="TValue"/>).
        /// </returns>
        public IList<(DateTime Time, TValue Value)> ToList()
            => timeline;

        /// <summary>
        ///     Convert the timeline to a dictionary.
        /// </summary>
        /// <returns>
        /// The timeline as an dictionary of <typeparamref name="TValue"/> by <see cref="DateTime"/>.
        /// </returns>
        public IDictionary<DateTime, TValue> ToDictionary()
            => timeline.ToDictionary(@event => @event.Time, @event => @event.Value);

        /// <inheritdoc />
        public override bool Equals(object? obj)
            => obj is Timeline<TValue> otherTimeline
               && this == otherTimeline;

        /// <inheritdoc />
        public override int GetHashCode()
            => timeline.GetHashCode();
    }
}
