import { byDateTime, isValidGameId } from '@dndbeyond/message-broker-lib';
import createCache from '@emotion/cache';
import { CacheProvider } from '@emotion/react';
import { DiceActionTypes, DiceVisibilities, } from '@dndbeyond/pocket-dimension-dice/constants';
import { useUserDiceDataStore } from '@dndbeyond/pocket-dimension-dice/hooks/useUserDiceDataStore';
import { ThemeProvider } from '@mui/material/styles';
import React, { createContext, useCallback, useContext, useEffect, useReducer, useRef, } from 'react';
import { getTssDefaultEmotionCache, TssCacheProvider } from 'tss-react';
import { useActiveCampaign } from '../../hooks/useActiveCampaign';
import { useActiveCharacters } from '../../hooks/useActiveCharacters';
import { useAwaitMessageBroker } from '../../hooks/useAwaitMessageBroker';
import { useDiceFamilies } from '../../hooks/useDiceFamilies';
import { useFetchHistory } from '../../hooks/useFetchHistory';
import { useMessageBroker } from '../../hooks/useMessageBroker';
import { darkTheme, lightTheme } from '../../shared/themes';
import { useActivePlayers } from '../../hooks/useActivePlayers';
/**
 * ids used with `MessageTargetOptions` to maintain a proper display order
 * other options, such as other non-dm users, would start with 4
 */
export var MessageTargetOptionOrder;
(function (MessageTargetOptionOrder) {
    MessageTargetOptionOrder[MessageTargetOptionOrder["Game"] = 0] = "Game";
    MessageTargetOptionOrder[MessageTargetOptionOrder["Self"] = 1] = "Self";
    MessageTargetOptionOrder[MessageTargetOptionOrder["Dm"] = 2] = "Dm";
})(MessageTargetOptionOrder || (MessageTargetOptionOrder = {}));
var DiceRollActionType;
(function (DiceRollActionType) {
    DiceRollActionType["Pending"] = "dice/roll/pending";
    DiceRollActionType["Rejected"] = "dice/roll/rejected";
    DiceRollActionType["Fulfilled"] = "dice/roll/fulfilled";
    DiceRollActionType["Deferred"] = "dice/roll/deferred";
})(DiceRollActionType || (DiceRollActionType = {}));
const replaceMessageBrokerGameIdActionType = 'replace/messageBroker/gameId';
const fetchActiveCharactersActionType = 'fetch/activeCharacters';
const fetchActivePlayersActionType = 'fetch/activePlayers';
const fetchActiveCampaignActionType = 'fetch/activeCampaign';
const setFetchHistoryActionType = 'set/fetchHistory';
const fetchDiceFamilyInfoActionType = 'fetch/diceFamilyInfo';
const addMessageTargetOptionActionType = 'add/messageTargetOption';
const setUserIdActionType = 'set/userId';
export const toggleMessageExpandedActionType = 'update/toggleMessageExpanded';
export const setDefaultMessageTargetOptionActionType = 'set/defaultMessageTargetOption';
const diceRollActionTypes = [
    DiceRollActionType.Pending,
    DiceRollActionType.Rejected,
    DiceRollActionType.Fulfilled,
    DiceRollActionType.Deferred,
];
// had a really hard time not naming this `isNotJustAnyMessage`, idk if all of
// this is necessary but I wanted to start out "thorough"
export const isMessage = (message) => 'entityId' in message &&
    'entityType' in message &&
    'persist' in message &&
    'messageScope' in message &&
    'messageTarget' in message &&
    'data' in message;
const isDiceRollActionType = (t) => diceRollActionTypes.includes(t);
const isReplaceMessageBrokerGameIdActionType = (t) => t === replaceMessageBrokerGameIdActionType;
const toRollId = ({ data: { rollId } }) => rollId;
const addMessageTargetOption = (option, state) => {
    state.messageTargetOptions.entities[option.id] = option;
    // keep the items in their proper display order
    const sortedIds = Object.keys(state.messageTargetOptions.entities).sort();
    return Object.assign(Object.assign({}, state), { messageTargetOptions: Object.assign(Object.assign({}, state.messageTargetOptions), { ids: sortedIds }) });
};
/**
 * n.b. `messages` state is keyed off of `message.data.rollId` (not
 * `message.id`) so that a fulfilled roll resolves a pending roll in line
 *
 * each message gets a unique uuid, and we simply overwrite the pending message
 * with the fulfilled one
 *
 * the fulfilled dice roll almost certainly has an update `dateTime` so we have
 * to resort on both pending and fulfilled messages
 */
