import Foundation
import SwiftUI

final class FileResource: Item, LocalizedItem {

    override var itemType: ItemType { .file }

    let type: FileType

    /// Indicate if the file content is stored by the app
    @Published
    var isExternallyStored: Bool

    /// The file/image description in German
    @Published
    var german: String?

    /// The file/image description in English
    @Published
    var english: String?

    /// A version string of this resource, mostly for assets
    @Published
    var version: String?

    /// A URL where the resource was copied/downloaded from
    @Published
    var sourceUrl: String?

    /// The list of generated image versions for this image
    @Published
    var generatedImageVersions: Set<String>

    /// A custom file path in the output folder where this file is located
    @Published
    var customOutputPath: String?

    /// The date when the file was added
    @Published
    var addedDate: Date

    /// The date when the file was last modified
    @Published
    var modifiedDate: Date

    /// Identify this file as an asset to be placed in the `asset` folder on generation
    @Published
    var isAsset: Bool

    /// The dimensions of the image
    var imageDimensions: CGSize? {
        get { content.dimensions(of: identifier) }
        set {
            content.cache(dimensions: newValue, of: identifier)
            didChange(save: false)
        }
    }

    /// The size of the file in bytes
    var fileSize: Int? {
        get { content.size(of: identifier) }
        set {
            content.cache(size: newValue, of: identifier)
            didChange(save: false)
        }
    }

    var savedData: Data?

    init(content: Content,
         id: String,
         isExternallyStored: Bool,
         english: String?,
         german: String?,
         version: String? = nil,
         sourceUrl: String? = nil,
         generatedImageVersions: Set<String> = [],
         customOutputPath: String? = nil,
         addedDate: Date = .now,
         modifiedDate: Date = .now,
         isAsset: Bool = false) {
        self.type = FileType(fileExtension: id.fileExtension)
        self.isExternallyStored = isExternallyStored
        self.german = german
        self.english = english
        self.version = version
        self.sourceUrl = sourceUrl
        self.generatedImageVersions = generatedImageVersions
        self.customOutputPath = customOutputPath
        self.addedDate = addedDate
        self.modifiedDate = modifiedDate
        self.isAsset = isAsset
        super.init(content: content, id: id)
    }

    /**
     Only for bundle images
     */
    init(content: Content, 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
        self.version = nil
        self.sourceUrl = nil
        self.generatedImageVersions = []
        self.customOutputPath = nil
        self.addedDate = Date.now
        self.modifiedDate = Date.now
        self.isAsset = false
        super.init(content: content, id: resourceImage)
    }

    // MARK: Text

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

    func save(textContent: String) -> Bool {
        guard content.storage.save(fileContent: textContent, for: identifier) else {
            return false
        }
        modifiedDate = .now
        return true
    }

    @discardableResult
    func save(fileData: Foundation.Data) -> Bool {
        guard content.storage.save(fileData: fileData, for: identifier) else {
            return false
        }
        modifiedDate = .now
        return true
    }

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

