//
//  AttachmentData.swift
//  ComposeUI
//
//  Created by Shadowfacts on 1/1/20.
//  Copyright © 2020 Shadowfacts. All rights reserved.
//

import UIKit
import Photos
import UniformTypeIdentifiers
import PencilKit
import InstanceFeatures

enum AttachmentData {
    case asset(PHAsset)
    case image(Data, originalType: UTType)
    case video(URL)
    case drawing(PKDrawing)
    case gif(Data)
    
    var type: AttachmentType {
        switch self {
        case let .asset(asset):
            return asset.attachmentType!
        case .image(_, originalType: _):
            return .image
        case .video(_):
            return .video
        case .drawing(_):
            return .image
        case .gif(_):
            return .image
        }
    }
    
    var isAsset: Bool {
        switch self {
        case .asset(_):
            return true
        default:
            return false
        }
    }
    
    var canSaveToDraft: Bool {
        switch self {
        case .video(_):
            return false
        default:
            return true
        }
    }
    
    func getData(features: InstanceFeatures, skipAllConversion: Bool = false, completion: @escaping (Result<(Data, UTType), Error>) -> Void) {
        switch self {
        case let .image(originalData, originalType):
            let data: Data
            let type: UTType
            switch originalType {
            case .png, .jpeg:
                data = originalData
                type =  originalType
            default:
                let image = UIImage(data: originalData)!
                // The quality of 0.8 was chosen completely arbitrarily, it may need to be tuned in the future.
                data = image.jpegData(compressionQuality: 0.8)!
                type = .jpeg
            }
            let processed = processImageData(data, type: type, features: features, skipAllConversion: skipAllConversion)
            completion(.success(processed))
        case let .asset(asset):
            if asset.mediaType == .image {
                let options = PHImageRequestOptions()
                options.version = .current
                options.deliveryMode = .highQualityFormat
                options.resizeMode = .none
                options.isNetworkAccessAllowed = true
                PHImageManager.default().requestImageDataAndOrientation(for: asset, options: options) { (data, dataUTI, orientation, info) in
                    guard let data = data, let dataUTI = dataUTI else {
                        completion(.failure(.missingData))
                        return
                    }
                    let processed = processImageData(data, type: UTType(dataUTI)!, features: features, skipAllConversion: skipAllConversion)
                    completion(.success(processed))
                }
            } else if asset.mediaType == .video {
                let options = PHVideoRequestOptions()
                options.deliveryMode = .automatic
                options.isNetworkAccessAllowed = true
                options.version = .current
                PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in
                    if let exportSession = exportSession {
                        AttachmentData.exportVideoData(session: exportSession, completion: completion)
                    } else if let error = info?[PHImageErrorKey] as? Error {
                        completion(.failure(.videoExport(error)))
                    } else {
                        completion(.failure(.noVideoExportSession))
                    }
                }
            } else {
                fatalError("assetType must be either image or video")
            }
        case let .video(url):
            let asset = AVURLAsset(url: url)
            guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
                completion(.failure(.noVideoExportSession))
                return
            }
            AttachmentData.exportVideoData(session: session, completion: completion)
            
