/**
 * @since Last modified: 2024/04/29 18:33:56
 * @namespace flatTools
 * @description The tools of flat data.
 * @see {@link https://codepen.io/axui/pen/zYXNRQB|demo @ codepen}
 */

'use strict';
import { T_el, T_fn, T_fn_none, T_null, T_obj, T_sel } from '../../../types/ax';
import { T_ajax_type, T_content_type } from '../../../types/utils';
import ax from '../ax';
import ajax from './ajax';
import clampVal from './clampVal';
import createEl from './createEl';
import deepClone from './deepClone';
import elState from './elState';
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';

type T_flatTools = {
    fromDl: T_fn;
    removeItem: T_fn;
    createItemObj: T_fn;
    addItem: T_fn;
    allToFlat: T_fn;
    moveItem: T_fn;
}
const flatTools: T_flatTools = {

    /**
     * @since Last modified: 2024/03/18 22:50:04
     * @memberOf
     * @description One-dimensional array deduplication.
     * @param {array} data - The array data.
     * @param {string} [prop] - The property specified by the deduplication operation.
     * @returns  {array} - The new array after removing duplicates.
     * @see {@link https://codepen.io/axui/pen/YzgZwRO|demo @ codepen}
     * @example
     * ax.dlToArr([0,11,22,11,12]);
     * <!--Return [0,11,22,12]-->
     */
    
    
    
    
    fromDl: (el: T_sel): T_obj[] => {
        
        let elem = getEl(el), result: T_obj[] = [];
        if (!elem) {
            console.warn(`No node, no access to data!`);
            return result;
        }
        let heads = [...elem.querySelectorAll('dt')],
            bodys = [...elem.querySelectorAll('dd')];
        
        if (heads.length !== bodys.length) {
            console.warn('Inconsistent number of "dt" and "dd" tags!');
            return result;
        }
        if (heads.length === 0) {
            console.warn('No "dt" and "dd" tags!');
            return result;
        }
        result = heads.map((k: any, i: number) => {
            
            return { content: bodys[i].innerHTML, ...getValsFromAttrs(k) }
        });
        return result;
    },
    /**
     * @memberOf flatTools
     * @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.flatTools.createBranch({source:'',target:'',flatData:''});
     */
    
    
    
    
    
    
    createItemObj: function (
        { source, flatData,  target}:
            { source: T_obj | string, flatData: any[], target?: string | number | T_obj}): void | T_obj {
        
        let obj: T_obj = {},
            targetObj = findItem(target, flatData),
            emptyArr = flatData.length === 0,
            sourceType = getDataType(source),
            setLabelVal = (data: T_obj, id: any) => {
                
                !data.hasOwnProperty('value') ? (data.hasOwnProperty('label') ? data.value = data.label : data.value = '') : null;
                
                !data.hasOwnProperty('label') ? data.label = ax.lang.flat.label + Date.now() : null;

            },
            setRootChild = (data: T_obj, id: number | string) => {
                
                let base = {
                    pId: rootStart,
                    path: rootStart + pathHyphen + id,
                    floor: floorStart
                }
                
                setLabelVal(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 as T_obj) };
        }
        
        if (emptyArr) {
            
            setRootChild(obj, idStart);
        } else {
            
            if (!targetObj) {
                
                obj.hasOwnProperty('id') ? newId = obj.id : null;
                
                setRootChild(obj, newId);
            } else {
                let props = {};
                
                setLabelVal(obj, newId);
                
                props = {
                    
                    path: targetObj.path.replace(new RegExp('(.*)' + targetObj.id), '$1' + obj.id),
                    floor: targetObj.floor
                };
                
                Object.assign(obj, props);
            }

        }

        return obj;
    },

    /**
     * @memberOf flatTools
     * @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.flatTools.addItem({source:''});
     */
    
    
    
    
    
    
    
    
    addItem: function ({ source, rootEl, flatData, treeData, brother, isFront = true, repeat = true, cb }:
        { source: T_obj, flatData: T_obj[], treeData: T_obj[], rootEl?: T_el, brother?: T_obj, isFront?: boolean, repeat?: boolean, cb?: T_fn }): void {
        if (!flatData || !source) { return }
        
        let existing = flatData.find((k: any) => 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 flatTools
     * @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.flatTools.removeItem({source:'',flatData:''});
     */
    
    
    
    
    
    
    removeItem: function ({ source, flatData, treeData, remove = true, cb, }:
        { source: T_obj | string | number, flatData: T_obj[], treeData: T_obj[], remove?: boolean, cb?: T_fn }): void {
        
        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 flatTools
     * @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.flatTools.getParentsFromPath({path:'',flatData:''});
     */
    
    
    
    
    
    
    getParentsFromPath({ path, flatData, labelHyphen = ax.config.labelHyphen, prop = 'label', pathHyphen = ax.config.pathHyphen }:
        { path: string, flatData: T_obj[], labelHyphen?: string, prop?: string, pathHyphen?: string }): T_obj {
        let paths = path.split(pathHyphen),
            parents = paths.map((k: any) => flatData.find((i: any) => i.id == k)).filter(Boolean),
            text = parents.map((k: any) => k[prop]).join(labelHyphen);
        return { parents, text };
    },
    /**
     * @memberOf flatTools
     * @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 {'get'|'post'} [params.ajaxType='post']  - ajax request 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 {HTMLElement} [params.bufSel]  - Node selector with loading effect.
     * @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.flatTools.allToFlat({content:''});
     */
    
    
    
    
    
    
    
    
    
    
    
    
    
    
    

    allToFlat: async function ({ content, idStart = ax.config.idStart, rootStart = ax.config.rootStart, floorStart = ax.config.rootStart, pathHyphen = ax.config.pathHyphen, contType = '', contData = {}, ajaxType = 'post', bufSel, fill = true, xhrName, cb }:
        { content: T_el | T_obj[] | string, idStart?: number, rootStart?: number, floorStart?: number, pathHyphen?: string, fill?: boolean, contType?: T_content_type, contData?: T_obj, ajaxType?: T_ajax_type, bufSel?: T_sel, xhrName?: string, cb?: T_fn }): Promise<T_obj[] | T_null> {
        
        if (isEmpty(content)) { return; }
        let treeData: T_obj[] = [],
            getFromNode = (node: T_el): T_obj[] => {
                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: T_obj[]): T_obj[] => {
                
                let copyData = deepClone(data);
                return this.toTree(copyData);
            };

        await getContent({
            content,
            contType,
            contData,
            ajaxType,
            xhrName,
            bufSel,
            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;
    },
    moveItem: function ({ source, target, flatData, treeData, rootEl, isFront = true, isChild = true, cb }:
        { source: string | number | T_el | T_obj, target: string | number | T_el | T_obj, flatData: T_obj[], treeData: T_obj[], rootEl?: T_el, isFront?: boolean, isChild?: boolean, cb?: T_fn }) {
        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 as T_obj).indexOf(targetObj),
                        id = isFront ? index : index + 1;
                    (targetParent as T_obj).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 flatTools;