const mergeDiceRollMessage = (message, state) => {
    var _a, _b;
    // n.b. mutates the `entities` object, but doesn't update `ids`
    state.messages.entities[message.data.rollId] = Object.assign(Object.assign({}, message), { 
        /**
         * prefer an existing (`/pending`) message's `dateTime` to avoid the case:
         *
         * 1. `/pending` message a, sort order is: [a]
         * 2. `/pending` message b, sort order is: [a, b]
         * 3. `/fulfilled` message a, sort order is: [b, a] // changes!
         * 4. `/fulfilled` message b, sort order is: [a, b] // changes again!
         *
         * messages from the server are already `/fulfilled` and should never update
         */
        dateTime: (_b = (_a = state.messages.entities[message.data.rollId]) === null || _a === void 0 ? void 0 : _a.dateTime) !== null && _b !== void 0 ? _b : message.dateTime });
    // ... because we want a new array of ids in the right sort order
    const sortedIds = Object.values(state.messages.entities)
        .sort(byDateTime)
        .map(toRollId);
    // n.b. there used to be an `areArraysEqual` check here that turned out to be
    // a little too good at it's job of preventing re-renders
    //
    // TODO: look again at performance here, maybe Redux makes sense at this point
    return Object.assign(Object.assign({}, state), { messages: Object.assign(Object.assign({}, state.messages), { ids: sortedIds }) });
};
const removeDiceRollMessage = (rollId, state) => {
    const messages = Object.assign({}, state.messages);
    let didMutate = false;
    if (rollId in messages.entities) {
        delete messages.entities[rollId];
        didMutate = true;
    }
    if (didMutate) {
        messages.ids = messages.ids.filter((id) => id in messages.entities);
        return Object.assign(Object.assign({}, state), { messages });
    }
    return state;
};
const toggleMessageExpanded = (expandedMessage, state) => {
    return Object.assign(Object.assign({}, state), { expandedMessages: state.expandedMessages.some((item) => item.messageId === expandedMessage.messageId)
            ? state.expandedMessages.filter((item) => item.messageId !== expandedMessage.messageId)
            : [...state.expandedMessages, expandedMessage] });
};
const expandNewestMessage = (messageId, state) => {
    if (state.messages.ids[0] === messageId) {
        // this will collapse old non-user-expanded messages
        const userExpandedMessages = state.expandedMessages.filter((item) => item.wasExpandedByUser);
        return Object.assign(Object.assign({}, state), { expandedMessages: [
                ...userExpandedMessages,
                { messageId, wasExpandedByUser: false },
            ] });
    }
    else {
        return state;
    }
};
const gameLogReducer = (state, action) => {
    switch (action.type) {
        case DiceRollActionType.Pending:
            return expandNewestMessage(action.payload.data.rollId, mergeDiceRollMessage(action.payload, state));
        case DiceRollActionType.Rejected:
            return removeDiceRollMessage(action.payload.data.rollId, state);
        case DiceRollActionType.Fulfilled:
            return expandNewestMessage(action.payload.data.rollId, mergeDiceRollMessage(action.payload, state));
        case DiceRollActionType.Deferred:
            // don't do anything, the worker will post back a fulfilled message
            return state;
        case fetchActiveCharactersActionType:
            return Object.assign(Object.assign({}, state), { activeCharacters: {
                    ids: action.payload.map(({ id }) => id.toString()),
                    entities: action.payload.reduce((acc, activeCharacter) => {
                        acc[activeCharacter.id.toString()] = activeCharacter;
                        return acc;
                    }, {}),
                } });
        case fetchActiveCampaignActionType:
            return Object.assign(Object.assign({}, state), { activeCampaign: action.payload });
        case fetchActivePlayersActionType:
            return Object.assign(Object.assign({}, state), { activePlayers: action.payload.reduce((acc, activePlayer) => {
                    const id = activePlayer.id.toString();
                    acc.ids.push(id);
                    acc.entities[id] = activePlayer;
                    return acc;
                }, {
                    ids: [],
                    entities: {},
                }) });
        case setFetchHistoryActionType:
            return Object.assign(Object.assign({}, state), { fetchHistory: action.payload });
        case fetchDiceFamilyInfoActionType:
            return Object.assign(Object.assign({}, state), { diceFamilies: {
                    ids: action.payload.map(({ id }) => id.toString()),
                    entities: action.payload.reduce((acc, diceFamilyInfo) => {
                        acc[diceFamilyInfo.id.toString()] = diceFamilyInfo;
                        return acc;
                    }, {}),
                } });
        case toggleMessageExpandedActionType:
            return toggleMessageExpanded(action.payload, state);
        case replaceMessageBrokerGameIdActionType:
            return Object.assign(Object.assign({}, state), { messages: {
                    ids: [],
                    entities: {},
                }, expandedMessages: [] });
        case setDefaultMessageTargetOptionActionType:
            return Object.assign(Object.assign({}, state), { defaultMessageTargetOption: state.messageTargetOptions.entities[action.payload] });
        case addMessageTargetOptionActionType:
            return addMessageTargetOption(action.payload, state);
        case setUserIdActionType:
            return Object.assign(Object.assign({}, state), { userId: action.payload });
        default:
            return state;
    }
};
const gameLogInitialState = {
    activeCharacters: {
        ids: [],
        entities: {},
    },
    activePlayers: {
        ids: [],
        entities: {},
    },
    diceFamilies: {
        ids: [],
        entities: {},
    },
    messages: {
        ids: [],
        entities: {},
    },
    expandedMessages: [],
    messageTargetOptions: {
        ids: [],
        entities: {},
    },
};
export const GameLogContext = createContext([gameLogInitialState, () => { }]);
const { Provider } = GameLogContext;
const muiCache = createCache({
    key: 'mui',
    prepend: true,
});
const DdbThemeProvider = ({ children, theme }) => (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore old type error
React.createElement(CacheProvider, { value: muiCache },
    React.createElement(TssCacheProvider, { value: getTssDefaultEmotionCache() },
        React.createElement(ThemeProvider, { theme: theme }, children))));
