import {ChannelTree, ChannelTreeEvents} from "tc-shared/tree/ChannelTree";
import {ChannelTreeEntry as ChannelTreeEntryModel, ChannelTreeEntryEvents} from "tc-shared/tree/ChannelTreeEntry";
import {EventHandler, Registry} from "tc-shared/events";
import {
    ChannelEntryInfo,
    ChannelIcons,
    ChannelTreeUIEvents,
    ClientIcons,
    ClientNameInfo,
    ClientTalkIconState,
    FullChannelTreeEntry,
    ServerState
} from "tc-shared/ui/tree/Definitions";
import * as React from "react";
import {LogCategory, logWarn} from "tc-shared/log";
import {ChannelEntry, ChannelProperties} from "tc-shared/tree/Channel";
import {ClientEntry, ClientProperties, ClientType, LocalClientEntry, MusicClientEntry} from "tc-shared/tree/Client";
import {ConnectionEvents, ConnectionState} from "tc-shared/ConnectionHandler";
import {VoiceConnectionEvents, VoiceConnectionStatus} from "tc-shared/connection/VoiceConnection";
import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTransfer";
import {GroupManager, GroupManagerEvents} from "tc-shared/permission/GroupManager";
import {ServerEntry} from "tc-shared/tree/Server";

export interface ChannelTreeRendererOptions {
    popoutButton: boolean;
}

export function initializeChannelTreeUiEvents(channelTree: ChannelTree, options: ChannelTreeRendererOptions) : Registry<ChannelTreeUIEvents> {
    const events = new Registry<ChannelTreeUIEvents>();
    events.enableDebug("channel-tree-view");
    initializeChannelTreeController(events, channelTree, options);
    return events;
}

function generateServerStatus(serverEntry: ServerEntry) : ServerState {
    switch (serverEntry.channelTree.client.connection_state) {
        case ConnectionState.AUTHENTICATING:
        case ConnectionState.CONNECTING:
        case ConnectionState.INITIALISING:
            return {
                state: "connecting",
                targetAddress: serverEntry.remote_address.host + (serverEntry.remote_address.port === 9987 ? "" : `:${serverEntry.remote_address.port}`)
            };

        case ConnectionState.DISCONNECTING:
        case ConnectionState.UNCONNECTED:
            return { state: "disconnected" };

        case ConnectionState.CONNECTED:
            return {
                state: "connected",
                name: serverEntry.properties.virtualserver_name,
                icon: { iconId: serverEntry.properties.virtualserver_icon_id, serverUniqueId: serverEntry.properties.virtualserver_unique_identifier }
            };
    }
}

function generateClientTalkStatus(client: ClientEntry) : { status: ClientTalkIconState, requestMessage?: string } {
    let status: ClientTalkIconState = "unset";

    if(client.properties.client_is_talker) {
        status = "granted";
    } else if(client.properties.client_talk_power < client.currentChannel().properties.channel_needed_talk_power) {
        status = "prohibited";

        if(client.properties.client_talk_request !== 0) {
            status = "requested";
        }
    }

    return {
        requestMessage: client.properties.client_talk_request_msg,
        status: status
    }
}

function generateClientIcons(client: ClientEntry) : ClientIcons {
    const uniqueServerId = client.channelTree.client.getCurrentServerUniqueId();

    const serverGroupIcons = client.assignedServerGroupIds()
        .map(groupId => client.channelTree.client.groups.findServerGroup(groupId))
        .filter(group => !!group && group.properties.iconid !== 0)
        .sort(GroupManager.sorter())
        .map(group => {
            return {
                iconId: group.properties.iconid,
                groupName: group.name,
                groupId: group.id,
                serverUniqueId: uniqueServerId
            };
        });

    const channelGroupIcon = [client.assignedChannelGroup()]
        .map(groupId => client.channelTree.client.groups.findChannelGroup(groupId))
        .filter(group => !!group && group.properties.iconid !== 0)
        .map(group => {
            return {
                iconId: group.properties.iconid,
                groupName: group.name,
                groupId: group.id,
                serverUniqueId: uniqueServerId
            };
        });

    const clientIcon = client.properties.client_icon_id === 0 ? [] : [client.properties.client_icon_id];
    return {
        serverGroupIcons: serverGroupIcons,
        channelGroupIcon: channelGroupIcon[0],
        clientIcon: clientIcon.length > 0 ? { iconId: clientIcon[0], serverUniqueId: uniqueServerId } : undefined
    };
}

