import Foundation
import SwiftUI
import Combine

final class Content: ChangeObservableItem {

    @ObservedObject
    var storage: Storage

    @Published
    var settings: Settings!

    @Published
    var posts: [Post]

    @Published
    var pages: [Page]

    @Published
    var tags: [Tag]

    @Published
    var files: [FileResource]

    @Published
    var tagOverview: Tag?

    @Published
    var results: GenerationResults

    @Published
    var storageErrors: [StorageError] = []

    @Published
    var generationStatus: String = "Ready to generate"

    @Published
    private(set) var isGeneratingWebsite = false

    @Published
    private(set) var shouldGenerateWebsite = false

    @Published
    private(set) var saveState: SaveState = .isSaved

    let imageGenerator: ImageGenerator

    var errorCallback: ((StorageError) -> Void)?

    var cancellables: Set<AnyCancellable> = []

    /// A cache of file sizes
    private var fileSizes: [String: Int] = [:]

    init() {
        let settings = Settings.default
        self.settings = settings
        self.posts = []
        self.pages = []
        self.tags = []
        self.files = []
        self.tagOverview = nil
        self.results = .init()

        let storage = Storage()
        self.storage = storage
        self.imageGenerator = ImageGenerator(
            storage: storage,
            settings: settings)
        storage.errorNotification = { [weak self] error in
            guard let self else { return }
            DispatchQueue.main.async {
                self.storageErrors.append(error)
            }
        }
        settings.content = self
    }

    private func clear() {
        self.settings = .default
        self.posts = []
        self.pages = []
        self.tags = []
        self.files = []
        self.tagOverview = nil
        self.results = .init()
    }

    var images: [FileResource] {
        files.filter { $0.type.isImage }
    }

    func set(isGenerating: Bool) {
        self.isGeneratingWebsite = isGenerating
    }

    func set(shouldGenerate: Bool) {
        self.shouldGenerateWebsite = shouldGenerate
    }

    func add(_ file: FileResource) {
        // TODO: Insert at correct index?
        files.insert(file, at: 0)
    }

    func add(_ page: Page) {
        // TODO: Insert at correct index?
        pages.insert(page, at: 0)
    }

    func update(contentPath: URL, callback: @escaping () -> ()) {
        guard storage.save(contentPath: contentPath) else {
            return
        }
        loadFromDisk(callback: callback)
    }

    // MARK: Removing items

    func remove(_ file: FileResource) {
        files.remove(file)
        for post in posts {
            post.remove(file)
        }

        for page in pages {
            page.remove(file)
        }

        for tag in tags {
            tag.remove(file)
        }
        settings.remove(file)
    }

    func remove(_ post: Post) {
        posts.remove(post)
    }

    func remove(_ page: Page) {
        pages.remove(page)
        for post in posts {
            if post.linkedPage == page {
                post.linkedPage = nil
            }
        }
        // TODO: Check for page links and other references in content
    }

    func remove(_ tag: Tag) {
        tags.remove(tag)
        for post in posts {
            post.tags.remove(tag)
        }
        for page in pages {
            page.tags.remove(tag)
        }
        // TODO: Check for tag links and other references is content
    }

    // MARK: Loading

    func file(withOutputPath: String) -> FileResource? {
        files.first { $0.absoluteUrl == withOutputPath }
    }

    func loadFromDisk(callback: @escaping () -> ()) {
        DispatchQueue.global().async {
            self.loadInBackground(callback: callback)
        }
    }

    private func loadInBackground(callback: @escaping () -> ()) {
        let loader = ModelLoader(content: self, storage: self.storage)
        let result = loader.load()

        DispatchQueue.main.async {
            self.files = result.files
            self.posts = result.posts
            self.pages = result.pages
            self.tags = result.tags
            self.settings = result.settings
            self.tagOverview = result.tagOverview
            self.storageErrors.append(contentsOf: result.errors)
            if !result.errors.isEmpty {
                self.saveState = .savingPausedDueToLoadErrors
            } else {
                self.saveState = .isSaved
            }
            callback()
            self.generateMissingVideoThumbnails()
        }
    }

    func resumeSavingAfterLoadingErrors() {
        saveState = .needsSave
        saveIfNeeded()
    }

    func generateMissingVideoThumbnails() {
        Task {
            for file in self.files {
                guard file.type.isVideo else { continue }
                guard !file.isExternallyStored else { continue }
                guard !storage.hasVideoThumbnail(for: file.identifier) else { continue }
                if await imageGenerator.createVideoThumbnail(for: file.identifier) {
                    print("Generated thumbnail for \(file.identifier)")
                    file.didChange()
                }
            }
        }
    }

    // MARK: Saving

    private(set) var lastSave: Date = .now

    private(set) var lastModification: Date = .now

    func update(saveState: SaveState) {
        DispatchQueue.main.async {
            self.saveState = saveState
        }
    }

    func setModificationTimestamp() {
        self.lastModification = .now
    }

    func setLastSaveTimestamp() {
        self.lastSave = .now
    }

    func needsSaving() {
        needsSave()
    }

    // MARK: File sizes

    func size(of file: String) -> Int? {
        fileSizes[file]
    }

    func cache(size: Int?, of file: String) {
        fileSizes[file] = size
    }

    // MARK: Image dimensions

    private var imageDimensions: [String: CGSize] = [:]

    private let imageDimensionsQueue = DispatchQueue(label: "imageDimensionsQueue")

    func dimensions(of image: String) -> CGSize? {
        imageDimensionsQueue.sync {
            imageDimensions[image]
        }
    }

    func cache(dimensions: CGSize?, of image: String) {
        imageDimensionsQueue.sync {
            imageDimensions[image] = dimensions
        }
    }
}
