/**
 * @author Martin Karkowski
 * @email m.karkowski@zema.de
 * @create date 2019-02-20 09:19:06
 * @modify date 2020-10-29 09:55:30
 * @desc [description]
 * 
 * A Basic DynamicLayout.
 * I uses a Toolbar, Sidebar
 * 
 * Toast are implemented by https://fkhadra.github.io/react-toastify/introduction
 * Dynamic Account https://github.com/STRML/react-grid-layout
 * 
 */

import React from 'react';
import { Card, Modal } from 'react-bootstrap';
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { rgetattr, rsetattr } from '../../../lib/helpers/objectMethods';
import DynamicRenderer from '../dynamic/dynamicRenderer';
import { IHotKeyAction } from './interfaces/IHotkeyAction';
import { IModalSettings } from './interfaces/IModalSettings';
import Toolbar, { IMenu, ToolbarProps } from './toolbar';
import { IDynamicRenderSettings } from '../dynamic/interfaces/IDynamicRenderSettings'
import { generateId } from '../../../lib/helpers/idMethods';
import { faEye, faEyeSlash, faMinusSquare, faWindowClose } from '@fortawesome/free-solid-svg-icons';
import GridLayout, { Responsive, WidthProvider } from 'react-grid-layout';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

export interface IDynamicWidget extends IDynamicRenderSettings {
    id: string,
    label: string,
    visible: boolean,
    gridSettings: {

        // A string corresponding to the component key
        i?: string,

        // These are all in grid units, not pixels
        x: number,
        y: number,
        w: number,
        h: number,
        minW?: number;
        maxW?: number;
        minH?: number;
        maxH?: number;

        // If true, equal to `isDraggable: false, isResizable: false`.
        static?: boolean,
        // If false, will not be draggable. Overrides `static`.
        isDraggable?: boolean;
        // If false, will not be resizable. Overrides `static`.
        isResizable?: boolean;
        // By default, a handle is only shown on the bottom-right (southeast) corner.
        // Note that resizing from the top or left is generally not intuitive.
        // Defaults to ['se']
        resizeHandles?: Array<'s' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne'>
        // If true and draggable, item will be moved only within grid.
        isBounded?: boolean
    },
    showLabel?:boolean;
    hideable?: boolean;
    bg?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light';
    border?: 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light';
    text?:'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info' | 'dark' | 'light';
}

// All callbacks below have signature (layout, oldItem, newItem, placeholder, e, element).
// 'start' and 'stop' callbacks pass `undefined` for 'placeholder'.
export type ItemCallback = (layout, oldItem, newItem, placeholder, e: MouseEvent, element: HTMLElement) => void;

