import * as React from "react";
import {Ref, useEffect, useRef, useState} from "react";
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
import {AvatarRenderer} from "tc-shared/ui/react-elements/Avatar";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import {Countdown} from "tc-shared/ui/react-elements/Countdown";
import {
    AbstractConversationUiEvents,
    ChatEvent,
    ChatEventLocalAction,
    ChatEventLocalUserSwitch,
    ChatEventMessageSendFailed,
    ChatEventModeChanged,
    ChatEventPartnerAction,
    ChatEventPartnerInstanceChanged,
    ChatEventQueryFailed,
    ChatHistoryState,
    ChatMessage
} from "./AbstractConversationDefinitions";
import {TimestampRenderer} from "tc-shared/ui/react-elements/TimestampRenderer";
import {BBCodeRenderer} from "tc-shared/text/bbcode";
import {getGlobalAvatarManagerFactory} from "tc-shared/file/Avatars";
import {ColloquialFormat, date_format, format_date_general, formatDayTime} from "tc-shared/utils/DateUtils";
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
import {ChatBox} from "tc-shared/ui/react-elements/ChatBox";

const cssStyle = require("./AbstractConversationRenderer.scss");

const ChatMessageTextRenderer = React.memo((props: { text: string, handlerId: string }) => {
    if(typeof props.text !== "string") { debugger; }
    return <BBCodeRenderer settings={{ convertSingleUrls: true }} message={props.text || ""} handlerId={props.handlerId} />;
});

const ChatEventMessageRenderer = React.memo((props: {
    message: ChatMessage,
    callbackDelete?: () => void,
    events: Registry<AbstractConversationUiEvents>,
    handlerId: string,

    refHTMLElement?: Ref<HTMLDivElement>
}) => {
    let deleteButton;

    if(props.callbackDelete) {
        deleteButton = (
            <div className={cssStyle.delete} onClick={props.callbackDelete} >
                <img src="img/icon_conversation_message_delete.svg" alt={""} />
            </div>
        );
    }

    const avatar = getGlobalAvatarManagerFactory().getManager(props.handlerId)?.resolveClientAvatar({ clientUniqueId: props.message.sender_unique_id, database_id: props.message.sender_database_id });
    return (
        <div className={cssStyle.containerMessage} ref={props.refHTMLElement}>
            <div className={cssStyle.avatar}>
                <AvatarRenderer
                    className={cssStyle.imageContainer}
                    alt={""}
                    avatar={avatar} />
            </div>
            <div className={cssStyle.message}>
                <div className={cssStyle.info}>
                    {deleteButton}
                    <a className={cssStyle.sender}>
                        <ClientTag clientName={props.message.sender_name} clientUniqueId={props.message.sender_unique_id} handlerId={props.handlerId} clientDatabaseId={props.message.sender_database_id} />
                    </a>
                    <span> </span> { /* Only for copy purposes */}
                    <a className={cssStyle.timestamp}>
                        <TimestampRenderer timestamp={props.message.timestamp} />
                    </a>
                    <br /> { /* Only for copy purposes */ }
                </div>
                <div className={cssStyle.text}>
                    <ChatMessageTextRenderer text={props.message.message} handlerId={props.handlerId} />
                </div>
                <br style={{ content: " ", display: "none" }} /> { /* Only for copy purposes */ }
            </div>
        </div>
    );
});


const TimestampEntry = (props: { timestamp: Date, refDiv: React.Ref<HTMLDivElement> }) => {
    const diff = date_format(props.timestamp, new Date());
    let formatted;
    let update: boolean;

    if(diff == ColloquialFormat.YESTERDAY) {
        formatted = <Translatable key={"yesterday"}>Yesterday</Translatable>;
        update = true;
    } else if(diff == ColloquialFormat.TODAY) {
        formatted = <Translatable key={"today"}>Today</Translatable>;
        update = true;
    } else if(diff == ColloquialFormat.GENERAL) {
        formatted = <>{format_date_general(props.timestamp, false)}</>;
        update = false;
    }

    const [ revision, setRevision ] = useState(0);

    useEffect(() => {
        if(!update)
            return;

        const nextHour = new Date();
        nextHour.setUTCMilliseconds(0);
        nextHour.setUTCMinutes(0);
        nextHour.setUTCHours(nextHour.getUTCHours() + 1);

        const id = setTimeout(() => {
            setRevision(revision + 1);
        }, nextHour.getTime() - Date.now() + 10);
        return () => clearTimeout(id);
    });

    return (
        <div className={cssStyle.containerTimestamp} ref={props.refDiv}>
            {formatted}
        </div>
    );
};

