import * as React from "react";
import {useEffect, useRef, useState} from "react";
import {FlatInputField} from "tc-shared/ui/react-elements/InputField";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
import {Switch} from "tc-shared/ui/react-elements/Switch";
import PermissionType from "tc-shared/permission/PermissionType";
import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";

import ResizeObserver from "resize-observer-polyfill";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import {Button} from "tc-shared/ui/react-elements/Button";
import {IconRenderer, LocalIconRenderer} from "tc-shared/ui/react-elements/Icon";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {copy_to_clipboard} from "tc-shared/utils/helpers";
import {createInfoModal} from "tc-shared/ui/elements/Modal";

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

export interface EditorGroupedPermissions {
    groupId: string,
    groupName: string,
    permissions: {
        id: number,
        name: string;
        description: string;
    }[],
    children: EditorGroupedPermissions[]
}

type PermissionEditorMode = "unset" | "no-permissions" | "normal";

export interface PermissionEditorEvents {
    action_set_mode: { mode: PermissionEditorMode, failedPermission?: string }
    action_toggle_client_button: { visible: boolean },
    action_toggle_client_list: { visible: boolean },

    action_set_filter: { filter?: string }
    action_set_assigned_only: { value: boolean }

    action_set_default_value: { value: number },

    action_open_icon_select: { iconId?: number }
    action_set_senseless_permissions: { permissions: string[] }

    action_remove_permissions: {
        permissions: {
            name: string;
            mode: "value" | "grant";
        }[]
    }
    action_remove_permissions_result: {
        permissions: {
            name: string;
            mode: "value" | "grant";
            success: boolean;
        }[]
    }

    action_set_permissions: {
        permissions: {
            name: string;

            mode: "value" | "grant";

            value?: number;
            flagNegate?: boolean;
            flagSkip?: boolean;
        }[]
    }
    action_set_permissions_result: {
        permissions: {
            name: string;

            mode: "value" | "grant";

            newValue?: number; /* undefined if it didnt worked */
            flagNegate?: boolean;
            flagSkip?: boolean;
        }[]
    }

    action_toggle_group: {
        groupId: string | null; /* if null, all groups are affected */
        collapsed: boolean;
    }

    action_start_permission_edit: {
        target: "value" | "grant";
        permission: string;
        defaultValue: number;
    },

    action_add_permission_group: {
        groupId: string,
        mode: "value" | "grant";
    },
    action_remove_permission_group: {
        groupId: string
        mode: "value" | "grant";
    }

    query_permission_list: {},
    query_permission_list_result: {
        hideSenselessPermissions: boolean;
        permissions: EditorGroupedPermissions[]
    },

    query_permission_values: {},
    query_permission_values_result: {
        status: "error" | "success"; /* no perms will cause a action_set_mode event with no permissions */

        error?: string;
        permissions?: {
            name: string;
            value?: number;
            flagNegate?: boolean;
            flagSkip?: boolean;
            granted?: number;
        }[]
    }
}

const ButtonIconPreview = (props: { events: Registry<PermissionEditorEvents>, connection: ConnectionHandler }) => {
    const [iconId, setIconId] = useState(0);
    const [unset, setUnset] = useState(true);

    props.events.reactUse("action_set_mode", event => setUnset(event.mode !== "normal"));

    props.events.reactUse("action_remove_permissions_result", event => {
        const iconPermission = event.permissions.find(e => e.name === PermissionType.I_ICON_ID);
        if (!iconPermission || !iconPermission.success) return;

        if (iconPermission.mode === "value")
            setIconId(0);
    });

    props.events.reactUse("action_set_permissions_result", event => {
        const iconPermission = event.permissions.find(e => e.name === PermissionType.I_ICON_ID);
        if (!iconPermission) return;

        if (typeof iconPermission.newValue === "number")
            setIconId(iconPermission.newValue);
    });

    props.events.reactUse("query_permission_values_result", event => {
        if (event.status !== "success") {
            setIconId(0);
            return;
        }

        const permission = event.permissions.find(e => e.name === PermissionType.I_ICON_ID);
        if (!permission) {
            setIconId(0);
            return;
        }

        if (typeof permission.value === "number") {
            setIconId(permission.value >>> 0);
        } else {
            setIconId(0);
        }
    });

    let icon;
    if (!unset && iconId > 0)
        icon = <LocalIconRenderer key={"icon-" + iconId} icon={props.connection.fileManager.icons.load_icon(iconId)}/>;

    return (
        <div className={cssStyle.containerIconSelect}>
            <div className={cssStyle.preview}
                 onClick={() => props.events.fire("action_open_icon_select", {iconId: iconId})}>
                {icon}
            </div>
            <div className={cssStyle.containerDropdown}>
                <div className={cssStyle.button}>
                    <div className="arrow down"/>
                </div>
                <div className={cssStyle.dropdown}>
                    {iconId ? <div className={cssStyle.entry} key={"edit-icon"}
                                   onClick={() => props.events.fire("action_open_icon_select", {iconId: iconId})}>
                        <Translatable>Edit icon</Translatable>
                    </div> : undefined}
                    {iconId ? <div className={cssStyle.entry} key={"remove-icon"}
                                   onClick={() => props.events.fire("action_remove_permissions", {
                                       permissions: [{
                                           name: PermissionType.I_ICON_ID,
                                           mode: "value"
                                       }]
                                   })}>
                        <Translatable>Remove icon</Translatable>
                    </div> : undefined}
                    {!iconId ? <div className={cssStyle.entry} key={"add-icon"}
                                    onClick={() => props.events.fire("action_open_icon_select", {iconId: 0})}>
                        <Translatable>Add icon</Translatable>
                    </div> : undefined}
                </div>
            </div>
        </div>
    );
};