export interface IDynamicLayoutProps<CallbackData> extends ToolbarProps<CallbackData> {
    hotkeys?: IHotKeyAction<CallbackData>[];
    components: IDynamicWidget[]
    layoutSettings: {
        //
        // Basic props
        //

        // This allows setting the initial width on the server side.
        // This is required unless using the HOC <WidthProvider> or similar
        width?: number,

        // If true, the container height swells and contracts to fit contents
        autoSize?: boolean,

        // Number of columns in this layout.
        cols?: number,

        // A CSS selector for tags that will not be draggable.
        // For example: draggableCancel:'.MyNonDraggableAreaClassName'
        // If you forget the leading . it will not work.
        draggableCancel?: string,

        // A CSS selector for tags that will act as the draggable handle.
        // For example: draggableHandle:'.MyDragHandleClassName'
        // If you forget the leading . it will not work.
        draggableHandle?: string,

        // If true, the layout will compact vertically
        verticalCompact?: boolean,

        // Compaction type.
        compactType?: ('vertical' | 'horizontal');

        // Layout is an array of object with the format:
        // {x: number, y: number, w: number, h: number}
        // The index into the layout must match the key used on each item component.
        // If you choose to use custom keys, you can specify that key in the layout
        // array objects like so:
        // {i: string, x: number, y: number, w: number, h: number}
        layout?: Array<[number, number, number, number]> | Array<{ i: string, x: number, y: number, w: number, h: number }> // If not provided, use data-grid props on children

        // Margin between items [x, y] in px.
        margin?: [number, number],

        // Padding inside the container [x, y] in px
        containerPadding?: [number, number]

        // Rows have a static height, but you can change this based on breakpoints
        // if you like.
        rowHeight?: number

        //
        // Flags
        //
        isDraggable?: boolean,
        isResizable?: boolean,
        isBounded?: boolean,
        // Uses CSS3 translate() instead of position top/left.
        // This makes about 6x faster paint performance
        useCSSTransforms?: boolean,
        // If parent DOM node of ResponsiveReactGridLayout or ReactGridLayout has "transform: scale(n)" css property,
        // we should set scale coefficient to avoid render artefacts while dragging.
        transformScale?: number,

        // If true, grid items won't change position when being
        // dragged over.
        preventCollision?: boolean;

        // If true, droppable elements (with `draggable={true}` attribute)
        // can be dropped on the grid. It triggers "onDrop" callback
        // with position and event object as parameters.
        // It can be useful for dropping an element in a specific position
        //
        // NOTE: In case of using Firefox you should add
        // `onDragStart={e => e.dataTransfer.setData('text/plain', '')}` attribute
        // along with `draggable={true}` otherwise this feature will work incorrect.
        // onDragStart attribute is required for Firefox for a dragging initialization
        // @see https://bugzilla.mozilla.org/show_bug.cgi?id=568313
        isDroppable?: boolean
        // Defines which resize handles should be rendered
        // Allows for any combination of:
        // 's' - South handle (bottom-center)
        // 'w' - West handle (left-center)
        // 'e' - East handle (right-center)
        // 'n' - North handle (top-center)
        // 'sw' - Southwest handle (bottom-left)
        // 'nw' - Northwest handle (top-left)
        // 'se' - Southeast handle (bottom-right)
        // 'ne' - Northeast handle (top-right)
        resizeHandles?: Array<'s' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne'>
        // Custom component for resize handles
        resizeHandle?: React.ReactElement<any> | ((resizeHandleAxis) => React.ReactElement<any>)

        //
        // Callbacks
        //

        // Callback so you can save the layout.
        // Calls back with (currentLayout) after every drag or resize stop.
        onLayoutChange?: (layout: GridLayout) => void,

        // Calls when drag starts.
        onDragStart?: ItemCallback,
        // Calls on each drag movement.
        onDrag?: ItemCallback,
        // Calls when drag is complete.
        onDragStop?: ItemCallback,
        // Calls when resize starts.
        onResizeStart?: ItemCallback,
        // Calls when resize movement happens.
        onResize?: ItemCallback,
        // Calls when resize is complete.
        onResizeStop?: ItemCallback,
        // Calls when an element has been dropped into the grid from outside.
        onDrop?: (layout, item, e: Event) => void

        // Ref for getting a reference for the grid's wrapping div.
        // You can use this instead of a regular ref and the deprecated `ReactDOM.findDOMNode()`` function.
        innerRef?: React.Ref<"div">
    },
    onResize?: () => void;
    onMount?: (layout: DynamicLayout<CallbackData>) => void;
    onUnmount?: () => void;
    onWidgetToggle?: (widget: IDynamicWidget, visble: boolean) => Promise<boolean>;
    menuName?: string
}



export interface IDynamicLayoutState<CallbackData> extends IDynamicLayoutProps<CallbackData> {
    modalSettings?: IModalSettings
    modalVisible?: boolean
}

class DynamicLayout<CallbackData> extends React.Component<IDynamicLayoutProps<CallbackData>, IDynamicLayoutState<CallbackData>> {

    protected _handleResize: () => void;
    protected _handleKeyDown: (event) => void;
    protected _handleKeyUp: (event) => void;
    protected _handleMouse: (event: MouseEvent) => void;


    protected _menuEntryId: string;

    public hotkeysEnabled = true;
    public pressedKey: string = null;
    public currentMousePosition: MouseEvent;

    /**
     * Function will be called if the Item has been rendered sucessfully.
     */
    componentDidMount() {

        const _this = this;

        // Listening to resizing Events
        function handleResize() {
            if (typeof _this.state.onResize === 'function') {
                _this.state.onResize();
            }
        }

        function handleKeyDown(event) {
            
            if (!_this.hotkeysEnabled || _this.state.hotkeys?.length === 0) {
                return;
            }

            if (_this.pressedKey !== event.code) {
                _this.pressedKey = event.code;

                try {
                    for (const hotkey of _this.state.hotkeys || []) {
                        if (hotkey.key === event.code) {
                            console.log('perform', hotkey.key)
                            hotkey.onPress(_this.state.generateData(), event)
                            break;
                        }
                    }
                } catch (e) {
                }
            }
        }

        function handleKeyUp(event) {
            if (!_this.hotkeysEnabled || _this.state.hotkeys?.length === 0) {
                return;
            }
            _this.pressedKey='';
            try {
                for (const hotkey of _this.state.hotkeys || []) {
                    if (hotkey.key === event.code && hotkey.onRelease) {
                        hotkey.onRelease(_this.state.generateData(), event)
                        break;
                    }
                }
            } catch (e) {

            }
        }

        function handleMouse(event) {
            _this.currentMousePosition = event;
        }

        if (typeof _this.state.onMount === 'function') {
            _this.state.onMount(this);
        }

        this._handleResize = handleResize;
        this._handleKeyDown = handleKeyDown;
        this._handleKeyUp = handleKeyUp;
        this._handleMouse = handleMouse;
        window.addEventListener('resize', this._handleResize);
        window.addEventListener('keydown', this._handleKeyDown);
        window.addEventListener('keyup', this._handleKeyUp);
        window.addEventListener('mousemove', this._handleMouse);



        this.widgets = this.widgets;
    }

