//
//  MainThreadOperationQueue.swift
//  RSCore
//
//  Created by Brent Simmons on 1/10/20.
//  Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//

import Foundation

public protocol MainThreadOperationDelegate: AnyObject {
	
	@MainActor func operationDidComplete(_ operation: MainThreadOperation)
	@MainActor func cancelOperation(_ operation: MainThreadOperation)
	@MainActor func make(_ childOperation: MainThreadOperation, dependOn parentOperation: MainThreadOperation)
}

/// Manage a queue of MainThreadOperation tasks.
///
/// Runs them one at a time; runs them on the main thread.
/// Any operation can use DispatchQueue or whatever to run code off of the main thread.
/// An operation calls back to the queue when it’s completed or canceled.
///
/// Use this only on the main thread.
/// The operation can be suspended and resumed.
/// It is *not* suspended on creation.
@MainActor public final class MainThreadOperationQueue {

	/// Use the shared queue when you don’t need to create a separate queue.
	@MainActor public static let shared: MainThreadOperationQueue = {
		MainThreadOperationQueue()
	}()

	private var operations = [Int: MainThreadOperation]()
	private var pendingOperationIDs = [Int]()
	private var currentOperationID: Int?
	@MainActor private static var incrementingID = 0
	private var isSuspended = false
	private let dependencies = MainThreadOperationDependencies()

	/// Meant for testing; not intended to be useful.
	public var pendingOperationsCount: Int {
		return pendingOperationIDs.count
	}

	public init() {
		// Silence compiler complaint about init not being public.
	}

	/// Add an operation to the queue.
	@MainActor public func add(_ operation: MainThreadOperation) {
		precondition(Thread.isMainThread)
		operation.operationDelegate = self
		let operationID = ensureOperationID(operation)
		operations[operationID] = operation

		assert(!pendingOperationIDs.contains(operationID))
		if !pendingOperationIDs.contains(operationID) {
			pendingOperationIDs.append(operationID)
		}

		runNextOperationIfNeeded()
	}

	/// Add multiple operations to the queue.
	/// This has the same effect as calling addOperation one-by-one.
	@MainActor public func addOperations(_ operations: [MainThreadOperation]) {
		operations.forEach{ add($0) }
	}

	/// Add a dependency. Do this *before* calling addOperation, since addOperation might run the operation right away.
	@MainActor public func make(_ childOperation: MainThreadOperation, dependOn parentOperation: MainThreadOperation) {
		precondition(Thread.isMainThread)
		let childOperationID = ensureOperationID(childOperation)
		let parentOperationID = ensureOperationID(parentOperation)
		dependencies.make(childOperationID, dependOn: parentOperationID)
	}

	/// Cancel all the current and pending operations.
	public func cancelAllOperations() {
		precondition(Thread.isMainThread)
		var operationIDsToCancel = pendingOperationIDs
		if let currentOperationID = currentOperationID {
			operationIDsToCancel.append(currentOperationID)
		}
		cancel(operationIDsToCancel)
	}

	/// Cancel some operations. If any of them have dependent operations,
	/// those operations will be canceled also.
	@MainActor public func cancelOperations(_ operations: [MainThreadOperation]) {
		precondition(Thread.isMainThread)
		let operationIDsToCancel = operations.map{ ensureOperationID($0) }
		assert(allOperationIDsArePendingOrCurrent(operationIDsToCancel))
		assert(allOperationIDsAreInStorage(operationIDsToCancel))

		cancel(operationIDsToCancel)
		runNextOperationIfNeeded()
	}

	/// Cancel operations with the given name. If any of them have dependent
	/// operations, they will be canceled too.
	///
	/// This will cancel the current operation, not just pending operations,
	/// if it has the specified name.
	@MainActor public func cancelOperations(named name: String) {
		precondition(Thread.isMainThread)
		guard let operationsToCancel = pendingAndCurrentOperations(named: name) else {
			return
		}
		cancelOperations(operationsToCancel)
	}

	/// Stop running operations until resume() is called.
	/// The current operation, if there is one, will run to completion —
	/// it will not be canceled.
	public func suspend() {
		precondition(Thread.isMainThread)
		isSuspended = true
	}

	/// Resume running operations.
	public func resume() {
		precondition(Thread.isMainThread)
		isSuspended = false
		runNextOperationIfNeeded()
	}
}

extension MainThreadOperationQueue: MainThreadOperationDelegate {

	@MainActor public func operationDidComplete(_ operation: MainThreadOperation) {
		precondition(Thread.isMainThread)
		operationDidFinish(operation)
	}

	@MainActor public func cancelOperation(_ operation: MainThreadOperation) {
		cancelOperations([operation])
	}
}

private extension MainThreadOperationQueue {

