import {queuedJoin} from '../fchat/channels';
import {decodeHTML} from '../fchat/common';
import {characterImage, ConversationSettings, EventMessage, Message, messageToString} from './common';
import core from './core';
import {Channel, Character, Connection, Conversation as Interfaces} from './interfaces';
import l from './localize';
import {CommandContext, isCommand, parse as parseCommand} from './slash_commands';
import MessageType = Interfaces.Message.Type;

function createMessage(this: void, type: MessageType, sender: Character, text: string, time?: Date): Message {
    if(type === MessageType.Message && text.match(/^\/me\b/) !== null) {
        type = MessageType.Action;
        text = text.substr(text.charAt(4) === ' ' ? 4 : 3);
    }
    return new Message(type, sender, text, time);
}

function safeAddMessage(this: void, messages: Interfaces.Message[], message: Interfaces.Message, max: number): void {
    if(messages.length >= max) messages.shift();
    messages.push(message);
}

abstract class Conversation implements Interfaces.Conversation {
    abstract enteredText: string;
    abstract readonly name: string;
    messages: Interfaces.Message[] = [];
    errorText = '';
    unread = Interfaces.UnreadState.None;
    lastRead: Interfaces.Message | undefined = undefined;
    infoText = '';
    abstract readonly maxMessageLength: number | undefined;
    _settings: Interfaces.Settings | undefined;
    protected abstract context: CommandContext;
    protected maxMessages = 100;
    protected allMessages: Interfaces.Message[] = [];
    private lastSent = '';

    constructor(readonly key: string, public _isPinned: boolean) {
    }

    get settings(): Interfaces.Settings {
        //tslint:disable-next-line:strict-boolean-expressions
        return this._settings || (this._settings = state.settings[this.key] || new ConversationSettings());
    }

    set settings(value: Interfaces.Settings) {
        this._settings = value;
        state.setSettings(this.key, value); //tslint:disable-line:no-floating-promises
    }

    get isPinned(): boolean {
        return this._isPinned;
    }

    set isPinned(value: boolean) {
        if(value === this._isPinned) return;
        this._isPinned = value;
        state.savePinned(); //tslint:disable-line:no-floating-promises
    }

    get reportMessages(): ReadonlyArray<Interfaces.Message> {
        return this.allMessages;
    }

    async send(): Promise<void> {
        if(this.enteredText.length === 0) return;
        if(isCommand(this.enteredText)) {
            const parsed = parseCommand(this.enteredText, this.context);
            if(typeof parsed === 'string') this.errorText = parsed;
            else {
                parsed.call(this);
                this.lastSent = this.enteredText;
                this.enteredText = '';
            }
        } else {
            this.lastSent = this.enteredText;
            await this.doSend();
        }
    }

    abstract async addMessage(message: Interfaces.Message): Promise<void>;

    loadLastSent(): void {
        this.enteredText = this.lastSent;
    }

    loadMore(): void {
        if(this.messages.length >= this.allMessages.length) return;
        this.maxMessages += 100;
        this.messages = this.allMessages.slice(-this.maxMessages);
    }

    show(): void {
        state.show(this);
    }

    onHide(): void {
        this.errorText = '';
        this.lastRead = this.messages[this.messages.length - 1];
        this.maxMessages = 100;
        this.messages = this.allMessages.slice(-this.maxMessages);
    }

    abstract close(): void;

    protected safeAddMessage(message: Interfaces.Message): void {
        safeAddMessage(this.allMessages, message, 500);
        safeAddMessage(this.messages, message, this.maxMessages);
    }

    protected abstract doSend(): Promise<void> | void;
}

class PrivateConversation extends Conversation implements Interfaces.PrivateConversation {
    readonly name = this.character.name;
    readonly context = CommandContext.Private;
    typingStatus: Interfaces.TypingStatus = 'clear';
    readonly maxMessageLength = core.connection.vars.priv_max;
    private _enteredText = '';
    private ownTypingStatus: Interfaces.TypingStatus = 'clear';
    private timer: number | undefined;
    private logPromise = core.logs.getBacklog(this).then((messages) => {
        this.allMessages.unshift(...messages);
        this.messages = this.allMessages.slice();
    });

