<template>
  <view
    class="leo-drag"
    id="leo-drag"
    ref="leoDragView"
    :style="[{ 'touch-action': isStopPropagation ? 'none' : 'auto' }]"
  >
    <!-- touch-action: 阻止滚动 -->
    <movable-area :style="[getAreaStyle]" v-if="showArea">
      <movable-view
        v-if="isStopPropagation"
        v-for="(item, index) in showList"
        :animation="animation"
        :direction="direction"
        :key="item.key"
        :damping="damping"
        :x="item.x"
        :y="item.y"
        :disabled="longpress ? disabled : false"
        @longpress="controlLongpress(index)"
        @touchstart.stop="handleDragStart($event, index)"
        @change="handleMoving"
        @touchend="handleDragEnd"
        :style="[
          { 'z-index': activeIndex === index ? 9 : 1 },
          { top: activeIndex === index ? scrollInfo.y + 'px' : 0 },

          isList ? { height: item.height + 'px' } : getItemStyle,
        ]"
        class="base-drag-wrapper"
      >
        <view class="slotContent" ref="slotContent" :style="[slotContentStyle]">
          <slot :data="{ ...item, index, activeIndex, moveToIndex }" name="content"></slot>
        </view>
        <view
          v-if="showPlaceholder(index)"
          class="placeholder"
          :style="{ height: showList[activeIndex]?.height + 'px', top: placeholderTopValue + 'px' }"
        >
          <slot name="placeholder"></slot>
        </view>
      </movable-view>
      <movable-view
        v-else
        v-for="(item, index) in showList"
        :animation="animation"
        :direction="direction"
        :key="`${item.key}-${index}`"
        :damping="damping"
        :x="item.x"
        :y="item.y"
        :disabled="longpress ? disabled : false"
        @longpress="controlLongpress(index)"
        @touchstart="handleDragStart($event, index)"
        @change="handleMoving"
        @touchend="handleDragEnd"
        :style="[
          { 'z-index': activeIndex === index ? 9 : 1 },
          { top: activeIndex === index ? scrollInfo.y + 'px' : 0 },
          isList ? { height: item.height + 'px' } : getItemStyle,
        ]"
        class="base-drag-wrapper"
      >
        <view class="slotContent" ref="slotContent" :style="[slotContentStyle]">
          <slot :data="{ ...item, index, activeIndex, moveToIndex }" name="content"></slot>
        </view>
        <view
          v-if="showPlaceholder(index)"
          class="placeholder"
          :style="{ height: showList[activeIndex]?.height + 'px', top: placeholderTopValue + 'px' }"
        >
          <slot name="placeholder"></slot>
        </view>
      </movable-view>
    </movable-area>
    <!-- 可用于遮罩指引提示，考虑定位的zindex问题，所以使用插槽插进来 -->
    <slot name="custom"></slot>
  </view>
</template>

<script setup lang="ts">
import { ref, computed, watch, nextTick, onMounted, getCurrentInstance } from 'vue'
import { deepCopy } from './useSort'

// #ifdef APP-NVUE
const dom = uni.requireNativePlugin('dom') // 引入原生插件
// #endif

// 定义MovableViewDirection类型，匹配期望的字面量联合类型
type MovableViewDirection = 'all' | 'vertical' | 'horizontal' | 'none'

interface ItemType {
  key?: string
  x?: number
  y?: number
  height?: number
  dropId?: number
  [key: string]: any // 添加索引签名，允许访问任何属性
}

interface ScrollInfoType {
  x: number
  y: number
}

interface PositionType {
  y: number
}

