//
//  DraftAttachment.swift
//  CoreData
//
//  Created by Shadowfacts on 4/22/23.
//

import CoreData
import PencilKit
import UniformTypeIdentifiers
import Photos
import InstanceFeatures
import Pachyderm

private let decoder = PropertyListDecoder()
private let encoder = PropertyListEncoder()

@objc
public final class DraftAttachment: NSManagedObject, Identifiable {
    
    @nonobjc public class func fetchRequest() -> NSFetchRequest<DraftAttachment> {
        return NSFetchRequest<DraftAttachment>(entityName: "DraftAttachment")
    }
    
    @NSManaged internal var assetID: String?
    @NSManaged public var attachmentDescription: String
    @NSManaged internal private(set) var drawingData: Data?
    @NSManaged public var editedAttachmentID: String?
    @NSManaged private var editedAttachmentKindString: String?
    @NSManaged public var editedAttachmentURL: URL?
    @NSManaged public var fileURL: URL?
    @NSManaged internal var fileType: String?
    @NSManaged public var id: UUID!
    
    @NSManaged internal var draft: Draft
    
    public var drawing: PKDrawing? {
        get {
            if let drawingData,
               let drawing = try? decoder.decode(PKDrawing.self, from: drawingData) {
                return drawing
            } else {
                return nil
            }
        }
        set {
            drawingData = try! encoder.encode(newValue)
        }
    }
    
    public var data: AttachmentData {
        if let editedAttachmentID {
            return .editing(editedAttachmentID, editedAttachmentKind!, editedAttachmentURL!)
        } else if let assetID {
            return .asset(assetID)
        } else if let drawing {
            return .drawing(drawing)
        } else if let fileURL, let fileType {
            return .file(fileURL, UTType(fileType)!)
        } else {
            return .none
        }
    }
    
    public var editedAttachmentKind: Attachment.Kind? {
        get {
            editedAttachmentKindString.flatMap(Attachment.Kind.init(rawValue:))
        }
        set {
            editedAttachmentKindString = newValue?.rawValue
        }
    }
    
    public enum AttachmentData {
        case asset(String)
        case drawing(PKDrawing)
        case file(URL, UTType)
        case editing(String, Attachment.Kind, URL)
        case none
    }
    
    public override func prepareForDeletion() {
        super.prepareForDeletion()
        if let fileURL {
            try? FileManager.default.removeItem(at: fileURL)
        }
    }
    
}

extension DraftAttachment {
    var type: AttachmentType {
        if let editedAttachmentKind {
            switch editedAttachmentKind {
            case .image:
                return .image
            case .video:
                return .video
            case .gifv:
                return .video
            case .audio, .unknown:
                return .unknown
            }
        } else if let assetID {
            guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetID], options: nil).firstObject else {
                return .unknown
            }
            switch asset.mediaType {
            case .image:
                return .image
            case .video:
                return .video
            default:
                return .unknown
            }
        } else if drawingData != nil {
            return .image
        } else if let fileType,
                  let type = UTType(fileType) {
            if type.conforms(to: .image) {
                return .image
            } else if type.conforms(to: .movie) {
                return .video
            } else {
                return .unknown
            }
        } else {
            return .unknown
        }
    }
    
    enum AttachmentType {
        case image, video, unknown
    }
}

//private let attachmentTypeIdentifier = "space.vaccor.Tusker.composition-attachment"

private let imageType = UTType.image.identifier
private let heifType = UTType.heif.identifier
private let heicType = UTType.heic.identifier
private let jpegType = UTType.jpeg.identifier
private let pngType = UTType.png.identifier
private let mp4Type = UTType.mpeg4Movie.identifier
private let quickTimeType = UTType.quickTimeMovie.identifier
private let gifType = UTType.gif.identifier

extension DraftAttachment: NSItemProviderReading {
    public static var readableTypeIdentifiersForItemProvider: [String] {
        // todo: is there a better way of handling movies than manually adding all possible UTI types?
        // just using kUTTypeMovie doesn't work, because we need the actually type in order to get the file extension
        // without the file extension, getting the thumbnail and exporting the video for attachment upload fails
        [/*typeIdentifier, */ gifType, heifType, heicType, jpegType, pngType, imageType, mp4Type, quickTimeType]
    }
    
