import { ref, watch } from 'vue';
import { defineStore } from 'pinia';

import Noty from 'noty';

import {
    checkCanInvite,
    displayLocation,
    extractFileId,
    extractFileVersion,
    getUserMemo,
    parseLocation,
    removeFromArray,
    replaceBioSymbols
} from '../shared/utils';
import {
    instanceRequest,
    notificationRequest,
    userRequest,
    worldRequest
} from '../api';
import { AppDebug } from '../service/appConfig';
import { database } from '../service/database';
import { useAdvancedSettingsStore } from './settings/advanced';
import { useAppearanceSettingsStore } from './settings/appearance';
import { useFavoriteStore } from './favorite';
import { useFriendStore } from './friend';
import { useGameStore } from './game';
import { useGeneralSettingsStore } from './settings/general';
import { useInstanceStore } from './instance';
import { useLocationStore } from './location';
import { useNotificationsSettingsStore } from './settings/notifications';
import { useSharedFeedStore } from './sharedFeed';
import { useUiStore } from './ui';
import { useUserStore } from './user';
import { useWristOverlaySettingsStore } from './settings/wristOverlay';
import { watchState } from '../service/watchState';

import configRepository from '../service/config';

export const useNotificationStore = defineStore('Notification', () => {
    const generalSettingsStore = useGeneralSettingsStore();
    const locationStore = useLocationStore();
    const favoriteStore = useFavoriteStore();
    const friendStore = useFriendStore();
    const notificationsSettingsStore = useNotificationsSettingsStore();
    const advancedSettingsStore = useAdvancedSettingsStore();
    const appearanceSettingsStore = useAppearanceSettingsStore();
    const userStore = useUserStore();
    const wristOverlaySettingsStore = useWristOverlaySettingsStore();
    const uiStore = useUiStore();
    const gameStore = useGameStore();
    const sharedFeedStore = useSharedFeedStore();
    const instanceStore = useInstanceStore();

    const notificationInitStatus = ref(false);
    const notificationTable = ref({
        data: [],
        filters: [
            {
                prop: 'type',
                value: []
            },
            {
                prop: ['senderUsername', 'message'],
                value: ''
            }
        ],
        pageSize: 20,
        pageSizeLinked: true,
        paginationProps: {
            layout: 'sizes,prev,pager,next,total'
        }
    });
    const unseenNotifications = ref([]);
    const isNotificationsLoading = ref(false);

    const notyMap = {};

    watch(
        () => watchState.isLoggedIn,
        (isLoggedIn) => {
            isNotificationsLoading.value = false;
            notificationTable.value.data = [];
            if (isLoggedIn) {
                initNotifications();
            }
        },
        { flush: 'sync' }
    );

    async function init() {
        notificationTable.value.filters[0].value = JSON.parse(
            await configRepository.getString(
                'VRCX_notificationTableFilters',
                '[]'
            )
        );
    }

    init();

    function handleNotification(args) {
        args.ref = applyNotification(args.json);
        const { ref } = args;
        const array = notificationTable.value.data;
        const { length } = array;
        for (let i = 0; i < length; ++i) {
            if (array[i].id === ref.id) {
                array[i] = ref;
                return;
            }
        }
        if (ref.senderUserId !== userStore.currentUser.id) {
            if (
                ref.type !== 'friendRequest' &&
                ref.type !== 'ignoredFriendRequest' &&
                !ref.type.includes('.')
            ) {
                database.addNotificationToDatabase(ref);
            }
            if (watchState.isFriendsLoaded && notificationInitStatus.value) {
                if (
                    ref.details?.worldId &&
                    !instanceStore.cachedInstances.has(ref.details.worldId)
                ) {
                    // get instance name for invite
                    const L = parseLocation(ref.details.worldId);
                    if (L.isRealInstance) {
                        instanceRequest.getCachedInstance({
                            worldId: L.worldId,
                            instanceId: L.instanceId
                        });
                    }
                }
                if (
                    notificationTable.value.filters[0].value.length === 0 ||
                    notificationTable.value.filters[0].value.includes(ref.type)
                ) {
                    uiStore.notifyMenu('notification');
                }
                unseenNotifications.value.push(ref.id);
                queueNotificationNoty(ref);
                sharedFeedStore.addEntry(ref);
            }
        }
        notificationTable.value.data.push(ref);
        const D = userStore.userDialog;
        if (
            D.visible === false ||
            ref.type !== 'friendRequest' ||
            ref.senderUserId !== D.id
        ) {
            return;
        }
        D.incomingRequest = true;
    }

    function handleNotificationHide(args) {
        let ref;
        const array = notificationTable.value.data;
        for (let i = array.length - 1; i >= 0; i--) {
            if (array[i].id === args.params.notificationId) {
                ref = array[i];
                break;
            }
        }
        if (typeof ref === 'undefined') {
            return;
        }
        args.ref = ref;
        if (
            ref.type === 'friendRequest' ||
            ref.type === 'ignoredFriendRequest' ||
            ref.type.includes('.')
        ) {
            for (let i = array.length - 1; i >= 0; i--) {
                if (array[i].id === ref.id) {
                    array.splice(i, 1);
                    break;
                }
            }
        } else {
            ref.$isExpired = true;
            database.updateNotificationExpired(ref);
        }
        handleNotificationExpire({
            ref,
            params: {
                notificationId: ref.id
            }
        });
    }

    function handleNotificationV2Update(args) {
        const notificationId = args.params.notificationId;
        const json = args.json;
        if (!json) {
            return;
        }
        json.id = notificationId;
        handleNotification({
            json,
            params: {
                notificationId
            }
        });
        if (json.seen) {
            handleNotificationSee({
                params: {
                    notificationId
                }
            });
        }
    }

    function handlePipelineNotification(args) {
        const ref = args.json;
        if (
            ref.type !== 'requestInvite' ||
            generalSettingsStore.autoAcceptInviteRequests === 'Off'
        ) {
            return;
        }

        let currentLocation = locationStore.lastLocation.location;
        if (locationStore.lastLocation.location === 'traveling') {
            currentLocation = locationStore.lastLocationDestination;
        }
        if (!currentLocation) {
            // game log disabled, use API location
            currentLocation = userStore.currentUser.$locationTag;
            if (userStore.currentUser.$travelingToLocation) {
                currentLocation = userStore.currentUser.$travelingToLocation;
            }
        }
        if (!currentLocation) {
            return;
        }
        if (
            generalSettingsStore.autoAcceptInviteRequests === 'All Favorites' &&
            !favoriteStore.state.favoriteFriends_.some(
                (x) => x.id === ref.senderUserId
            )
        ) {
            return;
        }
        if (
            generalSettingsStore.autoAcceptInviteRequests ===
                'Selected Favorites' &&
            !friendStore.localFavoriteFriends.has(ref.senderUserId)
        ) {
            return;
        }
        if (!checkCanInvite(currentLocation)) {
            return;
        }

        const L = parseLocation(currentLocation);
        worldRequest
            .getCachedWorld({
                worldId: L.worldId
            })
            .then((args1) => {
                notificationRequest
                    .sendInvite(
                        {
                            instanceId: L.tag,
                            worldId: L.tag,
                            worldName: args1.ref.name,
                            rsvp: true
                        },
                        ref.senderUserId
                    )
                    .then((_args) => {
                        const text = `Auto invite sent to ${ref.senderUsername}`;
                        if (AppDebug.errorNoty) {
                            AppDebug.errorNoty.close();
                        }
                        AppDebug.errorNoty = new Noty({
                            type: 'info',
                            text
                        });
                        AppDebug.errorNoty.show();
                        console.log(text);
                        notificationRequest.hideNotification({
                            notificationId: ref.id
                        });
                        return _args;
                    })
                    .catch((err) => {
                        console.error(err);
                    });
            });
    }

    function handleNotificationSee(args) {
        const { notificationId } = args.params;
        removeFromArray(unseenNotifications.value, notificationId);
        if (unseenNotifications.value.length === 0) {
            uiStore.removeNotify('notification');
        }
    }

    function handleNotificationAccept(args) {
        let ref;
        const array = notificationTable.value.data;
        for (let i = array.length - 1; i >= 0; i--) {
            if (array[i].id === args.params.notificationId) {
                ref = array[i];
                break;
            }
        }
        if (typeof ref === 'undefined') {
            return;
        }
        ref.$isExpired = true;
        args.ref = ref;
        handleNotificationExpire({
            ref,
            params: {
                notificationId: ref.id
            }
        });
        friendStore.handleFriendAdd({
            params: {
                userId: ref.senderUserId
            }
        });

        const D = userStore.userDialog;
        if (
            D.visible === false ||
            typeof args.ref === 'undefined' ||
            args.ref.type !== 'friendRequest' ||
            args.ref.senderUserId !== D.id
        ) {
            return;
        }
        D.isFriend = true;
    }

    function handleNotificationExpire(args) {
        const { ref } = args;
        const D = userStore.userDialog;
        if (
            D.visible === false ||
            ref.type !== 'friendRequest' ||
            ref.senderUserId !== D.id
        ) {
            return;
        }
        D.incomingRequest = false;
    }

    /**
     *
     * @param {object} json
     * @returns {object}
     */
    function applyNotification(json) {
        if (json.message) {
            json.message = replaceBioSymbols(json.message);
        }
        let ref;
        const array = notificationTable.value.data;
        for (let i = array.length - 1; i >= 0; i--) {
            if (array[i].id === json.id) {
                ref = array[i];
                break;
            }
        }
        // delete any null in json
        for (const key in json) {
            if (json[key] === null) {
                delete json[key];
            }
        }
        if (typeof ref === 'undefined') {
            ref = {
                id: '',
                senderUserId: '',
                senderUsername: '',
                type: '',
                message: '',
                details: {},
                seen: false,
                created_at: '',
                // VRCX
                $isExpired: false,
                //
                ...json
            };
        } else {
            Object.assign(ref, json);
            ref.$isExpired = false;
        }
        if (ref.details !== Object(ref.details)) {
            let details = {};
            if (ref.details !== '{}') {
                try {
                    const object = JSON.parse(ref.details);
                    if (object === Object(object)) {
                        details = object;
                    }
                } catch (err) {
                    console.log(err);
                }
            }
            ref.details = details;
        }
        if (ref.type === 'boop') {
            ref.message = ref.title;
            if (ref.details?.emojiId?.startsWith('default_')) {
                ref.details.imageUrl = ref.details.emojiId;
                ref.message += ` ${ref.details.emojiId.replace('default_', '')}`;
            } else {
                ref.details.imageUrl = `${AppDebug.endpointDomain}/file/${ref.details.emojiId}/${ref.details.emojiVersion}`;
            }
        }
        return ref;
    }

    function expireFriendRequestNotifications() {
        const array = notificationTable.value.data;
        for (let i = array.length - 1; i >= 0; i--) {
            if (
                array[i].type === 'friendRequest' ||
                array[i].type === 'ignoredFriendRequest' ||
                array[i].type.includes('.')
            ) {
                array.splice(i, 1);
            }
        }
    }

    /**
     *
     * @param {string} notificationId
     */
    function expireNotification(notificationId) {
        let ref;
        const array = notificationTable.value.data;
        for (let i = array.length - 1; i >= 0; i--) {
            if (array[i].id === notificationId) {
                ref = array[i];
                break;
            }
        }
        if (typeof ref === 'undefined') {
            return;
        }
        ref.$isExpired = true;
        database.updateNotificationExpired(ref);
        handleNotificationExpire({
            ref,
            params: {
                notificationId: ref.id
            }
        });
    }

    function handleNotificationV2(args) {
        const json = args.json;
        json.created_at = json.createdAt;
        if (json.title && json.message) {
            json.message = `${json.title}, ${json.message}`;
        } else if (json.title) {
            json.message = json.title;
        }
        handleNotification({
            json,
            params: {
                notificationId: json.id
            }
        });
    }

    /**
     *
     * @returns {Promise<void>}
     */
    async function refreshNotifications() {
        isNotificationsLoading.value = true;
        let count;
        let params;
        try {
            expireFriendRequestNotifications();
            params = {
                n: 100,
                offset: 0
            };
            count = 50; // 5000 max
            for (let i = 0; i < count; i++) {
                const args = await notificationRequest.getNotifications(params);
                for (const json of args.json) {
                    handleNotification({
                        json,
                        params: {
                            notificationId: json.id
                        }
                    });
                }
                unseenNotifications.value = [];
                params.offset += 100;
                if (args.json.length < 100) {
                    break;
                }
            }
            params = {
                n: 100,
                offset: 0
            };
            count = 50; // 5000 max
            for (let i = 0; i < count; i++) {
                const args =
                    await notificationRequest.getNotificationsV2(params);

                for (const json of args.json) {
                    json.created_at = json.createdAt;
                    if (json.title && json.message) {
                        json.message = `${json.title}, ${json.message}`;
                    } else if (json.title) {
                        json.message = json.title;
                    }
                    handleNotification({
                        json,
                        params: {
                            notificationId: json.id
                        }
                    });
                }

                unseenNotifications.value = [];
                params.offset += 100;
                if (args.json.length < 100) {
                    break;
                }
            }
            params = {
                n: 100,
                offset: 0
            };
            count = 50; // 5000 max
            for (let i = 0; i < count; i++) {
                const args =
                    await notificationRequest.getHiddenFriendRequests(params);
                for (const json of args.json) {
                    json.type = 'ignoredFriendRequest';
                    handleNotification({
                        json,
                        params: {
                            notificationId: json.id
                        }
                    });
                }
                unseenNotifications.value = [];
                params.offset += 100;
                if (args.json.length < 100) {
                    break;
                }
            }
        } catch (err) {
            console.error(err);
        } finally {
            isNotificationsLoading.value = false;
            notificationInitStatus.value = true;
        }
    }

    /**
     *
     * @param {object} noty
     */
    function queueNotificationNoty(noty) {
        noty.isFriend = friendStore.friends.has(noty.senderUserId);
        noty.isFavorite = friendStore.localFavoriteFriends.has(
            noty.senderUserId
        );
        const notyFilter = notificationsSettingsStore.sharedFeedFilters.noty;
        if (
            notyFilter[noty.type] &&
            (notyFilter[noty.type] === 'On' ||
                notyFilter[noty.type] === 'Friends' ||
                (notyFilter[noty.type] === 'VIP' && noty.isFavorite))
        ) {
            playNoty(noty);
        }
    }

    function playNoty(noty) {
        if (
            userStore.currentUser.status === 'busy' ||
            !watchState.isFriendsLoaded
        ) {
            return;
        }
        let displayName = '';
        if (noty.displayName) {
            displayName = noty.displayName;
        } else if (noty.senderUsername) {
            displayName = noty.senderUsername;
        } else if (noty.sourceDisplayName) {
            displayName = noty.sourceDisplayName;
        }
        if (displayName) {
            // don't play noty twice
            const notyId = `${noty.type},${displayName}`;
            if (notyMap[notyId] && notyMap[notyId] >= noty.created_at) {
                return;
            }
            notyMap[notyId] = noty.created_at;
        }
        const bias = new Date(Date.now() - 60000).toJSON();
        for (const [notyId, createdAt] of Object.entries(notyMap)) {
            if (createdAt < bias) {
                delete notyMap[notyId];
            }
        }
        if (noty.created_at < bias) {
            // don't play noty if it's over 1min old
            return;
        }

        const notiConditions = {
            Always: () => true,
            'Inside VR': () => gameStore.isSteamVRRunning,
            'Outside VR': () => !gameStore.isSteamVRRunning,
            'Game Closed': () => !gameStore.isGameRunning, // Also known as "Outside VRChat"
            'Game Running': () => gameStore.isGameRunning, // Also known as "Inside VRChat"
            'Desktop Mode': () =>
                gameStore.isGameNoVR && gameStore.isGameRunning,
            AFK: () =>
                notificationsSettingsStore.afkDesktopToast &&
                gameStore.isHmdAfk &&
                gameStore.isGameRunning &&
                !gameStore.isGameNoVR
        };

        const playNotificationTTS =
            notiConditions[notificationsSettingsStore.notificationTTS]?.();
        const playDesktopToast =
            notiConditions[notificationsSettingsStore.desktopToast]?.() ||
            notiConditions['AFK']();

        const playOverlayToast =
            notiConditions[notificationsSettingsStore.overlayToast]?.();
        const playOverlayNotification =
            notificationsSettingsStore.overlayNotifications && playOverlayToast;
        const playXSNotification =
            notificationsSettingsStore.xsNotifications && playOverlayToast;
        const playOvrtHudNotifications =
            notificationsSettingsStore.ovrtHudNotifications && playOverlayToast;
        const playOvrtWristNotifications =
            notificationsSettingsStore.ovrtWristNotifications &&
            playOverlayToast;

        let message = '';
        if (noty.title) {
            message = `${noty.title}, ${noty.message}`;
        } else if (noty.message) {
            message = noty.message;
        }
        const messageList = [
            'inviteMessage',
            'requestMessage',
            'responseMessage'
        ];
        for (let k = 0; k < messageList.length; k++) {
            if (
                typeof noty.details !== 'undefined' &&
                typeof noty.details[messageList[k]] !== 'undefined'
            ) {
                message = `, ${noty.details[messageList[k]]}`;
            }
        }
        if (playNotificationTTS) {
            playNotyTTS(noty, displayName, message);
        }
        if (
            playDesktopToast ||
            playXSNotification ||
            playOvrtHudNotifications ||
            playOvrtWristNotifications ||
            playOverlayNotification
        ) {
            if (notificationsSettingsStore.imageNotifications) {
                notySaveImage(noty).then((image) => {
                    if (playXSNotification) {
                        displayXSNotification(noty, message, image);
                    }
                    if (
                        playOvrtHudNotifications ||
                        playOvrtWristNotifications
                    ) {
                        displayOvrtNotification(
                            playOvrtHudNotifications,
                            playOvrtWristNotifications,
                            noty,
                            message,
                            image
                        );
                    }
                    if (playDesktopToast) {
                        displayDesktopToast(noty, message, image);
                    }
                    if (playOverlayNotification) {
                        displayOverlayNotification(noty, message, image);
                    }
                });
            } else {
                if (playXSNotification) {
                    displayXSNotification(noty, message, '');
                }
                if (playOvrtHudNotifications || playOvrtWristNotifications) {
                    displayOvrtNotification(
                        playOvrtHudNotifications,
                        playOvrtWristNotifications,
                        noty,
                        message,
                        ''
                    );
                }
                if (playDesktopToast) {
                    displayDesktopToast(noty, message, '');
                }
                if (playOverlayNotification) {
                    displayOverlayNotification(noty, message, '');
                }
            }
        }
    }

    /**
     *
     * @param {object} noty
     * @param {string} displayName
     * @param {string} message
     */
    async function playNotyTTS(noty, displayName, message) {
        if (notificationsSettingsStore.notificationTTSNickName) {
            const userId = getUserIdFromNoty(noty);
            const memo = await getUserMemo(userId);
            if (memo.memo) {
                const array = memo.memo.split('\n');
                const nickName = array[0];
                displayName = nickName;
            }
        }
        switch (noty.type) {
            case 'OnPlayerJoined':
                notificationsSettingsStore.speak(`${displayName} has joined`);
                break;
            case 'OnPlayerLeft':
                notificationsSettingsStore.speak(`${displayName} has left`);
                break;
            case 'OnPlayerJoining':
                notificationsSettingsStore.speak(`${displayName} is joining`);
                break;
            case 'GPS':
                notificationsSettingsStore.speak(
                    `${displayName} is in ${displayLocation(
                        noty.location,
                        noty.worldName,
                        noty.groupName
                    )}`
                );
                break;
            case 'Online':
                let locationName = '';
                if (noty.worldName) {
                    locationName = ` to ${displayLocation(
                        noty.location,
                        noty.worldName,
                        noty.groupName
                    )}`;
                }
                notificationsSettingsStore.speak(
                    `${displayName} has logged in${locationName}`
                );
                break;
            case 'Offline':
                notificationsSettingsStore.speak(
                    `${displayName} has logged out`
                );
                break;
            case 'Status':
                notificationsSettingsStore.speak(
                    `${displayName} status is now ${noty.status} ${noty.statusDescription}`
                );
                break;
            case 'invite':
                notificationsSettingsStore.speak(
                    `${displayName} has invited you to ${displayLocation(
                        noty.details.worldId,
                        noty.details.worldName,
                        noty.groupName
                    )}${message}`
                );
                break;
            case 'requestInvite':
                notificationsSettingsStore.speak(
                    `${displayName} has requested an invite${message}`
                );
                break;
            case 'inviteResponse':
                notificationsSettingsStore.speak(
                    `${displayName} has responded to your invite${message}`
                );
                break;
            case 'requestInviteResponse':
                notificationsSettingsStore.speak(
                    `${displayName} has responded to your invite request${message}`
                );
                break;
            case 'friendRequest':
                notificationsSettingsStore.speak(
                    `${displayName} has sent you a friend request`
                );
                break;
            case 'Friend':
                notificationsSettingsStore.speak(
                    `${displayName} is now your friend`
                );
                break;
            case 'Unfriend':
                notificationsSettingsStore.speak(
                    `${displayName} is no longer your friend`
                );
                break;
            case 'TrustLevel':
                notificationsSettingsStore.speak(
                    `${displayName} trust level is now ${noty.trustLevel}`
                );
                break;
            case 'DisplayName':
                notificationsSettingsStore.speak(
                    `${noty.previousDisplayName} changed their name to ${noty.displayName}`
                );
                break;
            case 'boop':
                notificationsSettingsStore.speak(noty.message);
                break;
            case 'groupChange':
                notificationsSettingsStore.speak(
                    `${displayName} ${noty.message}`
                );
                break;
            case 'group.announcement':
                notificationsSettingsStore.speak(noty.message);
                break;
            case 'group.informative':
                notificationsSettingsStore.speak(noty.message);
                break;
            case 'group.invite':
                notificationsSettingsStore.speak(noty.message);
                break;
            case 'group.joinRequest':
                notificationsSettingsStore.speak(noty.message);
                break;
            case 'group.transfer':
                notificationsSettingsStore.speak(noty.message);
                break;
            case 'group.queueReady':
                notificationsSettingsStore.speak(noty.message);
                break;
            case 'instance.closed':
                notificationsSettingsStore.speak(noty.message);
                break;
            case 'PortalSpawn':
                if (displayName) {
                    notificationsSettingsStore.speak(
                        `${displayName} has spawned a portal to ${displayLocation(
                            noty.instanceId,
                            noty.worldName,
                            noty.groupName
                        )}`
                    );
                } else {
                    notificationsSettingsStore.speak(
                        'User has spawned a portal'
                    );
                }
                break;
            case 'AvatarChange':
                notificationsSettingsStore.speak(
                    `${displayName} changed into avatar ${noty.name}`
                );
                break;
            case 'ChatBoxMessage':
                notificationsSettingsStore.speak(
                    `${displayName} said ${noty.text}`
                );
                break;
            case 'Event':
                notificationsSettingsStore.speak(noty.data);
                break;
            case 'External':
                notificationsSettingsStore.speak(noty.message);
                break;
            case 'VideoPlay':
                notificationsSettingsStore.speak(
                    `Now playing: ${noty.notyName}`
                );
                break;
            case 'BlockedOnPlayerJoined':
                notificationsSettingsStore.speak(
                    `Blocked user ${displayName} has joined`
                );
                break;
            case 'BlockedOnPlayerLeft':
                notificationsSettingsStore.speak(
                    `Blocked user ${displayName} has left`
                );
                break;
            case 'MutedOnPlayerJoined':
                notificationsSettingsStore.speak(
                    `Muted user ${displayName} has joined`
                );
                break;
            case 'MutedOnPlayerLeft':
                notificationsSettingsStore.speak(
                    `Muted user ${displayName} has left`
                );
                break;
            case 'Blocked':
                notificationsSettingsStore.speak(
                    `${displayName} has blocked you`
                );
                break;
            case 'Unblocked':
                notificationsSettingsStore.speak(
                    `${displayName} has unblocked you`
                );
                break;
            case 'Muted':
                notificationsSettingsStore.speak(
                    `${displayName} has muted you`
                );
                break;
            case 'Unmuted':
                notificationsSettingsStore.speak(
                    `${displayName} has unmuted you`
                );
                break;
        }
    }

    /**
     *
     * @param {object} noty
     * @returns
     */
    async function notySaveImage(noty) {
        const imageUrl = await notyGetImage(noty);
        let fileId = extractFileId(imageUrl);
        let fileVersion = extractFileVersion(imageUrl);
        let imageLocation = '';
        try {
            if (fileId && fileVersion) {
                imageLocation = await AppApi.GetImage(
                    imageUrl,
                    fileId,
                    fileVersion
                );
            } else if (imageUrl && imageUrl.startsWith('http')) {
                fileVersion = imageUrl.split('/').pop(); // 1416226261.thumbnail-500.png
                fileId = fileVersion.split('.').shift(); // 1416226261
                imageLocation = await AppApi.GetImage(
                    imageUrl,
                    fileId,
                    fileVersion
                );
            }
        } catch (err) {
            console.error(imageUrl, err);
        }
        return imageLocation;
    }

    function displayDesktopToast(noty, message, image) {
        switch (noty.type) {
            case 'OnPlayerJoined':
                desktopNotification(noty.displayName, 'has joined', image);
                break;
            case 'OnPlayerLeft':
                desktopNotification(noty.displayName, 'has left', image);
                break;
            case 'OnPlayerJoining':
                desktopNotification(noty.displayName, 'is joining', image);
                break;
            case 'GPS':
                desktopNotification(
                    noty.displayName,
                    `is in ${displayLocation(
                        noty.location,
                        noty.worldName,
                        noty.groupName
                    )}`,
                    image
                );
                break;
            case 'Online':
                let locationName = '';
                if (noty.worldName) {
                    locationName = ` to ${displayLocation(
                        noty.location,
                        noty.worldName,
                        noty.groupName
                    )}`;
                }
                desktopNotification(
                    noty.displayName,
                    `has logged in${locationName}`,
                    image
                );
                break;
            case 'Offline':
                desktopNotification(noty.displayName, 'has logged out', image);
                break;
            case 'Status':
                desktopNotification(
                    noty.displayName,
                    `status is now ${noty.status} ${noty.statusDescription}`,
                    image
                );
                break;
            case 'invite':
                desktopNotification(
                    noty.senderUsername,
                    `has invited you to ${displayLocation(
                        noty.details.worldId,
                        noty.details.worldName
                    )}${message}`,
                    image
                );
                break;
            case 'requestInvite':
                desktopNotification(
                    noty.senderUsername,
                    `has requested an invite${message}`,
                    image
                );
                break;
            case 'inviteResponse':
                desktopNotification(
                    noty.senderUsername,
                    `has responded to your invite${message}`,
                    image
                );
                break;
            case 'requestInviteResponse':
                desktopNotification(
                    noty.senderUsername,
                    `has responded to your invite request${message}`,
                    image
                );
                break;
            case 'friendRequest':
                desktopNotification(
                    noty.senderUsername,
                    'has sent you a friend request',
                    image
                );
                break;
            case 'Friend':
                desktopNotification(
                    noty.displayName,
                    'is now your friend',
                    image
                );
                break;
            case 'Unfriend':
                desktopNotification(
                    noty.displayName,
                    'is no longer your friend',
                    image
                );
                break;
            case 'TrustLevel':
                desktopNotification(
                    noty.displayName,
                    `trust level is now ${noty.trustLevel}`,
                    image
                );
                break;
            case 'DisplayName':
                desktopNotification(
                    noty.previousDisplayName,
                    `changed their name to ${noty.displayName}`,
                    image
                );
                break;
            case 'boop':
                desktopNotification(noty.senderUsername, noty.message, image);
                break;
            case 'groupChange':
                desktopNotification(noty.senderUsername, noty.message, image);
                break;
            case 'group.announcement':
                desktopNotification('Group Announcement', noty.message, image);
                break;
            case 'group.informative':
                desktopNotification('Group Informative', noty.message, image);
                break;
            case 'group.invite':
                desktopNotification('Group Invite', noty.message, image);
                break;
            case 'group.joinRequest':
                desktopNotification('Group Join Request', noty.message, image);
                break;
            case 'group.transfer':
                desktopNotification(
                    'Group Transfer Request',
                    noty.message,
                    image
                );
                break;
            case 'group.queueReady':
                desktopNotification(
                    'Instance Queue Ready',
                    noty.message,
                    image
                );
                break;
            case 'instance.closed':
                desktopNotification('Instance Closed', noty.message, image);
                break;
            case 'PortalSpawn':
                if (noty.displayName) {
                    desktopNotification(
                        noty.displayName,
                        `has spawned a portal to ${displayLocation(
                            noty.instanceId,
                            noty.worldName,
                            noty.groupName
                        )}`,
                        image
                    );
                } else {
                    desktopNotification('', 'User has spawned a portal', image);
                }
                break;
            case 'AvatarChange':
                desktopNotification(
                    noty.displayName,
                    `changed into avatar ${noty.name}`,
                    image
                );
                break;
            case 'ChatBoxMessage':
                desktopNotification(
                    noty.displayName,
                    `said ${noty.text}`,
                    image
                );
                break;
            case 'Event':
                desktopNotification('Event', noty.data, image);
                break;
            case 'External':
                desktopNotification('External', noty.message, image);
                break;
            case 'VideoPlay':
                desktopNotification('Now playing', noty.notyName, image);
                break;
            case 'BlockedOnPlayerJoined':
                desktopNotification(
                    noty.displayName,
                    'blocked user has joined',
                    image
                );
                break;
            case 'BlockedOnPlayerLeft':
                desktopNotification(
                    noty.displayName,
                    'blocked user has left',
                    image
                );
                break;
            case 'MutedOnPlayerJoined':
                desktopNotification(
                    noty.displayName,
                    'muted user has joined',
                    image
                );
                break;
            case 'MutedOnPlayerLeft':
                desktopNotification(
                    noty.displayName,
                    'muted user has left',
                    image
                );
                break;
            case 'Blocked':
                desktopNotification(noty.displayName, 'has blocked you', image);
                break;
            case 'Unblocked':
                desktopNotification(
                    noty.displayName,
                    'has unblocked you',
                    image
                );
                break;
            case 'Muted':
                desktopNotification(noty.displayName, 'has muted you', image);
                break;
            case 'Unmuted':
                desktopNotification(noty.displayName, 'has unmuted you', image);
                break;
        }
    }

    /**
     *
     * @param {string} noty
     * @param {string} message
     * @param {string} imageFile
     */
    function displayOverlayNotification(noty, message, imageFile) {
        let image = '';
        if (imageFile) {
            image = `file:///${imageFile}`;
        }
        AppApi.ExecuteVrOverlayFunction(
            'playNoty',
            JSON.stringify({ noty, message, image })
        );
    }

    /**
     *
     * @param {any} noty
     * @param {string} message
     * @param {string} image
     */
    function displayXSNotification(noty, message, image) {
        const timeout = Math.floor(
            parseInt(
                notificationsSettingsStore.notificationTimeout.toString(),
                10
            ) / 1000
        );
        const opacity =
            parseFloat(advancedSettingsStore.notificationOpacity.toString()) /
            100;
        switch (noty.type) {
            case 'OnPlayerJoined':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} has joined`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'OnPlayerLeft':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} has left`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'OnPlayerJoining':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} is joining`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'GPS':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} is in ${displayLocation(
                        noty.location,
                        noty.worldName,
                        noty.groupName
                    )}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Online':
                let locationName = '';
                if (noty.worldName) {
                    locationName = ` to ${displayLocation(
                        noty.location,
                        noty.worldName,
                        noty.groupName
                    )}`;
                }
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} has logged in${locationName}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Offline':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} has logged out`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Status':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} status is now ${noty.status} ${noty.statusDescription}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'invite':
                AppApi.XSNotification(
                    'VRCX',
                    `${
                        noty.senderUsername
                    } has invited you to ${displayLocation(
                        noty.details.worldId,
                        noty.details.worldName
                    )}${message}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'requestInvite':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.senderUsername} has requested an invite${message}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'inviteResponse':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.senderUsername} has responded to your invite${message}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'requestInviteResponse':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.senderUsername} has responded to your invite request${message}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'friendRequest':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.senderUsername} has sent you a friend request`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Friend':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} is now your friend`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Unfriend':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} is no longer your friend`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'TrustLevel':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} trust level is now ${noty.trustLevel}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'DisplayName':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.previousDisplayName} changed their name to ${noty.displayName}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'boop':
                AppApi.XSNotification(
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'groupChange':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.senderUsername}: ${noty.message}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.announcement':
                AppApi.XSNotification(
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.informative':
                AppApi.XSNotification(
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.invite':
                AppApi.XSNotification(
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.joinRequest':
                AppApi.XSNotification(
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.transfer':
                AppApi.XSNotification(
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.queueReady':
                AppApi.XSNotification(
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'instance.closed':
                AppApi.XSNotification(
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'PortalSpawn':
                if (noty.displayName) {
                    AppApi.XSNotification(
                        'VRCX',
                        `${
                            noty.displayName
                        } has spawned a portal to ${displayLocation(
                            noty.instanceId,
                            noty.worldName,
                            noty.groupName
                        )}`,
                        timeout,
                        opacity,
                        image
                    );
                } else {
                    AppApi.XSNotification(
                        'VRCX',
                        'User has spawned a portal',
                        timeout,
                        opacity,
                        image
                    );
                }
                break;
            case 'AvatarChange':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} changed into avatar ${noty.name}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'ChatBoxMessage':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} said ${noty.text}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Event':
                AppApi.XSNotification(
                    'VRCX',
                    noty.data,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'External':
                AppApi.XSNotification(
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'VideoPlay':
                AppApi.XSNotification(
                    'VRCX',
                    `Now playing: ${noty.notyName}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'BlockedOnPlayerJoined':
                AppApi.XSNotification(
                    'VRCX',
                    `Blocked user ${noty.displayName} has joined`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'BlockedOnPlayerLeft':
                AppApi.XSNotification(
                    'VRCX',
                    `Blocked user ${noty.displayName} has left`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'MutedOnPlayerJoined':
                AppApi.XSNotification(
                    'VRCX',
                    `Muted user ${noty.displayName} has joined`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'MutedOnPlayerLeft':
                AppApi.XSNotification(
                    'VRCX',
                    `Muted user ${noty.displayName} has left`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Blocked':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} has blocked you`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Unblocked':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} has unblocked you`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Muted':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} has muted you`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Unmuted':
                AppApi.XSNotification(
                    'VRCX',
                    `${noty.displayName} has unmuted you`,
                    timeout,
                    opacity,
                    image
                );
                break;
        }
    }

    function displayOvrtNotification(
        playOvrtHudNotifications,
        playOvrtWristNotifications,
        noty,
        message,
        image
    ) {
        const timeout = Math.floor(
            parseInt(
                notificationsSettingsStore.notificationTimeout.toString(),
                10
            ) / 1000
        );
        const opacity =
            parseFloat(advancedSettingsStore.notificationOpacity.toString()) /
            100;
        switch (noty.type) {
            case 'OnPlayerJoined':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} has joined`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'OnPlayerLeft':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} has left`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'OnPlayerJoining':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} is joining`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'GPS':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} is in ${displayLocation(
                        noty.location,
                        noty.worldName,
                        noty.groupName
                    )}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Online':
                let locationName = '';
                if (noty.worldName) {
                    locationName = ` to ${displayLocation(
                        noty.location,
                        noty.worldName,
                        noty.groupName
                    )}`;
                }
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} has logged in${locationName}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Offline':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} has logged out`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Status':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} status is now ${noty.status} ${noty.statusDescription}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'invite':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${
                        noty.senderUsername
                    } has invited you to ${displayLocation(
                        noty.details.worldId,
                        noty.details.worldName
                    )}${message}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'requestInvite':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.senderUsername} has requested an invite${message}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'inviteResponse':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.senderUsername} has responded to your invite${message}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'requestInviteResponse':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.senderUsername} has responded to your invite request${message}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'friendRequest':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.senderUsername} has sent you a friend request`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Friend':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} is now your friend`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Unfriend':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} is no longer your friend`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'TrustLevel':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} trust level is now ${noty.trustLevel}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'DisplayName':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.previousDisplayName} changed their name to ${noty.displayName}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'boop':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'groupChange':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.senderUsername}: ${noty.message}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.announcement':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.informative':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.invite':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.joinRequest':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.transfer':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'group.queueReady':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'instance.closed':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'PortalSpawn':
                if (noty.displayName) {
                    AppApi.OVRTNotification(
                        playOvrtHudNotifications,
                        playOvrtWristNotifications,
                        'VRCX',
                        `${
                            noty.displayName
                        } has spawned a portal to ${displayLocation(
                            noty.instanceId,
                            noty.worldName,
                            noty.groupName
                        )}`,
                        timeout,
                        opacity,
                        image
                    );
                } else {
                    AppApi.OVRTNotification(
                        playOvrtHudNotifications,
                        playOvrtWristNotifications,
                        'VRCX',
                        'User has spawned a portal',
                        timeout,
                        opacity,
                        image
                    );
                }
                break;
            case 'AvatarChange':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} changed into avatar ${noty.name}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'ChatBoxMessage':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} said ${noty.text}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Event':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    noty.data,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'External':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    noty.message,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'VideoPlay':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `Now playing: ${noty.notyName}`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'BlockedOnPlayerJoined':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `Blocked user ${noty.displayName} has joined`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'BlockedOnPlayerLeft':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `Blocked user ${noty.displayName} has left`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'MutedOnPlayerJoined':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `Muted user ${noty.displayName} has joined`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'MutedOnPlayerLeft':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `Muted user ${noty.displayName} has left`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Blocked':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} has blocked you`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Unblocked':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} has unblocked you`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Muted':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} has muted you`,
                    timeout,
                    opacity,
                    image
                );
                break;
            case 'Unmuted':
                AppApi.OVRTNotification(
                    playOvrtHudNotifications,
                    playOvrtWristNotifications,
                    'VRCX',
                    `${noty.displayName} has unmuted you`,
                    timeout,
                    opacity,
                    image
                );
                break;
        }
    }

    /**
     *
     * @param {object} noty
     * @returns
     */
    function getUserIdFromNoty(noty) {
        let userId = '';
        if (noty.userId) {
            userId = noty.userId;
        } else if (noty.senderUserId) {
            userId = noty.senderUserId;
        } else if (noty.sourceUserId) {
            userId = noty.sourceUserId;
        } else if (noty.displayName) {
            for (const ref of userStore.cachedUsers.values()) {
                if (ref.displayName === noty.displayName) {
                    userId = ref.id;
                    break;
                }
            }
        }
        return userId;
    }

    /**
     *
     * @param {object} noty
     * @returns
     */
    async function notyGetImage(noty) {
        let imageUrl = '';
        const userId = getUserIdFromNoty(noty);

        if (noty.thumbnailImageUrl) {
            imageUrl = noty.thumbnailImageUrl;
        } else if (noty.details && noty.details.imageUrl) {
            imageUrl = noty.details.imageUrl;
        } else if (noty.imageUrl) {
            imageUrl = noty.imageUrl;
        } else if (userId && !userId.startsWith('grp_')) {
            imageUrl = await userRequest
                .getCachedUser({
                    userId
                })
                .catch((err) => {
                    console.error(err);
                    return '';
                })
                .then((args) => {
                    if (!args.json) {
                        return '';
                    }
                    if (
                        appearanceSettingsStore.displayVRCPlusIconsAsAvatar &&
                        args.json.userIcon
                    ) {
                        return args.json.userIcon;
                    }
                    if (args.json.profilePicOverride) {
                        return args.json.profilePicOverride;
                    }
                    return args.json.currentAvatarThumbnailImageUrl;
                });
        }
        return imageUrl;
    }

    /**
     *
     * @param {string} displayName
     * @param {string} message
     * @param {string} image
     */
    function desktopNotification(displayName, message, image) {
        if (WINDOWS) {
            AppApi.DesktopNotification(displayName, message, image);
        } else {
            window.electron.desktopNotification(displayName, message, image);
        }
    }

    function queueGameLogNoty(noty) {
        let bias;
        // remove join/leave notifications when switching worlds
        if (
            noty.type === 'OnPlayerJoined'
            // noty.type === 'BlockedOnPlayerJoined' ||
            // noty.type === 'MutedOnPlayerJoined'
        ) {
            bias = locationStore.lastLocation.date + 30 * 1000; // 30 secs
            if (Date.parse(noty.created_at) <= bias) {
                return;
            }
        }
        if (
            noty.type === 'OnPlayerLeft' ||
            noty.type === 'BlockedOnPlayerLeft' ||
            noty.type === 'MutedOnPlayerLeft'
        ) {
            bias = locationStore.lastLocationDestinationTime + 5 * 1000; // 5 secs
            if (Date.parse(noty.created_at) <= bias) {
                return;
            }
        }
        if (
            noty.type === 'Notification' ||
            noty.type === 'LocationDestination'
            // skip unused entries
        ) {
            return;
        }
        if (noty.type === 'VideoPlay') {
            if (!noty.videoName) {
                // skip video without name
                return;
            }
            noty.notyName = noty.videoName;
            if (noty.displayName) {
                // add requester's name to noty
                noty.notyName = `${noty.videoName} (${noty.displayName})`;
            }
        }
        if (
            noty.type !== 'VideoPlay' &&
            noty.displayName === userStore.currentUser.displayName
        ) {
            // remove current user
            return;
        }
        noty.isFriend = false;
        noty.isFavorite = false;
        if (noty.userId) {
            noty.isFriend = friendStore.friends.has(noty.userId);
            noty.isFavorite = friendStore.localFavoriteFriends.has(noty.userId);
        } else if (noty.displayName) {
            for (const ref of userStore.cachedUsers.values()) {
                if (ref.displayName === noty.displayName) {
                    noty.isFriend = friendStore.friends.has(ref.id);
                    noty.isFavorite = friendStore.localFavoriteFriends.has(
                        ref.id
                    );
                    break;
                }
            }
        }
        const notyFilter = notificationsSettingsStore.sharedFeedFilters.noty;
        if (
            notyFilter[noty.type] &&
            (notyFilter[noty.type] === 'On' ||
                notyFilter[noty.type] === 'Everyone' ||
                (notyFilter[noty.type] === 'Friends' && noty.isFriend) ||
                (notyFilter[noty.type] === 'VIP' && noty.isFavorite))
        ) {
            playNoty(noty);
        }
    }

    function queueFeedNoty(noty) {
        if (noty.type === 'Avatar') {
            return;
        }
        // hide private worlds from feed
        if (
            wristOverlaySettingsStore.hidePrivateFromFeed &&
            noty.type === 'GPS' &&
            noty.location === 'private'
        ) {
            return;
        }
        noty.isFriend = friendStore.friends.has(noty.userId);
        noty.isFavorite = friendStore.localFavoriteFriends.has(noty.userId);
        const notyFilter = notificationsSettingsStore.sharedFeedFilters.noty;
        if (
            notyFilter[noty.type] &&
            (notyFilter[noty.type] === 'Everyone' ||
                (notyFilter[noty.type] === 'Friends' && noty.isFriend) ||
                (notyFilter[noty.type] === 'VIP' && noty.isFavorite))
        ) {
            playNoty(noty);
        }
    }

    function queueFriendLogNoty(noty) {
        if (noty.type === 'FriendRequest') {
            return;
        }
        noty.isFriend = friendStore.friends.has(noty.userId);
        noty.isFavorite = friendStore.localFavoriteFriends.has(noty.userId);
        const notyFilter = notificationsSettingsStore.sharedFeedFilters.noty;
        if (
            notyFilter[noty.type] &&
            (notyFilter[noty.type] === 'On' ||
                notyFilter[noty.type] === 'Friends' ||
                (notyFilter[noty.type] === 'VIP' && noty.isFavorite))
        ) {
            playNoty(noty);
        }
    }

    function queueModerationNoty(noty) {
        noty.isFriend = false;
        noty.isFavorite = false;
        if (noty.userId) {
            noty.isFriend = friendStore.friends.has(noty.userId);
            noty.isFavorite = friendStore.localFavoriteFriends.has(noty.userId);
        }
        const notyFilter = notificationsSettingsStore.sharedFeedFilters.noty;
        if (notyFilter[noty.type] && notyFilter[noty.type] === 'On') {
            playNoty(noty);
        }
    }

    async function initNotifications() {
        notificationInitStatus.value = false;
        notificationTable.value.data = await database.getNotifications();
        refreshNotifications();
    }

    function testNotification() {
        playNoty({
            type: 'Event',
            created_at: new Date().toJSON(),
            data: 'Notification Test'
        });
    }

    return {
        notificationInitStatus,
        notificationTable,
        unseenNotifications,
        isNotificationsLoading,

        initNotifications,
        expireNotification,
        refreshNotifications,
        queueNotificationNoty,
        playNoty,
        queueGameLogNoty,
        queueFeedNoty,
        queueFriendLogNoty,
        queueModerationNoty,
        handleNotificationAccept,
        handleNotificationSee,
        handlePipelineNotification,
        handleNotificationV2Update,
        handleNotificationHide,
        handleNotification,
        handleNotificationV2,
        testNotification
    };
});