function generateClientNameInfo(client: ClientEntry) : ClientNameInfo {
    let prefix = [];
    let suffix = [];
    for(const groupId of client.assignedServerGroupIds()) {
        const group = client.channelTree.client.groups.findServerGroup(groupId);
        if(!group) {
            continue;
        }

        if(group.properties.namemode === 1) {
            prefix.push(group.name);
        } else if(group.properties.namemode === 2) {
            suffix.push(group.name);
        }
    }

    const channelGroup = client.channelTree.client.groups.findChannelGroup(client.assignedChannelGroup());
    if(channelGroup) {
        if(channelGroup.properties.namemode === 1) {
            prefix.push(channelGroup.name);
        } else if(channelGroup.properties.namemode === 2) {
            suffix.push(channelGroup.name);
        }
    }

    const afkMessage = client.properties.client_away ? client.properties.client_away_message : undefined;
    return {
        name: client.clientNickName(),
        awayMessage: afkMessage,
        prefix: prefix,
        suffix: suffix
    };
}

function generateChannelIcons(channel: ChannelEntry) : ChannelIcons {
    let icons: ChannelIcons = {
        musicQuality: channel.properties.channel_codec === 3 || channel.properties.channel_codec === 5,
        codecUnsupported: true,
        default: channel.properties.channel_flag_default,
        moderated: channel.properties.channel_needed_talk_power !== 0,
        passwordProtected: channel.properties.channel_flag_password,
        channelIcon: {
            iconId: channel.properties.channel_icon_id,
            serverUniqueId: channel.channelTree.client.getCurrentServerUniqueId()
        }
    };

    const voiceConnection = channel.channelTree.client.serverConnection.getVoiceConnection();
    const voiceState = voiceConnection.getConnectionState();

    switch (voiceState) {
        case VoiceConnectionStatus.Connected:
            icons.codecUnsupported = !voiceConnection.decodingSupported(channel.properties.channel_codec);
            break;

        default:
            icons.codecUnsupported = true;
    }

    return icons;
}

function generateChannelInfo(channel: ChannelEntry) : ChannelEntryInfo {
    return {
        collapsedState: channel.child_channel_head || channel.channelClientsOrdered().length > 0 ? channel.isCollapsed() ? "collapsed" : "expended" : "unset",
        name: channel.parsed_channel_name.text,
        nameStyle: channel.parsed_channel_name.alignment
    };
}

const ChannelIconUpdateKeys: (keyof ChannelProperties)[] = [
    "channel_name",
    "channel_flag_password",

    "channel_maxclients",
    "channel_flag_maxclients_unlimited",

    "channel_maxfamilyclients",
    "channel_flag_maxfamilyclients_inherited",
    "channel_flag_maxfamilyclients_unlimited",
];

const ChannelIconsUpdateKeys: (keyof ChannelProperties)[] = [
    "channel_icon_id",
    "channel_codec",
    "channel_flag_default",
    "channel_flag_password",
    "channel_needed_talk_power",
];

const ClientNameInfoUpdateKeys: (keyof ClientProperties)[] = [
    "client_nickname",
    "client_away_message",
    "client_away",
    "client_channel_group_id",
    "client_servergroups"
];

const ClientTalkStatusUpdateKeys: (keyof ClientProperties)[] = [
    "client_is_talker",
    "client_talk_power",
    "client_talk_request",
    "client_talk_request_msg",
    "client_talk_power"
]

class ChannelTreeController {
    readonly events: Registry<ChannelTreeUIEvents>;
    readonly channelTree: ChannelTree;
    readonly options: ChannelTreeRendererOptions;

    /* the key here is the unique entry id! */
    private eventListeners: {[key: number]: (() => void)[]} = {};

    private readonly connectionStateListener;
    private readonly voiceConnectionStateListener;
    private readonly groupUpdatedListener;
    private readonly groupsReceivedListener;

    constructor(events, channelTree, options: ChannelTreeRendererOptions) {
        this.events = events;
        this.channelTree = channelTree;
        this.options = options;

        this.connectionStateListener = this.handleConnectionStateChanged.bind(this);
        this.voiceConnectionStateListener = this.handleVoiceConnectionStateChanged.bind(this);
        this.groupUpdatedListener = this.handleGroupsUpdated.bind(this);
        this.groupsReceivedListener = this.handleGroupsReceived.bind(this);
    }

