<script lang="ts" setup>
import dayjs from 'dayjs'
import { UITypes, isVirtualCol } from 'nocodb-sdk'
import type { Row } from '~/lib'
import { ref } from '#imports'
import { generateRandomNumber, isRowEmpty } from '~/utils'

const emits = defineEmits(['expandRecord'])

const { selectedDateRange, formattedData, formattedSideBarData, calendarRange, selectedDate, displayField, updateRowProperty } =
  useCalendarViewStoreOrThrow()

const container = ref<null | HTMLElement>(null)

const { width: containerWidth } = useElementSize(container)

const { isUIAllowed } = useRoles()

const meta = inject(MetaInj, ref())

const weekDates = computed(() => {
  const startOfWeek = new Date(selectedDateRange.value.start!)
  const endOfWeek = new Date(selectedDateRange.value.end!)
  const datesArray = []
  while (startOfWeek.getTime() <= endOfWeek.getTime()) {
    datesArray.push(new Date(startOfWeek))
    startOfWeek.setDate(startOfWeek.getDate() + 1)
  }
  return datesArray
})

const findFirstSuitableColumn = (recordsInDay: any, startDayIndex: number, spanDays: number) => {
  let column = 0
  while (true) {
    let isColumnSuitable = true
    for (let i = 0; i < spanDays; i++) {
      const dayIndex = startDayIndex + i
      if (!recordsInDay[dayIndex]) {
        recordsInDay[dayIndex] = {}
      }
      if (recordsInDay[dayIndex][column]) {
        isColumnSuitable = false
        break
      }
    }

    if (isColumnSuitable) {
      return column
    }
    column++
  }
}

const calendarData = computed(() => {
  if (!formattedData.value || !calendarRange.value) return []
  const recordsInDay: {
    [key: number]: {
      [key: number]: boolean
    }
  } = {
    0: {},
    1: {},
    2: {},
    3: {},
    4: {},
    5: {},
    6: {},
  }

  const recordsInRange: Array<Row> = []
  const perDayWidth = containerWidth.value / 7

  calendarRange.value.forEach((range) => {
    const fromCol = range.fk_from_col
    const toCol = range.fk_to_col
    if (fromCol && toCol) {
      for (const record of [...formattedData.value].filter((r) => {
        const startDate = dayjs(r.row[fromCol.title!])
        const endDate = dayjs(r.row[toCol.title!])
        if (!startDate.isValid() || !endDate.isValid()) return false
        return !endDate.isBefore(startDate)
      })) {
        const id = record.row.id ?? generateRandomNumber()
        let startDate = dayjs(record.row[fromCol.title!])
        const ogStartDate = startDate.clone()
        const endDate = dayjs(record.row[toCol.title!])

        if (startDate.isBefore(selectedDateRange.value.start)) {
          startDate = dayjs(selectedDateRange.value.start)
        }

        const startDaysDiff = startDate.diff(selectedDateRange.value.start, 'day')

        let spanDays = Math.max(Math.min(endDate.diff(startDate, 'day'), 6) + 1, 1)

        if (endDate.isAfter(startDate, 'month')) {
          spanDays = 7 - startDaysDiff
        }

        if (startDaysDiff > 0) {
          spanDays = Math.min(spanDays, 7 - startDaysDiff)
        }
        const widthStyle = `calc(max(${spanDays} * ${perDayWidth}px, ${perDayWidth}px))`

        let suitableColumn = -1
        for (let i = 0; i < spanDays; i++) {
          const dayIndex = startDaysDiff + i

          if (!recordsInDay[dayIndex]) {
            recordsInDay[dayIndex] = {}
          }

          if (suitableColumn === -1) {
            suitableColumn = findFirstSuitableColumn(recordsInDay, dayIndex, spanDays)
          }
        }

        // Mark the suitable column as occupied for the entire span
        for (let i = 0; i < spanDays; i++) {
          const dayIndex = startDaysDiff + i
          recordsInDay[dayIndex][suitableColumn] = true
        }

        let position = 'none'

        const isStartInRange =
          ogStartDate && ogStartDate.isBetween(selectedDateRange.value.start, selectedDateRange.value.end, 'day', '[]')
        const isEndInRange = endDate && endDate.isBetween(selectedDateRange.value.start, selectedDateRange.value.end, 'day', '[]')

        if (isStartInRange && isEndInRange) {
          position = 'rounded'
        } else if (
          startDate &&
          endDate &&
          startDate.isBefore(selectedDateRange.value.start) &&
          endDate.isAfter(selectedDateRange.value.end)
        ) {
          position = 'none'
        } else if (
          startDate &&
          endDate &&
          (startDate.isAfter(selectedDateRange.value.end) || endDate.isBefore(selectedDateRange.value.start))
        ) {
          position = 'none'
        } else if (isStartInRange) {
          position = 'leftRounded'
        } else if (isEndInRange) {
          position = 'rightRounded'
        }

        recordsInRange.push({
          ...record,
          rowMeta: {
            ...record.rowMeta,
            range: range as any,
            position,
            id,
            style: {
              width: widthStyle,
              left: `${startDaysDiff * perDayWidth}px`,
              top: `${suitableColumn * 40}px`,
            },
          },
        })
      }
    } else if (fromCol) {
      for (const record of formattedData.value) {
        const id = record.row.id ?? generateRandomNumber()

        const startDate = dayjs(record.row[fromCol.title!])
        const startDaysDiff = Math.max(startDate.diff(selectedDateRange.value.start, 'day'), 0)
        const suitableColumn = findFirstSuitableColumn(recordsInDay, startDaysDiff, 1)
        recordsInDay[startDaysDiff][suitableColumn] = true

        recordsInRange.push({
          ...record,
          rowMeta: {
            ...record.rowMeta,
            range: range as any,
            id,
            position: 'rounded',
            style: {
              width: `calc(${perDayWidth}px)`,
              left: `${startDaysDiff * perDayWidth}px`,
              top: `${suitableColumn * 40}px`,
            },
          },
        })
      }
    }
  })

  return recordsInRange
})