const ClientListButton = (props: { events: Registry<PermissionEditorEvents> }) => {
    const [visible, setVisible] = useState(true);
    const [toggled, setToggled] = useState(false);

    props.events.reactUse("action_toggle_client_button", event => setVisible(event.visible));
    props.events.reactUse("action_toggle_client_list", event => setToggled(event.visible));

    return <Button
        key={"button-clients"}
        className={cssStyle.clients + " " + (visible ? "" : cssStyle.hidden)}
        color={"green"}
        onClick={() => props.events.fire("action_toggle_client_list", {visible: !toggled})}>
        {toggled ? <Translatable key={"hide"}>Hide clients in group</Translatable> :
            <Translatable key={"show"}>Show clients in group</Translatable>}
    </Button>
};

const MenuBar = (props: { events: Registry<PermissionEditorEvents>, connection: ConnectionHandler }) => {
    return <div className={cssStyle.containerMenuBar}>
        <ClientListButton events={props.events}/>
        <FlatInputField
            className={cssStyle.filter}

            label={<Translatable>Filter permissions</Translatable>}
            labelType={"floating"}
            labelClassName={cssStyle.label}
            labelFloatingClassName={cssStyle.labelFloating}
            onInput={text => props.events.fire("action_set_filter", {filter: text})}
        />
        <div className={cssStyle.options}>
            <Switch initialState={false} label={<Translatable>Assigned only</Translatable>}
                    onChange={state => props.events.fire("action_set_assigned_only", {value: state})}/>
            { /* <Switch initialState={true} label={<Translatable>Editable only</Translatable>} /> */}
        </div>
        <ButtonIconPreview events={props.events} connection={props.connection}/>
    </div>;
};

interface LinkedGroupedPermissions {
    groupId: string;
    groupName: string;

    depth: number;
    parent: LinkedGroupedPermissions | undefined;
    children: LinkedGroupedPermissions[];

    permissions: {
        id: number;
        name: string;
        description: string;

        elementVisible: boolean;
    }[],
    anyPermissionVisible: boolean;

    nextGroup: LinkedGroupedPermissions;
    nextIfCollapsed: LinkedGroupedPermissions;

    collapsed: boolean;

    elementVisible: boolean;
}