const UnreadEntry = (props: { refDiv: React.Ref<HTMLDivElement> }) => (
    <div ref={props.refDiv} className={cssStyle.containerUnread}>
        <Translatable>Unread messages</Translatable>
    </div>
);

const LoadOderMessages = (props: { events: Registry<AbstractConversationUiEvents>, chatId: string, state: ChatHistoryState | "error", errorMessage?: string, retryTimestamp?: number, timestamp: number | undefined }) => {
    if(props.state === "none")
        return null;

    let innerMessage, onClick;
    if(props.state === "loading") {
        innerMessage = <><Translatable>loading older messages</Translatable> <LoadingDots /></>;
    } else if(props.state === "available") {
        const shouldThrottle = Date.now() < props.retryTimestamp;

        const [ revision, setRevision ] = useState(0);
        useEffect(() => {
            if(!shouldThrottle)
                return;

            const timeout = setTimeout(() => setRevision(revision + 1), props.retryTimestamp - Date.now());
            return () => clearTimeout(timeout);
        });

        if(shouldThrottle) {
            innerMessage = <React.Fragment key={"throttle"}>
                <Translatable>please wait</Translatable>&nbsp;
                <Countdown timestamp={props.retryTimestamp} finished={tr("1 second")} />
            </React.Fragment>;
        } else {
            onClick = props.state === "available" && props.timestamp ? () => props.events.fire("query_conversation_history", { chatId: props.chatId, timestamp: props.timestamp }) : undefined;
            innerMessage = <Translatable key={"can-load"}>Load older messages</Translatable>;
        }
    } else {
        innerMessage = (
            <>
                <Translatable>History query failed</Translatable> ({props.errorMessage})<br/>
                <Translatable>Try again in</Translatable> <Countdown timestamp={props.retryTimestamp} finished={tr("1 second")} />
            </>
        );
    }

    return (
        <div className={cssStyle.containerLoadMessages}>
            <div className={cssStyle.inner} onClick={onClick}>
                {innerMessage}
            </div>
        </div>
    )
};

const JumpToPresent = (props: { events: Registry<AbstractConversationUiEvents>, chatId: string }) => (
    <div
        className={cssStyle.containerLoadMessages + " " + cssStyle.present}
        onClick={() => props.events.fire("action_jump_to_present", { chatId: props.chatId })}
    >
        <div className={cssStyle.inner}>
            <Translatable>Jump to present</Translatable>
        </div>
    </div>
);

const ChatEventLocalUserSwitchRenderer = (props: { event: ChatEventLocalUserSwitch, timestamp: number, refHTMLElement: Ref<HTMLDivElement> }) => {
    return (
        <div className={cssStyle.containerSwitch} ref={props.refHTMLElement}>
            <a>
                {props.event.mode === "join" ? <Translatable>You joined at</Translatable> : <Translatable>You left at</Translatable>}
                &nbsp;
                {formatDayTime(new Date(props.timestamp))}
            </a>
            <div />
        </div>
    )
};

const ChatEventQueryFailedRenderer = (props: { event: ChatEventQueryFailed, refHTMLElement: Ref<HTMLDivElement> }) => {
    return (
        <div className={cssStyle.containerQueryFailed} ref={props.refHTMLElement}>
            <Translatable>failed to query history</Translatable>
            &nbsp;
            ({props.event.message})
        </div>
    )
};

const ChatEventMessageFailedRenderer = (props: { event: ChatEventMessageSendFailed, refHTMLElement: Ref<HTMLDivElement> }) => {
    if(props.event.error === "permission")
        return (
            <div className={cssStyle.containerMessageSendFailed} ref={props.refHTMLElement}>
                <Translatable>message send failed due to permission</Translatable>
                {" " + props.event.failedPermission}
            </div>
        );
    return (
        <div className={cssStyle.containerMessageSendFailed} ref={props.refHTMLElement}>
            <Translatable>failed to send message:</Translatable>
            &nbsp;
            {props.event.errorMessage || tr("Unknown error")}
        </div>
    )
};

