// This Source Code Form is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/
import * as _ from "lodash-es";
import { MutexLocked } from "@foxglove/den/async";
import { NoopStateProcessor } from "./NoopStateProcessor";
import { StateProcessorFactory, } from "./StateProcessorFactory";
/**
 * This is a player that wraps an underlying player and applies aliases to all topic names
 * in data emitted from the player.
 *
 * Aliases that alias input topics to other input topics or that request conflicting
 * aliases from multiple input topics to the same output topic are disallowed and flagged
 * as player problems
 */
export class TopicAliasingPlayer {
    #player;
    #inputs;
    #aliasedSubscriptions;
    #subscriptions = [];
    // True if no aliases are active and we can pass calls directly through to the
    // underlying player.
    #skipAliasing;
    #stateProcessorFactory = new StateProcessorFactory();
    #stateProcessor = new NoopStateProcessor();
    #lastPlayerState;
    // We only want to be emitting one state at a time however we also queue emits from global
    // variable updates which can happen at a different time to new state from the wrapped player. The
    // mutex prevents invoking the listener concurrently.
    #listener;
    constructor(player) {
        this.#player = player;
        this.#skipAliasing = true;
        this.#inputs = {
            aliasFunctions: [],
            topics: undefined,
            variables: {},
        };
    }
    setListener(listener) {
        this.#listener = new MutexLocked(listener);
        this.#player.setListener(async (state) => {
            await this.#onPlayerState(state);
        });
    }
    setAliasFunctions(aliasFunctions) {
        this.#inputs = { ...this.#inputs, aliasFunctions };
        this.#skipAliasing = aliasFunctions.length === 0;
    }
    close() {
        this.#player.close();
    }
    setSubscriptions(subscriptions) {
        this.#subscriptions = subscriptions;
        this.#aliasedSubscriptions = this.#stateProcessor.aliasSubscriptions(subscriptions);
        this.#player.setSubscriptions(this.#aliasedSubscriptions);
    }
    setPublishers(publishers) {
        this.#player.setPublishers(publishers);
    }
    setParameter(key, value) {
        this.#player.setParameter(key, value);
    }
    publish(request) {
        this.#player.publish(request);
    }
    async callService(service, request) {
        return await this.#player.callService(service, request);
    }
    startPlayback() {
        this.#player.startPlayback?.();
    }
    pausePlayback() {
        this.#player.pausePlayback?.();
    }
    seekPlayback(time) {
        this.#player.seekPlayback?.(time);
    }
    // eslint-disable-next-line @foxglove/no-boolean-parameters
    enableRepeatPlayback(enable) {
        this.#player.enableRepeatPlayback?.(enable);
    }
    playUntil(time) {
        if (this.#player.playUntil) {
            this.#player.playUntil(time);
            return;
        }
        this.#player.seekPlayback?.(time);
    }
    setPlaybackSpeed(speedFraction) {
        this.#player.setPlaybackSpeed?.(speedFraction);
    }
    setGlobalVariables(globalVariables) {
        this.#player.setGlobalVariables(globalVariables);
        // Set this before the lastPlayerstate skip below so we have global variables when
        // a player state is provided later.
        this.#inputs = { ...this.#inputs, variables: globalVariables };
        // We can skip re-processing if we don't have any alias functions setup or we have not
        // had any player state provided yet. The player state handler onPlayerState will handle alias
        // function processing when it is called.
        if (this.#skipAliasing ||
            this.#lastPlayerState == undefined ||
            this.#inputs.topics == undefined) {
            return;
        }
        const stateProcessor = this.#stateProcessorFactory.buildStateProcessor(this.#inputs);
        // If we have a new state processor, it means something about the aliases has changed and we
        // need to re-process the existing player state
        const shouldReprocess = stateProcessor !== this.#stateProcessor;
        this.#stateProcessor = stateProcessor;
        // If we have a new processor we might also have new subscriptions for downstream
        if (shouldReprocess) {
            this.#resetSubscriptions();
        }
        // Re-process the last player state if the processor has changed since we might have new downstream topics
        // for panels to subscribe or get new re-mapped messages.
        //
        // Skip this if we are playing and allow the next player state update to handle this to avoid
        // these emits interfering with player state updates. It does assume the player is emitting
        // state relatively quickly when playing so the new aliases are injected. If this assumption
        // changes this bail might need revisiting.
        if (shouldReprocess && this.#lastPlayerState.activeData?.isPlaying === false) {
            void this.#onPlayerState(this.#lastPlayerState);
        }
    }
    async fetchAsset(uri) {
        if (this.#player.fetchAsset) {
            return await this.#player.fetchAsset(uri);
        }
        throw Error("Player does not support fetching assets");
    }
    async #onPlayerState(playerState) {
        // If we are already emitting a player state, avoid emitting another one
        // This is a guard against global variable emits
        if (this.#listener?.isLocked() === true) {
            return;
        }
        return await this.#listener?.runExclusive(async (listener) => {
            if (this.#skipAliasing) {
                await listener(playerState);
                return;
            }
            // The player topics have changed so we need to re-build the aliases because player topics
            // are an input to the alias functions.
            if (playerState.activeData?.topics !== this.#inputs.topics) {
                this.#inputs = { ...this.#inputs, topics: playerState.activeData?.topics };
                const stateProcessor = this.#stateProcessorFactory.buildStateProcessor(this.#inputs);
                // if the state processor is changed, then we might need to re-process subscriptions since
                // we might now be able to produce new subscriptions
                if (this.#stateProcessor !== stateProcessor) {
                    this.#stateProcessor = stateProcessor;
                    this.#resetSubscriptions();
                }
            }
            // remember the last player state so we can re-use it when global variables are set
            this.#lastPlayerState = playerState;
            // Process the player state using the latest aliases
            const newState = this.#stateProcessor.process(playerState, this.#subscriptions);
            await listener(newState);
        });
    }
    /**
     * Re-calculate the subscriptions using the latest state processor. If the subscriptions have
     * changed then call setSubscriptions on the wrapped player.
     */
    #resetSubscriptions() {
        const aliasedSubscriptions = this.#stateProcessor.aliasSubscriptions(this.#subscriptions);
        if (!_.isEqual(this.#aliasedSubscriptions, aliasedSubscriptions)) {
            this.#aliasedSubscriptions = aliasedSubscriptions;
            this.#player.setSubscriptions(aliasedSubscriptions);
        }
    }
}
//# sourceMappingURL=TopicAliasingPlayer.js.map