const dragElement = ref<HTMLElement | null>(null)

const draggingId = ref<string | null>(null)

const resizeInProgress = ref(false)

const dragTimeout = ref<ReturnType<typeof setTimeout>>()

const isDragging = ref(false)
const dragRecord = ref<Row>()

const resizeDirection = ref<'right' | 'left' | null>()
const resizeRecord = ref<Row | null>(null)

const hoverRecord = ref<string | null>()

const useDebouncedRowUpdate = useDebounceFn((row: Row, updateProperty: string[], isDelete: boolean) => {
  updateRowProperty(row, updateProperty, isDelete)
}, 500)

const onResize = (event: MouseEvent) => {
  if (!isUIAllowed('dataEdit')) return
  if (!container.value || !resizeRecord.value) return
  const { width, left } = container.value.getBoundingClientRect()

  const percentX = (event.clientX - left - window.scrollX) / width

  const fromCol = resizeRecord.value.rowMeta.range?.fk_from_col
  const toCol = resizeRecord.value.rowMeta.range?.fk_to_col
  if (!fromCol || !toCol) return

  const ogEndDate = dayjs(resizeRecord.value.row[toCol.title!])
  const ogStartDate = dayjs(resizeRecord.value.row[fromCol.title!])

  const day = Math.min(Math.floor(percentX * 7), 6)

  if (resizeDirection.value === 'right') {
    let newEndDate = dayjs(selectedDateRange.value.start).add(day, 'day')
    const updateProperty = [toCol.title!]

    if (dayjs(newEndDate).isBefore(ogStartDate, 'day')) {
      newEndDate = ogStartDate.clone()
    }

    if (!newEndDate.isValid()) return

    const newRow = {
      ...resizeRecord.value,
      row: {
        ...resizeRecord.value.row,
        [toCol.title!]: newEndDate.format('YYYY-MM-DD'),
      },
    }

    formattedData.value = formattedData.value.map((r) => {
      const pk = extractPkFromRow(r.row, meta.value!.columns!)

      if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
        return newRow
      }
      return r
    })
    useDebouncedRowUpdate(newRow, updateProperty, false)
  } else if (resizeDirection.value === 'left') {
    let newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
    const updateProperty = [fromCol.title!]

    if (dayjs(newStartDate).isAfter(ogEndDate)) {
      newStartDate = dayjs(dayjs(ogEndDate)).clone()
    }
    if (!newStartDate) return

    const newRow = {
      ...resizeRecord.value,
      row: {
        ...resizeRecord.value.row,
        [fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD'),
      },
    }

    formattedData.value = formattedData.value.map((r) => {
      const pk = extractPkFromRow(r.row, meta.value!.columns!)

      if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
        return newRow
      }
      return r
    })
    useDebouncedRowUpdate(newRow, updateProperty, false)
  }
}

