//
//  SwiftDate
//  Parse, validate, manipulate, and display dates, time and timezones in Swift
//
//  Created by Daniele Margutti
//   - Web: https://www.danielemargutti.com
//   - Twitter: https://twitter.com/danielemargutti
//   - Mail: hello@danielemargutti.com
//
//  Copyright © 2019 Daniele Margutti. Licensed under MIT License.
//

import Foundation

/// Region define a context both for `Date` and `DateInRegion`.
/// Each `Date` is assigned to the currently set `SwiftDate.default
public struct Region: Decodable, Encodable, Equatable, Hashable, CustomStringConvertible {

	// MARK: - Properties

	/// Calendar associated with region
	public let calendar: Calendar

	/// Locale associated with region
	public var locale: Locale { return calendar.locale! }

	/// Timezone associated with region
	public var timeZone: TimeZone { return calendar.timeZone }

	/// Description of the object
	public var description: String {
		return "{calendar='\(calendar.identifier)', timezone='\(timeZone.identifier)', locale='\(locale.identifier)'}"
	}

	public func hash(into hasher: inout Hasher) {
		hasher.combine(calendar)
	}

	// MARK: Initialization

	/// Initialize a new region with given parameters.
	///
	/// - Parameters:
	///   - calendar: calendar for region, if not specified `defaultRegions`'s calendar is used instead.
	///   - timezone: timezone for region, if not specified `defaultRegions`'s timezone is used instead.
	///   - locale: locale for region, if not specified `defaultRegions`'s locale is used instead.
	public init(calendar: CalendarConvertible = SwiftDate.defaultRegion.calendar,
				zone: ZoneConvertible = SwiftDate.defaultRegion.timeZone,
				locale: LocaleConvertible = SwiftDate.defaultRegion.locale) {
		self.calendar = Calendar.newCalendar(calendar, configure: {
			$0.timeZone = zone.toTimezone()
			$0.locale = locale.toLocale()
		})
	}

	/// Initialize a new Region by reading the `timeZone`,`calendar` and `locale`
	/// parameters from the passed `DateComponents` instance.
	/// For any `nil` parameter the correspondent `SwiftDate.defaultRegion` is used instead.
	///
	/// - Parameter fromDateComponents: date components
	public init(fromDateComponents components: DateComponents) {
		let tz = (components.timeZone ?? Zones.current.toTimezone())
		let cal = (components.calendar ?? Calendars.gregorian.toCalendar())
		let loc = (cal.locale ?? Locales.current.toLocale())
        self.init(calendar: cal, zone: tz, locale: loc)
	}

	public static var UTC: Region {
		return Region(calendar: Calendar.autoupdatingCurrent,
					  zone: Zones.gmt.toTimezone(),
					  locale: Locale.autoupdatingCurrent)
	}

	/// Return the current local device's region where all attributes are set to the device's values.
	///
	/// - Returns: Region
	public static var local: Region {
		return Region(calendar: Calendar.autoupdatingCurrent,
					  zone: TimeZone.autoupdatingCurrent,
					  locale: Locale.autoupdatingCurrent)
	}

	/// ISO Region is defined by the gregorian calendar, gmt timezone and english posix locale
	public static var ISO: Region {
		return Region(calendar: Calendars.gregorian.toCalendar(),
					  zone: Zones.gmt.toTimezone(),
					  locale: Locales.englishUnitedStatesComputer)
	}

	/// Return an auto updating region where all settings are obtained from the current's device settings.
	///
	/// - Returns: Region
	public static var current: Region {
		return Region(calendar: Calendar.autoupdatingCurrent,
					  zone: TimeZone.autoupdatingCurrent,
					  locale: Locale.autoupdatingCurrent)
	}

	/// Return a new region in current's device timezone with optional adjust of the calendar and locale.
	///
	/// - Parameters:
	///   - locale: locale to set
	///   - calendar: calendar to set
	/// - Returns: region
	public static func currentIn(locale: LocaleConvertible? = nil, calendar: CalendarConvertible? = nil) -> Region {
		return Region(calendar: (calendar ?? SwiftDate.defaultRegion.calendar),
					  zone: SwiftDate.defaultRegion.timeZone,
					  locale: (locale ?? SwiftDate.defaultRegion.locale))
	}

	/// Return the current date expressed into the receiver region.
	///
	/// - Returns: `DateInRegion` instance
	public func nowInThisRegion() -> DateInRegion {
		return DateInRegion(Date(), region: self)
	}

	// MARK: - Codable Support

	enum CodingKeys: String, CodingKey {
		case calendar
		case locale
		case timezone
	}

	public func encode(to encoder: Encoder) throws {
		var container = encoder.container(keyedBy: CodingKeys.self)
		try container.encode(calendar.timeZone.identifier, forKey: .timezone)
		try container.encode(calendar.locale!.identifier, forKey: .locale)
		try container.encode(calendar.identifier.description, forKey: .calendar)
	}

	public init(from decoder: Decoder) throws {
		let values = try decoder.container(keyedBy: CodingKeys.self)
		let calId = Calendar.Identifier( try values.decode(String.self, forKey: .calendar))
		let tz = (TimeZone(identifier: try values.decode(String.self, forKey: .timezone)) ?? SwiftDate.defaultRegion.timeZone)
		let lc = Locale(identifier: try values.decode(String.self, forKey: .locale))
		calendar = Calendar.newCalendar(calId, configure: {
			$0.timeZone = tz
			$0.locale = lc
		})
	}

	// MARK: - Comparable

	public static func == (lhs: Region, rhs: Region) -> Bool {
		// Note: equality does not consider other parameters than the identifier of the major
		// attributes (calendar, timezone and locale). Deeper comparisor must be made directly
		// between Calendar (it may fail when you encode/decode autoUpdating calendars).
		return
			(lhs.calendar.identifier == rhs.calendar.identifier) &&
			(lhs.timeZone.identifier == rhs.timeZone.identifier) &&
			(lhs.locale.identifier == rhs.locale.identifier)
	}
}