const PermissionEntryRow = (props: {
    events: Registry<PermissionEditorEvents>,
    groupId: string,
    permission: string,
    value: PermissionValue,
    isOdd: boolean,
    depth: number,
    offsetTop: number,
    defaultValue: number,
    description: string
}) => {
    const [defaultValue, setDefaultValue] = useState(props.defaultValue);
    const [value, setValue] = useState<number>(props.value.value);
    const [forceValueUpdate, setForceValueUpdate] = useState(false);
    const [valueEditing, setValueEditing] = useState(false);
    const [valueApplying, setValueApplying] = useState(false);

    const [flagNegated, setFlagNegated] = useState(props.value.flagNegate);
    const [flagSkip, setFlagSkip] = useState(props.value.flagSkip);

    const [granted, setGranted] = useState(props.value.granted);
    const [forceGrantedUpdate, setForceGrantedUpdate] = useState(false);
    const [grantedEditing, setGrantedEditing] = useState(false);
    const [grantedApplying, setGrantedApplying] = useState(false);

    const refGranted = useRef<HTMLInputElement>();
    const refValueI = useRef<HTMLInputElement>();
    const refValueB = useRef<Switch>();

    const refSkip = useRef<Switch>();
    const refNegate = useRef<Switch>();

    const isActive = typeof value === "number" || typeof granted === "number";
    const isBoolPermission = props.permission.startsWith("b_");

    let valueElement, skipElement, negateElement, grantedElement;
    if (typeof value === "number") {
        if (isBoolPermission) {
            valueElement = <Switch ref={refValueB} key={"value-b"} initialState={value >= 1} disabled={valueApplying}
                                   onChange={flag => {
                                       props.events.fire("action_set_permissions", {
                                           permissions: [{
                                               name: props.permission,
                                               mode: "value",
                                               value: flag ? 1 : 0,
                                               flagSkip: flagSkip,
                                               flagNegate: flagNegated
                                           }]
                                       });
                                   }} onBlur={() => setValueEditing(false)}/>;
        } else if (valueApplying) {
            valueElement =
                <input key={"value-i-applying"} className={cssStyle.applying} type="number" placeholder={tr("applying")}
                       readOnly={true} onChange={() => {
                }}/>;
        } else {
            valueElement =
                <input ref={refValueI} key={"value-i"} type="number" disabled={valueApplying} defaultValue={value}
                       onBlur={() => {
                           setValueEditing(false);
                           if (!refValueI.current)
                               return;

                           const newValue = refValueI.current.value;
                           if (newValue === "") {
                               if (typeof value !== "number" && !forceValueUpdate) {
                                   /* no change */
                                   return;
                               }

                               setForceValueUpdate(false);
                               props.events.fire("action_remove_permissions", {
                                   permissions: [{
                                       name: props.permission,
                                       mode: "value"
                                   }]
                               });
                           } else {
                               const numberValue = parseInt(newValue);
                               if (isNaN(numberValue)) return;
                               if (numberValue === value && !forceValueUpdate) {
                                   /* no change */
                                   return;
                               }

                               setForceValueUpdate(false);
                               props.events.fire("action_set_permissions", {
                                   permissions: [{
                                       name: props.permission,
                                       mode: "value",
                                       value: numberValue,
                                       flagSkip: flagSkip,
                                       flagNegate: flagNegated
                                   }]
                               });
                           }
                       }} onChange={() => {
                }} onKeyPress={e => e.key === "Enter" && e.currentTarget.blur()}/>;
        }

        skipElement = <Switch key={"skip"} initialState={flagSkip} disabled={valueApplying} onChange={flag => {
            props.events.fire("action_set_permissions", {
                permissions: [{
                    name: props.permission,
                    mode: "value",
                    value: value,
                    flagSkip: flag,
                    flagNegate: flagNegated
                }]
            });
        }}/>;
        negateElement = <Switch key={"negate"} initialState={flagNegated} disabled={valueApplying} onChange={flag => {
            props.events.fire("action_set_permissions", {
                permissions: [{
                    name: props.permission,
                    mode: "value",
                    value: value,
                    flagSkip: flagSkip,
                    flagNegate: flag
                }]
            });
        }}/>;
    }

    if (typeof granted === "number") {
        if (grantedApplying) {
            grantedElement =
                <input key={"grant-applying"} className={cssStyle.applying} type="number" placeholder={tr("applying")}
                       readOnly={true} onChange={() => {
                }}/>;
        } else {
            grantedElement = <input ref={refGranted} key={"grant"} type="number" defaultValue={granted} onBlur={() => {
                setGrantedEditing(false);
                if (!refGranted.current)
                    return;

                const newValue = refGranted.current.value;
                if (newValue === "") {
                    if (typeof granted === "undefined")
                        return;

                    setForceGrantedUpdate(true);
                    props.events.fire("action_remove_permissions", {
                        permissions: [{
                            name: props.permission,
                            mode: "grant"
                        }]
                    });
                } else {
                    const numberValue = parseInt(newValue);
                    if (isNaN(numberValue)) return;
                    if (numberValue === granted && !forceGrantedUpdate) {
                        /* no change */
                        return;
                    }

                    setForceGrantedUpdate(true);
                    props.events.fire("action_set_permissions", {
                        permissions: [{
                            name: props.permission,
                            mode: "grant",
                            value: numberValue
                        }]
                    });
                }
            }} onChange={() => {
            }} onKeyPress={e => e.key === "Enter" && e.currentTarget.blur()}/>;
        }
    }

    props.events.reactUse("action_start_permission_edit", event => {
        if (event.permission !== props.permission)
            return;

        if (event.target === "grant") {
            setGranted(event.defaultValue);
            setGrantedEditing(true);
            setForceGrantedUpdate(true);
        } else {
            if (isBoolPermission && typeof value === "undefined") {
                setValue(event.defaultValue >= 1 ? 1 : 0);
                props.events.fire("action_set_permissions", {
                    permissions: [{
                        name: props.permission,
                        mode: "value",
                        value: event.defaultValue >= 1 ? 1 : 0,
                        flagSkip: flagSkip,
                        flagNegate: flagNegated
                    }]
                });
            } else {
                setValue(event.defaultValue);
                setForceValueUpdate(true);
                setValueEditing(true);
            }
        }
    });

    props.events.reactUse("action_set_permissions", event => {
        const values = event.permissions.find(e => e.name === props.permission);
        if (!values) return;

        if (values.mode === "value") {
            setValueApplying(true);
            refSkip.current?.setState({disabled: true});
            refNegate.current?.setState({disabled: true});
        } else {
            setGrantedApplying(true);
        }
    });

    props.events.reactUse("action_set_permissions_result", event => {
        const result = event.permissions.find(e => e.name === props.permission);
        if (!result) return;

        if (result.mode === "value") {
            setValueApplying(false);
            if (typeof result.newValue === "number") {
                setValue(result.newValue);
                setFlagSkip(result.flagSkip);
                setFlagSkip(result.flagNegate);

                refValueB.current?.setState({disabled: false, checked: result.newValue >= 1});
                refSkip.current?.setState({disabled: false, checked: result.flagSkip});
                refNegate.current?.setState({disabled: false, checked: result.flagNegate});
                refValueI.current && (refValueI.current.value = result.newValue.toString());

                props.value.value = result.newValue;
                props.value.flagSkip = result.flagSkip;
                props.value.flagNegate = result.flagNegate;
            } else {
                refValueB.current?.setState({disabled: false, checked: props.value.value >= 1});
                refSkip.current?.setState({disabled: false, checked: props.value.flagSkip});
                refNegate.current?.setState({disabled: false, checked: props.value.flagNegate});
                refValueI.current && (refValueI.current.value = props.value.value?.toString());

                setValue(props.value.value);
                setFlagSkip(props.value.flagSkip);
                setFlagSkip(props.value.flagNegate);
            }
        } else {
            setGrantedApplying(false);
            if (typeof result.newValue === "number") {
                setGranted(result.newValue);
                refGranted.current && (refGranted.current.value = result.newValue.toString());
            } else {
                setGranted(props.value.granted);
                refGranted.current && (refGranted.current.value = props.value.granted?.toString());
            }
        }
    });

    props.events.reactUse("action_remove_permissions", event => {
        const modes = event.permissions.find(e => e.name === props.permission);
        if (!modes) return;

        if (modes.mode === "value") {
            setValueApplying(true);
            refValueB.current?.setState({disabled: true});
            refSkip.current?.setState({disabled: true});
            refNegate.current?.setState({disabled: true});
        }

        if (modes.mode === "grant")
            setGrantedApplying(true);
    });

    props.events.reactUse("action_remove_permissions_result", event => {
        const modes = event.permissions.find(e => e.name === props.permission);
        if (!modes) return;

        if (modes.mode === "value") {
            modes.success && setValue(undefined);
            setValueApplying(false);
            setValueEditing(false);

            modes.success && setFlagSkip(false);
            modes.success && setFlagNegated(false);
        }

        if (modes.mode === "grant") {
            modes.success && setGranted(undefined);
            setGrantedEditing(false);
            setGrantedApplying(false);
        }
    });

    props.events.reactUse("action_set_default_value", event => setDefaultValue(event.value));

    useEffect(() => {
        if (grantedEditing)
            refGranted.current?.focus();

        if (valueEditing) {
            refValueI.current?.focus();
            refValueB.current?.focus();
        }
    });

    return (
        <div
            className={cssStyle.row + " " + cssStyle.permission + " " + (props.isOdd ? "" : cssStyle.even) + " " + (isActive ? cssStyle.active : "")}
            style={{paddingLeft: (props.depth + 1) + "em", top: props.offsetTop}}
            onDoubleClick={e => {
                if (e.isDefaultPrevented())
                    return;

                props.events.fire("action_start_permission_edit", {
                    permission: props.permission,
                    target: "value",
                    defaultValue: defaultValue
                });
                e.preventDefault();
            }}
            onContextMenu={e => {
                e.preventDefault();

                let entries: contextmenu.MenuEntry[] = [];
                if (typeof value === "undefined") {
                    entries.push({
                        type: contextmenu.MenuEntryType.ENTRY,
                        name: tr("Add permission"),
                        callback: () => props.events.fire("action_start_permission_edit", {
                            permission: props.permission,
                            target: "value",
                            defaultValue: defaultValue
                        })
                    });
                } else {
                    entries.push({
                        type: contextmenu.MenuEntryType.ENTRY,
                        name: tr("Remove permission"),
                        callback: () => props.events.fire("action_remove_permissions", {
                            permissions: [{
                                name: props.permission,
                                mode: "value"
                            }]
                        })
                    });
                }

                if (typeof granted === "undefined") {
                    entries.push({
                        type: contextmenu.MenuEntryType.ENTRY,
                        name: tr("Add grant permission"),
                        callback: () => props.events.fire("action_start_permission_edit", {
                            permission: props.permission,
                            target: "grant",
                            defaultValue: defaultValue
                        })
                    });
                } else {
                    entries.push({
                        type: contextmenu.MenuEntryType.ENTRY,
                        name: tr("Remove grant permission"),
                        callback: () => props.events.fire("action_remove_permissions", {
                            permissions: [{
                                name: props.permission,
                                mode: "grant"
                            }]
                        })
                    });
                }

                entries.push(contextmenu.Entry.HR());
                entries.push({
                    type: contextmenu.MenuEntryType.ENTRY,
                    name: tr("Collapse group"),
                    callback: () => props.events.fire("action_toggle_group", {groupId: props.groupId, collapsed: true})
                });
                entries.push({
                    type: contextmenu.MenuEntryType.ENTRY,
                    name: tr("Expend all"),
                    callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: false})
                });
                entries.push({
                    type: contextmenu.MenuEntryType.ENTRY,
                    name: tr("Collapse all"),
                    callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: true})
                });
                entries.push(contextmenu.Entry.HR());
                entries.push({
                    type: contextmenu.MenuEntryType.ENTRY,
                    name: tr("Show permission description"),
                    callback: () => {
                        createInfoModal(
                            tr("Permission description"),
                            tr("Permission description for permission ") + props.permission + ": <br>" + props.description
                        ).open();
                    }
                });
                entries.push({
                    type: contextmenu.MenuEntryType.ENTRY,
                    name: tr("Copy permission name"),
                    callback: () => copy_to_clipboard(props.permission)
                });

                contextmenu.spawn_context_menu(e.pageX, e.pageY, ...entries);
            }}
        >
            <div className={cssStyle.columnName}>
                {props.permission}
            </div>
            <div className={cssStyle.columnValue}>{valueElement}</div>
            <div className={cssStyle.columnSkip}>{skipElement}</div>
            <div className={cssStyle.columnNegate}>{negateElement}</div>
            <div className={cssStyle.columnGranted} onDoubleClick={e => {
                props.events.fire("action_start_permission_edit", {
                    permission: props.permission,
                    target: "grant",
                    defaultValue: defaultValue
                });
                e.preventDefault();
            }}>{grantedElement}</div>
        </div>
    );
};

