//
//  PersonaInquiry2.swift
//  PersonaInquiry2
//
//  Copyright © 2020 Persona. All rights reserved.
//

import Foundation
import Persona2

#if canImport(PersonaNfc)
import PersonaNfc
#endif

#if canImport(PersonaSna)
import PersonaSna
#endif

#if canImport(PersonaWebRtc)
import PersonaWebRtc
#endif

// file private for this variable avoids clang module linker bug
private var collectedData: InquiryData? = nil

@objc(PersonaInquiry2)
class PersonaInquiry2: RCTEventEmitter {

    override func supportedEvents() -> [String] {
        ["onComplete", "onCanceled", "onError", "onEvent"]
    }

    override static func moduleName() -> String {
        "PersonaInquiry2"
    }

    override static func requiresMainQueueSetup() -> Bool {
        true
    }

    override func constantsToExport() -> [AnyHashable : Any]! {
        ["INQUIRY_SDK_VERSION": Inquiry.versionNumber]
    }

    /// To keep the bridge simple, we'll expose a single method to both configure and start the inquiry. The config is
    /// validated within the React Native side of the bridge.
    @objc
    func startInquiry(_ options: NSDictionary) {
        let templateId = options["templateId"] as? String
        let templateVersion = options["templateVersion"] as? String
        let inquiryId = options["inquiryId"] as? String
        let referenceId = options["referenceId"] as? String
        let accountId = options["accountId"] as? String
        let environment = options["environment"] as? String
        let environmentId = options["environmentId"] as? String
        let sessionToken = options["sessionToken"] as? String
        let shareToken = options["shareToken"] as? String
        let themeObject = options["iosTheme"] as? [String: String]
        let themeSource = options["themeSource"] as? String
        let fieldsObject = options["fields"] as? [String: [String: Any]]
        let returnCollectedData = options["returnCollectedData"] as? Bool ?? false
        // our iOS SDK uses Locale.current.identifier which chooses app language instead of device language
        // imo this is the appropriate level of control, that way apps can enforce what languages they support
        // and not be beholden to our SDK using a device language they don't want or expect
        // react native apps are dumb and don't configure app languages correctly on iOS
        // so we'll just use the preferred device language as that is the frequent expectation from this platform
        let locale = options["locale"] as? String ?? Locale.preferredLanguages.first
        let styleVariantString = options["styleVariant"] as? String
        let styleVariant = StyleVariant.from(rawValue: styleVariantString)
        let themeSetId = options["themeSetId"] as? String
        let disablePresentationAnimation = options["disablePresentationAnimation"] as? Bool ?? false

        var nfcAdapter: InquiryNfcAdapter? = nil
#if canImport(PersonaNfc)
        nfcAdapter = PersonaNfcAdapter()
#endif

        var snaAdapter: InquirySnaAdapter? = nil
#if canImport(PersonaSna)
        snaAdapter = PersonaSnaAdapter()
#endif

        var webRtcAdapter: InquiryWebRtcAdapter? = nil
#if canImport(PersonaWebRtc)
        webRtcAdapter = PersonaWebRtcAdapter()
#endif

        var inquiry: Inquiry? = nil
        if let inquiryId {
            let builder = Inquiry.from(inquiryId: inquiryId, delegate: self)

            builder
                .sessionToken(sessionToken)
                .shareToken(shareToken)
                .theme(PersonaInquiry2.makeTheme(from: themeObject, themeSource: themeSource))
                .nfcAdapter(nfcAdapter)
                .snaAdapter(snaAdapter)
                .collectionDelegate(returnCollectedData ? self : nil)
                .locale(locale)
                .styleVariant(styleVariant)

            if let webRtcAdapter {
                builder.webRtcAdapter(webRtcAdapter)
            }
            inquiry = builder.build()
        } else if let templateId {
            let builder = Inquiry.from(templateId: templateId, delegate: self)
            builder
                .referenceId(referenceId)
                .accountId(accountId)
                .environment(Environment.from(rawValue: environment))
                .environmentId(environmentId)
                .fields(PersonaInquiry2.fieldsFrom(fieldsObject))
                .theme(PersonaInquiry2.makeTheme(from: themeObject, themeSource: themeSource))
                .themeSetId(themeSetId)
                .nfcAdapter(nfcAdapter)
                .snaAdapter(snaAdapter)
                .collectionDelegate(returnCollectedData ? self : nil)
                .locale(locale)
                .styleVariant(styleVariant)
                .shareToken(shareToken)

            if let webRtcAdapter {
                builder.webRtcAdapter(webRtcAdapter)
            }

            inquiry = builder.build()
        } else if let templateVersion {
            let builder = Inquiry.from(templateVersion: templateVersion, delegate: self)
            builder
                .referenceId(referenceId)
                .accountId(accountId)
                .environment(Environment.from(rawValue: environment))
                .environmentId(environmentId)
                .fields(PersonaInquiry2.fieldsFrom(fieldsObject))
                .theme(PersonaInquiry2.makeTheme(from: themeObject, themeSource: themeSource))
                .themeSetId(themeSetId)
                .nfcAdapter(nfcAdapter)
                .snaAdapter(snaAdapter)
                .collectionDelegate(returnCollectedData ? self : nil)
                .locale(locale)
                .styleVariant(styleVariant)
                .shareToken(shareToken)

            if let webRtcAdapter {
                builder.webRtcAdapter(webRtcAdapter)
            }

            inquiry = builder.build()
        }

        guard let inquiry else {
            print("An error occurred while starting the Inquiry, invalid config.")
            return
        }
        DispatchQueue.main.async {
            guard let viewController = self.findTopViewController() else {
                self.sendEvent(
                    withName: "onError",
                    body: [
                        "debugMessage": "Couldn't resolve a root view controller"
                    ]
                )
                return
            }

            inquiry.start(from: viewController, animated: !disablePresentationAnimation)
        }
    }