    initialize() {
        this.channelTree.client.events().on("notify_connection_state_changed", this.connectionStateListener);
        this.channelTree.client.serverConnection.getVoiceConnection().events.on("notify_connection_status_changed", this.voiceConnectionStateListener);
        this.channelTree.client.groups.events.on("notify_groups_updated", this.groupUpdatedListener);
        this.channelTree.client.groups.events.on("notify_groups_received", this.groupsReceivedListener);
        this.initializeServerEvents(this.channelTree.server);

        this.channelTree.events.registerHandler(this);

        if(this.channelTree.channelsInitialized) {
            this.handleChannelListReceived();
        }
    }

    destroy() {
        this.channelTree.client.events().off("notify_connection_state_changed", this.connectionStateListener);
        this.channelTree.client.serverConnection.getVoiceConnection().events.off("notify_connection_status_changed", this.voiceConnectionStateListener);
        this.channelTree.client.groups.events.off("notify_groups_updated", this.groupUpdatedListener);
        this.channelTree.client.groups.events.off("notify_groups_received", this.groupsReceivedListener);
        this.finalizeEvents(this.channelTree.server);

        this.channelTree.events.unregisterHandler(this);
        Object.values(this.eventListeners).forEach(callbacks => callbacks.forEach(callback => callback()));
        this.eventListeners = {};
    }

    private handleConnectionStateChanged(event: ConnectionEvents["notify_connection_state_changed"]) {
        if(event.newState !== ConnectionState.CONNECTED) {
            this.channelTree.channelsInitialized = false;
            this.sendChannelTreeEntriesFull([]);
        }
        this.sendServerStatus(this.channelTree.server);
    }

    private handleVoiceConnectionStateChanged(event: VoiceConnectionEvents["notify_connection_status_changed"]) {
        if(event.newStatus !== VoiceConnectionStatus.Connected && event.oldStatus !== VoiceConnectionStatus.Connected) {
            return;
        }

        if(!this.channelTree.channelsInitialized) {
            return;
        }

        /* Quicker than sending info for every client & channel */
        this.sendChannelTreeEntriesFull(undefined);
    }

    private handleGroupsUpdated(event: GroupManagerEvents["notify_groups_updated"]) {
        if(!this.channelTree.channelsInitialized) {
            return;
        }

        for(const update of event.updates) {
            if(update.key === "name-mode" || update.key === "name") {
                /* TODO: Only test if the client actually has the group (prevent twice updates than as well)? */
                this.channelTree.clients.forEach(client => this.sendClientNameInfo(client));
                break;
            }
        }

        for(const update of event.updates) {
            if(update.key === "icon" || update.key === "sort-id") {
                /* TODO: Only test if the client actually has the group (prevent twice updates than as well)? */
                this.channelTree.clients.forEach(client => this.sendClientIcons(client));
                break;
            }
        }
    }

    private handleGroupsReceived() {
        if(!this.channelTree.channelsInitialized) {
            return;
        }

        /* Faster than just sending each stuff individual */
        this.sendChannelTreeEntriesFull(undefined);
    }

    /* general channel tree event handlers */
    @EventHandler<ChannelTreeEvents>("notify_popout_state_changed")
    private handlePopoutStateChanged() {
        this.sendPopoutState();
    }

    @EventHandler<ChannelTreeEvents>("notify_channel_list_received")
    private handleChannelListReceived() {
        this.channelTree.channelsInitialized = true;
        this.channelTree.channels.forEach(channel => this.initializeChannelEvents(channel));
        this.channelTree.clients.forEach(channel => this.initializeClientEvents(channel));
        this.sendChannelTreeEntriesFull(undefined);
        this.sendSelectedEntry();
    }

    @EventHandler<ChannelTreeEvents>("notify_channel_created")
    private handleChannelCreated(event: ChannelTreeEvents["notify_channel_created"]) {
        if(!this.channelTree.channelsInitialized) { return; }
        this.initializeChannelEvents(event.channel);
        this.sendChannelTreeEntriesFull([event.channel.uniqueEntryId]);
    }

