/**
 For in explanation of the `actor` reference type, check out
 https://www.donnywals.com/an-introduction-to-synchronizing-access-with-swifts-actors/
 */
import Foundation
import UIKit

extension UIImage {
    var hasAlpha: Bool {
        guard let alphaInfo = self.cgImage?.alphaInfo else {
            return false
        }
        return alphaInfo != CGImageAlphaInfo.none &&
            alphaInfo != CGImageAlphaInfo.noneSkipFirst &&
            alphaInfo != CGImageAlphaInfo.noneSkipLast
    }
}

actor ImageLoader {
    private enum LoaderStatus {
        case inProgress(Task<UIImage, Error>)
        case fetched(UIImage)
    }

    private var images: [URLRequest: LoaderStatus] = [:]

    public func fetch(_ url: URL) async throws -> UIImage {
        let request = URLRequest(url: url)
        return try await fetch(request)
    }

    public func fetch(_ urlRequest: URLRequest) async throws -> UIImage {
        if let status = images[urlRequest] {
            switch status {
            case .fetched(let image):
                return image
            case .inProgress(let task):
                return try await task.value
            }
        }
        
        if let image = try? self.imageFromFileSystem(for: urlRequest) {
            images[urlRequest] = .fetched(image)
            return image
        }
                
        let task: Task<UIImage, Error> = Task {
            let (imageData, _) = try await URLSession.shared.data(forRequest: urlRequest)
            
            let image = UIImage(data: imageData)!
            try? self.persistImage(image, for: urlRequest)
            return image
        }
        
        images[urlRequest] = .inProgress(task)
        
        let image = try await task.value
        
        images[urlRequest] = .fetched(image)
        
        return image
    }
    
    public func imageFromFileSystem(for urlRequest: URLRequest) throws -> UIImage? {
        guard let url = fileName(for: urlRequest) else {
            assertionFailure("Unable to generate a local path for \(urlRequest)")
            return nil
        }
        
        let data = try Data(contentsOf: url)
        return UIImage(data: data)
    }
    
    public func fileName(for urlRequest: URLRequest) -> URL? {
        guard let fileName = urlRequest.url?.pathComponents.joined(separator: "-") else { return nil }
        return FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
    }
    
    public func persistImage(_ image: UIImage, for urlRequest: URLRequest) throws {
        let imageData: Data? = image.hasAlpha ? image.pngData() : image.jpegData(compressionQuality: 0.8)
        
        guard let url = fileName(for: urlRequest),
              let data = imageData else {
            assertionFailure("Unable to generate a local path for \(urlRequest)")
            return
        }

        try data.write(to: url)
    }
}

enum URLError: Error {
    case unknownError
    case noLocalFile
    case badServerResponse
}

extension URLSession {
    func data(forRequest request: URLRequest) async throws -> (Data, URLResponse) {
        if #available(iOS 15.0, *) {
            return try await self.data(for: request, delegate: nil)
        } else {
            
            // Fallback on earlier versions
            return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<(Data, URLResponse), Error>) -> Void in
                let task = self.dataTask(with: request) { maybeData, maybeResponse, maybeError in
                    
                    let result: Result<(Data, URLResponse), Error>
                    
                    defer {
                        continuation.resume(with: result)
                    }
                    
                    guard let response = maybeResponse,
                          let data = maybeData else {
                              result = .failure(maybeError ?? URLError.unknownError)
                              return
                          }
                    
                    result = .success((data, response))
                }
                task.resume()
            }
        }
    }
}
