<template>
  <uni-popup ref="popup" background-color="#fff" class="popup">
    <view class="center flex-col wh-full">
      <view class="flex-row justify-between p-15">
        <view @click="close">
          <uni-icons type="closeempty" size="20"></uni-icons>
        </view>
        <view>{{ title }}</view>
        <view></view>
      </view>
      <view class="picker-view">
        <picker-view class="wh-full pl-20 " :value="innerIndex" indicator-style='height: 50px;'
                     @change="changeHandler">
          <picker-view-column v-for="(item,index) in columns" :key="index">
            <view class="flex-col-center" v-for="(val,i) in item" :key="i"> {{ val }}</view>
          </picker-view-column>
        </picker-view>
      </view>
      <view class="button" style="width: 90%;margin: 5px auto;" @click="confirm">确定</view>
    </view>
  </uni-popup>
</template>

<script>
import dayjs from 'dayjs'

function times(n, iteratee) {
  let index = -1
  const result = Array(n < 0 ? 0 : n)
  while (++index < n) {
    result[index] = iteratee(index)
  }
  return result
}

function padZero(value) {
  return `00${value}`.slice(-2)
}

function range(min = 0, max = 0, value = 0) {
  return Math.max(min, Math.min(max, Number(value)))
}

function number(value) {
  return /^[\+-]?(\d+\.?\d*|\.\d+|\d\.\d+e\+\d+)$/.test(value)
}

function timeTest(value) {
  if (!value) return false
  // 判断是否数值或者字符串数值(意味着为时间戳)，转为数值，否则new Date无法识别字符串时间戳
  if (number(value)) value = +value
  return !/Invalid|NaN/.test(new Date(value).toString())
}