	var pendingOperations: [MainThreadOperation] {
		return pendingOperationIDs.compactMap { (operationID) -> MainThreadOperation? in
			guard let operation = operations[operationID] else {
				assertionFailure("Expected operation, got nil.")
				return nil
			}
			return operation
		}
	}

	var currentOperation: MainThreadOperation? {
		guard let operationID = currentOperationID else {
			return nil
		}
		return operations[operationID]
	}

	func pendingAndCurrentOperations(named name: String) -> [MainThreadOperation]? {
		var operations = pendingOperations.filter { $0.name == name }
		if let current = currentOperation, current.name == name {
			operations.append(current)
		}
		return operations.isEmpty ? nil : operations
	}

	func operationDidFinish(_ operation: MainThreadOperation) {
		guard let operationID = operation.id else {
			assertionFailure("Expected operation.id, got nil")
			return
		}
		if let currentOperationID = currentOperationID, currentOperationID == operationID {
			self.currentOperationID = nil
		}

		if operation.isCanceled {
			dependencies.operationIDWasCanceled(operationID)
		}
		else {
			dependencies.operationIDDidComplete(operationID)
		}

		callCompletionBlock(for: operation)
		removeFromStorage(operation)
		operation.operationDelegate = nil
		runNextOperationIfNeeded()
	}

	func runNextOperationIfNeeded() {
		DispatchQueue.main.async {
			guard !self.isSuspended && !self.isRunningAnOperation() else {
				return
			}
			guard let operation = self.popNextAvailableOperation() else {
				return
			}
			self.currentOperationID = operation.id!
			operation.run()
		}
	}

	func isRunningAnOperation() -> Bool {
		return currentOperationID != nil
	}

	func popNextAvailableOperation() -> MainThreadOperation? {
		for operationID in pendingOperationIDs {
			guard let operation = operations[operationID] else {
				assertionFailure("Expected pending operation to be found in operations dictionary.")
				continue
			}
			if operationIsAvailable(operation) {
				removeOperationIDsFromPendingOperationIDs([operationID])
				dependencies.operationIDWillRun(operationID)
				return operation
			}
		}
		return nil
	}

	func operationIsAvailable(_ operation: MainThreadOperation) -> Bool {
		return !operation.isCanceled && !dependencies.operationIDIsBlockedByDependency(operation.id!)
	}

	@MainActor func createOperationID() -> Int {
		precondition(Thread.isMainThread)
		Self.incrementingID += 1
		return Self.incrementingID
	}

	@MainActor func ensureOperationID(_ operation: MainThreadOperation) -> Int {
		if let operationID = operation.id {
			return operationID
		}

		let operationID = createOperationID()
		operation.id = operationID
		return operationID
	}

	func cancel(_ operationIDs: [Int]) {
		guard !operationIDs.isEmpty else {
			return
		}
		
		let operationIDsToCancel = operationIDsByAddingChildOperationIDs(operationIDs)
		setCanceledAndRemoveDelegate(for: operationIDsToCancel)
		callCompletionBlockForOperationIDs(operationIDsToCancel)
		clearCurrentOperationIDIfContained(by: operationIDsToCancel)
		removeOperationIDsFromPendingOperationIDs(operationIDsToCancel)
		removeOperationIDsFromStorage(operationIDsToCancel)
		dependencies.cancel(operationIDsToCancel)
	}

	func operationIDsByAddingChildOperationIDs(_ operationIDs: [Int]) -> [Int] {
		var operationIDsToCancel = operationIDs
		for operationID in operationIDs {
			if let childOperationIDs = dependencies.childOperationIDs(for: operationID) {
				operationIDsToCancel += childOperationIDs
			}
		}
		return operationIDsToCancel
	}

	func setCanceledAndRemoveDelegate(for operationIDs: [Int]) {
		for operationID in operationIDs {
			if let operation = operations[operationID] {
				operation.isCanceled = true
				operation.operationDelegate = nil
			}
		}
	}

	func clearCurrentOperationIDIfContained(by operationIDs: [Int]) {
		if let currentOperationID = currentOperationID, operationIDs.contains(currentOperationID) {
			self.currentOperationID = nil
		}
	}

	func removeOperationIDsFromPendingOperationIDs(_ operationIDs: [Int]) {
		var updatedPendingOperationIDs = pendingOperationIDs
		for operationID in operationIDs {
			if let ix = updatedPendingOperationIDs.firstIndex(of: operationID) {
				updatedPendingOperationIDs.remove(at: ix)
			}
		}

		pendingOperationIDs = updatedPendingOperationIDs
	}

	func removeFromStorage(_ operation: MainThreadOperation) {
		guard let operationID = operation.id else {
			assertionFailure("Expected operation.id, got nil.")
			return
		}
		removeOperationIDsFromStorage([operationID])
	}

	func removeOperationIDsFromStorage(_ operationIDs: [Int]) {
		DispatchQueue.main.async { [weak self] in
			for operationID in operationIDs {
				self?.operations[operationID] = nil
			}
		}
	}

