//
//  WindDirection.swift
//  1Weather
//
//  Created by Demid Merzlyakov on 19.02.2021.
//

import Foundation
import UIKit

public enum WeatherType: String, CaseIterable {
    case clear = "clear"
    case partlyCloudy = "partlyCloudy"
    case snowy = "snowy"
    case mostlyCloudy = "mostlyCloudy"
    case thunderstorm = "thunderstorm"
    case heavySnow = "heavySnow"
    case lightSnow = "lightSnow"
    case freezingRain = "freezingRain"
    case freezingFog = "freezingFog"
    case lightHail = "lightHail"
    case blowingDust = "blowingDust"
    case haze = "haze"
    case lightFog = "lightFog"
    case lightDrizzle = "lightDrizzle"
    case heavyRain = "heavyRain"
    case smoke = "smoke"
    case unknown = "unknown"
    
    public func localized(isDay: Bool) -> String {
        return "forecast.\(self.stringId(isDay: isDay))".localized()
    }

    public func image(isDay: Bool) -> UIImage {
        guard let result = UIImage(named: self.stringId(isDay: isDay)) else {
            assertionFailure("Image is missing for WeatherType \(self)")
            return UIImage()
        }
        return result
    }
    
    private static let dayNightCustomizable: Set<WeatherType> = Set<WeatherType>(arrayLiteral: .clear, .partlyCloudy, .snowy, .mostlyCloudy)
    
    private func stringId(isDay: Bool) -> String {
        var stringId = self.rawValue
        if WeatherType.dayNightCustomizable.contains(self) {
            stringId += isDay ? "Day" : "Night"
        }
        return stringId
    }
}

public enum TemperatureType:String, CaseIterable {
    case kelvin = "temp.kelvin"
    case celsius = "temp.celsius"
    case fahrenheit = "temp.fahrenheit"
    
    var unit:UnitTemperature {
        switch self {
        case .kelvin:
            return .kelvin
        case .celsius:
            return .celsius
        case .fahrenheit:
            return .fahrenheit
        }
    }
}

public enum WindSpeedType:String, CaseIterable {
    case metersPerSecond = "windSpeed.mps"
    case kilometersPerHour = "windSpeed.kph"
    case milesPerHour = "windSpeed.mph"
    case knots = "windSpeed.knots"
    
    var unit:UnitSpeed {
        switch self {
        case .metersPerSecond:
            return .metersPerSecond
        case .kilometersPerHour:
            return .kilometersPerHour
        case .milesPerHour:
            return .milesPerHour
        case .knots:
            return .knots
        }
    }
}

public enum PressureType:String, CaseIterable {
    case inchesOfMercury = "pressure.inch"
    case millibars = "pressure.mb"
    case millimetersOfMercury = "pressure.mm"
    case atmosphere = "pressure.atm"
    case kilopascals = "pressure.kpa"
    
    var unit:UnitPressure {
        switch self {
        case .inchesOfMercury:
            return .inchesOfMercury
        case .millibars:
            return .millibars
        case .millimetersOfMercury:
            return .millimetersOfMercury
        case .atmosphere:
            return UnitPressure(symbol: "atm", converter: UnitConverterLinear(coefficient: 101325))
        case .kilopascals:
            return .kilopascals
        }
    }
}

public enum DistanceType:String, CaseIterable {
    case miles = "distance.mi"
    case kilometers = "distance.km"
    
    var unit:UnitLength {
        switch self {
        case .miles:
            return .miles
        case .kilometers:
            return .kilometers
        }
    }
}

public enum WeatherConditionType: CaseIterable {
    case precipitation
    case humidity
    case uvIndex
    case pressure
    case dewPoint
    case visibility
    case wind
    
    var localized:String {
        switch self {
        case .precipitation:
            return "condition.precipitation".localized()
        case .humidity:
            return "condition.humidity".localized()
        case .uvIndex:
            return "condition.uvIndex".localized()
        case .pressure:
            return "condition.pressure".localized()
        case .dewPoint:
            return "condition.dewPoint".localized()
        case .visibility:
            return "condition.visibility".localized()
        case .wind:
            return "condition.wind".localized()
        }
    }
    
    var image:UIImage? {
        switch self {
        case .precipitation:
            return UIImage(named: "precipitation")
        case .humidity:
            return UIImage(named: "humidity")
        case .uvIndex:
            return UIImage(named: "uv_index")
        case .pressure:
            return UIImage(named: "pressure")
        case .dewPoint:
            return UIImage(named: "dewPoint")
        case .visibility:
            return UIImage(named: "visibility")
        case .wind:
            return UIImage(named: "blowingDust")
        }
    }
}

