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

import Foundation
import CryptoKit

struct DiskCacheTransformer<T> {
    let toData: (T) throws -> Data
    let fromData: (Data) throws -> T
}

class DiskCache<T> {
    
    let fileManager: FileManager
    let path: String
    let defaultExpiry: CacheExpiry
    let transformer: DiskCacheTransformer<T>
    
    private var fileStates = MultiThreadDictionary<String, FileState>()
    
    init(name: String, defaultExpiry: CacheExpiry, transformer: DiskCacheTransformer<T>, fileManager: FileManager = .default) throws {
        self.defaultExpiry = defaultExpiry
        self.transformer = transformer
        self.fileManager = fileManager
        
        let cacheDir = try fileManager.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        
        self.path = cacheDir.appendingPathComponent(name, isDirectory: true).path
        
        try createDirectory()
    }
    
    private func createDirectory() throws {
        if !fileManager.fileExists(atPath: path) {
            try fileManager.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
        }
    }
    
    private func makeFileName(for key: String) -> String {
        let ext = (key as NSString).pathExtension
        
        let digest = Insecure.MD5.hash(data: key.data(using: .utf8)!)
        let hash = digest.map { String($0, radix: 16) }.joined()
        
        if ext.isEmpty {
            return hash
        } else {
            return "\(hash).\(ext)"
        }
    }
    
    private func makeFilePath(for key: String) -> String {
        return (path as NSString).appendingPathComponent(makeFileName(for: key))
    }
    
    private func fileState(forKey key: String) -> FileState {
        return fileStates[key] ?? .unknown
    }
    
    func setObject(_ object: T, forKey key: String) throws {
        let data = try transformer.toData(object)
        let path = makeFilePath(for: key)
        guard fileManager.createFile(atPath: path, contents: data, attributes: [.modificationDate: defaultExpiry.date]) else {
            throw Error.couldNotCreateFile
        }
        fileStates[key] = .exists
    }
    
    func removeObject(forKey key: String) throws {
        let path = makeFilePath(for: key)
        try fileManager.removeItem(atPath: path)
        fileStates[key] = .doesNotExist
    }
    
    func existsObject(forKey key: String) throws -> Bool {
        switch fileState(forKey: key) {
        case .exists:
            return true
        case .doesNotExist:
            return false
        case .unknown:
            let path = makeFilePath(for: key)
            guard fileManager.fileExists(atPath: path) else {
                return false
            }
            let attributes = try fileManager.attributesOfItem(atPath: path)
            guard let date = attributes[.modificationDate] as? Date else {
                throw Error.malformedFileAttributes
            }
            return date.timeIntervalSinceNow >= 0
        }
    }
    
    func object(forKey key: String) throws -> T {
        let path = makeFilePath(for: key)
        let attributes = try fileManager.attributesOfItem(atPath: path)
        
        guard let date = attributes[.modificationDate] as? Date else {
            throw Error.malformedFileAttributes
        }
        guard date.timeIntervalSinceNow >= 0 else {
            try fileManager.removeItem(atPath: path)
            fileStates[key] = .doesNotExist
            throw Error.expired
        }
        
        let data = try Data(contentsOf: URL(fileURLWithPath: path, isDirectory: false))
        let object = try transformer.fromData(data)
        return object
    }
    
    func removeAll() throws {
        try fileManager.removeItem(atPath: path)
        try createDirectory()
        fileStates.withLock { dict in
            dict.removeAll()
        }
    }
    
    func getSizeInBytes() -> Int64? {
        return fileManager.recursiveSize(url: URL(fileURLWithPath: path, isDirectory: true))
    }
    
}

extension DiskCache {
    enum Error: Swift.Error {
        case malformedFileAttributes
        case couldNotCreateFile
        case expired
    }
}

extension DiskCache {
    enum FileState {
        case exists, doesNotExist, unknown
    }
}

extension DiskCache where T == Data {
    convenience init(name: String, defaultExpiry: CacheExpiry) throws {
        try self.init(name: name, defaultExpiry: defaultExpiry, transformer: DiskCacheTransformer(toData: { $0 }, fromData: { $0 }))
    }
}
