//
//  Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//

import Foundation

/// Benchmark async code by calling the passed in block parameter when the work
/// is done.
///
///     BenchAsync(title: "my benchmark") { completeBenchmark in
///         foo {
///             // consider benchmarking of "foo" complete
///             completeBenchmark()
///
///             // call any completion handler foo might have
///             fooCompletion()
///         }
///     }
public func BenchAsync(title: String, block: (@escaping () -> Void) -> Void) {
    let startTime = CACurrentMediaTime()

    block {
        let timeElapsed = CACurrentMediaTime() - startTime
        let formattedTime = String(format: "%0.2fms", timeElapsed * 1000)
        Logger.debug("[Bench] title: \(title), duration: \(formattedTime)")
    }
}

public func Bench(title: String, block: () -> Void) {
    BenchAsync(title: title) { finish in
        block()
        finish()
    }
}

public func Bench(title: String, block: () throws -> Void) throws {
    var thrownError: Error?
    BenchAsync(title: title) { finish in
        do {
            try block()
        } catch {
            thrownError = error
        }
        finish()
    }
    if let errorToRethrow = thrownError {
        throw errorToRethrow
    }
}

/// When it's not convenient to retain the event completion handler, e.g. when the measured event
/// crosses multiple classes, you can use the BenchEvent tools
///
///     // in one class
///     BenchEventStart(title: "message sending", eventId: message.id)
///     beginTheWork()
///
///     ...
///
///    // in another class
///    doTheLastThing()
///    BenchEventComplete(eventId: message.id)
///
/// Or in objc
///
///    [BenchManager startEventWithTitle:"message sending" eventId:message.id]
///    ...
///    [BenchManager completeEventWithEventId:eventId:message.id]
public func BenchEventStart(title: String, eventId: BenchmarkEventId) {
    BenchAsync(title: title) { finish in
        runningEvents[eventId] = Event(title: title, eventId: eventId, completion: finish)
    }
}

public func BenchEventComplete(eventId: BenchmarkEventId) {
    guard let event = runningEvents.removeValue(forKey: eventId) else {
        Logger.debug("no active event with id: \(eventId)")
        return
    }

    event.completion()
}

public typealias BenchmarkEventId = String

private struct Event {
    let title: String
    let eventId: BenchmarkEventId
    let completion: () -> Void
}

private var runningEvents: [BenchmarkEventId: Event] = [:]

@objc
public class BenchManager: NSObject {

    @objc
    public class func startEvent(title: String, eventId: BenchmarkEventId) {
        BenchEventStart(title: title, eventId: eventId)
    }

    @objc
    public class func completeEvent(eventId: BenchmarkEventId) {
        BenchEventComplete(eventId: eventId)
    }

    @objc
    public class func benchAsync(title: String, block: (@escaping () -> Void) -> Void) {
        BenchAsync(title: title, block: block)
    }

    @objc
    public class func bench(title: String, block: () -> Void) {
        Bench(title: title, block: block)
    }
}