    @EventHandler<ChannelTreeEvents>("notify_channel_moved")
    private handleChannelMoved(event: ChannelTreeEvents["notify_channel_moved"]) {
        if(!this.channelTree.channelsInitialized) { return; }
        this.sendChannelTreeEntriesFull([]);

        if(event.previousParent && !event.previousParent.child_channel_head) {
            /* the collapsed state arrow changed */
            this.sendChannelInfo(event.previousParent);
        }
        if(event.channel.parent_channel()) {
            /* the collapsed state arrow may changed */
            this.sendChannelInfo(event.channel.parent_channel());
        }
    }

    @EventHandler<ChannelTreeEvents>("notify_channel_deleted")
    private handleChannelDeleted(event: ChannelTreeEvents["notify_channel_deleted"]) {
        if(!this.channelTree.channelsInitialized) { return; }
        this.finalizeEvents(event.channel);
        this.sendChannelTreeEntriesFull([]);
    }

    @EventHandler<ChannelTreeEvents>("notify_client_enter_view")
    private handleClientEnter(event: ChannelTreeEvents["notify_client_enter_view"]) {
        if(!this.channelTree.channelsInitialized) { return; }

        this.initializeClientEvents(event.client);
        this.sendChannelInfo(event.targetChannel);
        this.sendChannelStatusIcon(event.targetChannel);
        this.sendChannelTreeEntriesFull([event.client.uniqueEntryId]);
    }

    @EventHandler<ChannelTreeEvents>("notify_client_leave_view")
    private handleClientLeave(event: ChannelTreeEvents["notify_client_leave_view"]) {
        if(!this.channelTree.channelsInitialized) { return; }

        this.finalizeEvents(event.client);
        this.sendChannelInfo(event.sourceChannel);
        this.sendChannelStatusIcon(event.sourceChannel);
        this.sendChannelTreeEntriesFull([]);
    }

    @EventHandler<ChannelTreeEvents>("notify_client_moved")
    private handleClientMoved(event: ChannelTreeEvents["notify_client_moved"]) {
        if(!this.channelTree.channelsInitialized) { return; }

        this.sendChannelInfo(event.oldChannel);
        this.sendChannelStatusIcon(event.oldChannel);

        this.sendChannelInfo(event.newChannel);
        this.sendChannelStatusIcon(event.newChannel);
        this.sendChannelTreeEntriesFull([]);

        this.sendClientTalkStatus(event.client);
    }

    @EventHandler<ChannelTreeEvents>("notify_selected_entry_changed")
    private handleSelectedEntryChanged(_event: ChannelTreeEvents["notify_selected_entry_changed"]) {
        if(!this.channelTree.channelsInitialized) { return; }

        this.sendSelectedEntry();
    }

    /* entry event handlers */
    private initializeTreeEntryEvents<T extends ChannelTreeEntryEvents>(entry: ChannelTreeEntryModel<T>, events: any[]) {
        events.push(entry.events.on("notify_unread_state_change", event => {
            this.events.fire_react("notify_unread_state", { unread: event.unread, treeEntryId: entry.uniqueEntryId });
        }));
    }

    private initializeChannelEvents(channel: ChannelEntry) {
        this.finalizeEvents(channel);
        const events = this.eventListeners[channel.uniqueEntryId] = [];

        this.initializeTreeEntryEvents(channel, events);
        events.push(channel.events.on("notify_collapsed_state_changed", () => {
            this.sendChannelInfo(channel);
            this.sendChannelTreeEntriesFull([]);
        }));

        events.push(channel.events.on("notify_properties_updated", event => {
            if("channel_name" in event.updated_properties) {
                this.sendChannelInfo(channel);
            }

            for (const key of ChannelIconUpdateKeys) {
                if (key in event.updated_properties) {
                    this.sendChannelStatusIcon(channel);
                    break;
                }
            }

            for (const key of ChannelIconsUpdateKeys) {
                if (key in event.updated_properties) {
                    this.sendChannelIcons(channel);
                    break;
                }
            }

            if("channel_needed_talk_power" in event.updated_properties) {
                channel.clients(false).forEach(client => this.sendClientTalkStatus(client));
            }
        }));

        events.push(channel.events.on("notify_cached_password_updated", () => {
            this.sendChannelStatusIcon(channel);
        }));

        events.push(channel.events.on("notify_subscribe_state_changed", () => {
            this.sendChannelStatusIcon(channel);
        }));
    }

