import React, { useEffect, useState, forwardRef, useRef, useImperativeHandle } from 'react';

import RootPortal from 'funda-root-portal';

import useComId from 'funda-utils/dist/cjs/useComId';
import useWindowScroll from 'funda-utils/dist/cjs/useWindowScroll';
import useClickOutside from 'funda-utils/dist/cjs/useClickOutside';
import {
    extractorExist,
    extractContentsOfBraces,
    extractContentsOfMixedCharactersWithBraces, 
    extractContentsOfMixedCharactersWithComma
} from 'funda-utils/dist/cjs/extract';
import {
    convertArrToValByBraces
} from 'funda-utils/dist/cjs/convert';
import {
    getAbsolutePositionOfStage
} from 'funda-utils/dist/cjs/getElementProperty';
import {
    htmlToPlain
} from 'funda-utils/dist/cjs/format-string';
import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';



import Group from './Group';


export type CascadingSelectOptionChangeFnType = (input: any, currentData: any, index: any, depth: any, value: string, closeFunc: any) => void;


export type CascadingSelectProps = {
    popupRef?: React.ForwardedRef<any>; // could use "Array" on popupRef.current, such as popupRef.current[0], popupRef.current[1]
    wrapperClassName?: string;
    controlClassName?: string;
    controlExClassName?: string;
    controlGroupWrapperClassName?: string;
    controlGroupTextClassName?: string;
    searchable?: boolean;
    searchPlaceholder?: string;
    perColumnHeadersShow?: boolean;
    exceededSidePosOffset?: number;
    value?: string;
    label?: React.ReactNode | string;
    name?: string;
    placeholder?: string;
    readOnly?: any;
    disabled?: any;
    required?: any;
    requiredLabel?: React.ReactNode | string;
    units?: React.ReactNode | string;
    iconLeft?: React.ReactNode | string;
    iconRight?: React.ReactNode | string;
    minLength?: any;
    maxLength?: any;
    /** Whether to use curly braces to save result and initialize default value */
    extractValueByBraces?: boolean;
    /** Set headers for each column group */
    columnTitle?: any[];
    /** Set whether to use "label" or "value" for the value of this form
     * Optional values: `label`, `value`
     */
    valueType?: string;
    /** Whether to display the close button. */
    showCloseBtn?: boolean;
    /** Set the depth value of the control to control the display of the pop-up layer appear above.
     * Please set it when multiple controls are used at the same time. */
    depth?: number;
    /** Set a loader component to show while the component waits for the next load of data. 
     * e.g. `<span>Loading...</span>` or any fancy loader element */
    loader?: React.ReactNode;
    /** Whether it can be modified in the input box */
    inputable?: boolean;
    /** Set an arrow of breadcrumb result */
    displayResultArrow?: React.ReactNode;
    /** Set an arrow of control */
    controlArrow?: React.ReactNode;
    /** Specify a class for trigger. */
    triggerClassName?: string;
    /** Set a piece of text or HTML code for the trigger */
    triggerContent?: React.ReactNode;
    /** Specify a class for clean node button. */
    cleanNodeBtnClassName?: string;
    /** Set a piece of text or HTML code for the clean node button */
    cleanNodeBtnContent?: React.ReactNode;
    /** -- */
    id?: string;
    style?: React.CSSProperties;
    tabIndex?: number;
    [key: `data-${string}`]: string | undefined;
    fetchFuncAsync?: any;
    fetchFuncMethod?: string;
    fetchFuncMethodParams?: any[];
    fetchCallback?: (data: any) => void;
    onFetch?: (data: any, childrenData: any) => void;
    onChange?: CascadingSelectOptionChangeFnType | null;
    onBlur?: (e: any) => void;
    onFocus?: (e: any) => void;
    /**
     * Customize the function of formatting the value of the input input box, and the parameters are labels, values, and queryIds
     * Returns a string as the value of the input
     */
    formatInputResult?: (param: Array<{label: string, value: string | number}>) => string;
};


