import {
    ChatEvent,
    ChatEventMessage,
    ChatMessage,
    ChatState,
    ConversationHistoryResponse
} from "../ui/frames/side/AbstractConversationDefinitions";
import {Registry} from "tc-shared/events";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {preprocessChatMessageForSend} from "tc-shared/text/chat";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {ErrorCode} from "tc-shared/connection/ErrorCode";
import {LogCategory, logWarn} from "tc-shared/log";
import {ChannelConversationMode} from "tc-shared/tree/Channel";
import {guid} from "tc-shared/crypto/uid";

export const kMaxChatFrameMessageSize = 50; /* max 100 messages, since the server does not support more than 100 messages queried at once */

export interface AbstractConversationEvents {
    notify_chat_event: {
        triggerUnread: boolean,
        event: ChatEvent
    },
    notify_unread_timestamp_changed: {
        timestamp: number
    },
    notify_unread_state_changed: {
        unread: boolean
    },
    notify_send_toggle: {
        enabled: boolean
    },
    notify_state_changed: {
        oldSTate: ChatState,
        newState: ChatState
    },
    notify_history_state_changed: {
        hasHistory: boolean
    },
    notify_conversation_mode_changed: {
        newMode: ChannelConversationMode
    },
    notify_read_state_changed: {
        readable: boolean
    }
}

export abstract class AbstractChat<Events extends AbstractConversationEvents> {
    readonly events: Registry<Events>;

    protected readonly connection: ConnectionHandler;
    protected readonly chatId: string;

    protected presentMessages: ChatEvent[] = [];
    protected presentEvents: Exclude<ChatEvent, ChatEventMessage>[] = []; /* everything excluding chat messages */

    private mode: ChatState = "unloaded";
    protected failedPermission: string;
    protected errorMessage: string;

    private conversationMode: ChannelConversationMode;

    protected unreadTimestamp: number;
    protected unreadState: boolean = false;

    protected messageSendEnabled: boolean = true;
    private conversationReadable = true;

    private history = false;

    protected constructor(connection: ConnectionHandler, chatId: string) {
        this.events = new Registry<Events>();
        this.connection = connection;
        this.chatId = chatId;
        this.unreadTimestamp = Date.now();
        this.conversationMode = ChannelConversationMode.Public;
    }

    destroy() {
        this.events.destroy();
    }

    public getCurrentMode() : ChatState { return this.mode; };

    protected setCurrentMode(mode: ChatState) {
        if(this.mode === mode) {
            return;
        }

        const oldState = this.mode;
        this.mode = mode;
        this.events.fire("notify_state_changed", { oldSTate: oldState, newState: mode });
    }

    public registerChatEvent(event: ChatEvent, triggerUnread: boolean) {
        if(event.type === "message") {
            let index = 0;
            while(index < this.presentMessages.length && this.presentMessages[index].timestamp <= event.timestamp) {
                index++;
            }

            this.presentMessages.splice(index, 0, event);

            const deleteMessageCount = Math.max(0, this.presentMessages.length - kMaxChatFrameMessageSize);
            this.presentMessages.splice(0, deleteMessageCount);
            if(deleteMessageCount > 0) {
                this.setHistory(true);
            }
            index -= deleteMessageCount;

            if(event.isOwnMessage) {
                this.setUnreadTimestamp(Date.now());
            } else if(!this.isUnread() && triggerUnread) {
                this.setUnreadTimestamp(event.message.timestamp - 1);
            } else if(!this.isUnread()) {
                /* mark the last message as read */
                this.setUnreadTimestamp(event.message.timestamp);
            }

            this.events.fire("notify_chat_event", {
                triggerUnread: triggerUnread,
                event: event
            });
        } else {
            this.presentEvents.push(event);
            this.presentEvents.sort((a, b) => a.timestamp - b.timestamp);
            /* TODO: Cutoff too old events! */

            if(!this.isUnread() && triggerUnread) {
                this.setUnreadTimestamp(event.timestamp - 1);
            } else if(!this.isUnread()) {
                /* mark the last message as read */
                this.setUnreadTimestamp(event.timestamp);
            }

            this.events.fire("notify_chat_event", {
                triggerUnread: triggerUnread,
                event: event
            });
        }
    }

    protected registerIncomingMessage(message: ChatMessage, isOwnMessage: boolean, uniqueId: string) {
        this.registerChatEvent({
            type: "message",
            isOwnMessage: isOwnMessage,
            uniqueId: uniqueId,
            timestamp: message.timestamp,
            message: message
        }, !isOwnMessage);
    }