    private initializeClientEvents(client: ClientEntry) {
        this.finalizeEvents(client);
        const events = this.eventListeners[client.uniqueEntryId] = [];
        this.initializeTreeEntryEvents(client, events);

        events.push(client.events.on("notify_status_icon_changed", event => {
            this.events.fire_react("notify_client_status", { treeEntryId: client.uniqueEntryId, status: event.newIcon });
        }));

        events.push(client.events.on("notify_properties_updated", event => {
            for (const key of ClientNameInfoUpdateKeys) {
                if (key in event.updated_properties) {
                    this.sendClientNameInfo(client);
                    break;
                }
            }

            for (const key of ClientTalkStatusUpdateKeys) {
                if (key in event.updated_properties) {
                    this.sendClientTalkStatus(client);
                    break;
                }
            }

            if("client_servergroups" in event.updated_properties || "client_channel_group_id" in event.updated_properties || "client_icon_id" in event.updated_properties) {
                this.sendClientIcons(client);
            }
        }));
    }

    private initializeServerEvents(server: ServerEntry) {
        this.finalizeEvents(server);
        const events = this.eventListeners[server.uniqueEntryId] = [];
        this.initializeTreeEntryEvents(server, events);

        events.push(server.events.on("notify_properties_updated", event => {
            if("virtualserver_name" in event.updated_properties || "virtualserver_icon_id" in event.updated_properties) {
                this.sendServerStatus(server);
            }
        }));
    }

    private finalizeEvents<T extends ChannelTreeEntryEvents>(entry: ChannelTreeEntryModel<T>) {
        if(this.eventListeners[entry.uniqueEntryId]) {
            this.eventListeners[entry.uniqueEntryId].forEach(callback => callback());
        }
        delete this.eventListeners[entry.uniqueEntryId];
    }

    /* notify state update methods */
    public sendPopoutState() {
        this.events.fire_react("notify_popout_state", {
            showButton: this.options.popoutButton,
            shown: this.channelTree.popoutController.hasBeenPopedOut()
        });
    }

    private buildFlatChannelTree() : { entry: ChannelTreeEntryModel<any>, depth: number }[] {
        const entries: { entry: ChannelTreeEntryModel<any>, depth: number }[] = [];

        /* at first comes the server */
        entries.push({ entry: this.channelTree.server, depth: 0 });

        const buildSubTree = (channel: ChannelEntry, depth: number) => {
            entries.push({ entry: channel, depth: depth });
            if(channel.isCollapsed()) {
                return;
            }

            let clients = channel.channelClientsOrdered();
            if(!this.channelTree.areServerQueriesShown()) {
                clients = clients.filter(client => client.properties.client_type_exact !== ClientType.CLIENT_QUERY);
            }

            entries.push(...clients.map(client => {
                return {
                    entry: client,
                    depth: depth + 1
                }
            }));
            channel.children(false).forEach(channel => buildSubTree(channel, depth + 1));
        };

        this.channelTree.rootChannel().forEach(entry => buildSubTree(entry, 1));
        return entries;
    }

