import baseComponent from '../helpers/baseComponent'
import classNames from '../helpers/libs/classNames'
import eventsMixin from '../helpers/mixins/eventsMixin'
import styleToCssString from '../helpers/libs/styleToCssString'
import { getTouchPoints, getPointsNumber } from '../helpers/shared/gestures'
import { useRect } from '../helpers/hooks/useDOM'

/**
 * 获取小数位数
 */
const getPrecision = (step) => {
    const stepString = step.toString()
    return stepString.indexOf('.') >= 0 ? stepString.length - stepString.indexOf('.') - 1 : 0
}

/**
 * 返回精度正确的值
 */
const checkValuePrecision = (val, step, min) => {
    const closestStep = Math.round((val - min) / step) * step + min
    const precision = getPrecision(step)
    return parseFloat(closestStep.toFixed(precision))
}

/**
 * 获取样式
 */
const getStyles = (value) => {
    return Array.isArray(value) ? value.map((n) => styleToCssString(n)) : styleToCssString(value)
}

const defaultEvents = {
    onChange() {},
    onAfterChange() {},
}

baseComponent({
    behaviors: [eventsMixin({ defaultEvents })],
    relations: {
        '../field/index': {
            type: 'ancestor',
        },
    },
    properties: {
        prefixCls: {
            type: String,
            value: 'wux-slider',
        },
        min: {
            type: Number,
            value: 0,
            observer: 'getMarks',
        },
        max: {
            type: Number,
            value: 100,
            observer: 'getMarks',
        },
        step: {
            type: Number,
            value: 1,
            observer: 'getMarks',
        },
        defaultValue: {
            type: Array,
            value: [0],
        },
        value: {
            type: Array,
            value: [0],
            observer(newVal) {
                if (this.data.controlled) {
                    this.updated(newVal)
                }
            },
        },
        controlled: {
            type: Boolean,
            value: false,
        },
        disabled: {
            type: Boolean,
            value: false,
        },
        showMark: {
            type: Boolean,
            value: false,
        },
        showValue: {
            type: [Boolean, Object],
            value: false,
        },
        tipFormatter: {
            type: String,
            value: '{d}',
        },
        markStyle: {
            type: [String, Object, Array],
            value: '',
            observer(newVal) {
                this.setData({
                    extMarkStyle: getStyles(newVal),
                })
            },
        },
        handleStyle: {
            type: [String, Object, Array],
            value: '',
            observer(newVal) {
                this.setData({
                    extHandleStyle: getStyles(newVal),
                })
            },
        },
        trackStyle: {
            type: [String, Object, Array],
            value: '',
            observer(newVal) {
                this.setData({
                    extTrackStyle: getStyles(newVal),
                })
            },
        },
        railStyle: {
            type: [String, Object],
            value: '',
            observer(newVal) {
                this.setData({
                    extRailStyle: styleToCssString(newVal),
                })
            },
        },
        wrapStyle: {
            type: [String, Object],
            value: '',
            observer(newVal) {
                this.setData({
                    extWrapStyle: styleToCssString(newVal),
                })
            },
        },
    },
    data: {
        offsets: [],
        inputValue: [],
        extMarkStyle: '',
        extHandleStyle: '',
        extTrackStyle: '',
        extRailStyle: '',
        extWrapStyle: '',
        isTouched: false,
        swiping: false,
    },
    computed: {
        classes: ['prefixCls, disabled, tipFormatter', function(prefixCls, disabled, tipFormatter) {
            const wrap = classNames(prefixCls, {
                [`${prefixCls}--disabled`]: disabled,
                [`${prefixCls}--has-tip`]: !!tipFormatter,
            })
            const min = `${prefixCls}__min`
            const railWrap = `${prefixCls}__rail-wrap`
            const rail = `${prefixCls}__rail`
            const mark = `${prefixCls}__mark`
            const track = `${prefixCls}__track`
            const handle = `${prefixCls}__handle`
            const max = `${prefixCls}__max`

            return {
                wrap,
                min,
                railWrap,
                rail,
                mark,
                track,
                handle,
                max,
            }
        }],
    },
    observers: {
        inputValue(newVal) {
            const offsets = newVal.map((value) => this.calcOffset(this.checkValue(value)))
            this.setData({ offsets })
        },
    },
    methods: {
        /**
         * 更新选中值及偏移量
         */
        updated(inputValue) {
            if (this.hasFieldDecorator) return
            if (this.data.inputValue !== inputValue) {
                this.setData({ inputValue })
            }
        },
        /**
         * 手指触摸动作开始
         */
        onTouchStart(e) {
            if (this.data.disabled || getPointsNumber(e) > 1) return
            const { index } = e.currentTarget.dataset
            this.isMoved = false
            this.startX = getTouchPoints(e).x
            this.moveX = 0
            // 记录选中值发生改变时的初始偏移量
            this.startPos = this.data.offsets[index] || 0
            // 记录最后一次选中项
            this.setData({ last: index, isTouched: true, isMoved: false })
        },
        /**
         * 手指触摸后移动
         */
        onTouchMove(e) {
            if (this.data.disabled || getPointsNumber(e) > 1) return
            const { index } = e.currentTarget.dataset
            const { prefixCls } = this.data

            this.isMoved = true
            this.setData({ isMoved: true })
            this.moveX = getTouchPoints(e).x

            useRect(`.${prefixCls}__rail`, this).then((rect) => {
                if (!rect || !this.isMoved) return

                const diffX = (this.moveX - this.startX) / rect.width * 100
                const nextOffsets = [...this.data.offsets]
                const offset = this.checkValue(this.startPos + diffX, 0, 100)
                const { inputValue } = this.data
                const currentValue = this.calcValue(offset)
                const prevValue = inputValue[index - 1]
                const nextValue = inputValue[index + 1]

                // 通过合法的当前值反算偏移量
                nextOffsets[index] = this.calcOffset(currentValue)

                // 判断当前值是否小于前一值，是则重新计算偏移量
                if (prevValue && prevValue > currentValue) {
                    nextOffsets[index] = this.calcOffset(prevValue)
                }

                // 判断当前值是否大于后一值，是则重新计算偏移量
                if (nextValue && nextValue < currentValue) {
                    nextOffsets[index] = this.calcOffset(nextValue)
                }

                // 判断当前值是否发生变化，是则触发 change 事件
                if (inputValue[index] !== currentValue) {
                    const value = this.getValue(nextOffsets)

                    if (!this.data.controlled) {
                        this.updated(value)
                    }

                    this.triggerEvent('change', { offsets: nextOffsets, value })
                }
            })
        },
        /**
         * 手指触摸动作结束
         */
        onTouchEnd(e) {
            if (this.data.disabled || getPointsNumber(e) > 1 || !this.isMoved) return
            this.isMoved = false
            this.setData({ isTouched: false, isMoved: false })
            const { offsets } = this.data
            const value = this.getValue(offsets)
            this.triggerEvent('afterChange', { offsets, value })
        },
        /**
         * 计算选中值
         */
        calcValue(ratio) {
            const { min, max } = this.data
            return this.trimValue(ratio * (max - min) / 100 + min)
        },
        /**
         * 计算偏移量
         */
        calcOffset(value) {
            const { min, max } = this.data
            const ratio = (value - min) / (max - min)
            return ratio * 100
        },
        /**
         * 判断元素是否在指定的范围内
         */
        checkValue(val, min = this.data.min, max = this.data.max) {
            if (val <= min) {
                return min
            }

            if (val >= max) {
                return max
            }

            return val
        },
        /**
         * 验证选中值
         */
        trimValue(val) {
            return checkValuePrecision(this.checkValue(val), this.data.step, this.data.min)
        },
        /**
         * 获取选中值
         */
        getValue(offsets = this.data.offsets) {
            return offsets.map((offset) => this.calcValue(offset))
        },
        /**
         * 获取间断点位置
         */
        getMarks() {
            if (!this.data.showMark) return

            const {min, max, step } = this.data
            const count = (max - min) / step
            const marks = []
            const offset = 100 * step / (max - min)

            for (let i = 1; i < count; i++) {
                marks.push(i * offset)
            }

            this.setData({ marks })
        },
        noop() {},
        getValueByPosition(position) {
            const { min, max, step } = this.data
            const newPosition = position < min ? min : position > max ? max : position
            let value = min
            const lengthPerStep = 100 / ((max - min) / step)
            const steps = Math.round(newPosition / lengthPerStep)
            value = steps * lengthPerStep * (max - min) * 0.01 + min
            return value
        },
        onRailClick(e) {
            if (this.data.disabled || getPointsNumber(e) > 1) return
            const { prefixCls, min, max, inputValue: sliderValue } = this.data
            useRect(`.${prefixCls}__rail-wrap`, this).then((rect) => {
                const position = ((getTouchPoints(e).x - rect.left) / Math.ceil(rect.width)) * (max - min) + min
                const targetValue = this.getValueByPosition(position)
                const indexLength = sliderValue.length - 1
                const range = indexLength > 0
                const nextOffsets = [...this.data.offsets]
                let nextSliderValue = [...sliderValue]
                let currentIndex = 0
                if (range) {
                    let prevIndex = 0
                    let nextIndex = null
                    for (let i = indexLength; i >= 0; i--) {
                        if (sliderValue[i] <= targetValue) {
                            prevIndex = i
                            break
                        }
                    }
                    if (prevIndex === indexLength) {
                        nextIndex = prevIndex
                        prevIndex = nextIndex - 1
                    } else {
                        nextIndex = prevIndex + 1
                    }
                    // 移动的滑块采用就近原则
                    if (
                        Math.abs(targetValue - sliderValue[prevIndex]) >
                        Math.abs(targetValue - sliderValue[nextIndex])
                    ) {
                        currentIndex = nextIndex
                        nextSliderValue[nextIndex] = targetValue
                    } else {
                        currentIndex = prevIndex
                        nextSliderValue[prevIndex] = targetValue
                    }
                } else {
                    nextSliderValue = [targetValue]
                }

                nextOffsets[currentIndex] = this.calcOffset(targetValue)

                if (sliderValue[currentIndex] !== targetValue) {
                    if (!this.data.controlled) {
                        this.updated(nextSliderValue)
                    }

                    this.triggerEvent('change', { offsets: nextOffsets, value: nextSliderValue })
                    this.triggerEvent('afterChange', { offsets: nextOffsets, value: nextSliderValue })
                }
            })
        },
    },
    attached() {
        const { defaultValue, value, controlled } = this.data
        const inputValue = controlled ? value : defaultValue

        this.getMarks()
        this.updated(inputValue)
    },
})
