//
//  TimePeriodCollection.swift
//  DateTools
//
//  Created by Grayson Webster on 8/17/16.
//  Copyright © 2016 Grayson Webster. All rights reserved.
//

import Foundation

/**
 *  Time period collections serve as loose sets of time periods. They are
 *  unorganized unless you decide to sort them, and have their own characteristics
 *  like a `beginning` and `end` that are extrapolated from the time periods within. Time
 *  period collections allow overlaps within their set of time periods.
 *
 *  [Visit our github page](https://github.com/MatthewYork/DateTools#time-period-collections) for more information.
 */
open class TimePeriodCollection: TimePeriodGroup {
    // MARK: - Collection Manipulation

    /**
     *  Append a TimePeriodProtocol to the periods array and check if the Collection's
     *  beginning and end should change.
     *
     * - parameter period: TimePeriodProtocol to add to the collection
     */
    public func append(_ period: TimePeriodProtocol) {
        periods.append(period)
        updateExtremes(period: period)
    }

    /**
     *  Append a TimePeriodProtocol array to the periods array and check if the Collection's
     *  beginning and end should change.
     *
     * - parameter periodArray: TimePeriodProtocol list to add to the collection
     */
    public func append(_ periodArray: [TimePeriodProtocol]) {
        for period in periodArray {
            periods.append(period)
            updateExtremes(period: period)
        }
    }

    /**
     *  Append a TimePeriodGroup's periods array to the periods array of self and check if the Collection's
     *  beginning and end should change.
     *
     * - parameter newPeriods: TimePeriodGroup to merge periods arrays with
     */
    public func append<C: TimePeriodGroup>(contentsOf newPeriods: C) {
        for period in newPeriods as TimePeriodGroup {
            periods.append(period)
            updateExtremes(period: period)
        }
    }

    /**
     *  Insert period into periods array at given index.
     *
     * - parameter newElement: The period to insert
     * - parameter index: Index to insert period at
     */
    public func insert(_ newElement: TimePeriodProtocol, at index: Int) {
        periods.insert(newElement, at: index)
        updateExtremes(period: newElement)
    }

    /**
     *  Remove from period array at the given index.
     *
     * - parameter at: The index in the collection to remove
     */
    public func remove(at: Int) {
        periods.remove(at: at)
        updateExtremes()
    }

    /**
     *  Remove all periods from period array.
     */
    public func removeAll() {
        periods.removeAll()
        updateExtremes()
    }

    // MARK: - Sorting

    // In place
    /**
     *  Sort periods array in place by beginning
     */
    public func sortByBeginning() {
        sort { (period1: TimePeriodProtocol, period2: TimePeriodProtocol) -> Bool in
            if period1.beginning == nil, period2.beginning == nil {
                return false
            } else if period1.beginning == nil {
                return true
            } else if period2.beginning == nil {
                return false
            } else {
                return period2.beginning! < period1.beginning!
            }
        }
    }

    /**
     *  Sort periods array in place
     */
    public func sort(by areInIncreasingOrder: (TimePeriodProtocol, TimePeriodProtocol) -> Bool) {
        periods.sort(by: areInIncreasingOrder)
    }

    // New collection
    /**
     *  Return collection with sorted periods array by beginning
     *
     * - returns: Collection with sorted periods
     */
    public func sortedByBeginning() -> TimePeriodCollection {
        let array = periods.sorted { (period1: TimePeriodProtocol, period2: TimePeriodProtocol) -> Bool in
            if period1.beginning == nil, period2.beginning == nil {
                return false
            } else if period1.beginning == nil {
                return true
            } else if period2.beginning == nil {
                return false
            } else {
                return period2.beginning! < period1.beginning!
            }
        }
        let collection = TimePeriodCollection()
        collection.append(array)
        return collection
    }

    /**
     *  Return collection with sorted periods array
     *
     * - returns: Collection with sorted periods
     */
    public func sorted(by areInIncreasingOrder: (TimePeriodProtocol, TimePeriodProtocol) -> Bool) -> TimePeriodCollection {
        let collection = TimePeriodCollection()
        collection.append(periods.sorted(by: areInIncreasingOrder))
        return collection
    }