    protected doSendMessage(message: string, targetMode: number, target: number) : Promise<boolean> {
        let msg = preprocessChatMessageForSend(message);
        return this.connection.serverConnection.send_command("sendtextmessage", {
            targetmode: targetMode,
            cid: target,
            target: target,
            msg: msg
        }, { process_result: false }).then(async () => true).catch(error => {
            if(error instanceof CommandResult) {
                if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
                    this.registerChatEvent({
                        type: "message-failed",
                        uniqueId: "msf-" + this.chatId + "-" + Date.now(),
                        timestamp: Date.now(),
                        error: "permission",
                        failedPermission: this.connection.permissions.resolveInfo(parseInt(error.json["failed_permid"]))?.name || tr("unknown")
                    }, false);
                } else {
                    this.registerChatEvent({
                        type: "message-failed",
                        uniqueId: "msf-" + this.chatId + "-" + Date.now(),
                        timestamp: Date.now(),
                        error: "error",
                        errorMessage: error.formattedMessage()
                    }, false);
                }
            } else if(typeof error === "string") {
                this.registerChatEvent({
                    type: "message-failed",
                    uniqueId: "msf-" + this.chatId + "-" + Date.now(),
                    timestamp: Date.now(),
                    error: "error",
                    errorMessage: error
                }, false);
            } else {
                logWarn(LogCategory.CHAT, tr("Failed to send channel chat message to %s: %o"), this.chatId, error);
                this.registerChatEvent({
                    type: "message-failed",
                    uniqueId: "msf-" + this.chatId + "-" + Date.now(),
                    timestamp: Date.now(),
                    error: "error",
                    errorMessage: tr("lookup the console")
                }, false);
            }
            return false;
        });
    }

    public getChatId() : string {
        return this.chatId;
    }

    public isUnread() : boolean {
        return this.unreadState;
    }

    public getConversationMode() : ChannelConversationMode {
        return this.conversationMode;
    }

    public isPrivate() : boolean {
        return this.conversationMode === ChannelConversationMode.Private;
    }

    protected setConversationMode(mode: ChannelConversationMode, logChange: boolean) {
        if(this.conversationMode === mode) {
            return;
        }

        if(logChange) {
            this.registerChatEvent({
                type: "mode-changed",
                uniqueId: guid() + "-mode-change",
                timestamp: Date.now(),
                newMode: mode === ChannelConversationMode.Public ? "normal" : mode === ChannelConversationMode.Private ? "private" : "none"
            }, true);
        }

        this.conversationMode = mode;
        this.events.fire("notify_conversation_mode_changed", { newMode: mode });
    }

    public isReadable() {
        return this.conversationReadable;
    }

    protected setReadable(flag: boolean) {
        if(this.conversationReadable === flag) {
            return;
        }

        this.conversationReadable = flag;
        this.events.fire("notify_read_state_changed", { readable: flag });
    }

    public isSendEnabled() : boolean {
        return this.messageSendEnabled;
    }

    public getUnreadTimestamp() : number | undefined {
        return this.unreadTimestamp;
    }

    public getPresentMessages() : ChatEvent[] {
        return this.presentMessages;
    }

    public getPresentEvents() : ChatEvent[] {
        return this.presentEvents;
    }

    public getErrorMessage() : string | undefined {
        return this.errorMessage;
    }

    public getFailedPermission() : string | undefined {
        return this.failedPermission;
    }

    public setUnreadTimestamp(timestamp: number) {
        if(this.unreadTimestamp !== timestamp) {
            this.unreadTimestamp = timestamp;
            this.events.fire("notify_unread_timestamp_changed", { timestamp: timestamp });
        }

        /* do update the unread state anyways since setUnreadTimestamp will be called when new messages arrive */
        this.updateUnreadState();
    }

    protected updateUnreadState() {
        const newState = this.unreadTimestamp < this.lastEvent()?.timestamp;
        if(this.unreadState !== newState) {
            this.unreadState = newState;
            this.events.fire("notify_unread_state_changed", { unread: newState });
        }
    }

    public hasHistory() : boolean {
        return this.history;
    }

    protected setHistory(hasHistory: boolean) {
        if(this.history === hasHistory) {
            return;
        }

        this.history = hasHistory;
        this.events.fire("notify_history_state_changed", { hasHistory: hasHistory });
    }

    protected lastEvent() : ChatEvent | undefined {
        if(this.presentMessages.length === 0) {
            return this.presentEvents.last();
        } else if(this.presentEvents.length === 0 || this.presentMessages.last().timestamp > this.presentEvents.last().timestamp) {
            return this.presentMessages.last();
        } else {
            return this.presentEvents.last();
        }
    }

    protected sendMessageSendingEnabled(enabled: boolean) {
        if(this.messageSendEnabled === enabled) {
            return;
        }

        this.messageSendEnabled = enabled;
        this.events.fire("notify_send_toggle", { enabled: enabled });
    }

    public abstract queryHistory(criteria: { begin?: number, end?: number, limit?: number }) : Promise<ConversationHistoryResponse>;
    public abstract queryCurrentMessages();
    public abstract sendMessage(text: string);
}

