<template>
    <div
        :class="wrapperClasses"
        v-click-outside:mousedown.capture="handleClose"
        v-click-outside.capture="handleClose"
    >
        <div ref="reference" :class="[prefixCls + '-rel']">
            <slot>
                <i-input
                    :key="forceInputRerender"
                    :element-id="elementId"
                    :class="[prefixCls + '-editor']"
                    :readonly="!editable || readonly"
                    :disabled="disabled"
                    :size="size"
                    :placeholder="placeholder"
                    :value="visualValue"
                    :name="name"
                    ref="input"

                    @on-input-change="handleInputChange"
                    @on-focus="handleFocus"
                    @on-blur="handleBlur"
                    @on-click="handleIconClick"
                    @click.native="handleFocus"
                    @keydown.native="handleKeydown"
                    @mouseenter.native="handleInputMouseenter"
                    @mouseleave.native="handleInputMouseleave"

                    :icon="iconType"
                ></i-input>
            </slot>
        </div>
        <transition name="transition-drop">
            <Drop
                @click.native="handleTransferClick"
                v-show="opened"
                :class="{ [prefixCls + '-transfer']: transfer }"
                :placement="placement"
                ref="drop"
                :data-transfer="transfer"
                v-transfer-dom>
                <div>
                    <component
                        :is="panel"
                        ref="pickerPanel"
                        :visible="visible"
                        :showTime="type === 'datetime' || type === 'datetimerange'"
                        :confirm="isConfirm"
                        :selectionMode="selectionMode"
                        :steps="steps"
                        :format="format"
                        :value="internalValue"
                        :start-date="startDate"
                        :split-panels="splitPanels"
                        :show-week-numbers="showWeekNumbers"
                        :picker-type="type"
                        :multiple="multiple"
                        :focused-date="focusedDate"

                        :time-picker-options="timePickerOptions"

                        v-bind="ownPickerProps"

                        @on-pick="onPick"
                        @on-pick-clear="handleClear"
                        @on-pick-success="onPickSuccess"
                        @on-pick-click="disableClickOutSide = true"
                        @on-selection-mode-change="onSelectionModeChange"
                    ></component>
                </div>
            </Drop>
        </transition>
    </div>