    /**
     * @param fullInfoEntries If `undefined` full entry info will be send.
     *                        Else only infos for entries which are contained within the entry id array will be send.
     */
    public sendChannelTreeEntriesFull(fullInfoEntries: number[] | undefined) {
        const entries = [] as FullChannelTreeEntry[];

        for(const entry of this.buildFlatChannelTree()) {
            if(!fullInfoEntries || fullInfoEntries.indexOf(entry.entry.uniqueEntryId) !== -1) {
                if(entry.entry instanceof ServerEntry) {
                    entries.push({
                        type: "server",
                        entryId: entry.entry.uniqueEntryId,
                        depth: entry.depth,
                        fullInfo: true,
                        state: generateServerStatus(entry.entry),
                        unread: entry.entry.isUnread()
                    });
                } else if(entry.entry instanceof ClientEntry) {
                    const talkStatus = generateClientTalkStatus(entry.entry);
                    entries.push({
                        type: entry.entry instanceof LocalClientEntry ? "client-local" : "client",
                        entryId: entry.entry.uniqueEntryId,
                        depth: entry.depth,
                        fullInfo: true,
                        unread: entry.entry.isUnread(),
                        name: generateClientNameInfo(entry.entry),
                        icons: generateClientIcons(entry.entry),
                        status: entry.entry.getStatusIcon(),
                        talkStatus: talkStatus.status,
                        talkRequestMessage: talkStatus.requestMessage
                    });
                } else if(entry.entry instanceof ChannelEntry) {
                    entries.push({
                        type: "channel",
                        entryId: entry.entry.uniqueEntryId,
                        depth: entry.depth,
                        fullInfo: true,
                        unread: entry.entry.isUnread(),
                        icons: generateChannelIcons(entry.entry),
                        info: generateChannelInfo(entry.entry),
                        icon: entry.entry.getStatusIcon()
                    })
                } else {
                    throw tr("Invalid flat channel tree entry");
                }
            } else {
                if(entry.entry instanceof ServerEntry) {
                    entries.push({
                        type: "server",
                        entryId: entry.entry.uniqueEntryId,
                        depth: entry.depth,
                        fullInfo: false
                    });
                } else if(entry.entry instanceof ClientEntry) {
                    entries.push({
                        type: entry.entry instanceof LocalClientEntry ? "client-local" : "client",
                        entryId: entry.entry.uniqueEntryId,
                        depth: entry.depth,
                        fullInfo: false
                    });
                } else if(entry.entry instanceof ChannelEntry) {
                    entries.push({
                        type: "channel",
                        entryId: entry.entry.uniqueEntryId,
                        depth: entry.depth,
                        fullInfo: false
                    })
                } else {
                    throw tr("Invalid flat channel tree entry");
                }
            }
        }

        this.events.fire_react("notify_tree_entries_full", { entries: entries });
    }

    public sendSelectedEntry() {
        const selectedEntry = this.channelTree.getSelectedEntry();
        this.events.fire_react("notify_selected_entry", { treeEntryId: selectedEntry ? selectedEntry.uniqueEntryId : 0 });
    }

    public sendChannelInfo(channel: ChannelEntry) {
        this.events.fire_react("notify_channel_info", {
            treeEntryId: channel.uniqueEntryId,
            info: generateChannelInfo(channel)
        })
    }

    public sendChannelStatusIcon(channel: ChannelEntry) {
        this.events.fire_react("notify_channel_icon", { icon: channel.getStatusIcon(), treeEntryId: channel.uniqueEntryId });
    }

    public sendChannelIcons(channel: ChannelEntry) {
        this.events.fire_react("notify_channel_icons", { icons: generateChannelIcons(channel), treeEntryId: channel.uniqueEntryId });
    }

    public sendClientNameInfo(client: ClientEntry) {
        this.events.fire_react("notify_client_name", {
            info: generateClientNameInfo(client),
            treeEntryId: client.uniqueEntryId
        });
    }

    public sendClientIcons(client: ClientEntry) {
        this.events.fire_react("notify_client_icons", {
            icons: generateClientIcons(client),
            treeEntryId: client.uniqueEntryId
        });
    }

    public sendClientTalkStatus(client: ClientEntry) {
        const status = generateClientTalkStatus(client);
        this.events.fire_react("notify_client_talk_status", { treeEntryId: client.uniqueEntryId, requestMessage: status.requestMessage, status: status.status });
    }

    public sendServerStatus(serverEntry: ServerEntry) {
        this.events.fire_react("notify_server_state", { treeEntryId: serverEntry.uniqueEntryId, state: generateServerStatus(serverEntry) });
    }
}