export interface AbstractChatManagerEvents<ConversationType> {
    notify_selected_changed: {
        oldConversation: ConversationType,
        newConversation: ConversationType
    },
    notify_conversation_destroyed: {
        conversation: ConversationType
    },
    notify_conversation_created: {
        conversation: ConversationType
    },
    notify_unread_count_changed: {
        unreadConversations: number
    },
    notify_cross_conversation_support_changed: {
        crossConversationSupported: boolean
    }
}

export abstract class AbstractChatManager<ManagerEvents extends AbstractChatManagerEvents<ConversationType>, ConversationType extends AbstractChat<ConversationEvents>, ConversationEvents extends AbstractConversationEvents> {
    readonly events: Registry<ManagerEvents>;
    readonly connection: ConnectionHandler;
    protected readonly listenerConnection: (() => void)[];
    private readonly listenerUnreadTimestamp: () => void;

    private conversations: {[key: string]: ConversationType} = {};
    private selectedConversation: ConversationType;

    private currentUnreadCount: number;
    private crossConversationSupport: boolean;

    /* FIXME: Access modifier */
    public historyUiStates: {[id: string]: {
        executingUIHistoryQuery: boolean,
        historyErrorMessage: string | undefined,
        historyRetryTimestamp: number
    }} = {};

    protected constructor(connection: ConnectionHandler) {
        this.connection = connection;
        this.events = new Registry<ManagerEvents>();
        this.listenerConnection = [];
        this.currentUnreadCount = 0;

        this.crossConversationSupport = true;

        this.listenerUnreadTimestamp = () => {
            let count = this.getConversations().filter(conversation => conversation.isUnread()).length;
            if(count === this.currentUnreadCount) { return; }

            this.currentUnreadCount = count;
            this.events.fire("notify_unread_count_changed", { unreadConversations: count });
        }
    }

    destroy() {
        this.events.destroy();

        this.listenerConnection.forEach(callback => callback());
        this.listenerConnection.splice(0, this.listenerConnection.length);
    }

    getConversations() : ConversationType[] {
        return Object.values(this.conversations);
    }

    getUnreadCount() : number {
        return this.currentUnreadCount;
    }

    hasCrossConversationSupport() : boolean {
        return this.crossConversationSupport;
    }

    getSelectedConversation() : ConversationType {
        return this.selectedConversation;
    }

    setSelectedConversation(conversation: ConversationType | undefined) {
        if(this.selectedConversation === conversation) {
            return;
        }

        const oldConversation = this.selectedConversation;
        this.selectedConversation = conversation;

        this.events.fire("notify_selected_changed", {
            oldConversation: oldConversation,
            newConversation: conversation
        });
    }

    findConversationById(id: string) : ConversationType {
        return this.conversations[id];
    }

    protected registerConversation(conversation: ConversationType) {
        conversation.events.on("notify_unread_state_changed", this.listenerUnreadTimestamp);
        this.conversations[conversation.getChatId()] = conversation;
        this.events.fire("notify_conversation_created", { conversation: conversation });
        this.listenerUnreadTimestamp();
    }

    protected unregisterConversation(conversation: ConversationType) {
        conversation = this.conversations[conversation.getChatId()];
        if(!conversation) { return; }

        if(conversation === this.selectedConversation) {
            this.setSelectedConversation(undefined);
        }

        delete this.historyUiStates[conversation.getChatId()];

        conversation.events.off("notify_unread_state_changed", this.listenerUnreadTimestamp);
        delete this.conversations[conversation.getChatId()];
        this.events.fire("notify_conversation_destroyed", { conversation: conversation });

        this.listenerUnreadTimestamp();
    }

    protected setCrossConversationSupport(supported: boolean) {
        if(this.crossConversationSupport === supported) {
            return;
        }

        this.crossConversationSupport = supported;
        this.events.fire("notify_cross_conversation_support_changed", { crossConversationSupported: supported });
    }
}