    func findTopViewController() -> UIViewController? {
        // Resolve rootViewController to top most presented view controller
        var topViewController = UIApplication.shared.keyWindow?.rootViewController
        while let presentedViewController = topViewController?.presentedViewController {
            topViewController = presentedViewController
        }

        return topViewController
    }
}

// MARK: - InquiryDelegate

extension PersonaInquiry2: InquiryDelegate {
    func inquiryComplete(inquiryId: String, status: String, fields: [String: InquiryField]) {
        sendEvent(
            withName: "onComplete",
            body: [
                "inquiryId": inquiryId,
                "status": status,
                "fields": PersonaInquiry2.fieldsToDictionary(fields: fields),
                "collectedData": collectedData?.toDictionary() as Any
            ] as [String : Any]
        )
    }

    func inquiryCanceled(inquiryId: String?, sessionToken: String?) {
        sendEvent(
            withName: "onCanceled",
            body: [
                "inquiryId": inquiryId,
                "sessionToken": sessionToken
            ]
        )
    }

    func inquiryError(_ error: Error) {
        // ⚠️ Inquiry errored
        sendEvent(
            withName: "onError",
            body: [
                "debugMessage": error.localizedDescription
            ]
        )
    }
    
    func inquiryEventOccurred(event: InquiryEvent) {
        let canProcessEvent = switch event {
        case .start, .pageChange:
            true
        @unknown default:
            false
        }
        guard canProcessEvent else { return }
        sendEvent(
            withName: "onEvent",
            body: [
                "event": inquiryEventToDictionary(event: event)
            ] as [String : Any]
        )
    }
}

// MARK: - InquiryCollectionDelegate

extension PersonaInquiry2: InquiryCollectionDelegate {

    func collectionComplete(data: InquiryData) {
        collectedData = data
    }
}

extension InquiryData {

