import { assign, find, findIndex, get, indexOf, isEmpty, pullAt, remove, set, size } from 'lodash'
import { defineStore } from 'pinia'
import { TabItem } from '@/objects/tabItem.js'

const useTabStore = defineStore('tab', {
    /**
     * @typedef {Object} ListEntryItem
     * @property {string|number[]} v value
     * @property {string} [dv] display value
     */

    /**
     * @typedef {Object} ListReplaceItem
     * @property {number} index
     * @property {string|number[]} v value
     * @property {string} [dv] display value
     */

    /**
     * @typedef {Object} HashEntryItem
     * @property {string} k field name
     * @property {string|number[]} v value
     * @property {string} [dv] display value
     */

    /**
     * @typedef {Object} HashReplaceItem
     * @property {string|number[]} k field name
     * @property {string|number[]} nk new field name
     * @property {string|number[]} v value
     * @property {string} [dv] display value
     */

    /**
     * @typedef {Object} SetEntryItem
     * @property {string|number[]} v value
     * @property {string} [dv] display value
     */

    /**
     * @typedef {Object} ZSetEntryItem
     * @property {number} s score
     * @property {string|number[]} v value
     * @property {string} [dv] display value
     */

    /**
     * @typedef {Object} ZSetReplaceItem
     * @property {number} s score
     * @property {string|number[]} v value
     * @property {string|number[]} nv new value
     * @property {string} [dv] display value
     */

    /**
     * @typedef {Object} StreamEntryItem
     * @property {string} id
     * @property {Object.<string, *>} v value
     * @property {string} [dv] display value
     */

    /**
     * @typedef {Object} TabState
     * @property {string} nav
     * @property {number} asideWidth
     * @property {TabItem[]} tabList
     * @property {number} activatedIndex
     */

    /**
     *
     * @returns {TabState}
     */
    state: () => ({
        nav: 'server',
        asideWidth: 300,
        tabList: [],
        activatedIndex: 0, // current activated tab index
    }),
    getters: {
        /**
         * get current tab list item
         * @returns {TabItem[]}
         */
        tabs() {
            // if (isEmpty(this.tabList)) {
            //     this.newBlankTab()
            // }
            return this.tabList
        },

        /**
         * get current activated tab item
         * @returns {TabItem|null}
         */
        currentTab() {
            return get(this.tabs, this.activatedIndex)
        },

        currentTabName() {
            return get(this.tabs, [this.activatedIndex, 'name'])
        },

        currentCheckedKeys() {
            const tab = this.currentTab
            return get(tab, 'checkedKeys', [])
        },
    },
    actions: {
        /**
         *
         * @param idx
         * @param {boolean} [switchNav]
         * @param {string} [subTab]
         * @private
         */
        _setActivatedIndex(idx, switchNav, subTab) {
            this.activatedIndex = idx
            if (switchNav === true) {
                this.nav = idx >= 0 ? 'browser' : 'server'
                if (!isEmpty(subTab)) {
                    set(this.tabList, [idx, 'subTab'], subTab)
                }
            } else {
                if (idx < 0) {
                    this.nav = 'server'
                }
            }
        },

        openBlank(server) {
            this.upsertTab({ server, clearValue: true })
        },

        /**
         * update or insert a new tab if not exists with the same name
         * @param {string} subTab
         * @param {string} server
         * @param {number} [db]
         * @param {number} [type]
         * @param {number} [ttl]
         * @param {string} [key]
         * @param {string} [keyCode]
         * @param {number} [size]
         * @param {number} [length]
         * @param {string} [matchPattern]
         * @param {boolean} [clearValue]
         * @param {string} format
         * @param {string} decode
         * @param {*} [value]
         */
        upsertTab({
            subTab,
            server,
            db,
            type,
            ttl,
            key,
            keyCode,
            size,
            length,
            matchPattern = '',
            clearValue,
            format = '',
            decode = '',
        }) {
            let tabIndex = findIndex(this.tabList, { name: server })
            if (tabIndex === -1) {
                const tabItem = new TabItem({
                    name: server,
                    title: server,
                    subTab,
                    server,
                    db,
                    type,
                    ttl,
                    key,
                    keyCode,
                    size,
                    length,
                    matchPattern,
                    value: undefined,
                    format,
                    decode,
                })
                this.tabList.push(tabItem)
                tabIndex = this.tabList.length - 1
            } else {
                const tab = this.tabList[tabIndex]
                tab.blank = false
                tab.subTab = subTab
                // tab.title = db !== undefined ? `${server}/db${db}` : `${server}`
                tab.title = server
                tab.server = server
                tab.db = db == null ? tab.db : db
                tab.type = type
                tab.ttl = ttl
                tab.key = key
                tab.keyCode = keyCode
                tab.size = size
                tab.length = length
                tab.matchPattern = matchPattern
                tab.format = format
                tab.decode = decode
                if (clearValue === true) {
                    tab.value = undefined
                }
            }
            this._setActivatedIndex(tabIndex, true, subTab)
        },

        /**
         * keep update value in tab
         * @param {string} server
         * @param {number} db
         * @param {string} key
         * @param {*} [value]
         * @param {string} [format]
         * @param {string] [decode]
         * @param {string} [matchPattern]
         * @param {boolean} [reset]
         * @param {boolean} [end] keep end status if not set
         * @param {number} [size]
         * @param {number} [length]
         */
        updateValue({ server, db, key, value, format, decode, matchPattern, reset, end, size = -1, length = -1 }) {
            const tabData = find(this.tabList, { name: server, db, key })
            if (tabData == null) {
                return
            }

            tabData.format = format || tabData.format
            tabData.decode = decode || tabData.decode
            tabData.matchPattern = matchPattern || ''
            if (size >= 0) {
                tabData.size = size
            }
            if (length >= 0) {
                tabData.length = length
            }
            if (typeof end === 'boolean') {
                tabData.end = end
            }
            if (!!!reset && typeof value === 'object') {
                if (value instanceof Array) {
                    tabData.value = tabData.value || []
                    // direct deconstruction leads to 'Maximum call stack size exceeded'？
                    // tabData.value.push(...value)
                    for (let i = 0; i < value.length; i++) {
                        tabData.value.push(value[i])
                    }
                } else {
                    tabData.value = assign(value, tabData.value || {})
                }
            } else {
                tabData.value = value
            }
        },

        /**
         * insert entries
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} type
         * @param {ListEntryItem[]|HashEntryItem[]|SetEntryItem[]|ZSetEntryItem[]|StreamEntryItem[]} entries
         * @param {boolean} [prepend] for list only
         */
        insertValueEntries({ server, db, key, type, entries, prepend }) {
            const tab = find(this.tabList, { name: server, db, key })
            if (tab == null) {
                return
            }

            switch (type.toLowerCase()) {
                case 'list': // {v:string, dv:[string]}[]
                    tab.value = tab.value || []
                    if (prepend === true) {
                        tab.value = [...entries, ...tab.value]
                    } else {
                        tab.value.push(...entries)
                    }
                    tab.length += size(entries)
                    break

                case 'hash': // {k:string, v:string, dv:[string]}[]
                case 'set': // {v: string, s: number}[]
                case 'zset': // {v: string, s: number}[]
                    tab.value = tab.value || []
                    tab.value.push(...entries)
                    tab.length += size(entries)
                    break

                case 'stream': // {id: string, v: {}}[]
                    tab.value = tab.value || []
                    tab.value = [...entries, ...tab.value]
                    tab.length += size(entries)
                    break
            }
        },

        /**
         * update entries' value
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} type
         * @param {ListEntryItem[]|HashEntryItem[]|SetEntryItem[]|ZSetEntryItem[]|StreamEntryItem[]} entries
         */
        updateValueEntries({ server, db, key, type, entries }) {
            const tab = find(this.tabList, { name: server, db, key })
            if (tab == null) {
                return
            }

            switch (type.toLowerCase()) {
                case 'hash': // {k:string, v:string, dv:string}[]
                    tab.value = tab.value || []
                    for (const entry of entries) {
                        let updated = false
                        for (const val of tab.value) {
                            if (val.k === entry.k) {
                                val.v = entry.v
                                val.dv = entry.dv
                                updated = true
                                break
                            }
                        }
                        if (!updated) {
                            // no match element, append
                            tab.value.push(entry)
                            tab.length += 1
                        }
                    }
                    break

                case 'zset': // {s:number, v:string, dv:string}[]
                    tab.value = tab.value || []
                    for (const entry of entries) {
                        let updated = false
                        for (const val of tab.value) {
                            if (val.v === entry.v) {
                                val.s = entry.s
                                val.dv = entry.dv
                                updated = true
                                break
                            }
                        }
                        if (!updated) {
                            // no match element, append
                            tab.value.push(entry)
                            tab.length += 1
                        }
                    }
                    break
            }
        },

        /**
         * replace entry item key or field in value(modify the index key)
         * @param {string} server
         * @param {number} db
         * @param {string|number[]} key
         * @param {string} type
         * @param {ListReplaceItem[]|HashReplaceItem[]|ZSetReplaceItem[]} entries
         * @param {number[]} [index] indexes for replacement, can improve search efficiency if configured
         */
        replaceValueEntries({ server, db, key, type, entries, index }) {
            const tab = find(this.tabList, { name: server, db, key })
            if (tab == null) {
                return
            }

            switch (type.toLowerCase()) {
                case 'list': // ListReplaceItem[]
                    tab.value = tab.value || []
                    for (const entry of entries) {
                        if (size(tab.value) > entry.index) {
                            tab.value[entry.index] = {
                                v: entry.v,
                                dv: entry.dv,
                            }
                        } else {
                            // out of range, append
                            tab.value.push(entry)
                            tab.length += 1
                        }
                    }
                    break

                case 'hash': // HashReplaceItem[]
                    tab.value = tab.value || []
                    for (const idx of index) {
                        const entry = get(tab.value, idx)
                        if (entry != null) {
                            /** @type HashReplaceItem[] **/
                            const replaceEntry = remove(entries, (e) => e.k === entry.k)
                            if (!isEmpty(replaceEntry)) {
                                entry.k = replaceEntry[0].nk
                                entry.v = replaceEntry[0].v
                                entry.dv = replaceEntry[0].dv
                            }
                        }
                    }

                    // the left entries do not included in index list, try to retrieve the whole list
                    for (const entry of entries) {
                        let updated = false
                        for (const val of tab.value) {
                            if (val.k === entry.k) {
                                val.k = entry.nk
                                val.v = entry.v
                                val.dv = entry.dv
                                updated = true
                                break
                            }
                        }
                        if (!updated) {
                            // no match element, append
                            tab.value.push({
                                k: entry.nk,
                                v: entry.v,
                                dv: entry.dv,
                            })
                            tab.length += 1
                        }
                    }
                    break

                case 'zset': // ZSetReplaceItem[]
                    tab.value = tab.value || []
                    for (const idx of index) {
                        const entry = get(tab.value, idx)
                        if (entry != null) {
                            /** @type ZSetReplaceItem[] **/
                            const replaceEntry = remove(entries, ({ v }) => v === entry.k)
                            if (!isEmpty(replaceEntry)) {
                                entry.s = replaceEntry[0].s
                                entry.v = replaceEntry[0].nv
                                entry.dv = replaceEntry[0].dv
                            }
                        }
                    }

                    // the left entries do not included in index list, try to retrieve the whole list
                    for (const entry of entries) {
                        let updated = false
                        for (const val of tab.value) {
                            if (val.v === entry.v) {
                                val.s = entry.s
                                val.v = entry.nv
                                val.dv = entry.dv
                                updated = true
                                break
                            }
                        }
                        if (!updated) {
                            // no match element, append
                            tab.value.push({
                                s: entry.s,
                                v: entry.nv,
                                dv: entry.dv,
                            })
                            tab.length += 1
                        }
                    }
                    break
            }
        },

        /**
         * remove value entries
         * @param {string} server
         * @param {number} db
         * @param {string} key
         * @param {string} type
         * @param {string[] | number[]} entries
         */
        removeValueEntries({ server, db, key, type, entries }) {
            const tab = find(this.tabList, { name: server, db, key })
            if (tab == null) {
                return
            }

            switch (type.toLowerCase()) {
                case 'list': // string[] | number[]
                    tab.value = tab.value || []
                    if (typeof entries[0] === 'number') {
                        // remove by index, sort by desc first
                        entries.sort((a, b) => b - a)
                        const removed = pullAt(tab.value, ...entries)
                        tab.length -= size(removed)
                    } else {
                        // append or prepend items
                        for (const elem of entries) {
                            if (!isEmpty(remove(tab.value, elem))) {
                                tab.length -= 1
                            }
                        }
                    }
                    break

                case 'hash': // string[]
                    tab.value = tab.value || {}
                    for (const k of entries) {
                        for (let i = 0; i < tab.value.length; i++) {
                            if (tab.value[i].k === k) {
                                tab.value.splice(i, 1)
                                tab.length -= 1
                                break
                            }
                        }
                    }
                    break

                case 'set': // string[]
                case 'zset': // string[]
                    tab.value = tab.value || []
                    for (const v of entries) {
                        for (let i = 0; i < tab.value.length; i++) {
                            if (tab.value[i].v === v) {
                                tab.value.splice(i, 1)
                                tab.length -= 1
                                break
                            }
                        }
                    }
                    break

                case 'stream': // string[]
                    tab.value = tab.value || []
                    for (const id of entries) {
                        for (let i = 0; i < tab.value.length; i++) {
                            if (tab.value[i].id === id) {
                                tab.value.splice(i, 1)
                                tab.length -= 1
                                break
                            }
                        }
                    }
                    break
            }
        },

        /**
         * update loading status of content in tab
         * @param {string} server
         * @param {number} db
         * @param {boolean} loading
         */
        updateLoading({ server, db, loading }) {
            const tab = find(this.tabList, { name: server, db })
            if (tab == null) {
                return
            }

            tab.loading = loading
        },

        /**
         * update ttl in tab
         * @param {string} server
         * @param {number} db
         * @param {string} key
         * @param {number} ttl
         */
        updateTTL({ server, db, key, ttl }) {
            let tab = find(this.tabList, { name: server, db, key })
            if (tab == null) {
                return
            }
            tab.ttl = ttl
        },

        /**
         * set tab's content to empty
         * @param {string} name
         */
        emptyTab(name) {
            const tab = find(this.tabList, { name })
            if (tab != null) {
                tab.key = null
                tab.value = null
            }
        },
        switchTab(tabIndex) {
            // const len = size(this.tabList)
            // if (tabIndex < 0 || tabIndex >= len) {
            //     tabIndex = 0
            // }
            // this.activatedIndex = tabIndex
            // const tabIndex = findIndex(this.tabList, {name})
            // if (tabIndex === -1) {
            //     return
            // }
            // this.activatedIndex = tabIndex
        },

        switchSubTab(name) {
            const tab = this.currentTab
            if (tab == null) {
                return
            }
            tab.subTab = name
        },

        /**
         *
         * @param {number} tabIndex
         * @returns {*|null}
         */
        removeTab(tabIndex) {
            const len = size(this.tabs)
            // ignore remove last blank tab
            if (len === 1 && this.tabs[0].blank) {
                return null
            }

            if (tabIndex < 0 || tabIndex >= len) {
                return null
            }
            const removed = this.tabList.splice(tabIndex, 1)

            // update select index if removed index equal current selected
            this.activatedIndex -= 1
            if (this.activatedIndex < 0) {
                if (this.tabList.length > 0) {
                    this._setActivatedIndex(0, false)
                } else {
                    this._setActivatedIndex(-1, false)
                }
            } else {
                this._setActivatedIndex(this.activatedIndex, false)
            }

            return size(removed) > 0 ? removed[0] : null
        },

        /**
         *
         * @param {string} tabName
         */
        removeTabByName(tabName) {
            const idx = findIndex(this.tabs, { name: tabName })
            if (idx !== -1) {
                this.removeTab(idx)
            }
        },

        /**
         *
         */
        removeAllTab() {
            this.tabList = []
            this._setActivatedIndex(-1, false)
        },

        /**
         * set expanded keys for server
         * @param {string} server
         * @param {string[]} keys
         */
        setExpandedKeys(server, keys = []) {
            /** @type TabItem**/
            let tab = find(this.tabList, { name: server })
            if (tab != null) {
                if (isEmpty(keys)) {
                    tab.expandedKeys = []
                } else {
                    tab.expandedKeys = keys
                }
            }
        },

        /**
         *
         * @param {string} server
         * @param {string} key
         */
        addExpandedKey(server, key) {
            /** @type TabItem**/
            let tab = find(this.tabList, { name: server })
            if (tab != null) {
                tab.expandedKeys.push(key)
            }
        },

        /**
         *
         * @param {string} server
         * @param {string} key
         */
        toggleExpandKey(server, key) {
            /** @type TabItem**/
            let tab = find(this.tabList, { name: server })
            if (tab != null) {
                const idx = indexOf(tab.expandedKeys, key)
                if (idx === -1) {
                    tab.expandedKeys.push(key)
                } else {
                    tab.expandedKeys.splice(idx, 1)
                }
            }
        },

        /**
         *
         * @param {string} server
         * @param {string} key
         */
        removeExpandedKey(server, key) {
            /** @type TabItem**/
            let tab = find(this.tabList, { name: server })
            if (tab != null) {
                remove(tab.expandedKeys, (v) => v === key)
            }
        },

        /**
         * set selected keys for server
         * @param {string} server
         * @param {string|string[]} [keys]
         */
        setSelectedKeys(server, keys = null) {
            /** @type TabItem**/
            let tab = find(this.tabList, { name: server })
            if (tab != null) {
                if (keys == null) {
                    // select nothing
                    tab.selectedKeys = []
                    tab.activatedKey = null
                } else if (typeof keys === 'string') {
                    tab.selectedKeys = [keys]
                } else {
                    tab.selectedKeys = keys
                }
            }
        },

        /**
         * get checked keys
         * @param server
         * @returns {CheckedKey[]}
         */
        getCheckedKeys(server) {
            /** @type TabItem**/
            let tab = find(this.tabList, { name: server })
            if (tab != null) {
                return tab.checkedKeys || []
            }
            return []
        },

        /**
         * set checked keys for server
         * @param {string} server
         * @param {CheckedKey[]} [keys]
         */
        setCheckedKeys(server, keys = null) {
            /** @type TabItem**/
            let tab = find(this.tabList, { name: server })
            if (tab != null) {
                if (isEmpty(keys)) {
                    // select nothing
                    tab.checkedKeys = []
                } else {
                    tab.checkedKeys = keys
                }
            }
        },

        /**
         * set activated key
         * @param {string} server
         * @param {string} key
         */
        setActivatedKey(server, key) {
            /** @type TabItem**/
            let tab = find(this.tabList, { name: server })
            if (tab != null) {
                if (!isEmpty(key) && key !== tab.activatedKey) {
                    tab.activatedKey = key
                    return true
                }
            }
            return false
        },
    },
})

export default useTabStore
