/**
 * EGroupware egw_action framework - egw action framework
 *
 * @link https://www.egroupware.org
 * @author Andreas Stöckel <as@stylite.de>
 * @copyright 2011 by Andreas Stöckel
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @package egw_action
 */

import {EgwActionImplementation} from "./EgwActionImplementation";
import {egw} from "../jsapi/egw_global";
import {EgwActionObjectInterface} from "./EgwActionObjectInterface";
import {EGW_AO_STATE_DRAGGING} from "./egw_action_constants";

export class EgwDragActionImplementation implements EgwActionImplementation {
    type = "drag";
    helper: HTMLDivElement = null;
    ddTypes: any[] = [];
    selected: any[] = [];
    defaultDDHelper: (_selected) => HTMLDivElement = (_selected) => {
        // Table containing clone of rows
        const table: HTMLTableElement = (document.createElement("table"));
        table.classList.add('egwGridView_grid', 'et2_egw_action_ddHelper_row');
        // tr element to use as last row to show 'more ...' label
        const moreRow: HTMLTableRowElement = (document.createElement('tr'))
        moreRow.classList.add('et2_egw_action_ddHelper_moreRow');
        // Main div helper container
        const div: HTMLDivElement = (document.createElement("div"));
        div.append(table);

        let rows = [];
        // Maximum number of rows to show
        let maxRows = 3;
        // item label
        const itemLabel = egw.lang(
            (
                egw.link_get_registry(egw.app_name(), _selected.length > 1 ? 'entries' : 'entry') || egw.app_name()
            ) as string
        );
        let index = 0;

        // Take select all into account when counting number of rows, because they may not be
        // in _selected object
        const pseudoNumRows = (_selected[0]?._context?._selectionMgr?._selectAll) ?
            _selected[0]._context?._selectionMgr?._total : _selected.length;

		// Clone nodes but use copy webComponent properties
		const carefulClone = (node, skip_text = false) =>
		{
			// Don't clone text nodes, it causes duplication in et2-description
			if(skip_text && node.nodeType == node.TEXT_NODE)
			{
				return;
			}

			let clone = node.cloneNode();

			let widget_class = window.customElements.get(clone.localName);
			let properties = widget_class ? widget_class.properties : [];
			for(let key in properties)
			{
				clone[key] = node[key];
			}
			// Children
			node.childNodes.forEach(c =>
			{
				// Don't clone text in et2-description, it causes duplication
				const child = carefulClone(c, skip_text || ["ET2-DESCRIPTION"].indexOf(c.tagName) != -1)
				if(child)
				{
					clone.appendChild(child);
				}
			})
			if(widget_class)
			{
				clone.requestUpdate();
			}
			return clone;
		}

		for(const egwActionObject of _selected)
		{
			let rowNode = egwActionObject.iface.getDOMNode();
			if(egwActionObject._context && egwActionObject._context instanceof HTMLElement)
			{
				rowNode = egwActionObject._context;
			}
			const row : Node = carefulClone(rowNode);
			if(row)
			{
				rows.push(row);
				table.append(row);
			}
            index++;
            if (index == maxRows) {
                // Label to show number of items
                const spanCnt = (document.createElement('span'))
                spanCnt.classList.add('et2_egw_action_ddHelper_itemsCnt')
                div.append(spanCnt);

                spanCnt.textContent = (pseudoNumRows + ' ' + itemLabel);
                // Number of not shown rows
                const restRows = pseudoNumRows - maxRows;
                if (restRows > 0) {
                    moreRow.textContent = egw.lang(`${pseudoNumRows - maxRows} more ${itemLabel} selected ...`);
                }
                table.append(moreRow);
                break;
            }
        }

        const text = (document.createElement('div'))
        text.classList.add('et2_egw_action_ddHelper_tip');
        div.append(text);

        // Add notice of Ctrl key, if supported
        if ('draggable' in document.createElement('span') &&
            navigator && navigator.userAgent.indexOf('Chrome') >= 0 && egw.app_name() == 'filemanager') // currently only filemanager supports drag out
        {
            if (rows.length == 1)
            {
                text.textContent=(egw.lang('You may drag file out to your desktop', itemLabel));
            }
            else
            {
                text.textContent=(egw.lang('Note: If you drag out these selected rows to desktop only the first selected row will be downloaded.', itemLabel));
            }
        }
// Final html DOM return as helper structure
        return div;
    };

