//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2016 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import CoreFoundation

/**
 `DateComponents` encapsulates the components of a date in an extendable, structured manner.
 
 It is used to specify a date by providing the temporal components that make up a date and time in a particular calendar: hour, minutes, seconds, day, month, year, and so on. It can also be used to specify a duration of time, for example, 5 hours and 16 minutes. A `DateComponents` is not required to define all the component fields.
 
 When a new instance of `DateComponents` is created, the date components are set to `nil`.
 */
public struct DateComponents : ReferenceConvertible, Hashable, Equatable, _MutableBoxing {
    public typealias ReferenceType = NSDateComponents
    
    internal var _handle: _MutableHandle<NSDateComponents>
    
    /// Initialize a `DateComponents`, optionally specifying values for its fields.
    public init(calendar: Calendar? = nil,
                timeZone: TimeZone? = nil,
                era: Int? = nil,
                year: Int? = nil,
                month: Int? = nil,
                day: Int? = nil,
                hour: Int? = nil,
                minute: Int? = nil,
                second: Int? = nil,
                nanosecond: Int? = nil,
                weekday: Int? = nil,
                weekdayOrdinal: Int? = nil,
                quarter: Int? = nil,
                weekOfMonth: Int? = nil,
                weekOfYear: Int? = nil,
                yearForWeekOfYear: Int? = nil) {
        _handle = _MutableHandle(adoptingReference: NSDateComponents())
        if let _calendar = calendar { self.calendar = _calendar }
        if let _timeZone = timeZone { self.timeZone = _timeZone }
        if let _era = era { self.era = _era }
        if let _year = year { self.year = _year }
        if let _month = month { self.month = _month }
        if let _day = day { self.day = _day }
        if let _hour = hour { self.hour = _hour }
        if let _minute = minute { self.minute = _minute }
        if let _second = second { self.second = _second }
        if let _nanosecond = nanosecond { self.nanosecond = _nanosecond }
        if let _weekday = weekday { self.weekday = _weekday }
        if let _weekdayOrdinal = weekdayOrdinal { self.weekdayOrdinal = _weekdayOrdinal }
        if let _quarter = quarter { self.quarter = _quarter }
        if let _weekOfMonth = weekOfMonth { self.weekOfMonth = _weekOfMonth }
        if let _weekOfYear = weekOfYear { self.weekOfYear = _weekOfYear }
        if let _yearForWeekOfYear = yearForWeekOfYear { self.yearForWeekOfYear = _yearForWeekOfYear }
    }
    
    
    // MARK: - Properties
    
    /// Translate from the NSDateComponentUndefined value into a proper Swift optional
    private func _getter(_ x : Int) -> Int? { return x == NSDateComponentUndefined ? nil : x }
    
    /// Translate from the proper Swift optional value into an NSDateComponentUndefined
    private func _setter(_ x : Int?) -> Int { if let xx = x { return xx } else { return NSDateComponentUndefined } }
    
    /// The `Calendar` used to interpret the other values in this structure.
    ///
    /// - note: API which uses `DateComponents` may have different behavior if this value is `nil`. For example, assuming the current calendar or ignoring certain values.
    public var calendar: Calendar? {
        get { return _handle.map { $0.calendar } }
        set { _applyMutation { $0.calendar = newValue } }
    }
    