const props = defineProps({
  slotContentStyle: {
    // 自定义slotContent的样式，列表的每个父级样式
    type: Object,
    default: () => ({}),
  },
  list: {
    // 列表
    type: Array as () => ItemType[],
    default: () => [],
  },
  column: {
    // 每行显示多少数量，如果是单项(column：1),可根据自定义高度动态变化，并且可拖拽滚动列表；
    type: Number,
    default: 1,
  },
  height: {
    // 可拖拽的区域宽度，auto 为根据子级高度(itemHeight)计算
    type: String,
    default: 'auto',
  },
  itemHeight: {
    // 每个子元素的高度，为auto时，计算最高元素
    type: String,
    default: 'auto',
  },
  direction: {
    // 可拖动方向
    type: String as () => MovableViewDirection,
    default: 'all',
    validator: (value: string) => {
      return ['all', 'vertical', 'horizontal', 'none'].includes(value)
    },
  },
  dragDirection: {
    // 目前拖动方向 upward-向上， down-向下
    type: String,
    default: 'down',
  },
  animation: {
    // 拖动时开启动画
    type: Boolean,
    default: true,
  },
  damping: {
    // 用于控制x或y改变时的动画和过界回弹的动画
    type: Number,
    default: 20,
  },
  swapMode: {
    // 替换模式：true:位置后排，false：两位替换
    type: Boolean,
    default: true,
  },
  longpress: {
    // 是否禁用
    type: Boolean,
    default: true,
  },
  supportLongpress: {
    // 是否支持长按
    type: Boolean,
    default: false,
  },
  isStopPropagation: {
    // 是否阻止事件冒泡: 可用于判断是否拖拽状态
    type: Boolean,
    default: false,
  },
})

const emit = defineEmits([
  'getAreaStyle',
  'getItemStyle',
  'pressStart',
  'handleDragStart',
  'handleMoving',
  'handleDragEnd',
  'getList',
  'update:list',
  'getItemHeight',
])

// 状态变量
const showList = ref<ItemType[]>([]) // 显示的数据；最终都会根据cloneList赋值
const cloneList = ref<ItemType[]>([]) // 为了不影响视图突兀变化，使用cloneList代替showList的数据下标变化；cloneList的数据会比showList优先变化
const disabled = ref(true) // 是否允许拖动
const activeIndex = ref(-1) // 当前拖动的item
const moveToIndex = ref(-1) // 当前拖动的item > 拖动到那一块： 也就是下一块item
const oldIndex = ref(-1) // 上一块item的位置
const tempDragInfo = ref({
  // 计算拖动的值
  x: '',
  y: '',
})
const oldPosition = ref<PositionType>({ y: 0 }) // 触边时滚动位置
const itemMaxHeight = ref('80px')
const width = ref('100%')
const showArea = ref(false) // 在nvue，会出现空白或者部分元素不显示的问题
const viewMaxHeight = ref(80) // 全部视图高度，仅在isList === true时，使用
const activeY = ref(0) // 获得下标的y轴
const scrollInfo = ref<ScrollInfoType>({ x: 0, y: 0 }) // 滚动产生的信息

// refs
const leoDragView = ref<any>(null)
const slotContent = ref<any[]>([])

const instance = getCurrentInstance()

// 拿到活动下标的高度
const placeholderTopValue = computed(() => {
  let height = 0
  try {
    if (showList.value && showList.value[activeIndex.value]) {
      height = -showList.value[activeIndex.value].height!
    }
  } catch (e) {
    console.log('eee', e)
  }
  return height
})

// 单独一排: 则不需要计算
const isList = computed(() => {
  let is = false
  // #ifndef APP-NVUE
  is = props.column === 1 // 说明是列表
  // #endif
  return is
})

// 设置每个item的高度。如果是列表(isList === true)则忽略 --- 默认获取最高的item
const getItemHeight = computed(() => {
  // 获取item的高度
  let value =
    props.itemHeight === 'auto' ? parseFloat(itemMaxHeight.value) : parseFloat(props.itemHeight)
  emit('getItemHeight', value)
  return value
})

// 设置item的宽度。
const getItemWidth = computed(() => {
  // 获取item的宽度
  const w = getRealWidth(width.value)
  return (parseFloat(w) / props.column).toFixed(2)
})

