/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.

SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/

import { type Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";

import { type SortAlgorithm } from "../models";
import { sortRoomsWithAlgorithm } from "../tag-sorting";
import { OrderingAlgorithm } from "./OrderingAlgorithm";
import { type TagID } from "../../../room-list-v3/skip-list/tag";
import { RoomUpdateCause } from "../../models";
import { RoomNotificationStateStore } from "../../../notifications/RoomNotificationStateStore";

type NaturalCategorizedRoomMap = {
    defaultRooms: Room[];
    mutedRooms: Room[];
};

/**
 * Uses the natural tag sorting algorithm order to determine tag ordering. No
 * additional behavioural changes are present.
 */
export class NaturalAlgorithm extends OrderingAlgorithm {
    private cachedCategorizedOrderedRooms: NaturalCategorizedRoomMap = {
        defaultRooms: [],
        mutedRooms: [],
    };
    public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
        super(tagId, initialSortingAlgorithm);
    }

    public setRooms(rooms: Room[]): void {
        const { defaultRooms, mutedRooms } = this.categorizeRooms(rooms);

        this.cachedCategorizedOrderedRooms = {
            defaultRooms: sortRoomsWithAlgorithm(defaultRooms, this.tagId, this.sortingAlgorithm),
            mutedRooms: sortRoomsWithAlgorithm(mutedRooms, this.tagId, this.sortingAlgorithm),
        };
        this.buildCachedOrderedRooms();
    }

    public handleRoomUpdate(room: Room, cause: RoomUpdateCause): boolean {
        const isSplice = cause === RoomUpdateCause.NewRoom || cause === RoomUpdateCause.RoomRemoved;
        const isInPlace =
            cause === RoomUpdateCause.Timeline ||
            cause === RoomUpdateCause.ReadReceipt ||
            cause === RoomUpdateCause.PossibleMuteChange;
        const isMuted = this.isMutedToBottom && this.getRoomIsMuted(room);

        if (!isSplice && !isInPlace) {
            throw new Error(`Unsupported update cause: ${cause}`);
        }

        if (cause === RoomUpdateCause.NewRoom) {
            if (isMuted) {
                this.cachedCategorizedOrderedRooms.mutedRooms = sortRoomsWithAlgorithm(
                    [...this.cachedCategorizedOrderedRooms.mutedRooms, room],
                    this.tagId,
                    this.sortingAlgorithm,
                );
            } else {
                this.cachedCategorizedOrderedRooms.defaultRooms = sortRoomsWithAlgorithm(
                    [...this.cachedCategorizedOrderedRooms.defaultRooms, room],
                    this.tagId,
                    this.sortingAlgorithm,
                );
            }
            this.buildCachedOrderedRooms();
            return true;
        } else if (cause === RoomUpdateCause.RoomRemoved) {
            return this.removeRoom(room);
        } else if (cause === RoomUpdateCause.PossibleMuteChange) {
            if (this.isMutedToBottom) {
                return this.onPossibleMuteChange(room);
            } else {
                return false;
            }
        }

        // TODO: Optimize this to avoid useless operations: https://github.com/vector-im/element-web/issues/14457
        // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
        if (isMuted) {
            this.cachedCategorizedOrderedRooms.mutedRooms = sortRoomsWithAlgorithm(
                this.cachedCategorizedOrderedRooms.mutedRooms,
                this.tagId,
                this.sortingAlgorithm,
            );
        } else {
            this.cachedCategorizedOrderedRooms.defaultRooms = sortRoomsWithAlgorithm(
                this.cachedCategorizedOrderedRooms.defaultRooms,
                this.tagId,
                this.sortingAlgorithm,
            );
        }
        this.buildCachedOrderedRooms();
        return true;
    }

    /**
     * Remove a room from the cached room list
     * @param room Room to remove
     * @returns {boolean} true when room list should update as result
     */
    private removeRoom(room: Room): boolean {
        const defaultIndex = this.cachedCategorizedOrderedRooms.defaultRooms.findIndex((r) => r.roomId === room.roomId);
        if (defaultIndex > -1) {
            this.cachedCategorizedOrderedRooms.defaultRooms.splice(defaultIndex, 1);
            this.buildCachedOrderedRooms();
            return true;
        }
        const mutedIndex = this.cachedCategorizedOrderedRooms.mutedRooms.findIndex((r) => r.roomId === room.roomId);
        if (mutedIndex > -1) {
            this.cachedCategorizedOrderedRooms.mutedRooms.splice(mutedIndex, 1);
            this.buildCachedOrderedRooms();
            return true;
        }

        logger.warn(`Tried to remove unknown room from ${this.tagId}: ${room.roomId}`);
        // room was not in cached lists, no update
        return false;
    }

    /**
     * Sets cachedOrderedRooms from cachedCategorizedOrderedRooms
     */
    private buildCachedOrderedRooms(): void {
        this.cachedOrderedRooms = [
            ...this.cachedCategorizedOrderedRooms.defaultRooms,
            ...this.cachedCategorizedOrderedRooms.mutedRooms,
        ];
    }

    private getRoomIsMuted(room: Room): boolean {
        // It's fine for us to call this a lot because it's cached, and we shouldn't be
        // wasting anything by doing so as the store holds single references
        const state = RoomNotificationStateStore.instance.getRoomState(room);
        return state.muted;
    }

    private categorizeRooms(rooms: Room[]): NaturalCategorizedRoomMap {
        if (!this.isMutedToBottom) {
            return { defaultRooms: rooms, mutedRooms: [] };
        }
        return rooms.reduce<NaturalCategorizedRoomMap>(
            (acc, room: Room) => {
                if (this.getRoomIsMuted(room)) {
                    acc.mutedRooms.push(room);
                } else {
                    acc.defaultRooms.push(room);
                }
                return acc;
            },
            { defaultRooms: [], mutedRooms: [] } as NaturalCategorizedRoomMap,
        );
    }

    private onPossibleMuteChange(room: Room): boolean {
        const isMuted = this.getRoomIsMuted(room);
        if (isMuted) {
            const defaultIndex = this.cachedCategorizedOrderedRooms.defaultRooms.findIndex(
                (r) => r.roomId === room.roomId,
            );

            // room has been muted
            if (defaultIndex > -1) {
                // remove from the default list
                this.cachedCategorizedOrderedRooms.defaultRooms.splice(defaultIndex, 1);
                // add to muted list and reorder
                this.cachedCategorizedOrderedRooms.mutedRooms = sortRoomsWithAlgorithm(
                    [...this.cachedCategorizedOrderedRooms.mutedRooms, room],
                    this.tagId,
                    this.sortingAlgorithm,
                );
                // rebuild
                this.buildCachedOrderedRooms();
                return true;
            }
        } else {
            const mutedIndex = this.cachedCategorizedOrderedRooms.mutedRooms.findIndex((r) => r.roomId === room.roomId);

            // room has been unmuted
            if (mutedIndex > -1) {
                // remove from the muted list
                this.cachedCategorizedOrderedRooms.mutedRooms.splice(mutedIndex, 1);
                // add to default list and reorder
                this.cachedCategorizedOrderedRooms.defaultRooms = sortRoomsWithAlgorithm(
                    [...this.cachedCategorizedOrderedRooms.defaultRooms, room],
                    this.tagId,
                    this.sortingAlgorithm,
                );
                // rebuild
                this.buildCachedOrderedRooms();
                return true;
            }
        }

        return false;
    }
}