public enum WindDirection: String, Codable, CaseIterable {
    case north = "N"
    case northNorthEast = "NNE"
    case northEast = "NE"
    case eastNorthEast = "ENE"
    case east = "E"
    case eastSouthEast = "ESE"
    case southEast = "SE"
    case southSouthEast = "SSE"
    case south = "S"
    case southSouthWest = "SSW"
    case southWest = "SW"
    case westSouthWest = "WSW"
    case west = "W"
    case westNorthWest = "WNW"
    case northWest = "NW"
    case northNorthWest = "NNW"
    
    var degrees:CGFloat {
        switch self {
        case .north:
            return 0
        case .northNorthEast:
            return 22.5
        case .northEast:
            return 45
        case .eastNorthEast:
            return 67.5
        case .east:
            return 90
        case .eastSouthEast:
            return 112.5
        case .southEast:
            return 135
        case .southSouthEast:
            return 157.5
        case .south:
            return 180
        case .southSouthWest:
            return 202.5
        case .southWest:
            return 225
        case .westSouthWest:
            return 247.5
        case .west:
            return 270
        case .westNorthWest:
            return 292.5
        case .northWest:
            return 315
        case .northNorthWest:
            return 337.5
        }
    }
}


/// Not everybody uses a 7 day week, but not a lot of people.
public enum WeekDay: String, Codable {
    case monday = "Monday"
    case tuesday = "Tuesday"
    case wednesday = "Wednesday"
    case thursday = "Thursday"
    case friday = "Friday"
    case saturday = "Saturday"
    case sunday = "Sunday"
    
    public init?(grigorianWeekDayNumber: Int) {
        switch grigorianWeekDayNumber {
        case 1:
            self = WeekDay.sunday
        case 2:
            self = WeekDay.monday
        case 3:
            self = WeekDay.tuesday
        case 4:
            self = WeekDay.wednesday
        case 5:
            self = WeekDay.thursday
        case 6:
            self = WeekDay.friday
        case 7:
            self = WeekDay.saturday
        default:
            return nil
        }
    }
}

public enum DayTime: Int, CaseIterable {
    case morning = 7
    case noon = 12
    case evening = 18
    case night = 23
    
    public var localized:String {
        switch self {
        case .morning:
            return "dayTime.morning".localized()
        case .noon:
            return "dayTime.noon".localized()
        case .evening:
            return "dayTime.evening".localized()
        case .night:
            return "dayTime.night".localized()
        }
    }
    
    /// For a given date (with arbitrary time) in a given time zone returns a date object for canonical time for the given DayTime.
    /// Example 1: Given a date 2020-01-01 04:33 UTC, dayTime .noon and timezone UTC+0, it will return 2020-01-01 12-00 UTC.
    /// Example 2 (with a non-UTC time zone): given a date 2020-01-01 04:33 UTC, dayTime .noon and timezone UTC-5 it will return 2019-12-31 12 UTC,
    /// because in UTC-5 it's still 2019-12-31, so it will return the canonical time for noon within that day.
    ///
    /// The canonical times are:
    /// • `.morning`: 7,
    /// • `.noon`: 12,
    /// • `.evening`: 18,
    /// • `.night`: 23
    ///
    /// - Parameters:
    ///   - dayTime: `DayTime`, for which we need to get the canonical time.
    ///   - day: A date for which we need to get a canonical time. The time part may be arbitrary as long as it's within the given date (according to the timeZone parameter).
    ///   - timeZone: TimeZone to use for time calculations.
    /// - Returns: A date that corresponds to a canonical time within the day specified by the `day` parameter in a given timeZone
    public static func canonicalTime(of dayTime: DayTime, for day: Date, in timeZone: TimeZone) -> Date? {
        var calendar = Calendar(identifier: .gregorian)
        calendar.timeZone = timeZone
        return calendar.date(bySettingHour: dayTime.rawValue, minute: 0, second: 0, of: day)
    }
    
    public init?(hour: Int) {
        self.init(rawValue: hour)
    }
}

public enum CelestialState: String, Codable {
    case normal = ""
    case alwaysUp = "always_up"
    case neverUp = "never_up"
}