// 给拖动区域设置宽高
const getAreaStyle = computed(() => {
  // 给可拖动区域设置宽高
  const w = getRealWidth(width.value)
  let h: string | number = props.height
  if (h === 'auto') {
    // 长度/横排的数量 * 高度 = 总高度； 不能使用viewMaxHeight的高度，因为这是根据每个item的高度分别增加的，而不是根据 最高的item增加的。
    h = isList.value
      ? viewMaxHeight.value
      : Math.ceil(showList.value.length / props.column) * getItemHeight.value
  }
  let style = {
    width: w + 'px',
    height: typeof h === 'number' ? h + 'px' : h,
  }
  emit('getAreaStyle', style)
  return style
})

// 给item设置高度和宽度
const getItemStyle = computed(() => {
  let itemHeightWidth = {
    width: getItemWidth.value + 'px',
    height: getItemHeight.value + 'px',
  }
  emit('getItemStyle', itemHeightWidth)
  return itemHeightWidth
})

// 获取实际的宽度
const getRealWidth = (w: string | number): string => {
  let width = w + ''
  if (width.includes('%')) {
    const windowWidth = uni.getSystemInfoSync().windowWidth
    width = (windowWidth * (parseFloat(width) / 100)).toString()
  }
  return width
}

// 省略初始化时添加的 x，y和key等参数
const omit = (obj: any, args: string[] = ['x', 'y', 'key', 'dropId', 'height']) => {
  if (!args) return obj
  const newObj: any = {}
  const isString = typeof args === 'string'
  const keys = Object.keys(obj).filter((item) => {
    if (isString) {
      return item !== args
    }
    return !args.includes(item)
  })

  keys.forEach((key) => {
    if (obj[key] !== undefined) newObj[key] = obj[key]
  })
  return newObj
}

// 获取当前的位置
const getPosition = (index: number, list = cloneList.value): [number, number] => {
  // 通过计算重新算换 偏移单位。
  let x = (index % props.column) * Number(getItemWidth.value)
  let y = 0
  if (isList.value) {
    // 如果是 单项数据时，不需要通过getItemHeight来排，而是需要根据每个item的高度自动填充
    for (let i = 0; i < list.length; i++) {
      if (index === i) break
      y += list[i].height!
    }
  } else {
    // 根据最高的item来决定整体的高度
    y = Math.floor(index / props.column) * getItemHeight.value
  }
  return [x, y]
}

// 初始化
const initList = (list: ItemType[] = [], changeheight: boolean = false): Promise<void> => {
  return new Promise((resolve) => {
    const newList = deepCopy(list)
    // 给每个item添加一x，y和key
    showList.value = newList.map((item, index) => {
      const [x, y] = getPosition(index)
      let data = {
        ...item,
        x,
        y,
        dropId: index + 1,
      }
      let key = 'slot' + Math.random() + index
      // 如果x轴和y轴没变，那么不用更新key来刷新状态
      if (x === item?.x && y === item?.y) {
        if (activeIndex.value !== index) {
          // 非当前点击的下标和目标下标的下标不需要生成新的key
          key = item.key
        }
      }
      // 判断拖动位置的元素是那个
      data.key = key
      return data
    })
    cloneList.value = deepCopy(showList.value)
    nextTick(() => {
      showArea.value = true
    })

    if (changeheight && props.itemHeight === 'auto') {
      // 获取到最高的item
      setTimeout(async () => {
        // #ifdef APP-NVUE
        showArea.value = false

        const calculateHeights = async () => {
          let max = 0
          let viewMaxHeightVal = 0

          const promises = slotContent.value.map((content, index) => {
            return new Promise<void>((resolve) => {
              dom.getComponentRect(content, (res: any) => {
                let size = res.size
                if (isList.value) {
                  cloneList.value[index].height = size.height
                }
                viewMaxHeightVal += size.height
                if (size.height > max) {
                  max = size.height
                }
                resolve()
              })
            })
          })

          await Promise.all(promises)
          viewMaxHeight.value = viewMaxHeightVal
          itemMaxHeight.value = max + 'px'
          nextTick(() => {
            initList(cloneList.value).then(resolve)
          })
        }

        await calculateHeights()
        // #endif

        // #ifndef APP-NVUE
        const query = uni.createSelectorQuery().in(instance.proxy)
        query
          .selectAll('.slotContent')
          .boundingClientRect((data) => {
            let domList = JSON.parse(JSON.stringify(data))
            let max = 0
            let viewMaxHeightVal = 0

            for (let i = 0; i < domList.length; i++) {
              let height = domList[i].height
              if (isList.value) {
                cloneList.value[i].height = height
              }
              viewMaxHeightVal += height
              if (height > max) {
                max = height
              }
            }
            viewMaxHeight.value = viewMaxHeightVal // 内容区域总高度
            itemMaxHeight.value = max + 'px'
            initList(cloneList.value).then(resolve)
          })
          .exec()
        // #endif
      }, 0)
    } else {
      resolve()
    }
  })
}