    constructor(readonly character: Character) {
        super(character.name.toLowerCase(), state.pinned.private.indexOf(character.name) !== -1);
        this.lastRead = this.messages[this.messages.length - 1];
        this.allMessages = [];
    }

    get enteredText(): string {
        return this._enteredText;
    }

    set enteredText(value: string) {
        this._enteredText = value;
        if(this.timer !== undefined) clearTimeout(this.timer);
        if(value.length > 0) {
            if(this.ownTypingStatus !== 'typing') this.setOwnTyping('typing');
            this.timer = window.setTimeout(() => this.setOwnTyping('paused'), 5000);
        } else if(this.ownTypingStatus !== 'clear') this.setOwnTyping('clear');
    }

    async addMessage(message: Interfaces.Message): Promise<void> {
        await this.logPromise;
        this.safeAddMessage(message);
        if(message.type !== Interfaces.Message.Type.Event) {
            if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
            if(this.settings.notify !== Interfaces.Setting.False && message.sender !== core.characters.ownCharacter)
                core.notifications.notify(this, message.sender.name, message.text, characterImage(message.sender.name), 'attention');
            if(this !== state.selectedConversation || !state.windowFocused)
                this.unread = Interfaces.UnreadState.Mention;
            this.typingStatus = 'clear';
        }
    }

    async close(): Promise<void> {
        state.privateConversations.splice(state.privateConversations.indexOf(this), 1);
        delete state.privateMap[this.character.name.toLowerCase()];
        await state.savePinned();
        if(state.selectedConversation === this) state.show(state.consoleTab);
    }

    async sort(newIndex: number): Promise<void> {
        state.privateConversations.splice(state.privateConversations.indexOf(this), 1);
        state.privateConversations.splice(newIndex, 0, this);
        return state.savePinned();
    }

    protected async doSend(): Promise<void> {
        await this.logPromise;
        if(this.character.status === 'offline') {
            this.errorText = l('chat.errorOffline', this.character.name);
            return;
        } else if(this.character.isIgnored) {
            this.errorText = l('chat.errorIgnored', this.character.name);
            return;
        }
        core.connection.send('PRI', {recipient: this.name, message: this.enteredText});
        const message = createMessage(MessageType.Message, core.characters.ownCharacter, this.enteredText);
        this.safeAddMessage(message);
        if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
        this.enteredText = '';
    }

    private setOwnTyping(status: Interfaces.TypingStatus): void {
        this.ownTypingStatus = status;
        core.connection.send('TPN', {character: this.name, status});
    }
}

class ChannelConversation extends Conversation implements Interfaces.ChannelConversation {
    readonly context = CommandContext.Channel;
    readonly name = this.channel.name;
    isSendingAds = this.channel.mode === 'ads';
    adCountdown = 0;
    private chat: Interfaces.Message[] = [];
    private ads: Interfaces.Message[] = [];
    private both: Interfaces.Message[] = [];
    private _mode!: Channel.Mode;
    private adEnteredText = '';
    private chatEnteredText = '';
    private logPromise = core.logs.getBacklog(this).then((messages) => {
        this.both.unshift(...messages);
        this.chat.unshift(...this.both.filter((x) => x.type !== MessageType.Ad));
        this.ads.unshift(...this.both.filter((x) => x.type === MessageType.Ad));
        this.lastRead = this.messages[this.messages.length - 1];
        this.messages = this.allMessages.slice(-this.maxMessages);
    });