const PermissionGroupRow = (props: { events: Registry<PermissionEditorEvents>, group: LinkedGroupedPermissions, isOdd: boolean, offsetTop: number }) => {
    const [collapsed, setCollapsed] = useState(props.group.collapsed);

    props.events.reactUse("action_toggle_group", event => {
        if (event.groupId !== null && event.groupId !== props.group.groupId)
            return;

        setCollapsed(event.collapsed);
    });

    return (
        <div className={cssStyle.row + " " + cssStyle.group + " " + (props.isOdd ? "" : cssStyle.even)}
             style={{paddingLeft: props.group.depth + "em", top: props.offsetTop}} onContextMenu={e => {
            e.preventDefault();

            let entries = [];
            entries.push({
                type: contextmenu.MenuEntryType.ENTRY,
                name: tr("Add permissions to this group"),
                callback: () => props.events.fire("action_add_permission_group", {
                    groupId: props.group.groupId,
                    mode: "value"
                })
            });
            entries.push({
                type: contextmenu.MenuEntryType.ENTRY,
                name: tr("Remove permissions from this group"),
                callback: () => props.events.fire("action_remove_permission_group", {
                    groupId: props.group.groupId,
                    mode: "value"
                })
            });
            entries.push({
                type: contextmenu.MenuEntryType.ENTRY,
                name: tr("Add granted permissions to this group"),
                callback: () => props.events.fire("action_add_permission_group", {
                    groupId: props.group.groupId,
                    mode: "grant"
                })
            });
            entries.push({
                type: contextmenu.MenuEntryType.ENTRY,
                name: tr("Remove granted permissions from this group"),
                callback: () => props.events.fire("action_remove_permission_group", {
                    groupId: props.group.groupId,
                    mode: "grant"
                })
            });
            entries.push(contextmenu.Entry.HR());
            if (collapsed) {
                entries.push({
                    type: contextmenu.MenuEntryType.ENTRY,
                    name: tr("Expend group"),
                    callback: () => props.events.fire("action_toggle_group", {
                        groupId: props.group.groupId,
                        collapsed: false
                    })
                });
            } else {
                entries.push({
                    type: contextmenu.MenuEntryType.ENTRY,
                    name: tr("Collapse group"),
                    callback: () => props.events.fire("action_toggle_group", {
                        groupId: props.group.groupId,
                        collapsed: true
                    })
                });
            }
            entries.push({
                type: contextmenu.MenuEntryType.ENTRY,
                name: tr("Expend all"),
                callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: false})
            });
            entries.push({
                type: contextmenu.MenuEntryType.ENTRY,
                name: tr("Collapse all"),
                callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: true})
            });
            contextmenu.spawn_context_menu(e.pageX, e.pageY, ...entries);
        }}
             onDoubleClick={() => props.events.fire("action_toggle_group", {
                 collapsed: !collapsed,
                 groupId: props.group.groupId
             })}
        >
            <div className={cssStyle.columnName}>
                <div className={"arrow " + (collapsed ? "right" : "down")}
                     onClick={() => props.events.fire("action_toggle_group", {
                         collapsed: !collapsed,
                         groupId: props.group.groupId
                     })}/>
                <div className={cssStyle.groupName} title={/* @tr-ignore */ tr(props.group.groupName)}>
                    <Translatable trIgnore={true}>{props.group.groupName}</Translatable>
                </div>
            </div>
            <div className={cssStyle.columnValue}/>
            <div className={cssStyle.columnSkip}/>
            <div className={cssStyle.columnNegate}/>
            <div className={cssStyle.columnGranted}/>
        </div>
    );
};