const onResizeEnd = () => {
  resizeInProgress.value = false
  resizeDirection.value = null
  resizeRecord.value = null
  document.removeEventListener('mousemove', onResize)
  document.removeEventListener('mouseup', onResizeEnd)
}

const onResizeStart = (direction: 'right' | 'left', event: MouseEvent, record: Row) => {
  if (!isUIAllowed('dataEdit')) return
  resizeInProgress.value = true
  resizeDirection.value = direction
  resizeRecord.value = record
  document.addEventListener('mousemove', onResize)
  document.addEventListener('mouseup', onResizeEnd)
}

const onDrag = (event: MouseEvent) => {
  if (!isUIAllowed('dataEdit')) return
  if (!container.value || !dragRecord.value) return
  const { width, left } = container.value.getBoundingClientRect()

  const percentX = (event.clientX - left - window.scrollX) / width

  const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
  const toCol = dragRecord.value.rowMeta.range?.fk_to_col

  if (!fromCol) return

  const day = Math.min(Math.floor(percentX * 7), 6)

  const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
  if (!newStartDate) return

  let endDate

  const newRow = {
    ...dragRecord.value,
    row: {
      ...dragRecord.value.row,
      [fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD'),
    },
  }

  if (toCol) {
    const fromDate = dragRecord.value.row[fromCol.title!] ? dayjs(dragRecord.value.row[fromCol.title!]) : null
    const toDate = dragRecord.value.row[toCol.title!] ? dayjs(dragRecord.value.row[toCol.title!]) : null

    if (fromDate && toDate) {
      endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
    } else if (fromDate && !toDate) {
      endDate = dayjs(newStartDate).endOf('day')
    } else if (!fromDate && toDate) {
      endDate = dayjs(newStartDate).endOf('day')
    } else {
      endDate = newStartDate.clone()
    }

    newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD')
  }

  formattedData.value = formattedData.value.map((r) => {
    const pk = extractPkFromRow(r.row, meta.value!.columns!)

    if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
      return newRow
    }
    return r
  })
}

const stopDrag = (event: MouseEvent) => {
  event.preventDefault()
  clearTimeout(dragTimeout.value!)

  if (!isUIAllowed('dataEdit')) return
  if (!isDragging.value || !container.value || !dragRecord.value) return

  const { width, left } = container.value.getBoundingClientRect()

  const percentX = (event.clientX - left - window.scrollX) / width

  const fromCol = dragRecord.value.rowMeta.range?.fk_from_col
  const toCol = dragRecord.value.rowMeta.range?.fk_to_col

  const day = Math.floor(percentX * 7)

  const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')
  if (!newStartDate || !fromCol) return

  let endDate

  const newRow = {
    ...dragRecord.value,
    row: {
      ...dragRecord.value.row,
      [fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD'),
    },
  }

  const updateProperty = [fromCol.title!]

  if (toCol) {
    const fromDate = dragRecord.value.row[fromCol.title!] ? dayjs(dragRecord.value.row[fromCol.title!]) : null
    const toDate = dragRecord.value.row[toCol.title!] ? dayjs(dragRecord.value.row[toCol.title!]) : null

    if (fromDate && toDate) {
      endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
    } else if (fromDate && !toDate) {
      endDate = dayjs(newStartDate).endOf('day')
    } else if (!fromDate && toDate) {
      endDate = dayjs(newStartDate).endOf('day')
    } else {
      endDate = newStartDate.clone()
    }

    newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD')

    updateProperty.push(toCol.title!)
  }

  if (!newRow) return

  if (dragElement.value) {
    formattedData.value = formattedData.value.map((r) => {
      const pk = extractPkFromRow(r.row, meta.value!.columns!)

      if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
        return newRow
      }
      return r
    })
  } else {
    formattedData.value = [...formattedData.value, newRow]
    formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
      const pk = extractPkFromRow(r.row, meta.value!.columns!)

      return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
    })
  }

  const allRecords = document.querySelectorAll('.draggable-record')
  allRecords.forEach((el) => {
    el.style.visibility = ''
    el.style.opacity = '100%'
  })

  if (dragElement.value) {
    dragElement.value.style.boxShadow = 'none'
    dragElement.value.classList.remove('hide')
    // isDragging.value = false
    draggingId.value = null
    dragElement.value = null
  }

  updateRowProperty(newRow, updateProperty, false)

  document.removeEventListener('mousemove', onDrag)
  document.removeEventListener('mouseup', stopDrag)
}

