import { LAUNCHER_SERVICE } from "../../common/constants";
import { RouterClient } from "../../clients/routerClient";
import { Logger } from "../../clients/logger";
import { objectFlatMap } from "../util";
import { getModel } from "redux-loop";
/**
 * Creates the client side of a remote redux.
 * Exposes a store enhancer that will synchronize a copy of a slice of state.
 * Initially used in the Interop Service and Linker UI.
 *
 * @param param.server - The string "address" of the remote redux server (see RemoteReduxServerAddress type for valid addresses)
 * @param param.slice - Th slice of state to observe and synchronize (i.e. rootState[slice])
 * @param param.dispatchOnly - If set to true then the remote store will not subscribe to updates. This reduces network traffic for situations that are write only.
 */
export const remoteReduxClientEnhancer = ({ server, slice, dispatchOnly, }) => {
    // Note: Detect disconnections from error-to-deliver messages on router; this is not 100% reliable if a window crashes or is killed by the OS
    const remoteReduxEnhancer = (createStore) => (reducer, preloadedState) => {
        const wrappedReducer = (state, action) => {
            if (action.type === "@@finsemble/updateState") {
                state = action.newState;
            }
            const newState = reducer(state, action);
            return {
                ...newState,
            };
        };
        const store = createStore(wrappedReducer, preloadedState);
        const enhancedStore = {
            ...store,
            dispatch: (action) => {
                Logger.system.debug(`Interop.remote.${action.type}`, store, action);
                if (action.type == "@@finsemble/updateState") {
                    store.dispatch(action);
                }
                else if (action.type) {
                    // Send action to remote store
                    RouterClient.query(server, action);
                }
                else if (action.then) {
                    action.then(store.dispatch);
                }
                return action;
            },
        };
        const routerChannel = `reduxSlice:${slice}:${Date.now()}_${Math.random()}`;
        RouterClient.addListener(routerChannel, (error, message) => {
            if (!error && message) {
                const newState = message === null || message === void 0 ? void 0 : message.data;
                const payload = { type: "@@finsemble/updateState", newState };
                enhancedStore.dispatch(payload);
            }
        });
        if (!dispatchOnly) {
            RouterClient.query(server, {
                "@@finsemble/action": "subscribe",
                "@@finsemble/slice": slice,
                "@@finsemble/routerChannel": routerChannel,
            }, () => { });
        }
        return enhancedStore;
    };
    return remoteReduxEnhancer;
};
/**
 * Handles action requests from the remote client.
 *
 * "subscribe" - Subscribes the remote client to the requested slice. Changes to that slice are
 * transmitted on the requested router channel, including an immediate initial broadcast of the
 * current slice's state. The windowName is registered so that we stop transmitting after receiving
 * a window close event for that window.
 */
const handleRemoteAction = ({ data, header, sliceSubscriptions, store, prevState, }) => {
    var _a, _b, _c;
    const { "@@finsemble/action": action, "@@finsemble/slice": subscribeToSlice, "@@finsemble/slices": subscribeToSlices, "@@finsemble/routerChannel": routerChannel, } = data;
    // Note, windowName is only use for cleanup purposes, to remove subscriptions when a window is closed. We don't need
    // to use webContentsName because multiple subscriptions from the same window are allowed
    const { windowName } = header.originMetadata;
    Logger.system.debug(`remoteReduxServerEnhancer received ${action} for ${subscribeToSlice} from ${windowName}`);
    if (action === "subscribe") {
        /** Creates a subscription to store updates */
        sliceSubscriptions[subscribeToSlice] = ((_a = sliceSubscriptions[subscribeToSlice]) !== null && _a !== void 0 ? _a : []).concat({
            windowName,
            routerChannel,
        });
        const state = (_b = store.getState()) === null || _b === void 0 ? void 0 : _b[subscribeToSlice];
        if (!prevState[subscribeToSlice])
            prevState[subscribeToSlice] = state[subscribeToSlice];
        RouterClient.transmit(routerChannel, state);
    }
    else if (action === "snapshot") {
        const slices = subscribeToSlices !== null && subscribeToSlices !== void 0 ? subscribeToSlices : (subscribeToSlice ? [subscribeToSlice] : []);
        /** Returns a snapshot of the store's state, perhaps only for the requested slices */
        if (slices.length) {
            const state = (_c = store.getState()) !== null && _c !== void 0 ? _c : {};
            return objectFlatMap(state, (key, value) => (slices.includes(key) ? [[key, value]] : []));
        }
        else {
            return store.getState();
        }
    }
    else {
        Logger.system.warn(`remoteReduxServerEnhancer: Unexpected "@@finsemble/action"=${action} received from ${header.origin}`);
    }
};
/**
 * Handles dispatch requests from the remote client. Any valid dispatch (contains a "type" field) is
 * accepted. All dispatches are modified to include the "@@remote" entry which can be used
 * to debug within redux devtools.
 */