const ChatEventPartnerInstanceChangedRenderer = (props: { event: ChatEventPartnerInstanceChanged, refHTMLElement: Ref<HTMLDivElement> }) => {
    return (
        <div className={cssStyle.containerPartnerInstanceChanged} ref={props.refHTMLElement}>
            <Translatable>You're&nbsp;now&nbsp;chatting&nbsp;with</Translatable>
            &nbsp;
            <a>{props.event.newClient}</a>
        </div>
    )
};

const ChatEventLocalActionRenderer = (props: { event: ChatEventLocalAction, refHTMLElement: Ref<HTMLDivElement> }) => {
    switch (props.event.action) {
        case "disconnect":
            return (
                <div className={cssStyle.containerLocalAction + " " + cssStyle.actionDisconnect} ref={props.refHTMLElement}>
                    <Translatable>You've disconnected from the server</Translatable>
                </div>
            );

        case "reconnect":
            return (
                <div className={cssStyle.containerLocalAction + " " + cssStyle.actionReconnect} ref={props.refHTMLElement}>
                    <Translatable>Chat reconnected</Translatable>
                </div>
            );
    }
};

const ChatEventPartnerActionRenderer = (props: { event: ChatEventPartnerAction, refHTMLElement: Ref<HTMLDivElement> }) => {
    switch (props.event.action) {
        case "close":
            return (
                <div className={cssStyle.containerPartnerAction + " " + cssStyle.actionClose} ref={props.refHTMLElement}>
                    <Translatable>Your chat partner has closed the conversation</Translatable>
                </div>
            );

        case "disconnect":
            return (
                <div className={cssStyle.containerPartnerAction + " " + cssStyle.actionDisconnect} ref={props.refHTMLElement}>
                    <Translatable>Your chat partner has disconnected</Translatable>
                </div>
            );

        case "reconnect":
            return (
                <div className={cssStyle.containerPartnerAction + " " + cssStyle.actionReconnect} ref={props.refHTMLElement}>
                    <Translatable>Your chat partner has reconnected</Translatable>
                </div>
            );
    }
    return null;
};

const ChatEventModeChangedRenderer = (props: { event: ChatEventModeChanged, refHTMLElement: Ref<HTMLDivElement> }) => {
    switch (props.event.newMode) {
        case "none":
            return (
                <div className={cssStyle.containerSwitch + " " + cssStyle.actionClose} ref={props.refHTMLElement}>
                    <a><Translatable>The conversation has been disabled</Translatable></a>
                    <div />
                </div>
            );

        case "private":
            return (
                <div className={cssStyle.containerSwitch + " " + cssStyle.actionClose} ref={props.refHTMLElement}>
                    <a><Translatable>The conversation has been made private</Translatable></a>
                    <div />
                </div>
            );

        case "normal":
            return (
                <div className={cssStyle.containerSwitch + " " + cssStyle.actionClose} ref={props.refHTMLElement}>
                    <a><Translatable>The conversation has been made public</Translatable></a>
                    <div />
                </div>
            );
    }
}

const PartnerTypingIndicator = (props: { events: Registry<AbstractConversationUiEvents>, chatId: string, timeout?: number }) => {
    const kTypingTimeout = props.timeout || 5000;


    const [ typingTimestamp, setTypingTimestamp ] = useState(0);
    props.events.reactUse("notify_partner_typing", event => {
        if(event.chatId !== props.chatId)
            return;

        setTypingTimestamp(Date.now());
    });

    props.events.reactUse("notify_chat_event", event => {
        if(event.chatId !== props.chatId)
            return;

        if(event.event.type === "message") {
            if(!event.event.isOwnMessage)
                setTypingTimestamp(0);
        } else if(event.event.type === "partner-action" || event.event.type === "local-action") {
            setTypingTimestamp(0);
        }
    });

    const isTyping = Date.now() - kTypingTimeout < typingTimestamp;

    useEffect(() => {
        if(!isTyping)
            return;

        const timeout = setTimeout(() => {
            setTypingTimestamp(0);
        }, kTypingTimeout);
        return () => clearTimeout(timeout);
    });

    return (
        <div className={cssStyle.containerPartnerTyping + (isTyping ? "" : " " + cssStyle.hidden)}>
            <Translatable>Partner is typing</Translatable> <LoadingDots enabled={isTyping} />
        </div>
    )
};

