import Foundation

extension Content {

    func generateWebsiteInAllLanguages() {
        performGenerationIfIdle {
            self.results.reset()
            self.storage.writeNotification = { [weak self] in
                self?.results.created(outputFile: $0)
            }
            self.addGloballyRequiredFiles()
            self.generatePagesInternal()
            self.generatePostFeedPagesInternal()
            self.generateTagPagesInternal()
            self.generateTagOverviewPagesInternal()

            self.copyRequiredFiles()
            self.generateRequiredImages()
            self.results.recalculate()
            self.generateListOfExternalFiles()
            self.generateListOfUrlMappings()
            self.updateUnusedFiles()
            self.status("Generation completed")
        }
    }

    func endCurrentGeneration() {
        guard isGeneratingWebsite, shouldGenerateWebsite else {
            return
        }
        DispatchQueue.main.async {
            self.set(shouldGenerate: false)
        }
    }

    func generatePostFeedPages() {
        performGenerationIfIdle {
            self.generatePostFeedPagesInternal()
        }
    }

    func check(content: String, of page: Page, for language: ContentLanguage, onComplete: @escaping (PageGenerationResults) -> Void) {
        performGenerationIfIdle {
            let results = self.results.makeResults(for: page, in: language)
            results.reset()
            let generator = PageContentParser(content: page.content, language: language, results: results)
            _ = generator.generatePage(from: content)
            self.results.recalculate()
            DispatchQueue.main.async {
                onComplete(results)
            }
        }
    }

    private func addGloballyRequiredFiles() {
        results.general.require(files: settings.general.requiredFiles)
    }

    private func copyRequiredFiles() {
        let count = results.requiredFiles.count
        var completed = 0
        for file in results.requiredFiles {
            guard shouldGenerateWebsite else { return }
            defer {
                completed += 1
                status("Copying required files: \(completed) / \(count)")
            }
            guard !file.isExternallyStored else {
                continue
            }
            let path = file.absoluteUrl
            if !storage.copy(file: file.identifier, to: path) {
                results.general.unsavedOutput(path, source: .general)
            }
        }
    }

    private func generateRequiredImages() {
        let images = results.imagesToGenerate.sorted()

        let count = images.count
        var completed = 0

        func didFinishOneImage() {
            completed += 1
            status("Generating required images: \(completed) / \(count)")
        }

        // Finish existing images
        var newImagesToGenerate: [ImageVersion] = []
        var avifImagesToGenerate: [ImageVersion] = []
        for image in images {
            guard shouldGenerateWebsite else { return }
            guard imageGenerator.needsToGenerate(image) else {
                results.created(outputFile: image.outputPath)
                didFinishOneImage()
                continue
            }
            if image.type == .avif {
                avifImagesToGenerate.append(image)
            } else {
                newImagesToGenerate.append(image)
            }
        }

        func generate(images: [ImageVersion]) {
            for image in images {
                guard shouldGenerateWebsite else { return }
                defer { didFinishOneImage() }
                if imageGenerator.generate(version: image) {
                    results.created(outputFile: image.outputPath)
                    continue
                }
                results.failed(image: image)
            }
        }

        generate(images: newImagesToGenerate)
        generate(images: avifImagesToGenerate)
        if completed != count {
            print("Expected \(count) images processed, but only \(completed) were")
        }
    }

    func generateAllPages() {
        performGenerationIfIdle {
            self.generatePagesInternal()
        }
    }

    func generatePage(_ page: Page) {
        performGenerationIfIdle {
            for language in ContentLanguage.allCases {
                self.generateInternal(page, in: language)
            }
            self.copyRequiredFiles()
            self.generateRequiredImages()
        }
    }

    func generatePage(_ page: Page, in language: ContentLanguage) {
        performGenerationIfIdle {
            self.generateInternal(page, in: language)
        }
    }

    // MARK: Routes