    /**
     * Function, that will be called before the network fails.
     */
    componentWillUnmount() {
        window.removeEventListener('resize', this._handleResize);
        window.removeEventListener('keydown', this._handleKeyDown);
        window.removeEventListener('keyup', this._handleKeyUp);
        window.removeEventListener('mousemove', this._handleMouse);

        // Call the unmount
        if (typeof this.state.onUnmount === 'function') {
            this.state.onUnmount();
        }
    }

    constructor(props) {
        super(props);

        this.state = Object.assign({
            modalSettings: null,
            modalVisible: false
        }, this.props);

        // Create a Menu Entry.
        this._menuEntryId = generateId();
    }

    public openPopup(settings: IModalSettings) {
        const modalSettings = settings;

        const _this = this;

        const _close = () => {
            _this.setState({
                modalVisible: false
            });
        };

        const _originalFunction = modalSettings.onHide;
        modalSettings.onHide = (...args) => {
            if (typeof _originalFunction === 'function') {
                _originalFunction(_close);
            } else {
                _close();
            }
        }

        _this.setState({
            modalVisible: true,
            modalSettings: modalSettings
        });

        // Return the Close Method.
        return _close;
    }

    public getDialogData<T = any>(settings: IModalSettings) {
        const _this = this;
        let close: () => void;

        return new Promise<T>((resolve, reject) => {
            // Adapt the Settings:
            const _onSubmit = rgetattr<(data) => Promise<void>>(settings, 'content.props.onSubmit', async (data) => { });
            const _onCancel = rgetattr<(err) => Promise<void>>(settings, 'content.props.onCancel', async (err) => { });

            const onSubmit = async (data) => {
                close();
                _onSubmit(data);
                resolve(data);
            }

            const onCancel = async (data) => {
                close();
                _onCancel(data);
                reject(data);
            }

            rsetattr(settings, 'content.props.onSubmit', onSubmit);
            rsetattr(settings, 'content.props.onCancel', onCancel);

            close = _this.openPopup(settings);
        });
    }

    private _dynamicMenuEntry: IMenu<CallbackData> = null;

    public get dynamicMenuEntry() {

        if (this._dynamicMenuEntry === null) {

            this._dynamicMenuEntry = {
                type: 'menu',
                id: this._menuEntryId,
                label: this.props.menuName || 'Widgets',
                items: []
            };

            this.toolbar.push(this._dynamicMenuEntry)
        }

        return this._dynamicMenuEntry;

    }

    public get toolbar() {
        return this.state.toolbar.items;
    }

    public get widgets(): IDynamicWidget[] {
        return this.state.components;
    }

    public set widgets(value: IDynamicWidget[]) {

        const menu = this.dynamicMenuEntry;

        const _this = this;

        menu.items = value.filter(item => item.hideable).map(item => {
            return {
                type: 'action',
                onClick() {
                    _this.toggleWidget(item, !item.visible);
                },
                label: (item.visible ? 'Hide' : 'Show') + ' Widget "' + item.label + '"',
                icon: (item.visible ? faEyeSlash : faEye)
            }
        })

        this.setState({
            components: value,
            toolbar: {
                items: this.toolbar
            }
        })
    }

    public async toggleWidget(widget: IDynamicWidget | string, visibility: boolean) {
        let widgetToAdapt: IDynamicWidget;

        const widgets = this.widgets;

        if (typeof widget === "string") {
            for (const _widget of widgets) {
                if (_widget.id = widget) {
                    widgetToAdapt = _widget;
                    break;
                }
            }
        } else {
            widgetToAdapt = widget;
        }

        let adapted = false;

        if (typeof this.state.onWidgetToggle === 'function') {
            // Call the Async Function to select a Tab.
            if (await this.state.onWidgetToggle(widgetToAdapt, visibility)) {
                widgetToAdapt.visible = visibility;
                adapted = true;
            }
        } else {
            // Change the visiblity.
            widgetToAdapt.visible = visibility;
            adapted = true;
        }

        if (adapted) {
            // Update the State.
            this.widgets = widgets;
        }
    }