interface ConversationMessagesProperties {
    events: Registry<AbstractConversationUiEvents>;
    handlerId: string;

    noFirstMessageOverlay?: boolean
    messagesDeletable?: boolean;
}

interface ConversationMessagesState {
    mode: "normal" | "loading" | "error" | "private" | "no-permission" | "not-supported" | "unselected";

    errorMessage?: string;
    failedPermission?: string;

    historyState: ChatHistoryState | "error";
    historyErrorMessage?: string;
    historyRetryTimestamp?: number;

    isBrowsingHistory: boolean;
}

@ReactEventHandler<ConversationMessages>(e => e.props.events)
class ConversationMessages extends React.PureComponent<ConversationMessagesProperties, ConversationMessagesState> {
    private readonly refMessages = React.createRef<HTMLDivElement>();
    private readonly refUnread = React.createRef<HTMLDivElement>();
    private readonly refTimestamp = React.createRef<HTMLDivElement>();
    private readonly refScrollToNewMessages = React.createRef<HTMLDivElement>();
    private readonly refScrollElement = React.createRef<HTMLDivElement>();
    private readonly refFirstChatEvent = React.createRef<HTMLDivElement>();

    private scrollElementPreviousOffset = 0;
    private scrollOffset: number | "bottom" | "element";

    private currentChatId: "unselected" | string = "unselected";
    private chatEvents: ChatEvent[] = [];
    private showSwitchEvents: boolean = false;

    private scrollEventUniqueId: string;
    private viewElementIndex = 0;
    private viewEntries: React.ReactElement[] = [];

    private unreadTimestamp: undefined | number;

    private chatFrameMaxMessageCount: number;
    private chatFrameMaxHistoryMessageCount: number;
    private historyRetryTimer: number;

    private ignoreNextScroll: boolean = false;
    private scrollHistoryAutoLoadThrottle: number = 0;

    constructor(props) {
        super(props);

        this.state = {
            mode: "unselected",

            historyState: "available",
            isBrowsingHistory: false,

            historyRetryTimestamp: 0
        }
    }

    private scrollToBottom() {
        this.ignoreNextScroll = true;
        requestAnimationFrame(() => {
            this.ignoreNextScroll = false;
            if(this.scrollOffset !== "bottom")
                return;

            if(!this.refMessages.current)
                return;

            this.refMessages.current.scrollTop = this.refMessages.current.scrollHeight;
        });
    }

    private scrollToNewMessage() {
        this.ignoreNextScroll = true;
        requestAnimationFrame(() => {
            this.ignoreNextScroll = false;
            if(!this.refUnread.current)
                return;

            this.refMessages.current.scrollTop = this.refUnread.current.offsetTop - this.refTimestamp.current.clientHeight;
        });
    }

    private fixScroll() {
        if(this.scrollOffset === "element") {
            this.ignoreNextScroll = true;
            requestAnimationFrame(() => {
                this.ignoreNextScroll = false;

                if(!this.refMessages.current)
                    return;

                let scrollTop;
                if(this.refScrollElement.current) {
                    /* scroll to the element */
                    scrollTop = this.refScrollElement.current.offsetTop - this.scrollElementPreviousOffset;
                } else {
                    /* just scroll to the bottom */
                    scrollTop = this.refMessages.current.scrollHeight;
                }
                this.refMessages.current.scrollTop = scrollTop;
                this.scrollOffset = scrollTop;
                this.scrollEventUniqueId = undefined;
            });
        } else if(this.scrollOffset !== "bottom") {
            this.ignoreNextScroll = true;
            requestAnimationFrame(() => {
                if(this.scrollOffset === "bottom")
                    return;
                this.ignoreNextScroll = false;

                this.refMessages.current.scrollTop = this.scrollOffset as any;
            });
        } else if(this.refUnread.current) {
            this.scrollToNewMessage();
        } else {
            this.scrollToBottom();
        }
    }

