import SwiftSoup

/**
 Handles both inline HTML and the external HTML command
 */
struct HtmlCommand: CommandProcessor {

    static let commandType: CommandType = .includedHtml

    let results: PageGenerationResults

    let content: Content

    init(content: Content, results: PageGenerationResults, language: ContentLanguage) {
        self.content = content
        self.results = results
    }

    /**
     Handle the HTML command

     Format: `![html](<fileId>)`
     */
    func process(_ arguments: [String], markdown: Substring) -> String {
        guard arguments.count == 1 else {
            invalid(markdown)
            return ""
        }
        let fileId = arguments[0]
        guard let file = content.file(fileId) else {
            results.missing(file: fileId, source: "External HTML command")
            return ""
        }
        let content = file.textContent()
        checkResources(in: content)
        return content
    }

    /**
     Handle inline HTML
     */
    func process(_ html: String, markdown: Substring) -> String {
        checkResources(in: html)
        return html
    }

    private func checkResources(in html: String) {
        let document: Document
        do {
            document = try SwiftSoup.parse(html)
        } catch {
            results.warning("Failed to parse inline HTML: \(error)")
            return
        }

        checkImages(in: document)
        checkLinks(in: document)
        checkSourceSets(in: document)
    }

    private func checkImages(in document: Document) {
        let srcAttributes: [String]
        do {
            let imgElements = try document.select("img")

            srcAttributes = try imgElements.array()
                .compactMap { try $0.attr("src") }
                .filter { !$0.trimmed.isEmpty }
        } catch {
            results.warning("Failed to check 'src' attributes of <img> elements in inline HTML: \(error)")
            return
        }

        for src in srcAttributes {
            findFile(path: src, source: "src of <img>")
        }
    }

    private func checkLinks(in document: Document) {
        let hrefs: [String]
        do {
            let linkElements = try document.select("a")

            hrefs = try linkElements.array()
                .compactMap { try $0.attr("href").trimmed }
                .filter { !$0.isEmpty }
        } catch {
            results.warning("Failed to check 'href' attributes of <a> elements in inline HTML: \(error)")
            return
        }

        for url in hrefs {
            if url.hasPrefix("http://") || url.hasPrefix("https://") {
                results.externalLink(to: url)
            } else {
                findFile(path: url, source: "href of <a>")
            }
        }
    }

    private func checkSourceSets(in document: Document) {
        let sources: [Element]
        do {
            sources = try document.select("source").array()
        } catch {
            results.warning("Failed to find <source> elements in inline HTML: \(error)")
            return
        }
        checkSourceSetAttributes(sources: sources)
        checkSourceAttributes(sources: sources)
    }

    private func checkSourceSetAttributes(sources: [Element]) {
        let srcSets: [String]
        do {
            srcSets = try sources
                .compactMap { try $0.attr("srcset") }
                .filter { !$0.trimmed.isEmpty }
        } catch {
            results.warning("Failed to check 'srcset' attributes of <source> elements in inline HTML: \(error)")
            return
        }

        for src in srcSets {
            findFile(path: src, source: "srcset of <source>")
        }
    }

    private func checkSourceAttributes(sources: [Element]) {
        let srcAttributes: [String]
        do {
            srcAttributes = try sources
                .compactMap { try $0.attr("src") }
                .filter { !$0.trimmed.isEmpty }
        } catch {
            results.warning("Failed to check 'src' attributes of <source> elements in inline HTML: \(error)")
            return
        }

        for src in srcAttributes {
            findFile(path: src, source: "src of <source>")
        }
    }

    private func findFile(path: String, source: String) {
        let type = FileType(fileExtension: path.fileExtension)

        guard path.hasPrefix("/") else {
            findFileWith(relativePath: path, type: type, source: source)
            return
        }

        guard !type.generatesImageVersions else {
            // Try to determine image version needed
            findImageVersion(path: path, type: type, source: source)
            return
        }
        if findFile(withAbsolutePath: path) {
            // File marked as required
            return
        }

        results.requiredOutput(path.withLeadingSlashRemoved, source: "HTML: \(source)")

//        let fileId = path.dropBeforeLast("/")
//        if content.isValidIdForFile(fileId) {
//            results.missing(file: fileId, source: "HTML: \(source)")
//        } else {
//            results.warning("Could not find file '\(path)' for \(source)")
//        }
    }

    private func findFile(withAbsolutePath absolutePath: String) -> Bool {
        guard let file = content.file(withOutputPath: absolutePath) else {
            return false
        }
        results.require(file: file)
        return true
    }

    private func findImageVersion(path: String, type: FileType, source: String) {
        // First check if image original should be used
        if findFile(withAbsolutePath: path) {
            return
        }
        let fileId = path.dropAfterLast("/").dropBeforeLast("/")
        guard let file = content.file(fileId) else {
            results.missing(file: fileId, source: "HTML: \(source)")
            return
        }
        results.warning("Could not determine image version for file '\(file.identifier)' for \(source)")
    }

    private func findFileWith(relativePath: String, type: FileType, source: String) {
        results.warning("Could not determine relative file '\(relativePath)' for \(source)")
    }
}
