//
//  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//

import Foundation
import AVFoundation

public enum OWSThumbnailError: Error {
    case failure(description: String)
    case assertionFailure(description: String)
    case externalError(description: String, underlyingError:Error)
}

@objc public class OWSLoadedThumbnail: NSObject {
    public typealias DataSourceBlock = () throws -> Data

    @objc
    public let image: UIImage
    let dataSourceBlock: DataSourceBlock

    @objc
    public init(image: UIImage, filePath: String) {
        self.image = image
        self.dataSourceBlock = {
            return try Data(contentsOf: URL(fileURLWithPath: filePath))
        }
    }

    @objc
    public init(image: UIImage, data: Data) {
        self.image = image
        self.dataSourceBlock = {
            return data
        }
    }

    @objc
    public func data() throws -> Data {
        return try dataSourceBlock()
    }
}

private struct OWSThumbnailRequest {
    public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void
    public typealias FailureBlock = (Error) -> Void

    let attachment: TSAttachmentStream
    let thumbnailDimensionPoints: UInt
    let success: SuccessBlock
    let failure: FailureBlock

    init(attachment: TSAttachmentStream, thumbnailDimensionPoints: UInt, success: @escaping SuccessBlock, failure: @escaping FailureBlock) {
        self.attachment = attachment
        self.thumbnailDimensionPoints = thumbnailDimensionPoints
        self.success = success
        self.failure = failure
    }
}

@objc public class OWSThumbnailService: NSObject {

    // MARK: - Singleton class

    @objc(shared)
    public static let shared = OWSThumbnailService()

    public typealias SuccessBlock = (OWSLoadedThumbnail) -> Void
    public typealias FailureBlock = (Error) -> Void

    private let serialQueue = DispatchQueue(label: "OWSThumbnailService")

    // This property should only be accessed on the serialQueue.
    //
    // We want to process requests in _reverse_ order in which they
    // arrive so that we prioritize the most recent view state.
    private var thumbnailRequestStack = [OWSThumbnailRequest]()

    private override init() {
        super.init()

        SwiftSingletons.register(self)
    }

    private func canThumbnailAttachment(attachment: TSAttachmentStream) -> Bool {
        return attachment.isImage || attachment.isAnimated || attachment.isVideo
    }

    // success and failure will be called async _off_ the main thread.
    @objc
    public func ensureThumbnail(forAttachment attachment: TSAttachmentStream,
                                                     thumbnailDimensionPoints: UInt,
                                                     success: @escaping SuccessBlock,
                                                     failure: @escaping FailureBlock) {
        serialQueue.async {
            let thumbnailRequest = OWSThumbnailRequest(attachment: attachment, thumbnailDimensionPoints: thumbnailDimensionPoints, success: success, failure: failure)
            self.thumbnailRequestStack.append(thumbnailRequest)

            self.processNextRequestSync()
        }
    }

    private func processNextRequestAsync() {
        serialQueue.async {
            self.processNextRequestSync()
        }
    }

    // This should only be called on the serialQueue.
    private func processNextRequestSync() {
        guard let thumbnailRequest = thumbnailRequestStack.popLast() else {
            return
        }

        do {
            let loadedThumbnail = try process(thumbnailRequest: thumbnailRequest)
            DispatchQueue.global().async {
                thumbnailRequest.success(loadedThumbnail)
            }
        } catch {
            Logger.error("Could not create thumbnail: \(error)")

            DispatchQueue.global().async {
                thumbnailRequest.failure(error)
            }
        }
    }

    // This should only be called on the serialQueue.
    //
    // It should be safe to assume that an attachment will never end up with two thumbnails of
    // the same size since:
    //
    // * Thumbnails are only added by this method.
    // * This method checks for an existing thumbnail using the same connection.
    // * This method is performed on the serial queue.
    private func process(thumbnailRequest: OWSThumbnailRequest) throws -> OWSLoadedThumbnail {
        let attachment = thumbnailRequest.attachment
        guard canThumbnailAttachment(attachment: attachment) else {
            throw OWSThumbnailError.failure(description: "Cannot thumbnail attachment.")
        }
        let thumbnailPath = attachment.path(forThumbnailDimensionPoints: thumbnailRequest.thumbnailDimensionPoints)
        if FileManager.default.fileExists(atPath: thumbnailPath) {
            guard let image = UIImage(contentsOfFile: thumbnailPath) else {
                throw OWSThumbnailError.failure(description: "Could not load thumbnail.")
            }
            return OWSLoadedThumbnail(image: image, filePath: thumbnailPath)
        }

        Logger.verbose("Creating thumbnail of size: \(thumbnailRequest.thumbnailDimensionPoints)")

        let thumbnailDirPath = (thumbnailPath as NSString).deletingLastPathComponent
        guard OWSFileSystem.ensureDirectoryExists(thumbnailDirPath) else {
            throw OWSThumbnailError.failure(description: "Could not create attachment's thumbnail directory.")
        }
        guard let originalFilePath = attachment.originalFilePath else {
            throw OWSThumbnailError.failure(description: "Missing original file path.")
        }
        let maxDimension = CGFloat(thumbnailRequest.thumbnailDimensionPoints)
        let thumbnailImage: UIImage
        if attachment.isImage || attachment.isAnimated {
            thumbnailImage = try OWSMediaUtils.thumbnail(forImageAtPath: originalFilePath, maxDimension: maxDimension)
        } else if attachment.isVideo {
            thumbnailImage = try OWSMediaUtils.thumbnail(forVideoAtPath: originalFilePath, maxDimension: maxDimension)
        } else {
            throw OWSThumbnailError.assertionFailure(description: "Invalid attachment type.")
        }
        guard let thumbnailData = thumbnailImage.jpegData(compressionQuality: 0.85) else {
            throw OWSThumbnailError.failure(description: "Could not convert thumbnail to JPEG.")
        }
        do {
            try thumbnailData.write(to: URL(fileURLWithPath: thumbnailPath), options: .atomicWrite)
        } catch let error as NSError {
            throw OWSThumbnailError.externalError(description: "File write failed: \(thumbnailPath), \(error)", underlyingError: error)
        }
        OWSFileSystem.protectFileOrFolder(atPath: thumbnailPath)
        return OWSLoadedThumbnail(image: thumbnailImage, data: thumbnailData)
    }
}