    private scrollToNewMessagesShown() {
        const newMessageOffset = this.refUnread.current?.offsetTop;
        return typeof this.scrollOffset === "number" && this.refMessages.current?.clientHeight + this.scrollOffset < newMessageOffset;
    }

    render() {
        let contents = [];

        switch (this.state.mode) {
            case "error":
                contents.push(<div key={"ol-error"} className={cssStyle.overlay}><a>{this.state.errorMessage ? this.state.errorMessage : tr("An unknown error happened.")}</a></div>);
                break;

            case "unselected":
                contents.push(<div key={"ol-unselected"} className={cssStyle.overlay}><a><Translatable>No conversation selected</Translatable></a></div>);
                break;

            case "loading":
                contents.push(<div key={"ol-loading"} className={cssStyle.overlay}><a><Translatable>Loading</Translatable> <LoadingDots maxDots={3}/></a></div>);
                break;

            case "private":
                contents.push(<div key={"ol-private"} className={cssStyle.overlay}><a>
                    <Translatable>This conversation is private.</Translatable><br />
                    <Translatable>Join the channel to participate.</Translatable></a>
                </div>);
                break;

            case "no-permission":
                contents.push(<div key={"ol-permission"} className={cssStyle.overlay}><a>
                    <Translatable>You don't have permissions to participate in this conversation!</Translatable><br />
                    {this.state.failedPermission}</a>
                </div>);
                break;

            case "not-supported":
                contents.push(<div key={"ol-support"} className={cssStyle.overlay}><a>
                    <Translatable>The target server does not support cross channel chat.</Translatable><br />
                    <Translatable>Join the channel if you want to write.</Translatable></a>
                </div>);
                break;

            case "normal":
                if(this.viewEntries.length === 0 && !this.props.noFirstMessageOverlay) {
                    contents.push(<div key={"ol-empty"} className={cssStyle.overlay}><a>
                        <Translatable>There have been no messages yet.</Translatable><br />
                        <Translatable>Be the first who talks in here!</Translatable></a>
                    </div>);
                } else {
                    contents = this.viewEntries;
                }
                break;
        }

        const firstMessageTimestamp = this.chatEvents[0]?.timestamp;
        return (
            <div className={cssStyle.containerMessages}>
                <div
                    className={cssStyle.messages} ref={this.refMessages}
                    onClick={() => this.state.mode === "normal" && this.props.events.fire("action_clear_unread_flag", { chatId: this.currentChatId })}
                    onScroll={() => {
                        if(this.ignoreNextScroll)
                            return;

                        const top = this.refMessages.current.scrollTop;
                        const total = this.refMessages.current.scrollHeight - this.refMessages.current.clientHeight;
                        const shouldFollow = top + 200 > total;

                        if(firstMessageTimestamp && top <= 20 && this.state.historyState === "available" && Math.max(this.scrollHistoryAutoLoadThrottle, this.state.historyRetryTimestamp) < Date.now()) {
                            /* only load history when we're in an upwards scroll move */
                            if(this.scrollOffset === "bottom" || this.scrollOffset > top) {
                                this.scrollHistoryAutoLoadThrottle = Date.now() + 500; /* don't spam events */
                                this.props.events.fire_react("query_conversation_history", { chatId: this.currentChatId, timestamp: firstMessageTimestamp });
                            }
                        }

                        this.scrollOffset = shouldFollow ? "bottom" : top;
                    }}
                >
                    <LoadOderMessages
                        events={this.props.events}
                        chatId={this.currentChatId}
                        state={this.state.historyState}
                        timestamp={firstMessageTimestamp}
                        retryTimestamp={this.state.historyRetryTimestamp}
                        errorMessage={this.state.historyErrorMessage}
                    />
                    {contents}
                    {this.state.isBrowsingHistory ? <div key={"jump-present-placeholder"} className={cssStyle.jumpToPresentPlaceholder} /> : undefined}
                </div>
                <div
                    ref={this.refScrollToNewMessages}
                    className={cssStyle.containerScrollNewMessage + " " + (this.scrollToNewMessagesShown() ? cssStyle.shown : "")}
                    onClick={() => { this.scrollOffset = "bottom"; this.scrollToNewMessage(); }}
                >
                    <Translatable>Scroll to new messages</Translatable>
                </div>
                {this.state.isBrowsingHistory ?
                    <JumpToPresent
                        key={"jump-to-present"}
                        events={this.props.events}
                        chatId={this.currentChatId} /> :
                    undefined
                }
                <PartnerTypingIndicator events={this.props.events} chatId={this.currentChatId} />
            </div>
        );
    }

