import * as React from "react";
import {useContext, useEffect, useRef, useState} from "react";
import {EventHandler, IpcRegistryDescription, ReactEventHandler, Registry} from "tc-shared/events";
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
import {createInputModal} from "tc-shared/ui/elements/Modal";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
import {copyToClipboard} from "tc-shared/utils/helpers";
import {FlatInputField} from "tc-shared/ui/react-elements/InputField";
import {arrayBufferBase64} from "tc-shared/utils/buffers";
import {tra} from "tc-shared/i18n/localize";
import {getIconManager} from "tc-shared/file/Icons";
import {PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefinitions";
import {
    ChannelInfo,
    GroupProperties,
    PermissionEditorTab,
    PermissionModalEvents
} from "tc-shared/ui/modal/permission/ModalDefinitions";
import {useTr} from "tc-shared/ui/react-elements/Helper";
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
import {ContextDivider} from "tc-shared/ui/react-elements/ContextDivider";
import {EditorRenderer} from "tc-shared/ui/modal/permission/EditorRenderer";
import {spawnContextMenu} from "tc-shared/ui/ContextMenu";
import {ClientIcon} from "svg-sprites/client-icons";
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";

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

export type PermissionEditorServerInfo = { handlerId: string, serverUniqueId: string  };
const ModalEventContext = React.createContext<Registry<PermissionModalEvents>>(undefined);
const EditorEventContext = React.createContext<Registry<PermissionEditorEvents>>(undefined);
const ServerInfoContext = React.createContext<PermissionEditorServerInfo>(undefined);

const GroupsButton = (props: { clientIcon: ClientIcon, alt: string, onClick?: () => void, disabled: boolean }) => {
    return (
        <div
            className={cssStyle.button + " " + (props.disabled ? cssStyle.disabled : "")}
            onClick={() => !props.disabled && props.onClick()}
        >
            <ClientIconRenderer icon={props.clientIcon} className={cssStyle.icon} />
        </div>
    );
};


const GroupsListEntry = React.memo((props: { group: GroupProperties, selected: boolean, callbackSelect: () => void, onContextMenu: (event: React.MouseEvent) => void }) => {
    const serverInfo = useContext(ServerInfoContext);

    let groupTypePrefix = "";
    switch (props.group.type) {
        case "query":
            groupTypePrefix = "[Q] ";
            break;

        case "template":
            groupTypePrefix = "[T] ";
            break;
    }
    return (
        <div className={cssStyle.entry + " " + (props.selected ? cssStyle.selected : "")} onClick={props.callbackSelect}
             onContextMenu={props.onContextMenu}>
            <RemoteIconRenderer icon={getIconManager().resolveIcon(props.group.iconId, serverInfo.serverUniqueId, serverInfo.handlerId)} />
            <div className={cssStyle.name}>{groupTypePrefix + props.group.name + " (" + props.group.id + ")"}</div>
        </div>
    )
});

@ReactEventHandler<GroupsList>(e => e.props.events)
class GroupsList extends React.PureComponent<{ events: Registry<PermissionModalEvents>, target: "server" | "channel" }, {
    selectedGroupId: number,
    showQueryGroups: boolean,
    showTemplateGroups: boolean,

    disableGroupAdd: boolean,
    disableGroupRename: boolean,
    disablePermissionCopy: boolean,
    disableDelete: boolean
}> {
    private readonly groups: GroupProperties[] = [];
    private visibleGroups: GroupProperties[] = [];

    private updateScheduleId;
    private modifyPower: number;
    private isListActive: boolean = false;

    constructor(props) {
        super(props);

        this.modifyPower = 0;
        this.state = {
            selectedGroupId: -1,
            showQueryGroups: false,
            showTemplateGroups: false,

            disableDelete: true,
            disablePermissionCopy: true,
            disableGroupAdd: true,
            disableGroupRename: true
        }
    }

    render() {
        this.updateGroups(false);

        return [
            <div key={"list"} className={cssStyle.list + " " + cssStyle.containerList} onContextMenu={event => {
                if (event.isDefaultPrevented())
                    return;

                event.preventDefault();
                spawn_context_menu(event.pageX, event.pageY, {
                    name: tr("Add group"),
                    icon_class: "client-add",
                    type: MenuEntryType.ENTRY,
                    callback: () => this.props.events.fire("action_create_group", {
                        target: this.props.target,
                        sourceGroup: 0
                    }),
                    invalidPermission: this.state.disableGroupAdd
                });
            }}>
                <div className={cssStyle.entries}>
                    {this.visibleGroups.map(e => (
                        <GroupsListEntry key={"group-" + e.id}
                                         group={e}
                                         selected={e.id === this.state.selectedGroupId}
                                         callbackSelect={() => this.props.events.fire("action_select_group", {
                                             id: e.id,
                                             target: this.props.target
                                         })}
                                         onContextMenu={event => {
                                             event.preventDefault();
                                             this.props.events.fire("action_select_group", {
                                                 target: this.props.target,
                                                 id: e.id
                                             });

                                             spawnContextMenu({ pageX: event.pageX, pageY: event.pageY }, [
                                                 {
                                                     type: "normal",
                                                     label: tr("Rename group"),
                                                     click: () => this.onGroupRename(),
                                                     icon: ClientIcon.ChangeNickname,
                                                     enabled: !this.state.disableGroupRename
                                                 }, {
                                                     type: "normal",
                                                     label: tr("Copy permissions"),
                                                     icon: ClientIcon.Copy,
                                                     click: () => this.props.events.fire("action_group_copy_permissions", {
                                                         target: this.props.target,
                                                         sourceGroup: e.id
                                                     }),
                                                     enabled: !this.state.disablePermissionCopy
                                                 }, {
                                                     type: "normal",
                                                     label: tr("Delete group"),
                                                     click: () => this.onGroupDelete(),
                                                     icon: ClientIcon.Delete,
                                                     enabled: !this.state.disableDelete
                                                 }, {
                                                     type: "separator"
                                                 }, {
                                                     type: "normal",
                                                     label: tr("Add group"),
                                                     click: () => this.props.events.fire("action_create_group", {
                                                         target: this.props.target,
                                                         sourceGroup: e.id
                                                     }),
                                                     icon: ClientIcon.Add,
                                                     enabled: !this.state.disableGroupAdd
                                                 }
                                             ]);
                                         }}
                        />
                    ))}
                </div>
            </div>,
            <div key={"buttons"} className={cssStyle.buttons}>
                <GroupsButton
                    clientIcon={ClientIcon.GroupAdd}
                    alt={this.props.target === "server" ? tr("Add server group") : tr("Add channel group")}
                    disabled={this.state.disableGroupAdd}
                    onClick={() => this.props.events.fire("action_create_group", {
                        target: this.props.target,
                        sourceGroup: this.state.selectedGroupId
                    })}
                />
                <GroupsButton
                    clientIcon={ClientIcon.GroupRename}
                    alt={this.props.target === "server" ? tr("Rename server group") : tr("Rename channel group")}
                    onClick={() => this.onGroupRename()}
                    disabled={this.state.disableGroupRename}
                />
                <GroupsButton
                    clientIcon={ClientIcon.GroupPermissionCopy}
                    alt={this.props.target === "server" ? tr("Copy server group permissions") : tr("Copy channel group permissions")}
                    disabled={this.state.disablePermissionCopy}
                    onClick={() => this.props.events.fire("action_group_copy_permissions", {
                        target: this.props.target,
                        sourceGroup: this.state.selectedGroupId
                    })}
                />
                <GroupsButton
                    clientIcon={ClientIcon.GroupDelete}
                    alt={this.props.target === "server" ? tr("Delete server group") : tr("Delete channel group")}
                    disabled={this.state.disableDelete}
                    onClick={() => this.onGroupDelete()}
                />
            </div>
        ];
    }

    private updateGroups(updateSelectedGroup: boolean) {
        /* sort groups */
        {
            const typeMappings = {
                "query": 3,
                "template": 2,
                "normal": 1
            };

            this.groups.sort((b, a) => {
                if (typeMappings[a.type] > typeMappings[b.type])
                    return 1;

                if (typeMappings[a.type] < typeMappings[b.type])
                    return -1;

                if (a.sortId < b.sortId)
                    return 1;

                if (a.sortId > b.sortId)
                    return -1;

                if (a.id > b.id)
                    return -1;

                if (a.id < b.id)
                    return 1;

                return 0;
            });
        }
        this.visibleGroups = this.groups.filter(e => e.type === "template" ? this.state.showTemplateGroups : e.type === "query" ? this.state.showQueryGroups : true);

        /* select any group */
        if (updateSelectedGroup && this.visibleGroups.findIndex(e => e.id === this.state.selectedGroupId) === -1 && this.visibleGroups.length !== 0)
            this.props.events.fire("action_select_group", {target: this.props.target, id: this.visibleGroups[0].id});
    }

    private scheduleUpdate() {
        clearTimeout(this.updateScheduleId);
        this.updateScheduleId = setTimeout(() => this.forceUpdate(), 10);
    }

    private selectedGroup() {
        return this.groups.find(e => e.id === this.state.selectedGroupId);
    }

    @EventHandler<PermissionModalEvents>("action_activate_tab")
    private handleGroupTabActive(events: PermissionModalEvents["action_activate_tab"]) {
        this.isListActive = this.props.target === "server" ? events.tab === "groups-server" : events.tab === "groups-channel";
        if (events.tab === "groups-server" || events.tab === "groups-channel") {
            if (typeof events.activeGroupId !== "undefined")
                this.setState({selectedGroupId: events.activeGroupId});

            if (this.isListActive)
                this.props.events.fire("action_set_permission_editor_subject", {
                    mode: events.tab,
                    groupId: events.activeGroupId || this.state.selectedGroupId,
                    clientDatabaseId: 0,
                    channelId: 0
                });
        }
    }

    @EventHandler<PermissionModalEvents>("notify_groups_reset")
    private handleReset() {
        this.groups.splice(0, this.groups.length);
    }

    @EventHandler<PermissionModalEvents>("notify_client_permissions")
    private handleClientPermissions(event: PermissionModalEvents["notify_client_permissions"]) {
        const selectedGroup = this.selectedGroup();

        this.modifyPower = this.props.target === "server" ? event.serverGroupModifyPower : event.channelGroupModifyPower;
        this.setState({
            showTemplateGroups: event.modifyTemplateGroups,
            showQueryGroups: event.modifyQueryGroups,

            disableGroupAdd: this.props.target === "server" ? !event.serverGroupCreate : !event.channelGroupCreate,
            disablePermissionCopy: this.props.target === "server" ? !event.serverGroupCreate : !event.channelGroupCreate,

            disableGroupRename: !selectedGroup || this.modifyPower === 0 || this.modifyPower < selectedGroup.needed_modify_power,
            disableDelete: !selectedGroup || this.modifyPower === 0 || this.modifyPower < selectedGroup.needed_modify_power,
        });
        /* this.forceUpdate(); */ /* No force update needed since if the state does not change the displayed groups would not either */
    }

    @EventHandler<PermissionModalEvents>("action_select_group")
    private handleSelect(event: PermissionModalEvents["action_select_group"]) {
        if (event.target !== this.props.target)
            return;

        if (this.state.selectedGroupId === event.id)
            return;

        const selectedGroup = this.groups.find(e => e.id === event.id);
        this.setState({
            selectedGroupId: event.id,

            disableGroupRename: !selectedGroup || this.modifyPower === 0 || this.modifyPower < selectedGroup.needed_modify_power,
            disableDelete: !selectedGroup || this.modifyPower === 0 || this.modifyPower < selectedGroup.needed_modify_power,
        });

        if (this.isListActive) {
            this.props.events.fire("action_set_permission_editor_subject", {
                mode: this.props.target === "server" ? "groups-server" : "groups-channel",
                groupId: event.id,
                clientDatabaseId: 0,
                channelId: 0
            });
        }
    }

    @EventHandler<PermissionModalEvents>("query_groups")
    private handleQuery(event: PermissionModalEvents["query_groups"]) {
        if (event.target !== this.props.target)
            return;

        this.groups.splice(0, this.groups.length);
    }

    @EventHandler<PermissionModalEvents>("notify_groups")
    private handleQueryResult(event: PermissionModalEvents["notify_groups"]) {
        if (event.target !== this.props.target)
            return;

        this.groups.splice(0, this.groups.length);
        this.groups.push(...event.groups);
        this.updateGroups(true);
        this.scheduleUpdate();
    }

    @EventHandler<PermissionModalEvents>("notify_groups_created")
    private handleGroupsCreated(event: PermissionModalEvents["notify_groups_created"]) {
        if (event.target !== this.props.target)
            return;

        this.groups.push(...event.groups);
        this.updateGroups(true);
        this.scheduleUpdate();
    }

    @EventHandler<PermissionModalEvents>("notify_groups_deleted")
    private handleGroupsDeleted(event: PermissionModalEvents["notify_groups_deleted"]) {
        if (event.target !== this.props.target)
            return;

        event.groups.forEach(id => {
            const index = this.groups.findIndex(e => e.id === id);
            if (index === -1) return;

            this.groups.splice(index, 1);
        });

        this.updateGroups(true);
        this.scheduleUpdate();
    }

    @EventHandler<PermissionModalEvents>("notify_group_updated")
    private handleGroupUpdated(event: PermissionModalEvents["notify_group_updated"]) {
        if (event.target !== this.props.target)
            return;

        const group = this.groups.find(e => e.id === event.id);
        if (!group) return;

        for (const update of event.properties) {
            switch (update.property) {
                case "name":
                    group.name = update.value;
                    break;

                case "icon":
                    group.iconId = update.value;
                    break;

                case "sort":
                    group.sortId = update.value;
                    break;

                case "save":
                    group.saveDB = update.value;
                    break;
            }
        }

        this.updateGroups(true);
        this.scheduleUpdate();
    }

    private onGroupRename() {
        const group = this.selectedGroup();
        if (!group) return;

        createInputModal(tr("Rename group"), tr("Enter the new group name"), name => {
            if (name.length < 3)
                return false;

            if (name.length > 64)
                return false;

            return this.groups.findIndex(e => e.name === name && e.type === group.type) === -1;
        }, result => {
            if (typeof result !== "string")
                return;

            this.props.events.fire("action_rename_group", {id: group.id, target: this.props.target, newName: result});
        }).open();
    }

    private onGroupDelete() {
        const group = this.selectedGroup();
        if (!group) return;

        this.props.events.fire("action_delete_group", {id: group.id, target: this.props.target, mode: "ask"});
    }

    componentDidMount(): void {
        this.props.events.fire("query_groups", {target: this.props.target});
    }
}


@ReactEventHandler<GroupsList>(e => e.props.events)
class ServerClientList extends React.Component<{ events: Registry<PermissionModalEvents> }, {
    selectedGroupId: number,
    selectedClientId: number,

    disableClientAdd: boolean,
    disableClientRemove: boolean,

    state: "loading" | "error" | "normal" | "no-permissions",
    error?: string;
}> {
    private readonly groups: GroupProperties[] = [];
    private clients: {
        name: string;
        databaseId: number;
        uniqueId: string;
    }[] = [];
    private clientMemberAddPower: number = 0;
    private clientMemberRemovePower: number = 0;

    constructor(props) {
        super(props);

        this.state = {
            selectedGroupId: 0,
            selectedClientId: 0,

            disableClientAdd: true,
            disableClientRemove: true,

            state: "loading"
        }
    }

    render() {
        const selectedGroup = this.selectedGroup();
        let groupTypePrefix = "";
        switch (selectedGroup?.type) {
            case "query":
                groupTypePrefix = "[Q] ";
                break;

            case "template":
                groupTypePrefix = "[T] ";
                break;
        }

        return [
            <div key={"list"} className={cssStyle.list + " " + cssStyle.containerList}
                 onContextMenu={e => this.onListContextMenu(e)}>
                {selectedGroup ? (
                    <ServerInfoContext.Consumer>
                        {serverInfo => (
                            <div key={"selected-group"} className={cssStyle.entry + " " + cssStyle.selectedGroup}>
                                <div className={cssStyle.icon}>
                                    <RemoteIconRenderer icon={getIconManager().resolveIcon(selectedGroup.iconId, serverInfo.serverUniqueId, serverInfo.handlerId)} />
                                </div>
                                <div className={cssStyle.name}>{groupTypePrefix + selectedGroup.name + " (" + selectedGroup.id + ")"}</div>
                            </div>
                        )}
                    </ServerInfoContext.Consumer>
                ) : undefined}
                <div className={cssStyle.entries}>
                    {this.clients.map(client => <div
                        key={"client-" + client.databaseId}
                        className={cssStyle.entry + " " + (this.state.selectedClientId === client.databaseId ? cssStyle.selected : "")}
                        onClick={() => this.setState({
                            selectedClientId: client.databaseId,
                            disableClientRemove: !selectedGroup || this.clientMemberRemovePower === 0 || selectedGroup.needed_member_remove > this.clientMemberRemovePower
                        })}
                        onContextMenu={e => {
                            e.preventDefault();

                            this.setState({selectedClientId: client.databaseId});
                            contextmenu.spawn_context_menu(e.pageX, e.pageY, {
                                type: contextmenu.MenuEntryType.ENTRY,
                                name: tr("Add client"),
                                icon_class: 'client-add',
                                callback: () => this.onClientAdd(),
                                invalidPermission: this.clientMemberAddPower === 0 || selectedGroup.needed_member_remove > this.clientMemberAddPower
                            }, {
                                type: contextmenu.MenuEntryType.ENTRY,
                                name: tr("Remove client"),
                                icon_class: 'client-delete',
                                callback: () => this.onClientRemove(),
                                invalidPermission: !selectedGroup || this.clientMemberRemovePower === 0 || selectedGroup.needed_member_remove > this.clientMemberRemovePower
                            }, {
                                type: contextmenu.MenuEntryType.ENTRY,
                                name: tr("Copy unique id"),
                                icon_class: 'client-copy',
                                callback: () => copyToClipboard(client.uniqueId)
                            }, contextmenu.Entry.HR(), {
                                type: contextmenu.MenuEntryType.ENTRY,
                                name: tr("Refresh"),
                                icon_class: 'client-refresh',
                                callback: () => this.onRefreshList()
                            })
                        }}
                    >{client.name || client.uniqueId}</div>)}
                </div>
                <div className={cssStyle.overlay + " " + (this.clients.length > 0 ? cssStyle.hidden : "")}>
                    <a><Translatable>This group contains no clients.</Translatable></a>
                </div>
                <div className={cssStyle.overlay + " " + (selectedGroup?.saveDB ? cssStyle.hidden : "")}>
                    <a><Translatable>This group is a temporary group.</Translatable></a>
                </div>
                <div
                    className={cssStyle.overlay + " " + cssStyle.error + " " + (this.state.state === "no-permissions" ? "" : cssStyle.hidden)}>
                    <a><Translatable>You don't have permissions to view the clients in this group</Translatable></a>
                </div>
                <div
                    className={cssStyle.overlay + " " + cssStyle.error + " " + (this.state.state === "error" ? "" : cssStyle.hidden)}>
                    <a><Translatable>Failed to query clients:</Translatable><br/>{this.state.error}</a>
                </div>
                <div className={cssStyle.overlay + " " + (selectedGroup ? cssStyle.hidden : "")}>
                    <a><Translatable>No group selected</Translatable></a>
                </div>
                <div className={cssStyle.overlay + " " + (this.state.state !== "loading" ? cssStyle.hidden : "")}>
                    <a><Translatable>loading</Translatable> <LoadingDots maxDots={3}/></a>
                </div>
            </div>,
            <div key={"buttons"} className={cssStyle.buttons}>
                <GroupsButton
                    image={"img/icon_group_add.svg"}
                    alt={tr("Add client")}
                    disabled={this.state.disableClientAdd}
                    onClick={() => this.onClientAdd()}
                />
                <GroupsButton
                    image={"img/icon_group_delete.svg"}
                    alt={tr("Remove client")}
                    disabled={this.state.disableClientRemove}
                    onClick={() => this.onClientRemove()}
                />
            </div>
        ];
    }

    private selectedClient() {
        return this.clients.find(e => e.databaseId === this.state.selectedClientId);
    }

    private selectedGroup() {
        return this.groups.find(e => e.id === this.state.selectedGroupId);
    }

    @EventHandler<PermissionModalEvents>("action_activate_tab")
    private handleGroupTabActive(events: PermissionModalEvents["action_activate_tab"]) {
        if (events.tab === "groups-server" || events.tab === "groups-channel") {
            if (typeof events.activeGroupId !== "undefined")
                this.setState({selectedGroupId: events.activeGroupId});
        }
    }

    @EventHandler<PermissionModalEvents>("query_group_clients")
    private handleQueryClients(events: PermissionModalEvents["query_group_clients"]) {
        if (events.id !== this.state.selectedGroupId)
            return;

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

    @EventHandler<PermissionModalEvents>("notify_group_clients")
    private handleQueryClientsResult(event: PermissionModalEvents["notify_group_clients"]) {
        if (event.id !== this.state.selectedGroupId)
            return;

        this.clients = (event.clients || []).slice(0);
        this.setState({
            state: event.status === "success" ? "normal" : event.status === "error" ? "error" : event.status === "no-permissions" ? "no-permissions" : "error",
            error: event.error || tr("unknown error")
        });
    }

    @EventHandler<PermissionModalEvents>("notify_client_list_toggled")
    private handleNotifyShow(events: PermissionModalEvents["notify_client_list_toggled"]) {
        if (!events.visible)
            return;

        this.props.events.fire("query_group_clients", {id: this.state.selectedGroupId});
    }

    @EventHandler<PermissionModalEvents>("notify_groups_reset")
    private handleReset() {
        this.groups.splice(0, this.groups.length);
        this.setState({selectedClientId: 0, selectedGroupId: 0})
    }

    @EventHandler<PermissionModalEvents>("notify_client_permissions")
    private handleClientPermissions(event: PermissionModalEvents["notify_client_permissions"]) {
        this.clientMemberAddPower = event.serverGroupMemberAddPower;
        this.clientMemberRemovePower = event.serverGroupMemberRemovePower;

        const group = this.selectedGroup();
        const client = this.selectedClient();
        this.setState({
            disableClientRemove: !client || this.clientMemberRemovePower === 0 || group.needed_member_remove > this.clientMemberRemovePower,
            disableClientAdd: !group || this.clientMemberAddPower === 0 || group.needed_member_add > this.clientMemberAddPower
        });
    }

    @EventHandler<PermissionModalEvents>("action_select_group")
    private handleSelect(event: PermissionModalEvents["action_select_group"]) {
        if (event.target !== "server")
            return;

        if (this.state.selectedGroupId === event.id)
            return;

        const selectedGroup = this.groups.find(e => e.id === event.id);
        const client = this.selectedClient();
        this.setState({
            selectedGroupId: event.id,

            disableClientRemove: !client || this.clientMemberRemovePower === 0 || selectedGroup.needed_member_remove > this.clientMemberRemovePower,
            disableClientAdd: !selectedGroup || this.clientMemberAddPower === 0 || selectedGroup.needed_member_add > this.clientMemberAddPower
        });
    }

    @EventHandler<PermissionModalEvents>("notify_groups")
    private handleQueryResult(event: PermissionModalEvents["notify_groups"]) {
        if (event.target !== "server")
            return;

        this.groups.splice(0, this.groups.length);
        this.groups.push(...event.groups);
        if (!this.selectedGroup())
            this.setState({selectedGroupId: 0, selectedClientId: 0});
    }

    @EventHandler<PermissionModalEvents>("notify_groups_created")
    private handleGroupsCreated(event: PermissionModalEvents["notify_groups_created"]) {
        if (event.target !== "server")
            return;

        this.groups.push(...event.groups);
    }

    @EventHandler<PermissionModalEvents>("notify_groups_deleted")
    private handleGroupsDeleted(event: PermissionModalEvents["notify_groups_deleted"]) {
        if (event.target !== "server")
            return;

        event.groups.forEach(id => {
            const index = this.groups.findIndex(e => e.id === id);
            if (index === -1) return;

            this.groups.splice(index, 1);
        });

        this.forceUpdate();
    }

    @EventHandler<PermissionModalEvents>("notify_group_updated")
    private handleGroupUpdated(event: PermissionModalEvents["notify_group_updated"]) {
        if (event.target !== "server")
            return;

        const group = this.groups.find(e => e.id === event.id);
        if (!group) return;

        for (const update of event.properties) {
            switch (update.property) {
                case "name":
                    group.name = update.value;
                    break;

                case "icon":
                    group.iconId = update.value;
                    break;

                case "sort":
                    group.sortId = update.value;
                    break;

                case "save":
                    group.saveDB = update.value;
                    break;
            }
        }

        if (this.state.selectedGroupId === event.id)
            this.forceUpdate();
    }

    @EventHandler<PermissionModalEvents>("action_server_group_add_client_result")
    private handleServerGroupAddClientResult(event: PermissionModalEvents["action_server_group_add_client_result"]) {
        if (event.id !== this.state.selectedGroupId || event.status !== "success")
            return;

        this.props.events.fire("query_group_clients", {id: this.state.selectedGroupId});
    }

    @EventHandler<PermissionModalEvents>("action_server_group_remove_client_result")
    private handleServerGroupRemoveClientResult(event: PermissionModalEvents["action_server_group_remove_client_result"]) {
        if (event.id !== this.state.selectedGroupId || event.status !== "success")
            return;

        this.props.events.fire("query_group_clients", {id: this.state.selectedGroupId});
    }

    private onRefreshList() {
        this.props.events.fire("query_group_clients", {id: this.state.selectedGroupId});
    }

    private onListContextMenu(event: React.MouseEvent) {
        if (event.isDefaultPrevented())
            return;

        contextmenu.spawn_context_menu(event.pageX, event.pageY, {
            type: contextmenu.MenuEntryType.ENTRY,
            name: tr("Add client"),
            icon_class: 'client-add',
            callback: () => this.onClientAdd()
        }, {
            type: contextmenu.MenuEntryType.ENTRY,
            name: tr("Refresh"),
            icon_class: 'client-refresh',
            callback: () => this.onRefreshList()
        });
    }

    private onClientAdd() {
        createInputModal(tr("Add client to server group"), tr("Enter the client unique id or database id"), text => {
            if (!text) return false;
            if (!!text.match(/^[0-9]+$/))
                return true;
            try {
                return atob(text).length == 20;
            } catch (error) {
                return false;
            }
        }, async text => {
            if (typeof (text) !== "string")
                return;

            let targetClient;
            if (!!text.match(/^[0-9]+$/)) {
                targetClient = parseInt(text);
            } else {
                targetClient = text.trim();
            }

            this.props.events.fire("action_server_group_add_client", {
                client: targetClient,
                id: this.state.selectedGroupId
            });
        }).open();
    }

    private onClientRemove() {
        this.props.events.fire("action_server_group_remove_client", {
            id: this.state.selectedGroupId,
            client: this.state.selectedClientId
        });
    }
}

const ServerGroupsSideBar = React.memo(() => {
    const events = useContext(ModalEventContext);
    const [active, setActive] = useState(true);
    const [clientList, setClientList] = useState(false);

    events.reactUse("action_activate_tab", event => setActive(event.tab === "groups-server"));
    events.reactUse("notify_client_list_toggled", event => setClientList(event.visible));

    return (
        <div
            className={cssStyle.sideContainer + " " + cssStyle.containerServerGroups + " " + (active ? "" : cssStyle.hidden)}>
            <div className={cssStyle.containerGroupList + " " + (!clientList ? "" : cssStyle.hidden)}>
                <GroupsList events={events} target={"server"}/>
            </div>
            <div className={cssStyle.containerClientList + " " + (clientList ? "" : cssStyle.hidden)}>
                <ServerClientList events={events} />
            </div>
        </div>
    );
});

const ChannelGroupsSideBar = React.memo(() => {
    const events = useContext(ModalEventContext);
    const [active, setActive] = useState(false);

    events.reactUse("action_activate_tab", event => setActive(event.tab === "groups-channel"));

    return (
        <div
            className={cssStyle.sideContainer + " " + cssStyle.containerChannelGroups + " " + (active ? "" : cssStyle.hidden)}>
            <GroupsList events={events} target={"channel"}/>
        </div>
    );
});

@ReactEventHandler<ChannelList>(e => e.props.events)
class ChannelList extends React.Component<{ serverInfo: PermissionEditorServerInfo, events: Registry<PermissionModalEvents>, tabTarget: "channel" | "client-channel" }, { selectedChanelId: number }> {
    private channels: ChannelInfo[] = [];
    private isActiveTab = false;

    constructor(props) {
        super(props);

        this.state = {
            selectedChanelId: 0
        }
    }

    render() {
        return (
            <div className={cssStyle.containerList + " " + cssStyle.listChannels} onContextMenu={e => {
                e.preventDefault();

                spawn_context_menu(e.pageX, e.pageY, {
                    type: MenuEntryType.ENTRY,
                    icon_class: "client-check_update",
                    name: tr("Refresh"),
                    callback: () => this.props.events.fire("query_channels")
                });
            }}>
                <div className={cssStyle.entries}>
                    {this.channels.map(e => (
                        <div key={"channel-" + e.id}
                             className={cssStyle.entry + " " + (e.id === this.state.selectedChanelId ? cssStyle.selected : "")}
                             style={{paddingLeft: `calc(0.25em + ${e.depth * 16}px)`}}
                             onClick={() => this.props.events.fire("action_select_channel", {
                                 target: this.props.tabTarget,
                                 id: e.id
                             })}
                        >
                            <RemoteIconRenderer icon={getIconManager().resolveIcon(e.iconId, this.props.serverInfo.serverUniqueId, this.props.serverInfo.handlerId)} />
                            <a className={cssStyle.name}>{e.name + " (" + e.id + ")"}</a>
                        </div>
                    ))}
                </div>
            </div>
        )
    }

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

    @EventHandler<PermissionModalEvents>("notify_channels")
    private handleQueryChannelsResult(event: PermissionModalEvents["notify_channels"]) {
        this.channels = event.channels.slice(0);
        if (this.channels.length > 0 && this.channels.findIndex(e => e.id === this.state.selectedChanelId) === -1)
            this.setState({selectedChanelId: this.channels[0].id});
        else
            this.forceUpdate();
    }

    @EventHandler<PermissionModalEvents>("notify_channel_updated")
    private handleChannelUpdated(event: PermissionModalEvents["notify_channel_updated"]) {
        const channel = this.channels.find(e => e.id === event.id);
        if (!channel) return;

        switch (event.property) {
            case "icon":
                channel.iconId = event.value;
                break;

            case "name":
                channel.name = event.value;
                break;

            default:
                return;
        }

        this.forceUpdate();
    }

    @EventHandler<PermissionModalEvents>("action_select_channel")
    private handleActionSelectChannel(event: PermissionModalEvents["action_select_channel"]) {
        if (event.target !== this.props.tabTarget) {
            return;
        }

        this.setState({selectedChanelId: event.id});
        if (this.isActiveTab) {
            this.props.events.fire("action_set_permission_editor_subject", {
                mode: this.props.tabTarget,
                channelId: event.id
            });
        }
    }

    @EventHandler<PermissionModalEvents>("action_activate_tab")
    private handleActionTabSelect(event: PermissionModalEvents["action_activate_tab"]) {
        this.isActiveTab = event.tab === this.props.tabTarget;
        if (!this.isActiveTab) {
            return;
        }

        if (typeof event.activeChannelId === "number") {
            this.setState({ selectedChanelId: event.activeChannelId });
        }

        this.props.events.fire_later("action_set_permission_editor_subject", {
            mode: this.props.tabTarget,
            channelId: typeof event.activeChannelId === "number" ? event.activeChannelId : this.state.selectedChanelId
        });
    }
}

const ChannelSideBar = React.memo(() => {
    const serverInfo = useContext(ServerInfoContext);
    const events = useContext(ModalEventContext);
    const [active, setActive] = useState(false);

    events.reactUse("action_activate_tab", event => setActive(event.tab === "channel"));

    return (
        <div
            className={cssStyle.sideContainer + " " + cssStyle.containerChannels + " " + (active ? "" : cssStyle.hidden)}>
            <ChannelList serverInfo={serverInfo} events={events} tabTarget={"channel"}/>
        </div>
    );
});

const ClientSelect = React.memo((props: { tabTarget: "client" | "client-channel" }) => {
    const events = React.useContext(ModalEventContext);

    const [clientIdentifier, setClientIdentifier] = useState<number | string | undefined>(undefined);
    const [clientInfo, setClientInfo] = useState<{ name: string, uniqueId: string, databaseId: number }>(undefined);

    const refInput = useRef<FlatInputField>();
    const refNickname = useRef<FlatInputField>();
    const refUniqueIdentifier = useRef<FlatInputField>();
    const refDatabaseId = useRef<FlatInputField>();

    events.reactUse("action_activate_tab", event => {
        if (event.tab !== props.tabTarget) {
            return;
        }

        if (typeof event.activeClientDatabaseId !== "undefined") {
            events.fire("action_select_client", {
                target: props.tabTarget,
                id: event.activeClientDatabaseId === 0 ? "undefined" : event.activeClientDatabaseId
            });
        } else {
            if (clientInfo && clientInfo.databaseId) {
                events.fire("action_set_permission_editor_subject", {
                    mode: props.tabTarget,
                    clientDatabaseId: clientInfo.databaseId
                });
            } else {
                events.fire("action_set_permission_editor_subject", {mode: props.tabTarget, clientDatabaseId: 0});
            }
        }
    });

    const resetInfoFields = (placeholder?: string) => {
        refNickname.current?.setValue(undefined);
        refUniqueIdentifier.current?.setValue(undefined);
        refDatabaseId.current?.setValue(undefined);

        refNickname.current?.setState({placeholder: placeholder});
        refUniqueIdentifier.current?.setState({placeholder: placeholder});
        refDatabaseId.current?.setState({placeholder: placeholder});
    };

    events.reactUse("query_client_info", event => {
        if (event.client !== clientIdentifier)
            return;

        refInput.current?.setState({disabled: true});
        resetInfoFields(tr("loading..."));
        events.fire("action_set_permission_editor_subject", {mode: props.tabTarget, clientDatabaseId: 0});
    });

    events.reactUse("notify_client_info", event => {
        if (event.client !== clientIdentifier)
            return;

        refInput.current?.setState({disabled: false});
        if (event.state === "success") {
            setClientInfo(event.info);

            refNickname.current?.setValue(event.info.name);
            refUniqueIdentifier.current?.setValue(event.info.uniqueId);
            refDatabaseId.current?.setValue(event.info.databaseId + "");
            events.fire("action_set_permission_editor_subject", {
                mode: props.tabTarget,
                clientDatabaseId: event.info.databaseId
            });
            return;
        } else if (event.state === "error") {
            refInput.current.setState({disabled: false, isInvalid: true, invalidMessage: event.error});
        } else if (event.state === "no-permission") {
            refInput.current.setState({
                disabled: false,
                isInvalid: true,
                invalidMessage: tra("failed on permission {0}", event.failedPermission)
            });
        } else if (event.state === "no-such-client") {
            refInput.current.setState({disabled: false, isInvalid: true, invalidMessage: tr("no client found")});
            refInput.current.focus();
        }

        refNickname.current?.setState({placeholder: undefined});
        refUniqueIdentifier.current?.setState({placeholder: undefined});
        refDatabaseId.current?.setState({placeholder: undefined});
    });

    events.reactUse("action_select_client", event => {
        if (event.target !== props.tabTarget)
            return;

        setClientIdentifier(event.id);
        refInput.current.setValue(typeof event.id === "undefined" ? "" : event.id.toString());
        if (typeof event.id === "number" || typeof event.id === "string") {
            /* first do the state update */
            events.fire_react("query_client_info", {client: event.id});
        } else {
            refInput.current?.setValue(undefined);
            resetInfoFields(undefined);
            events.fire("action_set_permission_editor_subject", {mode: props.tabTarget, clientDatabaseId: 0});
        }
    });

    return (
        <div className={cssStyle.clientSelect}>
            <FlatInputField
                ref={refInput}
                label={tr("Database or Unique ID")}
                className={cssStyle.inputField}
                labelType={"floating"}
                finishOnEnter={true}
                onInput={value => {
                    if (value.match(/^[0-9]{1,8}$/)) {
                        refInput.current?.setState({isInvalid: false});
                    } else if (value.length === 0) {
                        refInput.current?.setState({isInvalid: false});
                    } else {
                        try {
                            if (arrayBufferBase64(value).byteLength !== 20)
                                throw void 0;

                            refInput.current?.setState({isInvalid: false});
                        } catch (e) {
                            refInput.current?.setState({isInvalid: true, invalidMessage: undefined});
                        }
                    }
                }}
                onBlur={() => {
                    const value = refInput.current.value();
                    let client;
                    if (value.match(/^[0-9]{1,8}$/)) {
                        client = parseInt(value);
                    } else if (value.length === 0) {
                        client = undefined;
                    } else {
                        try {
                            if (arrayBufferBase64(value).byteLength !== 20 && value !== "serveradmin") {
                                refInput.current?.setState({
                                    isInvalid: true,
                                    invalidMessage: tr("Invalid UUID length")
                                });
                                return;
                            }

                            client = value;
                        } catch (e) {
                            refInput.current?.setState({isInvalid: true, invalidMessage: tr("Invalid UUID")});
                            return;
                        }
                    }
                    refInput.current?.setState({isInvalid: false});
                    events.fire("action_select_client", {id: client, target: props.tabTarget});
                }}
            />
            <hr/>
            <FlatInputField ref={refNickname} label={tr("Nickname")} className={cssStyle.infoField} disabled={true}/>
            <FlatInputField ref={refUniqueIdentifier} label={tr("Unique identifier")} className={cssStyle.infoField}
                            disabled={true}/>
            <FlatInputField ref={refDatabaseId} label={tr("Client database ID")} className={cssStyle.infoField}
                            disabled={true}/>
        </div>
    );
});

const ClientSideBar = React.memo(() => {
    const events = useContext(ModalEventContext);
    const [active, setActive] = useState(false);

    events.reactUse("action_activate_tab", event => setActive(event.tab === "client"));

    return (
        <div
            className={cssStyle.sideContainer + " " + cssStyle.containerClient + " " + (active ? "" : cssStyle.hidden)}>
            <ClientSelect tabTarget={"client"}/>
        </div>
    );
});

const ClientChannelSideBar = React.memo(() => {
    const serverInfo = useContext(ServerInfoContext);
    const events = useContext(ModalEventContext);
    const [active, setActive] = useState(false);

    events.reactUse("action_activate_tab", event => setActive(event.tab === "client-channel"));

    return (
        <div
            className={cssStyle.sideContainer + " " + cssStyle.containerChannelClient + " " + (active ? "" : cssStyle.hidden)}>
            <ClientSelect tabTarget={"client-channel"}/>
            <ChannelList serverInfo={serverInfo} events={events} tabTarget={"client-channel"}/>
        </div>
    );
});

export const PermissionTabName: { [T in PermissionEditorTab]: { name: string, useTranslate: () => string, renderTranslate: () => React.ReactNode } } = {
    "groups-server": {name: "Server Groups", useTranslate: () => useTr("Server Groups"), renderTranslate: () => <Translatable>Server Groups</Translatable>},
    "groups-channel": {name: "Channel Groups", useTranslate: () => useTr("Channel Groups"), renderTranslate: () => <Translatable>Channel Groups</Translatable>},
    "channel": {name: "Channel Permissions", useTranslate: () => useTr("Channel Permissions"), renderTranslate: () => <Translatable>Channel Permissions</Translatable>},
    "client": {name: "Client Permissions", useTranslate: () => useTr("Client Permissions"), renderTranslate: () => <Translatable>Client Permissions</Translatable>},
    "client-channel": {name: "Client Channel Permissions", useTranslate: () => useTr("Client Channel Permissions"), renderTranslate: () => <Translatable>Client Channel Permissions</Translatable>},
};

const ActiveTabInfo = React.memo(() => {
    const events = useContext(ModalEventContext);
    const [activeTab, setActiveTab] = useState<PermissionEditorTab>("groups-server");
    events.reactUse("action_activate_tab", event => setActiveTab(event.tab));

    return (
        <div className={cssStyle.header + " " + cssStyle.activeTabInfo}>
            <div className={cssStyle.entry}>
                <a title={PermissionTabName[activeTab].useTranslate()} key={"tab-" + activeTab}>
                    {PermissionTabName[activeTab].renderTranslate()}
                </a>
            </div>
        </div>
    );
});

const TabSelectorEntry = React.memo((props: { entry: PermissionEditorTab }) => {
    const events = useContext(ModalEventContext);
    const [active, setActive] = useState(props.entry === "groups-server");

    events.reactUse("action_activate_tab", event => setActive(event.tab === props.entry));

    return (
        <div className={cssStyle.entry + " " + (active ? cssStyle.selected : "")}
             onClick={() => !active && events.fire("action_activate_tab", {tab: props.entry})}>
            <a title={PermissionTabName[props.entry].useTranslate()}>
                {PermissionTabName[props.entry].renderTranslate()}
            </a>
        </div>
    );
});

const TabSelector = React.memo(() => {
    return (
        <div className={cssStyle.header + " " + cssStyle.tabSelector}>
            <TabSelectorEntry entry={"groups-server"}/>
            <TabSelectorEntry entry={"groups-channel"}/>
            <TabSelectorEntry entry={"channel"}/>
            <TabSelectorEntry entry={"client"}/>
            <TabSelectorEntry entry={"client-channel"}/>
        </div>
    );
});

const InitialRendererTrigger = React.memo(() => {
    const events = useContext(ModalEventContext);
    useEffect(() => events.fire("notify_initial_rendered"), []);
    return null;
})

export type DefaultTabValues = { groupId?: number, channelId?: number, clientDatabaseId?: number };
export class PermissionEditorModal extends AbstractModal {
    readonly serverInfo: PermissionEditorServerInfo;
    readonly modalEvents: Registry<PermissionModalEvents>;
    readonly editorEvents: Registry<PermissionEditorEvents>;

    constructor(serverInfo: PermissionEditorServerInfo, modalEvents: IpcRegistryDescription<PermissionModalEvents>, editorEvents: IpcRegistryDescription<PermissionEditorEvents>) {
        super();

        this.serverInfo = serverInfo;
        this.modalEvents = Registry.fromIpcDescription(modalEvents);
        this.editorEvents = Registry.fromIpcDescription(editorEvents);
    }

    protected onDestroy() {
        super.onDestroy();

        this.modalEvents.destroy();
        this.editorEvents.destroy();
    }

    renderBody() {
        return (
            <ModalEventContext.Provider value={this.modalEvents}>
                <EditorEventContext.Provider value={this.editorEvents}>
                    <ServerInfoContext.Provider value={this.serverInfo}>
                        <div className={cssStyle.container}>
                            <div className={cssStyle.contextContainer + " " + cssStyle.left}>
                                <ActiveTabInfo />
                                <ServerGroupsSideBar key={"server-groups"} />
                                <ChannelGroupsSideBar key={"channel-groups"} />
                                <ChannelSideBar key={"channel"} />
                                <ClientSideBar key={"client"} />
                                <ClientChannelSideBar key={"client-channel"} />
                            </div>
                            <ContextDivider id={"permission-editor"} defaultValue={25} direction={"horizontal"} />
                            <div className={cssStyle.contextContainer + " " + cssStyle.right}>
                                <TabSelector />
                                <EditorRenderer events={this.editorEvents} handlerId={this.serverInfo.handlerId} serverUniqueId={this.serverInfo.serverUniqueId} />
                            </div>
                        </div>
                        <InitialRendererTrigger />
                    </ServerInfoContext.Provider>
                </EditorEventContext.Provider>
            </ModalEventContext.Provider>
        );
    }

    renderTitle(): React.ReactElement<Translatable> {
        return <Translatable>Server permission editor</Translatable>;
    }
}

export default PermissionEditorModal;