<template>
    <div class="picker-column" :class="className">
        <ul
            ref="wrapper"
            :style="wrapperStyle"
            class="wrapper"
            @transitionend="onTransitionEnd"
        >
            <li
                v-for="(option, index) in options"
                :key="index"
                :style="itemStyle"
                class="item"
                :class="{
                    disabled: option && option.disabled,
                    selected: index === currentIndex
                }"
                @click="onClickItem(index)"
            >
                <div
                    class="ellipsis"
                    v-if="allowHtml"
                    v-html="getOptionText(option)"
                />
                <div class="ellipsis" v-else>{{ getOptionText(option) }}</div>
            </li>
        </ul>
    </div>
</template>

<script>
import touchMixin from './touch.js';
const DEFAULT_DURATION = 200;

// Inertial sliding ideas:
//When the finger leaves the screen, if the interval from the last move is less than `MOMENTUM_LIMIT_TIME` and the move distance is greater than `MOMENTUM_LIMIT_DISTANCE`, perform inertial sliding
const MOMENTUM_LIMIT_TIME = 300;
const MOMENTUM_LIMIT_DISTANCE = 15;
function isOptionDisabled(option) {
    return typeof option === 'object' && option.disabled;
}
function getElementTranslateY(element) {
    const style = window.getComputedStyle(element);
    const transform = style.transform || style.webkitTransform;
    const translateY = transform.slice(7, transform.length - 1).split(', ')[5];

    return Number(translateY);
}
export default {
    name: 'picker-column',
    mixins: [touchMixin],
    emits: ['change'],
    props: {
        valueKey: String,
        allowHtml: Boolean,
        className: String,
        itemHeight: Number,
        defaultIndex: Number,
        swipeDuration: [Number, String],
        visibleItemCount: [Number, String],
        initialOptions: {
            type: Array,
            default: () => []
        }
    },
    computed: {
        wrapperStyle() {
            return {
                transform: `translate3d(0, ${
                    this.offset + this.baseOffset
                }px, 0)`,
                transitionDuration: `${this.duration}ms`,
                transitionProperty: this.duration ? 'all' : 'none'
            };
        },
        count() {
            return this.options.length;
        },

        baseOffset() {
            return (this.itemHeight * (this.visibleItemCount - 1)) / 2;
        },
        itemStyle() {
            return {
                height: `${this.itemHeight}px`
            };
        }
    },
    data: function() {
        return {
            offset: 0,
            duration: 0,
            options: JSON.parse(JSON.stringify(this.initialOptions)),
            currentIndex: this.defaultIndex
        };
    },
    created() {
        if (this.$parent.children) {
            this.$parent.children.push(this);
        }

        this.setIndex(this.currentIndex);
    },
    mounted() {
        this.bindTouchEvent(this.$el);
    },
    unmounted() {
        const { children } = this.$parent;

        if (children) {
            children.splice(children.indexOf(this), 1);
        }
    },
    watch: {
        initialOptions: 'setOptions',
        defaultIndex(val) {
            this.setIndex(val);
        }
    },
    methods: {
        setOptions(options) {
            if (JSON.stringify(options) !== JSON.stringify(this.options)) {
                this.options = JSON.parse(JSON.stringify(options));
                this.setIndex(this.defaultIndex);
            }
        },
        onTouchStart(event) {
            this.touchStart(event);

            if (this.moving) {
                const translateY = getElementTranslateY(this.$refs.wrapper);
                this.offset = Math.min(0, translateY - this.baseOffset);
                this.startOffset = this.offset;
            } else {
                this.startOffset = this.offset;
            }

            this.duration = 0;
            this.transitionEndTrigger = null;
            this.touchStartTime = Date.now();
            this.momentumOffset = this.startOffset;
        },

        onTouchMove(event) {
            this.touchMove(event);

            if (this.direction === 'vertical') {
                this.moving = true;
                event.preventDefault();
                event.stopPropagation();
            }

            this.offset = this.range(
                this.startOffset + this.deltaY,
                -(this.count * this.itemHeight),
                this.itemHeight
            );

            const now = Date.now();
            if (now - this.touchStartTime > MOMENTUM_LIMIT_TIME) {
                this.touchStartTime = now;
                this.momentumOffset = this.offset;
            }
        },

        onTouchEnd() {
            const distance = this.offset - this.momentumOffset;
            const duration = Date.now() - this.touchStartTime;
            const allowMomentum =
                duration < MOMENTUM_LIMIT_TIME &&
                Math.abs(distance) > MOMENTUM_LIMIT_DISTANCE;

            if (allowMomentum) {
                this.momentum(distance, duration);
                return;
            }

            const index = this.getIndexByOffset(this.offset);
            this.duration = DEFAULT_DURATION;
            this.setIndex(index, true);

            // compatible with desktop scenario
            // use setTimeout to skip the click event triggered after touchstart
            setTimeout(() => {
                this.moving = false;
            }, 0);
        },

        onTransitionEnd() {
            this.stopMomentum();
        },

        onClickItem(index) {
            if (this.moving) {
                return;
            }

            this.transitionEndTrigger = null;
            this.duration = DEFAULT_DURATION;
            this.setIndex(index, true);
        },

        adjustIndex(index) {
            index = this.range(index, 0, this.count);

            for (let i = index; i < this.count; i++) {
                if (!isOptionDisabled(this.options[i])) return i;
            }

            for (let i = index - 1; i >= 0; i--) {
                if (!isOptionDisabled(this.options[i])) return i;
            }
        },

        getOptionText(option) {
            if (
                option &&
                typeof option === 'object' &&
                this.valueKey in option
            ) {
                return option[this.valueKey];
            }
            return option;
        },

        setIndex(index, emitChange) {
            index = this.adjustIndex(index) || 0;

            const offset = -index * this.itemHeight;

            const trigger = () => {
                if (index !== this.currentIndex) {
                    this.currentIndex = index;

                    if (emitChange) {
                        this.$emit('change', index);
                    }
                }
            };

            // trigger the change event after transitionend when moving
            if (this.moving && offset !== this.offset) {
                this.transitionEndTrigger = trigger;
            } else {
                trigger();
            }

            this.offset = offset;
        },

        setValue(value) {
            const { options } = this;
            for (let i = 0; i < options.length; i++) {
                if (this.getOptionText(options[i]) === value) {
                    return this.setIndex(i);
                }
            }
        },

        getValue() {
            return this.options[this.currentIndex];
        },

        getIndexByOffset(offset) {
            return this.range(
                Math.round(-offset / this.itemHeight),
                0,
                this.count - 1
            );
        },

        momentum(distance, duration) {
            const speed = Math.abs(distance / duration);

            distance = this.offset + (speed / 0.003) * (distance < 0 ? -1 : 1);

            const index = this.getIndexByOffset(distance);

            this.duration = +this.swipeDuration;
            this.setIndex(index, true);
        },

        stopMomentum() {
            this.moving = false;
            this.duration = 0;

            if (this.transitionEndTrigger) {
                this.transitionEndTrigger();
                this.transitionEndTrigger = null;
            }
        },

        range(num, min, max) {
            return Math.min(Math.max(num, min), max);
        }
    }
};
</script>

<style lang="scss">
$picker-option-text-color: #000;
$picker-option-disabled-opacity: 0.3;
$picker-option-font-size: 16px;
.picker-column {
    flex: 1;
    overflow: hidden;
    font-size: $picker-option-font-size;
    .ellipsis {
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }

    .wrapper {
        transition-timing-function: cubic-bezier(0.23, 1, 0.68, 1);
        margin: 0;
    }

    .item {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 0 4px;
        color: $picker-option-text-color;

        &.disabled {
            cursor: not-allowed;
            opacity: $picker-option-disabled-opacity;
        }
    }
}
</style>