    componentDidMount(): void {
        this.props.events.fire("query_selected_chat");
        this.scrollToBottom();
    }

    componentDidUpdate(prevProps: Readonly<ConversationMessagesProperties>, prevState: Readonly<ConversationMessagesState>, snapshot?: any): void {
        requestAnimationFrame(() => {
            this.refScrollToNewMessages.current?.classList.toggle(cssStyle.shown, this.scrollToNewMessagesShown());
        });
    }

    componentWillUnmount(): void {
        clearTimeout(this.historyRetryTimer);
        this.historyRetryTimer = undefined;
    }

    private sortEvents() {
        this.chatEvents.sort((a, b) => a.timestamp - b.timestamp);
    }

    /* builds the view from the messages */
    private buildView() {
        this.viewEntries = [];

        let timeMarker = new Date(0);
        let unreadSet = false, timestampRefSet = false;

        let firstEvent = true;
        for(let event of this.chatEvents) {
            const mdate = new Date(event.timestamp);
            if(mdate.getFullYear() !== timeMarker.getFullYear() || mdate.getMonth() !== timeMarker.getMonth() || mdate.getDate() !== timeMarker.getDate()) {
                timeMarker = new Date(mdate.getFullYear(), mdate.getMonth(), mdate.getDate(), 1);
                this.viewEntries.push(<TimestampEntry key={"t" + this.viewElementIndex++} timestamp={timeMarker} refDiv={timestampRefSet ? undefined : this.refTimestamp} />);
                timestampRefSet = true;
            }

            if(event.timestamp >= this.unreadTimestamp && !unreadSet) {
                this.viewEntries.push(<UnreadEntry refDiv={this.refUnread} key={"u" + this.viewElementIndex++} />);
                unreadSet = true;
            }

            let reference = this.scrollEventUniqueId === event.uniqueId ? this.refScrollElement : firstEvent ? this.refFirstChatEvent : undefined;
            firstEvent = false;

            switch (event.type) {
                case "message":
                    this.viewEntries.push(<ChatEventMessageRenderer
                        key={event.uniqueId}
                        message={event.message}
                        events={this.props.events}
                        callbackDelete={this.props.messagesDeletable ? () => this.props.events.fire("action_delete_message", { chatId: this.currentChatId, uniqueId: event.uniqueId }) : undefined}
                        handlerId={this.props.handlerId}
                        refHTMLElement={reference}
                    />);
                    break;

                case "message-failed":
                    this.viewEntries.push(<ChatEventMessageFailedRenderer
                        key={event.uniqueId}
                        event={event}
                        refHTMLElement={reference}
                    />);
                    break;

                case "local-user-switch":
                    this.viewEntries.push(<ChatEventLocalUserSwitchRenderer
                        key={event.uniqueId}
                        timestamp={event.timestamp}
                        event={event}
                        refHTMLElement={reference}
                    />);
                    break;

                case "query-failed":
                    this.viewEntries.push(<ChatEventQueryFailedRenderer
                        key={event.uniqueId}
                        event={event}
                        refHTMLElement={reference}
                    />);
                    break;

                case "partner-instance-changed":
                    this.viewEntries.push(<ChatEventPartnerInstanceChangedRenderer
                        key={event.uniqueId}
                        event={event}
                        refHTMLElement={reference}
                    />);
                    break;

                case "local-action":
                    this.viewEntries.push(<ChatEventLocalActionRenderer
                        key={event.uniqueId}
                        event={event}
                        refHTMLElement={reference}
                    />);
                    break;

                case "partner-action":
                    this.viewEntries.push(<ChatEventPartnerActionRenderer
                        key={event.uniqueId}
                        event={event}
                        refHTMLElement={reference}
                    />);
                    break;

                case "mode-changed":
                    this.viewEntries.push(<ChatEventModeChangedRenderer
                        key={event.uniqueId}
                        event={event}
                        refHTMLElement={reference}
                    />);
                    break;
            }
        }
    }

