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 {
    ChannelIcons,
    ChannelTreeEntry,
    ChannelTreeUIEvents,
    ClientTalkIconState,
    ServerState
} from "tc-shared/ui/tree/Definitions";
import {ChannelTreeRenderer} from "./Renderer";
import * as React from "react";
import * as ReactDOM from "react-dom";
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";
import {server_connections} from "tc-shared/ConnectionManager";

export interface ChannelTreeRendererOptions {
    popoutButton: boolean;
}

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

    ReactDOM.render(<ChannelTreeRenderer handlerId={channelTree.client.handlerId} events={events} />, target);

    let handlerDestroyListener;
    server_connections.events().on("notify_handler_deleted", handlerDestroyListener = event => {
        if(event.handler !== channelTree.client) {
            return;
        }

        ReactDOM.unmountComponentAtNode(target);
        server_connections.events().off("notify_handler_deleted", handlerDestroyListener);
        events.fire("notify_destroy");
        events.destroy();
    });
}

/* FIXME: Client move is not a part of the channel tree, it's part of our own controller here */
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 channelTreeInitialized = false;

    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.register_handler(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.unregister_handler(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.channelTreeInitialized = false;
            this.sendChannelTreeEntries();
        }
        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.channelTreeInitialized) {
            return;
        }

        this.channelTree.channels.forEach(channel => this.sendChannelIcons(channel));
    }

    private handleGroupsUpdated(event: GroupManagerEvents["notify_groups_updated"]) {
        if(!this.channelTreeInitialized) {
            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.channelTreeInitialized) {
            return;
        }

        this.channelTree.clients.forEach(channel => this.sendClientNameInfo(channel));
        this.channelTree.clients.forEach(client => this.sendClientIcons(client));
    }

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

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

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

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

        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.channelTreeInitialized) { return; }
        this.finalizeEvents(event.channel);
        this.sendChannelTreeEntries();
    }

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

        this.initializeClientEvents(event.client);
        this.sendChannelInfo(event.targetChannel);
        this.sendChannelStatusIcon(event.targetChannel);
        this.sendChannelTreeEntries();
    }

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

        this.finalizeEvents(event.client);
        this.sendChannelInfo(event.sourceChannel);
        this.sendChannelStatusIcon(event.sourceChannel);
        this.sendChannelTreeEntries();
    }

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

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

        this.sendChannelInfo(event.newChannel);
        this.sendChannelStatusIcon(event.newChannel);
        this.sendChannelTreeEntries();
    }

    @EventHandler<ChannelTreeEvents>("notify_selected_entry_changed")
    private handleSelectedEntryChanged(_event: ChannelTreeEvents["notify_selected_entry_changed"]) {
        if(!this.channelTreeInitialized) { 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("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.sendChannelTreeEntries();
        }));

        events.push(channel.events.on("notify_properties_updated", event => {
            for (const key of ChannelIconUpdateKeys) {
                if (key in event.updated_properties) {
                    this.sendChannelInfo(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("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()
        });
    }

    public sendChannelTreeEntries() {
        const entries = [] as ChannelTreeEntry[];

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

        const buildSubTree = (channel: ChannelEntry, depth: number) => {
            entries.push({ type: "channel", entryId: channel.uniqueEntryId, 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 {
                type: client instanceof LocalClientEntry ? "client-local" : "client",
                depth: depth + 1,
                entryId: client.uniqueEntryId
            } as ChannelTreeEntry }));
            channel.children(false).forEach(channel => buildSubTree(channel, depth + 1));
        };

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

        this.events.fire_react("notify_tree_entries", { 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: {
                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
            }
        })
    }

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

    public sendChannelIcons(channel: ChannelEntry) {
        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: this.channelTree.client.getCurrentServerUniqueId()
            }
        };

        const voiceConnection = this.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;
        }

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

    public sendClientNameInfo(client: ClientEntry) {
        let prefix = [];
        let suffix = [];
        for(const groupId of client.assignedServerGroupIds()) {
            const group = this.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 = this.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;
        this.events.fire_react("notify_client_name", {
            info: {
                name: client.clientNickName(),
                awayMessage: afkMessage,
                prefix: prefix,
                suffix: suffix
            },
            treeEntryId: client.uniqueEntryId
        });
    }

    public sendClientIcons(client: ClientEntry) {
        const uniqueServerId = this.channelTree.client.getCurrentServerUniqueId();

        const serverGroupIcons = client.assignedServerGroupIds()
            .map(groupId => this.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 => this.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];
        this.events.fire_react("notify_client_icons", {
            icons: {
                serverGroupIcons: serverGroupIcons,
                channelGroupIcon: channelGroupIcon[0],
                clientIcon: clientIcon.length > 0 ? { iconId: clientIcon[0], serverUniqueId: uniqueServerId } : undefined
            },
            treeEntryId: client.uniqueEntryId
        });
    }

    public sendClientTalkStatus(client: ClientEntry) {
        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";
            }
        }

        this.events.fire_react("notify_client_talk_status", { treeEntryId: client.uniqueEntryId, requestMessage: client.properties.client_talk_request_msg, status: status });
    }

    public sendServerStatus(serverEntry: ServerEntry) {
        let status: ServerState;

        switch (this.channelTree.client.connection_state) {
            case ConnectionState.AUTHENTICATING:
            case ConnectionState.CONNECTING:
            case ConnectionState.INITIALISING:
                status = {
                    state: "connecting",
                    targetAddress: serverEntry.remote_address.host + (serverEntry.remote_address.port === 9987 ? "" : `:${serverEntry.remote_address.port}`)
                };
                break;

            case ConnectionState.DISCONNECTING:
            case ConnectionState.UNCONNECTED:
                status = { state: "disconnected" };
                break;

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

        this.events.fire_react("notify_server_state", { treeEntryId: serverEntry.uniqueEntryId, state: status });
    }
}

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("notify_visibility_changed", event)));

    events.on("query_tree_entries", () => controller.sendChannelTreeEntries());
    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("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("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 => {
            const entry = channelTree.findEntryId(entryId);
            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 => {
            const entry = channelTree.findEntryId(entryId);
            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;
        }
    });
}