/**
 * @since Last modified: 2024-07-11 09:30:49
 * @namespace treeTools
 * @description The tools of tree.
 * @see {@link https://codepen.io/axui/pen/YzMXzKQ|demo @ codepen}
 */
'use strict';
import ax from '../ax';
import createEl from './createEl';
import deepClone from './deepClone';
import getContent from './getContent';
import getDataType from './getDataType';
import getEl from './getEl';
import increaseId from './increaseId';
import isEmpty from './isEmpty';
import getValsFromAttrs from './getValsFromAttrs';
import isNull from './isNull';
import findItem from './findItem';
import config from '../global/config';
const treeTools = {
    /**
     * @memberOf treeTools
     * @description Multi-dimensional tree array data to one-dimensional tree array data.
     * @param {any[]} data  - Multi-dimensional array data with "children" property.
     * @returns {object} Returns a one-dimensional tree array data.
     * @see {@link https://codepen.io/axui/pen/KKYpyqL|demo @ codepen}
     * @example
     * ax.treeTools.toFlat([]);
     */
    toFlat: function (data) {
        let result = [];
        data.forEach(k => {
            if (k.children && !data.includes(k.children[0])) {
                result = [...result, k, ...this.toFlat(k.children)];
            }
            else {
                result.push(k);
            }
        });
        return result;
    },
    /**
     * @memberOf treeTools
     * @description One-dimensional tree array data to multi-dimensional tree array data.
     * @param {any[]} data  - One-dimensional array data.
     * @returns {object} Returns a multi-dimensional tree array data.
     * @see {@link https://codepen.io/axui/pen/gOyPLmb|demo @ codepen}
     * @example
     * ax.treeTools.flat([]);
     */
    toTree: (data) => {
        if (!data || data.length === 0 || !data[0].hasOwnProperty('pId')) {
            return data;
        }
        let root = data[0].pId;
        data.forEach(k => {
            let children = data.filter(i => k.id == i.pId);
            if (children.length) {
                k.children = children;
            }
        });
        return data.filter(k => k.pId == root);
    },
    /**
     * @memberOf treeTools
     * @description Iterate over the tree data and take the path hierarchical path with id, pid and floor to make it easier to find parent-child chains.
     * @param {object} params  - Parameters.
     * @param {object[]} params.source  - Multidimensional array data with "children" property.
     * @param {number} [params.rootStart=-1]  - Root node id, usually -1.
     * @param {number} [params.idStart=0]  - The start id of the child node, usually 0 or 1.
     * @param {number} [params.floorStart=0]  - The start id of the floor, usually 0 or 1.
     * @param {string} [params.pathHyphen='>']  - Path connectors for the property path,usually '>'.
     * @param {boolean} [params.lbl2Val=true]  - If there is no 'value' property, should the 'label' value be used as the 'value'.
     * @returns {object} Returns the original array, but with id, pId, floor, and path properties.
     * @see {@link https://codepen.io/axui/pen/mdgJEqL|demo @ codepen}
     * @example
     * ax.treeTools.addIdPath({});
     */
    addIdPath: ({ source, rootStart = config.rootStart, idStart = config.idStart, floorStart = config.floorStart, pathHyphen = config.pathHyphen, lbl2Val = true }) => {
        if (!Array.isArray(source) || source.length === 0 || source[0].hasOwnProperty('path')) {
            return source;
        }
        let hasId = source[0].hasOwnProperty('id') ? true : false, index = idStart, each = (data, floor, path = rootStart) => {
            data.forEach(item => {
                if (!hasId) {
                    item.id = index;
                    index++;
                }
                item.floor = floor;
                item.pId = item.pId || item.pId === 0 ? item.pId : rootStart;
                item.path = path.toString() + pathHyphen + item.id.toString();
                (!item.hasOwnProperty('value') && item.hasOwnProperty('label') && lbl2Val) ? item.value = item.label : null;
                if (item.children && item.children.length > 0) {
                    item.children.forEach((k) => {
                        k.pId = item.id;
                    });
                    each(item.children, floor + 1, item.path);
                }
                if (!item.path) {
                    item.path = item.pId + pathHyphen + item.id;
                }
            });
        };
        each(source, floorStart);
        return source;
    },
    /**
     * @memberOf treeTools
     * @description Convert the select/datalist node tree to an array of objects, in the format: [{node:'',label:'',value:''},{node:'',label:'',value:''}...].If you need to bring the id, pId, path and floor properties you need to use the "treeTools.addIdPath" method.
     * @param {HTMLElement} el - Node selector
     * @returns {object[]}  - Returns the One- or two-dimensional tree array data.
     * @see {@link https://codepen.io/axui/pen/MWRwVxK|demo @ codepen}
     * @example
     * ax.treeTools.fromSelect('#id');
     * Node's format:
     *<select>
     * <optgroup label="TEXT">
     *     <option>TEXT</option>
     *     <option>TEXT</option>
     * </optgroup>
     * <optgroup label="TEXT">
     *     <option>TEXT</option>
     *     <option>TEXT</option>
     * </optgroup>
     *</select>
     *select or datalist are ok!
     */
    fromSelect: function (el) {
        let elem = getEl(el), result = [];
        if (!elem) {
            console.warn(`No node, no access to data!`);
            return result;
        }
        let getChild = () => {
            let data = [];
            [...elem.children].forEach(k => {
                data.push(getJson(k));
            });
            return data;
        }, getJson = (elem) => {
            let obj = { node: elem, ...getValsFromAttrs(elem) };
            if (elem.nodeName == 'OPTGROUP') {
                obj.children = [];
                [...elem.children].forEach(k => {
                    obj.children.push(getJson(k));
                });
            }
            return obj;
        };
        result = getChild();
        return result;
    },
    /**
     * @memberOf treeTools
     * @description Convert the ul+li/ol+li node tree to an array of objects, in the format: [{node:'',label:'',value:''},{node:'',label:'',value:''}...].If you need to bring the id, pId, path and floor properties you need to use the "treeTools.addIdPath" method.
     * @param {HTMLElement} el - Node selector
     * @returns {object[]}  - Returns the One- or two-dimensional tree array data.
     * @see {@link https://codepen.io/axui/pen/OJGVZOJ|demo @ codepen}
     * @example
     * ax.treeTools.fromUl('#id');
     * Node's format:
     * <ul>
     * <li><i>TEXT</i></li>
     * <li draggable="true"><i expanded="true">TEXT</i>
     *      <ul>
     *          <li><i>TEXT</i></li>
     *          <li><i>TEXT</i></li>
     *      </ul>
     * </li>
     * </ul>
     * ul+li or ol+li are ok!
     */
    fromUl: function (el) {
        let elem = getEl(el), result = [];
        if (!elem) {
            console.warn(`No node, no access to data!`);
            return result;
        }
        let getChild = () => {
            let data = [];
            [...elem.children].forEach(k => {
                data.push(getJson(k));
            });
            return data;
        }, getJson = (elem) => {
            let child1st = elem.firstElementChild, labelEl = child1st.querySelector('[role="label"]'), obj = { node: child1st, ...getValsFromAttrs(elem), ...getValsFromAttrs(child1st), ...getValsFromAttrs(labelEl) };
            let parent = elem.querySelector('ul') || elem.querySelector('ol');
            if (parent) {
                obj.children = [];
                [...parent.children].forEach(k => {
                    obj.children.push(getJson(k));
                });
            }
            return obj;
        };
        result = getChild();
        return result;
    },
    /**
     * @memberOf treeTools
     * @description Create a new branch object of tree (not yet pushed into the array or nodes generated).
     * @param {object} params  - Parameters.
     * @param {object|string|number} params.source  - Data sources.
     * @param {object[]} params.flatData  - One-dimensional tree array data.
     * @param {object|string|number} [params.target]  - Target object of flat data.
     * @param {boolean} [params.isChild]  - Is the new branch a sub-branch of target.
     * @param {number} [params.rootStart=-1]  - Root node id, usually -1.
     * @param {number} [params.idStart=0]  - The start id of the child node, usually 0 or 1.
     * @param {string} [params.pathHyphen='>']  - Path connectors for the property path,usually '>'.
     * @returns {object|void} Returns a new branch data with at least id, path, floor, label and pId properties.
     * @see {@link https://codepen.io/axui/pen/rNbVLpe|demo @ codepen}
     * @example
     * ax.treeTools.createBranch({source:'',target:'',flatData:''});
     */
    createBranchObj: function ({ source, flatData, treeData, target, isChild = true, rootStart = config.rootStart, idStart = config.idStart, floorStart = config.floorStart, pathHyphen = config.pathHyphen }) {
        let obj = {}, targetObj = findItem(target, flatData), emptyArr = flatData.length === 0, sourceType = getDataType(source), setIdLabelVal = (data, id) => {
            !data.hasOwnProperty('id') ? data.id = id : null;
            !data.hasOwnProperty('value') ? (data.hasOwnProperty('label') ? data.value = data.label : data.value = '') : null;
            !data.hasOwnProperty('label') ? data.label = ax.lang.tree.label + id : null;
        }, setRootChild = (data, id) => {
            let base = {
                pId: rootStart,
                path: rootStart + pathHyphen + id,
                floor: floorStart
            };
            setIdLabelVal(data, id);
            Object.assign(data, base);
        };
        if (isNull(source) || isNull(flatData)) {
            return obj;
        }
        if (sourceType === 'String' || sourceType === 'Number') {
            obj.label = source;
            obj.value = source;
        }
        else if (sourceType === 'Object') {
            obj = { ...source };
        }
        if (emptyArr) {
            setRootChild(obj, idStart);
        }
        else {
            let newId = increaseId(flatData);
            if (!targetObj) {
                obj.hasOwnProperty('id') ? newId = obj.id : null;
                setRootChild(obj, newId);
            }
            else {
                let props = {};
                setIdLabelVal(obj, newId);
                props = isChild ? {
                    pId: targetObj.id,
                    path: targetObj.path + pathHyphen + obj.id,
                    floor: targetObj.floor + 1
                } : {
                    pId: targetObj.pId,
                    path: targetObj.path.replace(new RegExp('(.*)' + targetObj.id), '$1' + obj.id),
                    floor: targetObj.floor
                };
                Object.assign(obj, props);
            }
        }
        return obj;
    },
    /**
     * @memberOf treeTools
     * @description Child node data to parent node data.
     * @param {object} params  - Parameters.
     * @param {object} params.source  - Data sources.
     * @param {string} [params.nodeName=ul]  - Node name of the child node container after conversion to a parent node.
     * @param {'flex'|'block'} [params.display='flex']  - he display attribute of the child node container.
     * @param {boolean} [params.expanded=true]  - Whether or not to expand the parent node after converting it to a parent node.
     * @param {function} [params.cb]  - Callback function, no parameters.
     * @returns null.
     * @see {@link https://codepen.io/axui/pen/oNOXMEw|demo @ codepen}
     * @example
     * ax.treeTools.childToParent({source:''});
     */
    childToParent: (source, arrowEnd = true) => {
        if (!source.children) {
            return;
        }
        if (!source.bodyEl) {
            source.bodyEl = createEl('ul');
            source.headEl.insertAdjacentElement('afterEnd', source.bodyEl);
        }
        if (!source.arrowEl) {
            source.arrowEl = createEl('i', { role: 'arrow' });
            source.headEl.insertAdjacentElement(arrowEnd ? 'beforeEnd' : 'afterBegin', source.arrowEl);
        }
    },
    parentToChild: (source) => {
        if (source.children) {
            return;
        }
        source.target.bodyEl.remove();
        source.target.bodyEl = null;
        source.target.arrowEl.remove();
        source.target.arrowEl = null;
        source.target.expanded = false;
    },
    /**
     * @memberOf treeTools
     * @description Adding nodes to the data after "createBranch".
     * @param {object} params  - Parameters.
     * @param {object} params.source  - Data sources.
     * @param {object[]} params.flatData  - One-dimensional array data.
     * @param {HTMLElement} params.rootEl  - Root node of tree, required if source.floor=0.
     * @param {object} [params.brother]  -Parent item for reference, required if source.floor!=0.
     * @param {boolean} [params.isChild=true]  - Whether the added node is a child node, "true" is a child node of brother; "false" is a sibling node of brother.
     * @param {boolean} [params.isFront=true]  - Whether the new node is added in front or behind, true means the sibling node is added in front of brother; true means it is added in the first position of "brother.children".
     * @returns null.
     * @see {@link https://codepen.io/axui/pen/RwOPYoE|demo @ codepen}
     * @example
     * ax.treeTools.addBranch({source:''});
     */
    addBranch: function ({ source, rootEl, flatData, treeData, brother, isFront = true, repeat = true, cb }) {
        if (!flatData || !source) {
            return;
        }
        let existing = flatData.find((k) => k.pId === source.pId && k.label === source.label), brotherObj = findItem(brother, flatData), parentObj = findItem(source.pId, flatData);
        if (!repeat && existing) {
            return;
        }
        if (parentObj) {
            if (brotherObj && parentObj.children.includes(brotherObj)) {
                let index = flatData.indexOf(brotherObj);
                if (isFront) {
                    parentObj.children.splice(index, 0, source);
                    brotherObj.wrapEl.insertAdjacentElement('beforeBegin', source.wrapEl);
                }
                else {
                    parentObj.children.splice(index + 1, 0, source);
                    brotherObj.wrapEl.insertAdjacentElement('afterEnd', source.wrapEl);
                }
            }
            else {
                if (isFront) {
                    parentObj.children.unshift(source);
                    parentObj.bodyEl.insertAdjacentElement('afterBegin', source.wrapEl);
                }
                else {
                    parentObj.children.push(source);
                    parentObj.bodyEl.insertAdjacentElement('beforeEnd', source.wrapEl);
                }
            }
        }
        else {
            if (brotherObj && brotherObj.pId === source.pId) {
                let index = treeData.indexOf(brotherObj);
                if (isFront) {
                    index > 0 ? treeData.splice(index, 0, source) : null;
                    brotherObj.wrapEl.insertAdjacentElement('beforeBegin', source.wrapEl);
                }
                else {
                    index > 0 ? treeData.splice(index + 1, 0, source) : null;
                    brotherObj.wrapEl.insertAdjacentElement('afterEnd', source.wrapEl);
                }
            }
            else {
                if (!rootEl) {
                    return;
                }
                if (isFront) {
                    treeData.unshift(source);
                    rootEl.insertAdjacentElement('afterBegin', source.wrapEl);
                }
                else {
                    treeData.push(source);
                    rootEl.insertAdjacentElement('beforeEnd', source.wrapEl);
                }
            }
        }
        cb && cb(source, treeData);
    },
    /**
     * @memberOf treeTools
     * @description Create a new branch object of tree (not yet pushed into the array or nodes generated).
     * @param {object} params  - Parameters.
     * @param {object|string|number} params.source  - Source to be deleted.The source to be deleted, if it is a string or a number, will be found from flatData.
     * @param {object[]} params.flatData  - One-dimensional tree array data.
     * @param {object[]} [params.treeData]  - Multi-dimensional tree array data.
     * @param {string} [params.pathHyphen='>']  - Path connectors for the property path,usually '>'.
     * @param {number} [params.floorStart=0]  - The top floor of array,usually 0.
     * @param {T_fn_none} [params.cb]  - Callback function that executes after removing.
     * @returns null.
     * @see {@link https://codepen.io/axui/pen/QWPbEMR|demo @ codepen}
     * @example
     * ax.treeTools.removeBranch({source:'',flatData:''});
     */
    removeBranch: function ({ source, flatData, treeData, cb, }) {
        if (isNull(source) || !flatData || flatData.length === 0) {
            return;
        }
        let obj = findItem(source, flatData), floorStart = treeData[0].floor;
        if (!obj) {
            return;
        }
        if (obj.floor === floorStart) {
            treeData.splice(treeData.indexOf(obj), 1);
        }
        else {
            let parent = findItem(obj.pId, flatData);
            if (!parent) {
                return;
            }
            let children = parent.children, index = children.indexOf(obj);
            children.splice(index, 1);
            if (children.length === 0) {
                this.parentToChild(parent);
            }
            ;
        }
        obj?.wrapEl && obj.wrapEl.remove();
        cb && cb(obj);
    },
    /**
     * @memberOf treeTools
     * @description Adding nodes to the data after "createBranch".
     * @param {object} params  - Parameters.
     * @param {string} params.path  - The path attribute of the child node, connecting the parent node with ">".
     * @param {object[]} params.flatData  - One-dimensional array data.
     * @param {string} [params.labelHyphen="/"]  - The concatenator of the parent node's set of label values,usually '/'.
     * @param {string} [params.prop="label"]  - Output the property name of the parent node.
     * @param {string} [params.pathHyphen='>']  - Path connectors for the property path,usually '>'.
     * @returns {object} - Return a object , that has parents and text properties.
     * @see {@link https://codepen.io/axui/pen/BaENOwa|demo @ codepen}
     * @example
     * ax.treeTools.getParentsFromPath({path:'',flatData:''});
     */
    getParentsFromPath({ path, flatData, labelHyphen = config.labelHyphen, prop = 'label', pathHyphen = config.pathHyphen }) {
        let paths = path.split(pathHyphen), parents = paths.map((k) => flatData.find((i) => i.id == k)).filter(Boolean), text = parents.map((k) => k[prop]).join(labelHyphen);
        return { parents, text };
    },
    /**
     * @memberOf treeTools
     * @description Get the full tree data via node, json or asynchronously.
     * @param {object} params  - Parameters.
     * @param {any} params.content  - Data content, may be node selector, json data, asynchronous address.The parameter is the same as the "content" parameter in getContent.
     * @param {number} [params.idStart=0]  - The start id of the child node, usually 0 or 1.
     * @param {number} [params.rootStart=-1]  - Root node id, usually -1.
     * @param {number} [params.floorStart=0]  - Root floor, usually 0.
     * @param {string} [params.pathHyphen='>']  - The hyphen of path,usally '>'.
     * @param {boolean} [params.async=false]  - Whether to enable asynchronous ajax request data, when the content should be string type.
     * @param {object} [params.contType='']  - ajax request content type.The parameter is the same as the "contType" parameter in getContent.
     * @param {object} [params.contData={}]  - ajax request data.The parameter is the same as the "contData" parameter in getContent.
     * @param {object} [params.ajax]  - Other parameters of AJAX requests, including type, xhrName, and spinSel.
     * @param {boolean} [params.fill=true]  - Whether to append id, pid, floor and path properties to tree nodes.
     * @param {string} [params.xhrName]  - The xhr name of the instance.
     * @param {function} [params.cb]  - Callback function, supports one parameter i.e. "treeData".
     * @returns {object[]} - Returns a Promise with the complete multidimensional tree array.
     * @see {@link https://codepen.io/axui/pen/ZEZGMoJ|demo @ codepen}
     * @example
     * ax.treeTools.allToTree({content:''});
     */
    allToTree: async function ({ content, idStart = config.idStart, rootStart = config.rootStart, floorStart = config.rootStart, pathHyphen = config.pathHyphen, contType = '', contData = {}, ajax = {}, fill = true, ins, cb }) {
        if (isEmpty(content)) {
            return;
        }
        let treeData = [], getFromNode = (node) => {
            let nodeName = node.nodeName, arr = [];
            if (nodeName === 'SELECT' || nodeName === 'DATALIST') {
                arr = this.fromSelect(node);
            }
            else if (nodeName === 'UL' || nodeName === 'OL') {
                arr = this.fromUl(node);
            }
            return arr;
        }, getFromArray = (data) => {
            let copyData = deepClone(data);
            return this.toTree(copyData);
        };
        await getContent.call(ins, {
            content,
            contType,
            contData,
            ajax,
            cb: (data) => {
                let dataType = getDataType(data);
                if (dataType === 'Array') {
                    treeData = getFromArray(data);
                }
                else if (dataType.includes('HTML')) {
                    ['UL', 'OL', 'DATALIST', 'SELECT'].includes(data.nodeName) && (treeData = getFromNode(data));
                }
                else if (dataType === 'String') {
                    let tmp = createEl('div', '', data), node = tmp.querySelector('ul') || tmp.querySelector('ol') || tmp.querySelector('datalist') || tmp.querySelector('select');
                    if (node) {
                        treeData = getFromNode(node);
                        tmp.remove();
                    }
                }
            }
        });
        fill ? this.addIdPath({ source: treeData, rootStart, idStart, floorStart, pathHyphen }) : null;
        cb && cb(treeData);
        return treeData;
    },
    getPathHyphen: (treeData) => {
        let item = treeData[0], id = item.id, floor = item.floor, pId = item.pId, path = item.path, hyphen = item.path.replace(pId, '').replace(id, '');
        return { id, floor, pId, path, hyphen };
    },
    graftBranch: function ({ source, target, flatData, treeData, rootEl, isFront = true, isChild = true, cb }) {
        if (isNull(source) || isEmpty(treeData)) {
            return;
        }
        let sourceObj = findItem(source, flatData), targetObj = findItem(target, flatData), firstData = this.getPathHyphen(treeData), floorStart = firstData.floor, rootStart = firstData.pId, pathHyphen = firstData.hyphen;
        if (!sourceObj) {
            return;
        }
        let sourceParent = findItem(sourceObj.pId, flatData);
        if (!sourceParent) {
            treeData.splice(treeData.indexOf(sourceObj), 1);
        }
        else {
            sourceParent.children.splice(sourceParent.children.indexOf(sourceObj), 1);
        }
        if (!targetObj) {
            if (isFront) {
                rootEl?.insertAdjacentElement('afterBegin', sourceObj.wrapEl);
                treeData.unshift(sourceObj);
            }
            else {
                rootEl?.insertAdjacentElement('beforeEnd', sourceObj.wrapEl);
                treeData.push(sourceObj);
            }
            sourceObj.pId = rootStart;
            sourceObj.floor = floorStart;
            sourceObj.path = rootStart + pathHyphen + sourceObj.id;
        }
        else {
            if (isChild) {
                targetObj.children = [];
                if (isFront) {
                    targetObj.bodyEl.insertAdjacentElement('afterBegin', sourceObj.wrapEl);
                    targetObj.children.unshift(sourceObj);
                }
                else {
                    targetObj.bodyEl.insertAdjacentElement('beforeEnd', sourceObj.wrapEl);
                    targetObj.children.push(sourceObj);
                }
                sourceObj.pId = targetObj.id;
                sourceObj.floor = targetObj.floor + 1;
                sourceObj.path = targetObj.path + pathHyphen + sourceObj.id;
            }
            else {
                if (targetObj.floorStart === floorStart) {
                    let index = treeData.indexOf(targetObj), id = isFront ? index : index + 1;
                    treeData.splice(id, 0, sourceObj);
                }
                else {
                    let targetParent = findItem(targetObj.pId, flatData), index = targetParent.indexOf(targetObj), id = isFront ? index : index + 1;
                    targetParent.children.splice(id, 0, sourceObj);
                }
                sourceObj.pId = targetObj.pId;
                sourceObj.floor = targetObj.floor;
                sourceObj.path = targetObj.path.replace(new RegExp('(.*)' + targetObj.id), '$1' + sourceObj.id);
                let position = isFront ? 'beforeBegin' : 'afterEnd';
                targetObj.wrapEl.insertAdjacentElement(position, sourceObj.wrapEl);
            }
        }
        cb && cb(sourceObj, targetObj);
    }
};
export default treeTools;