    /// A time zone.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var timeZone: TimeZone? {
        get { return _handle.map { $0.timeZone } }
        set { _applyMutation { $0.timeZone = newValue } }
    }
    
    /// An era or count of eras.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var era: Int? {
        get { return _handle.map { _getter($0.era) } }
        set { _applyMutation { $0.era = _setter(newValue) } }
    }
    
    /// A year or count of years.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var year: Int? {
        get { return _handle.map { _getter($0.year) } }
        set { _applyMutation { $0.year = _setter(newValue) } }
    }
    
    /// A month or count of months.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var month: Int? {
        get { return _handle.map { _getter($0.month) } }
        set { _applyMutation { $0.month = _setter(newValue) } }
    }
    
    /// A day or count of days.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var day: Int? {
        get { return _handle.map { _getter($0.day) } }
        set { _applyMutation { $0.day = _setter(newValue) } }
    }
    
    /// An hour or count of hours.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var hour: Int? {
        get { return _handle.map { _getter($0.hour) } }
        set { _applyMutation { $0.hour = _setter(newValue) } }
    }
    
    /// A minute or count of minutes.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var minute: Int? {
        get { return _handle.map { _getter($0.minute) } }
        set { _applyMutation { $0.minute = _setter(newValue) } }
    }
    
    /// A second or count of seconds.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var second: Int? {
        get { return _handle.map { _getter($0.second) } }
        set { _applyMutation { $0.second = _setter(newValue) } }
    }
    
    /// A nanosecond or count of nanoseconds.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var nanosecond: Int? {
        get { return _handle.map { _getter($0.nanosecond) } }
        set { _applyMutation { $0.nanosecond = _setter(newValue) } }
    }
    
    /// A weekday or count of weekdays.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var weekday: Int? {
        get { return _handle.map { _getter($0.weekday) } }
        set { _applyMutation { $0.weekday = _setter(newValue) } }
    }
    
    /// A weekday ordinal or count of weekday ordinals.
    /// Weekday ordinal units represent the position of the weekday within the next larger calendar unit, such as the month. For example, 2 is the weekday ordinal unit for the second Friday of the month.///
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var weekdayOrdinal: Int? {
        get { return _handle.map { _getter($0.weekdayOrdinal) } }
        set { _applyMutation { $0.weekdayOrdinal = _setter(newValue) } }
    }
    
    /// A quarter or count of quarters.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var quarter: Int? {
        get { return _handle.map { _getter($0.quarter) } }
        set { _applyMutation { $0.quarter = _setter(newValue) } }
    }
    
    /// A week of the month or a count of weeks of the month.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var weekOfMonth: Int? {
        get { return _handle.map { _getter($0.weekOfMonth) } }
        set { _applyMutation { $0.weekOfMonth = _setter(newValue) } }
    }
    
    /// A week of the year or count of the weeks of the year.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var weekOfYear: Int? {
        get { return _handle.map { _getter($0.weekOfYear) } }
        set { _applyMutation { $0.weekOfYear = _setter(newValue) } }
    }
    
    /// The ISO 8601 week-numbering year of the receiver.
    ///
    /// The Gregorian calendar defines a week to have 7 days, and a year to have 356 days, or 366 in a leap year. However, neither 356 or 366 divide evenly into a 7 day week, so it is often the case that the last week of a year ends on a day in the next year, and the first week of a year begins in the preceding year. To reconcile this, ISO 8601 defines a week-numbering year, consisting of either 52 or 53 full weeks (364 or 371 days), such that the first week of a year is designated to be the week containing the first Thursday of the year.
    ///
    /// You can use the yearForWeekOfYear property with the weekOfYear and weekday properties to get the date corresponding to a particular weekday of a given week of a year. For example, the 6th day of the 53rd week of the year 2005 (ISO 2005-W53-6) corresponds to Sat 1 January 2005 on the Gregorian calendar.
    /// - note: This value is interpreted in the context of the calendar in which it is used.
    public var yearForWeekOfYear: Int? {
        get { return _handle.map { _getter($0.yearForWeekOfYear) } }
        set { _applyMutation { $0.yearForWeekOfYear = _setter(newValue) } }
    }
    
    /// Set to true if these components represent a leap month.
    public var isLeapMonth: Bool? {
        get { return _handle.map { $0.leapMonthSet ? $0.isLeapMonth : nil } }
        set {
            _applyMutation {
                // Technically, the underlying class does not support setting isLeapMonth to nil, but it could - so we leave the API consistent.
                if let b = newValue {
                    $0.isLeapMonth = b
                } else {
                    $0.isLeapMonth = false
                }
            }
        }
    }
    
    /// Returns a `Date` calculated from the current components using the `calendar` property.
    public var date: Date? {
        if let d = _handle.map({$0.date}) {
            return d as Date
        } else {
            return nil
        }
    }
    
    // MARK: - Generic Setter/Getters
    
    /// Set the value of one of the properties, using an enumeration value instead of a property name.
    ///
    /// The calendar and timeZone and isLeapMonth properties cannot be set by this method.
    public mutating func setValue(_ value: Int?, for component: Calendar.Component) {
        _applyMutation {
            $0.setValue(_setter(value), forComponent: Calendar._toCalendarUnit([component]))
        }
    }
    
    /// Returns the value of one of the properties, using an enumeration value instead of a property name.
    ///
    /// The calendar and timeZone and isLeapMonth property values cannot be retrieved by this method.
    public func value(for component: Calendar.Component) -> Int? {
        return _handle.map {
            $0.value(forComponent: Calendar._toCalendarUnit([component]))
        }
    }
    
    // MARK: -
    
    /// Returns true if the combination of properties which have been set in the receiver is a date which exists in the `calendar` property.
    ///
    /// This method is not appropriate for use on `DateComponents` values which are specifying relative quantities of calendar components.
    ///
    /// Except for some trivial cases (e.g., 'seconds' should be 0 - 59 in any calendar), this method is not necessarily cheap.
    ///
    /// If the time zone property is set in the `DateComponents`, it is used.
    ///
    /// The calendar property must be set, or the result is always `false`.
    public var isValidDate: Bool {
        return _handle.map { $0.isValidDate }
    }
    
    /// Returns true if the combination of properties which have been set in the receiver is a date which exists in the specified `Calendar`.
    ///
    /// This method is not appropriate for use on `DateComponents` values which are specifying relative quantities of calendar components.
    ///
    /// Except for some trivial cases (e.g., 'seconds' should be 0 - 59 in any calendar), this method is not necessarily cheap.
    ///
    /// If the time zone property is set in the `DateComponents`, it is used.
    public func isValidDate(in calendar: Calendar) -> Bool {
        return _handle.map { $0.isValidDate(in: calendar) }
    }
    
    // MARK: -
    
    public var hashValue: Int {
        return _handle.map { $0.hash }
    }
    
    public static func ==(lhs: DateComponents, rhs: DateComponents) -> Bool {
        // Don't copy references here; no one should be storing anything
        return lhs._handle._uncopiedReference().isEqual(rhs._handle._uncopiedReference())
    }
    
    // MARK: - Bridging Helpers
    
    internal init(reference: NSDateComponents) {
        _handle = _MutableHandle(reference: reference)
    }
}