    constructor(readonly channel: Channel) {
        super(`#${channel.id.replace(/[^\w- ]/gi, '')}`, state.pinned.channels.indexOf(channel.id) !== -1);
        core.watch<Channel.Mode | undefined>(function(): Channel.Mode | undefined {
            const c = this.channels.getChannel(channel.id);
            return c !== undefined ? c.mode : undefined;
        }, (value: Channel.Mode | undefined) => {
            if(value === undefined) return;
            this.mode = value;
            if(value !== 'both') this.isSendingAds = value === 'ads';
        });
        this.mode = channel.mode === 'both' && channel.id in state.modes ? state.modes[channel.id]! : channel.mode;
    }

    get maxMessageLength(): number {
        return core.connection.vars[this.isSendingAds ? 'lfrp_max' : 'chat_max'];
    }

    get mode(): Channel.Mode {
        return this._mode;
    }

    set mode(mode: Channel.Mode) {
        this._mode = mode;
        this.maxMessages = 100;
        this.allMessages = this[mode];
        this.messages = this.allMessages.slice(-this.maxMessages);
        if(mode === this.channel.mode && this.channel.id in state.modes) delete state.modes[this.channel.id];
        else if(mode !== this.channel.mode && mode !== state.modes[this.channel.id]) state.modes[this.channel.id] = mode;
        else return;
        state.saveModes(); //tslint:disable-line:no-floating-promises
    }

    get enteredText(): string {
        return this.isSendingAds ? this.adEnteredText : this.chatEnteredText;
    }

    set enteredText(value: string) {
        if(this.isSendingAds) this.adEnteredText = value;
        else this.chatEnteredText = value;
    }

    get reportMessages(): ReadonlyArray<Interfaces.Message> {
        return this.both;
    }

    addModeMessage(mode: Channel.Mode, message: Interfaces.Message): void {
        if(this._mode === mode) this.safeAddMessage(message);
        else safeAddMessage(this[mode], message, 500);
    }

    async addMessage(message: Interfaces.Message): Promise<void> {
        await this.logPromise;
        if((message.type === MessageType.Message || message.type === MessageType.Ad) && message.text.match(/^\/warn\b/) !== null) {
            const member = this.channel.members[message.sender.name];
            if(member !== undefined && member.rank > Channel.Rank.Member || message.sender.isChatOp)
                message = new Message(MessageType.Warn, message.sender, message.text.substr(6), message.time);
        }

        if(message.type === MessageType.Ad) {
            this.addModeMessage('ads', message);
            if(core.state.settings.logAds) await core.logs.logMessage(this, message);
        } else {
            this.addModeMessage('chat', message);
            if(message.type !== Interfaces.Message.Type.Event) {
                if(message.type === Interfaces.Message.Type.Warn) this.addModeMessage('ads', message);
                if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
                if(this.unread === Interfaces.UnreadState.None && (this !== state.selectedConversation || !state.windowFocused)
                    && this.mode !== 'ads')
                    this.unread = Interfaces.UnreadState.Unread;
            } else this.addModeMessage('ads', message);
        }
        this.addModeMessage('both', message);
    }

    close(): void {
        core.connection.send('LCH', {channel: this.channel.id});
    }

    async sort(newIndex: number): Promise<void> {
        state.channelConversations.splice(state.channelConversations.indexOf(this), 1);
        state.channelConversations.splice(newIndex, 0, this);
        return state.savePinned();
    }

    protected async doSend(): Promise<void> {
        const isAd = this.isSendingAds;
        if(isAd && this.adCountdown > 0) return;
        core.connection.send(isAd ? 'LRP' : 'MSG', {channel: this.channel.id, message: this.enteredText});
        await this.addMessage(
            createMessage(isAd ? MessageType.Ad : MessageType.Message, core.characters.ownCharacter, this.enteredText, new Date()));
        if(isAd) {
            this.adCountdown = core.connection.vars.lfrp_flood;
            const interval = setInterval(() => {
                this.adCountdown -= 1;
                if(this.adCountdown === 0) clearInterval(interval);
            }, 1000);
        } else this.enteredText = '';
    }
}

