import Foundation
import AppKit
import SDWebImageAVIFCoder
import SDWebImageWebPCoder
import AVFoundation

final class ImageGenerator {

    private let storage: Storage

    private let settings: Settings

    init(storage: Storage, settings: Settings) {
        self.storage = storage
        self.settings = settings
    }

    private var outputFolder: String {
        settings.paths.imagesOutputFolderPath
    }

    func needsToGenerate(_ version: ImageVersion) -> Bool {
        if version.wasPreviouslyGenerated {
            return !exists(version) 
        }
        if exists(version) {
            // Mark as already generated
            version.wasNowGenerated()
            return false
        }
        return true
    }

    // MARK: Files

    private func exists(_ version: ImageVersion) -> Bool {
        storage.hasFileInOutputFolder(version.outputPath)
    }

    private func write(imageData data: Data, of version: ImageVersion) -> Bool {
        return storage.write(data, to: version.outputPath)
    }

    // MARK: Image operations

    func generate(version: ImageVersion) -> Bool {
        if version.image.type == .route {
            return generateImageForRoute(version: version)
        }
        if version.type == .avif {
            if version.image.type == .gif {
                // Skip GIFs, since they can't be converted by avifenc
                return true
            }

            if createAvifUsingBash(version: version) {
                version.wasNowGenerated()
                return true
            }
            return false
        }
        guard let data = version.image.dataContent() else {
            print("ImageGenerator: Failed to load data for image \(version.image.identifier)")
            return false
        }

        return generate(version: version, data: data)
    }

    private func generate(version: ImageVersion, data: Data) -> Bool {
        guard let originalImage = NSImage(data: data) else {
            print("ImageGenerator: Failed to load image \(version.image.identifier)")
            return false
        }

        let representation = create(image: originalImage, width: CGFloat(version.maximumWidth), height: CGFloat(version.maximumHeight))

        guard let data = create(image: representation, type: version.type, quality: version.quality) else {
            print("ImageGenerator: Failed to get data for type \(version.type) of image \(version.image.identifier)")
            return false
        }

        guard write(imageData: data, of: version) else {
            return false
        }
        version.wasNowGenerated()
        return true
    }

    private func create(image originalImage: NSImage, width: CGFloat, height: CGFloat) -> NSBitmapImageRep {
        let sourceRep = originalImage.representations[0]
        let sourceSize = NSSize(width: sourceRep.pixelsWide, height: sourceRep.pixelsHigh)
        let maximumSize = NSSize(width: width, height: height)
        let destinationSize = sourceSize.scaledToFit(in: maximumSize)

        // create NSBitmapRep manually, if using cgImage, the resulting size is wrong
        let representation = NSBitmapImageRep(
            bitmapDataPlanes: nil,
            pixelsWide: Int(destinationSize.width),
            pixelsHigh: Int(destinationSize.height),
            bitsPerSample: 8,
            samplesPerPixel: 4,
            hasAlpha: true,
            isPlanar: false,
            colorSpaceName: NSColorSpaceName.deviceRGB,
            bytesPerRow: Int(destinationSize.width) * 4,
            bitsPerPixel: 32)!

        let ctx = NSGraphicsContext(bitmapImageRep: representation)
        NSGraphicsContext.saveGraphicsState()
        NSGraphicsContext.current = ctx
        originalImage.draw(in: NSMakeRect(0, 0, destinationSize.width, destinationSize.height))
        ctx?.flushGraphics()
        NSGraphicsContext.restoreGraphicsState()
        return representation
    }

    // MARK: Routes

    private func generateImageForRoute(version: ImageVersion) -> Bool {
        let largeImagePath = version.image.mapImagePath

        guard storage.hasFileInOutputFolder(largeImagePath) else {
            print("ImageGenerator: No map image generated for route \(version.image.identifier)")
            return false
        }

        let largeImageUrl = URL(fileURLWithPath: largeImagePath)
        guard let imageData = try? Data(contentsOf: largeImageUrl) else {
            print("ImageGenerator: Failed to read map image data for route \(version.image.identifier)")
            return false
        }

        if version.type == .avif {
            let originalImagePath = version.image.outputPath(width: version.maximumWidth, height: version.maximumHeight, type: .png)
            guard createAvifUsingBash(version: version, imagePath: originalImagePath) else {
                return false
            }
            version.wasNowGenerated()
            return true
        }

        return generate(version: version, data: imageData)
    }

    // MARK: Avif images