export function initializeChannelTreeController(events: Registry<ChannelTreeUIEvents>, channelTree: ChannelTree, options: ChannelTreeRendererOptions) {
    /* initialize the general update handler */
    const controller = new ChannelTreeController(events, channelTree, options);
    controller.initialize();
    events.on("notify_destroy", () => controller.destroy());

    /* initialize the query handlers */
    events.on("query_popout_state", () => controller.sendPopoutState());

    events.on("query_unread_state", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry) {
            logWarn(LogCategory.CHANNEL, tr("Tried to query the unread state of an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        events.fire_react("notify_unread_state", { treeEntryId: event.treeEntryId, unread: entry.isUnread() });
    });

    events.on("notify_destroy", channelTree.client.events().on("notify_visibility_changed", event => events.fire_react("notify_visibility_changed", event)));

    events.on("query_tree_entries", event => controller.sendChannelTreeEntriesFull(event.fullInfo ? undefined : []));
    events.on("query_selected_entry", () => controller.sendSelectedEntry());
    events.on("query_channel_info", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ChannelEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to query the channel state of an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        controller.sendChannelInfo(entry);
    });
    events.on("query_channel_icon", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ChannelEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to query the channels status icon of an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        controller.sendChannelStatusIcon(entry);
    });
    events.on("query_channel_icons", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ChannelEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to query the channels icons of an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        controller.sendChannelIcons(entry);
    });
    events.on("query_client_status", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ClientEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to query the client status of an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        events.fire_react("notify_client_status", { treeEntryId: entry.uniqueEntryId, status: entry.getStatusIcon() });
    });
    events.on("query_client_name", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ClientEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to query the client name of an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        controller.sendClientNameInfo(entry);
    });
    events.on("query_client_icons", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ClientEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to query the client icons of an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        controller.sendClientIcons(entry);
    });
    events.on("query_client_talk_status", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ClientEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to query the client talk status of an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        controller.sendClientTalkStatus(entry);
    });
    events.on("query_server_state", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ServerEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to query the server state of an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        controller.sendServerStatus(entry);
    });

    events.on("action_toggle_popout", event => {
        if(event.shown) {
            channelTree.popoutController.popout();
        } else {
            channelTree.popoutController.popin();
        }
    })

    events.on("action_set_collapsed_state", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ChannelEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to set the collapsed state state of an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        entry.setCollapsed(event.state === "collapsed");
    });

    events.on("action_select", event => {
        if(event.treeEntryId === 0) {
            channelTree.setSelectedEntry(undefined);
            return;
        }

        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry) {
            logWarn(LogCategory.CHANNEL, tr("Tried to select an invalid channel tree entry with id %o"), event.treeEntryId);
            return;
        }

        channelTree.setSelectedEntry(entry);
    });

    events.on("action_show_context_menu", event => {
        const entries = event.treeEntryIds.map(entryId => {
            const entry = channelTree.findEntryId(entryId);
            if(!entry) {
                logWarn(LogCategory.CHANNEL, tr("Tried to open a context menu for an invalid channel tree entry with id %o"), entryId);
            }
            return entry;
        }).filter(entry => !!entry);

        if(entries.length === 0) {
            channelTree.showContextMenu(event.pageX, event.pageY);
            return;
        } else if(entries.length === 1) {
            entries[0].showContextMenu(event.pageX, event.pageY);
        } else {
            channelTree.showMultiSelectContextMenu(entries, event.pageX, event.pageY);
        }
    });

    events.on("action_channel_join", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ChannelEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to join an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        entry.joinChannel();
    });

    events.on("action_channel_open_file_browser", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ChannelEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to open the file browser for an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        channelTree.setSelectedEntry(entry);
        spawnFileTransferModal(entry.channelId);
    });

    events.on("action_client_double_click", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof ClientEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Tried to execute a double click action for an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        if (entry instanceof LocalClientEntry) {
            entry.openRename(events);
        } else if (entry instanceof MusicClientEntry) {
            /* no action defined yet */
        } else {
            entry.open_text_chat();
        }
    });


    events.on("action_client_name_submit", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof LocalClientEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Having a client nickname submit notify for an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        events.fire_react("notify_client_name_edit", { treeEntryId: event.treeEntryId, initialValue: undefined });
        if(!event.name || event.name === entry.clientNickName()) { return; }

        entry.renameSelf(event.name).then(result => {
            if(result) { return; }
            events.fire_react("notify_client_name_edit", { treeEntryId: event.treeEntryId, initialValue: event.name });
        })
    });

    events.on("action_move_clients", event => {
        const entry = channelTree.findEntryId(event.targetTreeEntry);
        if(!entry) {
            logWarn(LogCategory.CHANNEL, tr("Received client move notify with an unknown target entry id %o"), event.targetTreeEntry);
            return;
        }

        let targetChannel: ChannelEntry;
        if(entry instanceof ClientEntry) {
            targetChannel = entry.currentChannel();
        } else if(entry instanceof ChannelEntry) {
            targetChannel = entry;
        } else {
            logWarn(LogCategory.CHANNEL, tr("Received client move notify with an invalid target entry id %o"), event.targetTreeEntry);
            return;
        }

        if(!targetChannel) {
            /* should not happen often that a client hasn't a channel */
            return;
        }

        const clients = event.entries.map(entryId => {
            if(entryId.type !== "client") { return; }

            let entry: ServerEntry | ChannelEntry | ClientEntry;
            if("uniqueTreeId" in entryId) {
                entry = channelTree.findEntryId(entryId.uniqueTreeId);
            } else {
                let clients = channelTree.clients.filter(client => client.properties.client_unique_identifier === entryId.clientUniqueId);
                if(entryId.clientId) {
                    clients = clients.filter(client => client.clientId() === entryId.clientId);
                }

                if(entryId.clientDatabaseId) {
                    clients = clients.filter(client => client.properties.client_database_id === entryId.clientDatabaseId);
                }

                if(clients.length === 1) {
                    entry = clients[0];
                }
            }

            if(!entry || !(entry instanceof ClientEntry)) {
                logWarn(LogCategory.CHANNEL, tr("Received client move notify with an entry id which isn't a client. Entry id: %o"), entryId);
                return undefined;
            }

            return entry;
        }).filter(client => !!client).filter(client => client.currentChannel() !== targetChannel);

        if(clients.length === 0) {
            return;
        }

        let bulks = clients.map(client => { return { clid: client.clientId() }; });
        bulks[0]["cid"] = targetChannel.channelId;

        channelTree.client.serverConnection.send_command("clientmove", bulks);
    });

    events.on("action_move_channels", event => {
        const targetChannel = channelTree.findEntryId(event.targetTreeEntry);
        if(!targetChannel || !(targetChannel instanceof ChannelEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Received channel move notify with an unknown/invalid target entry id %o"), event.targetTreeEntry);
            return;
        }

        let channels = event.entries.map(entryId => {
            if(entryId.type !== "channel") { return; }

            let entry: ServerEntry | ChannelEntry | ClientEntry;
            if("uniqueTreeId" in entryId) {
                entry = channelTree.findEntryId(entryId.uniqueTreeId);
            } else {
                entry = channelTree.findChannel(entryId.channelId);
            }

            if(!entry || !(entry instanceof ChannelEntry)) {
                logWarn(LogCategory.CHANNEL, tr("Received channel move notify with a channel id which isn't a channel. Entry id: %o"), entryId);
                return undefined;
            }

            return entry;
        }).filter(channel => !!channel);

        /* remove all channel in channel channels */
        channels = channels.filter(channel => {
            while((channel = channel.parent_channel())) {
                if(channels.indexOf(channel) !== -1) {
                    return false;
                }
            }
            return true;
        });

        channels = channels.filter(channel => channel !== targetChannel);

        if(channels.length === 0) {
            return;
        }

        let parentChannelId: number, previousChannelId: number;
        if(event.mode === "before") {
            parentChannelId = targetChannel.hasParent() ? targetChannel.parent_channel().channelId : 0;
            previousChannelId = targetChannel.channel_previous ? targetChannel.channel_previous.channelId : 0;
        } else if(event.mode == "after") {
            parentChannelId = targetChannel.hasParent() ? targetChannel.parent_channel().channelId : 0;
            previousChannelId = targetChannel.channelId;
        } else if(event.mode === "child") {
            parentChannelId = targetChannel.channelId;
            previousChannelId = 0;
        } else {
            return;
        }

        (async () => {
            let channel: ChannelEntry;
            while((channel = channels.pop_front())) {
                const success = await channelTree.client.serverConnection.send_command("channelmove", {
                    cid: channel.channelId,
                    cpid: parentChannelId,
                    order: previousChannelId
                }).then(() => true).catch(() => false);

                if(!success) {
                    break;
                }

                previousChannelId = channel.channelId;
            }
        })();
    });

    events.on("notify_client_name_edit_failed", event => {
        const entry = channelTree.findEntryId(event.treeEntryId);
        if(!entry || !(entry instanceof LocalClientEntry)) {
            logWarn(LogCategory.CHANNEL, tr("Having a client nickname edit failed notify for an invalid tree entry with id %o"), event.treeEntryId);
            return;
        }

        switch (event.reason) {
            case "scroll-to":
                entry.openRenameModal();
                break;
        }
    });
}