</template>
<script>


    import iInput from '../../components/input/input.vue';
    import Drop from '../../components/select/dropdown.vue';
    import {directive as clickOutside} from 'v-click-outside-x';
    import TransferDom from '../../directives/transfer-dom';
    import { oneOf } from '../../utils/assist';
    import { DEFAULT_FORMATS, RANGE_SEPARATOR, TYPE_VALUE_RESOLVER_MAP, getDayCountOfMonth } from './util';
    import {findComponentsDownward} from '../../utils/assist';
    import Emitter from '../../mixins/emitter';

    const prefixCls = 'ivu-date-picker';
    const pickerPrefixCls = 'ivu-picker';

    const isEmptyArray = val => val.reduce((isEmpty, str) => isEmpty && !str || (typeof str === 'string' && str.trim() === ''), true);
    const keyValueMapper = {
        40: 'up',
        39: 'right',
        38: 'down',
        37: 'left',
    };

    const mapPossibleValues = (key, horizontal, vertical) => {
        if (key === 'left') return horizontal * -1;
        if (key === 'right') return horizontal * 1;
        if (key === 'up') return vertical * 1;
        if (key === 'down') return vertical * -1;
    };

    const pulseElement = (el) => {
        const pulseClass = 'ivu-date-picker-btn-pulse';
        el.classList.add(pulseClass);
        setTimeout(() => el.classList.remove(pulseClass), 200);
    };

    const extractTime = date => {
        if (!date) return [0, 0, 0];
        return [
            date.getHours(), date.getMinutes(), date.getSeconds()
        ];
    };


    export default {
        mixins: [ Emitter ],
        components: { iInput, Drop },
        directives: { clickOutside, TransferDom },
        props: {
            format: {
                type: String
            },
            readonly: {
                type: Boolean,
                default: false
            },
            disabled: {
                type: Boolean,
                default: false
            },
            editable: {
                type: Boolean,
                default: true
            },
            clearable: {
                type: Boolean,
                default: true
            },
            confirm: {
                type: Boolean,
                default: false
            },
            open: {
                type: Boolean,
                default: null
            },
            multiple: {
                type: Boolean,
                default: false
            },
            timePickerOptions: {
                default: () => ({}),
                type: Object,
            },
            splitPanels: {
                type: Boolean,
                default: false
            },
            showWeekNumbers: {
                type: Boolean,
                default: false
            },
            startDate: {
                type: Date
            },
            size: {
                validator (value) {
                    return oneOf(value, ['small', 'large', 'default']);
                }
            },
            placeholder: {
                type: String,
                default: ''
            },
            placement: {
                validator (value) {
                    return oneOf(value, ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end', 'left', 'left-start', 'left-end', 'right', 'right-start', 'right-end']);
                },
                default: 'bottom-start'
            },
            transfer: {
                type: Boolean,
                default: false
            },
            name: {
                type: String
            },
            elementId: {
                type: String
            },
            steps: {
                type: Array,
                default: () => []
            },
            value: {
                type: [Date, String, Array]
            },
            options: {
                type: Object,
                default: () => ({})
            }
        },
        data(){
            const isRange = this.type.includes('range');
            const emptyArray = isRange ? [null, null] : [null];
            const initialValue = isEmptyArray((isRange ? this.value : [this.value]) || []) ? emptyArray : this.parseDate(this.value);
            const focusedTime = initialValue.map(extractTime);

            return {
                prefixCls: prefixCls,
                showClose: false,
                visible: false,
                internalValue: initialValue,
                disableClickOutSide: false,    // fixed when click a date,trigger clickoutside to close picker
                disableCloseUnderTransfer: false,  // transfer 模式下,点击Drop也会触发关闭,
                selectionMode: this.onSelectionModeChange(this.type),
                forceInputRerender: 1,
                isFocused: false,
                focusedDate: initialValue[0] || this.startDate || new Date(),
                focusedTime: {
                    column: 0, // which column inside the picker
                    picker: 0, // which picker
                    time: focusedTime, // the values array into [hh, mm, ss],
                    active: false
                },
                internalFocus: false,
            };
        },
        computed: {
            wrapperClasses(){
                return [prefixCls, {
                    [prefixCls + '-focused']: this.isFocused
                }];
            },
            publicVModelValue(){
                if (this.multiple){
                    return this.internalValue.slice();
                } else {
                    const isRange = this.type.includes('range');
                    let val = this.internalValue.map(date => date instanceof Date ? new Date(date) : (date || ''));

                    if (this.type.match(/^time/)) val = val.map(this.formatDate);
                    return (isRange || this.multiple) ? val : val[0];
                }
            },
            publicStringValue(){
                const {formatDate, publicVModelValue, type} = this;
                if (type.match(/^time/)) return publicVModelValue;
                if (this.multiple) return formatDate(publicVModelValue);
                return Array.isArray(publicVModelValue) ? publicVModelValue.map(formatDate) : formatDate(publicVModelValue);
            },
            opened () {
                return this.open === null ? this.visible : this.open;
            },
            iconType () {
                let icon = 'ios-calendar-outline';
                if (this.type === 'time' || this.type === 'timerange') icon = 'ios-clock-outline';
                if (this.showClose) icon = 'ios-close';
                return icon;
            },
            transition () {
                const bottomPlaced = this.placement.match(/^bottom/);
                return bottomPlaced ? 'slide-up' : 'slide-down';
            },
            visualValue() {
                return this.formatDate(this.internalValue);
            },
            isConfirm(){
                return this.confirm || this.type === 'datetime' || this.type === 'datetimerange' || this.multiple;
            }
        },
        methods: {
            onSelectionModeChange(type){
                if (type.match(/^date/)) type = 'date';
                this.selectionMode = oneOf(type, ['year', 'month', 'date', 'time']) && type;
                return this.selectionMode;
            },
            // 开启 transfer 时,点击 Drop 即会关闭,这里不让其关闭
            handleTransferClick () {
                if (this.transfer) this.disableCloseUnderTransfer = true;
            },
            handleClose (e) {
                if (this.disableCloseUnderTransfer) {
                    this.disableCloseUnderTransfer = false;
                    return false;
                }

                if (e && e.type === 'mousedown' && this.visible) {
                    e.preventDefault();
                    e.stopPropagation();
                    return;
                }

                if (this.visible) {
                    const pickerPanel = this.$refs.pickerPanel && this.$refs.pickerPanel.$el;
                    if (e && pickerPanel && pickerPanel.contains(e.target)) return; // its a click inside own component, lets ignore it.

                    this.visible = false;
                    e && e.preventDefault();
                    e && e.stopPropagation();
                    return;
                }

                this.isFocused = false;
                this.disableClickOutSide = false;
            },
            handleFocus (e) {
                if (this.readonly) return;
                this.isFocused = true;
                if (e && e.type === 'focus') return; // just focus, don't open yet
                this.visible = true;
            },
            handleBlur (e) {
                if (this.internalFocus){
                    this.internalFocus = false;
                    return;
                }
                if (this.visible) {
                    e.preventDefault();
                    return;
                }

                this.isFocused = false;
                this.onSelectionModeChange(this.type);
                this.internalValue = this.internalValue.slice(); // trigger panel watchers to reset views
                this.reset();
                this.$refs.pickerPanel.onToggleVisibility(false);

            },
            handleKeydown(e){
                const keyCode = e.keyCode;

                // handle "tab" key
                if (keyCode === 9){
                    if (this.visible){
                        e.stopPropagation();
                        e.preventDefault();

                        if (this.isConfirm){
                            const selector = `.${pickerPrefixCls}-confirm > *`;
                            const tabbable = this.$refs.drop.$el.querySelectorAll(selector);
                            this.internalFocus = true;
                            const element = [...tabbable][e.shiftKey ? 'pop' : 'shift']();
                            element.focus();
                        } else {
                            this.handleClose();
                        }
                    } else {
                        this.focused = false;
                    }
                }

                // open the panel
                const arrows = [37, 38, 39, 40];
                if (!this.visible && arrows.includes(keyCode)){
                    this.visible = true;
                    return;
                }

                // close on "esc" key
                if (keyCode === 27){
                    if (this.visible) {
                        e.stopPropagation();
                        this.handleClose();
                    }
                }

                // select date, "Enter" key
                if (keyCode === 13){
                    const timePickers = findComponentsDownward(this, 'TimeSpinner');
                    if (timePickers.length > 0){
                        const columnsPerPicker = timePickers[0].showSeconds ? 3 : 2;
                        const pickerIndex = Math.floor(this.focusedTime.column / columnsPerPicker);
                        const value = this.focusedTime.time[pickerIndex];

                        timePickers[pickerIndex].chooseValue(value);
                        return;
                    }

                    if (this.type.match(/range/)){
                        this.$refs.pickerPanel.handleRangePick(this.focusedDate, 'date');
                    } else {
                        const panels = findComponentsDownward(this, 'PanelTable');
                        const compareDate = (d) => {
                            const sliceIndex = ['year', 'month', 'date'].indexOf((this.type)) + 1;
                            return [d.getFullYear(), d.getMonth(), d.getDate()].slice(0, sliceIndex).join('-');
                        };
                        const dateIsValid = panels.find(({cells}) => {
                            return cells.find(({date, disabled}) => compareDate(date) === compareDate(this.focusedDate) && !disabled);
                        });
                        if (dateIsValid) this.onPick(this.focusedDate, false, 'date');
                    }
                }

                if (!arrows.includes(keyCode)) return; // ignore rest of keys

                // navigate times and dates
                if (this.focusedTime.active) e.preventDefault(); // to prevent cursor from moving
                this.navigateDatePanel(keyValueMapper[keyCode], e.shiftKey);
            },
            reset(){
                this.$refs.pickerPanel.reset && this.$refs.pickerPanel.reset();
            },
            navigateTimePanel(direction){

                this.focusedTime.active = true;
                const horizontal = direction.match(/left|right/);
                const vertical = direction.match(/up|down/);
                const timePickers = findComponentsDownward(this, 'TimeSpinner');

                const maxNrOfColumns = (timePickers[0].showSeconds ? 3 : 2) * timePickers.length;
                const column = (currentColumn => {
                    const incremented = currentColumn + (horizontal ? (direction === 'left' ? -1 : 1) : 0);
                    return (incremented + maxNrOfColumns) % maxNrOfColumns;
                })(this.focusedTime.column);

                const columnsPerPicker = maxNrOfColumns / timePickers.length;
                const pickerIndex = Math.floor(column / columnsPerPicker);
                const col = column % columnsPerPicker;


                if (horizontal){
                    const time = this.internalValue.map(extractTime);

                    this.focusedTime = {
                        ...this.focusedTime,
                        column: column,
                        time: time
                    };
                    timePickers.forEach((instance, i) => {
                        if (i === pickerIndex) instance.updateFocusedTime(col, time[pickerIndex]);
                        else instance.updateFocusedTime(-1, instance.focusedTime);
                    });
                }

                if (vertical){
                    const increment = direction === 'up' ? 1 : -1;
                    const timeParts = ['hours', 'minutes', 'seconds'];


                    const pickerPossibleValues = timePickers[pickerIndex][`${timeParts[col]}List`];
                    const nextIndex = pickerPossibleValues.findIndex(({text}) => this.focusedTime.time[pickerIndex][col] === text) + increment;
                    const nextValue = pickerPossibleValues[nextIndex % pickerPossibleValues.length].text;
                    const times = this.focusedTime.time.map((time, i) => {
                        if (i !== pickerIndex) return time;
                        time[col] = nextValue;
                        return time;
                    });
                    this.focusedTime = {
                        ...this.focusedTime,
                        time: times
                    };

                    timePickers.forEach((instance, i) => {
                        if (i === pickerIndex) instance.updateFocusedTime(col, times[i]);
                        else instance.updateFocusedTime(-1, instance.focusedTime);
                    });
                }
            },
            navigateDatePanel(direction, shift){

                const timePickers = findComponentsDownward(this, 'TimeSpinner');
                if (timePickers.length > 0) {
                    // we are in TimePicker mode
                    this.navigateTimePanel(direction, shift, timePickers);
                    return;
                }

                if (shift){
                    if (this.type === 'year'){
                        this.focusedDate = new Date(
                            this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 10),
                            this.focusedDate.getMonth(),
                            this.focusedDate.getDate()
                        );
                    } else {
                        this.focusedDate = new Date(
                            this.focusedDate.getFullYear() + mapPossibleValues(direction, 0, 1),
                            this.focusedDate.getMonth() + mapPossibleValues(direction, 1, 0),
                            this.focusedDate.getDate()
                        );
                    }

                    const position = direction.match(/left|down/) ? 'prev' : 'next';
                    const double = direction.match(/up|down/) ? '-double' : '';

                    // pulse button
                    const button = this.$refs.drop.$el.querySelector(`.ivu-date-picker-${position}-btn-arrow${double}`);
                    if (button) pulseElement(button);
                    return;
                }

                const initialDate = this.focusedDate || (this.internalValue && this.internalValue[0]) || new Date();
                const focusedDate = new Date(initialDate);

                if (this.type.match(/^date/)){
                    const lastOfMonth = getDayCountOfMonth(initialDate.getFullYear(), initialDate.getMonth());
                    const startDay = initialDate.getDate();
                    const nextDay = focusedDate.getDate() +  mapPossibleValues(direction, 1, 7);

                    if (nextDay < 1) {
                        if (direction.match(/left|right/)) {
                            focusedDate.setMonth(focusedDate.getMonth() + 1);
                            focusedDate.setDate(nextDay);
                        } else {
                            focusedDate.setDate(startDay + Math.floor((lastOfMonth - startDay) / 7) * 7);
                        }
                    } else if (nextDay > lastOfMonth){
                        if (direction.match(/left|right/)) {
                            focusedDate.setMonth(focusedDate.getMonth() - 1);
                            focusedDate.setDate(nextDay);
                        } else {
                            focusedDate.setDate(startDay % 7);
                        }
                    } else {
                        focusedDate.setDate(nextDay);
                    }
                }

                if (this.type.match(/^month/)) {
                    focusedDate.setMonth(focusedDate.getMonth() + mapPossibleValues(direction, 1, 3));
                }

                if (this.type.match(/^year/)) {
                    focusedDate.setFullYear(focusedDate.getFullYear() + mapPossibleValues(direction, 1, 3));
                }

                this.focusedDate = focusedDate;
            },
            handleInputChange (event) {
                const isArrayValue = this.type.includes('range') || this.multiple;
                const oldValue = this.visualValue;
                const newValue = event.target.value;
                const newDate = this.parseDate(newValue);
                const disabledDateFn =
                    this.options &&
                    typeof this.options.disabledDate === 'function' &&
                    this.options.disabledDate;
                const valueToTest = isArrayValue ? newDate : newDate[0];
                const isDisabled = disabledDateFn && disabledDateFn(valueToTest);
                const isValidDate = newDate.reduce((valid, date) => valid && date instanceof Date, true);

                if (newValue !== oldValue && !isDisabled && isValidDate) {
                    this.emitChange(this.type);
                    this.internalValue = newDate;
                } else {
                    this.forceInputRerender++;
                }
            },
            handleInputMouseenter () {
                if (this.readonly || this.disabled) return;
                if (this.visualValue && this.clearable) {
                    this.showClose = true;
                }
            },
            handleInputMouseleave () {
                this.showClose = false;
            },
            handleIconClick () {
                if (this.showClose) {
                    this.handleClear();
                } else if (!this.disabled) {
                    this.handleFocus();
                }
            },
            handleClear () {
                this.visible = false;
                this.internalValue = this.internalValue.map(() => null);
                this.$emit('on-clear');
                this.dispatch('FormItem', 'on-form-change', '');
                this.emitChange(this.type);
                this.reset();

                setTimeout(
                    () => this.onSelectionModeChange(this.type),
                    500 // delay to improve dropdown close visual effect
                );
            },
            emitChange (type) {
                this.$nextTick(() => {
                    this.$emit('on-change', this.publicStringValue, type);
                    this.dispatch('FormItem', 'on-form-change', this.publicStringValue);
                });
            },
            parseDate(val) {
                const isRange = this.type.includes('range');
                const type = this.type;
                const parser = (
                    TYPE_VALUE_RESOLVER_MAP[type] ||
                    TYPE_VALUE_RESOLVER_MAP['default']
                ).parser;
                const format = this.format || DEFAULT_FORMATS[type];
                const multipleParser = TYPE_VALUE_RESOLVER_MAP['multiple'].parser;

                if (val && type === 'time' && !(val instanceof Date)) {
                    val = parser(val, format);
                } else if (this.multiple && val) {
                    val = multipleParser(val, format);
                } else if (isRange) {
                    if (!val){
                        val = [null, null];
                    } else {
                        if (typeof val === 'string') {
                            val = parser(val, format);
                        } else if (type === 'timerange') {
                            val = parser(val, format).map(v => v || '');
                        } else {
                            const [start, end] = val;
                            if (start instanceof Date && end instanceof Date){
                                val = val.map(date => new Date(date));
                            } else if (typeof start === 'string' && typeof end === 'string'){
                                val = parser(val.join(RANGE_SEPARATOR), format);
                            } else if (!start || !end){
                                val = [null, null];
                            }
                        }
                    }
                } else if (typeof val === 'string' && type.indexOf('time') !== 0){
                    val = parser(val, format) || null;
                }

                return (isRange || this.multiple) ? (val || []) : [val];
            },
            formatDate(value){
                const format = DEFAULT_FORMATS[this.type];

                if (this.multiple) {
                    const formatter = TYPE_VALUE_RESOLVER_MAP.multiple.formatter;
                    return formatter(value, this.format || format);
                } else {
                    const {formatter} = (
                        TYPE_VALUE_RESOLVER_MAP[this.type] ||
                        TYPE_VALUE_RESOLVER_MAP['default']
                    );
                    return formatter(value, this.format || format);
                }
            },
            onPick(dates, visible = false, type) {
                if (this.multiple){
                    const pickedTimeStamp = dates.getTime();
                    const indexOfPickedDate = this.internalValue.findIndex(date => date && date.getTime() === pickedTimeStamp);
                    const allDates = [...this.internalValue, dates].filter(Boolean);
                    const timeStamps = allDates.map(date => date.getTime()).filter((ts, i, arr) => arr.indexOf(ts) === i && i !== indexOfPickedDate); // filter away duplicates
                    this.internalValue = timeStamps.map(ts => new Date(ts));
                } else {
                    this.internalValue = Array.isArray(dates) ? dates : [dates];
                }

                if (this.internalValue[0]) this.focusedDate = this.internalValue[0];
                this.focusedTime = {
                    ...this.focusedTime,
                    time: this.internalValue.map(extractTime)
                };

                if (!this.isConfirm) this.onSelectionModeChange(this.type); // reset the selectionMode
                if (!this.isConfirm) this.visible = visible;
                this.emitChange(type);
            },
            onPickSuccess(){
                this.visible = false;
                this.$emit('on-ok');
                this.focus();
                this.reset();
            },
            focus() {
                this.$refs.input && this.$refs.input.focus();
            }
        },
        watch: {
            visible (state) {
                if (state === false){
                    this.$refs.drop.destroy();
                }
                this.$refs.drop.update();
                this.$emit('on-open-change', state);
            },
            value(val) {
                this.internalValue = this.parseDate(val);
            },
            open (val) {
                this.visible = val === true;
            },
            type(type){
                this.onSelectionModeChange(type);
            },
            publicVModelValue(now, before){
                const newValue = JSON.stringify(now);
                const oldValue = JSON.stringify(before);
                const shouldEmitInput = newValue !== oldValue || typeof now !== typeof before;
                if (shouldEmitInput) this.$emit('input', now); // to update v-model
            },
        },
        mounted () {
            const initialValue = this.value;
            const parsedValue = this.publicVModelValue;
            if (typeof initialValue !== typeof parsedValue || JSON.stringify(initialValue) !== JSON.stringify(parsedValue)){
                this.$emit('input', this.publicVModelValue); // to update v-model
            }
            if (this.open !== null) this.visible = this.open;

            // to handle focus from confirm buttons
            this.$on('focus-input', () => this.focus());
        }
    };
</script>