<template>
    <div
        :class="classes"
        v-click-outside.capture="handleCloseDrop"
        v-click-outside:mousedown.capture="handleCloseDrop">
        <div ref="reference">
            <SelectHead 
                ref="selectHead"
                v-model="values"
                :size="size"
                :clearable="clearable"
                :disabled="disabled"
                :readonly="readonly"
                :isFocused="isFocused"
                :filterable="filterable"
                :placeholder="placeholder"
                :maxHeight="maxHeight"
                @on-search="handleSearch"
                @on-change="handleChange"
                @click="handleToggleDrop">
                <slot slot="suffix" name="suffix"></slot>
            </SelectHead>
        </div>
        <transition name="transition-drop" v-if="dropable">
            <Dropdown 
                ref="dropdown"
                :transfer="transfer"
                :style="{height: dropHeight}"
                v-show="dropVisible">
                <Scroller ref="scroller">
                    <ul ref="dropContent">
                        <slot name="dropHead"></slot>
                        <Empty v-show="!selectOptions.length && !loading" :showImage="false"/>
                        <FunctionalOptions 
                            v-show="selectOptions.length"
                            :options="selectOptions"
                            :slot-update-hook="updateSlotOptions"
                            :slot-options="slotOptions"></FunctionalOptions>
                        <slot name="dropFooter"></slot>
                    </ul>
                </Scroller>
            </Dropdown>
        </transition>
    </div>