	func callCompletionBlockForOperationIDs(_ operationIDs: [Int]) {
		let completedOperations = operationIDs.compactMap { operations[$0] }
		callCompletionBlockForOperations(completedOperations)
	}

	func callCompletionBlockForOperations(_ operations: [MainThreadOperation]) {
		for operation in operations {
			callCompletionBlock(for: operation)
		}
	}

	func callCompletionBlock(for operation: MainThreadOperation) {
		guard let completionBlock = operation.completionBlock else {
			return
		}
		completionBlock(operation)
		operation.completionBlock = nil
	}

	func allOperationIDsArePendingOrCurrent(_ operationIDs: [Int]) -> Bool {
		// Used by an assert.
		for operationID in operationIDs {
			if currentOperationID != operationID && !pendingOperationIDs.contains(operationID) {
				return false
			}
		}
		return true
	}

	func allOperationIDsAreInStorage(_ operationIDs: [Int]) -> Bool {
		// Used by an assert.
		for operationID in operationIDs {
			guard let _ = operations[operationID] else {
				return false
			}
		}
		return true
	}
}

private final class MainThreadOperationDependencies {

	private var dependencies = [Int: Dependency]() // Key is parentOperationID

	private final class Dependency {

		let operationID: Int
		var parentOperationDidComplete = false
		var isEmpty: Bool {
			return childOperationIDs.isEmpty
		}
		var childOperationIDs = [Int]()

		init(operationID: Int) {
			self.operationID = operationID
		}

		func remove(_ childOperationID: Int) {
			if let ix = childOperationIDs.firstIndex(of: childOperationID) {
				childOperationIDs.remove(at: ix)
			}
		}

		func add(_ childOperationID: Int) {
			guard !childOperationIDs.contains(childOperationID) else {
				return
			}
			childOperationIDs.append(childOperationID)
		}

		func operationIDIsBlocked(_ operationID: Int) -> Bool {
			if parentOperationDidComplete {
				return false
			}
			return childOperationIDs.contains(operationID)
		}
	}

	/// Add a dependency: make childOperationID dependent on parentOperationID.
	func make(_ childOperationID: Int, dependOn parentOperationID: Int) {
		let dependency = ensureDependency(parentOperationID)
		dependency.add(childOperationID)
	}

	/// Child operationIDs for a possible dependency.
	func childOperationIDs(for parentOperationID: Int) -> [Int]? {
		if let dependency = dependencies[parentOperationID] {
			return dependency.childOperationIDs
		}
		return nil
	}

	/// Update dependencies when an operation is completed.
	func operationIDDidComplete(_ operationID: Int) {
		if let dependency = dependencies[operationID] {
			dependency.parentOperationDidComplete = true
		}
		removeChildOperationID(operationID)
		removeEmptyDependencies()
	}

	/// Update dependencies when an operation finished but was canceled.
	func operationIDWasCanceled(_ operationID: Int) {
		removeAllReferencesToOperationIDs([operationID])
	}

	/// Update dependencies when canceling operations.
	func cancel(_ operationIDs: [Int]) {
		removeAllReferencesToOperationIDs(operationIDs)
	}

	/// Update dependencies when an operation is about to run.
	func operationIDWillRun(_ operationID: Int) {
		removeChildOperationIDs([operationID])
	}

	/// Find out if an operationID is blocked by a dependency.
	func operationIDIsBlockedByDependency(_ operationID: Int) -> Bool {
		for dependency in dependencies.values {
			if dependency.operationIDIsBlocked(operationID) {
				return true
			}
		}
		return false
	}

	private func ensureDependency(_ parentOperationID: Int) -> Dependency {
		if let dependency = dependencies[parentOperationID] {
			return dependency
		}
		let dependency = Dependency(operationID: parentOperationID)
		dependencies[parentOperationID] = dependency
		return dependency
	}
}

private extension MainThreadOperationDependencies {

	func removeAllReferencesToOperationIDs(_ operationIDs: [Int]) {
		removeDependencies(operationIDs)
		removeChildOperationIDs(operationIDs)
	}

	func removeDependencies(_ parentOperationIDs: [Int]) {
		parentOperationIDs.forEach { dependencies[$0] = nil }
	}

	func removeChildOperationIDs(_ operationIDs: [Int]) {
		operationIDs.forEach{ removeChildOperationID($0) }
		removeEmptyDependencies()
	}

	func removeChildOperationID(_ operationID: Int) {
		dependencies.values.forEach{ $0.remove(operationID) }
	}

	func removeEmptyDependencies() {
		let parentOperationIDs = dependencies.keys
		for parentOperationID in parentOperationIDs {
			let dependency = dependencies[parentOperationID]!
			if dependency.isEmpty {
				dependencies[parentOperationID] = nil
			}
		}
	}
}