    /**
     * Helper Function to render a Toast.
     * @param message The Message of the Toast
     * @param type The Type.
     * @param timeout A Timeout in [ms], after which the Toast should disapear. 0 = infinity
     */
    public showToast(message: string, type?: 'info' | 'warning' | 'success' | 'error' | 'default' | 'dark' | 'light' | 'dark', autoClose = 5000) {
        if (type !== undefined && type !== null) {
            toast[type](message, {
                position: "top-center",
                autoClose,
                hideProgressBar: false,
                closeOnClick: true,
                pauseOnHover: true,
                draggable: true,
                progress: undefined,
            });
        } else {
            toast(message, {
                position: "top-center",
                autoClose,
                hideProgressBar: false,
                closeOnClick: true,
                pauseOnHover: true,
                draggable: true,
                progress: undefined,
            });
        }
    }

    public render() {
        const _this = this;

        const toolbar = (<Toolbar toolbar={this.state.toolbar} generateData={this.state.generateData} brand={{
            label: 'nopeBackend',
            ref: '/',
            type: 'link',
        }}></Toolbar>);

        let layout = this.state.components.filter(item => item.visible).map(item => [item.gridSettings.x,item.gridSettings.y,item.gridSettings.w,item.gridSettings.h]);
        layout = layout.length > 0 ? layout : undefined;


        const children = this.state.components.filter(item => item.visible).map((item, idx) => {
            if (item.hideable) {
                return (
                    <Card key={idx} data-grid={item.gridSettings} bg={item.bg} border={item.border} text={item.text} >
                        <FontAwesomeIcon icon={faWindowClose} style={{
                            position: 'absolute',
                            top: '-5px',
                            left: '-5px'
                        }} onClick={_ => _this.toggleWidget(item, !item.visible)}/> 

                        { item.showLabel ? <Card.Header>{item.label}</Card.Header> : ''}

                        <Card.Body style={{overflow: 'scroll'}}>
                            {
                                React.createElement(DynamicRenderer, { ...item })
                            }
                        </Card.Body>
                    </Card>
                )
            }

            return (
                <Card key={idx} data-grid={item.gridSettings} bg={item.bg} border={item.border} text={item.text}>
                    { item.showLabel ? <Card.Header>{item.label}</Card.Header> : ''}
                    <Card.Body style={{ overflow: 'scroll' }}>
                        {
                            React.createElement(DynamicRenderer, { ...item })
                        }
                    </Card.Body>
                </Card>)
        });

        const ResponsiveGridLayout = WidthProvider(Responsive);
        const grid = React.createElement(GridLayout, {
            ...this.state.layoutSettings,            
            layout
            // breakpoints: {
            //     lg: 1200, 
            //     md: 996, 
            //     sm: 768,
            //     xs: 480,
            //     xxs: 0
            // },
            // cols: { 
            //     lg: 12,
            //     md: 10,
            //     sm: 6,
            //     xs: 4,
            //     xxs: 1 
            // }
        } as any, ...children);

        const toast = (<ToastContainer
            position="top-center"
            autoClose={5000}
            hideProgressBar={false}
            newestOnTop={false}
            closeOnClick
            rtl={false}
            pauseOnFocusLoss
            draggable
            pauseOnHover
        />);

        const renderedElements = [toolbar, grid, toast];
        if (this.state.modalVisible && this.state.modalSettings != undefined) {
            renderedElements.push((<Modal
                show={this.state.modalVisible}
                backdrop={typeof this.state.modalSettings.backdrop !== undefined ? this.state.modalSettings.backdrop : 'static'}
                keyboard={false}
                size={this.state.modalSettings.size}
                centered={this.state.modalSettings.centered || false}
                onHide={this.state.modalSettings.onHide}
                scrollable
            >
                {
                    this.state.modalSettings.header ?
                        <Modal.Header>
                            <Modal.Title>{this.state.modalSettings.header}</Modal.Title>
                        </Modal.Header> :
                        ''
                }

                <Modal.Body>
                    {/* Render the dynamic Component */}
                    <DynamicRenderer component={this.state.modalSettings.content.component} props={this.state.modalSettings.content.props} />
                </Modal.Body>
            </Modal>));
        }
        return React.createElement("div", {}, ...renderedElements);
    }
}

export default DynamicLayout;