import Foundation

struct ImageToGenerate {

    let size: Int

    let image: FileResource
}

extension ImageToGenerate: Hashable {

    func hash(into hasher: inout Hasher) {
        hasher.combine(size)
        hasher.combine(image.identifier)
    }
}

final class PageGenerationResults: ObservableObject {

    let itemId: LocalizedItemId

    private unowned let delegate: GenerationResults

    /// The files that could not be accessed
    @Published
    private(set) var inaccessibleFiles: Set<FileResource>

    /// The files that could not be parsed, with the error message produced
    @Published
    private(set) var unparsableFiles: [FileResource : Set<String>]

    /// The missing files directly used by this page, and the source of the file
    @Published
    private(set) var missingFiles: [String: Set<String>]

    /// The missing files linked to from other files.
    @Published
    private(set) var missingLinkedFiles: [String : Set<FileResource>]

    /// The missing tags linked to by this page, and the source of the link
    @Published
    private(set) var missingLinkedTags: [String : Set<String>]

    /// The missing pages linked to by this page, and the source of the link
    @Published
    private(set) var missingLinkedPages: [String : Set<String>]

    /// The footer scripts or html to add to the end of the body
    @Published
    private(set) var requiredFooters: Set<String>

    /// The known header elements to include in the page
    @Published
    private(set) var requiredHeaders: Set<KnownHeaderElement>

    /// The known icons that need to be included as hidden SVGs
    @Published
    private(set) var requiredIcons: Set<PageIcon>

    /// The pages linked to by the page
    @Published
    private(set) var linkedPages: Set<Page>

    /// The tags linked to by this page
    @Published
    private(set) var linkedTags: Set<Tag>

    /// The links to external content in this page
    @Published
    private(set) var externalLinks: Set<String>

    /// The files used by this page, but not necessarily required in the output folder
    @Published
    private(set) var usedFiles: Set<FileResource>

    /// The files that need to be copied
    @Published
    private(set) var requiredFiles: Set<FileResource>

    /// The image versions required for this page
    @Published
    private(set) var imagesToGenerate: Set<ImageVersion>

    @Published
    private(set) var invalidCommands: [(command: CommandType?, markdown: String)]

    @Published
    private(set) var invalidBlocks: [(block: ContentBlock?, markdown: String)]

    @Published
    private(set) var warnings: Set<String>

    /// The files that could not be saved to the output folder
    @Published
    private(set) var unsavedOutputFiles: [String: Set<ItemReference>] = [:]

    /// The files that need to be present in the output folder
    @Published
    private(set) var requiredOutputFiles: [String: Set<String>] = [:]

    private(set) var pageIsEmpty: Bool

    private(set) var redirect: (originalUrl: String, newUrl: String)?

    init(itemId: LocalizedItemId, delegate: GenerationResults) {
        self.itemId = itemId
        self.delegate = delegate
        inaccessibleFiles = []
        unparsableFiles = [:]
        missingFiles = [:]
        missingLinkedFiles = [:]
        missingLinkedTags = [:]
        missingLinkedPages = [:]
        requiredHeaders = []
        requiredFooters = []
        requiredIcons = []
        linkedPages = []
        linkedTags = []
        externalLinks = []
        usedFiles = []
        requiredFiles = []
        imagesToGenerate = []
        invalidCommands = []
        invalidBlocks = []
        warnings = []
        unsavedOutputFiles = [:]
        requiredOutputFiles = [:]
        pageIsEmpty = false
        redirect = nil
    }

    func onMain(_ operation: @escaping () -> Void) {
        DispatchQueue.main.async {
            operation()
        }
    }

    func reset() {
        onMain {
            self.inaccessibleFiles = []
            self.unparsableFiles = [:]
            self.missingFiles = [:]
            self.missingLinkedFiles = [:]
            self.missingLinkedTags = [:]
            self.missingLinkedPages = [:]
            self.requiredHeaders = []
            self.requiredFooters = []
            self.requiredIcons = []
            self.linkedPages = []
            self.linkedTags = []
            self.externalLinks = []
            self.usedFiles = []
            self.requiredFiles = []
            self.imagesToGenerate = []
            self.invalidCommands = []
            self.invalidBlocks = []
            self.warnings = []
            self.unsavedOutputFiles = [:]
            self.requiredOutputFiles = [:]
            self.pageIsEmpty = false
            self.redirect = nil
        }
    }