// 方法
// 显示占位
const showPlaceholder = (i: number): boolean => {
  // 位于底部或者指定位置等于活动位置时,都不会显示占位
  let isShow = false
  if (moveToIndex.value > activeIndex.value) {
    // 指定下标位置大于活动下标，i-1; 目的是为了用指定位置的下一级判断占位
    if (moveToIndex.value === i - 1) {
      isShow = true
    }
  } else if (moveToIndex.value === activeIndex.value) {
    return false // 位置不变时
  } else if (moveToIndex.value < activeIndex.value) {
    if (moveToIndex.value === i) {
      isShow = true
    }
  }
  return isShow
}

// 长按事件
const controlLongpress = (i: number) => {
  if (props.supportLongpress) {
    handleLongpress(i)
  }
}

// 长按事件或者在外部直接调用，才允许拖动
const handleDragStart = (event: any, index: number) => {
  emit('pressStart', { event, data: cloneList.value[index] })
}

// 允许拖动状态
const handleLongpress = (index: number) => {
  disabled.value = false
  // 哪个开始活动
  activeIndex.value = index
  // 记录一下旧的下标
  oldIndex.value = index
  emit('handleDragStart', index)
}

// 滚动时，改动列表的y轴
const changeScrollInfo = (detail: { y: number }) => {
  // 滚动触碰边缘触发
  scrollInfo.value.y += detail.y
}

// 拖拽中
const handleMoving = (e: any) => {
  if (e.detail.source !== 'touch') return
  let { x, y } = e.detail

  if (isList.value) {
    // 如果是列表需要添加滚动产生的边距
    y = y + scrollInfo.value.y

    oldPosition.value.y = y
  }
  changeListPosition({ x, y })
}