type PermissionValue = { value?: number, flagNegate?: boolean, flagSkip?: boolean, granted?: number };

@ReactEventHandler<PermissionList>(e => e.props.events)
class PermissionList extends React.Component<{ events: Registry<PermissionEditorEvents> }, { state: "loading" | "normal" | "error", viewHeight: number, scrollOffset: number, error?: string }> {
    private readonly refContainer = React.createRef<HTMLDivElement>();
    private resizeObserver: ResizeObserver;

    private permissionsHead: LinkedGroupedPermissions;
    private permissionByGroupId: { [key: string]: LinkedGroupedPermissions } = {};
    private permissionValuesByName: { [key: string]: PermissionValue } = {};

    private hideSenselessPermissions = true;
    private senselessPermissions: string[] = [];

    private currentListElements: React.ReactElement[] = [];
    private heightPerElement = 28; /* default font size 28px */
    private heightPerElementInitialized = false;

    private filterText: string | undefined;
    private filterAssignedOnly: boolean = false;

    private loadingPermissionList = true;
    private loadingPermissionValues = false;

    private defaultPermissionValue = 1;

    constructor(props) {
        super(props);

        this.state = {
            viewHeight: 0,
            scrollOffset: 0,
            state: "loading"
        }
    }

    render() {
        const view = this.visibleEntries();
        let elements = this.state.state === "normal" ? this.currentListElements.slice(Math.max(0, view.begin - 5), Math.min(view.end + 5, this.currentListElements.length)) : [];

        return (
            <div className={cssStyle.body} ref={this.refContainer}
                 onScroll={() => this.state.state === "normal" && this.setState({scrollOffset: this.refContainer.current.scrollTop})}
                 onContextMenu={e => {
                     if (e.isDefaultPrevented())
                         return;

                     e.preventDefault();
                     contextmenu.spawn_context_menu(e.pageX, e.pageY, {
                         type: contextmenu.MenuEntryType.ENTRY,
                         name: tr("Expend all"),
                         callback: () => this.props.events.fire("action_toggle_group", {
                             groupId: null,
                             collapsed: false
                         })
                     }, {
                         type: contextmenu.MenuEntryType.ENTRY,
                         name: tr("Collapse all"),
                         callback: () => this.props.events.fire("action_toggle_group", {groupId: null, collapsed: true})
                     });
                 }}>
                {elements}
                <div key={"space"} className={cssStyle.spaceAllocator}
                     style={{height: this.state.state === "normal" ? this.currentListElements.length * this.heightPerElement : 0}}/>
                <div key={"loading"}
                     className={cssStyle.overlay + " " + (this.state.state === "loading" ? "" : cssStyle.hidden)}>
                    <a title={tr("loading")}><Translatable>loading</Translatable> <LoadingDots maxDots={3}/></a>
                </div>
                <div key={"error"}
                     className={cssStyle.overlay + " " + cssStyle.error + " " + (this.state.state === "error" ? "" : cssStyle.hidden)}>
                    <a title={tr("An error happened")}>{this.state.error}</a>
                </div>
            </div>
        )
    }

    private visibleEntries() {
        let view_entry_count = Math.ceil(this.state.viewHeight / this.heightPerElement);
        const view_entry_begin = Math.floor(this.state.scrollOffset / this.heightPerElement);
        const view_entry_end = Math.min(this.currentListElements.length, view_entry_begin + view_entry_count);

        return {
            begin: view_entry_begin,
            end: view_entry_end
        }
    }