    func toDictionary() -> [String: Any?] {
        var inquiryData = [[String: Any?]]()
        for data in self.stepData {
            var currentStepData = [String: Any?]()

            switch data {
            case .ui(let uiData):
                currentStepData["type"] = "UiStepData"
                currentStepData["stepName"] = uiData.name
                currentStepData["componentParams"] = uiData.componentData.reduce(into: [[String: Any?]]()) { partial, element in
                    switch element {
                    case .int(let key, let value):
                        partial.append([key: value])
                    case .bool(let key, let value):
                        partial.append([key: value])
                    case .string(let key, let value):
                        partial.append([key: value])
                    case .strings(let key, let value):
                        partial.append([key: value])
                    case .double(let key, let value):
                        partial.append([key: value])
                    case .address(let key, let value):
                        partial.append([key: value])
                    default:
                        return
                    }
                }
            case .selfie(let selfieData):
                currentStepData["type"] = "SelfieStepData"
                currentStepData["stepName"] = selfieData.name
                if let centerPhoto = selfieData.centerPhoto {
                    currentStepData["centerCapture"] = [
                        "captureMethod": centerPhoto.captureMethod,
                        "absoluteFilePath": centerPhoto.filePath
                    ]
                }

                if let leftPhoto = selfieData.leftPhoto {
                    currentStepData["leftCapture"] = [
                        "captureMethod": leftPhoto.captureMethod,
                        "absoluteFilePath": leftPhoto.filePath
                    ]
                }

                if let rightPhoto = selfieData.rightPhoto {
                    currentStepData["rightCapture"] = [
                        "captureMethod": rightPhoto.captureMethod,
                        "absoluteFilePath": rightPhoto.filePath
                    ]
                }

            case .governmentId(let govIdData):
                currentStepData["type"] = "GovernmentIdStepData"
                currentStepData["stepName"] = govIdData.name

                var captures = [[String: Any]]()
                for file in govIdData.files {
                    captures.append([
                        "idClass": govIdData.idClass,
                        "captureMethod": file.captureMethod,
                        "side": file.page,
                        "frames": file.frames.map {
                            ["absoluteFilePath": $0.filePath]
                        }
                    ])
                }
                currentStepData["captures"] = captures
            case .document(let docData):
                currentStepData["type"] = "DocumentStepData"
                currentStepData["stepName"] = docData.name

                var documents = [[String: String]]()
                for file in docData.files {
                    documents.append(["absoluteFilePath": file.filePath])
                }
                currentStepData["documents"] = documents
            default:
                continue
            }
            inquiryData.append(currentStepData)
        }
        return ["stepData": inquiryData]
    }
}

// MARK: - Helpers

/// Returns a UIColor from a string
private func color(from string: String?) -> UIColor? {
    guard let string = string else {
        return nil
    }
    return UIColor(hex: string)
}

/// Returns an int for asset size from a string
private func int(from string: String?) -> Int? {
    guard let string = string else {
        return nil
    }
    return Int(string)
}

extension PersonaInquiry2 {

    enum InvalidConfiguration: Error {
        case argumentError(String)
    }

