import Foundation
import SwiftSignalKit
import ManagedFile

final class MediaBoxFileManager {
    enum Mode {
        case read
        case readwrite
    }
    
    enum AccessError: Error {
        case generic
    }
    
    final class Item {
        final class Accessor {
            private let file: ManagedFile
            
            init(file: ManagedFile) {
                self.file = file
            }
            
            func write(_ data: UnsafeRawPointer, count: Int) -> Int {
                return self.file.write(data, count: count)
            }
            
            func read(_ data: UnsafeMutableRawPointer, _ count: Int) -> Int {
                return self.file.read(data, count)
            }
            
            func readData(count: Int) -> Data {
                return self.file.readData(count: count)
            }
            
            func seek(position: Int64) -> Bool {
                return self.file.seek(position: position)
            }
        }
        
        weak var manager: MediaBoxFileManager?
        let path: String
        let mode: Mode
        
        weak var context: ItemContext?
        
        init(manager: MediaBoxFileManager, path: String, mode: Mode) {
            self.manager = manager
            self.path = path
            self.mode = mode
        }
        
        deinit {
            if let manager = self.manager, let context = self.context {
                manager.discardItemContext(context: context)
            }
        }
        
        func access(_ f: (Accessor) throws -> Void) throws {
            if let context = self.context {
                try f(Accessor(file: context.file))
            } else {
                if let manager = self.manager {
                    if let context = manager.takeContext(path: self.path, mode: self.mode) {
                        self.context = context
                        try f(Accessor(file: context.file))
                    } else {
                        throw AccessError.generic
                    }
                } else {
                    throw AccessError.generic
                }
            }
        }
        
        func sync() {
            if let context = self.context {
                context.sync()
            }
        }
    }
    
    final class ItemContext {
        let id: Int
        let path: String
        let mode: Mode
        let file: ManagedFile
        
        private var isDisposed: Bool = false
        
        init?(id: Int, path: String, mode: Mode) {
            let mappedMode: ManagedFile.Mode
            switch mode {
            case .read:
                mappedMode = .read
            case .readwrite:
                mappedMode = .readwrite
            }
            
            guard let file = ManagedFile(queue: nil, path: path, mode: mappedMode) else {
                return nil
            }
            self.file = file
            
            self.id = id
            self.path = path
            self.mode = mode
        }
        
        deinit {
            assert(self.isDisposed)
        }
        
        func dispose() {
            if !self.isDisposed {
                self.isDisposed = true
                self.file._unsafeClose()
            } else {
                assertionFailure()
            }
        }
        
        func sync() {
            self.file.sync()
        }
    }
    
    private let queue: Queue?
    private var contexts: [Int: ItemContext] = [:]
    private var nextItemId: Int = 0
    private let maxOpenFiles: Int
    
    init(queue: Queue?) {
        self.queue = queue
        self.maxOpenFiles = 16
    }
    
    func open(path: String, mode: Mode) -> Item? {
        if let queue = self.queue {
            assert(queue.isCurrent())
        }
        
        return Item(manager: self, path: path, mode: mode)
    }
    
    private func takeContext(path: String, mode: Mode) -> ItemContext? {
        if let queue = self.queue {
            assert(queue.isCurrent())
        }
        
        if self.contexts.count > self.maxOpenFiles {
            if let minKey = self.contexts.keys.min(), let context = self.contexts[minKey] {
                self.discardItemContext(context: context)
            }
        }
        
        let id = self.nextItemId
        self.nextItemId += 1
        let context = ItemContext(id: id, path: path, mode: mode)
        self.contexts[id] = context
        return context
    }
    
    private func discardItemContext(context: ItemContext) {
        if let queue = self.queue {
            assert(queue.isCurrent())
        }
        
        if let context = self.contexts.removeValue(forKey: context.id) {
            context.dispose()
        }
    }
}