    componentDidMount(): void {
        this.resizeObserver = new ResizeObserver(entries => {
            if (entries.length !== 1) {
                if (entries.length === 0)
                    log.warn(LogCategory.PERMISSIONS, tr("Permission editor resize observer fired resize event with no entries!"));
                else
                    log.warn(LogCategory.PERMISSIONS, tr("Permission editor resize observer fired resize event with more than one entry which should not be possible (%d)!"), entries.length);
                return;
            }
            const bounds = entries[0].contentRect;
            if (this.state.viewHeight !== bounds.height) {
                log.debug(LogCategory.PERMISSIONS, tr("Handling height update and change permission view height to %d from %d"), bounds.height, this.state.viewHeight);
                this.setState({
                    viewHeight: bounds.height
                });
            }
        });
        this.resizeObserver.observe(this.refContainer.current);

        this.props.events.fire("query_permission_list");
    }

    componentWillUnmount(): void {
        this.resizeObserver.disconnect();
        this.resizeObserver = undefined;
    }

    private initializeElementHeight() {
        if (this.heightPerElementInitialized)
            return;

        requestAnimationFrame(() => {
            const firstElement = this.refContainer.current?.firstElementChild;
            /* the first element might be the space allocator in cases without any shown row elements */
            if (firstElement && firstElement.classList.contains(cssStyle.row)) {
                this.heightPerElementInitialized = true;
                const rect = firstElement.getBoundingClientRect();
                if (this.heightPerElement !== rect.height) {
                    this.heightPerElement = rect.height;
                    this.updateReactComponents();
                }
            }
        });
    }

    componentDidUpdate(prevProps: Readonly<{ events: Registry<PermissionEditorEvents> }>, prevState: Readonly<{ state: "loading" | "normal" | "error", viewHeight: number, scrollOffset: number, error?: string }>, snapshot?: any): void {
        if (prevState.state !== "normal" && this.state.state === "normal")
            requestAnimationFrame(() => this.refContainer.current.scrollTop = this.state.scrollOffset);
        if (this.state.state === "normal")
            this.initializeElementHeight();
    }

    @EventHandler<PermissionEditorEvents>("query_permission_list")
    private handlePermissionListQuery() {
        this.loadingPermissionList = true;
        this.setState({state: "loading"});
    }

    @EventHandler<PermissionEditorEvents>("query_permission_list_result")
    private handlePermissionList(event: PermissionEditorEvents["query_permission_list_result"]) {
        this.loadingPermissionList = false;

        this.hideSenselessPermissions = event.hideSenselessPermissions;
        const visitGroup = (group: EditorGroupedPermissions, parent: LinkedGroupedPermissions, depth: number): LinkedGroupedPermissions => {
            const result: LinkedGroupedPermissions = {
                groupName: group.groupName,
                groupId: group.groupId,

                collapsed: false,

                permissions: group.permissions.map(e => {
                    return {
                        name: e.name,
                        id: e.id,
                        description: e.description,

                        elementVisible: true
                    }
                }),
                anyPermissionVisible: true,

                depth: depth,
                parent: parent,
                children: [],

                nextGroup: undefined,
                nextIfCollapsed: undefined, /* will be set later */

                elementVisible: true,
            };

            if (group.children && group.children.length > 0) {
                result.nextGroup = visitGroup(group.children[0], result, depth + 1);
                result.children.push(result.nextGroup);

                let currentHead = result.nextGroup;
                for (let index = 1; index < group.children.length; index++) {
                    currentHead.nextIfCollapsed = visitGroup(group.children[index], result, depth + 1);
                    currentHead = currentHead.nextIfCollapsed;
                    result.children.push(currentHead);
                }
            }

            return result;
        };

        this.permissionsHead = visitGroup(event.permissions[0], undefined, 0);
        let currentHead = this.permissionsHead;
        for (let index = 1; index < event.permissions.length; index++) {
            currentHead.nextIfCollapsed = visitGroup(event.permissions[index], undefined, 0);
            currentHead = currentHead.nextIfCollapsed;
        }

        /* fixup the next group linkage */
        currentHead = this.permissionsHead;
        while (currentHead) {
            if (!currentHead.nextIfCollapsed && currentHead.parent)
                currentHead.nextIfCollapsed = currentHead.parent.nextIfCollapsed;

            if (!currentHead.nextGroup)
                currentHead.nextGroup = currentHead.nextIfCollapsed;

            currentHead = currentHead.nextGroup;
        }

        /* build up the group key index */
        this.permissionByGroupId = {};
        currentHead = this.permissionsHead;
        while (currentHead) {
            this.permissionByGroupId[currentHead.groupId] = currentHead;
            currentHead = currentHead.nextGroup;
        }

        this.setState({state: this.loadingPermissionList || this.loadingPermissionValues ? "loading" : this.state.error ? "error" : "normal"});
        this.updateReactComponents();
    }

    @EventHandler<PermissionEditorEvents>("action_set_senseless_permissions")
    private handleSenselessPermissions(event: PermissionEditorEvents["action_set_senseless_permissions"]) {
        this.senselessPermissions = event.permissions.slice(0);
        this.updateReactComponents();
    }

    @EventHandler<PermissionEditorEvents>("query_permission_values")
    private handleRequestPermissionValues() {
        this.loadingPermissionValues = true;
        this.setState({state: "loading"});
    }