    public static func object(withItemProviderData data: Data, typeIdentifier: String) throws -> DraftAttachment {
        var data = data
        var type = UTType(typeIdentifier)!
        
        // this seems to only occur when the item is a UIImage, rather than just image data,
        // which seems to only occur when sharing a screenshot directly from the markup screen
        if type == .image,
           let image = try? NSKeyedUnarchiver.unarchivedObject(ofClass: UIImage.self, from: data),
           let pngData = image.pngData() {
            data = pngData
            type = .png
        }
        
        let attachment = DraftAttachment(entity: DraftsPersistentContainer.shared.persistentStoreCoordinator.managedObjectModel.entitiesByName["DraftAttachment"]!, insertInto: nil)
        attachment.id = UUID()
        attachment.fileURL = try writeDataToFile(data, id: attachment.id, type: type)
        attachment.fileType = type.identifier
        attachment.attachmentDescription = ""
        return attachment
    }
    
    static var attachmentsDirectory: URL {
        let containerURL = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.space.vaccor.Tusker")!
        return containerURL.appendingPathComponent("Documents").appendingPathComponent("attachments")
    }
    
    static func writeDataToFile(_ data: Data, id: UUID, type: UTType) throws -> URL {
        let directoryURL = attachmentsDirectory
        try FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true)
        let attachmentURL = directoryURL.appendingPathComponent(id.uuidString, conformingTo: type)
        try data.write(to: attachmentURL)
        return attachmentURL
    }
}

// MARK: Exporting

extension DraftAttachment {
    func getData(features: InstanceFeatures, skipAllConversion: Bool = false, completion: @escaping (Result<(Data, UTType), ExportError>) -> Void) {
        if let assetID {
            guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetID], options: nil).firstObject else {
                completion(.failure(.noAsset))
                return
            }
            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, let dataUTI else {
                        completion(.failure(.missingAssetData))
                        return
                    }
                    let processed = Self.processImageData(data, type: UTType(dataUTI)!, features: features, skipAllConversion: skipAllConversion)
                    completion(.success(processed))
                }
            } else if asset.mediaType == .video {
                let options = PHVideoRequestOptions()
                options.version = .current
                options.deliveryMode = .automatic
                options.isNetworkAccessAllowed = true
                PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { exportSession, info in
                    if let exportSession {
                        Self.exportVideoData(session: exportSession, features: features, completion: completion)
                    } else if let error = info?[PHImageErrorKey] as? Error {
                        completion(.failure(.videoExport(error)))
                    } else {
                        completion(.failure(.noVideoExportSession))
                    }
                }
            } else {
                completion(.failure(.unknownAssetType))
            }
        } else if let drawingData {
            guard let drawing = try? decoder.decode(PKDrawing.self, from: drawingData) else {
                completion(.failure(.loadingDrawing))
                return
            }
            let image = drawing.imageInLightMode(from: drawing.bounds, scale: 1)
            completion(.success((image.pngData()!, .png)))
        } else if let fileURL, let fileType {
            let type = UTType(fileType)!
            
            if type.conforms(to: .movie) {
                let asset = AVURLAsset(url: fileURL)
                guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
                    completion(.failure(.noVideoExportSession))
                    return
                }
                Self.exportVideoData(session: session, features: features, completion: completion)
            } else {
                let fileData: Data
                do {
                    fileData = try Data(contentsOf: fileURL)
                } catch {
                    completion(.failure(.loadingData))
                    return
                }
                
                if type != .gif,
                   type.conforms(to: .image) {
                    let result = Self.processImageData(fileData, type: type, features: features, skipAllConversion: skipAllConversion)
                    completion(.success(result))
                } else {
                    completion(.success((fileData, type)))
                }
            }
        } else {
            completion(.failure(.noData))
        }
    }
    
    private static 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 || type == .heif {
            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, features: InstanceFeatures, completion: @escaping (Result<(Data, UTType), ExportError>) -> Void) {
        session.outputFileType = .mp4
        session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
        if let configuration = features.mediaAttachmentsConfiguration {
            session.fileLengthLimit = Int64(configuration.videoSizeLimit)
        }
        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 ExportError: Error {
        case noAsset
        case unknownAssetType
        case missingAssetData
        case videoExport(Error)
        case noVideoExportSession
        case loadingDrawing
        case loadingData
        case noData
    }
}
