/*
In 99,99% of time YOU DON'T NEEDED TO CHANGE ANYTHING in this file!
This file declare state object for client and reducers/actions/middleware types
*/
import Foundation
import Combine

typealias StoreCallback = (ClientAction) -> Void
typealias Stateable = Codable & Equatable
typealias ClientStore = Store<ClientState, ClientAction>
typealias Reducer<State: Stateable, Action: Codable> = (inout State, Action) -> Void
typealias Middleware<State: Stateable, Action: Codable> = (State, Action) -> AnyPublisher<Action, Never>

final class Store<State: Stateable, Action: Codable>: ObservableObject {
    // State is read-only (changes only over reducers)
    private(set) var state: State

    // Serial queue for performing any actions sequentially
    private let serialQueue = DispatchQueue(label: "im.narayana.snikket.xmppclient.serial.\(UUID().uuidString)", qos: .userInteractive)

    private let reducer: Reducer<State, Action>
    private(set) var middlewares: [Middleware<State, Action>]
    private var middlewareCancellables: Set<AnyCancellable> = []

    // Init
    init(initialState: State, reducer: @escaping Reducer<State, Action>, middlewares: [Middleware<State, Action>] = []) {
        state = initialState
        self.reducer = reducer
        self.middlewares = middlewares
    }

    // Run reducers/middlewares
    func dispatch(_ action: Action) {
        serialQueue.sync { [weak self] in
            guard let wSelf = self else { return }
            let newState = wSelf.dispatch(wSelf.state, action)
            wSelf.state = newState
        }
    }

    private func dispatch(_ currentState: State, _ action: Action) -> State {
        let startTime = CFAbsoluteTimeGetCurrent()

        // Do reducing
        var newState = currentState
        reducer(&newState, action)

        // Dispatch all middleware functions
        for middleware in middlewares {
            middleware(newState, action)
                .receive(on: DispatchQueue.main)
                .sink(receiveValue: dispatch)
                .store(in: &middlewareCancellables)
        }

        // Check performance
        let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
        if timeElapsed > 0.05 {
            #if DEBUG
                print(
                    """
                    --
                    (Ignore this warning ONLY in case, when execution is paused by your breakpoint)
                    🕐Execution time: \(timeElapsed)
                    ❌WARNING! Some reducers/middlewares work too long! It will lead to issues in production build!
                    Because of execution each action is synchronous the any stuck will reduce performance dramatically.
                    Probably you need check which part of reducer/middleware should be async (wrapped with Futures, as example)
                    --
                    """
                )
            #else
            #endif
        }

        return newState
    }
}
