//
//  StringsFileGenerator.swift
//
//
//  Created by Thibaut Schmitt on 04/01/2022.
//

import Foundation
import ToolCore

// swiftlint:disable type_body_length file_length

enum StringsFileGenerator {

    // MARK: - Strings Files

    static func writeStringsFiles(
        sections: [Section],
        langs: [String],
        defaultLang: String,
        tags: [String],
        lprojPathFormat: String
    ) {

        var stringsFilesContent = [String: String]()
        for lang in langs {
            stringsFilesContent[lang] = Self.generateStringsFileContent(
                lang: lang,
                defaultLang: defaultLang,
                tags: tags,
                sections: sections
            )
        }

        // Write strings file content
        langs.forEach { lang in
            guard let fileContent = stringsFilesContent[lang] else { return }

            let stringsFilePath = String(format: lprojPathFormat, lang)
            let stringsFilePathURL = URL(fileURLWithPath: stringsFilePath)
            do {
                try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8)
            } catch {
                let error = StringiumError.writeFile(error.localizedDescription, stringsFilePath)
                print(error.description)
                Stringium.exit(withError: error)
            }
        }
    }

    static func writeXcStringsFiles(
        sections: [Section],
        langs: [String],
        defaultLang: String,
        tags: [String],
        xcStringsFilePath: String
    ) {

        let fileContent: String = Self.generateXcStringsFileContent(
            langs: langs,
            defaultLang: defaultLang,
            tags: tags,
            sections: sections
        )

        let stringsFilePathURL = URL(fileURLWithPath: xcStringsFilePath)
        do {
            try fileContent.write(to: stringsFilePathURL, atomically: false, encoding: .utf8)
        } catch {
            let error = StringiumError.writeFile(error.localizedDescription, xcStringsFilePath)
            print(error.description)
            Stringium.exit(withError: error)
        }
    }

    static func generateStringsFileContent(
        lang: String,
        defaultLang: String,
        tags inputTags: [String],
        sections: [Section]
    ) -> String {
        var stringsFileContent = """
        /**
         * Apple Strings File
         * Generated by ResgenSwift \(ResgenSwiftVersion)
         * Language: \(lang)
         */\n
        """

        sections.forEach { section in
            // Check that at least one string will be generated
            guard section.hasOneOrMoreMatchingTags(tags: inputTags) else {
                return // Go to next section
            }

            stringsFileContent += "\n/********** \(section.name) **********/\n\n"
            section.definitions.forEach { definition in
                var skipDefinition = false // Set to true if not matching tag
                let translationOpt: String? = {
                    // If no matching tag => skip
                    if definition.hasOneOrMoreMatchingTags(inputTags: inputTags) == false {
                        skipDefinition = true
                        return nil
                    }

                    // If tags contains `noTranslationTag` => get default lang
                    if definition.tags.contains(Stringium.noTranslationTag) {
                        return definition.translations[defaultLang]
                    }

                    // Else: get specific lang
                    return definition.translations[lang]
                }()

                if let translation = translationOpt {
                    stringsFileContent += "\"\(definition.name)\" = \"\(translation)\";\n\n"
                } else if skipDefinition == false {
                    let error = StringiumError.langNotDefined(lang, definition.name, definition.reference != nil)
                    print(error.description)
                    Stringium.exit(withError: error)
                }
            }
        }

        return stringsFileContent
    }

    // MARK: - XcStrings Generation

    static func generateXcStringsFileContent(
        langs: [String],
        defaultLang: String,
        tags inputTags: [String],
        sections: [Section]
    ) -> String {
        generateXcStringsFileContentFromRootObject(
            rootObject: generateRootObject(
                langs: langs,
                defaultLang: defaultLang,
                tags: inputTags,
                sections: sections
            )
        )
        // Remove unwanted \ generated when transforming object to json
        .replacingOccurrences(of: "\\\\", with: "\\")
        .replacingOccurrences(of: "\\\"", with: "\"")
    }

    static func generateXcStringsFileContentFromRootObject(rootObject: Root) -> String {
        do {
            let encoder = JSONEncoder()
            encoder.outputFormatting = [.prettyPrinted, .sortedKeys]

            let json = try encoder.encode(rootObject)

            return String(decoding: json, as: UTF8.self)
        } catch {
            debugPrint("Failed to encode: \(error)")
        }

        return ""
    }

    static func generateRootObject(
        langs: [String],
        defaultLang: String,
        tags inputTags: [String],
        sections: [Section]
    ) -> Root {

        var xcStringDefinitionTab: [XCStringDefinition] = []

        sections.forEach { section in // swiftlint:disable:this closure_body_length
                                      // Check that at least one string will be generated
            guard section.hasOneOrMoreMatchingTags(tags: inputTags) else {
                return // Go to next section
            }

            section
                .definitions
                .forEach { definition in // swiftlint:disable:this closure_body_length
                    var isNoTranslation = false

                    var localizationTab: [XCStringLocalization] = []

                    guard definition.hasOneOrMoreMatchingTags(inputTags: inputTags) else {
                        return
                    }

                    if definition.tags.contains(Stringium.noTranslationTag) {
                        isNoTranslation = true
                    }

                    if isNoTranslation {
                        // Search for langs in yaml
                        for lang in langs {
                            if let value = definition.translations[defaultLang], !value.isEmpty {
                                let localization = XCStringLocalization(
                                    lang: lang,
                                    content: XCStringLocalizationLangContent(
                                        stringUnit: DefaultStringUnit(
                                            state: "translated",
                                            value: value
                                        )
                                    )
                                )
                                localizationTab.append(localization)
                            }
                        }
                    } else {
                        // Search for langs in twine
                        for (lang, value) in definition.translations
                        where langs.contains(lang) && !value.isEmpty {
                            let localization = XCStringLocalization(
                                lang: lang,
                                content: XCStringLocalizationLangContent(
                                    stringUnit: DefaultStringUnit(
                                        state: "translated",
                                        value: value
                                    )
                                )
                            )
                            localizationTab.append(localization)
                        }
                    }

                    let xcStringDefinition = XCStringDefinition(
                        title: definition.name,
                        content: XCStringDefinitionContent(
                            comment: definition.comment,
                            extractionState: "manual",
                            localizations: XCStringLocalizationContainer(
                                localizations: localizationTab
                            )
                        )
                    )

                    xcStringDefinitionTab.append(xcStringDefinition)
                }
        }

        let xcStringContainer = XCStringDefinitionContainer(strings: xcStringDefinitionTab)

        return Root(
            sourceLanguage: defaultLang,
            strings: xcStringContainer,
            version: "1.0"
        )
    }

    // MARK: - Extension file

    static func writeExtensionFiles(
        sections: [Section],
        defaultLang lang: String,
        tags: [String],
        isStatic: Bool,
        inputFilename: String,
        extensionName: String,
        extensionFilePath: String,
        extensionSuffix: String,
        visibility: ExtensionVisibility,
        assetBundle: AssetBundle
    ) {
        // Get extension content
        let extensionFileContent = Self.getExtensionContent(
            sections: sections,
            defaultLang: lang,
            tags: tags,
            isStatic: isStatic,
            inputFilename: inputFilename,
            extensionName: extensionName,
            extensionSuffix: extensionSuffix,
            visibility: visibility,
            assetBundle: assetBundle
        )

        // Write content
        let extensionFilePathURL = URL(fileURLWithPath: extensionFilePath)
        do {
            try extensionFileContent.write(to: extensionFilePathURL, atomically: false, encoding: .utf8)
        } catch {
            let error = StringiumError.writeFile(extensionFilePath, error.localizedDescription)
            print(error.description)
            Stringium.exit(withError: error)
        }
    }

    // MARK: - Extension content

    static func getExtensionContent(
        sections: [Section],
        defaultLang lang: String,
        tags: [String],
        isStatic: Bool,
        inputFilename: String,
        extensionName: String,
        extensionSuffix: String,
        visibility: ExtensionVisibility,
        assetBundle: AssetBundle
    ) -> String {
        [
            Self.getHeader(
                stringsFilename: inputFilename,
                extensionClassname: extensionName
            ),
            Self.getEnumKey(
                sections: sections,
                tags: tags,
                extensionClassname: extensionName,
                extensionSuffix: extensionSuffix,
                visibility: visibility
            ),
            Self.getProperties(
                sections: sections,
                defaultLang: lang,
                tags: tags,
                isStatic: isStatic,
                visibility: visibility,
                assetBundle: assetBundle
            ),
            Self.getFooter()
        ]
            .joined(separator: "\n")
    }

    // MARK: - Extension part

    private static func getHeader(stringsFilename: String, extensionClassname: String) -> String {
        """
        // Generated by ResgenSwift.Strings.\(Stringium.toolName) \(ResgenSwiftVersion)

        import UIKit

        fileprivate let kStringsFileName = "\(stringsFilename)"

        extension \(extensionClassname) {
        """
    }

    private static func getEnumKey(
        sections: [Section],
        tags: [String],
        extensionClassname: String,
        extensionSuffix: String,
        visibility: ExtensionVisibility
    ) -> String {
        var enumDefinition = "\n    \(visibility) enum Key\(extensionSuffix.uppercasedFirst()): String {\n"

        // Enum
        sections.forEach { section in
            // Check that at least one string will be generated
            guard section.hasOneOrMoreMatchingTags(tags: tags) else {
                return // Go to next section
            }

            section.definitions.forEach { definition in
                guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
                    return // Go to next definition
                }
                enumDefinition += "        case \(definition.name) = \"\(definition.name)\"\n"
            }
        }

        // KeyPath accessors
        enumDefinition += "\n"
        enumDefinition += "        \(visibility) var keyPath: KeyPath<\(extensionClassname), String> {\n"
        enumDefinition += "            switch self {\n"
        sections.forEach { section in
            // Check that at least one string will be generated
            guard section.hasOneOrMoreMatchingTags(tags: tags) else {
                return // Go to next section
            }

            section.definitions.forEach { definition in
                guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
                    return // Go to next definition
                }
                enumDefinition += "            case .\(definition.name): return \\\(extensionClassname).\(definition.name)\n"
            }
        }
        enumDefinition += "            }\n" // Switch
        enumDefinition += "        }\n" // var keyPath
        enumDefinition += "    }" // Enum

        return enumDefinition
    }

    private static func getProperties(
        sections: [Section],
        defaultLang lang: String,
        tags: [String],
        isStatic: Bool,
        visibility: ExtensionVisibility,
        assetBundle: AssetBundle
    ) -> String {
        sections.compactMap { section in
            // Check that at least one string will be generated
            guard section.hasOneOrMoreMatchingTags(tags: tags) else {
                return nil // Go to next section
            }

            var res = "\n    // MARK: - \(section.name)\n"
            res += section.definitions.compactMap { definition in
                guard definition.hasOneOrMoreMatchingTags(inputTags: tags) == true else {
                    return nil // Go to next definition
                }

                let property = definition.getNSLocalizedStringProperty(
                    forLang: lang,
                    isStatic: isStatic,
                    visibility: visibility,
                    assetBundle: assetBundle
                )

                return "\n\(property)"
            }
            .joined(separator: "\n")
            return res
        }
        .joined(separator: "\n")
    }

    private static func getFooter() -> String {
        """
        }

        """
    }
}