    // MARK: Adding entries

    func inaccessibleContent(file: FileResource) {
        onMain { self.inaccessibleFiles.insert(file) }
        delegate.inaccessibleContent(file: file)
    }

    func invalid(command: CommandType?, _ markdown: Substring) {
        let markdown = String(markdown)
        onMain { self.invalidCommands.append((command, markdown)) }
        delegate.invalidCommand(markdown)
    }

    func invalid(block: ContentBlock?, _ markdown: Substring) {
        let markdown = String(markdown)
        onMain { self.invalidBlocks.append((block, markdown)) }
        delegate.invalidBlock(markdown)
    }

    func missing(page: String, source: String) {
        onMain { self.missingLinkedPages[page, default: []].insert(source) }
        delegate.missing(page: page)
    }

    func missing(tag: String, source: String) {
        onMain { self.missingLinkedTags[tag, default: []].insert(source) }
        delegate.missing(tag: tag)
    }

    func missing(file: String, source: String) {
        onMain { self.missingFiles[file, default: []].insert(source) }
        delegate.missing(file: file)
    }

    func require(image: ImageVersion) {
        onMain { self.imagesToGenerate.insert(image) }
        used(file: image.image)
        delegate.generate(image)
    }

    func require(imageSet: ImageSet) {
        let jobs = imageSet.jobs
        onMain { self.imagesToGenerate.formUnion(jobs) }
        used(file: imageSet.image)
        delegate.generate(jobs)
    }

    func invalidFormat(file: FileResource, error: String) {
        onMain { self.unparsableFiles[file, default: []].insert(error) }
        delegate.unparsable(file: file)
    }

    func missing(file: String, containedIn sourceFile: FileResource) {
        onMain { self.missingLinkedFiles[file, default: []].insert(sourceFile) }
        delegate.missing(file: file)
    }

    func used(file: FileResource) {
        onMain { self.usedFiles.insert(file) }
        // TODO: Notify delegate
    }

    func require(file: FileResource) {
        onMain { self.requiredFiles.insert(file) }
        used(file: file)
        delegate.require(file: file)
    }

    func require(files: [FileResource]) {
        onMain { self.requiredFiles.formUnion(files) }
        onMain { self.usedFiles.formUnion(files) }
        delegate.require(files: files)
    }

    func require(footer: String) {
        onMain { self.requiredFooters.insert(footer) }
    }

    func require(header: KnownHeaderElement) {
        onMain { self.requiredHeaders.insert(header) }
    }

    func require(headers: KnownHeaderElement...) {
        onMain { self.requiredHeaders.formUnion(headers) }
    }

    func require<S>(headers: S) where S: Sequence, S.Element == KnownHeaderElement {
        onMain { self.requiredHeaders.formUnion(headers) }
    }

    func require(icon: PageIcon) {
        onMain { self.requiredIcons.insert(icon) }
    }

    func require(icons: PageIcon...) {
        onMain { self.requiredIcons.formUnion(icons) }
    }

    func require(icons: [PageIcon]) {
        onMain { self.requiredIcons.formUnion(icons) }
    }

    func requiredOutput(_ path: String, source: String) {
        onMain { self.requiredOutputFiles[path, default: []].insert(source) }
        delegate.requiredOutputFile(path)
    }

    func linked(to page: Page) {
        onMain { self.linkedPages.insert(page) }
    }

    func linked(to tag: Tag) {
        onMain { self.linkedTags.insert(tag) }
    }

    func externalLink(to url: String) {
        onMain { self.externalLinks.insert(url) }
        delegate.externalLink(url)
    }

    func warning(_ warning: String) {
        onMain { self.warnings.insert(warning) }
        delegate.warning(warning)
    }

    func unsavedOutput(_ path: String, source: ItemReference) {
        onMain { self.unsavedOutputFiles[path, default: []].insert(source) }
        delegate.unsaved(path)
    }

    func markPageAsEmpty() {
        guard case .page(let page) = itemId.itemType else { return }
        onMain { self.pageIsEmpty = true }
        delegate.empty(.init(language: itemId.language, pageId: page.identifier))
    }

    func redirect(from originalUrl: String, to newUrl: String) {
        onMain { self.redirect = (originalUrl, newUrl) }
        delegate.redirect(from: originalUrl, to: newUrl)
    }
}

