import { nextTick, reactive, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { toast } from 'vue-sonner';
import { useI18n } from 'vue-i18n';

import {
    checkVRChatCache,
    compareByDisplayName,
    compareById,
    compareByLocationAt,
    displayLocation,
    getAvailablePlatforms,
    getBundleDateSize,
    getGroupName,
    getWorldName,
    hasGroupPermission,
    isRealInstance,
    parseLocation,
    replaceBioSymbols
} from '../shared/utils';
import {
    groupRequest,
    instanceRequest,
    userRequest,
    worldRequest
} from '../api';
import {
    accessTypeLocaleKeyMap,
    instanceContentSettings
} from '../shared/constants';
import { database } from '../service/database';
import { useAppearanceSettingsStore } from './settings/appearance';
import { useFriendStore } from './friend';
import { useGroupStore } from './group';
import { useLocationStore } from './location';
import { useNotificationStore } from './notification';
import { usePhotonStore } from './photon';
import { useSharedFeedStore } from './sharedFeed';
import { useUiStore } from './ui';
import { useUserStore } from './user';
import { useWorldStore } from './world';
import { watchState } from '../service/watchState';

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

export const useInstanceStore = defineStore('Instance', () => {
    const locationStore = useLocationStore();
    const worldStore = useWorldStore();
    const friendStore = useFriendStore();
    const appearanceSettingsStore = useAppearanceSettingsStore();
    const groupStore = useGroupStore();
    const notificationStore = useNotificationStore();
    const uiStore = useUiStore();
    const userStore = useUserStore();
    const sharedFeedStore = useSharedFeedStore();
    const photonStore = usePhotonStore();
    const { t } = useI18n();

    const state = reactive({
        updatePlayerListTimer: null,
        updatePlayerListPending: false
    });

    let cachedInstances = new Map();

    function cleanInstanceCache() {
        const maxSize = 200;
        if (cachedInstances.size <= maxSize) {
            return;
        }
        const removable = [];
        cachedInstances.forEach((ref, id) => {
            if (
                [...friendStore.friends.values()].some(
                    (f) => f.$location?.tag === id
                )
            ) {
                return;
            }
            removable.push({
                id,
                fetchedAt: Date.parse(ref.$fetchedAt) || 0
            });
        });
        removable.sort((a, b) => a.fetchedAt - b.fetchedAt);
        const overBy = cachedInstances.size - maxSize;
        for (let i = 0; i < overBy && i < removable.length; i++) {
            cachedInstances.delete(removable[i].id);
        }
    }

    const lastInstanceApplied = ref('');

    const currentInstanceWorld = ref({
        ref: {},
        instance: {},
        isPC: false,
        isQuest: false,
        isIos: false,
        avatarScalingDisabled: false,
        focusViewDisabled: false,
        inCache: false,
        cacheSize: '',
        bundleSizes: {},
        lastUpdated: ''
    });

    /** @type {import('vue').Ref<any>} */
    const currentInstanceLocation = ref({});

    const queuedInstances = reactive(new Map());

    const previousInstancesInfoDialog = ref({
        instanceId: '',
        visible: false
    });

    const previousInstancesListDialog = ref({
        visible: false,
        variant: 'user',
        userRef: {
            id: '',
            displayName: ''
        },
        worldRef: {
            id: '',
            name: ''
        },
        groupRef: {
            id: '',
            name: ''
        }
    });

    const previousInstancesListState = ref({
        user: { search: '', pageSize: 10, pageIndex: 0 },
        world: { search: '', pageSize: 10, pageIndex: 0 },
        group: { search: '', pageSize: 10, pageIndex: 0 }
    });

    const instanceJoinHistory = reactive(new Map());

    const currentInstanceUsersData = ref([]);

    watch(
        () => watchState.isLoggedIn,
        (isLoggedIn) => {
            currentInstanceUsersData.value = [];
            instanceJoinHistory.clear();
            hidePreviousInstancesDialogs();
            cachedInstances.clear();
            queuedInstances.clear();
            if (isLoggedIn) {
                getInstanceJoinHistory();
            }
        },
        { flush: 'sync' }
    );

    async function getInstanceJoinHistory() {
        try {
            const data = await database.getInstanceJoinHistory();
            instanceJoinHistory.clear();
            for (const [key, value] of data) {
                instanceJoinHistory.set(key, value);
            }
        } catch (error) {
            console.error('Failed to get instance join history:', error);
        }
    }

    function addInstanceJoinHistory(location, dateTime) {
        if (!location || !dateTime) {
            return;
        }

        if (instanceJoinHistory.has(location)) {
            instanceJoinHistory.delete(location);
        }

        const epoch = new Date(dateTime).getTime();
        instanceJoinHistory.set(location, epoch);
    }

    function hidePreviousInstancesDialogs() {
        previousInstancesInfoDialog.value.visible = false;
        previousInstancesListDialog.value.visible = false;
    }

    async function resolveUserRef(input) {
        if (!input) {
            return { id: '', displayName: '' };
        }
        if (typeof input === 'string') {
            input = { id: input, displayName: '' };
        }
        const id = input.id || input.userId || '';
        let displayName = input.displayName || '';
        if (id && !displayName) {
            try {
                const args = await userRequest.getCachedUser({ userId: id });
                displayName = args?.ref?.displayName || displayName;
                return { ...args.ref, id, displayName };
            } catch {
                return { ...input, id, displayName };
            }
        }
        return { ...input, id, displayName };
    }

    async function resolveWorldRef(input) {
        if (!input) {
            return { id: '', name: '' };
        }
        if (typeof input === 'string') {
            input = { id: input, name: '' };
        }
        const id = input.id || input.worldId || '';
        let name = input.name || '';
        if (id && !name) {
            try {
                const args = await worldRequest.getCachedWorld({ worldId: id });
                name = args?.ref?.name || name;
                return { ...args.ref, id, name };
            } catch {
                return { ...input, id, name };
            }
        }
        return { ...input, id, name };
    }

    async function resolveGroupRef(input) {
        if (!input) {
            return { id: '', name: '' };
        }
        if (typeof input === 'string') {
            input = { id: input, name: '' };
        }
        const id = input.id || input.groupId || '';
        let name = input.name || '';
        if (id && !name) {
            try {
                const args = await groupRequest.getCachedGroup({ groupId: id });
                name = args?.ref?.name || name;
                return { ...args.ref, id, name };
            } catch {
                return { ...input, id, name };
            }
        }
        return { ...input, id, name };
    }

    function translateAccessType(accessTypeNameRaw) {
        const key = accessTypeLocaleKeyMap[accessTypeNameRaw];
        if (!key) {
            return accessTypeNameRaw;
        }
        if (
            accessTypeNameRaw === 'groupPublic' ||
            accessTypeNameRaw === 'groupPlus'
        ) {
            const groupKey = accessTypeLocaleKeyMap.group;
            return `${t(groupKey)} ${t(key)}`;
        }
        return t(key);
    }

    function formatPreviousInstancesInfoLabel(
        instanceId,
        worldNameOverride = ''
    ) {
        const location = parseLocation(instanceId);
        const worldId = location.worldId;
        const worldName =
            worldNameOverride ||
            (worldId ? worldStore.cachedWorlds.get(worldId)?.name : '') ||
            '';
        const baseLabel = worldName || worldId || instanceId || '';
        const accessTypeLabel = translateAccessType(
            location.accessTypeName || ''
        );
        if (!accessTypeLabel || !location.instanceId) {
            return baseLabel;
        }
        return `${baseLabel} · ${accessTypeLabel}`;
    }

    function showPreviousInstancesInfoDialog(instanceId, options = {}) {
        previousInstancesInfoDialog.value.visible = true;
        previousInstancesInfoDialog.value.instanceId = instanceId;
        uiStore.openDialog({
            type: 'previous-instances-info',
            id: instanceId || '',
            label: instanceId
                ? formatPreviousInstancesInfoLabel(instanceId)
                : '',
            skipBreadcrumb: options.skipBreadcrumb
        });
        if (instanceId) {
            const location = parseLocation(instanceId);
            if (
                location.worldId &&
                !worldStore.cachedWorlds.get(location.worldId)?.name
            ) {
                worldRequest
                    .getCachedWorld({ worldId: location.worldId })
                    .then((args) => {
                        uiStore.setDialogCrumbLabel(
                            'previous-instances-info',
                            instanceId,
                            formatPreviousInstancesInfoLabel(
                                instanceId,
                                args?.ref?.name || ''
                            )
                        );
                    })
                    .catch(() => {});
            }
        }
    }

    async function showPreviousInstancesListDialog(
        variant,
        targetRef,
        options = {}
    ) {
        previousInstancesListDialog.value.variant = variant;
        let resolved = null;
        if (variant === 'user') {
            resolved = await resolveUserRef(targetRef);
            previousInstancesListDialog.value.userRef = resolved;
        } else if (variant === 'world') {
            resolved = await resolveWorldRef(targetRef);
            previousInstancesListDialog.value.worldRef = resolved;
        } else {
            resolved = await resolveGroupRef(targetRef);
            previousInstancesListDialog.value.groupRef = resolved;
        }
        previousInstancesListDialog.value.visible = true;
        const dialogId = resolved?.id || '';
        const label = resolved?.id
            ? variant === 'user'
                ? resolved.displayName || resolved.id
                : resolved.name || resolved.id
            : '';
        uiStore.openDialog({
            type: `previous-instances-${variant}`,
            id: dialogId,
            label,
            skipBreadcrumb: options.skipBreadcrumb
        });
    }

    function updateCurrentInstanceWorld() {
        let L;
        let instanceId = locationStore.lastLocation.location;
        if (locationStore.lastLocation.location === 'traveling') {
            instanceId = locationStore.lastLocationDestination;
        }
        if (!instanceId) {
            currentInstanceWorld.value = {
                ref: {},
                instance: {},
                isPC: false,
                isQuest: false,
                isIos: false,
                avatarScalingDisabled: false,
                focusViewDisabled: false,
                inCache: false,
                cacheSize: '',
                bundleSizes: {},
                lastUpdated: ''
            };
            currentInstanceLocation.value = {};
        } else if (instanceId !== currentInstanceLocation.value.tag) {
            currentInstanceWorld.value = {
                ref: {},
                instance: {},
                isPC: false,
                isQuest: false,
                isIos: false,
                avatarScalingDisabled: false,
                focusViewDisabled: false,
                inCache: false,
                cacheSize: '',
                bundleSizes: {},
                lastUpdated: ''
            };
            L = parseLocation(instanceId);
            currentInstanceLocation.value = L;
            worldRequest
                .getWorld({
                    worldId: L.worldId
                })
                .then((args) => {
                    currentInstanceWorld.value.ref = args.ref;
                    const { isPC, isQuest, isIos } = getAvailablePlatforms(
                        args.ref.unityPackages
                    );
                    currentInstanceWorld.value.isPC = isPC;
                    currentInstanceWorld.value.isQuest = isQuest;
                    currentInstanceWorld.value.isIos = isIos;
                    currentInstanceWorld.value.avatarScalingDisabled =
                        args.ref?.tags.includes(
                            'feature_avatar_scaling_disabled'
                        );
                    currentInstanceWorld.value.focusViewDisabled =
                        args.ref?.tags.includes('feature_focus_view_disabled');
                    checkVRChatCache(args.ref)
                        .then((cacheInfo) => {
                            if (cacheInfo.Item1 > 0) {
                                currentInstanceWorld.value.inCache = true;
                                currentInstanceWorld.value.cacheSize = `${(
                                    cacheInfo.Item1 / 1048576
                                ).toFixed(2)} MB`;
                            }
                        })
                        .catch((error) => {
                            console.error(
                                'Error checking VRChat cache:',
                                error
                            );
                        });
                    getBundleDateSize(args.ref)
                        .then((bundleSizes) => {
                            currentInstanceWorld.value.bundleSizes =
                                bundleSizes;
                        })
                        .catch((error) => {
                            console.error(
                                'Error fetching bundle sizes:',
                                error
                            );
                        });
                    return args;
                })
                .catch((error) => {
                    console.error('Error fetching world data:', error);
                });
        } else {
            worldRequest
                .getCachedWorld({
                    worldId: currentInstanceLocation.value.worldId
                })
                .then((args) => {
                    currentInstanceWorld.value.ref = args.ref;
                    const { isPC, isQuest, isIos } = getAvailablePlatforms(
                        args.ref.unityPackages
                    );
                    currentInstanceWorld.value.isPC = isPC;
                    currentInstanceWorld.value.isQuest = isQuest;
                    currentInstanceWorld.value.isIos = isIos;
                    checkVRChatCache(args.ref).then((cacheInfo) => {
                        if (cacheInfo.Item1 > 0) {
                            currentInstanceWorld.value.inCache = true;
                            currentInstanceWorld.value.cacheSize = `${(
                                cacheInfo.Item1 / 1048576
                            ).toFixed(2)} MB`;
                        }
                    });
                });
        }
        if (isRealInstance(instanceId)) {
            const ref = cachedInstances.get(instanceId);
            if (typeof ref !== 'undefined') {
                currentInstanceWorld.value.instance = ref;
            } else {
                L = parseLocation(instanceId);
                if (L.isRealInstance) {
                    instanceRequest
                        .getInstance({
                            worldId: L.worldId,
                            instanceId: L.instanceId
                        })
                        .then((args) => {
                            currentInstanceWorld.value.instance = args.ref;
                        })
                        .catch((error) => {
                            console.error(
                                'Error fetching instance data:',
                                error
                            );
                        });
                }
            }
        }
    }

    /**
     *
     * @param {object} json
     * @returns {object} ref
     */
    function applyInstance(json) {
        if (!json?.id) {
            return null;
        }
        if (!json.$fetchedAt) {
            json.$fetchedAt = new Date().toJSON();
        }
        let ref = cachedInstances.get(json.id);
        if (typeof ref === 'undefined') {
            ref = {
                id: '',
                location: '',
                instanceId: '',
                name: '',
                worldId: '',
                type: '',
                ownerId: '',
                tags: [],
                active: false,
                full: false,
                n_users: 0,
                hasCapacityForYou: true, // not present depending on endpoint
                capacity: 0,
                recommendedCapacity: 0,
                userCount: 0,
                queueEnabled: false, // only present with group instance type
                queueSize: 0, // only present when queuing is enabled
                platforms: {},
                gameServerVersion: 0,
                hardClose: null, // boolean or null
                closedAt: null, // string or null
                secureName: '',
                shortName: '',
                world: {},
                users: [], // only present when you're the owner
                clientNumber: '',
                contentSettings: {},
                photonRegion: '',
                region: '',
                canRequestInvite: false,
                permanent: false,
                private: '', // part of instance tag
                hidden: '', // part of instance tag
                nonce: '', // only present when you're the owner
                strict: false, // deprecated
                displayName: null,
                groupAccessType: null, // only present with group instance type
                roleRestricted: false, // only present with group instance type
                instancePersistenceEnabled: null,
                playerPersistenceEnabled: null,
                ageGate: null,
                // VRCX
                $fetchedAt: '',
                $disabledContentSettings: [],
                ...json
            };
            cachedInstances.set(ref.id, ref);
            cleanInstanceCache();
        } else {
            Object.assign(ref, json);
        }
        ref.$location = parseLocation(ref.location);
        if (ref.world?.id) {
            worldRequest
                .getCachedWorld({
                    worldId: ref.world.id
                })
                .then((args) => {
                    ref.world = args.ref;
                    return args;
                });
        }
        ref.$disabledContentSettings = [];
        if (json.contentSettings && Object.keys(json.contentSettings).length) {
            for (const setting of instanceContentSettings) {
                if (
                    typeof json.contentSettings[setting] === 'undefined' ||
                    json.contentSettings[setting] === true
                ) {
                    continue;
                }
                ref.$disabledContentSettings.push(setting);
            }
        }
        if (ref.displayName) {
            ref.displayName = replaceBioSymbols(ref.displayName);
        }
        if (
            userStore.userDialog.visible &&
            userStore.userDialog.ref?.$location?.tag === ref.id
        ) {
            userStore.applyUserDialogLocation();
        }
        if (
            worldStore.worldDialog.visible &&
            worldStore.worldDialog.id === ref.worldId
        ) {
            applyWorldDialogInstances();
        }
        if (
            groupStore.groupDialog.visible &&
            groupStore.groupDialog.id === ref.ownerId
        ) {
            applyGroupDialogInstances();
        }
        for (const groupInstance of groupStore.groupInstances) {
            if (groupInstance.instance.id === ref.id) {
                groupInstance.instance = ref;
            }
        }
        lastInstanceApplied.value = ref.id;
        return ref;
    }

    async function getInstanceName(location) {
        let instanceName = '';

        const L = parseLocation(location);
        if (L.isRealInstance && L.worldId && L.instanceId) {
            try {
                const args = await instanceRequest.getCachedInstance({
                    worldId: L.worldId,
                    instanceId: L.instanceId
                });
                instanceName = args.ref.displayName;
            } catch (e) {
                console.error('getInstanceName failed location', location, e);
            }
        }

        return instanceName;
    }

    /**
     *
     * @param {string} worldId
     * @param {any} options
     * @returns {Promise<{json: *, params}|null>}
     */
    async function createNewInstance(worldId = '', options) {
        let D = options;

        if (!D) {
            D = {
                loading: false,
                accessType: await configRepository.getString(
                    'instanceDialogAccessType',
                    'public'
                ),
                region: await configRepository.getString(
                    'instanceRegion',
                    'US West'
                ),
                worldId: worldId,
                groupId: await configRepository.getString(
                    'instanceDialogGroupId',
                    ''
                ),
                groupAccessType: await configRepository.getString(
                    'instanceDialogGroupAccessType',
                    'plus'
                ),
                ageGate: await configRepository.getBool(
                    'instanceDialogAgeGate',
                    false
                ),
                queueEnabled: await configRepository.getBool(
                    'instanceDialogQueueEnabled',
                    true
                ),
                roleIds: [],
                groupRef: {}
            };
        }

        let type = 'public';
        let canRequestInvite = false;
        switch (D.accessType) {
            case 'friends':
                type = 'friends';
                break;
            case 'friends+':
                type = 'hidden';
                break;
            case 'invite':
                type = 'private';
                break;
            case 'invite+':
                type = 'private';
                canRequestInvite = true;
                break;
            case 'group':
                type = 'group';
                break;
        }
        let region = 'us';
        if (D.region === 'US East') {
            region = 'use';
        } else if (D.region === 'Europe') {
            region = 'eu';
        } else if (D.region === 'Japan') {
            region = 'jp';
        }
        const params = {
            type,
            canRequestInvite,
            worldId: D.worldId,
            ownerId: userStore.currentUser.id,
            region
        };
        if (type === 'group') {
            params.groupAccessType = D.groupAccessType;
            params.ownerId = D.groupId;
            params.queueEnabled = D.queueEnabled;
            if (D.groupAccessType === 'members') {
                params.roleIds = D.roleIds;
            }
        }
        if (
            D.ageGate &&
            type === 'group' &&
            hasGroupPermission(D.groupRef, 'group-instance-age-gated-create')
        ) {
            params.ageGate = true;
        }
        if (D.displayName) {
            params.displayName = D.displayName;
        }
        try {
            const args = await instanceRequest.createInstance(params);
            return args;
        } catch (err) {
            console.error(err);
            return null;
        }
    }

    function applyWorldDialogInstances() {
        let ref;
        let instance;
        const D = worldStore.worldDialog;
        if (!D.visible) {
            return;
        }
        const instances = {};
        if (D.ref.instances) {
            for (instance of D.ref.instances) {
                // instance = [ instanceId, occupants ]
                const instanceId = instance[0];
                instances[instanceId] = {
                    id: instanceId,
                    tag: `${D.id}:${instanceId}`,
                    $location: {},
                    friendCount: 0,
                    users: [],
                    shortName: '',
                    ref: {}
                };
            }
        }
        const { instanceId, shortName } = D?.$location || {};
        if (instanceId && typeof instances[instanceId] === 'undefined') {
            instances[instanceId] = {
                id: instanceId,
                tag: `${D.id}:${instanceId}`,
                $location: {},
                friendCount: 0,
                users: [],
                shortName,
                ref: {}
            };
        }
        const cachedCurrentUser = userStore.cachedUsers.get(
            userStore.currentUser.id
        );
        const lastLocation$ = cachedCurrentUser?.$location;
        const playersInInstance = locationStore.lastLocation.playerList;
        if (lastLocation$?.worldId === D.id && playersInInstance.size > 0) {
            // pull instance json from cache
            const friendsInInstance = locationStore.lastLocation.friendList;
            instance = {
                id: lastLocation$.instanceId,
                tag: lastLocation$.tag,
                $location: {},
                friendCount: friendsInInstance.size,
                users: [],
                shortName: '',
                ref: {}
            };
            instances[instance.id] = instance;
            for (const friend of friendsInInstance.values()) {
                // if friend isn't in instance add them
                const addUser = !instance.users.some(function (user) {
                    return friend.userId === user.id;
                });
                if (addUser) {
                    ref = userStore.cachedUsers.get(friend.userId);
                    if (typeof ref !== 'undefined') {
                        instance.users.push(ref);
                    }
                }
            }
        }
        for (const friend of friendStore.friends.values()) {
            const { ref } = friend;
            if (
                typeof ref === 'undefined' ||
                typeof ref.$location === 'undefined' ||
                ref.$location.worldId !== D.id ||
                (ref.$location.instanceId === lastLocation$?.instanceId &&
                    playersInInstance.size > 0 &&
                    ref.location !== 'traveling')
            ) {
                continue;
            }
            if (ref.location === locationStore.lastLocation.location) {
                // don't add friends to currentUser gameLog instance (except when traveling)
                continue;
            }
            const { instanceId } = ref.$location;
            instance = instances[instanceId];
            if (typeof instance === 'undefined') {
                instance = {
                    id: instanceId,
                    tag: `${D.id}:${instanceId}`,
                    $location: {},
                    friendCount: 0,
                    users: [],
                    shortName: '',
                    ref: {}
                };
                instances[instanceId] = instance;
            }
            instance.users.push(ref);
        }
        ref = userStore.cachedUsers.get(userStore.currentUser.id);
        if (typeof ref !== 'undefined' && ref.$location.worldId === D.id) {
            const { instanceId } = ref.$location;
            instance = instances[instanceId];
            if (typeof instance === 'undefined') {
                instance = {
                    id: instanceId,
                    tag: `${D.id}:${instanceId}`,
                    $location: {},
                    friendCount: 0,
                    users: [],
                    shortName: '',
                    ref: {}
                };
                instances[instanceId] = instance;
            }
            instance.users.push(ref); // add self
        }
        const rooms = [];
        for (instance of Object.values(instances)) {
            // due to references on callback of API.getUser()
            // this should be block scope variable
            const L = parseLocation(`${D.id}:${instance.id}`);
            instance.location = L.tag;
            if (!L.shortName) {
                L.shortName = instance.shortName;
            }
            instance.$location = L;
            L.user = {};
            if (L.userId) {
                ref = userStore.cachedUsers.get(L.userId);
                if (typeof ref === 'undefined') {
                    userRequest
                        .getUser({
                            userId: L.userId
                        })
                        .then((args) => {
                            if (args.ref.id === L.userId) {
                                Object.assign(L.user, args.ref);
                                instance.$location = L;
                                applyWorldDialogInstances();
                            }
                        });
                } else {
                    L.user = ref;
                }
            }
            if (instance.friendCount === 0) {
                instance.friendCount = instance.users.length;
            }
            if (appearanceSettingsStore.instanceUsersSortAlphabetical) {
                instance.users.sort(compareByDisplayName);
            } else {
                instance.users.sort(compareByLocationAt);
            }
            rooms.push(instance);
        }
        // get instance from cache
        for (const room of rooms) {
            ref = cachedInstances.get(room.tag);
            if (typeof ref !== 'undefined') {
                Object.assign(room.ref, ref);
            }
        }
        rooms.sort(function (a, b) {
            // sort selected and current instance to top
            if (
                b.location === D.$location.tag ||
                b.location === lastLocation$?.tag
            ) {
                // sort selected instance above current instance
                if (a.location === D.$location.tag) {
                    return -1;
                }
                return 1;
            }
            if (
                a.location === D.$location.tag ||
                a.location === lastLocation$?.tag
            ) {
                // sort selected instance above current instance
                if (b.location === D.$location.tag) {
                    return 1;
                }
                return -1;
            }
            // sort by number of users when no friends in instance
            if (
                a.users.length === 0 &&
                b.users.length === 0 &&
                a.ref?.userCount !== b.ref?.userCount
            ) {
                if (a.ref?.userCount < b.ref?.userCount) {
                    return 1;
                }
                return -1;
            }
            // sort by number of friends in instance
            if (a.users.length !== b.users.length) {
                if (a.users.length < b.users.length) {
                    return 1;
                }
                return -1;
            }
            // sort by id
            return compareById(a, b);
        });
        D.rooms = rooms;
    }

    /**
     *
     * @param {object} inputInstances
     */
    function applyGroupDialogInstances(inputInstances) {
        let ref;
        let instance;
        const D = groupStore.groupDialog;
        if (!D.visible) {
            return;
        }
        const instances = {};
        for (instance of D.instances) {
            instances[instance.tag] = {
                ...instance,
                friendCount: 0,
                users: []
            };
        }
        if (typeof inputInstances !== 'undefined') {
            for (instance of inputInstances) {
                instances[instance.location] = {
                    id: instance.instanceId,
                    tag: instance.location,
                    $location: {},
                    friendCount: 0,
                    users: [],
                    shortName: instance.shortName,
                    ref: instance
                };
            }
        }
        const cachedCurrentUser = userStore.cachedUsers.get(
            userStore.currentUser.id
        );
        const lastLocation$ = cachedCurrentUser?.$location;
        const currentLocation = lastLocation$?.tag;
        const playersInInstance = locationStore.lastLocation.playerList;
        if (lastLocation$?.groupId === D.id && playersInInstance.size > 0) {
            const friendsInInstance = locationStore.lastLocation.friendList;
            instance = {
                id: lastLocation$.instanceId,
                tag: currentLocation,
                $location: {},
                friendCount: friendsInInstance.size,
                users: [],
                shortName: '',
                ref: {}
            };
            instances[currentLocation] = instance;
            for (const friend of friendsInInstance.values()) {
                // if friend isn't in instance add them
                const addUser = !instance.users.some(function (user) {
                    return friend.userId === user.id;
                });
                if (addUser) {
                    ref = userStore.cachedUsers.get(friend.userId);
                    if (typeof ref !== 'undefined') {
                        instance.users.push(ref);
                    }
                }
            }
        }
        for (const friend of friendStore.friends.values()) {
            const { ref } = friend;
            if (
                typeof ref === 'undefined' ||
                typeof ref.$location === 'undefined' ||
                ref.$location.groupId !== D.id ||
                (ref.$location.instanceId === lastLocation$?.instanceId &&
                    playersInInstance.size > 0 &&
                    ref.location !== 'traveling')
            ) {
                continue;
            }
            if (ref.location === locationStore.lastLocation.location) {
                // don't add friends to currentUser gameLog instance (except when traveling)
                continue;
            }
            const { instanceId, tag } = ref.$location;
            instance = instances[tag];
            if (typeof instance === 'undefined') {
                instance = {
                    id: instanceId,
                    tag,
                    $location: {},
                    friendCount: 0,
                    users: [],
                    shortName: '',
                    ref: {}
                };
                instances[tag] = instance;
            }
            instance.users.push(ref);
        }
        ref = userStore.cachedUsers.get(userStore.currentUser.id);
        if (typeof ref !== 'undefined' && ref.$location.groupId === D.id) {
            const { instanceId, tag } = ref.$location;
            instance = instances[tag];
            if (typeof instance === 'undefined') {
                instance = {
                    id: instanceId,
                    tag,
                    $location: {},
                    friendCount: 0,
                    users: [],
                    shortName: '',
                    ref: {}
                };
                instances[tag] = instance;
            }
            instance.users.push(ref); // add self
        }
        const rooms = [];
        for (instance of Object.values(instances)) {
            // due to references on callback of API.getUser()
            // this should be block scope variable
            const L = parseLocation(instance.tag);
            instance.location = instance.tag;
            instance.$location = L;
            if (instance.friendCount === 0) {
                instance.friendCount = instance.users.length;
            }
            if (appearanceSettingsStore.instanceUsersSortAlphabetical) {
                instance.users.sort(compareByDisplayName);
            } else {
                instance.users.sort(compareByLocationAt);
            }
            rooms.push(instance);
        }
        // get instance
        for (const room of rooms) {
            ref = cachedInstances.get(room.tag);
            if (typeof ref !== 'undefined') {
                Object.assign(room.ref, ref);
            } else if (isRealInstance(room.tag)) {
                instanceRequest.getInstance({
                    worldId: room.$location.worldId,
                    instanceId: room.$location.instanceId
                });
            }
        }
        rooms.sort(function (a, b) {
            // sort current instance to top
            if (b.location === currentLocation) {
                return 1;
            }
            if (a.location === currentLocation) {
                return -1;
            }
            // sort by number of users when no friends in instance
            if (a.users.length === 0 && b.users.length === 0) {
                if (a.ref?.userCount < b.ref?.userCount) {
                    return 1;
                }
                return -1;
            }
            // sort by number of friends in instance
            if (a.users.length < b.users.length) {
                return 1;
            }
            return -1;
        });
        D.instances = rooms;
    }

    function removeAllQueuedInstances() {
        queuedInstances.forEach((ref) => {
            toast.info(`Removed instance ${ref.$worldName} from queue`);
            toast.dismiss(ref.$msgBox);
        });
        queuedInstances.clear();
    }

    /**
     *
     * @param {string} instanceId
     */
    function removeQueuedInstance(instanceId) {
        const ref = queuedInstances.get(instanceId);
        if (typeof ref !== 'undefined') {
            toast.dismiss(ref.$msgBox);
            queuedInstances.delete(instanceId);
        }
    }

    /**
     *
     * @param {string} instanceId
     */
    function applyQueuedInstance(instanceId) {
        queuedInstances.forEach((ref) => {
            if (ref.location !== instanceId) {
                toast.info(
                    t('message.instance.removed_form_queue', {
                        worldName: ref.$worldName
                    })
                );
                toast.dismiss(ref.$msgBox);
                queuedInstances.delete(ref.location);
            }
        });
        if (!instanceId) {
            return;
        }
        if (!queuedInstances.has(instanceId)) {
            const L = parseLocation(instanceId);
            if (L.isRealInstance) {
                instanceRequest
                    .getInstance({
                        worldId: L.worldId,
                        instanceId: L.instanceId
                    })
                    .then((args) => {
                        if (args.json?.queueSize) {
                            instanceQueueUpdate(
                                instanceId,
                                args.json?.queueSize,
                                args.json?.queueSize
                            );
                        }
                    })
                    .catch((error) => {
                        console.error(
                            'Error fetching instance data for queue:',
                            error
                        );
                    });
            }
            instanceQueueUpdate(instanceId, 0, 0);
        }
    }

    /**
     *
     * @param {string} instanceId
     */
    function instanceQueueReady(instanceId) {
        const ref = queuedInstances.get(instanceId);
        if (typeof ref !== 'undefined') {
            toast.dismiss(ref.$msgBox);
            queuedInstances.delete(instanceId);
        }
        const L = parseLocation(instanceId);
        const group = groupStore.cachedGroups.get(L.groupId);
        const groupName = group?.name ?? '';
        const worldName = ref?.$worldName ?? '';
        const location = displayLocation(instanceId, worldName, groupName);
        toast.success(`Instance ready to join ${location}`);
        const noty = {
            created_at: new Date().toJSON(),
            type: 'group.queueReady',
            imageUrl: group?.iconUrl,
            message: `Instance ready to join ${location}`,
            location: instanceId,
            senderUserId: L.groupId,
            groupName,
            worldName
        };
        if (
            notificationStore.notificationTable.filters[0].value.length === 0 ||
            notificationStore.notificationTable.filters[0].value.includes(
                noty.type
            )
        ) {
            uiStore.notifyMenu('notification');
        }
        notificationStore.queueNotificationNoty(noty);
        sharedFeedStore.addEntry(noty);
        notificationStore.notificationTable.data.push(noty);
    }

    /**
     *
     * @param {string} instanceId
     * @param {number} position
     * @param {number} queueSize
     * @returns {Promise<void>}
     */
    async function instanceQueueUpdate(instanceId, position, queueSize) {
        let ref = queuedInstances.get(instanceId);
        if (typeof ref === 'undefined') {
            ref = {
                $msgBox: null,
                $groupName: '',
                $worldName: '',
                location: instanceId,
                position: 0,
                queueSize: 0,
                updatedAt: 0
            };
        }
        ref.position = position;
        ref.queueSize = queueSize;
        ref.updatedAt = Date.now();
        if (!ref.$groupName) {
            ref.$groupName = await getGroupName(instanceId);
        }
        if (!ref.$worldName) {
            ref.$worldName = await getWorldName(instanceId);
        }
        const location = displayLocation(
            instanceId,
            ref.$worldName,
            ref.$groupName
        );
        toast.dismiss(ref.$msgBox ?? undefined);
        ref.$msgBox = toast.info(
            `You are in position ${ref.position} of ${ref.queueSize} in the queue for ${location} `,
            {
                duration: Infinity,
                position: 'bottom-right',
                closeButton: true,
                class: 'vrc-instance-queue-message'
            }
        );
        queuedInstances.set(instanceId, ref);
        // workerTimers.setTimeout(this.instanceQueueTimeout, 3600000);
    }

    function getCurrentInstanceUserList() {
        if (!watchState.isFriendsLoaded) {
            return;
        }
        if (state.updatePlayerListTimer) {
            state.updatePlayerListPending = true;
        } else {
            updatePlayerListExecute();
            state.updatePlayerListTimer = setTimeout(() => {
                if (state.updatePlayerListPending) {
                    updatePlayerListExecute();
                }
                state.updatePlayerListTimer = null;
            }, 150);
        }
    }

    function updatePlayerListExecute() {
        try {
            updatePlayerListDebounce();
        } catch (err) {
            console.error(err);
        }
        state.updatePlayerListTimer = null;
        state.updatePlayerListPending = false;
    }

    function updatePlayerListDebounce() {
        const users = [];
        const pushUser = function (ref) {
            let photonId = -1;
            let isFriend = false;
            let isBlocked = false;
            let isMuted = false;
            let isAvatarInteractionDisabled = false;
            let isChatBoxMuted = false;
            let ageVerified = false;
            photonStore.photonLobbyCurrent.forEach((ref1, id) => {
                if (typeof ref1 !== 'undefined') {
                    if (
                        (typeof ref.id !== 'undefined' &&
                            typeof ref1.id !== 'undefined' &&
                            ref1.id === ref.id) ||
                        (typeof ref.displayName !== 'undefined' &&
                            typeof ref1.displayName !== 'undefined' &&
                            ref1.displayName === ref.displayName)
                    ) {
                        photonId = id;
                    }
                }
            });
            let isMaster = false;
            if (
                photonStore.photonLobbyMaster !== 0 &&
                photonId === photonStore.photonLobbyMaster
            ) {
                isMaster = true;
            }
            let isModerator = false;
            const lobbyJointime = photonStore.photonLobbyJointime.get(photonId);
            let inVRMode = null;
            let groupOnNameplate = '';
            if (typeof lobbyJointime !== 'undefined') {
                inVRMode = lobbyJointime.inVRMode;
                groupOnNameplate = lobbyJointime.groupOnNameplate;
                isModerator = lobbyJointime.canModerateInstance;
            }
            // if (groupOnNameplate) {
            //     API.getCachedGroup({
            //         groupId: groupOnNameplate
            //     }).then((args) => {
            //         groupOnNameplate = args.ref.name;
            //     });
            // }
            let timeoutTime = 0;
            if (typeof ref.id !== 'undefined') {
                isFriend = ref.isFriend;
                if (
                    photonStore.timeoutHudOverlayFilter === 'VIP' ||
                    photonStore.timeoutHudOverlayFilter === 'Friends'
                ) {
                    photonStore.photonLobbyTimeout.forEach((ref1) => {
                        if (ref1.userId === ref.id) {
                            timeoutTime = ref1.time;
                        }
                    });
                } else {
                    photonStore.photonLobbyTimeout.forEach((ref1) => {
                        if (ref1.displayName === ref.displayName) {
                            timeoutTime = ref1.time;
                        }
                    });
                }
                isBlocked = ref.$moderations.isBlocked;
                isMuted = ref.$moderations.isMuted;
                isAvatarInteractionDisabled =
                    ref.$moderations.isAvatarInteractionDisabled;
                isChatBoxMuted = ref.$moderations.isChatBoxMuted;
                ageVerified = ref.ageVerificationStatus === '18+';
            }
            users.push({
                ref,
                displayName: ref.displayName,
                timer: ref.$location_at,
                $trustSortNum: ref.$trustSortNum ?? 0,
                photonId,
                isMaster,
                isModerator,
                inVRMode,
                groupOnNameplate,
                isFriend,
                timeoutTime,
                isBlocked,
                isMuted,
                isAvatarInteractionDisabled,
                isChatBoxMuted,
                ageVerified
            });
        };

        const playersInInstance = locationStore.lastLocation.playerList;
        if (playersInInstance.size > 0) {
            let ref = userStore.cachedUsers.get(userStore.currentUser.id);
            if (typeof ref !== 'undefined' && playersInInstance.has(ref.id)) {
                pushUser(ref);
            }
            for (const player of playersInInstance.values()) {
                // if friend isn't in instance add them
                if (player.displayName === userStore.currentUser.displayName) {
                    continue;
                }
                const addUser = !users.some(function (user) {
                    return player.displayName === user.displayName;
                });
                if (addUser) {
                    ref = userStore.cachedUsers.get(player.userId);
                    if (typeof ref !== 'undefined') {
                        pushUser(ref);
                    } else {
                        let { joinTime } =
                            locationStore.lastLocation.playerList.get(
                                player.userId
                            );
                        if (!joinTime) {
                            joinTime = Date.now();
                        }
                        ref = {
                            // if userId is missing just push displayName
                            displayName: player.displayName,
                            $location_at: joinTime,
                            $online_for: joinTime
                        };
                        pushUser(ref);
                    }
                }
            }
        }
        currentInstanceUsersData.value = users;
    }

    // $app.methods.instanceQueueClear = function () {
    //     // remove all instances from queue
    //     queuedInstances.forEach((ref) => {
    //         toast.dismiss(ref.$msgBox);
    //         queuedInstances.delete(ref.location);
    //     });
    // };

    return {
        state,

        cachedInstances,
        lastInstanceApplied,
        currentInstanceWorld,
        currentInstanceLocation,
        queuedInstances,
        previousInstancesInfoDialog,
        previousInstancesListDialog,
        previousInstancesListState,
        instanceJoinHistory,
        currentInstanceUsersData,

        applyInstance,
        updateCurrentInstanceWorld,
        createNewInstance,
        applyWorldDialogInstances,
        applyGroupDialogInstances,
        removeAllQueuedInstances,
        removeQueuedInstance,
        applyQueuedInstance,
        instanceQueueReady,
        instanceQueueUpdate,
        hidePreviousInstancesDialogs,
        showPreviousInstancesInfoDialog,
        showPreviousInstancesListDialog,
        addInstanceJoinHistory,
        getCurrentInstanceUserList,
        getInstanceJoinHistory,
        getInstanceName
    };
});