const handleDispatch = (data, header, dispatch) => {
    var _a;
    if (!(data === null || data === void 0 ? void 0 : data.type)) {
        Logger.system.warn(`remoteReduxServerEnhancer: Received invalid dispatch action. No "type" specified.`, data);
        return;
    }
    dispatch({
        ...data,
        "@@remote": {
            windowName: (_a = header === null || header === void 0 ? void 0 : header.originMetadata) === null || _a === void 0 ? void 0 : _a.windowName,
            header,
        },
    });
};
/**
 * Starts the router listener and responder for a remoteReduxServerEnhancer. This code manages
 * inbound subscriptions to store slices from clients as well as dispatches from those clients.
 */
const startInboundServerHandlers = ({ server, sliceSubscriptions, store, dispatch, prevState, }) => {
    /**
     * Listen for window closings. We'll stop transmitting store changes to closed windows.
     */
    RouterClient.addListener(LAUNCHER_SERVICE.WINDOW_CLOSED, (err, response) => {
        if (err) {
            Logger.system.error(`remoteReduxServerEnhancer: Error in listenForWindowClosings() ${err}`);
        }
        else {
            const { name: windowName } = response === null || response === void 0 ? void 0 : response.data;
            // Delete all sliceClientMap entries for the closed window
            Object.keys(sliceSubscriptions).forEach((slice) => {
                sliceSubscriptions[slice] = sliceSubscriptions[slice].filter((value) => value.windowName !== windowName);
                if (!sliceSubscriptions[slice].length)
                    delete sliceSubscriptions[slice];
            });
        }
    });
    /**
     * Listen for incoming messages on our published `server` channel. Message types of
     * `@@finsemble/action` will subscribe the listener to their requested slice.
     * All other messages with a "type" will be dispatched to the local store.
     */
    RouterClient.addResponder(server, async (error, queryMessage) => {
        if (error) {
            Logger.system.error("remoteReduxServerEnhancer: Error adding responder.", error);
            // Fatal error - the global channel is not available
        }
        else {
            if (queryMessage) {
                const { data, header } = queryMessage;
                let response = null;
                if (data["@@finsemble/action"])
                    response = handleRemoteAction({ data, header, sliceSubscriptions, store, prevState });
                else
                    handleDispatch(data, header, dispatch);
                queryMessage.sendQueryResponse(null, response);
            }
            else {
                Logger.system.error("remoteReduxServerEnhancer: No queryMessage received from remote client");
            }
        }
    });
};
/**
 * Creates the server side of a remote redux. This enhancer should be added before DevTools.instrument().
 *
 * @param param.server string The router channel that this server should listen on
 */
export const remoteReduxServerEnhancer = ({ server }) => {
    const remoteReduxEnhancer = (createStore) => (reducer) => {
        Logger.system.debug(`remoteReduxServerEnhancer: Starting on routerChannel ${server}`);
        // These object _references_ must persist for the duration of the enhancer. handleRemoteAction
        // maintains closure references to these object for analyzing state changes. Don't ever
        // modify this code to replace them (NO! `sliceSubscriptions = { ...sliceSubscriptions }`)
        let sliceSubscriptions = {};
        let prevState = {};
        /**
         * The wrappedReducer injects logic to catch state changes and forward them to remote clients.
         * For each run of the reducer, the sliceSubscriptions is iterated. Each slice is examined to
         * determine if it has changed using deep equal. If the slice was changed, then the updated
         * slice is transmitted to each of the subscribers.
         */
        const wrappedReducer = (state, action) => {
            const newState = reducer(state, action);
            Object.entries(sliceSubscriptions).forEach(([slice, clients]) => {
                /**
                 * Use reference equality to determine if the slice has changed. Actions on other slices
                 * will not change any other slice's reference objects (due to redux's immutability) so
                 * this code should result in transmits only when data downstream of the slice has changed.
                 *
                 * This is redux-loop compatible. `getModel()` will automatically convert loops into pure state
                 * for transmission to the client.
                 */
                const actualState = getModel(newState);
                if (actualState[slice] !== prevState[slice]) {
                    prevState[slice] = actualState[slice];
                    clients.forEach((client) => {
                        RouterClient.transmit(client.routerChannel, actualState[slice]);
                    });
                }
            });
            return newState;
        };
        const store = createStore(wrappedReducer);
        const enhancedStore = {
            ...store,
            // Pass through dispatch
            dispatch: (action) => store.dispatch(action),
        };
        startInboundServerHandlers({
            server,
            sliceSubscriptions,
            store,
            dispatch: enhancedStore.dispatch,
            prevState,
        });
        return enhancedStore;
    };
    return remoteReduxEnhancer;
};
//# sourceMappingURL=remoteReduxEnhancer.js.map