import Foundation
import LottieMeshSwift
import Postbox
import SwiftSignalKit
import GZip
import AppBundle

public final class MeshAnimationCache {
    private final class Item {
        var isPending: Bool = false
        let disposable = MetaDisposable()
        var readyPath: String?
        var animation: MeshAnimation?
    }

    private let mediaBox: MediaBox

    private var items: [String: Item] = [:]

    public init(mediaBox: MediaBox) {
        self.mediaBox = mediaBox
    }

    public func get(resource: MediaResource) -> MeshAnimation? {
        if let item = self.items[resource.id.stringRepresentation] {
            if let animation = item.animation {
                return animation
            } else if let readyPath = item.readyPath, let data = try? Data(contentsOf: URL(fileURLWithPath: readyPath), options: [.alwaysMapped]) {
                let buffer = MeshReadBuffer(data: data)
                let animation = MeshAnimation.read(buffer: buffer)
                item.animation = animation
                return animation
            } else {
                return nil
            }
        } else {
            let item = Item()
            self.items[resource.id.stringRepresentation] = item

            let path = self.mediaBox.cachedRepresentationPathForId(resource.id.stringRepresentation, representationId: "mesh-animation", keepDuration: .general)
            if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.alwaysMapped]) {
                let animation = MeshAnimation.read(buffer: MeshReadBuffer(data: data))
                item.readyPath = path
                item.animation = animation
                return animation
            } else {
                self.cache(item: item, resource: resource)
                return nil
            }
        }
    }
    
    public func get(bundleName: String) -> MeshAnimation? {
        if let item = self.items[bundleName] {
            if let animation = item.animation {
                return animation
            } else if let readyPath = item.readyPath, let data = try? Data(contentsOf: URL(fileURLWithPath: readyPath), options: [.alwaysMapped]) {
                let buffer = MeshReadBuffer(data: data)
                let animation = MeshAnimation.read(buffer: buffer)
                item.animation = animation
                return animation
            } else {
                return nil
            }
        } else {
            let item = Item()
            self.items[bundleName] = item

            let path = self.mediaBox.cachedRepresentationPathForId(bundleName, representationId: "mesh-animation", keepDuration: .general)
            if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.alwaysMapped]) {
                let animation = MeshAnimation.read(buffer: MeshReadBuffer(data: data))
                item.readyPath = path
                item.animation = animation
                return animation
            } else {
                self.cache(item: item, bundleName: bundleName)
                return nil
            }
        }
    }

    private func cache(item: Item, resource: MediaResource) {
        let mediaBox = self.mediaBox
        item.isPending = true
        item.disposable.set((self.mediaBox.resourceData(resource)
        |> filter { data in
            return data.complete
        }
        |> take(1)
        |> mapToSignal { data -> Signal<(MeshAnimation, String)?, NoError> in
            return Signal { subscriber in
                guard let zippedData = try? Data(contentsOf: URL(fileURLWithPath: data.path), options: [.alwaysMapped]) else {
                    subscriber.putNext(nil)
                    subscriber.putCompletion()
                    return EmptyDisposable
                }
                let jsonData = TGGUnzipData(zippedData, 1 * 1024 * 1024) ?? zippedData
                if let tempFile = generateMeshAnimation(data: jsonData) {
                    mediaBox.storeCachedResourceRepresentation(resource.id.stringRepresentation, representationId: "mesh-animation", keepDuration: .general, tempFile: tempFile, completion: { path in
                        if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.alwaysMapped]) {
                            let animation = MeshAnimation.read(buffer: MeshReadBuffer(data: data))
                            subscriber.putNext((animation, path))
                            subscriber.putCompletion()
                        } else {
                            subscriber.putNext(nil)
                            subscriber.putCompletion()
                        }
                    })
                } else {
                    subscriber.putNext(nil)
                    subscriber.putCompletion()
                    return EmptyDisposable
                }

                return EmptyDisposable
            }
            |> runOn(Queue.concurrentBackgroundQueue())
        }
        |> deliverOnMainQueue).start(next: { [weak self] animationAndPath in
            guard let strongSelf = self else {
                return
            }
            if let animationAndPath = animationAndPath {
                if let item = strongSelf.items[resource.id.stringRepresentation] {
                    item.isPending = false
                    item.animation = animationAndPath.0
                    item.readyPath = animationAndPath.1
                }
            }
        }))
    }
    
    private func cache(item: Item, bundleName: String) {
        let mediaBox = self.mediaBox
        item.isPending = true
        item.disposable.set((Signal<(MeshAnimation, String)?, NoError> { subscriber in
            guard let path = getAppBundle().path(forResource: bundleName, ofType: "tgs") else {
                subscriber.putNext(nil)
                subscriber.putCompletion()
                return EmptyDisposable
            }
            
            guard let zippedData = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.alwaysMapped]) else {
                subscriber.putNext(nil)
                subscriber.putCompletion()
                return EmptyDisposable
            }
            let jsonData = TGGUnzipData(zippedData, 1 * 1024 * 1024) ?? zippedData
            if let tempFile = generateMeshAnimation(data: jsonData) {
                mediaBox.storeCachedResourceRepresentation(bundleName, representationId: "mesh-animation", keepDuration: .general, tempFile: tempFile, completion: { path in
                    if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.alwaysMapped]) {
                        let animation = MeshAnimation.read(buffer: MeshReadBuffer(data: data))
                        subscriber.putNext((animation, path))
                        subscriber.putCompletion()
                    } else {
                        subscriber.putNext(nil)
                        subscriber.putCompletion()
                    }
                })
            } else {
                subscriber.putNext(nil)
                subscriber.putCompletion()
                return EmptyDisposable
            }

            return EmptyDisposable
        }
        |> runOn(Queue.concurrentDefaultQueue())
        |> deliverOnMainQueue).start(next: { [weak self] animationAndPath in
            guard let strongSelf = self else {
                return
            }
            if let animationAndPath = animationAndPath {
                if let item = strongSelf.items[bundleName] {
                    item.isPending = false
                    item.animation = animationAndPath.0
                    item.readyPath = animationAndPath.1
                }
            }
        }))
    }
}