    @EventHandler<PermissionEditorEvents>("query_permission_values_result")
    private handleRequestPermissionValuesResult(event: PermissionEditorEvents["query_permission_values_result"]) {
        this.loadingPermissionValues = false;
        this.permissionValuesByName = {};

        Object.values(this.permissionValuesByName).forEach(e => {
            e.value = undefined;
            e.granted = undefined;
        });
        (event.permissions || []).forEach(permission => Object.assign(this.permissionValuesByName[permission.name] || (this.permissionValuesByName[permission.name] = {}), {
            value: permission.value,
            granted: permission.granted,
            flagNegate: permission.flagNegate,
            flagSkip: permission.flagSkip
        }));

        this.setState({
            state: this.loadingPermissionList || this.loadingPermissionValues ? "loading" : event.status !== "success" ? "error" : "normal",
            error: event.error
        });

        if (event.status === "success")
            this.updateReactComponents();
    }

    @EventHandler<PermissionEditorEvents>("action_set_permissions_result")
    private handlePermissionSetResult(event: PermissionEditorEvents["action_set_permissions_result"]) {
        event.permissions.forEach(e => {
            if (typeof e.newValue !== "number")
                return;

            const values = this.permissionValuesByName[e.name] || (this.permissionValuesByName[e.name] = {});
            if (e.mode === "value") {
                values.value = e.newValue;
                values.flagSkip = e.flagSkip;
                values.flagNegate = e.flagNegate;
            } else {
                values.granted = e.newValue;
            }
        });
    }

    @EventHandler<PermissionEditorEvents>("action_remove_permissions_result")
    private handlePermissionRemoveResult(event: PermissionEditorEvents["action_remove_permissions_result"]) {
        event.permissions.forEach(e => {
            if (!e.success)
                return;

            const values = this.permissionValuesByName[e.name] || (this.permissionValuesByName[e.name] = {});
            if (e.mode === "value") {
                values.value = undefined;
                values.flagSkip = false;
                values.flagNegate = false;
            } else {
                values.granted = undefined;
            }
        });
    }

    @EventHandler<PermissionEditorEvents>("action_toggle_group")
    private handleToggleGroup(event: PermissionEditorEvents["action_toggle_group"]) {
        if (event.groupId === null) {
            Object.values(this.permissionByGroupId).forEach(e => e.collapsed = event.collapsed);
        } else {
            const group = this.permissionByGroupId[event.groupId];
            if (!group) {
                console.warn(tr("Received group toogle for unknwon group: %s"), event.groupId);
                return;
            }

            if (group.collapsed === event.collapsed)
                return;

            group.collapsed = event.collapsed;
        }
        this.updateReactComponents();
    }

    @EventHandler<PermissionEditorEvents>("action_set_filter")
    private handleSetFilter(event: PermissionEditorEvents["action_set_filter"]) {
        if (this.filterText === event.filter)
            return;

        this.filterText = event.filter;
        this.updateReactComponents();
    }

    @EventHandler<PermissionEditorEvents>("action_set_assigned_only")
    private handleSetAssignedFilter(event: PermissionEditorEvents["action_set_assigned_only"]) {
        if (this.filterAssignedOnly === event.value)
            return;

        this.filterAssignedOnly = event.value;
        this.updateReactComponents();
    }

    @EventHandler<PermissionEditorEvents>("action_set_default_value")
    private handleSetDefaultPermissionValue(event: PermissionEditorEvents["action_set_default_value"]) {
        this.defaultPermissionValue = event.value;
    }

    @EventHandler<PermissionEditorEvents>("action_add_permission_group")
    private handleEnablePermissionGroup(event: PermissionEditorEvents["action_add_permission_group"]) {
        const group = this.permissionByGroupId[event.groupId];
        if (!group) return;

        const permissions: { id: number, name: string, elementVisible: boolean }[] = [];
        const visitGroup = (group: LinkedGroupedPermissions) => {
            permissions.push(...group.permissions);
            group.children.forEach(visitGroup);
        };
        visitGroup(group);

        this.props.events.fire("action_set_permissions", {
            permissions: permissions.map(e => {
                return {
                    name: e.name,
                    mode: event.mode as "value" | "grant",

                    value: e.name.startsWith("b_") && event.mode === "value" ? 1 : this.defaultPermissionValue,
                    flagNegate: false,
                    flagSkip: false
                }
            })
        });
    }

    @EventHandler<PermissionEditorEvents>("action_remove_permission_group")
    private handleDisablePermissionGroup(event: PermissionEditorEvents["action_remove_permission_group"]) {
        const group = this.permissionByGroupId[event.groupId];
        if (!group) return;

        const permissions: { id: number, name: string, elementVisible: boolean }[] = [];
        const visitGroup = (group: LinkedGroupedPermissions) => {
            permissions.push(...group.permissions);
            group.children.forEach(visitGroup);
        };
        visitGroup(group);

        this.props.events.fire("action_remove_permissions", {
            permissions: permissions.map(e => {
                return {
                    name: e.name,
                    mode: event.mode as "value" | "grant",
                }
            })
        });
    }

