//
//  ImageDataCache.swift
//  Tusker
//
//  Created by Shadowfacts on 1/16/21.
//  Copyright © 2021 Shadowfacts. All rights reserved.
//

import UIKit

class ImageDataCache {
    
    private let memory: MemoryCache<Entry>
    let disk: DiskCache<Data>?
    
    private let storeOriginalDataInMemory: Bool
    private let desiredPixelSize: CGSize?
    
    init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry?, storeOriginalDataInMemory: Bool, desiredPixelSize: CGSize?) {
        self.memory = MemoryCache(defaultExpiry: memoryExpiry)
        
        if let diskExpiry = diskExpiry {
            self.disk = try! DiskCache(name: name, defaultExpiry: diskExpiry)
        } else {
            self.disk = nil
        }
        
        self.storeOriginalDataInMemory = storeOriginalDataInMemory
        self.desiredPixelSize = desiredPixelSize
    }
    
    func has(_ key: String) throws -> Bool {
        if memory.existsObject(forKey: key) {
            return true
        } else if let disk = self.disk,
                  try disk.existsObject(forKey: key) {
            return true
        } else {
            return false
        }
    }
    
    func get(_ key: String, loadOriginal: Bool) throws -> Entry? {
        if storeOriginalDataInMemory || !loadOriginal,
           let memoryEntry = try? memory.object(forKey: key) {
            return memoryEntry
        } else if let disk = self.disk,
                  let data = try? disk.object(forKey: key),
                  let image = UIImage(data: data) {
            return Entry(data: data, image: image)
        } else {
            return nil
        }
    }
    
    func getImage(_ key: String) throws -> UIImage? {
        return try get(key, loadOriginal: false)?.image
    }
    
    func getData(_ key: String) throws -> Data? {
        return try get(key, loadOriginal: false)?.data
    }
    
    func set(_ key: String, data: Data, image: UIImage?) throws {
        guard let image = scaleImageIfDesired(data: data) ?? image else { return }
        
        let entry = Entry(data: storeOriginalDataInMemory ? data : nil, image: image)
        memory.setObject(entry, forKey: key)
        
        if let disk = self.disk {
            try disk.setObject(data, forKey: key)
        }
    }
    
    func removeAll() throws {
        memory.removeAll()
        try? disk?.removeAll()
    }
    
    // TODO: consider removing this and letting ImageCache just use the UIImage thumbnailing API
    private func scaleImageIfDesired(data: Data) -> UIImage? {
        guard let desiredPixelSize = desiredPixelSize,
              let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceShouldCache: false] as CFDictionary) else {
            return nil
        }
        
        let maxDimension = max(desiredPixelSize.width, desiredPixelSize.height)
        let downsampleOptions: [CFString: Any] = [
            kCGImageSourceCreateThumbnailFromImageAlways: true,
            kCGImageSourceShouldCacheImmediately: true,
            kCGImageSourceCreateThumbnailWithTransform: true,
            kCGImageSourceThumbnailMaxPixelSize: maxDimension
        ]
        
        if let downsampled = CGImageSourceCreateThumbnailAtIndex(source, 0, downsampleOptions as CFDictionary) {
            return UIImage(cgImage: downsampled)
        } else {
            return nil
        }
    }
    
}

extension ImageDataCache {
    struct Entry {
        let data: Data?
        let image: UIImage
    }
}