    private func create(image: NSBitmapImageRep, type: FileType, quality: CGFloat) -> Data? {
        switch type {
        case .jpg:
            return image.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: 0.6)])
        case .png:
            return image.representation(using: .png, properties: [.compressionFactor: NSNumber(value: 0.6)])
        case .avif:
            return createAvif(image: image, quality: 0.7)
        case .webp:
            return createWebp(image: image, quality: 0.8)
        case .gif:
            return image.representation(using: .gif, properties: [.compressionFactor: NSNumber(value: quality)])
        case .svg:
            return nil
        case .tiff:
            return nil
        default:
            return nil
        }
    }

    private func createAvif(image: NSBitmapImageRep, quality: CGFloat) -> Data? {
        let newImage = NSImage(size: image.size)
        newImage.addRepresentation(image)
        return SDImageAVIFCoder.shared.encodedData(with: newImage, format: .AVIF, options: [.encodeCompressionQuality: quality])
    }

    private func createAvifUsingBash(version: ImageVersion) -> Bool {
        let baseVersion = ImageVersion(
            image: version.image,
            type: version.image.type,
            maximumWidth: version.maximumWidth,
            maximumHeight: version.maximumHeight)
        let originalImagePath = storage.outputPath(to: baseVersion.outputPath)!.path()
        return createAvifUsingBash(version: version, imagePath: originalImagePath)
    }

    private func createAvifUsingBash(version: ImageVersion, imagePath: String) -> Bool {
        let generatedImagePath = storage.outputPath(to: version.outputPath)!.path()
        let quality = Int(version.quality * 100)

        // TODO: Run in security scope
        let process = Process()
        process.launchPath = settings.tools.avifencPath
        process.arguments = ["-q", "\(quality)", imagePath, generatedImagePath]

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

        process.launch()
        process.waitUntilExit()

        if process.terminationStatus != 0 {
            print("ImageGenerator: Failed to create AVIF image \(version.image.identifier)")
            let outputData = pipe.fileHandleForReading.readDataToEndOfFile()
            let outputString = String(data: outputData, encoding: .utf8) ?? ""
            print(outputString)
            return false
        }
        return true
    }

    private func createWebp(image: NSBitmapImageRep, quality: CGFloat) -> Data? {
        let newImage = NSImage(size: image.size)
        newImage.addRepresentation(image)
        return SDImageWebPCoder.shared.encodedData(with: newImage, format: .webP, options: [.encodeCompressionQuality: quality])
    }

    // MARK: Video thumbnails

    @discardableResult
    func createVideoThumbnail(for videoId: String) async -> Bool {
        guard let image = await storage.with(file: videoId, perform: generateThumbnail) else {
            print("Failed to generate thumbnail image for video \(videoId)")
            return false
        }
        let scaled = create(image: image, width: image.size.width, height: image.size.height)
        guard let data = scaled.representation(using: .jpeg, properties: [.compressionFactor: NSNumber(value: 0.6)]) else {
            print("Failed to get thumbnail jpg data of video \(videoId)")
            return false
        }
        if !storage.save(thumbnail: data, for: videoId) {
            print("Failed to save thumbnail of video \(videoId)")
        }
        print("Generated video thumbnail for \(videoId)")
        return true
    }

    private func generateThumbnail(for url: URL) async -> NSImage? {
        let time = CMTime(seconds: 1, preferredTimescale: 600)
        let asset = AVURLAsset(url: url)
        let imageGenerator = AVAssetImageGenerator(asset: asset)
        imageGenerator.appliesPreferredTrackTransform = true // Correct for orientation
        return await withCheckedContinuation { continuation in
            imageGenerator.generateCGImageAsynchronously(for: time) { cgImage, _, error in
                if let error {
                    print("Error generating thumbnail for \(url.path()): \(error.localizedDescription)")
                }
                if let cgImage {
                    let image = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
                    continuation.resume(returning: image)
                } else {
                    continuation.resume(returning: nil)
                }
            }
        }
    }

    func getVideoDuration(for videoId: String) async -> TimeInterval? {
        guard let duration = await storage.with(file: videoId, perform: getVideoDuration) else {
            print("Failed to determine duration for video \(videoId)")
            return nil
        }
        return duration
    }

    private func getVideoDuration(url: URL) async -> TimeInterval? {
        let asset = AVURLAsset(url: url)
        do {
            let duration = try await asset.load(.duration)
            return CMTimeGetSeconds(duration)
        } catch {
            print("ImageGenerator: Failed to determine video duration: \(error.localizedDescription)")
            return nil
        }
    }
}