</template>
<script>
    import Dropdown from './dropdown'
    import Scroller from '../scroller/'
    import Empty from '../empty/'
    import FunctionalOptions from './functional-options.vue'
    import SelectHead from './select-head'
    import ClickOutside from '../../../directives/clickoutside'
    import Emitter from '../../../mixins/emitter'
    import Locale from '../../../mixins/locale'
    import { off } from '../../../utils/dom'
    import { oneOf } from '../../../utils/assist'
    import { t } from '../../../locale/'
    const findOptionsInVNode = (node) => {
        const opts = node.componentOptions;
        if (opts && opts.tag.match(optionRegexp)) return [node]
        if (!node.children && (!opts || !opts.children)) return []
        const children = [...(node.children || []), ...(opts && opts.children || [])]
        const options = children.reduce(
            (arr, el) => [...arr, ...findOptionsInVNode(el)], []
        ).filter(Boolean)
        return options.length > 0 ? options : []
    }

    const extractOptions = (options) => options.reduce((options, slotEntry) => {
        return options.concat(findOptionsInVNode(slotEntry))
    }, [])

    // 读取当前dom下的文本
    const getNestedProperty = function (data) {
        let text = '';

        (data || []).forEach((child) => {
            if(child.children && child.children.length) {
                text += getNestedProperty(child.children)
            }
            return text += (child.text || '')
        })
        return text
    }

    const getOptionLabel = option => {
        if (!option) return
        try {
            if (option.componentOptions.propsData.label) return option.componentOptions.propsData.label
            const textContent = getNestedProperty(option.componentOptions.children || [])
            return textContent || ''
        } catch (error) {}
    }
    // 判断value值是否正确
    const getIsCorrectValue = value => {
        return !(typeof value === 'undefined' || (String(value).trim() === '' && !Number.isFinite(value)))
    }
    const prefixCls = 'yy-select'
    const optionRegexp = /^y-y-option$|^YYOption$/i
    const optionGroupRegexp = /option-?group/i
    export default {
        name: 'YYSelect',
        mixins: [ Emitter, Locale ],
        components: {
            Dropdown,
            Empty,
            Scroller,
            FunctionalOptions,
            SelectHead
        },
        directives: {
            ClickOutside
        },
        props: {
            value: {
                type: String | Number | Array,
                default: function () {
                    return []
                }
            },
            customLabel: { 
                type: String | Number
            },
            size: {
                validator(value) {
                    return oneOf(value, ['small', 'large'])
                }
            },
            labelInValue: {
                type: Boolean,
                default: false
            },
            disabled: {
                type: Boolean,
                default: false
            },
            dropable: { // 是否禁止下拉框
                type: Boolean,
                default: true
            },
            readonly: {
                type: Boolean,
                default: false
            },
            clearable: { 
                type: Boolean,
                default: false
            },
            placeholder: {
                type: String,
                default: t('select')
            },
            multiple: {
                type: Boolean,
                default: false
            },
            filterable: {
                type: Boolean,
                default: false
            },
            filterMethod: {
                type: Function
            },
            invertable: {
                type: Boolean,
                default: true
            },
            sortable: {
                type: Boolean,
                default: false
            },
            loading: {
                type: Boolean,
                default: false
            },
            transfer: {
                type: Boolean,
                default: true
            },
            fuzzyQuery: {
                type: Boolean,
                default: false
            },
            maxHeight: {
                type: Number,
            }
        },
        data() {
            return {
                prefixCls: prefixCls,
                slotOptions: this.$slots.default,
                dropVisible: false,
                dropHeight: 'auto',
                query: '',
                isFocused: false,
                values: []
            }
        },
        computed: {
            classes() {
                return [
                    `${prefixCls}`,
                    {
                        [`${prefixCls}-${this.size}`]: !!this.size,
                        [`${prefixCls}-disabled`]: this.disabled,
                        [`${prefixCls}-readonly`]: this.readonly,
                        [`${prefixCls}-focus`]: this.isFocused,
                        [`${prefixCls}-single`]: !this.multiple
                    }
                ]
            },
            selectOptions() {
                const selectOptions = []
                const slotOptions = (this.slotOptions || [])
                const selectedValues = this.values.filter(Boolean).map(({
                    value
                }) => value)

                for (let option of slotOptions) {
                    const cOptions = option.componentOptions
                    if (!cOptions) continue

                    if (cOptions.tag.match(optionGroupRegexp)) { 
                        let children = cOptions.children

                        if(this.query && this.filterable && !this.filterMethod) {
                            children = children.filter(
                                ({componentOptions}) => {
                                    return componentOptions && this.validateOption(componentOptions)
                                }
                            )
                        }

                        children = children.map(opt => {
                            return this.processOption(opt, selectedValues)
                        })
                        
                        if (children.length > 0) selectOptions.push({
                            ...option,
                            componentOptions: {
                                ...cOptions,
                                children: children
                            }
                        })
                    } else {
                        if (this.query && !this.filterMethod) {
                            const optionPassesFilter = this.filterable ? this.validateOption(cOptions) : option
                            if (!optionPassesFilter) continue
                        }
                        selectOptions.push(this.processOption(option, selectedValues))
                    }
                }
                return selectOptions
            },
            flatOptions(){
                return extractOptions(this.selectOptions)
            },
            publicValue() {
                if (this.labelInValue) {
                    return this.values
                } else {
                    return this.multiple ? this.values.map(option => option.value) : (this.values[0] || {}).value
                }
            }
        },
        watch: {
            value: {
                immediate: true,
                handler: function(newValue) {
                    this.$nextTick(()=>{
                        if(getIsCorrectValue(newValue)) {
                            this.values = this.getInitialValue()
                        } else {
                            this.values = []
                        }
                        if(this.dropable) {
                            this.$refs.scroller.updateScroll()
                        }
                    })
                }
            },
            values(now, before) {
                const newValue = JSON.stringify(now)
                const oldValue = JSON.stringify(before)
                const shouldEmitInput = newValue !== oldValue && this.publicValue !== this.value

                if (shouldEmitInput) {
                    this.$emit('input', this.publicValue); // to update v-model
                    this.$emit('on-change', this.publicValue)
                    this.dispatch('YYFormItem', 'on-form-change', this.publicValue)
                }
                if(this.sortable) {
                    this.$refs.selectHead.sortable()
                }
            }
        },
        methods: {
            getOptionData(value) {
                const option = this.flatOptions.find(({
                    componentOptions
                }) => componentOptions.propsData.value === value)
                // 当option不存在， 父级又传值了需要自定义组合
                if (!option && getIsCorrectValue(value) && this.customLabel) return {
                    value: value,
                    label: value + this.customLabel
                }

                const label = getOptionLabel(option)
                let showValue = undefined
                if (option && option.componentOptions && option.componentOptions.propsData) {
                    showValue = option.componentOptions.propsData.showValue
                }
                return {
                    value: value,
                    label: label,
                    showValue: showValue
                }
            },
            getInitialValue() {
                const { multiple, filterMethod, value } = this
                let initialValue = Array.isArray(value) ? value : [value]
                // 当不是多选的时候传的值不正确直接设置为空数组
                if (!multiple && !getIsCorrectValue(initialValue[0])) {
                    initialValue = []
                }

                if (filterMethod && !multiple && getIsCorrectValue(value)) {
                    const data = this.getOptionData(value)
                    this.query = data ? data.label : String(value)
                }
                return initialValue.map(v => {
                    if (typeof v !== 'number' && !v) return null
                    if(getIsCorrectValue(v.value) && v.label) { // 如果父级传值过来不用遍历
                        return v
                    }
                    return this.getOptionData(v)
                }).filter((item) => {
                    return Boolean(item) || item === 0
                })
            },
            setDropHeight() {
                if(this.dropVisible && this.dropable) {
                    this.$nextTick(()=>{
                        const offsetHeight = this.$refs.dropContent && this.$refs.dropContent.offsetHeight
                        this.dropHeight = offsetHeight > 200 ? '200px' : 'auto'
                        this.$refs.scroller.updateScroll()
                    })
                }
            },
            handleClearQuery() {
                this.query = ''
                this.$refs.selectHead.query = ''
            },
            handleToggleDrop() {
                this.broadcast('YYSelectDrop', 'on-update-popper')
                this.dropVisible = !this.dropVisible
                this.isFocused = this.dropVisible
                this.$emit('on-open-change', this.dropVisible)
                
                this.setDropHeight()
            },
            handleCloseDrop() {
                if(this.dropVisible) {
                    this.dropVisible = false
                    this.$emit('on-open-change', this.dropVisible)
                    this.isFocused = this.dropVisible
                    this.dispatch('YYFormItem', 'on-form-change', this.values)
                }
            },
            handleSelect(option) {
                const valueIsSelected = this.values.find(({
                    value
                }) => value === option.value)

                if (this.multiple) {
                    if (valueIsSelected) {
                        this.values = this.values.filter(({
                            value
                        }) => value !== option.value)
                    } else {
                        this.values = this.values.concat(option)
                    }
                } else {
                    if (valueIsSelected) {
                        if(this.invertable) {
                            this.values = []
                        }
                    } else {
                        this.values = [option]
                    }
                    this.handleCloseDrop()
                }

                if (this.filterable) {
                    const inputField = this.$el.querySelector('input[type="text"]')
                    this.$nextTick(() => inputField.focus())
                }

                this.broadcast('YYSelectDrop', 'on-update-popper')
            },
            handleSearch(query) {
                this.query = query
                this.filterMethod && this.filterMethod(this.query)
                this.broadcast('YYSelectDrop', 'on-update-popper')
                this.dropVisible = true
                this.isFocused = true
            },
            handleChange(newIndex, oldIndex) {
                this.values.splice(newIndex, 0, this.values.splice(oldIndex, 1)[0])
                this.$emit('input', this.publicValue); // to update v-model
                this.$emit('on-change', this.publicValue)
            },
            updateSlotOptions() {
                this.slotOptions = this.$slots.default
                this.setDropHeight()
            },
            processOption(option, values) {
                if (!option.componentOptions) return option
                const optionValue = option.componentOptions.propsData.value
                const disabled = option.componentOptions.propsData.disabled
                const isSelected = values.includes(optionValue)

                const propsData = {
                    ...option.componentOptions.propsData,
                    selected: isSelected,
                    multiple: this.multiple,
                    disabled: typeof disabled === 'undefined' ? false : disabled !== false,
                }

                return {
                    ...option,
                    componentOptions: {
                        ...option.componentOptions,
                        propsData: propsData
                    }
                }
            },
            validateOption({ children, elm, propsData }) {
                const value = propsData.value
                const label = propsData.label || ''
                const textContent = (elm && elm.textContent) || (children || []).reduce((str, node) => {
                    const nodeText = node.elm ? node.elm.textContent : node.text
                    return `${str} ${nodeText}`
                }, '') || ''
                const stringValues = JSON.stringify([value, label, textContent])
                const query = this.query.toLowerCase().trim()
                if (this.fuzzyQuery) {
                    const queryReg = new RegExp(query.split('').join('.*'))
                    return queryReg.test(stringValues.toLowerCase())
                }
                return stringValues.toLowerCase().includes(query)
            },
            updateDefaultValue() {
                this.$nextTick(() => {
                    if (!this.filterMethod && this.selectOptions.length > 0) {
                        this.values = this.getInitialValue()
                    }
                })
            }
        },
        mounted() {
            this.$on('on-select-selected', this.handleSelect)
            if (!this.filterMethod && this.selectOptions.length > 0) {
                this.values = this.getInitialValue()
            }
        }
    }
</script>