// 改变列表位置
const changeListPosition = ({ x = 0, y = 0, type }: { x: number; y: number; type?: string }) => {
  // x 手指按下拖动，产生的位置，超出了item的宽度，那么就改变下标，包括y轴。
  let currentX = Math.floor((x + Number(getItemWidth.value) / 2) / Number(getItemWidth.value))
  let currentY = Math.floor((y + getItemHeight.value / 2) / getItemHeight.value)
  // moveToIndex：通过计算横排数量，偏移量（ x，y ），得出下标位置
  moveToIndex.value = Math.min(currentY * props.column + currentX, cloneList.value.length - 1) // 滑到了哪个位置
  // 如果是列表的情况 -- 触碰元素的边缘就检测到下标
  if (isList.value) {
    // 列表的情况, 无需考虑x轴;  通过y轴和列表的高度来判断;
    let currentH = 0
    for (let i = 0; i < cloneList.value.length; i++) {
      currentH += cloneList.value[i].height!
      if (props.dragDirection === 'down') {
        // 往下的情况下 ---- 当前活动的下标的高度( 也就是拖动的底部 )碰到下一个元素时； 赋值下标
        /*
          下一个下标的5/1；位置触发
          如果到了最后一个时，让下一个触发的高度变成当前拖动到指定位置的高度，避免照成到达了底部后下标位置有差异的问题；
        **/
        let nextH = cloneList.value[i + 1] ? cloneList.value[i + 1].height! / 5 : currentH
        if (currentH > y + cloneList.value[activeIndex.value].height! - nextH) {
          moveToIndex.value = i
          break
        }
      } else {
        // 往上拖拽时 ---- 当前活动的下标( 也就是拖动的顶部 )碰到上一个元素时； 赋值下标
        let theH = cloneList.value[i - 1] ? cloneList.value[i - 1].height! / 5 : 0 // 下一个下标的5/1；位置触发
        if (currentH > y + theH) {
          moveToIndex.value = i
          break
        }
      }
    }
  }
  if (oldIndex.value !== moveToIndex.value && oldIndex.value !== -1 && moveToIndex.value !== -1) {
    // 排序替换
    const newList = deepCopy(cloneList.value) // cloneList的数据会比showList优先变化
    const replaceList = deepCopy(cloneList.value) // cloneList的数据会比showList优先变化
    let splicItem = newList.splice(activeIndex.value, 1)[0] // 删除指定下标，并且返回删除的数据
    newList.splice(moveToIndex.value, 0, splicItem) // 被移入的下标数据替换成刚刚删除的下标数据
    if (props.swapMode) {
      if (isList.value) {
        // 需要先吧高度先替换掉---
        replaceList.splice(moveToIndex.value, 0, ...replaceList.splice(activeIndex.value, 1))
      }
      // 替换位置
      showList.value.forEach((item, index) => {
        if (index !== activeIndex.value) {
          let itemIndex = newList.findIndex((val) => val.dropId === item.dropId)
          // 在修改轴和y轴时，需要先吧height改变，因为列表的y轴就是根据height赋值的。
          let position = getPosition(itemIndex, replaceList)
          item.x = position[0]
          item.y = position[1]
        }
      })
    } else {
      // 两点换位
      replaceList[activeIndex.value].height = showList.value[moveToIndex.value].height
      replaceList[moveToIndex.value].height = showList.value[activeIndex.value].height

      showList.value.forEach((item, index) => {
        if (isList.value) {
          if (props.dragDirection === 'down') {
            if (index > activeIndex.value && index < moveToIndex.value) {
              // 在这个期间的高度和y都需要变化
              ;[item.x, item.y] = getPosition(index, replaceList)
            }
          } else if (index < activeIndex.value && index > moveToIndex.value) {
            // 在这个期间的高度和y都需要变化
            ;[item.x, item.y] = getPosition(index, replaceList)
          }
        }
        // 说明需要把不需要换位的下标替回来
        if (index === oldIndex.value) {
          ;[item.x, item.y] = getPosition(oldIndex.value, replaceList)
        }
        // 当前移动的下标和目标下标互换位置
        if (index === moveToIndex.value) {
          ;[item.x, item.y] = getPosition(activeIndex.value, replaceList)
        }
      })
    }
    oldIndex.value = moveToIndex.value
  }

  let list = deepCopy(showList.value)
  emit('handleMoving', { list, x, y })
}