class ConsoleConversation extends Conversation {
    readonly context = CommandContext.Console;
    readonly name = l('chat.consoleTab');
    readonly maxMessageLength = undefined;
    enteredText = '';

    constructor() {
        super('_', false);
        this.allMessages = [];
    }

    //tslint:disable-next-line:no-empty
    close(): void {
    }

    async addMessage(message: Interfaces.Message): Promise<void> {
        this.safeAddMessage(message);
        if(core.state.settings.logMessages) await core.logs.logMessage(this, message);
        if(this !== state.selectedConversation || !state.windowFocused) this.unread = Interfaces.UnreadState.Unread;
    }

    protected doSend(): void {
        this.errorText = l('chat.consoleChat');
    }
}

class State implements Interfaces.State {
    privateConversations: PrivateConversation[] = [];
    channelConversations: ChannelConversation[] = [];
    privateMap: {[key: string]: PrivateConversation | undefined} = {};
    channelMap: {[key: string]: ChannelConversation | undefined} = {};
    consoleTab!: ConsoleConversation;
    selectedConversation: Conversation = this.consoleTab;
    recent: Interfaces.RecentConversation[] = [];
    pinned!: {channels: string[], private: string[]};
    settings!: {[key: string]: Interfaces.Settings};
    modes!: {[key: string]: Channel.Mode | undefined};
    windowFocused = document.hasFocus();

    get hasNew(): boolean {
        return this.privateConversations.some((x) => x.unread === Interfaces.UnreadState.Mention) ||
            this.channelConversations.some((x) => x.unread === Interfaces.UnreadState.Mention);
    }

    getPrivate(character: Character): PrivateConversation {
        const key = character.name.toLowerCase();
        let conv = state.privateMap[key];
        if(conv !== undefined) return conv;
        conv = new PrivateConversation(character);
        this.privateConversations.push(conv);
        this.privateMap[key] = conv;
        state.addRecent(conv); //tslint:disable-line:no-floating-promises
        return conv;
    }

    byKey(key: string): Conversation | undefined {
        if(key === '_') return this.consoleTab;
        return (key[0] === '#' ? this.channelMap : this.privateMap)[key];
    }

    async savePinned(): Promise<void> {
        this.pinned.channels = this.channelConversations.filter((x) => x.isPinned).map((x) => x.channel.id);
        this.pinned.private = this.privateConversations.filter((x) => x.isPinned).map((x) => x.name);
        await core.settingsStore.set('pinned', this.pinned);
    }

    async saveModes(): Promise<void> {
        await core.settingsStore.set('modes', this.modes);
    }

    async setSettings(key: string, value: Interfaces.Settings): Promise<void> {
        this.settings[key] = value;
        await core.settingsStore.set('conversationSettings', this.settings);
    }

    async addRecent(conversation: Conversation): Promise<void> {
        const remove = <T extends Interfaces.RecentConversation>(predicate: (item: T) => boolean) => {
            for(let i = 0; i < this.recent.length; ++i)
                if(predicate(<T>this.recent[i])) {
                    this.recent.splice(i, 1);
                    break;
                }
        };
        if(Interfaces.isChannel(conversation)) {
            remove<Interfaces.RecentChannelConversation>((c) => c.channel === conversation.channel.id);
            this.recent.unshift({channel: conversation.channel.id, name: conversation.channel.name});
        } else {
            remove<Interfaces.RecentPrivateConversation>((c) => c.character === conversation.name);
            state.recent.unshift({character: conversation.name});
        }
        if(this.recent.length >= 50) this.recent.pop();
        await core.settingsStore.set('recent', this.recent);
    }

    show(conversation: Conversation): void {
        this.selectedConversation.onHide();
        conversation.unread = Interfaces.UnreadState.None;
        this.selectedConversation = conversation;
    }