    private updateReactComponents() {
        let currentGroup = this.permissionsHead;
        let visibleGroups: LinkedGroupedPermissions[] = [];
        while (currentGroup) {
            visibleGroups.push(currentGroup);
            if (currentGroup.collapsed) {
                currentGroup = currentGroup.nextIfCollapsed;
                continue;
            }

            currentGroup.anyPermissionVisible = false;
            for (const permission of currentGroup.permissions) {
                permission.elementVisible = false;
                if (this.filterText && permission.name.indexOf(this.filterText) === -1)
                    continue;

                if (this.hideSenselessPermissions && this.senselessPermissions.findIndex(e => e === permission.name) !== -1)
                    continue;

                const permissionValue = this.permissionValuesByName[permission.name] || (this.permissionValuesByName[permission.name] = {});
                if (this.filterAssignedOnly) {
                    if (typeof permissionValue.value !== "number" && typeof permissionValue.granted !== "number") {
                        continue;
                    }
                }

                permission.elementVisible = true;
                currentGroup.anyPermissionVisible = true;
            }

            currentGroup = currentGroup.nextGroup;
        }

        /* update the visibility from the bottom to the top */
        visibleGroups.sort((a, b) => b.depth - a.depth);
        visibleGroups.forEach(e => {
            for (const child of e.children) {
                if (child.elementVisible) {
                    e.elementVisible = true;
                    return;
                }
            }

            e.elementVisible = e.anyPermissionVisible;
        });

        /* lets build up the final list view */
        this.currentListElements = [];
        let index = 0;
        currentGroup = this.permissionsHead;
        while (currentGroup) {
            if (currentGroup.elementVisible) {
                this.currentListElements.push(<PermissionGroupRow key={"group-" + currentGroup.groupId}
                                                                  events={this.props.events} group={currentGroup}
                                                                  isOdd={index % 2 === 1}
                                                                  offsetTop={this.heightPerElement * index}/>);
                index++;
            }

            if (currentGroup.collapsed) {
                currentGroup = currentGroup.nextIfCollapsed;
                continue;
            } else if (!currentGroup.elementVisible) {
                currentGroup = currentGroup.nextGroup;
                continue;
            }

            currentGroup.permissions.forEach(e => {
                if (!e.elementVisible)
                    return;

                this.currentListElements.push(<PermissionEntryRow
                    key={"permission-" + e.name + " - " + Math.random()} /* force a update of this */
                    events={this.props.events}
                    permission={e.name}
                    groupId={currentGroup.groupId}
                    isOdd={index % 2 === 1}
                    depth={currentGroup.depth}
                    offsetTop={this.heightPerElement * index}
                    value={this.permissionValuesByName[e.name] || {}}
                    defaultValue={this.defaultPermissionValue}
                    description={e.description}
                />);
                index++;
            });

            currentGroup = currentGroup.nextGroup;
        }

        this.forceUpdate();
    }
}

const PermissionTable = (props: { events: Registry<PermissionEditorEvents> }) => {
    const [mode, setMode] = useState<PermissionEditorMode>("unset");
    const [failedPermission, setFailedPermission] = useState(undefined);

    props.events.reactUse("action_set_mode", event => {
        setMode(event.mode);
        setFailedPermission(event.failedPermission);
    });

    return (
        <div className={cssStyle.permissionTable}>
            <div className={cssStyle.header}>
                <div className={cssStyle.row + " " + cssStyle.header}>
                    <div className={cssStyle.columnName}>
                        <a title={tr("Permission Name")}><Translatable>Permission Name</Translatable></a>
                    </div>
                    <div className={cssStyle.columnValue}>
                        <a title={tr("Value")}><Translatable>Value</Translatable></a>
                    </div>
                    <div className={cssStyle.columnSkip}>
                        <a title={tr("Skip")}><Translatable>Skip</Translatable></a>
                    </div>
                    <div className={cssStyle.columnNegate}>
                        <a title={tr("Negate")}><Translatable>Negate</Translatable></a>
                    </div>
                    <div className={cssStyle.columnGranted}>
                        <a title={tr("Granted")}><Translatable>Granted</Translatable></a>
                    </div>
                </div>
            </div>
            <PermissionList events={props.events}/>
            <div className={cssStyle.overlay + " " + cssStyle.unset + " " + (mode === "unset" ? "" : cssStyle.hidden)}/>

            <div
                className={cssStyle.overlay + " " + cssStyle.noPermissions + " " + (mode === "no-permissions" ? "" : cssStyle.hidden)}>
                <a><Translatable>You don't have the permissions to view this
                    permissions</Translatable><br/>({failedPermission})</a>
            </div>
        </div>
    );
};

const RefreshButton = (props: { events: Registry<PermissionEditorEvents> }) => {
    const [unset, setUnset] = useState(true);
    const [nextTime, setNextTime] = useState(0);
    const refButton = useRef<Button>();

    props.events.reactUse("action_set_mode", event => setUnset(event.mode !== "normal" && event.mode !== "no-permissions"));
    props.events.reactUse("query_permission_values", () => {
        setNextTime(Date.now() + 5000);
        refButton.current?.setState({disabled: true});
    });

    useEffect(() => {
        if (Date.now() >= nextTime) {
            refButton.current?.setState({disabled: false});
            return;
        }

        const id = setTimeout(() => refButton.current?.setState({disabled: false}), Math.max(0, nextTime - Date.now()));
        return () => clearTimeout(id);
    });

    return <Button
        ref={refButton}
        disabled={unset || Date.now() < nextTime}
        onClick={() => props.events.fire("query_permission_values")}
    >
        <IconRenderer icon={"client-check_update"}/> <Translatable>Update</Translatable>
    </Button>
};

interface PermissionEditorProperties {
    connection: ConnectionHandler;
    events: Registry<PermissionEditorEvents>;
}

interface PermissionEditorState {
    state: "no-permissions" | "unset" | "normal";
}

export class PermissionEditor extends React.Component<PermissionEditorProperties, PermissionEditorState> {
    render() {
        return [
            <MenuBar key={"menu-bar"} events={this.props.events} connection={this.props.connection}/>,
            <PermissionTable key={"table"} events={this.props.events}/>,
            <div key={"footer"} className={cssStyle.containerFooter}>
                <RefreshButton events={this.props.events}/>
            </div>
        ]
    }

    componentDidMount(): void {
        this.props.events.fire("action_set_mode", {mode: "unset"});
    }
}