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

internal class RelativeFormatterLanguagesCache {

    static let shared = RelativeFormatterLanguagesCache()

    @Atomic
    private(set) var cachedValues = [String: [String: Any]]()
    
    func flavoursForLocaleID(_ langID: String) -> [String: Any]? {
        do {
            
            guard let cachedValue = cachedValues[langID] else {
                var fileURL = Bundle.appModule?.url(forResource: langID, withExtension: "json", subdirectory: "langs")
                if fileURL == nil {
                    fileURL = Bundle(for: RelativeFormatter.self).resourceURL?.appendingPathComponent("langs/\(langID).json")
                }
                
                guard let fullURL = fileURL else {
                    return nil
                }
                let data = try Data(contentsOf: fullURL)
                let json = try JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)

                if let value = json as? [String: Any] {
                    cachedValues[langID] = value
                    return value
                }
                return nil
            }
            
            return cachedValue

        } catch {
            debugPrint("Failed to read data for language id: \(langID)")
            return nil
        }
    }
}

public enum RelativeFormatterLanguage: String, CaseIterable {
    case af = "af" // Locales.afrikaans
    case am = "am" // Locales.amharic
    case ar_AE = "ar_AE" // Locales.arabicUnitedArabEmirates
    case ar = "ar" // Locales.arabic
    case `as` = "as" // Locales.assamese
    case az = "az" // Locales.assamese
    case be = "be" // Locales.belarusian
    case bg = "bg" // Locales.bulgarian
    case bn = "bn" // Locales.bengali
    case br = "br" // Locales.breton
    case bs = "bs" // Locales.bosnian
    case bs_Cyrl = "bs-Cyrl" // Locales.belarusian
    case ca = "ca" // Locales.catalan
    case cz = "cz" // Locales.czech
    case cy = "cy" // Locales.welsh
    case cs = "cs" // Locales.czech
    case da = "da" // Locales.danish
    case de = "de" // Locales.dutch
    case dsb = "dsb" // Locales.lowerSorbian
    case dz = "dz" // Locales.dzongkha
    case ee = "ee" // Locales.ewe
    case el = "el" // Locales.greek
    case en = "en" // Locales.english
    case es_AR = "es_AR" // Locales.spanishArgentina
    case es_PY = "es_PY" // Locales.spanishParaguay
    case es_MX = "es_MX" // Locales.spanishMexico
    case es_US = "es_US" // Locales.spanishUnitedStates
    case es = "es" // Locales.spanish
    case et = "et" // Locales.estonian
    case eu = "eu" // Locales.basque
    case fa = "fa" // Locales.persian
    case fi = "fi" // Locales.finnish
    case fil = "fil" // Locales.filipino
    case fo = "fo" // Locales.faroese
    case fr_CA = "fr_CA" // French (Canada)
    case fr = "fr" // French
    case fur = "fur" // Friulian
    case fy = "fy" // Western Frisian
    case ga = "ga" // Irish
    case gd = "gd" // Scottish Gaelic
    case gl = "gl" // Galician
    case gu = "gu" // Gujarati
    case he = "he" // Hebrew
    case hi = "hi" // Hindi
    case hr = "hr" // Croatian
    case hsb = "hsb" // Upper Sorbian
    case hu = "hu" // Hungarian
    case hy = "hy" // Armenian
    case id = "id" // Indonesian
    case `is` = "is" // Icelandic
    case it = "it" // Locales.italian
    case ja = "ja" // Japanese
    case jgo = "jgo" // Ngomba
    case ka = "ka" // Georgian
    case kea = "kea" // Kabuverdianu
    case kk = "kk" // Kazakh
    case kl = "kl" // Kalaallisut
    case km = "km" // Khmer
    case kn = "kn" // Kannada
    case ko = "ko" // Korean
    case kok = "kok" // Konkani
    case ksh = "ksh" // Colognian
    case ky = "ky" // Kyrgyz
    case lb = "lb" // Luxembourgish
    case lkt = "lkt" // Lakota
    case lo = "lo" // Lao
    case lt = "lt" // Lithuanian
    case lv = "lv" // Latvian
    case mk = "mk" // Macedonian
    case ml = "ml" // Malayalam
    case mn = "mn" // Mongolian
    case mr = "mr" // Marathi
    case ms = "ms" // Malay
    case mt = "mt" // Maltese
    case my = "my" // Burmese
    case mzn = "mzn" // Mazanderani
    case nb = "nb" // Norwegian Bokmål
    case ne = "ne" // Nepali
    case nl = "nl" // Netherland
    case nn = "nn" // Norwegian Nynorsk
    case or = "or" // Odia
    case pa = "pa" // Punjabi
    case pl = "pl" // Polish
    case ps = "ps" // Pashto
    case pt = "pt" // Portuguese
    case ro = "ro" // Romanian
    case ru = "ru" // Russian
    case sah = "sah" // Sakha
    case sd = "sd" // Sindhi
    case se_FI = "se_FI" // Northern Sami (Finland)
    case se = "se" // Northern Sami
    case si = "si" // Sinhala
    case sk = "sk" // Slovak
    case sl = "sl" // Slovenian
    case sq = "sq" // Albanian
    case sr_Latn = "sr_Latn" // Serbian (Latin)
    case sr = "sr" // Serbian
    case sv = "sv" // Swedish
    case sw = "sw" // Swedish
    case ta = "ta" // Tamil
    case te = "te" // Telugu
    case th = "th" // Thai
    case ti = "ti" // Tigrinya
    case tk = "tk" // Turkmen
    case to = "to" // Tongan
    case tr = "tr" // Turkish
    case ug = "ug" // Uyghur
    case uk = "uk" // Ukrainian
    case ur_IN = "ur_IN" // Urdu (India)
    case ur = "ur" // Urdu
    case uz_Cyrl = "uz_Cyrl" // Uzbek (Cyrillic)
    case uz = "uz" // Uzbek (Cyrillic)
    case vi = "vi" // Vietnamese
    case wae = "wae" // Walser
    case yue_Hans = "yue_Hans" // Cantonese (Simplified)
    case yue_Hant = "yue_Hant" // Cantonese (Traditional)
    case zh_Hans_HK = "zh_Hans_HK" // Chinese (Simplified, Hong Kong [China])
    case zh_Hans_MO = "zh_Hans_MO" // Chinese (Simplified, Macau [China])
    case zh_Hans_SG = "zh_Hans_SG" // Chinese (Simplified, Singapore)
    case zh_Hant_HK = "zh_Hant_HK" // Chinese (Traditional, Hong Kong [China])
    case zh_Hant_MO = "zh_Hant_MO" // Chinese (Traditional, Macau [China])
    case zh_Hans = "zh_Hans" // Chinese (Simplified)
    case zh_Hant = "zh_Hant" // Chinese (Traditional)
    case zh = "zh" // Chinese
    case zu = "zu" // Zulu