const CascadingSelect = forwardRef((props: CascadingSelectProps, externalRef: any) => {
    const {
        popupRef,
        wrapperClassName,
        controlClassName,
        controlExClassName,
        controlGroupWrapperClassName,
        controlGroupTextClassName,
        searchable = false,
        searchPlaceholder = '',
        perColumnHeadersShow = true,
        exceededSidePosOffset,
        readOnly,
        disabled,
        required,
        requiredLabel,
        units,
        iconLeft,
        iconRight,
        minLength,
        maxLength,
        value,
        label,
        placeholder,
        name,
        id,
        extractValueByBraces,
        columnTitle,
        depth,
        loader,
        inputable = false,
        displayResultArrow,
        controlArrow,
        valueType,
        showCloseBtn,
        style,
        tabIndex,
        triggerClassName,
        triggerContent,
        cleanNodeBtnClassName,
        cleanNodeBtnContent,
        fetchFuncAsync,
        fetchFuncMethod,
        fetchFuncMethodParams,
        fetchCallback,
        onFetch,
        onChange,
        onBlur,
        onFocus,
        formatInputResult,
        ...attributes
    } = props;


    const DEPTH = depth || 1055;  // the default value same as bootstrap
    const POS_OFFSET = 0;
    const EXCEEDED_SIDE_POS_OFFSET = Number(exceededSidePosOffset) || 15;
    const VALUE_BY_BRACES = typeof extractValueByBraces === 'undefined' ? true : extractValueByBraces;
    const uniqueID = useComId();
    const idRes = id || uniqueID;
    const rootRef = useRef<any>(null);
    const inputRef = useRef<any>(null);
    const listRef = useRef<any>(null);
    const MIN_SPACE_FOR_DROPDOWN = 200; // Minimum space needed to show dropdown below trigger

    // searchable
    const [columnSearchKeywords, setColumnSearchKeywords] = useState<string[]>([]);


    const propExist = (p: any) => {
        return typeof p !== 'undefined' && p !== null && p !== '';
    };

    const resultInput = (curData: string[] | number[], curQueryIdsData: string[] | number[]) => {
        return VALUE_BY_BRACES ? convertArrToValByBraces(curData.map((item: any, i: number) => `${item}[${curQueryIdsData[i]}]`)) : curData.map((item: any, i: number) => `${item}[${curQueryIdsData[i]}]`)!.join(',');
    };

    const resultInputPureText = (inputStr: string) => {
        return VALUE_BY_BRACES ? `{${inputStr}[]}` : `${inputStr}[]`;

        // value1: {{curLabel[curValue]}[]}
        // value2: curLabel[curValue][]
    };


    // exposes the following methods
    useImperativeHandle(
        popupRef,
        () => ({
            close: () => {
                cancel();
            },
        }),
        [popupRef],
    );



    // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    // DO NOT USE `useState()` for `dictionaryData`, `listData`,  
    // because the list uses vanilla JS DOM events which will cause the results of useState not to be displayed in real time.
    // @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@



    const dictionaryData = useRef<any[]>([]);

    const [loading, setLoading] = useState<boolean>(false);
    const [columnTitleData, setColumnTitleData] = useState<any[]>([]);
    const [hasErr, setHasErr] = useState<boolean>(false);
    const [changedVal, setChangedVal] = useState<string>(value || '');

    

    //for variable 
    const listData = useRef<any[]>([]);
    const selectedData = useRef<any>({
        labels: [],
        values: []
    });

    const [isShow, setIsShow] = useState<boolean>(false);


    // click outside
    useClickOutside({
        enabled: isShow && rootRef.current && listRef.current,
        isOutside: (event: any) => {

            // close dropdown when other dropdown is opened
            return (
                (rootRef.current !== event.target && !rootRef.current.contains(event.target as HTMLElement)) &&
                listRef.current !== event.target && !listRef.current.contains(event.target as HTMLElement)
            )
              
        },
        handle: (event: any) => {
            cancel();
        }
    }, [isShow, rootRef, listRef]);

    

    // Add function to the element that should be used as the scrollable area.
    const [scrollData, windowScrollUpdate] = useWindowScroll({
        performance: ['debounce', 500],   // "['debounce', 500]" or "['throttle', 500]"
        handle: (scrollData: any) => {
            popwinPosInit(false);
        }
    });



    function popwinPosInit(showAct: boolean = true) {
        if (rootRef.current === null || inputRef.current === null) return

        // update modal position
        const _modalRef: any = document.querySelector(`#casc-menu__items-wrapper-${idRes}`);
        const _triggerRef: any = inputRef.current;

        // console.log(getAbsolutePositionOfStage(_triggerRef));

        if (_modalRef === null) return;

        const { x } = getAbsolutePositionOfStage(_triggerRef);
        const { y, width, height } = getAbsolutePositionOfStage(_triggerRef);
        const _triggerBox = _triggerRef.getBoundingClientRect();
        let targetPos = '';

        // STEP 1:
        //-----------
        // display wrapper
        if (showAct) _modalRef.classList.add('active');


        // STEP 2:
        //-----------
        // Detect position
        if (window.innerHeight - _triggerBox.top > MIN_SPACE_FOR_DROPDOWN) {
            targetPos = 'bottom';
        } else {
            targetPos = 'top';
        }


        // STEP 3:
        //-----------
        // Adjust position
        if (targetPos === 'top') {
            _modalRef.style.left = x + 'px';
            //_modalRef.style.top = y - POS_OFFSET - (listRef.current.clientHeight) - 2 + 'px';
            _modalRef.style.top = 'auto';
            _modalRef.style.bottom = (window.innerHeight - _triggerBox.top) + POS_OFFSET + 2 + 'px';
            _modalRef.style.setProperty('position', 'fixed', 'important');
            _modalRef.classList.add('pos-top');
        }

        if (targetPos === 'bottom') {
            _modalRef.style.left = x + 'px';
            _modalRef.style.bottom = 'auto';
            _modalRef.style.top = y + height + POS_OFFSET + 'px';
            _modalRef.style.setProperty('position', 'absolute', 'important');
            _modalRef.classList.remove('pos-top');
        }
   


        // STEP 4:
        //-----------
        // Determine whether it exceeds the far right or left side of the screen
        const _modalContent = _modalRef;
        const _modalBox = _modalContent.getBoundingClientRect();
        if (typeof _modalContent.dataset.offset === 'undefined' && _modalBox.left > 0) {

            // 10 pixels is used to account for some bias in mobile devices
            if ((_modalBox.right + 10) > window.innerWidth) {
                const _modalOffsetPosition = _modalBox.right - window.innerWidth + EXCEEDED_SIDE_POS_OFFSET;
                _modalContent.dataset.offset = _modalOffsetPosition;
                _modalContent.style.marginLeft = `-${_modalOffsetPosition}px`;
                // console.log('_modalPosition: ', _modalOffsetPosition)
            }

            if ((_modalBox.left - 10) < 0) {
                const _modalOffsetPosition = Math.abs(_modalBox.left) + EXCEEDED_SIDE_POS_OFFSET;
                _modalContent.dataset.offset = _modalOffsetPosition;
                _modalContent.style.marginLeft = `${_modalOffsetPosition}px`;
                // console.log('_modalPosition: ', _modalOffsetPosition)
            }


        }



    }



    function popwinPosHide() {

        const _modalRef: any = document.querySelector(`#casc-menu__items-wrapper-${idRes}`);

        if (_modalRef !== null) {
            // remove classnames and styles
            _modalRef.classList.remove('active');

        }

    }

    function updateColDisplay(useFetch: boolean, emptyAction: boolean = false, level: number | undefined) {
        if (listRef.current === null) return;

        let latestDisplayColIndex: number = 0;
        const currentItemsInner: any = listRef.current.querySelector('.casc-menu__items-inner');
        if (currentItemsInner !== null) {
            const colItemsWrapper = [].slice.call(currentItemsInner.querySelectorAll('.casc-menu__items-col'));
            colItemsWrapper.forEach((perCol: any) => {
                perCol.classList.remove('hide-col');
            });

            colItemsWrapper.some((perCol: any, i: number) => {
                const hasActive = [].slice.call(perCol.querySelectorAll('[data-opt]')).some((el: HTMLElement) => el.classList.contains('active'));
                if (!hasActive) {
                    latestDisplayColIndex = i;
                    return true;
                }
                return false;
            });

            // remove columns behind the current empty trigger
            colItemsWrapper.forEach((perCol: any, i: number) => {
                if (!emptyAction) {
                    if (useFetch) {
                        if (i > latestDisplayColIndex && latestDisplayColIndex > 0) perCol.classList.add('hide-col');
                    } else {
                        if (i === latestDisplayColIndex && latestDisplayColIndex > 0) perCol.classList.add('hide-col');
                    }
                } else {
                    if (typeof level !== 'undefined' && Number.isInteger(level)) {
                        if (i > level) perCol.classList.add('hide-col');
                    }

                }



            });
        }

    }


    function cancel() {
        // hide list
        setIsShow(false);
        popwinPosHide();
    }

    function activate() {
        // show list
        setIsShow(true);

        // window position
        setTimeout(() => {
            popwinPosInit();
        }, 0);

    }


    async function fetchData(params: any) {

        if (typeof fetchFuncAsync === 'object') {


            //
            setLoading(true);


            const response: any = await fetchFuncAsync[`${fetchFuncMethod}`](...params.split(','));
            let _ORGIN_DATA = response.data;

            // loading 
            setLoading(false);

            if (typeof _ORGIN_DATA[0] === 'undefined') return;

            // reset data structure
            if (typeof (fetchCallback) === 'function') {
                _ORGIN_DATA = fetchCallback(_ORGIN_DATA);
            }


            // Determine whether the data structure matches
            if (_ORGIN_DATA.length > 0 && typeof _ORGIN_DATA[0].id === 'undefined') {
                console.warn('The data structure does not match, please refer to the example in the component documentation.');
                setHasErr(true);
                _ORGIN_DATA = [];
            }

            // STEP 1: ===========
            // column titles
            fillColumnTitle(_ORGIN_DATA);


            // STEP 2: ===========
            // dictionary data (orginal)
            dictionaryData.current = _ORGIN_DATA;

            // STEP 3: ===========
            // Add an empty item to each list to support empty item selection
            const _EMPTY_SUPPORTED_DATA = JSON.parse(JSON.stringify(_ORGIN_DATA));
            addEmptyOpt(_EMPTY_SUPPORTED_DATA, 0);


            // STEP 4: ===========
            // Turn the data of each group into an array
            listData.current = [_EMPTY_SUPPORTED_DATA];



            // STEP 5: ===========
            //
            onFetch?.(_EMPTY_SUPPORTED_DATA, _ORGIN_DATA);



            // STEP 6: ===========
            // update column display with DOM
            updateColDisplay(true, false, undefined);



            return [_ORGIN_DATA, _EMPTY_SUPPORTED_DATA];
        } else {
            return [];
        }


    }


    //
    function doFetch() {
        // data fetch action
        const _params: any[] = fetchFuncMethodParams || [];
        return fetchData((_params).join(','));
    }




    function handleFocus(event: any) {
        rootRef.current?.classList.add('focus-floating');

        //
        handleDisplayOptions(null);

        //
        onFocus?.(event);
    }


    function handleBlur(event: any) {

        //----
        //remove focus style
        rootRef.current?.classList.remove('focus-floating');


        //
        onBlur?.(event);
    }




    function handleDisplayOptions(event: any) {
        if (event) event.preventDefault();
        if (isShow) return;
        activate();

    }


    function handleClickItem(e: any, resValue: any, index: number, level: number, curData: any[]) {
        e.preventDefault();

        // update column display with DOM
        //////////////////////////////////////////
        updateColDisplay(true, false, level);


        // update value
        //////////////////////////////////////////
        const inputVal = updateValue(dictionaryData.current, resValue.id, level);


        // callback
        //////////////////////////////////////////
        if (typeof onChange === 'function') {
            const curValString = valueType === 'value' ? inputVal[0] : inputVal[1];
            const curValCallback = typeof formatInputResult === 'function' ? formatInputResult(
                VALUE_BY_BRACES
                    ? extractContentsOfMixedCharactersWithBraces(curValString)
                    : extractContentsOfMixedCharactersWithComma(curValString)
            ) : curValString;

            onChange(inputRef.current, resValue, index, level, curValCallback, cancel);
        }


        // update data
        //////////////////////////////////////////
        const newData: any = curData;  // such as: [Array(6), Array(3)]

        // All the elements from start(array.length - start) to the end of the array will be deleted.
        newData.splice(level + 1);

        // When requesting a return asynchronously, a new column is added only under the currently clicked column, 
        // and the previous column cannot be affected.
        // Make sure that subsequent asynchronous requests will only insert new columns towards the level+1 position.
        listData.current = [...newData];


        // active status
        if (resValue.children) {
            const childList = resValue.children;
            markAllItems(childList);
            newData[level + 1] = childList;
        }


        markCurrent(newData[level], index);


        // update actived items
        //////////////////////////////////////////
        listData.current = newData;


        // close modal
        //////////////////////////////////////////
        if (typeof resValue.children === 'undefined' && resValue.id.toString().indexOf('$EMPTY_ID_') < 0) {
            //
            cancel();
        }


        // active current option with DOM
        //////////////////////////////////////////
        const currentItemsInner: any = e.currentTarget.closest('.casc-menu__items-inner');
        if (currentItemsInner !== null) {
            curData.forEach((v: any, col: number) => {
                const colItemsWrapper = currentItemsInner.querySelectorAll('.casc-menu__items-col');
                colItemsWrapper.forEach((perCol: HTMLUListElement) => {
                    const _col = Number(perCol.dataset.col);

                    if (_col >= level) {
                        [].slice.call(perCol.querySelectorAll('[data-opt]')).forEach((node: HTMLElement) => {
                            node.classList.remove('active');
                        });
                    }
                });
            });


            // not header option
            if (typeof e.currentTarget.dataset.optHeader === 'undefined') e.currentTarget.classList.add('active');


        }


    }



    /**
     * Active the selected item
    * @param arr 
    * @param index 
    * @returns 
    */
    function markCurrent(arr: any[], index: number) {
        if (!Array.isArray(arr)) return;

        // click an item
        //////////////////////////////////////////
        for (let i = 0; i < arr.length; i++) {
            if (i === index) {
                arr[i].current = true;
            } else {
                arr[i].current = false;
            }
        }

        // return result
        //////////////////////////////////////////
        return arr;
    }

    /**
     * Deactivate all items
     * @param arr 
     * @returns 
     */
    function markAllItems(arr: any[]) {
        for (let i = 0; i < arr.length; i++) {
            arr[i].current = false;
            if (arr[i].children) markAllItems(arr[i].children);
        }
    }



    function updateValue(arr: any[], targetVal: any, level: number | boolean = false) {

        const inputEl: any = inputRef.current;
        let _valueData: any, _labelData: any;


        if (targetVal.toString().indexOf('$EMPTY_ID_') >= 0) {

            // If clearing the current column
            //////////////////////////////////////////
            _valueData = selectedData.current.values;
            _labelData = selectedData.current.labels;

            // update result to input
            _valueData.splice(level);
            _labelData.splice(level);

            //
            selectedData.current = {
                labels: _labelData,
                values: _valueData
            };


        } else {


            // click an item
            //////////////////////////////////////////
            //search JSON key that contains specific string
            const _labels = queryResultOfJSON(arr, targetVal, 'value');
            const _values = queryResultOfJSON(arr, targetVal, 'key');



            // update result to input
            _valueData = _values ? _values.map((item: any) => item) : [];
            _labelData = _labels ? _labels.map((item: any) => item) : [];

            //
            selectedData.current = {
                labels: _labelData,
                values: _valueData
            };



        }

        // update selected data 
        //////////////////////////////////////////
        const inputVal_0 = resultInput(_valueData, _valueData);
        const inputVal_1 = resultInput(_labelData, _valueData);

        if (valueType === 'value') {
            if (inputEl !== null) setChangedVal(inputVal_0);
        } else {
            if (inputEl !== null) setChangedVal(inputVal_1);
        }

        return {
            0: inputVal_0,
            1: inputVal_1
        }

    }


    function cleanValue() {
        selectedData.current = {
            labels: [],
            values: []
        };


        dictionaryData.current = [];
        listData.current = [];

        setChangedVal('');
    }


    function chkValueExist(v: any) {
        return typeof v !== 'undefined' && v !== '';
    }


    function initDefaultValue(defaultValue: any) {

        // STEP 1:
        // change the value to trigger component rendering
        //--------------------------------  
        if (!chkValueExist(defaultValue)) {
            cleanValue();
        } else {
            setChangedVal(defaultValue);
        }


        // STEP 2:
        // do fetch
        //--------------------------------  
        doFetch()?.then((response: any) => {


            const _data = response[1];

            // Determine whether the splicing value of the default value is empty
            if (chkValueExist(defaultValue)) {


                // if the default value uses the pure string
                if (!extractorExist(defaultValue)) {

                    //Set a default value
                    selectedData.current = {
                        labels: [defaultValue],
                        values: ['']
                    };
                    setChangedVal(defaultValue);

                    return; // required RETURN
                }


                const rowQueryAttr: string = valueType === 'value' ? 'id' : 'name';
                const targetVal: any = defaultValue.match(/(\[.*?\])/gi)!.map((item: any, i: number) => VALUE_BY_BRACES ? extractContentsOfBraces(defaultValue)[i].replace(item, '') : defaultValue.split(',')[i].replace(item, ''));

                //
                const _allColumnsData: any[] = [];
                const _allLables: any[] = [];
                const _allValues: any[] = [];


                // loop over each column
                //////////////////////////////////////////
                for (let col = 0; col <= targetVal.length; col++) {

                    if (col === 0) {

                        // STEP 1: ===========
                        //active item from current column
                        //////////////////////////////////////////
                        const newData: any[] = JSON.parse(JSON.stringify(_data));
                        const activedIndex = _data.findIndex((item: any) => {
                            return item[rowQueryAttr].toString() === targetVal[col].toString();
                        });

                        markAllItems(newData);
                        markCurrent(newData, activedIndex);

                        //
                        if (activedIndex !== -1) {
                            _allLables.push(newData[activedIndex].name);
                        }

                        _allColumnsData.push(newData);

                    }

                    if (col > 0) {


                        const _findNode: any = searchObject(_data, function (v: any) { return v != null && v != undefined && v[rowQueryAttr] == targetVal[col - 1]; });

                        const childList = typeof _findNode[0] !== 'undefined' ? _findNode[0].children : undefined;

                        // STEP 1: ===========
                        //active item from current column
                        //////////////////////////////////////////
                        if (typeof childList !== 'undefined') {

                            const newData: any[] = JSON.parse(JSON.stringify(childList));
                            const activedIndex = newData.findIndex((item: any) => {
                                if (typeof targetVal[col] === 'undefined') return -1;
                                return item[rowQueryAttr].toString() === targetVal[col].toString();
                            });

                            markAllItems(newData);
                            markCurrent(newData, activedIndex);

                            //
                            if (activedIndex !== -1) {
                                _allLables.push(newData[activedIndex].name);
                            }

                            _allColumnsData.push(newData);

                        }


                    }


                }




                // STEP 2: ===========
                // update actived items
                //////////////////////////////////////////
                listData.current = _allColumnsData;



                // STEP 3: ===========
                // Set a default value
                //////////////////////////////////////////
                selectedData.current = {
                    labels: _allLables,
                    values: _allValues
                };


            }


        });


        // Determine whether the splicing value of the default value is empty
        if (typeof defaultValue !== 'undefined' && defaultValue !== '') {

            const formattedDefaultValue = VALUE_BY_BRACES ? extractContentsOfBraces(defaultValue) : defaultValue.split(',');

            const emptyDefaultValueCheck = Array.isArray(formattedDefaultValue) ? formattedDefaultValue.every((item: any, index: number) => {
                if (item !== '[]') {
                    return false;
                }

                return true;
            }) : true;

            if (emptyDefaultValueCheck) {
                cleanValue();
            }

        }



    }



    function fillColumnTitle(obj: any[]) {

        const dataDepth = getDepth(obj);
        const oldColumnTitleData = columnTitle ? columnTitle : [];
        const newColumnTitleData = new Array(dataDepth)?.fill('');
        oldColumnTitleData!.forEach((item: any, index: number) => {
            newColumnTitleData[index] = item;
        });

        //
        if (oldColumnTitleData.length > dataDepth) {
            newColumnTitleData.splice(dataDepth, oldColumnTitleData.length - dataDepth);
        }


        setColumnTitleData(newColumnTitleData);
    }

    function getDepth(obj: any[]) {
        let depth = 0;

        obj.forEach((item: any) => {
            if (item.children) {
                item.children.forEach(function (d: any) {
                    const tmpDepth = getDepth(item.children);
                    if (tmpDepth > depth) {
                        depth = tmpDepth;
                    }
                });
            }
        });

        return 1 + depth;
    }

    function addEmptyOpt(obj: any[], index: number) {

        index++;

        obj.unshift({
            id: "$EMPTY_ID_" + index,
            name: "",
            itemDepth: obj.length === 0 ? 0 : obj[0].itemDepth
        });

        obj.forEach((item: any, depth: number) => {
            if (item.children) {
                addEmptyOpt(item.children, index * (depth + 1));
            }
        });
    }

    function searchObject(object: any, matchCallback: any, result: any[] = [], searched: any[] = []) {
        if (searched.indexOf(object as never) !== -1 && object === Object(object)) {
            return;
        }
        searched.push(object as never);
        if (matchCallback(object)) {
            result.push(object as never);
        }
        try {
            if (object === Object(object)) {
                for (var property in object) {
                    if (property.indexOf("$") !== 0) {
                        searchObject(object[property], matchCallback, result, searched);
                    }
                }
            }
        }
        catch (e) {
            throw e;
        }
        return result;
    }



    function queryResultOfJSON(data: any[], targetVal: any, returnType: string) {

        let callbackValueNested: any[] = [];
        let lastFirstLevelName = '';
        let loop = true;
        let resDepth = 0;
        const rowQueryAttr = 'id';

        const getIndexOf = function (arr: any[], val: any) {
            for (let i = 0; i < arr.length; i++) {
                if (arr[i][rowQueryAttr].toString() === val.toString()) return i;
            }
            return -1;
        };


        const searchJsonStr = function (list: any[], depth?: any) {

            // `depth` is very important, it is used to accurately judge the final result
            if (typeof (depth) === 'undefined') {
                depth = 0;
            } else {
                depth++;
            }

            for (let i = 0; i < list.length; i++) {

                const row = list[i];
                let callbackValue: any;

                if (returnType === 'key') callbackValue = row[rowQueryAttr].toString();
                if (returnType === 'value') callbackValue = row.name.toString();


                if (loop) {
                    // get first-level item
                    if (getIndexOf(data, row[rowQueryAttr]) !== -1) {
                        callbackValueNested.push(callbackValue as never);
                        lastFirstLevelName = callbackValue;
                    }

                    // get child-level item
                    if (row.children) {
                        callbackValueNested.push(callbackValue as never);
                    }

                }


                //check the value
                if (row[rowQueryAttr].toString() === targetVal.toString()) {
                    callbackValueNested.push(callbackValue as never);
                    loop = false;
                    resDepth = depth;
                    break;
                }

                // Note: Recursion must be placed here
                if (loop) {
                    if (row.children) {
                        searchJsonStr(row.children, depth);
                    }
                }


            }


        }
        searchJsonStr(data);


        // (1) Remove duplicate values
        //------------------------------------------
        callbackValueNested = callbackValueNested.filter(function (item, index, arr) {
            return arr.indexOf(item, 0) === index;
        });


        // (2) Delete needless first-level
        //------------------------------------------
        let resAll = callbackValueNested.slice(callbackValueNested.indexOf(lastFirstLevelName as never), callbackValueNested.length)


        // (3) Returns result
        //------------------------------------------
        if (resAll.length > 1) {
            // Get first-level item
            resAll.splice(1);

            // Get child-level item
            let resChild = callbackValueNested.slice(-resDepth); // Get the last elements in reverse

            // Combine
            resAll = resAll.concat(resChild);

        }

        return resAll;

    }


    function displayInfo() {

        const _data = selectedData.current;
        let formattedDefaultValue: any = changedVal !== '' ? VALUE_BY_BRACES ? extractContentsOfBraces(changedVal) : changedVal.split(',') : [];
        let _labels = Array.isArray(_data.labels) && _data.labels.length > 0 ? _data.labels : [];

        // Prevent value from being a pure string that does not include "[]"
        if (formattedDefaultValue === '') formattedDefaultValue = [];

        // Sometimes the array may be empty due to rendering speed
        if (_labels.length === 0) {
            _labels = formattedDefaultValue.map((s: string | number) => s.toString().replace(/[\w\s]/gi, '').replace(/\[\]/g, ''));
        }


        // Traversing the next level, if there is no match, the last label will be empty
        _labels = _labels.filter((v: string) => v != '');


        return _labels.length > 0 ? _labels.map((item: any, i: number, arr: any[]) => {
            if (arr.length - 1 === i) {
                return (
                    <div key={i}>
                        <span dangerouslySetInnerHTML={{
                            __html: item
                        }}></span>
                    </div>
                )
            } else {
                return (
                    <div key={i}>
                        <span dangerouslySetInnerHTML={{
                            __html: item
                        }}></span>
                        {arrowGenerator()}
                    </div>
                )
            }
        }) : '';

    }
    function arrowGenerator() {
        return displayResultArrow ? displayResultArrow : <svg viewBox="0 0 22 22" width="8px"><path d="m345.44 248.29l-194.29 194.28c-12.359 12.365-32.397 12.365-44.75 0-12.354-12.354-12.354-32.391 0-44.744l171.91-171.91-171.91-171.9c-12.354-12.359-12.354-32.394 0-44.748 12.354-12.359 32.391-12.359 44.75 0l194.29 194.28c6.177 6.18 9.262 14.271 9.262 22.366 0 8.099-3.091 16.196-9.267 22.373" transform="matrix(.03541-.00013.00013.03541 2.98 3.02)" fill="#a5a5a5" /></svg>;
    }


    useEffect(() => {


        // Initialize default value (request parameters for each level)
        //--------------
        initDefaultValue(value);

    }, [value]);

    // Automatically complete and truncate column Search Keywords each time the number of columns changes
    useEffect(() => {
        if (listData.current.length !== columnSearchKeywords.length) {
            setColumnSearchKeywords(
                Array(listData.current.length).fill('').map((v, i) => columnSearchKeywords[i] || '')
            );
        }
    }, [listData.current.length]);


    return (
        <>

            <div
                className={clsWrite(wrapperClassName, 'casc-menu__wrapper mb-3 position-relative', `casc-menu__wrapper ${wrapperClassName}`)}
                ref={rootRef}
                data-overlay-id={`casc-menu__items-wrapper-${idRes}`}
            >
                {label ? <>{typeof label === 'string' ? <label htmlFor={idRes} className="form-label" dangerouslySetInnerHTML={{ __html: `${label}` }}></label> : <label htmlFor={idRes} className="form-label" >{label}</label>}</> : null}

                {triggerContent ? <>
                    <div className={clsWrite(wrapperClassName, 'casc-menu__trigger d-inline w-auto', `casc-menu__trigger ${triggerClassName}`)} onClick={handleDisplayOptions}>{triggerContent}</div>
                </> : null}


                {!hasErr ? (
                    <RootPortal show={true} containerClassName="CascadingSelect">

                        <div
                            ref={listRef}
                            id={`casc-menu__items-wrapper-${idRes}`}
                            className="casc-menu__items-wrapper position-absolute border shadow small"
                            style={{ zIndex: DEPTH, display: 'none' }}
                        >
                            <ul className="casc-menu__items-inner">
                                {loading ? <><div className="casc-menu__items-loader">{loader || <svg height="12px" width="12px" viewBox="0 0 512 512"><g><path fill="inherit" d="M256,0c-23.357,0-42.297,18.932-42.297,42.288c0,23.358,18.94,42.288,42.297,42.288c23.357,0,42.279-18.93,42.279-42.288C298.279,18.932,279.357,0,256,0z" /><path fill="inherit" d="M256,427.424c-23.357,0-42.297,18.931-42.297,42.288C213.703,493.07,232.643,512,256,512c23.357,0,42.279-18.93,42.279-42.288C298.279,446.355,279.357,427.424,256,427.424z" /><path fill="inherit" d="M74.974,74.983c-16.52,16.511-16.52,43.286,0,59.806c16.52,16.52,43.287,16.52,59.806,0c16.52-16.511,16.52-43.286,0-59.806C118.261,58.463,91.494,58.463,74.974,74.983z" /><path fill="inherit" d="M377.203,377.211c-16.503,16.52-16.503,43.296,0,59.815c16.519,16.52,43.304,16.52,59.806,0c16.52-16.51,16.52-43.295,0-59.815C420.489,360.692,393.722,360.7,377.203,377.211z" /><path fill="inherit" d="M84.567,256c0.018-23.348-18.922-42.279-42.279-42.279c-23.357-0.009-42.297,18.932-42.279,42.288c-0.018,23.348,18.904,42.279,42.279,42.279C65.645,298.288,84.567,279.358,84.567,256z" /><path fill="inherit" d="M469.712,213.712c-23.357,0-42.279,18.941-42.297,42.288c0,23.358,18.94,42.288,42.297,42.297c23.357,0,42.297-18.94,42.279-42.297C512.009,232.652,493.069,213.712,469.712,213.712z" /><path fill="inherit" d="M74.991,377.22c-16.519,16.511-16.519,43.296,0,59.806c16.503,16.52,43.27,16.52,59.789,0c16.52-16.519,16.52-43.295,0-59.815C118.278,360.692,91.511,360.692,74.991,377.22z" /><path fill="inherit" d="M437.026,134.798c16.52-16.52,16.52-43.304,0-59.824c-16.519-16.511-43.304-16.52-59.823,0c-16.52,16.52-16.503,43.295,0,59.815C393.722,151.309,420.507,151.309,437.026,134.798z" /></g></svg>}</div></> : null}
                                {showCloseBtn ? <a href="#" tabIndex={-1} onClick={(e) => {
                                    e.preventDefault();
                                    cancel();
                                }} className="casc-menu__close position-absolute top-0 end-0 mt-0 mx-1"><svg width="10px" height="10px" viewBox="0 0 1024 1024"><path fill="#000" d="M195.2 195.2a64 64 0 0 1 90.496 0L512 421.504 738.304 195.2a64 64 0 0 1 90.496 90.496L602.496 512 828.8 738.304a64 64 0 0 1-90.496 90.496L512 602.496 285.696 828.8a64 64 0 0 1-90.496-90.496L421.504 512 195.2 285.696a64 64 0 0 1 0-90.496z" /></svg></a> : null}



                                {listData.current.map((item: any, level: number) => {

                                    if (item.length > 0) {

                                        // filter data
                                        let filteredItem = item;
                                        if (searchable && columnSearchKeywords[level]) {
                                            const keyword = columnSearchKeywords[level].toLowerCase();
                                            filteredItem = item.filter((opt: any) =>
                                                (htmlToPlain(opt.name) || '').toLowerCase().includes(keyword)
                                            );
                                        }


                                        return (
                                            <li key={level} data-col={level} className="casc-menu__items-col">
               
                                                {/* SEARCH BOX */}
                                                {searchable && (
                                                    <div className="casc-menu__items-col-searchbox">
                                                        <input
                                                            type="text"
                                                            placeholder={searchPlaceholder}
                                                            value={columnSearchKeywords[level] || ''}
                                                            onChange={e => {
                                                                const newKeywords = [...columnSearchKeywords];
                                                                newKeywords[level] = e.target.value;
                                                                setColumnSearchKeywords(newKeywords);
                                                            }}
                                                        />
                                                    </div>
                                                )}
                                                {/* /SEARCH BOX */}

                                                <Group
                                                    perColumnHeadersShow={perColumnHeadersShow}
                                                    level={level}
                                                    columnTitle={columnTitleData}
                                                    data={filteredItem}  // filter result
                                                    cleanNodeBtnClassName={cleanNodeBtnClassName}
                                                    cleanNodeBtnContent={cleanNodeBtnContent}
                                                    selectEv={(e, value, index, ) => handleClickItem(e, value, index, level, listData.current)}
                                                />
                                            </li>
                                        )
                                    } else {
                                        return null;
                                    }

                                })}
                            </ul>

                        </div>
                    </RootPortal>

                ) : null}


                <div className={combinedCls(
                        'casc-menu__val',
                        {
                            'inputable': inputable,
                        }

                    )}
                    onClick={handleDisplayOptions}
                >


                    {/* INPIT */}
                    <div className={combinedCls(
                        'position-relative',
                        clsWrite(controlGroupWrapperClassName, 'input-group'),
                        {
                            'has-left-content': propExist(iconLeft),
                            'has-right-content': propExist(iconRight) || propExist(units)
                        }

                    )}>

                        {propExist(iconLeft) ? <><span className={clsWrite(controlGroupTextClassName, 'input-group-text')}>{iconLeft}</span></> : null}

                        <div className="input-group-control-container flex-fill position-relative">
                            <input
                                ref={(node) => {
                                    inputRef.current = node;
                                    if (typeof externalRef === 'function') {
                                        externalRef(node);
                                    } else if (externalRef) {
                                        externalRef.current = node;
                                    }
                                }}
                                id={idRes}
                                data-overlay-id={`casc-menu__items-wrapper-${idRes}`}
                                name={name}
                                className={combinedCls(
                                    clsWrite(controlClassName, 'form-control'),
                                    controlExClassName,
                                    {
                                        'rounded': !propExist(iconLeft) && !propExist(iconRight) && !propExist(units),
                                        'rounded-start-0': propExist(iconLeft),
                                        'rounded-end-0': propExist(iconRight) || propExist(units)
                                    }
                                )}

                                placeholder={placeholder}
                                value={(() => {
                                    const curValForamt: string = resultInputPureText(changedVal);
                                    let curValCallback: string = curValForamt;

                                    // STEP 1
                                    //============
                                    if (typeof formatInputResult === 'function') {
                               
                                        return formatInputResult(
                                            VALUE_BY_BRACES
                                                ? extractContentsOfMixedCharactersWithBraces(curValCallback)
                                                : extractContentsOfMixedCharactersWithComma(curValCallback)
                                        );
                                        
                                    } else {
                                        return changedVal;
                                    }


                                })()
                                }
                                // placeholder will not change if defaultValue is used
                                onFocus={handleFocus}
                                onBlur={handleBlur}
                                autoComplete="off"
                                disabled={disabled || null}
                                readOnly={readOnly || null}
                                required={required || null}
                                minLength={minLength || null}
                                maxLength={maxLength || null}
                                style={style}
                                tabIndex={tabIndex || 0}
                                onChange={inputable ? (e) => {
                                    setChangedVal(e.target.value);
                                    if (typeof onChange === 'function') {
                                        onChange(
                                            e, // input dom event
                                            null, // currentData
                                            null, // index
                                            null, // depth
                                            e.target.value, // value
                                            cancel
                                        );
                                    }
                                } : undefined}
                                {...attributes}

                            />



                            {/** REVIEW RESULT */}
                            {!inputable ? <div className="casc-menu__result">{displayInfo()}</div> : null}


                            {/* Required marking */}
                            {required ? <>{requiredLabel || requiredLabel === '' ? requiredLabel : <span className="position-absolute end-0 top-0 my-2 mx-2 pe-3"><span className="text-danger">*</span></span>}</> : ''}

                        </div>


                        {propExist(units) ? <><span className={clsWrite(controlGroupTextClassName, 'input-group-text')}>{units}</span></> : null}
                        {propExist(iconRight) ? <><span className={clsWrite(controlGroupTextClassName, 'input-group-text')}>{iconRight}</span></> : null}

                    </div>
                    {/* /INPIT */}
                 

                    {isShow ? <div
                        className="casc-menu__closemask"
                        onClick={(e) => {
                            e.preventDefault();
                            cancel();
                        }}></div> : null}


                    <span className="arrow" style={{ pointerEvents: 'none' }}>
                        {controlArrow ? controlArrow : <svg width="10px" height="10px" viewBox="0 -4.5 20 20">
                            <g stroke="none" strokeWidth="1" fill="none">
                                <g transform="translate(-180.000000, -6684.000000)" className="arrow-fill-g" fill="#a5a5a5">
                                    <g transform="translate(56.000000, 160.000000)">
                                        <path d="M144,6525.39 L142.594,6524 L133.987,6532.261 L133.069,6531.38 L133.074,6531.385 L125.427,6524.045 L124,6525.414 C126.113,6527.443 132.014,6533.107 133.987,6535 C135.453,6533.594 134.024,6534.965 144,6525.39">
                                        </path>
                                    </g>
                                </g>
                            </g>
                        </svg>}
                    </span>

                </div>





            </div>


        </>
    )
});

export default CascadingSelect;
