//
//  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

public protocol DateToStringTrasformable {
	static func format(_ date: DateRepresentable, options: Any?) -> String
}

public protocol StringToDateTransformable {
	static func parse(_ string: String, region: Region?, options: Any?) -> DateInRegion?
}

// MARK: - Formatters

/// Format to represent a date to string
///
/// - iso: standard iso format. The ISO8601 formatted date, time and millisec "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
/// - extended: Extended format. "eee dd-MMM-yyyy GG HH:mm:ss.SSS zzz"
/// - rss: The RSS formatted date "EEE, d MMM yyyy HH:mm:ss ZZZ" i.e. "Fri, 09 Sep 2011 15:26:08 +0200"
/// - altRSS: The Alternative RSS formatted date "d MMM yyyy HH:mm:ss ZZZ" i.e. "09 Sep 2011 15:26:08 +0200"
/// - dotNet: The dotNet formatted date "/Date(%d%d)/" i.e. "/Date(1268123281843)/"
/// - httpHeader: The http header formatted date "EEE, dd MMM yyyy HH:mm:ss zzz" i.e. "Tue, 15 Nov 1994 12:45:26 GMT"
/// - custom: custom string format
/// - standard: A generic standard format date i.e. "EEE MMM dd HH:mm:ss Z yyyy"
/// - date: Date only format (short = "2/27/17", medium = "Feb 27, 2017", long = "February 27, 2017", full = "Monday, February 27, 2017"
/// - time: Time only format (short = "2:22 PM", medium = "2:22:06 PM", long = "2:22:06 PM EST", full = "2:22:06 PM Eastern Standard Time"
/// - dateTime: Date/Time format (short = "2/27/17, 2:22 PM", medium = "Feb 27, 2017, 2:22:06 PM", long = "February 27, 2017 at 2:22:06 PM EST", full = "Monday, February 27, 2017 at 2:22:06 PM Eastern Standard Time"
public enum DateToStringStyles {
	case iso(_: ISOFormatter.Options)
	case extended
	case rss
	case altRSS
	case dotNet
	case httpHeader
	case sql
	case date(_: DateFormatter.Style)
	case time(_: DateFormatter.Style)
	case dateTime(_: DateFormatter.Style)
    case dateTimeMixed(dateStyle: DateFormatter.Style, timeStyle: DateFormatter.Style)
	case custom(_: String)
	case standard
	case relative(style: RelativeFormatter.Style?)

	public func toString(_ date: DateRepresentable) -> String {
		switch self {
		case .iso(let opts):			return ISOFormatter.format(date, options: opts)
		case .extended:					return date.formatterForRegion(format: DateFormats.extended).string(from: date.date)
		case .rss:						return date.formatterForRegion(format: DateFormats.rss).string(from: date.date)
		case .altRSS:					return date.formatterForRegion(format: DateFormats.altRSS).string(from: date.date)
		case .sql:						return date.formatterForRegion(format: DateFormats.sql).string(from: date.date)
		case .dotNet:					return DOTNETFormatter.format(date, options: nil)
		case .httpHeader:				return date.formatterForRegion(format: DateFormats.httpHeader).string(from: date.date)
		case .custom(let format):		return date.formatterForRegion(format: format).string(from: date.date)
		case .standard:					return date.formatterForRegion(format: DateFormats.standard).string(from: date.date)
		case .date(let style):
			return date.formatterForRegion(format: nil, configuration: {
				$0.dateStyle = style
				$0.timeStyle = .none
			}).string(from: date.date)
		case .time(let style):
			return date.formatterForRegion(format: nil, configuration: {
				$0.dateStyle = .none
				$0.timeStyle = style
			}).string(from: date.date)
		case .dateTime(let style):
			return date.formatterForRegion(format: nil, configuration: {
				$0.dateStyle = style
				$0.timeStyle = style
			}).string(from: date.date)
        case .dateTimeMixed(let dateStyle, let timeStyle):
            return date.formatterForRegion(format: nil, configuration: {
                $0.dateStyle = dateStyle
                $0.timeStyle = timeStyle
            }).string(from: date.date)
		case .relative(let style):
			return RelativeFormatter.format(date, options: style)
		}
	}

}

// MARK: - Parsers

/// String to date transform
///
/// - iso: standard automatic iso parser (evaluate the date components automatically)
/// - extended: Extended format. "eee dd-MMM-yyyy GG HH:mm:ss.SSS zzz"
/// - rss: The RSS formatted date "EEE, d MMM yyyy HH:mm:ss ZZZ" i.e. "Fri, 09 Sep 2011 15:26:08 +0200"
/// - altRSS: The Alternative RSS formatted date "d MMM yyyy HH:mm:ss ZZZ" i.e. "09 Sep 2011 15:26:08 +0200"
/// - dotNet: The dotNet formatted date "/Date(%d%d)/" i.e. "/Date(1268123281843)/"
/// - httpHeader: The http header formatted date "EEE, dd MMM yyyy HH:mm:ss zzz" i.e. "Tue, 15 Nov 1994 12:45:26 GMT"
/// - strict: custom string format with lenient options active
/// - custom: custom string format
/// - standard: A generic standard format date i.e. "EEE MMM dd HH:mm:ss Z yyyy"
public enum StringToDateStyles {
	case iso(_: ISOParser.Options)
	case extended
	case rss
	case altRSS
	case dotNet
	case sql
	case httpHeader
	case strict(_: String)
	case custom(_: String)
	case standard

	public func toDate(_ string: String, region: Region) -> DateInRegion? {
		switch self {
		case .iso(let options):				return ISOParser.parse(string, region: region, options: options)
		case .custom(let format):			return DateInRegion(string, format: format, region: region)
		case .extended:						return DateInRegion(string, format: DateFormats.extended, region: region)
		case .sql:							return DateInRegion(string, format: DateFormats.sql, region: region)
		case .rss:							return DateInRegion(string, format: DateFormats.rss, region: Region.ISO)?.convertTo(locale: region.locale)
		case .altRSS:						return DateInRegion(string, format: DateFormats.altRSS, region: Region.ISO)?.convertTo(locale: region.locale)
		case .dotNet:						return DOTNETParser.parse(string, region: region, options: nil)
		case .httpHeader:					return DateInRegion(string, format: DateFormats.httpHeader, region: region)
		case .standard:						return DateInRegion(string, format: DateFormats.standard, region: region)
		case .strict(let format):
			let formatter = DateFormatter.sharedFormatter(forRegion: region, format: format)
			formatter.isLenient = false
			guard let absDate = formatter.date(from: string) else { return nil }
			return DateInRegion(absDate, region: region)
		}
	}
}
