import { handleSearch, handleSearchBlur, handleSearchClose, handleSearchKeyDown } from "./injectSearchable.js";
import { handleSortableTree, handleSortableTreeDidUpdate } from "./injectSortable.js";

import type { TreeNodeId, TreeNodeProps } from "../components/TreeNode.js";

export { handleSortableTree, handleSortableTreeDidUpdate };
export { handleSearch, handleSearchBlur, handleSearchClose, handleSearchKeyDown };

/**
 * Get a list of all checked nodes (flat).
 *
 * @param withDepth If true an object is returned with the depth levels
 * @returns
 */
export function getRecursivelyCheckedNodes(tree: TreeNodeProps[], withDepth: false): TreeNodeProps[];
export function getRecursivelyCheckedNodes(tree: TreeNodeProps[], withDepth: boolean = false) {
    if (withDepth) {
        let depth = 0;
        const result: { [s: number]: TreeNodeProps[] } = {};
        const fnChild = (obj: TreeNodeProps) => {
            if (!obj.$visible) {
                return;
            }

            const { childNodes = [], checked } = obj;
            if (checked) {
                if (!result[depth]) {
                    result[depth] = [];
                }
                result[depth].push(obj);
            }
            if (childNodes.length) {
                depth++;
                childNodes.forEach(fnChild);
                depth--;
            }
        };
        tree.forEach(fnChild);
        return result;
    } else {
        const result: TreeNodeProps[] = [];
        const fnChild = (obj: TreeNodeProps) => {
            if (!obj.$visible) {
                return;
            }

            const { childNodes = [], checked } = obj;
            checked && result.push(obj);
            if (childNodes.length) {
                childNodes.forEach(fnChild);
            }
        };
        tree.forEach(fnChild);
        return result;
    }
}

/**
 * Create a unique id.
 *
 * @returns
 */