    @EventHandler<AbstractConversationUiEvents>("notify_selected_chat")
    private handleNotifySelectedChat(event: AbstractConversationUiEvents["notify_selected_chat"]) {
        if(this.currentChatId === event.chatId) {
            return;
        }

        this.currentChatId = event.chatId;
        this.chatEvents = [];

        if(this.currentChatId === "unselected") {
            this.setState({ mode: "unselected" });
        } else {
            this.props.events.fire("query_conversation_state", {
                chatId: this.currentChatId
            });

            this.setState({ mode: "loading" });
        }
    }

    @EventHandler<AbstractConversationUiEvents>("notify_conversation_state")
    private handleConversationStateUpdate(event: AbstractConversationUiEvents["notify_conversation_state"]) {
        if(event.chatId !== this.currentChatId)
            return;

        if(event.state === "no-permissions") {
            this.chatEvents = [];
            this.buildView();
            this.setState({
                mode: "no-permission",
                failedPermission: event.failedPermission
            });
        } else if(event.state === "loading") {
            this.chatEvents = [];
            this.buildView();
            this.setState({
                mode: "loading"
            });
        } else if(event.state === "normal") {
            this.chatFrameMaxMessageCount = event.chatFrameMaxMessageCount;
            this.chatFrameMaxHistoryMessageCount = event.chatFrameMaxMessageCount * 2;
            this.showSwitchEvents = event.showUserSwitchEvents;

            this.unreadTimestamp = event.unreadTimestamp;
            this.chatEvents = event.events.slice(0).filter(e => e.type !== "local-user-switch" || event.showUserSwitchEvents);
            this.sortEvents();
            this.buildView();

            this.scrollOffset = "bottom";
            this.setState({
                mode: "normal",
                isBrowsingHistory: false,

                historyState: event.historyState,
                historyErrorMessage: event.historyErrorMessage,
                historyRetryTimestamp: event.historyRetryTimestamp
            }, () => this.scrollToBottom());
        } else if(event.state === "private") {
            this.chatEvents = [];
            this.buildView();
            this.setState({
                mode: event.crossChannelChatSupported ? "private" : "not-supported"
            });
        } else {
            this.chatEvents = [];
            this.buildView();
            this.setState({
                mode: "error",
                errorMessage: 'errorMessage' in event ? event.errorMessage : tr("Unknown error/Invalid state")
            });
        }
    }

    @EventHandler<AbstractConversationUiEvents>("notify_chat_event")
    private handleChatEvent(event: AbstractConversationUiEvents["notify_chat_event"]) {
        if(event.chatId !== this.currentChatId || this.state.isBrowsingHistory)
            return;

        if(event.event.type === "local-user-switch" && !this.showSwitchEvents)
            return;

        this.chatEvents.push(event.event);
        this.sortEvents();
        if(typeof this.unreadTimestamp === "undefined" && event.triggerUnread)
            this.unreadTimestamp = event.event.timestamp;

        const spliceCount = Math.max(0, this.chatEvents.length - this.chatFrameMaxMessageCount);
        this.chatEvents.splice(0, spliceCount);
        if(spliceCount > 0 && this.state.historyState === "none")
            this.setState({ historyState: "available" });

        this.buildView();
        this.forceUpdate(() => this.scrollToBottom());
    }

    @EventHandler<AbstractConversationUiEvents>("notify_chat_message_delete")
    private handleMessageDeleted(event: AbstractConversationUiEvents["notify_chat_message_delete"]) {
        if(event.chatId !== this.currentChatId) {
            return;
        }

        this.chatEvents = this.chatEvents.filter(mEvent => event.messageIds.indexOf(mEvent.uniqueId) === -1);

        this.buildView();
        this.forceUpdate(() => this.scrollToBottom());
    }