    /// Converts a dictionary into a theme
    static func makeTheme(from dictionary: [String: String]? = nil, themeSource: String? = nil) -> InquiryTheme? {
        guard let dictionary = dictionary else {
            return nil
        }

        var theme = InquiryTheme(themeSource: themeSource == "server" ? .server : .client)

        if let color = color(from: dictionary["backgroundColor"]) {
            theme.backgroundColor = color
        }

        if let color = color(from: dictionary["primaryColor"]) {
            theme.primaryColor = color
        }

        if let color = color(from: dictionary["darkPrimaryColor"]) {
            theme.darkPrimaryColor = color
        }

        if let color = color(from: dictionary["accentColor"]) {
            theme.accentColor = color
        }

        if let color = color(from: dictionary["errorColor"]) {
            theme.errorColor = color
        }

        if let fontName = dictionary["errorTextFont"],
           let font = UIFont(name: fontName, size: 16) {
            theme.errorTextFont = font
        }

        if let color = color(from: dictionary["overlayBackgroundColor"]) {
            theme.overlayBackgroundColor = color
        }

        if let alignment = dictionary["titleTextAlignment"] {
            var titleTextAlignment: NSTextAlignment {
                switch alignment {
                case "left":
                    return .left
                case "center":
                    return .center
                case "right":
                    return .right
                case "justified":
                    return .justified
                case "natural":
                    return .natural
                default:
                    // Default to left on invalid input
                    return .left
                }
            }
            theme.titleTextAlignment = titleTextAlignment
        }

        if let color = color(from: dictionary["titleTextColor"]) {
            theme.titleTextColor = color
        }

        if let fontName = dictionary["titleTextFont"],
           let font = UIFont(name: fontName, size: 28) {
            theme.titleTextFont = font
        }

        if let fontName = dictionary["cardTitleTextFont"],
           let font = UIFont(name: fontName, size: 20) {
            theme.cardTitleTextFont = font
        }

        if let alignment = dictionary["bodyTextAlignment"] {
            var bodyTextAlignment: NSTextAlignment {
                switch alignment {
                case "left":
                    return .left
                case "center":
                    return .center
                case "right":
                    return .right
                case "justified":
                    return .justified
                case "natural":
                    return .natural
                default:
                    // Default to left on invalid input
                    return .left
                }
            }
            theme.bodyTextAlignment = bodyTextAlignment
        }

        if let color = color(from: dictionary["bodyTextColor"]) {
            theme.bodyTextColor = color
        }

        if let fontName = dictionary["bodyTextFont"],
           let font = UIFont(name: fontName, size: 17) {
            theme.bodyTextFont = font
        }

        if let color = color(from: dictionary["footnoteTextColor"]) {
            theme.footnoteTextColor = color
        }

        if let fontName = dictionary["footnoteTextFont"],
           let font = UIFont(name: fontName, size: 14) {
            theme.footnoteTextFont = font
        }

        if let color = color(from: dictionary["formLabelTextColor"]) {
            theme.formLabelTextColor = color
        }

        if let fontName = dictionary["formLabelTextFont"],
           let font = UIFont(name: fontName, size: 16) {
            theme.formLabelTextFont = font
        }

        if let color = color(from: dictionary["textFieldTextColor"]) {
            theme.textFieldTextColor = color
        }

        if let color = color(from: dictionary["textFieldBackgroundColor"]) {
            theme.textFieldBackgroundColor = color
        }

        if let color = color(from: dictionary["textFieldBorderColor"]) {
            theme.textFieldBorderColor = color
        }

        if let radiusString = dictionary["textFieldCornerRadius"],
           let radiusNumber = NumberFormatter().number(from: radiusString),
           let radius = CGFloat(exactly: radiusNumber) {
            theme.textFieldCornerRadius = radius
        }

        if let fontName = dictionary["textFieldFont"],
           let font = UIFont(name: fontName, size: 16) {
            theme.textFieldFont = font
        }

        if let fontName = dictionary["textFieldPlaceholderFont"],
           let font = UIFont(name: fontName, size: 16) {
            theme.textFieldPlaceholderFont = font
        }

        if let color = color(from: dictionary["pickerTextColor"]) {
            theme.pickerTextColor = color
        }

        if let fontName = dictionary["pickerTextFont"],
           let font = UIFont(name: fontName, size: 18) {
            theme.pickerTextFont = font
        }

        if let color = color(from: dictionary["buttonBackgroundColor"]) {
            theme.buttonBackgroundColor = color
        }

        if let color = color(from: dictionary["buttonDisabledBackgroundColor"]) {
            theme.buttonDisabledBackgroundColor = color
        }

        if let color = color(from: dictionary["buttonTouchedBackgroundColor"]) {
            theme.buttonTouchedBackgroundColor = color
        }

        if let color = color(from: dictionary["buttonImageTintColor"]) {
            theme.buttonImageTintColor = color
        }

        if let color = color(from: dictionary["buttonTextColor"]) {
            theme.buttonTextColor = color
        }

        if let color = color(from: dictionary["buttonDisabledTextColor"]) {
            theme.buttonDisabledTextColor = color
        }

        if let alignment = dictionary["buttonTextAlignment"] {
            var buttonTextAlignment: NSTextAlignment {
                switch alignment {
                case "left":
                    return .left
                case "center":
                    return .center
                case "right":
                    return .right
                case "justified":
                    return .justified
                case "natural":
                    return .natural
                default:
                    // Default to left on invalid input
                    return .left
                }
            }
            theme.buttonTextAlignment = buttonTextAlignment
        }

        if let radiusString = dictionary["buttonCornerRadius"],
           let radiusNumber = NumberFormatter().number(from: radiusString),
           let radius = CGFloat(exactly: radiusNumber) {
            theme.buttonCornerRadius = radius
        }

        if let widthString = dictionary["buttonBorderWidth"],
           let widthNumber = NumberFormatter().number(from: widthString),
           let width = CGFloat(exactly: widthNumber) {
            theme.buttonBorderWidth = width
        }

        if let color = color(from: dictionary["buttonBorderColor"]) {
            theme.buttonBorderColor = color
        }

        if let fontName = dictionary["buttonFont"],
           let font = UIFont(name: fontName, size: 18) {
            theme.buttonFont = font
        }

        if let color = color(from: dictionary["buttonSecondaryBackgroundColor"]) {
            theme.buttonSecondaryBackgroundColor = color
        }

        if let color = color(from: dictionary["buttonSecondaryDisabledBackgroundColor"]) {
            theme.buttonSecondaryDisabledBackgroundColor = color
        }

        if let color = color(from: dictionary["buttonSecondaryTouchedBackgroundColor"]) {
            theme.buttonSecondaryTouchedBackgroundColor = color
        }

        if let color = color(from: dictionary["buttonSecondaryImageTintColor"]) {
            theme.buttonSecondaryImageTintColor = color
        }

        if let color = color(from: dictionary["buttonSecondaryTextColor"]) {
            theme.buttonSecondaryTextColor = color
        }

        if let color = color(from: dictionary["buttonSecondaryDisabledTextColor"]) {
            theme.buttonSecondaryDisabledTextColor = color
        }

        if let alignment = dictionary["buttonSecondaryTextAlignment"] {
            var buttonSecondaryTextAlignment: NSTextAlignment {
                switch alignment {
                case "left":
                    return .left
                case "center":
                    return .center
                case "right":
                    return .right
                case "justified":
                    return .justified
                case "natural":
                    return .natural
                default:
                    // Default to left on invalid input
                    return .left
                }
            }
            theme.buttonSecondaryTextAlignment = buttonSecondaryTextAlignment
        }

        if let radiusString = dictionary["buttonSecondaryCornerRadius"],
           let radiusNumber = NumberFormatter().number(from: radiusString),
           let radius = CGFloat(exactly: radiusNumber) {
            theme.buttonSecondaryCornerRadius = radius
        }

        if let widthString = dictionary["buttonSecondaryBorderWidth"],
           let widthNumber = NumberFormatter().number(from: widthString),
           let width = CGFloat(exactly: widthNumber) {
            theme.buttonSecondaryBorderWidth = width
        }

        if let color = color(from: dictionary["buttonSecondaryBorderColor"]) {
            theme.buttonSecondaryBorderColor = color
        }

        if let fontName = dictionary["buttonSecondaryFont"],
           let font = UIFont(name: fontName, size: 18) {
            theme.buttonSecondaryFont = font
        }

        if let color = color(from: dictionary["selectedCellBackgroundColor"]) {
            theme.selectedCellBackgroundColor = color
        }

        if let color = color(from: dictionary["closeButtonTintColor"]) {
            theme.closeButtonTintColor = color
        }

        if let color = color(from: dictionary["cancelButtonBackgroundColor"]) {
            theme.cancelButtonBackgroundColor = color
        }

        if let color = color(from: dictionary["cancelButtonTextColor"]) {
            theme.cancelButtonTextColor = color
        }

        if let color = color(from: dictionary["cancelButtonAlternateBackgroundColor"]) {
            theme.cancelButtonAlternateBackgroundColor = color
        }

        if let color = color(from: dictionary["cancelButtonAlternateTextColor"]) {
            theme.cancelButtonAlternateTextColor = color
        }

        if let loadingAnimationAssetName = dictionary["loadingAnimationAssetName"],
           let loadingAnimationAssetWidth = int(from: dictionary["loadingAnimationAssetWidth"]),
           let loadingAnimationAssetHeight = int(from: dictionary["loadingAnimationAssetHeight"]),
           let loadingAnimationAssetPath = Bundle(for: PersonaInquiry2.self).path(forResource: loadingAnimationAssetName, ofType: "json") {
            let customLoadingAnimationAsset = InquiryTheme.AnimationAsset(
                path: loadingAnimationAssetPath,
                size: CGSize(width: loadingAnimationAssetWidth, height: loadingAnimationAssetHeight),
                loopMode: .loop
            )

            theme.loadingAnimation = customLoadingAnimationAsset
        }

        if let processingAnimationAssetName = dictionary["processingAnimationAssetName"],
           let processingAnimationAssetWidth = int(from: dictionary["processingAnimationAssetWidth"]),
           let processingAnimationAssetHeight = int(from: dictionary["processingAnimationAssetHeight"]),
           let processingAnimationAssetPath = Bundle(for: PersonaInquiry2.self).path(forResource: processingAnimationAssetName, ofType: "json") {
            let customProcessingAnimationAsset = InquiryTheme.AnimationAsset(
                path: processingAnimationAssetPath,
                size: CGSize(width: processingAnimationAssetWidth, height: processingAnimationAssetHeight),
                loopMode: .loop
            )

            theme.processingAnimation = customProcessingAnimationAsset
        }

        if let assetName = dictionary["selfieAnimationAssetName"],
           let width = int(from: dictionary["selfieAnimationAssetWidth"]),
           let height = int(from: dictionary["selfieAnimationAssetHeight"]),
           let path = Bundle(for: PersonaInquiry2.self).path(forResource: assetName, ofType: "json") {
            let customAsset = InquiryTheme.AnimationAsset(
                path: path,
                size: CGSize(width: width, height: height),
                loopMode: .loop
            )

            theme.selfieAsset = customAsset
        }

        return theme
    }