    private func generateMapImage(route: FileResource) -> Bool {
        let size = route.mapImageDimensions
        let path = URL(fileURLWithPath: route.mapImagePath)

        guard let workoutData = route.workoutData else {
            print("ImageGenerator: Failed to get workout data for route \(route.identifier)")
            return false
        }
        let mapImager = MapImageCreator(locations: workoutData.locations)
        guard let (largeImage, points) = mapImager.createMapSnapshot(size: .init(width: size.width, height: size.height)) else {
            print("ImageGenerator: Failed to generate map image for route \(route.identifier)")
            return false
        }
        guard largeImage.writePng(to: path) else {
            print("ImageGenerator: Failed to save map image for route \(route.identifier)")
            return false
        }

        let statisticsGenerator = StatisticsFileGenerationJob(
            mapCoordinates: points,
            workout: workoutData,
            path: route.statisticsFilePath,
            numberOfSamples: 600
        )

        guard statisticsGenerator.run() else {
            print("ImageGenerator: Failed to generate statistics for route \(route.identifier)")
            return false
        }

        return true
    }

    // MARK: Find items by id

    func page(_ pageId: String) -> Page? {
        pages.first { $0.identifier == pageId }
    }

    func image(_ imageId: String) -> FileResource? {
        files.first { $0.identifier == imageId && $0.type.isImage }
    }

    func video(_ videoId: String) -> FileResource? {
        files.first { $0.identifier == videoId && $0.type.isVideo }
    }

    func file(_ fileId: String) -> FileResource? {
        files.first { $0.identifier == fileId }
    }

    func tag(_ tagId: String) -> Tag? {
        tags.first { $0.identifier == tagId }
    }

    // MARK: Generation input

    func navigationBar(in language: ContentLanguage) -> [NavigationBar.Link] {
        settings.navigation.navigationItems.map {
            .init(text: $0.title(in: language),
                  url: $0.absoluteUrl(in: language))
        }
    }

    private func pageHeaders(css: FileResource?) -> Set<HeaderElement> {
        var result: Set<HeaderElement> = [.charset, .viewport]
        if let css {
            result.insert(.css(file: css, order: HeaderElement.defaultCssFileOrder))
        }
        if let manifest = settings.pages.manifestFile {
            result.insert(.manifest(manifest))
        }
        return result
    }

    var postPageHeaders: Set<HeaderElement> {
        pageHeaders(css: settings.posts.defaultCssFile)
    }

    var contentPageHeaders: Set<HeaderElement> {
        pageHeaders(css: settings.pages.defaultCssFile)
    }

    // MARK: Generation

    private func performGenerationIfIdle(_ operation: @escaping () -> ()) {
        DispatchQueue.main.async {
            guard !self.isGeneratingWebsite else {
                return
            }
            self.set(isGenerating: true)
            self.set(shouldGenerate: true)
            DispatchQueue.global(qos: .userInitiated).async {
                operation()
                DispatchQueue.main.async {
                    self.set(isGenerating: false)
                    self.set(shouldGenerate: false)
                }
            }
        }
    }

    private func status(_ message: String) {
        DispatchQueue.main.async {
            self.generationStatus = message
        }
    }

    /**
     - Note: Run on background thread
     */
    private func generatePagesInternal() {
        let count = pages.count
        for index in pages.indices {
            guard shouldGenerateWebsite else { return }
            let page = pages[index]
            status("Generating pages: \(index) / \(count)")
            guard !page.isExternalUrl else {
                continue
            }
            for language in ContentLanguage.allCases {
                generateInternal(page, in: language)
            }
        }
    }

    /**
     - Note: Run on background thread
     */
    private func generatePostFeedPagesInternal() {
        status("Generating post feed")
        for language in ContentLanguage.allCases {
            guard shouldGenerateWebsite else { return }
            let results = results.makeResults(for: .feed, in: language)
            let source = FeedGeneratorSource(
                language: language,
                content: self,
                results: results)

            let generator = PostListPageGenerator(source: source)
            generator.createPages(for: posts)
        }

    }

