//
//  LazyDecoding.swift
//  Tusker
//
//  Created by Shadowfacts on 4/11/20.
//  Copyright © 2020 Shadowfacts. All rights reserved.
//

import Foundation
import CoreData

private let decoder = PropertyListDecoder()
private let encoder = PropertyListEncoder()

@propertyWrapper
public struct LazilyDecoding<Enclosing: NSManagedObject, Value: Codable> {

    private let keyPath: ReferenceWritableKeyPath<Enclosing, Data?>
    private let fallback: Value
    private var value: Value?
    private var observation: NSKeyValueObservation?
    private var skipClearingOnNextUpdate = false
    
    init(from keyPath: ReferenceWritableKeyPath<Enclosing, Data?>, fallback: Value) {
        self.keyPath = keyPath
        self.fallback = fallback
    }

    public var wrappedValue: Value {
        get { fatalError("called LazilyDecoding wrappedValue getter") }
        set { fatalError("called LazilyDecoding wrappedValue setter") }
    }
    
    public static subscript(_enclosingInstance instance: Enclosing, wrapped wrappedKeyPath: ReferenceWritableKeyPath<Enclosing, Value>, storage storageKeyPath: ReferenceWritableKeyPath<Enclosing, Self>) -> Value {
        get {
            instance.performOnContext {
                var wrapper = instance[keyPath: storageKeyPath]
                if let value = wrapper.value {
                    return value
                } else {
                    guard let data = instance[keyPath: wrapper.keyPath] else { return wrapper.fallback }
                    do {
                        let value = try decoder.decode(Box.self, from: data)
                        wrapper.value = value.value
                        wrapper.observation = instance.observe(wrapper.keyPath, changeHandler: { instance, _ in
                            var wrapper = instance[keyPath: storageKeyPath]
                            if wrapper.skipClearingOnNextUpdate {
                                wrapper.skipClearingOnNextUpdate = false
                            } else {
                                wrapper.removeCachedValue()
                            }
                            instance[keyPath: storageKeyPath] = wrapper
                        })
                        instance[keyPath: storageKeyPath] = wrapper
                        return value.value
                    } catch {
                        return wrapper.fallback
                    }
                }
            }
        }
        set {
            instance.performOnContext {
                var wrapper = instance[keyPath: storageKeyPath]
                wrapper.value = newValue
                wrapper.skipClearingOnNextUpdate = true
                instance[keyPath: storageKeyPath] = wrapper
                let newData = try! encoder.encode(Box(value: newValue))
                instance[keyPath: wrapper.keyPath] = newData
            }
        }
    }
    
    mutating func removeCachedValue() {
        value = nil
        observation = nil
    }
    
}

extension NSManagedObject {
    fileprivate func performOnContext<V>(_ f: () -> V) -> V {
        if let managedObjectContext {
            managedObjectContext.performAndWait(f)
        } else {
            f()
        }
    }
}

extension LazilyDecoding {
    init<T>(arrayFrom keyPath: ReferenceWritableKeyPath<Enclosing, Data?>) where Value == [T] {
        self.init(from: keyPath, fallback: [])
    }
}

extension LazilyDecoding {
    // PropertyListEncoder only allows top-level types to be dicts or arrays, which breaks encoding nil-able values.
    // Wrapping everything in a Box ensures that it's always a dict.
    struct Box: Codable {
        let value: Value
    }
}