export function uuid() {
    function s4() {
        return Math.floor((1 + Math.random()) * 0x10000)
            .toString(16)
            .substring(1);
    }
    return `aiot-${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
}

/**
 * Helper function to add a class to a DOMElement.
 *
 * @param el The element
 * @param className The class names
 */
export function addClass(el: HTMLElement, className: string) {
    if (el.classList) el.classList.add(className);
    else el.className += ` ${className}`;
}

/**
 * Helper function to remove a class to a DOMElement.
 *
 * @param el The element
 * @param className The class names
 */
export function removeClass(el: HTMLElement, className: string) {
    if (el.classList) el.classList.remove(className);
    else el.className = el.className.replace(new RegExp(`(^|\\b)${className.split(" ").join("|")}(\\b|$)`, "gi"), " ");
}

/**
 * Get parent of a element.
 *
 * @see https://gist.github.com/ziggi/2f15832b57398649ee9b
 */
export function parents(elem: HTMLElement, selector?: string) {
    const elements = [];
    const ishaveselector = selector !== undefined;

    while ((elem = elem.parentElement) !== null) {
        if (elem.nodeType !== window.Node.ELEMENT_NODE) {
            continue;
        }

        if (!ishaveselector || elem.matches(selector)) {
            elements.push(elem);
        }
    }

    return elements;
}

/**
 * Get the item for a JSON path for the passed object.
 *
 * @param object The object
 * @param path The path
 * @returns
 * @private
 */
function getObjectPath(object: any, path?: string) {
    const names = (path || "").split(".");
    for (let i = 0; object && i < names.length; i++) {
        object = object[names[i]];
    }
    return object;
}

/**
 * Set the item for a JSON path for the passed object.
 *
 * @param object The object
 * @param path The path
 * @param value The stringify-able value
 * @private
 */
function setObjectPath(object: any, path?: string, value?: any) {
    let l;
    const names = (path || "").split(".");
    if ((l = names.length) > 0) {
        for (let i = 0; object && i < l - 1; i++) {
            if (!object[names[i]]) {
                object[names[i]] = {};
            }
            object = object[names[i]];
        }
        object[names[l - 1]] = value;
    }
}

export interface IStorage {
    /**
     * Set the item for a JSON path for this storage object.
     *
     * @param {string} path The path
     * @param {any} value The stringify-able value
     */
    setItem(path: string, value: any): void;

    /**
     * Get the item for a JSON path for this storage object.
     *
     * @param {string} path The path
     * @returns {any}
     */
    getItem(path: string): any;
}

/**
 * Similar to {@link Storage} but it does not persist anything in the localStorage.
 */
export class NonPeristableStorage implements IStorage {
    public cache = {};

    public setItem(path: string, value: any) {
        setObjectPath(this.cache, path, value);
    }

    public getItem(path?: string) {
        return path ? getObjectPath(this.cache, path) : this.cache;
    }
}

/**
 * Create a helper class to store compact JSON data in local storage if supported.
 *
 * @param {int} id Unique id for the storage key. Prefixed with "AIOT-"
 */
export class Storage implements IStorage {
    public id: string;
    public cache: any;

    public constructor(id: string) {
        this.id = id;
        this.cache = undefined;
    }

    public setItem(path: string, value: any) {
        const collection = this.getItem();
        setObjectPath(collection, path, value);
        window.localStorage.setItem(`AIOT-${this.id}`, JSON.stringify(collection));
        this.cache = undefined;
    }

    public getItem(path?: string) {
        if (!this.cache) {
            this.cache = JSON.parse(window.localStorage.getItem(`AIOT-${this.id}`) || "{}");
        }
        return path ? getObjectPath(this.cache, path) : this.cache;
    }
}

/**
 * If true the current browser supports local storage.
 */
export const SUPPORTS_LOCAL_STORAGE = (function () {
    const test = "test";
    try {
        const _localStorage = window["localStorage"];
        _localStorage.setItem(test, test);
        _localStorage.removeItem(test);
        return true;
    } catch (e) {
        return false;
    }
})();

/**
 * Allows to inject a virtual CSS style sheet.
 *
 * @param {string} id The unique id for the style. If already available it gets overriden by the new style rules
 * @param {string} style The style rules
 * @returns {DOMElement|boolean} If successful it returns the script-DOMElement and false if failure
 */
export function injectStyle(id: string, style: string) {
    try {
        // Remove old element
        const script = document.getElementById(id);
        script && script.remove();

        // Create element
        const elem = document.createElement("div");
        elem.id = id;
        elem.style.display = "none";
        elem.innerHTML = `&shy;<style>${style}</style>`;
        document.body.appendChild(elem);
        return elem;
    } catch (e) {
        return false;
    }
}

/**
 * Resizes a given container with calc(100% - width).
 *
 * @param {string} containerId The container ID
 * @param {string} oppositeId The opposite ID of container
 * @param {int} width Width in px
 */
export function resizeOpposite(containerId: string, oppositeId: string, width: number) {
    return injectStyle(
        `${containerId}-styleOpposite`,
        `#${oppositeId}{ width: -webkit-calc(100% - ${width}px);width: -moz-calc(100% - ${
            width
        }px);width: calc(100% - ${width}px); }`,
    );
}

/**
 * Allows to update a tree node item by id.
 *
 * @param id The id of the tree node
 * @param tree The properties to modify
 * @param callback
 * @param setState
 */
export function updateTreeItemById(
    id: TreeNodeId,
    tree: TreeNodeProps[],
    callback: (node: TreeNodeProps) => void,
    setState?: (newTree: TreeNodeProps[]) => void,
) {
    const newTree = tree;
    for (let i = 0; i < newTree.length; i++) {
        if (newTree[i].id === id) {
            /**
             * This function is called when the tree node is found in updateTreeItemById.
             * You can then modify the node.
             */
            callback(newTree[i]);
            break;
        } else if (newTree[i].childNodes) {
            updateTreeItemById(id, newTree[i].childNodes, callback);
        }
    }
    /**
     * This function is called when the tree node is successfully modified by your
     * callback and can no be setted as state in your component.
     */
    setState && newTree[0] && setState(newTree);
}

function _findTreeItemById(
    id: TreeNodeId,
    tree: TreeNodeProps[] = [],
    callback: (node: TreeNodeProps, parentId: TreeNodeId) => void,
    parentId: TreeNodeId = 0,
) {
    for (let i = 0; i < tree.length; i++) {
        if (tree[i].id === id) {
            callback(tree[i], parentId);
            break;
        } else if (tree[i].childNodes) {
            _findTreeItemById(id, tree[i].childNodes, callback, tree[i].id);
        }
    }
}

/**
 * Get the parent id of a tree node.
 *
 * @param id The id which you want to fetch the parent of
 * @param tree Treenode properties
 * @param parentId If the item has no parent this id is returned
 * @returns Can also return undefined if not found
 */
export function getTreeParentById(id: TreeNodeId, tree: TreeNodeProps[], parentId: TreeNodeId = 0) {
    let found: TreeNodeId;
    _findTreeItemById(id, tree, (node, parentId) => (found = parentId), parentId);
    return found;
}

/**
 * Get the tree node by id.
 *
 * @param id The id
 * @param tree Treenode properties
 * @returns
 */
export function getTreeItemById(id: TreeNodeId, tree: TreeNodeProps[]) {
    let found: TreeNodeProps;
    _findTreeItemById(id, tree, (node) => (found = node));
    return found;
}

/**
 * Builds an ordered parent pairs array.
 *
 * @param tree The tree
 * @param rootId The root id
 * @returns With fid (node id) and the given pid (parent id)
 */
export function buildOrderedParentPairs(tree: TreeNodeProps[] = [], rootId: TreeNodeId = 0) {
    const pairs: { fid: TreeNodeId; pid: TreeNodeId }[] = [];
    const mapper = function (this: TreeNodeId, node: any) {
        pairs.push({ fid: node.id, pid: this });
        node.childNodes && node.childNodes.forEach(mapper.bind(node.id));
    };
    tree.forEach(mapper.bind(rootId));
    return pairs;
}

/**
 * Checks if two flat arrays are equal.
 */
export function flatArrayEqual(arr1: Array<any>, arr2: Array<any>) {
    if (arr1.length !== arr2.length) return false;
    for (let i = arr1.length; i--; ) {
        if (arr1[i] !== arr2[i]) return false;
    }
    return true;
}

/**
 * detect IE
 * returns version of IE or false, if browser is not Internet Explorer
 *
 * @see https://stackoverflow.com/questions/19999388/check-if-user-is-using-ie-with-jquery
 */
export function detectIE() {
    const ua = window.navigator.userAgent;

    const msie = ua.indexOf("MSIE ");
    if (msie > 0) {
        // IE 10 or older => return version number
        return parseInt(ua.substring(msie + 5, ua.indexOf(".", msie)), 10);
    }

    const trident = ua.indexOf("Trident/");
    if (trident > 0) {
        // IE 11 => return version number
        const rv = ua.indexOf("rv:");
        return parseInt(ua.substring(rv + 3, ua.indexOf(".", rv)), 10);
    }

    const edge = ua.indexOf("Edge/");
    if (edge > 0) {
        // Edge (IE 12+) => return version number
        return parseInt(ua.substring(edge + 5, ua.indexOf(".", edge)), 10);
    }

    // other browser
    return false;
}