extension DateComponents : CustomStringConvertible, CustomDebugStringConvertible, CustomReflectable {
    public var description: String {
        return _handle.map { $0.description }
    }
    
    public var debugDescription: String {
        return _handle.map { $0.debugDescription }
    }
    
    public var customMirror: Mirror {
        var c: [(label: String?, value: Any)] = []
        if let r = calendar { c.append((label: "calendar", value: r)) }
        if let r = timeZone { c.append((label: "timeZone", value: r)) }
        if let r = era { c.append((label: "era", value: r)) }
        if let r = year { c.append((label: "year", value: r)) }
        if let r = month { c.append((label: "month", value: r)) }
        if let r = day { c.append((label: "day", value: r)) }
        if let r = hour { c.append((label: "hour", value: r)) }
        if let r = minute { c.append((label: "minute", value: r)) }
        if let r = second { c.append((label: "second", value: r)) }
        if let r = nanosecond { c.append((label: "nanosecond", value: r)) }
        if let r = weekday { c.append((label: "weekday", value: r)) }
        if let r = weekdayOrdinal { c.append((label: "weekdayOrdinal", value: r)) }
        if let r = quarter { c.append((label: "quarter", value: r)) }
        if let r = weekOfMonth { c.append((label: "weekOfMonth", value: r)) }
        if let r = weekOfYear { c.append((label: "weekOfYear", value: r)) }
        if let r = yearForWeekOfYear { c.append((label: "yearForWeekOfYear", value: r)) }
        if let r = isLeapMonth { c.append((label: "isLeapMonth", value: r)) }
        return Mirror(self, children: c, displayStyle: Mirror.DisplayStyle.struct)
    }
}

// MARK: - Bridging

extension DateComponents : _ObjectTypeBridgeable {
    public static func _isBridgedToObjectiveC() -> Bool {
        return true
    }
    
    public static func _getObjectiveCType() -> Any.Type {
        return NSDateComponents.self
    }
    
    @_semantics("convertToObjectiveC")
    public func _bridgeToObjectiveC() -> NSDateComponents {
        return _handle._copiedReference()
    }
    
    public static func _forceBridgeFromObjectiveC(_ dateComponents: NSDateComponents, result: inout DateComponents?) {
        if !_conditionallyBridgeFromObjectiveC(dateComponents, result: &result) {
            fatalError("Unable to bridge \(DateComponents.self) to \(self)")
        }
    }
    
    public static func _conditionallyBridgeFromObjectiveC(_ dateComponents: NSDateComponents, result: inout DateComponents?) -> Bool {
        result = DateComponents(reference: dateComponents)
        return true
    }
    
    public static func _unconditionallyBridgeFromObjectiveC(_ source: NSDateComponents?) -> DateComponents {
        var result: DateComponents? = nil
        _forceBridgeFromObjectiveC(source!, result: &result)
        return result!
    }
}