    /// Table with the data of the language.
    /// Data is structured in:
    /// { flavour: { unit : { data } } }
    public var flavours: [String: Any] {
        return RelativeFormatterLanguagesCache.shared.flavoursForLocaleID(self.rawValue) ?? [:]
    }

    public var identifier: String {
        return self.rawValue
    }

    public func quantifyKey(forValue value: Double) -> RelativeFormatter.PluralForm? {
        switch self {

        case .sr_Latn, .sr, .uk:
            let mod10 = Int(value) % 10
            let mod100 = Int(value) % 100

            switch mod10 {
            case 1:
                switch mod100 {
                case 11:
                    break
                default:
                    return .one
                }
            case 2, 3, 4:
                switch mod100 {
                case 12, 13, 14:
                    break
                default:
                    return .few
                }
            default:
                break
            }

            return .many

        case .ru, .sk, .sl:
            let mod10 = Int(value) % 10
            let mod100 = Int(value) % 100

            switch mod100 {
            case 11...14:
                break

            default:
                switch mod10 {
                case 1:
                    return .one
                case 2...4:
                    return .few
                default:
                    break
                }

            }
            return .many

        case .ro:
            let mod100 = Int(value) % 100

            switch value {
            case 0:
                return .few
            case 1:
                return .one
            default:
                if mod100 > 1 && mod100 <= 19 {
                    return .few
                }
            }

            return .other

        case .pa:
            switch value {
            case 0, 1:
                return .one
            default:
                return .other
            }

        case .mt:
            switch value {
            case 1: return .one
            case 0: return .few
            case 2...10: return .few
            case 11...19: return .many
            default: return .other
            }

        case .lt, .lv:
            let mod10 = Int(value) % 10
            let mod100 = Int(value) % 100

            if value == 0 {
                return .zero
            }

            if value == 1 {
                return .one
            }

            switch mod10 {
            case 1:
                if mod100 != 11 {
                    return .one
                }
                return .many
            default:
                return .many
            }

        case .ksh, .se:
            switch value {
            case 0: return .zero
            case 1: return .one
            default: return .other
            }

        case .`is`:
            let mod10 = Int(value) % 10
            let mod100 = Int(value) % 100

            if value == 0 {
                return .zero
            }

            if value == 1 {
                return .one
            }

            switch mod10 {
            case 1:
                if mod100 != 11 {
                    return .one
                }
            default:
                break
            }

            return .many

        case .id, .ja, .ms, .my, .mzn, .sah, .se_FI, .si, .th, .yue_Hans, .yue_Hant,
             .zh_Hans_HK, .zh_Hans_MO, .zh_Hans_SG, .zh_Hant_HK, .zh_Hant_MO, .zh:

            return .other

        case .hy:
            return (value >= 0 && value < 2 ? .one : .other)

        case .ga, .gd:
            switch Int(value) {
            case 1: return .one
            case 2: return .two
            case 3...6: return .few
            case 7...10: return .many
            default: return .other
            }

        case .fr_CA, .fr:
            return (value >= 0 && value < 2 ? .one : .other)

        case .dz, .kea, .ko, .kok, .lkt, .lo:
            return nil

        case .cs: // Locales.czech
            switch value {
            case 1:
                return .one
            case 2, 3, 4:
                return .few
            default:
                return .other
            }

        case .cy:
            switch value {
            case 0:    return .zero
            case 1: return .one
            case 2: return .two
            case 3: return .few
            case 6: return .many
            default: return .other
            }

        case .cz, .dsb:
            switch value {
            case 1:
                return .one
            case 2, 3, 4:
                return .few
            default:
                return .other
            }

        case .br:
            let n = Int(value)
            return n % 10 == 1 && n % 100 != 11 && n % 100 != 71 && n % 100 != 91 ? .zero : n % 10 == 2 && n % 100 != 12 && n % 100 != 72 && n % 100 != 92 ? .one : (n % 10 == 3 || n % 10 == 4 || n % 10 == 9) && n % 100 != 13 && n % 100 != 14 && n % 100 != 19 && n % 100 != 73 && n % 100 != 74 && n % 100 != 79 && n % 100 != 93 && n % 100 != 94 && n % 100 != 99 ? .two : n % 1_000_000 == 0 && n != 0 ? .many : .other

        case .be, .bs, .bs_Cyrl, .hr, .hsb, .pl:
            let mod10 = Int(value) % 10
            let mod100 = Int(value) % 100

            switch mod10 {
            case 1:
                switch mod100 {
                case 11:
                    break
                default:
                    return .one
                }
            case 2, 3, 4:
                switch mod100 {
                case 12, 13, 14:
                    break
                default:
                    return .few
                }
            default:
                break
            }
            return .many

        case .ar, .ar_AE, .he:
            switch value {
            case 0: return .zero
            case 1: return .one
            case 2: return .two
            default:
                let mod100 = Int(value) % 100
                if mod100 >= 3 && mod100 <= 10 {
                    return .few
                } else if mod100 >= 11 {
                    return .many
                } else {
                    return .other
                }
            }

        case .am, .bn, .fa, .gu, .kn, .mr, .zu:
            return (value >= 0 && value <= 1 ? .one : .other)

        default:
            return (value == 1 ? .one : .other)

        }
    }

}