    async reloadSettings(): Promise<void> {
        //tslint:disable:strict-boolean-expressions
        this.pinned = await core.settingsStore.get('pinned') || {private: [], channels: []};
        this.modes = await core.settingsStore.get('modes') || {};
        for(const conversation of this.channelConversations)
            conversation._isPinned = this.pinned.channels.indexOf(conversation.channel.id) !== -1;
        for(const conversation of this.privateConversations)
            conversation._isPinned = this.pinned.private.indexOf(conversation.name) !== -1;
        this.recent = await core.settingsStore.get('recent') || [];
        const settings = <{[key: string]: ConversationSettings}> await core.settingsStore.get('conversationSettings') || {};
        for(const key in settings) {
            const settingsItem = new ConversationSettings();
            for(const itemKey in settings[key])
                settingsItem[<keyof ConversationSettings>itemKey] = settings[key][<keyof ConversationSettings>itemKey];
            settings[key] = settingsItem;
            const conv = (key[0] === '#' ? this.channelMap : this.privateMap)[key];
            if(conv !== undefined) conv._settings = settingsItem;
        }
        this.settings = settings;
        //tslint:enable
    }
}

let state: State;

async function addEventMessage(this: void, message: Interfaces.Message): Promise<void> {
    await state.consoleTab.addMessage(message);
    if(core.state.settings.eventMessages && state.selectedConversation !== state.consoleTab)
        await state.selectedConversation.addMessage(message);
}

function isOfInterest(this: void, character: Character): boolean {
    return character.isFriend || character.isBookmarked || state.privateMap[character.name.toLowerCase()] !== undefined;
}

function isOp(conv: ChannelConversation): boolean {
    const ownChar = core.characters.ownCharacter;
    return ownChar.isChatOp || conv.channel.members[ownChar.name]!.rank > Channel.Rank.Member;
}