// 拖拽结束
const handleDragEnd = (e: any) => {
  setTimeout(() => {
    scrollInfo.value = { x: 0, y: 0 }
    if (disabled.value) return // 为点击不能出发
    if (
      moveToIndex.value !== -1 &&
      activeIndex.value !== -1 &&
      moveToIndex.value !== activeIndex.value
    ) {
      // 拖动了的话，并且目标位置和活动位置不一样，才来这里
      if (props.swapMode) {
        cloneList.value.splice(
          moveToIndex.value,
          0,
          ...cloneList.value.splice(activeIndex.value, 1),
        )
      } else {
        let active = cloneList.value[activeIndex.value] // 当前下标
        let move = cloneList.value[moveToIndex.value] // 目标下标
        // 替换
        cloneList.value[activeIndex.value] = move
        cloneList.value[moveToIndex.value] = active
      }
    }
    initList(cloneList.value)
    const endList = showList.value.map((item) => omit(item))
    const list = deepCopy(cloneList.value)
    emit('update:list', endList) // vue3 的更新方法
    emit('getList', endList) // 更新列表
    emit('handleDragEnd', { list }) // 结束拖拽

    activeIndex.value = -1
    oldIndex.value = -1
    moveToIndex.value = -1
    disabled.value = true
  })
}

// 数据的长度不变的情况下-更新数据-减少加载带来的画面卡顿
const updataList = (list: ItemType[]) => {
  // 注意做好拖拽时规避 更新
  if (props.isStopPropagation) return // 拖拽中状态不可更新数据
  const newList = deepCopy(list)
  for (let i = 0; i < newList.length; i++) {
    showList.value[i] = { ...showList.value[i], ...newList[i] }
  }
  cloneList.value = deepCopy(showList.value)
}

// 因为可能获取的宽度是0，需要需要重调
const setLeoDrag = (size: number = 5) => {
  if (size < 0) return

  // #ifdef APP-NVUE
  dom.getComponentRect(leoDragView.value, (res: any) => {
    let data = res.size
    getViewCallback(data, size)
  })
  // #endif

  // #ifndef APP-NVUE
  nextTick(() => {
    const query = uni.createSelectorQuery().in(instance.proxy)
    query
      .select('.leo-drag')
      .boundingClientRect((data) => {
        getViewCallback(data, size)
      })
      .exec()
  })
  // #endif
}

// 初次获取元素宽高后
const getViewCallback = (data: any, size: number) => {
  if (!data || data.width < 50) {
    setTimeout(() => {
      size = size - 1
      setLeoDrag(size)
    }, 300)
    return
  }
  width.value = data.width
  nextTick(() => {
    initList(showList.value, true) // 初始化 dom
  })
}

// 监听列表变化
watch(
  () => props.list,
  (newList) => {
    let list = deepCopy(newList) || []
    // 先拿到新增数据，因为是从父级侦听获取数据，并且是向后添加数据，所以需要使用剪切
    if (list.length > showList.value.length) {
      list.splice(0, showList.value.length)
      list = [...showList.value, ...list]
      initList(list, true)
    } else {
      // 如果小于显示长度 说明刷新或者删除了说明元素，这个时候应该更新列表
      nextTick(() => {
        initList(list, true)
      })
    }
  },
  { deep: true, immediate: true },
)

// 创建时初始化数据
showList.value = deepCopy(props.list) || []

// 组件挂载后初始化
onMounted(() => {
  setLeoDrag()
})

// 暴露方法给父组件调用
defineExpose({
  initList,
  updataList,
  handleLongpress,
})
</script>

<style lang="scss" scoped>
.base-drag-wrapper {
  opacity: 1;
  z-index: 1;
  color: #212121;
  display: flex;
  align-items: center;
  transition: top 0.4s;

  flex-wrap: wrap;
  /* #ifndef APP-NVUE */
  width: 100%;
  /* #endif */
}
.slotContent {
  display: flex;
  /* #ifndef APP-NVUE */
  width: 100%;
  /* #endif */

  :deep() {
    // 微信小程序会多嵌一层view--综合一起设置
    & > view {
      width: 100%;
      flex: 1;
    }
  }
}
.placeholder {
  position: absolute;
  width: 100%;
  height: 100%;
  animation: animationShows 0.51s linear;
}
@keyframes animationShows {
  0% {
    opacity: 0;
  }
  20% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

.leo-drag {
  /* #ifndef APP-NVUE */
  width: 100%;
  /* #endif */
}
.loading {
  color: pink;
}
</style>