        case let .drawing(drawing):
            let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1)
            completion(.success((image.pngData()!, .png)))
        case let .gif(data):
            completion(.success((data, .gif)))
        }
    }
    
    private func processImageData(_ data: Data, type: UTType, features: InstanceFeatures, skipAllConversion: Bool) -> (Data, UTType) {
        guard !skipAllConversion else {
            return (data, type)
        }
        
        var data = data
        var type = type
        let image = CIImage(data: data)!
        let needsColorSpaceConversion = features.needsWideColorGamutHack && image.colorSpace?.name != CGColorSpace.sRGB
        
        // neither Mastodon nor Pleroma handles HEIC well, so convert to JPEG
        // they also do a bad job converting wide color gamut images (they seem to just drop the profile, letting the wide-gamut values be reinterprete as sRGB)
        // if that changes in the future, we'll need to pass the InstanceFeatures in here somehow and gate the conversion
        if needsColorSpaceConversion || type == .heic {
            let context = CIContext()
            let colorSpace = needsColorSpaceConversion || image.colorSpace != nil ? CGColorSpace(name: CGColorSpace.sRGB)! : image.colorSpace!
            if type == .png {
                data = context.pngRepresentation(of: image, format: .ARGB8, colorSpace: colorSpace)!
            } else {
                data = context.jpegRepresentation(of: image, colorSpace: colorSpace)!
                type = .jpeg
            }
        }
        
        return (data, type)
    }
    
    private static func exportVideoData(session: AVAssetExportSession, completion: @escaping (Result<(Data, UTType), Error>) -> Void) {
        session.outputFileType = .mp4
        session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
        session.exportAsynchronously {
            guard session.status == .completed else {
                completion(.failure(.videoExport(session.error!)))
                return
            }
            do {
                let data = try Data(contentsOf: session.outputURL!)
                completion(.success((data, .mpeg4Movie)))
            } catch {
                completion(.failure(.videoExport(error)))
            }
        }
    }
    
    enum AttachmentType {
        case image, video
    }
    
    enum Error: Swift.Error, LocalizedError {
        case missingData
        case videoExport(Swift.Error)
        case noVideoExportSession
        
        var localizedDescription: String {
            switch self {
            case .missingData:
                return "Missing Data"
            case .videoExport(let error):
                return "Exporting video: \(error)"
            case .noVideoExportSession:
                return "Couldn't create video export session"
            }
        }
    }
}

extension PHAsset {
    var attachmentType: AttachmentData.AttachmentType? {
        switch self.mediaType {
        case .image:
            return .image
        case .video:
            return .video
        default:
            return nil
        }
    }
}

extension AttachmentData: Codable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        
        switch self {
        case let .asset(asset):
            try container.encode("asset", forKey: .type)
            try container.encode(asset.localIdentifier, forKey: .assetIdentifier)
        case let .image(originalData, originalType):
            try container.encode("image", forKey: .type)
            try container.encode(originalType, forKey: .imageType)
            try container.encode(originalData, forKey: .imageData)
        case .video(_):
            throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "video CompositionAttachments cannot be encoded"))
        case let .drawing(drawing):
            try container.encode("drawing", forKey: .type)
            let drawingData = drawing.dataRepresentation()
            try container.encode(drawingData, forKey: .drawing)
        case .gif(_):
            throw EncodingError.invalidValue(self, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "gif CompositionAttachments cannot be encoded"))
        }
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        switch try container.decode(String.self, forKey: .type) {
        case "asset":
            let identifier = try container.decode(String.self, forKey: .assetIdentifier)
            guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [identifier], options: nil).firstObject else {
                throw DecodingError.dataCorruptedError(forKey: .assetIdentifier, in: container, debugDescription: "Could not fetch asset with local identifier")
            }
            self = .asset(asset)
        case "image":
            let data = try container.decode(Data.self, forKey: .imageData)
            if let type = try container.decodeIfPresent(UTType.self, forKey: .imageType) {
                self = .image(data, originalType: type)
            } else {
                guard let image = UIImage(data: data) else {
                    throw DecodingError.dataCorruptedError(forKey: .imageData, in: container, debugDescription: "CompositionAttachment data could not be decoded into UIImage")
                }
                let jpegData = image.jpegData(compressionQuality: 1)!
                self = .image(jpegData, originalType: .jpeg)
            }
        case "drawing":
            let drawingData = try container.decode(Data.self, forKey: .drawing)
            let drawing = try PKDrawing(data: drawingData)
            self = .drawing(drawing)
        default:
            throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "CompositionAttachment type must be one of image, asset, or drawing")
        }
    }
    
    enum CodingKeys: CodingKey {
        case type
        case imageData
        case imageType
        /// The local identifier of the PHAsset for this attachment
        case assetIdentifier
        /// The PKDrawing object for this attachment.
        case drawing
    }
}

extension AttachmentData: Equatable {
    static func ==(lhs: AttachmentData, rhs: AttachmentData) -> Bool {
        switch (lhs, rhs) {
        case let (.asset(a), .asset(b)):
            return a.localIdentifier == b.localIdentifier
        case let (.image(a, originalType: aType), .image(b, originalType: bType)):
            return a == b && aType == bType
        case let (.video(a), .video(b)):
            return a == b
        case let (.drawing(a), .drawing(b)):
            return a == b
        default:
            return false
        }
    }
}