export default function(this: void): Interfaces.State {
    state = new State();
    window.addEventListener('focus', () => {
        state.windowFocused = true;
        if(state.selectedConversation !== undefined!) state.selectedConversation.unread = Interfaces.UnreadState.None;
    });
    window.addEventListener('blur', () => {
        state.windowFocused = false;
        if(state.selectedConversation !== undefined!)
             state.selectedConversation.lastRead = state.selectedConversation.messages[state.selectedConversation.messages.length - 1];
    });
    const connection = core.connection;
    connection.onEvent('connecting', async(isReconnect) => {
        state.channelConversations = [];
        state.channelMap = {};
        if(!isReconnect) {
            state.consoleTab = new ConsoleConversation();
            state.privateConversations = [];
            state.privateMap = {};
        } else state.consoleTab.unread = Interfaces.UnreadState.None;
        state.selectedConversation = state.consoleTab;
        await state.reloadSettings();
    });
    connection.onEvent('connected', (isReconnect) => {
        if(isReconnect) return;
        for(const item of state.pinned.private) state.getPrivate(core.characters.get(item));
        queuedJoin(state.pinned.channels.slice());
    });
    core.channels.onEvent(async(type, channel, member) => {
        if(type === 'join')
            if(member === undefined) {
                const conv = new ChannelConversation(channel);
                state.channelMap[channel.id] = conv;
                state.channelConversations.push(conv);
                await state.addRecent(conv);
            } else {
                const conv = state.channelMap[channel.id];
                if(conv === undefined) return;
                if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default &&
                    !core.state.settings.joinMessages) return;
                const text = l('events.channelJoin', `[user]${member.character.name}[/user]`);
                await conv.addMessage(new EventMessage(text));
            }
        else if(member === undefined) {
            const conv = state.channelMap[channel.id];
            if(conv === undefined) return;
            state.channelConversations.splice(state.channelConversations.indexOf(conv), 1);
            delete state.channelMap[channel.id];
            await state.savePinned();
            if(state.selectedConversation === conv) state.show(state.consoleTab);
        } else {
            const conv = state.channelMap[channel.id];
            if(conv === undefined) return;
            if(conv.settings.joinMessages === Interfaces.Setting.False || conv.settings.joinMessages === Interfaces.Setting.Default &&
                !core.state.settings.joinMessages) return;
            const text = l('events.channelLeave', `[user]${member.character.name}[/user]`);
            await conv.addMessage(new EventMessage(text));
        }
    });

    connection.onMessage('PRI', async(data, time) => {
        const char = core.characters.get(data.character);
        if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character});
        const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
        const conv = state.getPrivate(char);
        await conv.addMessage(message);
    });
    connection.onMessage('MSG', async(data, time) => {
        const char = core.characters.get(data.character);
        const conversation = state.channelMap[data.channel.toLowerCase()];
        if(conversation === undefined) return core.channels.leave(data.channel);
        if(char.isIgnored && !isOp(conversation)) return;
        const message = createMessage(MessageType.Message, char, decodeHTML(data.message), time);
        await conversation.addMessage(message);

        const words = conversation.settings.highlightWords.slice();
        if(conversation.settings.defaultHighlights) words.push(...core.state.settings.highlightWords);
        if(conversation.settings.highlight === Interfaces.Setting.Default && core.state.settings.highlight ||
            conversation.settings.highlight === Interfaces.Setting.True) words.push(core.connection.character);
        //tslint:disable-next-line:no-null-keyword
        const results = words.length > 0 ? message.text.match(new RegExp(`\\b(${words.join('|')})\\b`, 'i')) : null;
        if(results !== null) {
            core.notifications.notify(conversation, data.character, l('chat.highlight', results[0], conversation.name, message.text),
                characterImage(data.character), 'attention');
            if(conversation !== state.selectedConversation || !state.windowFocused) conversation.unread = Interfaces.UnreadState.Mention;
            message.isHighlight = true;
        } else if(conversation.settings.notify === Interfaces.Setting.True) {
            core.notifications.notify(conversation, conversation.name, messageToString(message),
                characterImage(data.character), 'attention');
            if(conversation !== state.selectedConversation || !state.windowFocused) conversation.unread = Interfaces.UnreadState.Mention;
        }
    });
    connection.onMessage('LRP', async(data, time) => {
        const char = core.characters.get(data.character);
        const conv = state.channelMap[data.channel.toLowerCase()];
        if(conv === undefined) return core.channels.leave(data.channel);
        if((char.isIgnored || core.state.hiddenUsers.indexOf(char.name) !== -1) && !isOp(conv)) return;
        await conv.addMessage(new Message(MessageType.Ad, char, decodeHTML(data.message), time));
    });
    connection.onMessage('RLL', async(data, time) => {
        const sender = core.characters.get(data.character);
        let text: string;
        if(data.type === 'bottle')
            text = l('chat.bottle', `[user]${data.target}[/user]`);
        else {
            const results = data.results.length > 1 ? `${data.results.join('+')} = ${data.endresult}` : data.endresult.toString();
            text = l('chat.roll', data.rolls.join('+'), results);
        }
        const message = new Message(MessageType.Roll, sender, text, time);
        if('channel' in data) {
            const channel = (<{channel: string}>data).channel.toLowerCase();
            const conversation = state.channelMap[channel];
            if(conversation === undefined) return core.channels.leave(channel);
            if(sender.isIgnored && !isOp(conversation)) return;
            if(data.type === 'bottle' && data.target === core.connection.character) {
                core.notifications.notify(conversation, conversation.name, messageToString(message),
                    characterImage(data.character), 'attention');
                if(conversation !== state.selectedConversation || !state.windowFocused)
                    conversation.unread = Interfaces.UnreadState.Mention;
                message.isHighlight = true;
            }
            await conversation.addMessage(message);
        } else {
            if(sender.isIgnored) return;
            const char = core.characters.get(
                data.character === connection.character ? (<{recipient: string}>data).recipient : data.character);
            if(char.isIgnored) return connection.send('IGN', {action: 'notify', character: data.character});
            const conversation = state.getPrivate(char);
            await conversation.addMessage(message);
        }
    });
    connection.onMessage('NLN', async(data, time) => {
        const message = new EventMessage(l('events.login', `[user]${data.identity}[/user]`), time);
        if(isOfInterest(core.characters.get(data.identity))) await addEventMessage(message);
        const conv = state.privateMap[data.identity.toLowerCase()];
        if(conv !== undefined && (!core.state.settings.eventMessages || conv !== state.selectedConversation))
            await conv.addMessage(message);
    });
    connection.onMessage('FLN', async(data, time) => {
        const message = new EventMessage(l('events.logout', `[user]${data.character}[/user]`), time);
        if(isOfInterest(core.characters.get(data.character))) await addEventMessage(message);
        const conv = state.privateMap[data.character.toLowerCase()];
        if(conv === undefined) return;
        conv.typingStatus = 'clear';
        if(!core.state.settings.eventMessages || conv !== state.selectedConversation) await conv.addMessage(message);
    });
    connection.onMessage('TPN', (data) => {
        const conv = state.privateMap[data.character.toLowerCase()];
        if(conv !== undefined) conv.typingStatus = data.status;
    });
    connection.onMessage('CBU', async(data, time) => {
        const text = l('events.ban', data.channel, data.character, data.operator);
        const conv = state.channelMap[data.channel.toLowerCase()];
        if(conv === undefined) return core.channels.leave(data.channel);
        conv.infoText = text;
        return addEventMessage(new EventMessage(text, time));
    });
    connection.onMessage('CKU', async(data, time) => {
        const text = l('events.kick', data.channel, data.character, data.operator);
        const conv = state.channelMap[data.channel.toLowerCase()];
        if(conv === undefined) return core.channels.leave(data.channel);
        conv.infoText = text;
        return addEventMessage(new EventMessage(text, time));
    });
    connection.onMessage('CTU', async(data, time) => {
        const text = l('events.timeout', data.channel, data.character, data.operator, data.length.toString());
        const conv = state.channelMap[data.channel.toLowerCase()];
        if(conv === undefined) return core.channels.leave(data.channel);
        conv.infoText = text;
        return addEventMessage(new EventMessage(text, time));
    });
    connection.onMessage('BRO', async(data, time) => {
        const text = data.character === undefined ? decodeHTML(data.message) :
            l('events.broadcast', `[user]${data.character}[/user]`, decodeHTML(data.message.substr(data.character.length + 23)));
        return addEventMessage(new EventMessage(text, time));
    });
    connection.onMessage('CIU', async(data, time) => {
        const text = l('events.invite', `[user]${data.sender}[/user]`, `[session=${data.title}]${data.name}[/session]`);
        return addEventMessage(new EventMessage(text, time));
    });
    connection.onMessage('ERR', async(data, time) => {
        state.selectedConversation.errorText = data.message;
        return addEventMessage(new EventMessage(`[color=red]${l('events.error', data.message)}[/color]`, time));
    });

    connection.onMessage('IGN', async(data, time) => {
        if(data.action !== 'add' && data.action !== 'delete') return;
        return addEventMessage(new EventMessage(l(`events.ignore_${data.action}`, data.character), time));
    });
    connection.onMessage('RTB', async(data, time) => {
        let url = 'https://www.f-list.net/';
        let text: string, character: string;
        if(data.type === 'comment') { //tslint:disable-line:prefer-switch
            switch(data.target_type) {
                case 'newspost':
                    url += `newspost/${data.target_id}/#Comment${data.id}`;
                    break;
                case 'bugreport':
                    url += `view_bugreport.php?id=/${data.target_id}/#${data.id}`;
                    break;
                case 'changelog':
                    url += `log.php?id=/${data.target_id}/#${data.id}`;
                    break;
                case 'feature':
                    url += `vote.php?id=/${data.target_id}/#${data.id}`;
            }
            const key = `events.rtbComment${(data.parent_id !== 0 ? 'Reply' : '')}`;
            text = l(key, `[user]${data.name}[/user]`, l(`events.rtbComment_${data.target_type}`), `[url=${url}]${data.target}[/url]`);
            character = data.name;
        } else if(data.type === 'note') {
            text = l('events.rtb_note', `[user]${data.sender}[/user]`, `[url=${url}view_note.php?note_id=${data.id}]${data.subject}[/url]`);
            character = data.sender;
        } else if(data.type === 'friendrequest') {
            text = l(`events.rtb_friendrequest`, `[user]${data.name}[/user]`);
            character = data.name;
        } else {
            switch(data.type) {
                case 'grouprequest':
                    url += 'panel/group_requests.php';
                    break;
                case 'bugreport':
                    url += `view_bugreport.php?id=${data.id}`;
                    break;
                case 'helpdeskticket':
                    url += `view_ticket.php?id=${data.id}`;
                    break;
                case 'helpdeskreply':
                    url += `view_ticket.php?id=${data.id}`;
                    break;
                case 'featurerequest':
                    url += `vote.php?fid=${data.id}`;
                    break;
                default: //TODO
                    return;
            }
            text = l(`events.rtb_${data.type}`, `[user]${data.name}[/user]`,
                data.title !== undefined ? `[url=${url}]${data.title}[/url]` : url);
            character = data.name;
        }
        await addEventMessage(new EventMessage(text, time));
        if(data.type === 'note')
            core.notifications.notify(state.consoleTab, character, text, characterImage(character), 'newnote');
    });
    type SFCMessage = (Interfaces.Message & {sfc: Connection.ServerCommands['SFC'] & {confirmed?: true}});
    const sfcList: SFCMessage[] = [];
    connection.onMessage('SFC', async(data, time) => {
        let text: string, message: Interfaces.Message;
        if(data.action === 'report') {
            text = l('events.report', `[user]${data.character}[/user]`, decodeHTML(data.tab), decodeHTML(data.report));
            core.notifications.notify(state.consoleTab, data.character, text, characterImage(data.character), 'modalert');
            message = new EventMessage(text, time);
            safeAddMessage(sfcList, message, 500);
            (<SFCMessage>message).sfc = data;
        } else {
            text = l('events.report.confirmed', `[user]${data.moderator}[/user]`, `[user]${data.character}[/user]`);
            for(const item of sfcList)
                if(item.sfc.logid === data.logid) {
                    item.sfc.confirmed = true;
                    break;
                }
            message = new EventMessage(text, time);
        }
        return addEventMessage(message);
    });
    connection.onMessage('STA', async(data, time) => {
        if(data.character === core.connection.character) {
            await addEventMessage(new EventMessage(l(data.statusmsg.length > 0 ? 'events.status.ownMessage' : 'events.status.own',
                l(`status.${data.status}`), decodeHTML(data.statusmsg)), time));
            return;
        }
        const char = core.characters.get(data.character);
        if(!isOfInterest(char)) return;
        const status = l(`status.${data.status}`);
        const key = data.statusmsg.length > 0 ? 'events.status.message' : 'events.status';
        const message = new EventMessage(l(key, `[user]${data.character}[/user]`, status, decodeHTML(data.statusmsg)), time);
        await addEventMessage(message);
        const conv = state.privateMap[data.character.toLowerCase()];
        if(conv !== undefined && core.state.settings.eventMessages && conv !== state.selectedConversation) await conv.addMessage(message);
    });
    connection.onMessage('SYS', async(data, time) => {
        state.selectedConversation.infoText = data.message;
        return addEventMessage(new EventMessage(data.message, time));
    });
    connection.onMessage('UPT', async(data, time) => addEventMessage(new EventMessage(l('events.uptime',
        data.startstring, data.channels.toString(), data.users.toString(), data.accepted.toString(), data.maxusers.toString()), time)));
    connection.onMessage('ZZZ', async(data, time) => {
        state.selectedConversation.infoText = data.message;
        return addEventMessage(new EventMessage(data.message, time));
    });
    return state;
}