    /// Converts a dictionary containing elements like
    ///     `["firstName": ["type": "string", "value": "Name"]]` to
    ///     `["firstName": .string("Name")]`
    static func fieldsFrom(_ dictionary: [String: [String: Any]]?) -> [String: InquiryField] {
        dictionary?.compactMapValues { element in
            guard let type = element["type"] as? String else {
                return nil
            }

            switch type {
            case "string":
                guard let value = element["value"] as? String else {
                    return nil
                }
                return .string(value)

            case "integer":
                guard let value = element["value"] as? Int else {
                    return nil
                }
                return .int(value)

            case "float":
                if let value = element["value"] as? Float {
                    return .float(value)
                } else if let value = element["value"] as? Double {
                    return .float(Float(value))
                }
                return nil

            case "boolean":
                guard let value = element["value"] as? Bool else {
                    return nil
                }
                return .bool(value)

            case "date":
                guard let value = element["value"] as? String,
                      let date = DateFormatters.dateFormatter.date(from: value) else {
                    return nil
                }
                return .date(date)

            case "datetime":
                guard let value = element["value"] as? String,
                      let date = DateFormatters.dateTimeFormatter.date(from: value) else {
                    return nil
                }
                return .datetime(date)

            default:
                return .unknown
            }
        } ?? [:]
    }