    // 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.removeGeneratedImages()
        }
    }

    var imageToDisplay: Image? {
        guard let displayImageData else {
            return nil
        }
        update(fileSize: displayImageData.count)
        guard let loadedImage = NSImage(data: displayImageData) else {
            print("Failed to create image \(identifier)")
            return nil
        }
        update(imageDimensions: loadedImage.size)

        return .init(nsImage: loadedImage)
    }

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

    func getImageDimensions() -> CGSize? {
        if let imageDimensions {
            return imageDimensions
        }
        guard let size = getCurrentImageDimensions() else {
            return nil
        }
        self.update(imageDimensions: size)
        return size
    }

    private var displayImageData: Foundation.Data? {
        if type.isImage {
            guard let data = content.storage.fileData(for: identifier) else {
                print("Failed to load data for image \(identifier)")
                return nil
            }
            return data
        }
        if type.isVideo {
            return content.storage.getVideoThumbnail(for: identifier)
        }
        return nil
    }

    private func getCurrentImageDimensions() -> CGSize? {
        guard let displayImageData else {
            return nil
        }
        guard let loadedImage = NSImage(data: displayImageData) 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.identifier)
            self.update(fileSize: size)
        }
    }

    func removeGeneratedImages() {
        guard type.isImage else { return }
        guard content.storage.deleteInOutputFolder(outputImageFolder) else {
            return
        }
        self.generatedImageVersions = []
    }

    /// The path to the output folder where image versions are stored (no leading slash)
    var outputImageFolder: String {
        "\(content.settings.paths.imagesOutputFolderPath)/\(identifier.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(type: FileType? = nil, width: Int, height: Int, language: ContentLanguage, quality: CGFloat = 0.7, extraAttributes: String? = nil) -> ImageSet {
        let description = self.localized(in: language)
        return .init(
            image: self,
            type: type,
            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)
    }

    func linkPreviewImage(results: PageGenerationResults) -> String {
        let type: FileType
        switch self.type {
        case .jpg, .png, .gif: type = self.type
        default: type = .jpg
        }
        let version = imageVersion(
            width: content.settings.general.linkPreviewImageWidth,
            height: content.settings.general.linkPreviewImageHeight,
            type: type)
        results.require(image: version)
        return content.settings.general.url + version.outputPath
    }

    // MARK: Workout

    #warning("Set correct map image size, ratio from settings?")
    var mapImageDimensions: (width: Int, height: Int) {
        (width: content.settings.pages.largeImageWidth, height: 0)
    }

    var mapImagePath: String {
        let dimension = mapImageDimensions
        return outputPath(width: dimension.width, height: dimension.height, type: .png)
    }

    var statisticsFilePath: URL {
        let path = "\(content.settings.paths.filesOutputFolderPath)/\(identifier.fileNameWithoutExtension).route"
        let fullPath = makeCleanAbsolutePath(path)
        return URL(filePath: fullPath, directoryHint: .notDirectory)
    }

    var workoutData: WorkoutData? {
        guard type == .route else {
            return nil
        }
        guard let data = dataContent() else {
            return nil
        }
        return try? WorkoutData(data: data)
    }

    var routeOverview: RouteOverview? {
        workoutData?.overview
    }

    // MARK: Video thumbnail

    func createVideoThumbnail() {
        guard type.isVideo else { return }
        guard !content.storage.hasVideoThumbnail(for: identifier) else { return }
        Task {
            if await content.imageGenerator.createVideoThumbnail(for: identifier) {
                didChange()
            }
        }
    }

    private var _videoType: String?

    func videoType() -> String? {
        if let _videoType {
            return _videoType
        }
        _videoType = determineVideoType()
        return _videoType
    }

    private func determineVideoType() -> String? {
        let ffmpegPath = content.settings.tools.ffprobePath
        switch type {
        case .webm:
            return "video/webm"
        case .mp4, .m4v:
            if isExternallyStored {
                return "video/mp4"
            }
            break
        default:
            return nil
        }

        return content.storage.with(file: identifier) { path in
            let process = Process()
            let arguments = "-v error -select_streams v:0 -show_entries stream=codec_tag_string -of default=noprint_wrappers=1:nokey=1 \(path.path())"
                .components(separatedBy: " ")
            process.launchPath = ffmpegPath
            process.arguments = Array(arguments)

            let pipe = Pipe()
            process.standardOutput = pipe
            process.standardError = pipe

            process.launch()
            process.waitUntilExit()

            let outputData = pipe.fileHandleForReading.readDataToEndOfFile()
            let outputString = String(data: outputData, encoding: .utf8) ?? ""

            if process.terminationStatus != 0 {
                print("Failed to determine video type for \(identifier)")
                print(outputString)
                return nil
            }
            let firstLine = outputString.components(separatedBy: .newlines).first!.trimmed
            guard let type = VideoBlock.SourceType.h265(codec: firstLine) else {
                print("Unknown codec type for \(identifier): \(firstLine)")
                print(outputString)
                return "video/mp4"
            }
            return type.mimeType
        }
    }

    // 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, including a leading slash
     */
    var absoluteUrl: String {
        if let customOutputPath {
            if customOutputPath.hasPrefix("/") {
                return customOutputPath
            } else {
                return "/" + customOutputPath
            }
        }
        let path = pathPrefix + "/" + identifier
        return makeCleanAbsolutePath(path)
    }

    private var pathPrefix: String {
        if isAsset {
            return content.settings.paths.assetsOutputFolderPath
        }
        if type.isImage {
            return content.settings.paths.imagesOutputFolderPath
        }
        if type.isVideo {
            return content.settings.paths.videosOutputFolderPath
        }
        if type.isAudio {
            return content.settings.paths.audioOutputFolderPath
        }
        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 {
            identifier = newId
            return true
        }
        guard content.storage.move(file: identifier, to: newId) else {
            print("Failed to move file \(identifier) to \(newId)")
            return false
        }
        identifier = newId
        return true
    }
}

extension Array where Element == ContentLabel {

    mutating func insertOrReplace(icon: PageIcon, value: String) {
        insertOrReplace(label: .init(icon: icon, value: value))
    }

    mutating func insertOrReplace(label: ContentLabel) {
        if let index = firstIndex(where: { $0.icon == label.icon }) {
            self[index] = label
        } else {
            append(label)
        }
    }
}

extension FileResource: CustomStringConvertible {

    var description: String {
        identifier
    }
}

extension FileResource: StorageItem {

    convenience init(content: Content, id: String, data: FileResource.Data, isExternalFile: Bool) {
        self.init(
            content: content,
            id: id,
            isExternallyStored: isExternalFile,
            english: data.englishDescription,
            german: data.germanDescription,
            version: data.version,
            sourceUrl: data.sourceUrl,
            generatedImageVersions: Set(data.generatedImages ?? []),
            customOutputPath: data.customOutputPath,
            addedDate: data.addedDate,
            modifiedDate: data.modifiedDate,
            isAsset: data.isAsset ?? false)
        savedData = data
    }

    var data: Data {
        .init(
            englishDescription: english,
            germanDescription: german,
            generatedImages: generatedImageVersions.sorted().nonEmpty,
            customOutputPath: customOutputPath,
            version: version,
            sourceUrl: sourceUrl,
            addedDate: addedDate,
            modifiedDate: modifiedDate,
            isAsset: isAsset ? true : nil)
    }

    /// This struct holds metadata about a file resource that is stored in the content folder.
    struct Data: Codable, Equatable {
        let englishDescription: String?
        let germanDescription: String?
        let generatedImages: [String]?
        let customOutputPath: String?
        let version: String?
        let sourceUrl: String?
        let addedDate: Date
        let modifiedDate: Date
        let isAsset: Bool?
    }

    func saveToDisk(_ data: Data) -> Bool {
        content.storage.save(fileResource: data, for: identifier)
    }
}