export default {
  name: 'datetime-picker',
  props: {
    value: {
      type: [String, Number],
      default: Number(new Date())
    },
    // 顶部标题
    title: {
      type: String,
      default: ''
    },
    // 展示格式，mode=date为日期选择，mode=time为时间选择，mode=year-month为年月选择，mode=datetime为日期时间选择
    mode: {
      type: String,
      default: 'datetime'
    },
    // 可选的最大时间
    maxDate: {
      type: Number,
      // 最大默认值为后10年
      default: new Date(new Date().getFullYear() + 10, 0, 1).getTime()
    },
    // 可选的最小时间
    minDate: {
      type: Number,
      // 最小默认值为前10年
      default: new Date(new Date().getFullYear() - 10, 0, 1).getTime()
    },
    // 可选的最小小时，仅mode=time有效
    minHour: {
      type: Number,
      default: 0
    },
    // 可选的最大小时，仅mode=time有效
    maxHour: {
      type: Number,
      default: 23
    },
    // 可选的最小分钟，仅mode=time有效
    minMinute: {
      type: Number,
      default: 0
    },
    // 可选的最大分钟，仅mode=time有效
    maxMinute: {
      type: Number,
      default: 59
    },
    // 选项过滤函数
    filter: {
      type: [Function, null],
      default: null
    },
    // 选项格式化函数
    formatter: {
      type: [Function, null],
      default: null
    },
    // 各列的默认索引
    defaultIndex: {
      type: Array,
      default: () => []
    }
  },
  data() {
    return {
      columns: [],
      innerDefaultIndex: [],
      innerFormatter: (type, value) => value,
      // 上一次选择的列索引
      lastIndex: [],
      // 索引值 ，对应picker-view的value
      innerIndex: [],
      // 各列的值
      innerColumns: [],
      // 上一次的变化列索引
      columnIndex: 0,
    }
  },
  watch: {
    // 监听默认索引的变化，重新设置对应的值
    innerDefaultIndex: {
      immediate: true,
      handler(n) {
        this.setIndexs(n, true)
      }
    },
    // 监听columns参数的变化
    columns: {
      immediate: true,
      deep: true,
      handler(n) {
        this.setColumns(n)
      }
    },
  },
  mounted() {
    this.init()
  },
  // #ifdef VUE3
  emits: ['close', 'canel', 'confirm', 'change'],
  // #endif
  methods: {
    init() {
      this.innerValue = this.correctValue(this.value)
      this.updateColumnValue(this.innerValue)
    },
    open() {
      this.$refs.popup.open('bottom')
    },
    // 关闭选择器
    close() {
      this.$refs.popup.close()
      this.$emit('close')
    },
    // 点击工具栏的确定按钮
    confirm() {
      this.$emit('confirm', {
        value: this.innerValue,
        mode: this.mode
      })
      this.close()
    },
    //用正则截取输出值,当出现多组数字时,抛出错误
    intercept(e, type) {
      let judge = e.match(/\d+/g)
      //判断是否掺杂数字
      if (judge.length > 1) {
        console.error("请勿在过滤或格式化函数时添加数字")
        return 0
      } else if (type && judge[0].length == 4) {//判断是否是年份
        return judge[0]
      } else if (judge[0].length > 2) {
        console.error("请勿在过滤或格式化函数时添加数字")
        return 0
      } else {
        return judge[0]
      }
    },
    isArray(value) {
      if (typeof Array.isArray === 'function') {
        return Array.isArray(value)
      }
      return Object.prototype.toString.call(value) === '[object Array]'
    },

    deepClone(obj) {
      // 对常见的“非”值，直接返回原来值
      if ([null, undefined, NaN, false].includes(obj)) return obj
      if (typeof obj !== 'object' && typeof obj !== 'function') {
        // 原始类型直接返回
        return obj
      }
      const o = this.isArray(obj) ? [] : {}
      for (const i in obj) {
        if (obj.hasOwnProperty(i)) {
          o[i] = typeof obj[i] === 'object' ? this.deepClone(obj[i]) : obj[i]
        }
      }
      return o
    },
    setIndexs(index, setLastIndex) {
      this.innerIndex = this.deepClone(index)
      if (setLastIndex) {
        this.setLastIndex(index)
      }
    },
    setLastIndex(index) {
      // 当能进入此方法，意味着当前设置的各列默认索引，即为“上一次”的选中值，需要记录，是因为changeHandler中
      // 需要拿前后的变化值进行对比，得出当前发生改变的是哪一列
      this.lastIndex = this.deepClone(index)
    },
    setColumns(columns) {
      this.innerColumns = this.deepClone(columns)
      // 如果在设置各列数据时，没有被设置默认的各列索引defaultIndex，那么用0去填充它，数组长度为列的数量
      if (this.innerIndex.length === 0) {
        this.innerIndex = new Array(columns.length).fill(0)
      }
    },
    changeHandler(e) {
      const {value} = e.detail
      let index = 0,
          columnIndex = 0
      // 通过对比前后两次的列索引，得出当前变化的是哪一列
      for (let i = 0; i < value.length; i++) {
        let item = value[i]
        if (item !== (this.lastIndex[i] || 0)) { // 把undefined转为合法假值0
          // 设置columnIndex为当前变化列的索引
          columnIndex = i
          // index则为变化列中的变化项的索引
          index = item
          break // 终止循环，即使少一次循环，也是性能的提升
        }
      }
      this.columnIndex = columnIndex
      const values = this.innerColumns
      // 将当前的各项变化索引，设置为"上一次"的索引变化值
      this.setLastIndex(value)
      this.setIndexs(value)

      this.zchange({indexs: value, values})
    },
    // 列发生变化时触发
    zchange(e) {
      const {indexs, values} = e
      let selectValue = ''
      if (this.mode === 'time') {
        // 根据value各列索引，从各列数组中，取出当前时间的选中值
        selectValue = `${this.intercept(values[0][indexs[0]])}:${this.intercept(values[1][indexs[1]])}`
      } else {
        // 将选择的值转为数值，比如'03'转为数值的3，'2019'转为数值的2019
        const year = parseInt(this.intercept(values[0][indexs[0]], 'year'))
        const month = parseInt(this.intercept(values[1][indexs[1]]))
        let date = parseInt(values[2] ? this.intercept(values[2][indexs[2]]) : 1)
        let hour = 0, minute = 0
        // 此月份的最大天数
        const maxDate = dayjs(`${year}-${month}`).daysInMonth()
        // year-month模式下，date不会出现在列中，设置为1，为了符合后边需要减1的需求
        if (this.mode === 'year-month') {
          date = 1
        }
        // 不允许超过maxDate值
        date = Math.min(maxDate, date)
        if (this.mode === 'datetime') {
          hour = parseInt(this.intercept(values[3][indexs[3]]))
          minute = parseInt(this.intercept(values[4][indexs[4]]))
        }
        // 转为时间模式
        selectValue = Number(new Date(year, month - 1, date, hour, minute))
      }
      // 取出准确的合法值，防止超越边界的情况
      selectValue = this.correctValue(selectValue)
      this.innerValue = selectValue
      this.updateColumnValue(selectValue)
      // 发出change时间，value为当前选中的时间戳
      this.$emit('change', {
        value: selectValue,
        // #ifndef MP-WEIXIN
        // 微信小程序不能传递this实例，会因为循环引用而报错
        picker: this.$refs.picker,
        // #endif
        mode: this.mode
      })
    },
    // 更新各列的值，进行补0、格式化等操作
    updateColumnValue(value) {
      this.innerValue = value
      this.updateColumns()
      this.updateIndexs(value)
    },
    // 更新索引
    updateIndexs(value) {

      let values = []
      const formatter = this.formatter || this.innerFormatter
      if (this.mode === 'time') {
        // 将time模式的时间用:分隔成数组
        const timeArr = value.split(':')
        // 使用formatter格式化方法进行管道处理
        values = [formatter('hour', timeArr[0]), formatter('minute', timeArr[1])]
      } else {
        values = [
          formatter('year', `${dayjs(value).year()}`),
          // 月份补0
          formatter('month', padZero(dayjs(value).month() + 1))
        ]

        if (this.mode === 'date') {
          // date模式，需要添加天列
          values.push(formatter('day', padZero(dayjs(value).date())))
        }
        if (this.mode === 'datetime') {
          // 数组的push方法，可以写入多个参数
          values.push(formatter('day', padZero(dayjs(value).date())), formatter('hour', padZero(dayjs(value).hour())), formatter('minute', padZero(dayjs(value).minute())))
        }
      }
      // 根据当前各列的所有值，从各列默认值中找到默认值在各列中的索引
      const indexs = this.columns.map((column, index) => {
        // 通过取大值，可以保证不会出现找不到索引的-1情况
        return Math.max(0, column.findIndex(item => parseInt(item) === parseInt(values[index])))
      })
      this.innerDefaultIndex = indexs
    },
    // 更新各列的值
    updateColumns() {
      const formatter = this.formatter || this.innerFormatter
      // 获取各列的值，并且map后，对各列的具体值进行补0操作
      this.columns = this.getOriginColumns().map((column) => column.values.map((value) => formatter(column.type, value)))
    },
    getOriginColumns() {
      // 生成各列的值
      const results = this.getRanges().map(({type, range}) => {
        let values = times(range[1] - range[0] + 1, (index) => {
          let value = range[0] + index
          value = type === 'year' ? `${value}年` : padZero(value)
          if (type === 'month') {
            value = `${value}月`
          }
          if (type === 'day') {
            value = `${value}日`
          }
          if (type === 'hour') {
            value = `${value}时`
          }
          if (type === "minute") {
            value = `${value}分`
          }
          return value
        })
        // 进行过滤
        if (this.filter) {
          values = this.filter(type, values)
        }
        return {type, values}
      })
      return results
    },
    // 得出合法的时间
    correctValue(value) {
      const isDateMode = this.mode !== 'time'
      if (isDateMode && !timeTest(value)) {
        // 如果是日期类型，但是又没有设置合法的当前时间的话，使用最小时间为当前时间
        value = this.minDate
      } else if (!isDateMode && !value) {
        // 如果是时间类型，而又没有默认值的话，就用最小时间
        value = `${`00${this.minHour}`.slice(-2)}:${`00${this.minMinute}`.slice(-2)}`
      }
      // 时间类型
      if (!isDateMode) {
        if (String(value).indexOf(':') === -1) return console.error('时间错误，请传递如12:24的格式')
        let [hour, minute] = value.split(':')
        // 对时间补零，同时控制在最小值和最大值之间
        hour = padZero(range(this.minHour, this.maxHour, Number(hour)))
        minute = padZero(range(this.minMinute, this.maxMinute, Number(minute)))
        return `${hour}:${minute}`
      } else {
        // 如果是日期格式，控制在最小日期和最大日期之间
        value = dayjs(value).isBefore(dayjs(this.minDate)) ? this.minDate : value
        value = dayjs(value).isAfter(dayjs(this.maxDate)) ? this.maxDate : value
        return value
      }
    },
    // 获取每列的最大和最小值
    getRanges() {
      if (this.mode === 'time') {
        return [
          {
            type: 'hour',
            range: [this.minHour, this.maxHour],
          },
          {
            type: 'minute',
            range: [this.minMinute, this.maxMinute],
          },
        ];
      }
      const {maxYear, maxDate, maxMonth, maxHour, maxMinute,} = this.getBoundary('max', this.innerValue);
      const {minYear, minDate, minMonth, minHour, minMinute,} = this.getBoundary('min', this.innerValue);
      const result = [
        {
          type: 'year',
          range: [minYear, maxYear],
        },
        {
          type: 'month',
          range: [minMonth, maxMonth],
        },
        {
          type: 'day',
          range: [minDate, maxDate],
        },
        {
          type: 'hour',
          range: [minHour, maxHour],
        },
        {
          type: 'minute',
          range: [minMinute, maxMinute],
        },
      ];
      if (this.mode === 'date')
        result.splice(3, 2);
      if (this.mode === 'year-month')
        result.splice(2, 3);
      return result;
    },
    // 根据minDate、maxDate、minHour、maxHour等边界值，判断各列的开始和结束边界值
    getBoundary(type, innerValue) {
      const value = new Date(innerValue)
      const boundary = new Date(this[`${type}Date`])
      const year = dayjs(boundary).year()
      let month = 1
      let date = 1
      let hour = 0
      let minute = 0
      if (type === 'max') {
        month = 12
        // 月份的天数
        date = dayjs(value).daysInMonth()
        hour = 23
        minute = 59
      }
      // 获取边界值，逻辑是：当年达到了边界值(最大或最小年)，就检查月允许的最大和最小值，以此类推
      if (dayjs(value).year() === year) {
        month = dayjs(boundary).month() + 1
        if (dayjs(value).month() + 1 === month) {
          date = dayjs(boundary).date()
          if (dayjs(value).date() === date) {
            hour = dayjs(boundary).hour()
            if (dayjs(value).hour() === hour) {
              minute = dayjs(boundary).minute()
            }
          }
        }
      }
      return {
        [`${type}Year`]: year,
        [`${type}Month`]: month,
        [`${type}Date`]: date,
        [`${type}Hour`]: hour,
        [`${type}Minute`]: minute
      }
    },
  },
}
</script>

<style lang="scss" scoped>
.popup {
  z-index: 9999;
}

.center {
  padding-bottom: var(--window-bottom);

}

.picker-view {
  height: 400rpx;
}
</style>