const initialDate = new Date();
export const GameLogContextProvider = ({ authUrl, baseUrl, ddbApiUrl, diceServiceUrl, diceThumbnailsUrl, entityId, children, targetingDisabled, darkThemeEnabled, diceWorkerContext, }) => {
    const [state, dispatch] = useReducer(gameLogReducer, Object.assign(Object.assign({}, gameLogInitialState), { entityId,
        baseUrl,
        diceThumbnailsUrl,
        targetingDisabled }));
    const chainRef = useRef(Promise.resolve());
    const mb = useAwaitMessageBroker();
    const diceWorkerCtx = useContext(diceWorkerContext ||
        createContext({}));
    useEffect(() => {
        if (!mb) {
            return;
        }
        dispatch({
            type: setUserIdActionType,
            payload: mb.userId,
        });
    }, [mb, mb === null || mb === void 0 ? void 0 : mb.userId]);
    useEffect(() => {
        if (!(mb && isValidGameId(mb.gameId))) {
            return;
        }
        if (mb.gameId) {
            dispatch({
                type: addMessageTargetOptionActionType,
                payload: {
                    id: MessageTargetOptionOrder.Game,
                    displayValue: 'Everyone',
                    messageScope: 'gameId',
                    messageTarget: mb.gameId,
                },
            });
        }
        dispatch({
            type: addMessageTargetOptionActionType,
            payload: {
                id: MessageTargetOptionOrder.Self,
                displayValue: 'Self',
                messageScope: 'userId',
                messageTarget: mb.userId,
            },
        });
        // initialize with game as the default
        if (!state.defaultMessageTargetOption) {
            dispatch({
                type: setDefaultMessageTargetOptionActionType,
                payload: mb.gameId
                    ? MessageTargetOptionOrder.Game
                    : MessageTargetOptionOrder.Self,
            });
        }
    }, [mb, mb === null || mb === void 0 ? void 0 : mb.userId, mb === null || mb === void 0 ? void 0 : mb.gameId, state.defaultMessageTargetOption]);
    useEffect(() => {
        var _a, _b;
        // we only want to display the dm target option if the user is not the dm
        const dmId = (_b = (_a = state === null || state === void 0 ? void 0 : state.activeCampaign) === null || _a === void 0 ? void 0 : _a.dmId) === null || _b === void 0 ? void 0 : _b.toString();
        if (!(mb && dmId && mb.userId !== dmId)) {
            return;
        }
        dispatch({
            type: addMessageTargetOptionActionType,
            payload: {
                id: MessageTargetOptionOrder.Dm,
                displayValue: 'Dungeon Master',
                messageScope: 'userId',
                messageTarget: dmId,
            },
        });
    }, [mb, mb === null || mb === void 0 ? void 0 : mb.userId, state.activeCampaign]);
    useMessageBroker(useCallback((message) => {
        if (!(isMessage(message) && isDiceRollActionType(message.eventType))) {
            if (isReplaceMessageBrokerGameIdActionType(message.eventType)) {
                dispatch({
                    type: message.eventType,
                    payload: null,
                });
            }
            return;
        }
        chainRef.current = chainRef.current.then(() => new Promise((resolve) => {
            setImmediate(() => {
                dispatch({
                    type: message.eventType,
                    payload: message,
                });
                resolve();
            });
        }));
    }, []));
    useActiveCharacters(authUrl, ddbApiUrl, useCallback((activeCharacters) => dispatch({
        type: fetchActiveCharactersActionType,
        payload: activeCharacters,
    }), []));
    useActiveCampaign(authUrl, ddbApiUrl, useCallback((activeCampaign) => {
        var _a, _b;
        return dispatch({
            type: fetchActiveCampaignActionType,
            payload: {
                id: activeCampaign.id,
                name: activeCampaign.name,
                dateCreated: activeCampaign.dateCreated,
                dmUsername: activeCampaign.dmDisplayName,
                dmId: activeCampaign.dmId,
                playerCount: (_b = (_a = activeCampaign.activePlayers) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0,
            },
        });
    }, []));
    useActivePlayers(authUrl, ddbApiUrl, useCallback(({ activePlayers }) => dispatch({
        type: fetchActivePlayersActionType,
        payload: activePlayers,
    }), []));
    useMessageBroker(useCallback((message) => {
        var _a, _b;
        const diceVisibility = (_a = useUserDiceDataStore
            .getState()
            .getUserDiceData(Number(mb === null || mb === void 0 ? void 0 : mb.userId))) === null || _a === void 0 ? void 0 : _a.settings.visibility;
        if ((diceWorkerCtx === null || diceWorkerCtx === void 0 ? void 0 : diceWorkerCtx.isPddReady) &&
            !document.hidden &&
            isMessage(message) &&
            new Date(Number(message.dateTime)) > initialDate &&
            message.eventType === DiceRollActionType.Deferred &&
            (diceVisibility === DiceVisibilities.Shared ||
                (diceVisibility === DiceVisibilities.Self &&
                    message.userId === String(mb === null || mb === void 0 ? void 0 : mb.userId)))) {
            (_b = diceWorkerCtx === null || diceWorkerCtx === void 0 ? void 0 : diceWorkerCtx.physicsWorker) === null || _b === void 0 ? void 0 : _b.postMessage({
                type: DiceRollActionType.Deferred,
                payload: message,
            });
        }
    }, [diceWorkerCtx === null || diceWorkerCtx === void 0 ? void 0 : diceWorkerCtx.physicsWorker, diceWorkerCtx === null || diceWorkerCtx === void 0 ? void 0 : diceWorkerCtx.isPddReady, mb === null || mb === void 0 ? void 0 : mb.userId]));
    useMessageBroker(useCallback((message) => {
        var _a;
        if (isMessage(message) &&
            new Date(Number(message.dateTime)) > initialDate &&
            message.eventType === DiceActionTypes.SetUpdate) {
            if (message.userId === (mb === null || mb === void 0 ? void 0 : mb.userId)) {
                const currentUserDiceData = useUserDiceDataStore
                    .getState()
                    .getUserDiceData(Number(mb === null || mb === void 0 ? void 0 : mb.userId));
                if (currentUserDiceData) {
                    useUserDiceDataStore
                        .getState()
                        .setUserDiceData(Number(mb === null || mb === void 0 ? void 0 : mb.userId), Object.assign(Object.assign({}, currentUserDiceData), { setId: message.data }));
                }
            }
            (_a = diceWorkerCtx === null || diceWorkerCtx === void 0 ? void 0 : diceWorkerCtx.renderedWorker) === null || _a === void 0 ? void 0 : _a.postMessage({
                type: DiceActionTypes.SetUpdate,
                payload: message.data,
            });
        }
    }, [diceWorkerCtx === null || diceWorkerCtx === void 0 ? void 0 : diceWorkerCtx.renderedWorker, mb === null || mb === void 0 ? void 0 : mb.userId]));
    useFetchHistory(useCallback((fetchHistory) => dispatch({
        type: 'set/fetchHistory',
        payload: fetchHistory,
    }), []));
    useDiceFamilies(diceServiceUrl, useCallback((diceFamilies) => dispatch({
        type: fetchDiceFamilyInfoActionType,
        payload: diceFamilies,
    }), []));
    return (React.createElement(Provider, { value: [state, dispatch] },
        React.createElement(DdbThemeProvider, { theme: darkThemeEnabled ? darkTheme : lightTheme }, children)));
};
//# sourceMappingURL=GameLogContextProvider.js.map