    /**
     - Note: Run on background thread
     */
    private func generateTagPagesInternal() {
        let count = tags.count
        for index in tags.indices {
            guard shouldGenerateWebsite else { return }
            let tag = tags[index]
            status("Generating tag pages: \(index) / \(count)")
            generatePagesInternal(for: tag)
        }
    }

    /**
     - Note: Run on background thread
     */
    private func generatePagesInternal(for tag: Tag) {
        for language in ContentLanguage.allCases {
            let results = results.makeResults(for: tag, in: language)

            let posts = posts.filter { $0.contains(tag) }
            guard posts.count > 0 else { continue }

            let source = TagPageGeneratorSource(
                language: language,
                content: self,
                results: results,
                tag: tag)
            let generator = PostListPageGenerator(source: source)
            generator.createPages(for: posts)

            if let originalUrl = tag.localized(in: language).originalUrl {
                results.redirect(from: originalUrl, to: tag.absoluteUrl(in: language))
            }
        }
    }

    /**
     - Note: Run on background thread
     */
    private func generateTagOverviewPagesInternal() {
        guard let tagOverview else {
            print("Generator: No tag overview page to generate")
            return
        }
        status("Generating tag overview page")
        for language in ContentLanguage.allCases {
            guard shouldGenerateWebsite else { return }
            let results = results.makeResults(for: .tagOverview, in: language)
            let generator = TagOverviewGenerator(content: self, language: language, results: results)
            generator.generatePages(tags: tags, overview: tagOverview)
        }
    }

    /**
     - Note: Run on background thread
     */
    private func generateInternal(_ page: Page, in language: ContentLanguage) {
        let results = results.makeResults(for: page, in: language)
        let pageGenerator = PageGenerator(content: self)

        let relativePageUrl = page.absoluteUrl(in: language)
        let filePath = relativePageUrl + ".html"
        let pageUrl = settings.general.url + relativePageUrl

        guard let content = pageGenerator.generate(page: page, language: language, results: results, pageUrl: pageUrl) else {
            print("Failed to generate page \(page.identifier) in language \(language)")
            return
        }

        guard storage.write(content, to: filePath) else {
            print("Failed to save page \(page.identifier)")
            return
        }

        if let originalUrl = page.localized(in: language).originalUrl {
            results.redirect(from: originalUrl, to: pageUrl)
        }
    }

    // MARK: Additional infos

    private var externalFileListName: String { "external-files.txt" }

    private func generateListOfExternalFiles() {
        let files = results.requiredFiles
            .filter { $0.isExternallyStored }

        guard !files.isEmpty else {
            if storage.hasFileInOutputFolder(externalFileListName) {
                storage.deleteInOutputFolder(externalFileListName)
            }
            return
        }

        let content = files
            .map { $0.absoluteUrl }
            .sorted()
            .joined(separator: "\n")

        storage.write(content, to: externalFileListName)
    }

    private var redirectsListFileName: String { "redirects.conf" }

    private func generateListOfUrlMappings() {
        let redirects = results.redirects.map { "\($0.key) \($0.value);" }
        guard !redirects.isEmpty else {
            if storage.hasFileInOutputFolder(redirectsListFileName) {
                storage.deleteInOutputFolder(redirectsListFileName)
            }
            return
        }

        let list = redirects.sorted().joined(separator: "\n    ")

        let content =
            """
            map $request_uri $redirect_uri {
                /en.html /feed;
                /de.html /blog;
                \(list)
            }
            """

        storage.write(content, to: redirectsListFileName)
    }

    private func updateUnusedFiles() {
        let existing = storage.getAllOutputFiles()
        DispatchQueue.main.async {
            self.results.determineMissingRequiredFiles(existingFiles: existing)
            self.results.determineFiles(unusedIn: existing)
            self.results.objectWillChange.send()
        }
    }
}