    registerAction: (_actionObjectInterface: EgwActionObjectInterface, _triggerCallback: Function, _context: any) => boolean = (_aoi, _callback, _context) => {
        const node = _aoi.getDOMNode() && _aoi.getDOMNode()[0] ? _aoi.getDOMNode()[0] : _aoi.getDOMNode();

        if (node) {
			if(typeof _aoi.handlers == "undefined")
			{
				_aoi.handlers = {};
			}
			if(typeof _aoi.handlers[this.type] == "undefined")
			{
				_aoi.handlers[this.type] = [];
			}

			// Prevent selection
            node.onselectstart = function () {
                return false;
            };
            if (!(window.FileReader && 'draggable' in document.createElement('span'))) {
                // No DnD support
                return;
            }

            // It shouldn't be so hard to get the action...
            let action = null;
            const groups = _context.getActionImplementationGroups();
            if (!groups.drag) {
                return;
            }

			if(_aoi.handlers[this.type].length !== 0)
			{
				// Already bound
				return;
			}

			// Bind mouse handlers
            //et2_dataview_view_aoi binds mousedown event in et2_dataview_rowAOI to "egwPreventSelect" function from egw_action_common via jQuery.mousedown
            //jQuery(node).off("mousedown",egwPreventSelect)
            //et2_dataview_view_aoi binds mousedown event in et2_dataview_rowAOI to "egwPreventSelect" function from egw_action_common via addEventListener
            //node.removeEventListener("mousedown",egwPreventSelect)
			const mousedown = (event) =>
			{
                if (_context.isSelection(event)) {
                    node.setAttribute("draggable", false);
                } else if (event.which != 3) {
                    document.getSelection().removeAllRanges();
                }
			};
			node.addEventListener("mousedown", mousedown)
			_aoi.handlers[this.type].push({type: 'mousedown', listener: mousedown});

			const mouseup = (event) =>
			{
				node.setAttribute("draggable", true);

				// Set cursor back to auto. Seems FF can't handle cursor reversion
				document.body.style.cursor = 'auto'
			};
			node.addEventListener("mouseup", mouseup)
			_aoi.handlers[this.type].push({type: 'mousedown', listener: mousedown});


            node.setAttribute('draggable', true);
            const ai = this
            const dragstart = function (event) {

				let dragActionObject = _context;
				if(this.findActionTarget)
				{
					dragActionObject = this.findActionTarget(event).action ?? _context;
				}

				// The helper function is called before the start function
                // is evoked. Call the given callback function. The callback
                // function will gather the selected elements and action links
                // and call the doExecuteImplementation function. This
                // will call the onExecute function of the first action
                // in order to obtain the helper object (stored in ai.helper)
                // and the multiple dragDropTypes (ai.ddTypes)
				_callback.call(dragActionObject, false, ai);

				// Stop parent elements from also starting to drag if we're nested
				if(ai.selected.length)
				{
					event.stopPropagation();
				}

				if(action && egw.app_name() == 'filemanager')
				{
					if(dragActionObject.isSelection(event))
					{
						return;
					}

					// Get all selected
                    const selected = ai.selected;

                    // Set file data
                    for (let i = 0; i < 1; i++) {
                        let d = selected[i].data || egw.dataGetUIDdata(selected[i].id).data || {};
                        if (d && d.mime && d.download_url) {
                            let url = d.download_url;

                            // NEED an absolute URL
                            if (url[0] == '/') url = egw.link(url);
                            // egw.link adds the webserver, but that might not be an absolute URL - try again
                            if (url[0] == '/') url = window.location.origin + url;
                            event.dataTransfer.setData("DownloadURL", d.mime + ':' + d.name + ':' + url);
                        }
                    }
                    event.dataTransfer.effectAllowed = 'copy';

                    if (event.dataTransfer.types.length == 0) {
                        // No file data? Abort: drag does nothing
                        event.preventDefault();
                        return;
                    }
                } else {
                    event.dataTransfer.effectAllowed = 'linkMove';
                }


                const data = {
                    ddTypes: ai.ddTypes,
                    selected: ai.selected.map((item) => {
                        return {id: item.id}
                    })
                };

                if (!ai.helper) {
                    ai.helper = ai.defaultDDHelper(ai.selected);
                }
                // Add a basic class to the helper in order to standardize the background layout
                ai.helper.classList.add('et2_egw_action_ddHelper', 'ui-draggable-dragging');
                document.body.append(ai.helper);

				// Set a dragging state
				ai.selected.forEach((item) =>
				{
					item.iface?.setState(ai.selected[0].iface.getState() | EGW_AO_STATE_DRAGGING);
				});
                this.classList.add('drag--moving');

                event.dataTransfer.setData('application/json', JSON.stringify(data))

				// Wait for any webComponents to finish
				let wait = [];
				const webComponents = [];
				const check = (element) =>
				{
					if(typeof element.updateComplete !== "undefined")
					{
						webComponents.push(element)
						element.requestUpdate();
						wait.push(element.updateComplete);
					}
					element.childNodes.forEach(child => check(child));
				}
				check(ai.helper);
				// Clumsily force widget update, since we can't do it async
				Promise.all(wait).then(() =>
				{
					wait = [];
					webComponents.forEach(e => wait.push(e.updateComplete));
					Promise.all(wait).then(() =>
					{
						event.dataTransfer.setDragImage(ai.helper, 12, 12);
					});
				});

				this.setAttribute('data-egwActionObjID', JSON.stringify(data.selected));
            };

            const dragend = (_) => {
                const helper = document.querySelector('.et2_egw_action_ddHelper');
                if (helper) helper.remove();
                const draggable = document.querySelector('.drag--moving');
                if (draggable) draggable.classList.remove('drag--moving');
                // cleanup drop hover class from all other DOMs if there's still anything left
                Array.from(document.getElementsByClassName('et2dropzone drop-hover')).forEach(_i=>{_i.classList.remove('drop-hover')})
				// Clean up selected
				ai.selected = [];
			};

            // Drag Event listeners
			node.addEventListener('dragstart', dragstart, false);
			_aoi.handlers[this.type].push({type: 'dragstart', listener: dragstart});
			node.addEventListener('dragend', dragend, false);
			_aoi.handlers[this.type].push({type: 'dragend', listener: dragend});

            return true;
        }
        return false;
    };