const dragStart = (event: MouseEvent, record: Row) => {
  if (!isUIAllowed('dataEdit')) return
  if (resizeInProgress.value) return
  let target = event.target as HTMLElement

  isDragging.value = false

  dragTimeout.value = setTimeout(() => {
    isDragging.value = true
    while (!target.classList.contains('draggable-record')) {
      target = target.parentElement as HTMLElement
    }

    const allRecords = document.querySelectorAll('.draggable-record')
    allRecords.forEach((el) => {
      if (!el.getAttribute('data-unique-id').includes(record.rowMeta.id!)) {
        // el.style.visibility = 'hidden'
        el.style.opacity = '30%'
      }
    })

    dragRecord.value = record

    isDragging.value = true
    dragElement.value = target
    draggingId.value = record.rowMeta.id!
    dragRecord.value = record

    document.addEventListener('mousemove', onDrag)
    document.addEventListener('mouseup', stopDrag)
  }, 200)

  const onMouseUp = () => {
    clearTimeout(dragTimeout.value!)
    document.removeEventListener('mouseup', onMouseUp)
    if (!isDragging.value) {
      emits('expandRecord', record)
    }
  }

  document.addEventListener('mouseup', onMouseUp)
}

const dropEvent = (event: DragEvent) => {
  if (!isUIAllowed('dataEdit')) return
  event.preventDefault()

  const data = event.dataTransfer?.getData('text/plain')
  if (data) {
    const {
      record,
    }: {
      record: Row
    } = JSON.parse(data)
    const { width, left } = container.value.getBoundingClientRect()

    const percentX = (event.clientX - left - window.scrollX) / width

    const fromCol = record.rowMeta.range?.fk_from_col
    const toCol = record.rowMeta.range?.fk_to_col

    if (!fromCol) return

    const day = Math.floor(percentX * 7)

    const newStartDate = dayjs(selectedDateRange.value.start).add(day, 'day')

    let endDate

    const newRow = {
      ...record,
      row: {
        ...record.row,
        [fromCol.title!]: dayjs(newStartDate).format('YYYY-MM-DD'),
      },
    }

    const updateProperty = [fromCol.title!]

    if (toCol) {
      const fromDate = record.row[fromCol.title!] ? dayjs(record.row[fromCol.title!]) : null
      const toDate = record.row[toCol.title!] ? dayjs(record.row[toCol.title!]) : null

      if (fromDate && toDate) {
        endDate = dayjs(newStartDate).add(toDate.diff(fromDate, 'day'), 'day')
      } else if (fromDate && !toDate) {
        endDate = dayjs(newStartDate).endOf('day')
      } else if (!fromDate && toDate) {
        endDate = dayjs(newStartDate).endOf('day')
      } else {
        endDate = newStartDate.clone()
      }
      newRow.row[toCol.title!] = dayjs(endDate).format('YYYY-MM-DD')
      updateProperty.push(toCol.title!)
    }

    if (!newRow) return

    if (dragElement.value) {
      formattedData.value = formattedData.value.map((r) => {
        const pk = extractPkFromRow(r.row, meta.value!.columns!)

        if (pk === extractPkFromRow(newRow.row, meta.value!.columns!)) {
          return newRow
        }
        return r
      })
    } else {
      formattedData.value = [...formattedData.value, newRow]
      formattedSideBarData.value = formattedSideBarData.value.filter((r) => {
        const pk = extractPkFromRow(r.row, meta.value!.columns!)

        return pk !== extractPkFromRow(newRow.row, meta.value!.columns!)
      })
    }

    if (dragElement.value) {
      dragElement.value.style.boxShadow = 'none'
      dragElement.value.classList.remove('hide')

      dragElement.value = null
    }
    updateRowProperty(newRow, updateProperty, false)
  }
}
</script>