public enum MoonPhase: String, Codable {
    case newMoon = "New Moon"
    case waxingCrescentMoon = "Waxing Crescent Moon"
    case quarterMoon = "Quarter Moon"
    case waxingGibbousMoon = "Waxing Gibbous Moon"
    case fullMoon = "Full Moon"
    case waningGibbousMoon = "Waning Gibbous Moon"
    case lastQuarterMoon = "Last Quarter Moon"
    case waningCrescentMoon = "Waning Crescent Moon"
    case unknown = ""
    
    var localized:String {
        switch self {
        case .newMoon:
            return "moon.phase.new".localized()
        case .waxingCrescentMoon:
            return "moon.phase.waxingCrescent".localized()
        case .quarterMoon:
            return "moon.phase.firstQuarter".localized()
        case .waxingGibbousMoon:
            return "moon.phase.waxingGibbous".localized()
        case .fullMoon:
            return "moon.phase.fullMoon".localized()
        case .waningGibbousMoon:
            return "moon.phase.waningGibbous".localized()
        case .lastQuarterMoon:
            return "moon.phase.thirdQuarter".localized()
        case .waningCrescentMoon:
            return "moon.phase.wanningCrescent".localized()
        case .unknown:
            return "unknown"
        }
    }
}

public struct Time: CustomStringConvertible, Codable {
    public let hours: UInt8
    public let minutes: UInt8
    
    public var description: String {
        var amOrPm = "am"
        
        var convertedHours = hours
        if convertedHours > 12 {
            amOrPm = "pm"
            convertedHours -= 12
        }
        if convertedHours == 0 {
            convertedHours = 12
        }
        return "\(String(format: "%02d", convertedHours)):\(minutes) \(amOrPm)"
    }
    
    public init(hours: UInt8, minutes: UInt8) {
        self.hours = hours
        self.minutes = minutes
    }
    
    public init(from decoder: Decoder) throws {
        let valueContainer = try decoder.singleValueContainer()
        let value = try valueContainer.decode(String.self).trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
        
        var isAm: Bool?
        if value.contains("am") {
            isAm = true
        }
        else if value.contains("pm") {
            isAm = false
        }
        
        let timeRegEx = try! NSRegularExpression(pattern: "(\\d\\d?):(\\d\\d?)") //it's a correct regexp, otherwise it'd better crash during development.
        let allMatches = timeRegEx.matches(in: value, range: NSRangeFromString(value))
        let nsValue = value as NSString
        guard let match = allMatches.first else {
            throw DecodingError.dataCorruptedError(in: valueContainer, debugDescription: "Couldn't parse time info from string \(value).")
        }
        let parsedHours = UInt8(nsValue.substring(with: match.range(at: 1)))! // we know if will get parsed, because regexp guarantees it's two digits without a sign.
        let parsedMinutes = UInt8(nsValue.substring(with: match.range(at: 1)))! // same here
        
        guard parsedMinutes >= 0 && parsedMinutes <= 59 else {
            throw DecodingError.dataCorruptedError(in: valueContainer, debugDescription: "Minute value is not acceptible: \(parsedMinutes)")
        }
        minutes = parsedMinutes
        
        if let isAm = isAm {
            guard parsedHours >= 1 && parsedHours <= 12 else {
                throw DecodingError.dataCorruptedError(in: valueContainer, debugDescription: "Hour value is not acceptible for a 12h format: \(parsedHours)")
            }
            
            if parsedHours == 12 {
                if isAm {
                    hours = 0
                }
                else {
                    hours = 12
                }
            }
            else {
                if !isAm {
                    hours = parsedHours + 12
                }
                else {
                    hours = parsedHours
                }
            }
        }
        else {
            guard parsedHours >= 0 && parsedHours <= 23 else {
                throw DecodingError.dataCorruptedError(in: valueContainer, debugDescription: "Hour value is not acceptible for a 24h format: \(parsedHours)")
            }
            hours = parsedHours
        }
    }
    
    public func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(self.description)
    }
}

public typealias Temperature = Measurement<UnitTemperature>
public typealias WindSpeed = Measurement<UnitSpeed>
public typealias Percent = UInt
public typealias Visibility = Measurement<UnitLength>
public typealias Pressure = Measurement<UnitPressure>

extension Measurement {
    public init?(valueString: String?, unit: UnitType) {
        guard let valueString = valueString else {
            return nil
        }
        guard let value = Double(valueString) else {
            return nil
        }
        self.init(value: value, unit: unit)
    }
}