    // MARK: - Collection Relationship

    // Potentially use .reduce() instead of these functions
    /**
     *  Returns from the `TimePeriodCollection` a sub-collection of `TimePeriod`s
     *  whose start and end dates fall completely inside the interval of the given `TimePeriod`.
     *
     * - parameter period: The period to compare each other period against
     *
     * - returns: Collection of periods inside the given period
     */
    public func allInside(in period: TimePeriodProtocol) -> TimePeriodCollection {
        let collection = TimePeriodCollection()
        // Filter by period
        collection.periods = periods.filter { (timePeriod: TimePeriodProtocol) -> Bool in
            timePeriod.isInside(of: period)
        }
        return collection
    }

    /**
     *  Returns from the `TimePeriodCollection` a sub-collection of `TimePeriod`s containing
     *  the given date.
     *
     * - parameter date: The date to compare each period to
     *
     * - returns: Collection of periods intersected by the given date
     */
    public func periodsIntersected(by date: Date) -> TimePeriodCollection {
        let collection = TimePeriodCollection()
        // Filter by period
        collection.periods = periods.filter { (timePeriod: TimePeriodProtocol) -> Bool in
            timePeriod.contains(date, interval: .closed)
        }
        return collection
    }

    /**
     *  Returns from the `TimePeriodCollection` a sub-collection of `TimePeriod`s
     *  containing either the start date or the end date--or both--of the given `TimePeriod`.
     *
     *  - parameter period: The period to compare each other period to
     *
     *  - returns: Collection of periods intersected by the given period
     */
    public func periodsIntersected(by period: TimePeriodProtocol) -> TimePeriodCollection {
        let collection = TimePeriodCollection()
        // Filter by periop
        collection.periods = periods.filter { (timePeriod: TimePeriodProtocol) -> Bool in
            timePeriod.intersects(with: period)
        }
        return collection
    }

    // MARK: - Map

    public func map(_ transform: (TimePeriodProtocol) throws -> TimePeriodProtocol) rethrows -> TimePeriodCollection {
        var mappedArray = [TimePeriodProtocol]()
        mappedArray = try periods.map(transform)
        let mappedCollection = TimePeriodCollection()
        for period in mappedArray {
            mappedCollection.periods.append(period)
            mappedCollection.updateExtremes(period: period)
        }
        return mappedCollection
    }

    // MARK: - Operator Overloads

    /**
     *  Operator overload for comparing `TimePeriodCollection`s to each other
     */
    public static func == (left: TimePeriodCollection, right: TimePeriodCollection) -> Bool {
        return left.equals(right)
    }

    // MARK: - Helpers

    internal func updateExtremes(period: TimePeriodProtocol) {
        // Check incoming period against previous beginning and end date
        if count == 1 {
            _beginning = period.beginning
            _end = period.end
        } else {
            _beginning = nilOrEarlier(date1: _beginning, date2: period.beginning)
            _end = nilOrLater(date1: _end, date2: period.end)
        }
    }

    internal func updateExtremes() {
        if periods.isEmpty {
            _beginning = nil
            _end = nil
        } else {
            _beginning = periods[0].beginning
            _end = periods[0].end
            for i in 1 ..< periods.count {
                _beginning = nilOrEarlier(date1: _beginning, date2: periods[i].beginning)
                _end = nilOrEarlier(date1: _end, date2: periods[i].end)
            }
        }
    }

    internal func nilOrEarlier(date1: Date?, date2: Date?) -> Date? {
        if date1 == nil || date2 == nil {
            return nil
        } else {
            return date1!.earlierDate(date2!)
        }
    }

    internal func nilOrLater(date1: Date?, date2: Date?) -> Date? {
        if date1 == nil || date2 == nil {
            return nil
        } else {
            return date1!.laterDate(date2!)
        }
    }
}