    /// Converts a dictionary containing InquiryFields like
    ///     `["firstName": .string("Name")]` to
    ///     `["firstName": ["type": "string", "value": "Name"]]`
    static func fieldsToDictionary(fields: [String: InquiryField]) -> [String: [String: Any?]] {
        fields.mapValues {
            switch $0 {
            case .string(let value):
                return [
                    "type": "string",
                    "value": value
                ]

            case .int(let value):
                return [
                    "type": "integer",
                    "value": value
                ]

            case .float(let value):
                return [
                    "type": "float",
                    "value": value
                ]

            case .bool(let value):
                return [
                    "type": "boolean",
                    "value": value
                ]

            case .date(let value):
                return [
                    "type": "date",
                    "value": value
                ]

            case .datetime(let value):
                return [
                    "type": "datetime",
                    "value": value
                ]

            case .unknown:
                fallthrough

            @unknown default:
                return [
                    "type": "unknown",
                    "value": "unknown"
                ]
            }
        }
    }

    /// Converts an InquiryEvent like
    ///     `.pageChange(name: "start", path: "inquriy/start")]` to
    ///     `["name": "start", "path": "inquriy/start"]`
    private func inquiryEventToDictionary(event: InquiryEvent) -> [String: String] {
        switch event {
            case .start(let value):
                return [
                    "type": "start",
                    "inquiryId": value.inquiryId,
                    "sessionToken": value.sessionToken,
                ]
            case .pageChange(let value):
                return [
                    "type": "page_change",
                    "name": value.name,
                    "path": value.path,
                ]
            @unknown default:
                return [
                    "type": "unknown",
                    "value": "unknown"
                ]
        }
    }
}