    unregisterAction: (_actionObjectInterface: EgwActionObjectInterface) => boolean =(_aoi) => {
        const node = _aoi.getDOMNode();

        if (node) {
            node.setAttribute('draggable', "false");
        }
		// Unregister handlers
		if(_aoi.handlers)
		{
			_aoi.handlers[this.type]?.forEach(h => node.removeEventListener(h.type, h.listener));
			delete _aoi.handlers[this.type];
		}
        return true;
    };

    /**
     * Builds the context menu and shows it at the given position/DOM-Node.
     *
     * @param {string} _context
     * @param {array} _selected
     * @param {object} _links
     */
    executeImplementation: (_context: any, _selected: any, _links: any) => any = (_context, _selected, _links) => {
        // Reset the helper object of the action implementation
        this.helper = null;
        let hasLink = false;

        // Store the drag-drop types
        this.ddTypes = [];
        this.selected = _selected;

        // Call the onExecute event of the first actionObject
        for (const k in _links) {
            if (_links[k].visible) {
                hasLink = true;

                // Only execute the following code if a JS function is registered
                // for the action and this is the first action link
                if (!this.helper && _links[k].actionObj.onExecute.hasHandler()) {
                    this.helper = _links[k].actionObj.execute(_selected);
                }

                // Push the dragType of the associated action object onto the
                // drag type list - this allows an element to support multiple
                // drag/drop types.
                const type: string[] = Array.isArray(_links[k].actionObj.dragType)
                    ? _links[k].actionObj.dragType
                    : [_links[k].actionObj.dragType];
                for (const i of type) {
                    if (this.ddTypes.indexOf(i) === -1) {
                        this.ddTypes.push(i);
                    }
                }
            }
        }
        // If no helper has been defined, create a default one
        if (!this.helper && hasLink) {
            this.helper = this.defaultDDHelper(_selected);
        }

        return true;
    };
}

/**
 * @deprecated use upper case class
 */
export class egwDragActionImplementation extends EgwDragActionImplementation {
}

let _dragActionImpl = null

export function getDragImplementation():EgwDragActionImplementation {
    if (!_dragActionImpl) {
        _dragActionImpl = new EgwDragActionImplementation();
    }
    return _dragActionImpl
}
