import Foundation
import SwiftUI

final class FileResource: Item {

    let type: FileType

    @Published
    var isExternallyStored: Bool

    @Published
    var german: String

    @Published
    var english: String

    /// The dimensions of the image
    @Published
    var imageDimensions: CGSize? = nil

    /// The size of the file in bytes
    @Published
    var fileSize: Int? = nil

    init(content: Content, id: String, isExternallyStored: Bool, en: String, de: String) {
        self.type = FileType(fileExtension: id.fileExtension)
        self.english = en
        self.german = de
        self.isExternallyStored = isExternallyStored
        super.init(content: content, id: id)
    }

    /**
     Only for bundle images
     */
    init(resourceImage: String, type: FileType) {
        self.type = type
        self.english = "A test image included in the bundle"
        self.german = "Ein Testbild aus dem Bundle"
        self.isExternallyStored = true
        super.init(content: .mock, id: resourceImage) // TODO: Add images to mock
    }

    // MARK: Text

    func textContent() -> String {
        content.storage.fileContent(for: id) ?? ""
    }

    func dataContent() -> Data? {
        content.storage.fileData(for: id)
    }

    // MARK: Images

    var aspectRatio: CGFloat {
        guard let imageDimensions else {
            return 0
        }
        guard imageDimensions.height > 0 else {
            return 0
        }
        return imageDimensions.width / imageDimensions.height
    }

    private func update(imageDimensions size: CGSize?) {
        guard let imageDimensions, let size else {
            // First computation
            DispatchQueue.main.async {
                self.imageDimensions = size
            }
            return
        }
        guard imageDimensions != size else {
            return
        }
        // Image must have changed, so force regeneration
        DispatchQueue.main.async {
            self.imageDimensions = size
            self.didChange()
            self.removeGeneratedImages()
        }
    }

    var imageToDisplay: Image {
        guard let imageData = content.storage.fileData(for: id) else {
            print("Failed to load data for image \(id)")
            return failureImage
        }
        update(fileSize: imageData.count)
        guard let loadedImage = NSImage(data: imageData) else {
            print("Failed to create image \(id)")
            return failureImage
        }
        update(imageDimensions: loadedImage.size)

        return .init(nsImage: loadedImage)
    }

    func determineImageDimensions() {
        let size = getImageDimensions()
        self.update(imageDimensions: size)
    }

    private func getImageDimensions() -> CGSize? {
        guard type.isImage else {
            return nil
        }
        guard let imageData = content.storage.fileData(for: id) else {
            return nil
        }
        guard let loadedImage = NSImage(data: imageData) else {
            return nil
        }
        return loadedImage.size
    }

    func update(fileSize size: Int?) {
        guard let fileSize, let size else {
            // First computation
            DispatchQueue.main.async {
                self.fileSize = size
            }
            return
        }
        guard fileSize != size else {
            return
        }
        // File must have changed
        DispatchQueue.main.async {
            self.fileSize = size
            self.didChange()
            self.removeGeneratedImages()
        }
    }

    func determineFileSize() {
        DispatchQueue.global(qos: .userInitiated).async {
            let size = self.content.storage.size(of: self.id)
            self.update(fileSize: size)
        }
    }

    func removeGeneratedImages() {
        guard type.isImage else { return }
        content.imageGenerator.removeVersions(of: id)
        content.storage.deleteInOutputFolder(outputImageFolder)
    }

    private var failureImage: Image {
        Image(systemSymbol: .exclamationmarkTriangle)
    }

    /// The path to the output folder where image versions are stored (no leading slash)
    var outputImageFolder: String {
        "\(content.settings.paths.imagesOutputFolderPath)/\(id.fileNameWithoutExtension)"
    }

    func outputPath(width: Int, height: Int, type: FileType?) -> String {
        let prefix = "/\(outputImageFolder)/\(width)x\(height)"
        guard let ext = type?.fileExtension else {
            return prefix
        }
        return prefix + "." + ext
    }

    func imageSet(width: Int, height: Int, language: ContentLanguage, quality: CGFloat = 0.7, extraAttributes: String? = nil) -> ImageSet {
        let description = self.localized(in: language)
        return .init(
            image: self,
            maxWidth: width,
            maxHeight: height,
            description: description,
            quality: quality,
            extraAttributes: extraAttributes)
    }

    func imageVersion(width: Int, height: Int, type: FileType) -> ImageVersion {
        .init(image: self, type: type, maximumWidth: width, maximumHeight: height)
    }

    // MARK: Paths

    func removeFileFromOutputFolder() {
        content.storage.deleteInOutputFolder(absoluteUrl)
        if type.isImage {
            removeGeneratedImages()
        }
    }

    /**
     Get the url path to a file in the output folder.
     The result is an absolute path from the output folder for use in HTML.
     */
    var absoluteUrl: String {
        let path = pathPrefix + "/" + id
        return makeCleanAbsolutePath(path)
    }

    private var pathPrefix: String {
        if type.isImage {
            return content.settings.paths.imagesOutputFolderPath
        }
        if type.isVideo {
            return content.settings.paths.videosOutputFolderPath
        }
        if type.isAudio {
            return content.settings.paths.audioOutputFolderPath
        }
        if type.isAsset {
            return content.settings.paths.assetsOutputFolderPath
        }
        return content.settings.paths.filesOutputFolderPath
    }

    // MARK: File

    func isValid(id: String) -> Bool {
        !id.isEmpty &&
        content.isValidIdForFile(id) &&
        content.isNewIdForFile(id)
    }

    @discardableResult
    func update(id newId: String) -> Bool {
        guard !isExternallyStored else {
            id = newId
            return true
        }
        guard content.storage.move(file: id, to: newId) else {
            print("Failed to move file \(id) to \(newId)")
            return false
        }
        id = newId
        return true
    }
}

extension FileResource: LocalizedItem {

}

extension FileResource: CustomStringConvertible {

    var description: String {
        id
    }
}