extension Environment {
    public static func from(rawValue: String?) -> Environment {
        // If nil, return default: production
        guard let rawValue = rawValue else {
            return .production
        }

        // If garbage passed in and can't be converted to Environment, then return default: production
        if let environment = Environment(rawValue: rawValue) {
            return environment
        } else {
            return .production
        }
    }
}

extension StyleVariant {
    public static func from(rawValue: String?) -> StyleVariant? {
        guard let rawValue = rawValue else {
            return nil
        }

        if let styleVariant = StyleVariant(rawValue: rawValue) {
            return styleVariant
        } else {
            return nil
        }
    }
}

// https://www.hackingwithswift.com/example-code/uicolor/how-to-convert-a-hex-color-to-a-uicolor
extension UIColor {
    public convenience init?(hex: String) {
        let r, g, b, a: CGFloat

        if hex.hasPrefix("#") {
            let start = hex.index(hex.startIndex, offsetBy: 1)
            let hexColor = String(hex[start...])

            if hexColor.count == 8 {
                let scanner = Scanner(string: hexColor)
                var hexNumber: UInt64 = 0

                if scanner.scanHexInt64(&hexNumber) {
                    r = CGFloat((hexNumber & 0xff000000) >> 24) / 255
                    g = CGFloat((hexNumber & 0x00ff0000) >> 16) / 255
                    b = CGFloat((hexNumber & 0x0000ff00) >> 8) / 255
                    a = CGFloat(hexNumber & 0x000000ff) / 255

                    self.init(red: r, green: g, blue: b, alpha: a)
                    return
                }
            } else if hexColor.count == 6 {
                let scanner = Scanner(string: hexColor)
                var hexNumber: UInt64 = 0

                if scanner.scanHexInt64(&hexNumber) {
                    r = CGFloat((hexNumber & 0xff0000) >> 16) / 255
                    g = CGFloat((hexNumber & 0x00ff00) >> 8) / 255
                    b = CGFloat((hexNumber & 0x0000ff)) / 255

                    self.init(red: r, green: g, blue: b, alpha: CGFloat(100))
                    return
                }
            }
        }

        return nil
    }
}

struct DateFormatters {

    /// Returns a formatter that transforms a date from "yyyy-MM-dd"
    static let dateFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd"
        return formatter
    }()

    /// Returns a formatter that transforms a date from a ISO8601 string
    static let dateTimeFormatter: DateFormatter = {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
        return formatter
    }()
}