    @EventHandler<AbstractConversationUiEvents>("notify_unread_timestamp_changed")
    private handleUnreadTimestampChanged(event: AbstractConversationUiEvents["notify_unread_timestamp_changed"]) {
        if (event.chatId !== this.currentChatId)
            return;

        const oldUnreadTimestamp = this.unreadTimestamp;
        if(this.chatEvents.last()?.timestamp > event.timestamp) {
            this.unreadTimestamp = event.timestamp;
        } else {
            this.unreadTimestamp = undefined;
        }

        if(oldUnreadTimestamp !== this.unreadTimestamp) {
            this.buildView();
            this.forceUpdate();
        }
    }

    @EventHandler<AbstractConversationUiEvents>("notify_panel_show")
    private handlePanelShow() {
        this.fixScroll();
    }

    @EventHandler<AbstractConversationUiEvents>("query_conversation_history")
    private handleQueryConversationHistory(event: AbstractConversationUiEvents["query_conversation_history"]) {
        if (event.chatId !== this.currentChatId)
            return;

        this.setState({
            historyState: "loading"
        });
    }

    @EventHandler<AbstractConversationUiEvents>("notify_conversation_history")
    private handleNotifyConversationHistory(event: AbstractConversationUiEvents["notify_conversation_history"]) {
        if (event.chatId !== this.currentChatId)
            return;


        clearTimeout(this.historyRetryTimer);
        if(event.state === "error") {
            this.setState({
                historyState: "error",
                historyErrorMessage: event.errorMessage,
                historyRetryTimestamp: event.retryTimestamp
            });

            this.historyRetryTimer = setTimeout(() => {
                this.setState({
                    historyState: "available"
                });
                this.historyRetryTimer = undefined;
            }, event.retryTimestamp - Date.now()) as any;
        } else {
            this.scrollElementPreviousOffset = this.refFirstChatEvent.current ? this.refFirstChatEvent.current.offsetTop - this.refFirstChatEvent.current.parentElement.scrollTop : 0;
            this.scrollEventUniqueId = this.chatEvents[0].uniqueId;

            this.chatEvents.push(...event.events);
            this.sortEvents();

            const spliceCount = Math.max(0, this.chatEvents.length - this.chatFrameMaxHistoryMessageCount);
            this.chatEvents.splice(this.chatFrameMaxHistoryMessageCount, spliceCount);

            this.buildView();
            this.setState({
                isBrowsingHistory: true,
                historyState: event.hasMoreMessages ? "available" : "none",
                historyRetryTimestamp: event.retryTimestamp
            }, () => {
                this.scrollOffset = "element";
                this.fixScroll();
            });
        }
    }
}

export const ConversationPanel = React.memo((props: { events: Registry<AbstractConversationUiEvents>, handlerId: string, messagesDeletable: boolean, noFirstMessageOverlay: boolean }) => {
    const currentChat = useRef({ id: "unselected" });
    const chatEnabled = useRef(false);

    const refChatBox = useRef<ChatBox>();

    const updateChatBox = () => {
        refChatBox.current.setState({ enabled: currentChat.current.id !== "unselected" && chatEnabled.current });
    };

    props.events.reactUse("notify_selected_chat", event => {
        currentChat.current.id = event.chatId;
        updateChatBox();
    });

    props.events.reactUse("notify_conversation_state", event => {
        chatEnabled.current = event.state === "normal" && event.sendEnabled;
        updateChatBox();
    });

    props.events.reactUse("notify_send_enabled", event => {
        if(event.chatId !== currentChat.current.id)
            return;

        chatEnabled.current = event.enabled;
        updateChatBox();
    });
    props.events.reactUse("action_focus_chat", () => refChatBox.current?.events.fire("action_request_focus"));

    useEffect(() => {
        return refChatBox.current.events.on("notify_typing", () => props.events.fire("action_self_typing", { chatId: currentChat.current.id }));
    });

    return <div className={cssStyle.panel}>
        <ConversationMessages events={props.events} handlerId={props.handlerId} messagesDeletable={props.messagesDeletable} noFirstMessageOverlay={props.noFirstMessageOverlay} />
        <ChatBox
            ref={refChatBox}
            onSubmit={text => props.events.fire("action_send_message", { chatId: currentChat.current.id, text: text }) }
        />
    </div>
});