<template>
  <div class="flex relative flex-col prevent-select" data-testid="nc-calendar-week-view" @drop="dropEvent">
    <div class="flex">
      <div
        v-for="(date, weekIndex) in weekDates"
        :key="weekIndex"
        :class="{
          '!border-brand-500 !border-b-gray-200': dayjs(date).isSame(selectedDate, 'day'),
        }"
        class="w-1/7 text-center text-sm text-gray-500 w-full py-1 border-gray-200 border-l-gray-50 border-t-gray-50 last:border-r-0 border-1 bg-gray-50"
      >
        {{ dayjs(date).format('DD ddd') }}
      </div>
    </div>
    <div ref="container" class="flex h-[calc(100vh-11.6rem)]">
      <div
        v-for="(date, dateIndex) in weekDates"
        :key="dateIndex"
        :class="{
          '!border-1 !border-t-0 border-brand-500': dayjs(date).isSame(selectedDate, 'day'),
        }"
        class="flex flex-col border-r-1 min-h-[100vh] last:border-r-0 items-center w-1/7"
        data-testid="nc-calendar-week-day"
        @click="selectedDate = dayjs(date)"
      ></div>
    </div>
    <div
      class="absolute nc-scrollbar-md overflow-y-auto mt-9 pointer-events-none inset-0"
      data-testid="nc-calendar-week-record-container"
    >
      <div
        v-for="(record, id) in calendarData"
        :key="id"
        :data-testid="`nc-calendar-week-record-${record.row[displayField!.title!]}`"
        :data-unique-id="record.rowMeta.id"
        :style="{
          ...record.rowMeta.style,
          boxShadow:
            record.rowMeta.id === draggingId
              ? '0px 8px 8px -4px rgba(0, 0, 0, 0.04), 0px 20px 24px -4px rgba(0, 0, 0, 0.10)'
              : 'none',
        }"
        class="absolute group draggable-record pointer-events-auto nc-calendar-week-record-card"
        @mousedown="dragStart($event, record)"
        @mouseleave="hoverRecord = null"
        @mouseover="hoverRecord = record.rowMeta.id"
      >
        <LazySmartsheetRow :row="record">
          <LazySmartsheetCalendarRecordCard
            :hover="hoverRecord === record.rowMeta.id"
            :position="record.rowMeta.position"
            :record="record"
            :resize="!!record.rowMeta.range?.fk_to_col && isUIAllowed('dataEdit')"
            color="blue"
            @dblclick="emits('expand-record', record)"
            @resize-start="onResizeStart"
          >
            <template v-if="!isRowEmpty(record, displayField)">
              <div
                :class="{
                  '!mt-2': displayField.uidt === UITypes.SingleLineText,
                  '!mt-1': displayField.uidt === UITypes.MultiSelect || displayField.uidt === UITypes.SingleSelect,
                }"
              >
                <LazySmartsheetVirtualCell
                  v-if="isVirtualCol(displayField)"
                  v-model="record.row[displayField.title]"
                  :column="displayField"
                  :row="record"
                />

                <LazySmartsheetCell
                  v-else
                  v-model="record.row[displayField.title]"
                  :column="displayField"
                  :edit-enabled="false"
                  :read-only="true"
                />
              </div>
            </template>
          </LazySmartsheetCalendarRecordCard>
        </LazySmartsheetRow>
      </div>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.hide {
  transition: 0.01s;
  transform: translateX(-9999px);
}
.prevent-select {
  -webkit-user-select: none; /* Safari */
  -ms-user-select: none; /* IE 10 and IE 11 */
  user-select: none; /* Standard syntax */
}
</style>
