# composeTreeObj.mjs

    import get from 'lodash/get'
    import each from 'lodash/each'
    import set from 'lodash/set'
    import cloneDeep from 'lodash/cloneDeep'
    import omit from 'lodash/omit'
    import isestr from './isestr.mjs'
    import isearr from './isearr.mjs'
    import isbol from './isbol.mjs'
    import iser from './iser.mjs'

    /**
     * 組合項目物件陣列成為樹狀物件
     *
     * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/composeTreeObj.test.mjs Github}
     * @memberOf wsemi
     * @param {Array} items 輸入項目物件陣列
     * @param {Object} [opt={}] 輸入設定物件，預設{}
     * @param {String} [bindKey='id'] 輸入項目物件識別用欄位字串，預設'id'
     * @param {String} [bindParent='parentId'] 輸入項目物件內存放父節點欄位字串，預設'parentId'
     * @param {String} [bindChildren='children'] 輸入產生樹狀物件時，各節點內存放子節點欄位字串，預設'children'
     * @param {Boolean} [saveExtProps=false] 輸入是否儲存項目物件從屬資訊布林值，預設false
     * @returns {Array} 回傳物件陣列
     * @example
     *
     * let data = [
     *     {
     *         id: 1,
     *         text: '1-a',
     *     },
     *     {
     *         id: 2,
     *         text: '2-b',
     *     },
     *     {
     *         id: 3,
     *         text: '3-c',
     *         parentId: 2,
     *     },
     *     {
     *         id: 4,
     *         text: '4-d',
     *         parentId: 2,
     *     },
     *     {
     *         id: 5,
     *         text: '5-e',
     *         parentId: 3,
     *     },
     *     {
     *         id: 6,
     *         text: 'empty',
     *     },
     * ]
     * let r = composeTreeObj(data)
     * let cr = JSON.stringify(r)
     * console.log(cr)
     * // => [
     * //   {
     * //     "id": 1,
     * //     "text": "1-a"
     * //   },
     * //   {
     * //     "id": 2,
     * //     "text": "2-b",
     * //     "children": [
     * //       {
     * //         "id": 3,
     * //         "text": "3-c",
     * //         "parentId": 2,
     * //         "children": [
     * //           {
     * //             "id": 5,
     * //             "text": "5-e",
     * //             "parentId": 3
     * //           }
     * //         ]
     * //       },
     * //       {
     * //         "id": 4,
     * //         "text": "4-d",
     * //         "parentId": 2
     * //       }
     * //     ]
     * //   },
     * //   {
     * //     "id": 6,
     * //     "text": "empty"
     * //   }
     * // ]
     *
     */
    function composeTreeObj(items, opt = {}) {
        let privateLevel = '$level'
        let privateParents = '$parents'

        //check
        if (!isearr(items)) {
            return []
        }

        //bindKey
        let bindKey = get(opt, 'bindKey', null)
        if (!isestr(bindKey)) {
            bindKey = 'id'
        }

        //bindParent
        let bindParent = get(opt, 'bindParent', null)
        if (!isestr(bindParent)) {
            bindParent = 'parentId'
        }

        //bindChildren
        let bindChildren = get(opt, 'bindChildren', null)
        if (!isestr(bindChildren)) {
            bindChildren = 'children'
        }

        //saveExtProps
        let saveExtProps = get(opt, 'saveExtProps', null)
        if (!isbol(saveExtProps)) {
            saveExtProps = false
        }

        function addNodeLevel(items) {
            let r = []

            //cloneDeep
            items = cloneDeep(items)

            function addNodeForTop() {
                let _items = []

                each(items, (v, k) => {
                    if (iser(v[bindParent])) {
                        //若不存在bindParent就代表頂層, 加入至r
                        r.push({
                            [privateLevel]: 0,
                            [privateParents]: [],
                            ...v,
                        })
                    }
                    else {
                        //非頂層項目物件, 加入至_items
                        _items.push(v)
                    }
                })
                // console.log('_items a', _items)
                // console.log('r a', r)

                //re-save
                items = _items

            }

            function addNodeInParent(p) {
                let _items = []

                each(items, (v, k) => {
                    let vbp = get(v, bindParent)
                    let pbk = get(p, bindKey)
                    if (vbp && pbk && (vbp === pbk)) {
                        //若非頂層項目物件v的bindParent, 等於所傳入的父層物件p的bindKey, 就代表找到從屬, 加入至r

                        //parentIDs
                        let parentIDs = get(p, privateParents, [])
                        parentIDs = cloneDeep(parentIDs)
                        parentIDs.push(p[bindKey])

                        //push
                        r.push({
                            [privateLevel]: p[privateLevel] + 1, //level加1
                            [privateParents]: parentIDs, //儲存父層節點的bindKey
                            ...v,
                        })

                    }
                    else {
                        //非頂層項目物件v非隸屬於父層物件p, 加入至_items
                        _items.push(v)
                    }
                })
                // console.log('_items b', _items)
                // console.log('r b', r)

                //re-save
                items = _items

            }

            //addNodeForTop
            addNodeForTop()

            //check
            if (r.length === 0) {
                return r
            }

            //建立非頂層節點關聯
            let i = -1
            while (true) {
                i += 1

                //check, 存取超過陣列長度
                if (i > r.length - 1) {
                    break
                }

                //p
                let p = r[i]

                //addNodeInParent
                addNodeInParent(p)

                //check, 若已無非頂層物件則跳出
                if (items.length === 0) {
                    break
                }

            }

            return r
        }

        function buildTree(items) {
            let r = []

            function findParent(v) {
                for (let i = 0; i < items.length; i++) {
                    let p = items[i]
                    if (v[bindParent] === p[bindKey]) {
                        return p
                    }
                }
                return null
            }

            function omitProps(v) {
                if (saveExtProps) {
                    return v
                }
                return omit(v, [privateLevel, privateParents])
            }

            //填入子節點至keyChildren欄位
            let kp = {}
            each(items, (v, k) => { //r為依照層級高低循序建立
                if (v[privateLevel] === 0) {

                    //omitProps
                    let omv = omitProps(v)

                    //push
                    r.push(omv)

                    //save kp
                    kp[v[bindKey]] = [k] //頂層節點之指標k為keys內容
                    // console.log(k, 'r', cloneDeep(r))

                }
                else {
                    // console.log(k, 'v', cloneDeep(v))

                    //findParent
                    let p = findParent(v)
                    // console.log(k, 'p', cloneDeep(p))

                    //check, 找得到父層節點才處理
                    if (p !== null) {

                        //keys
                        let keys = kp[p[bindKey]]
                        keys = [...keys, bindChildren]
                        // console.log(k, 'keys', cloneDeep(keys))

                        let cs = get(r, keys, [])
                        // console.log(k, 'cs1', cloneDeep(cs))

                        //omitProps
                        let omv = omitProps(v)

                        //push
                        cs.push(omv)

                        //save kp
                        kp[v[bindKey]] = [...keys, cs.length - 1]
                        // console.log(k, 'cs2', cloneDeep(cs))

                        //set
                        set(r, keys, cs)
                        // console.log(k, 'r', cloneDeep(r))

                    }
                    else {
                        console.log(`can not find parent for ${get(v, bindParent)}`)
                    }

                }
            })

            return r
        }

        //cloneDeep
        items = cloneDeep(items)

        //addNodeLevel
        items = addNodeLevel(items)

        //buildTree
        let r = buildTree(items)

        return r
    }

    export default composeTreeObj