//
//  ImageCache.swift
//  Tusker
//
//  Created by Shadowfacts on 8/21/18.
//  Copyright © 2018 Shadowfacts. All rights reserved.
//

import UIKit

class ImageCache {
    
    static let avatars = ImageCache(name: "Avatars", memoryExpiry: .seconds(60 * 60), diskExpiry: .seconds(60 * 60 * 24 * 7), desiredSize: CGSize(width: 50, height: 50))
    static let headers = ImageCache(name: "Headers", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
    static let attachments = ImageCache(name: "Attachments", memoryExpiry: .seconds(60 * 2))
    static let emojis = ImageCache(name: "Emojis", memoryExpiry: .seconds(60 * 5), diskExpiry: .seconds(60 * 60 * 24 * 7))
    
    #if DEBUG
    private static let disableCaching = ProcessInfo.processInfo.environment.keys.contains("DISABLE_IMAGE_CACHE")
    #else
    private static let disableCaching = false
    #endif

    private let cache: ImageDataCache
    private let desiredPixelSize: CGSize?
    
    init(name: String, memoryExpiry: CacheExpiry, diskExpiry: CacheExpiry? = nil, desiredSize: CGSize? = nil) {
        // todo: might not always want to use UIScreen.main for this, e.g. Catalyst?
        let pixelSize = desiredSize?.applying(.init(scaleX: UIScreen.main.scale, y: UIScreen.main.scale))
        self.desiredPixelSize = pixelSize
        self.cache = ImageDataCache(name: name, memoryExpiry: memoryExpiry, diskExpiry: diskExpiry, storeOriginalDataInMemory: diskExpiry == nil, desiredPixelSize: pixelSize)
    }
    
    func get(_ url: URL, loadOriginal: Bool = false, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
        if !ImageCache.disableCaching,
           let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
            completion?(entry.data, entry.image)
            return nil
        } else {
            return getFromSource(url, completion: completion)
        }
    }
    
    func getFromSource(_ url: URL, completion: ((Data?, UIImage?) -> Void)?) -> Request? {
        return Task.detached(priority: .userInitiated) {
            let result = await self.fetch(url: url)
            switch result {
            case .data(let data):
                completion?(data, nil)
            case .dataAndImage(let data, let image):
                completion?(data, image)
            case .none:
                completion?(nil, nil)
            }
        }
    }
    
    func get(_ url: URL, loadOriginal: Bool = false) async -> (Data?, UIImage?) {
        if !ImageCache.disableCaching,
           let entry = try? cache.get(url.absoluteString, loadOriginal: loadOriginal) {
            return (entry.data, entry.image)
        } else {
            let result = await self.fetch(url: url)
            switch result {
            case .data(let data):
                return (data, nil)
            case .dataAndImage(let data, let image):
                return (data, image)
            case .none:
                return (nil, nil)
            }
        }
    }
    
    func fetchIfNotCached(_ url: URL) {
        // if caching is disabled, don't bother fetching since nothing will be done with the result
        guard !ImageCache.disableCaching else { return }
        
        if !((try? cache.has(url.absoluteString)) ?? false) {
            Task.detached(priority: .medium) {
                _ = await self.fetch(url: url)
            }
        }
    }
    
    private func fetch(url: URL) async -> FetchResult {
        guard let (data, _) = try? await URLSession.shared.data(from: url) else {
            return .none
        }
        guard let image = UIImage(data: data) else {
            try? cache.set(url.absoluteString, data: data, image: nil)
            return .data(data)
        }
        let preparedImage: UIImage?
        if let desiredPixelSize {
            preparedImage = await image.byPreparingThumbnail(ofSize: desiredPixelSize)
        } else {
            preparedImage = await image.byPreparingForDisplay()
        }
        try? cache.set(url.absoluteString, data: data, image: preparedImage ?? image)
        return .dataAndImage(data, preparedImage ?? image)
    }
    
    func getData(_ url: URL) -> Data? {
        return try? cache.getData(url.absoluteString)
    }
    
    func get(_ url: URL, loadOriginal: Bool = false) -> ImageDataCache.Entry? {
        return try? cache.get(url.absoluteString, loadOriginal: loadOriginal)
    }
    
    func reset() throws {
        try cache.removeAll()
    }
    
    func getDiskSizeInBytes() -> Int64? {
        return cache.disk?.getSizeInBytes()
    }
    
    typealias Request = Task<Void, Never>
    
    enum FetchResult {
        case data(Data)
        case dataAndImage(Data, UIImage)
        case none
    }
    
}
