<!--
// Copyright © 2023 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
  import { Event, ReccuringInstance } from '@hcengineering/calendar'
  import { DocumentUpdate, Ref, Timestamp } from '@hcengineering/core'
  import { getClient } from '@hcengineering/presentation'
  import ui, {
    ActionIcon,
    CalendarItem,
    IconDownOutline,
    IconUpOutline,
    Label,
    MILLISECONDS_IN_DAY,
    Scroller,
    addZero,
    areDatesEqual,
    closeTooltip,
    deviceOptionsStore as deviceInfo,
    day as getDay,
    getMonday,
    getWeekDayName,
    resizeObserver,
    ticker,
    isWeekend
  } from '@hcengineering/ui'
  import { showMenu } from '@hcengineering/view-resources'
  import { createEventDispatcher, onDestroy, onMount } from 'svelte'
  import type {
    CalendarADGrid,
    CalendarADRows,
    CalendarCell,
    CalendarElement,
    CalendarElementRect,
    CalendarGrid
  } from '..'
  import calendar from '../plugin'
  import { isReadOnly, updateReccuringInstance } from '../utils'
  import EventElement from './EventElement.svelte'

  export let events: Event[]
  export let mondayStart = true
  export let selectedDate: Date = new Date()
  export let currentDate: Date = selectedDate
  export let displayedDaysCount = 7
  export let displayedHours = 24
  export let startHour = 0
  export let startFromWeekStart = true
  export let weekFormat: 'narrow' | 'short' | 'long' = 'long'
  export let showHeader: boolean = true
  export let showFooter: boolean = true
  export let clearCells: boolean = false
  export let dragItemId: Ref<Event> | null = null

  export let todayDate = new Date()

  const client = getClient()
  const dispatch = createEventDispatcher()

  $: checkToday($ticker)

  function checkToday (now: number): void {
    if (!areDatesEqual(todayDate, new Date())) {
      todayDate = new Date()
    }
  }

  const ampm = new Intl.DateTimeFormat([], { hour: 'numeric' }).resolvedOptions().hour12
  const getTimeFormat = (hour: number, min: number = 0): string => {
    if (min === 0) {
      return ampm
        ? `${hour > 12 ? hour - 12 : hour}${hour < 12 ? 'am' : 'pm'}`
        : `${addZero(hour === 24 ? 0 : hour)}:00`
    } else {
      return ampm
        ? `${hour > 12 ? hour - 12 : hour}:${addZero(min)}${hour < 12 ? 'am' : 'pm'}`
        : `${addZero(hour === 24 ? 0 : hour)}:${addZero(min)}`
    }
  }
  const rem = (n: number): number => n * fontSize
  const initDisplayedDaysCount = displayedDaysCount

  export function getCalendarRect (): DOMRect | undefined {
    return container ? calendarRect : undefined
  }

  const offsetTZ = new Date().getTimezoneOffset() * 60 * 1000

  const toCalendar = (
    events: Event[],
    date: Date,
    days: number = 1,
    startHour: number = 0,
    endHour: number = 24
  ): CalendarItem[] => {
    const result: CalendarItem[] = []
    for (let day = 0; day < days; day++) {
      const startDay = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(0, 0, 0, 0)
      const startDate = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(startHour, 0, 0, 0)
      const lastDate = new Date(MILLISECONDS_IN_DAY * day + date.getTime()).setHours(endHour, 0, 0, 0)
      events.forEach((event) => {
        const eventStart = event.allDay ? event.date + offsetTZ : event.date
        const eventEnd = event.allDay ? event.dueDate + offsetTZ : event.dueDate
        if ((eventStart < lastDate && eventEnd > startDate) || (eventStart === eventEnd && eventStart === startDay)) {
          result.push({
            _id: event._id,
            allDay: event.allDay,
            date: eventStart,
            dueDate: eventEnd,
            day,
            access: event.access
          })
        }
      })
    }
    const sd = date.setHours(0, 0, 0, 0)
    const ld = new Date(MILLISECONDS_IN_DAY * (days - 1) + date.getTime()).setHours(23, 59, 59, 999)
    events
      .filter((ev) => ev.allDay)
      .sort((a, b) => b.dueDate - b.date - (a.dueDate - a.date))
      .forEach((event) => {
        const eventStart = event.date + offsetTZ
        const eventEnd = event.dueDate + offsetTZ
        if ((eventStart < ld && eventEnd > sd) || (eventStart === eventEnd && eventStart === sd)) {
          result.push({
            _id: event._id,
            allDay: event.allDay,
            date: eventStart,
            dueDate: eventEnd,
            day: -1,
            access: event.access
          })
        }
      })
    return result
  }

  $: calendarEvents = toCalendar(events, weekMonday, displayedDaysCount, startHour, displayedHours + startHour)

  $: fontSize = $deviceInfo.fontSize
  $: docHeight = $deviceInfo.docHeight
  $: cellHeight = 4 * fontSize
  $: weekMonday = startFromWeekStart
    ? getMonday(currentDate, mondayStart)
    : new Date(new Date(currentDate).setHours(0, 0, 0, 0))

  let timer: any
  let container: HTMLElement
  let scroller: HTMLElement
  let calendarWidth: number = 0
  let calendarRect: DOMRect
  let containerRect: DOMRect
  let colWidth: number = 0
  let nowLineTop: number = -1
  let timeNow: string = '--:--'
  let nowLineStyle: string = ''
  let newEvents = calendarEvents
  let grid: CalendarGrid[] = Array<CalendarGrid>(displayedDaysCount)
  let alldays: CalendarItem[] = []
  let alldaysGrid: CalendarADGrid[] = Array<CalendarADGrid>(displayedDaysCount)
  let adMaxRow: number = 1
  let adRows: CalendarADRows[]
  const cellBorder: number = 1
  const stepsPerHour: number = 4
  const heightHeader: number = 4.75
  const heightAD: number = 2
  const minAD: number = 2
  const maxAD: number = 3
  let minHeightAD: number = 0
  let maxHeightAD: number = 0
  let shownHeightAD: number = 0
  let shownAD: boolean = false
  let shortAlldays: { id: string, day: number, fixRow?: boolean }[] = []
  let moreCounts: number[] = Array<number>(displayedDaysCount)
  const nullCalendarElement: CalendarElementRect = {
    top: 0,
    bottom: 0,
    left: 0,
    right: 0,
    width: 0,
    height: rem(heightAD),
    fit: true,
    visibility: 1
  }

  $: minimizedAD = maxHeightAD === minHeightAD && maxHeightAD !== 0

  const updateEvents = (events: CalendarItem[]): CalendarItem[] => {
    grid = new Array<CalendarGrid>(displayedDaysCount)
    alldaysGrid = new Array<CalendarADGrid>(displayedDaysCount)
    alldays = []
    shortAlldays = []
    moreCounts = new Array<number>(displayedDaysCount)
    if (shownAD && adMaxRow <= maxAD) shownAD = false
    prepareAllDays()

    events
      .filter((ev) => !ev.allDay)
      .forEach((event, i, arr) => {
        if (grid[event.day] === undefined) {
          grid[event.day] = {
            columns: [{ elements: [{ id: event._id, date: event.date, dueDate: event.dueDate, cols: 1 }] }]
          }
        } else {
          const index = grid[event.day].columns.findIndex(
            (col) => col.elements[col.elements.length - 1].dueDate <= event.date
          )
          const intersects = grid[event.day].columns.filter((col) =>
            checkIntersect(col.elements[col.elements.length - 1], event)
          )
          if (index === -1) {
            const size = intersects.length + 1
            grid[event.day].columns.forEach((col) => {
              if (checkIntersect(col.elements[col.elements.length - 1], event)) {
                col.elements[col.elements.length - 1].cols = size
              }
            })
            grid[event.day].columns.push({
              elements: [{ id: event._id, date: event.date, dueDate: event.dueDate, cols: size }]
            })
          } else {
            let maxCols = 1
            intersects.forEach((col) => {
              if (col.elements[col.elements.length - 1].cols > maxCols) {
                maxCols = col.elements[col.elements.length - 1].cols
              }
            })
            if (intersects.length >= maxCols) maxCols = intersects.length + 1
            grid[event.day].columns.forEach((col) => {
              if (checkIntersect(col.elements[col.elements.length - 1], event)) {
                col.elements[col.elements.length - 1].cols = maxCols
              }
            })
            grid[event.day].columns[index].elements.push({
              id: event._id,
              date: event.date,
              dueDate: event.dueDate,
              cols: maxCols
            })
          }
        }
        if (i === arr.length - 1) checkGrid()
      })
    return events
  }
  $: newEvents = updateEvents(calendarEvents)

  const checkGrid = () => {
    for (let i = 0; i < displayedDaysCount; i++) {
      if (grid[i] === null || typeof grid[i] !== 'object' || grid[i].columns.length === 0) continue
      for (let j = 0; j < grid[i].columns.length - 1; j++) {
        grid[i].columns[j].elements.forEach((el) => {
          grid[i].columns[j + 1].elements.forEach((nextEl) => {
            if (checkIntersect(el, nextEl)) {
              if (el.cols > nextEl.cols) nextEl.cols = el.cols
              else el.cols = nextEl.cols
            }
          })
        })
      }
    }
  }

  const addNullRow = () => {
    for (let i = 0; i < displayedDaysCount; i++) alldaysGrid[i].alldays.push(null)
    adMaxRow++
  }
  const prepareAllDays = () => {
    alldays = calendarEvents.filter((ev) => ev.allDay)
    shortAlldays = []
    adRows = []
    for (let i = 0; i < displayedDaysCount; i++) alldaysGrid[i] = { alldays: [null] }
    adMaxRow = 1
    alldays
      .filter((event) => event.day === -1)
      .forEach((event) => {
        const days = calendarEvents
          .filter((ev) => ev.allDay && ev.day !== -1 && event._id === ev._id)
          .map((ev) => {
            return ev.day
          })
        let emptyRow = 0
        for (let checkRow = 0; checkRow < adMaxRow; checkRow++) {
          const empty = days.every((day) => alldaysGrid[day].alldays[checkRow] === null)
          if (empty) {
            emptyRow = checkRow
            break
          } else if (checkRow === adMaxRow - 1) {
            emptyRow = adMaxRow
            addNullRow()
            break
          }
        }
        adRows.push({ id: event._id, row: emptyRow, startCol: days[0], endCol: days[days.length - 1] })
        days.forEach((day) => (alldaysGrid[day].alldays[emptyRow] = event._id))
      })
    const shown = minimizedAD ? minAD : maxAD
    let tempEventID: string = ''
    for (let r = 0; r < shown; r++) {
      const lastRow = r === shown - 1
      for (let d = 0; d < displayedDaysCount; d++) {
        if (!lastRow && tempEventID !== alldaysGrid[d].alldays[r] && alldaysGrid[d].alldays[r] !== null) {
          tempEventID = alldaysGrid[d].alldays[r] ?? ''
          if (tempEventID !== '') shortAlldays.push({ id: tempEventID, day: d })
        } else if (lastRow) {
          const filtered = alldaysGrid[d].alldays.slice(shown).filter((ev) => ev !== null)
          if (tempEventID !== alldaysGrid[d].alldays[r] && alldaysGrid[d].alldays[r] !== null) {
            if (filtered.length > 0) {
              tempEventID = ''
              moreCounts[d] = filtered.length + 1
            } else {
              tempEventID = alldaysGrid[d].alldays[r] ?? ''
              if (tempEventID !== '') shortAlldays.push({ id: tempEventID, day: d })
              moreCounts[d] = 0
            }
          } else if (alldaysGrid[d].alldays[r] === null) {
            tempEventID = ''
            if (filtered.length > 1) moreCounts[d] = filtered.length
            else {
              if (filtered.length === 1 && filtered[0] !== null) {
                shortAlldays.push({ id: filtered[0], day: d, fixRow: true })
              }
              moreCounts[d] = 0
            }
          } else if (alldaysGrid[d].alldays[r] === tempEventID) {
            if (filtered.length > 0) {
              tempEventID = ''
              moreCounts[d] = filtered.length + 1
            } else moreCounts[d] = 0
          }
        }
      }
    }
  }

  const checkIntersect = (date1: CalendarItem | CalendarElement, date2: CalendarItem | CalendarElement): boolean => {
    return (
      (date2.date <= date1.date && date2.dueDate > date1.date) ||
      (date2.date >= date1.date && date2.date < date1.dueDate)
    )
  }
  const convertToTime = (date: Timestamp | Date): { hours: number, mins: number } => {
    const temp = new Date(date)
    return { hours: temp.getHours() - startHour, mins: temp.getMinutes() }
  }

  const getGridOffset = (mins: number, end: boolean = false): number => {
    if (mins === 0) return end ? 2 + cellBorder : 2
    return mins < 2 ? (end ? 1 : 2) : mins >= 57 ? (end ? 1 + cellBorder : 1) : 1
  }

  const getRect = (event: CalendarItem): CalendarElementRect => {
    const result = { ...nullCalendarElement }
    const checkDate = new Date(weekMonday.getTime() + MILLISECONDS_IN_DAY * event.day)
    const startDay = checkDate.setHours(startHour, 0, 0, 0)
    const endDay = checkDate.setHours(displayedHours - 1, 59, 59, 999)
    const startTime = event.date <= startDay ? { hours: startHour, mins: 0 } : convertToTime(event.date)
    const endTime =
      event.dueDate > endDay ? { hours: displayedHours - startHour, mins: 0 } : convertToTime(event.dueDate)
    if (getDay(weekMonday, event.day).setHours(endTime.hours, endTime.mins, 0, 0) <= todayDate.getTime()) {
      result.visibility = 0
    }
    result.top =
      (showHeader ? rem(heightHeader) : 0) +
      styleAD +
      cellHeight * startTime.hours +
      (startTime.mins / 60) * cellHeight +
      getGridOffset(startTime.mins)
    result.bottom =
      (showFooter ? rem(2) + 1 : 0) +
      cellHeight * (displayedHours - startHour - endTime.hours - 1) +
      ((60 - endTime.mins) / 60) * cellHeight +
      getGridOffset(endTime.mins, true)
    let cols = 1
    let index: number = 0
    grid[event.day].columns.forEach((col, i) => {
      col.elements.forEach((el) => {
        if (el.id === event._id) {
          cols = el.cols
          index = i
        }
      })
    })
    const elWidth = (colWidth - rem(0.25) - (cols - 1) * rem(0.125)) / cols
    result.width = elWidth
    result.left = rem(3.5) + event.day * colWidth + index * elWidth + index * rem(0.125) + rem(0.125) + cellBorder
    result.right =
      (displayedDaysCount - event.day - 1) * colWidth +
      rem(0.125) +
      (cols - index - 1) * elWidth +
      (cols - index - 1) * rem(0.125)
    return result
  }
  const getADRect = (id: string, day?: number, fixRow?: boolean): CalendarElementRect => {
    const result = { ...nullCalendarElement }
    const index = adRows.findIndex((ev) => ev.id === id)

    const checkTime = new Date().setHours(0, 0, 0, 0)
    const startEvent = getDay(weekMonday, typeof day !== 'undefined' ? day : adRows[index].startCol).getTime()
    const endEvent = getDay(weekMonday, adRows[index].endCol).setHours(24)
    if (endEvent <= checkTime) result.visibility = 0
    else if (startEvent <= checkTime && endEvent > checkTime) {
      const eventTime = endEvent - startEvent
      const remained = endEvent - checkTime
      const proc = remained / eventTime
      result.visibility = proc
    }

    const shown = minimizedAD ? minAD : maxAD
    const row = fixRow ? shown - 1 : adRows[index].row
    const lastRow = row === shown - 1
    const fitting = !shownAD && lastRow && typeof day !== 'undefined'
    result.top = rem(0.125 + row * (heightAD + 0.125))
    if (fitting) {
      result.left = rem(0.125) + day * (colWidth + 0.125)
      if (day < adRows[index].endCol) {
        for (let d = day; d <= adRows[index].endCol; d++) {
          if (moreCounts[d] !== 0) {
            result.width = colWidth * (d - day) - rem(0.25)
            break
          } else if (d === adRows[index].endCol) result.width = colWidth + colWidth * (d - day) - rem(0.25)
        }
      } else result.width = colWidth - rem(0.25)
    } else if (fitting && (row > shown - 1 || (lastRow && moreCounts[day] > 0))) {
      result.fit = false
    } else {
      result.left = rem(0.125) + adRows[index].startCol * (colWidth + 0.125)
      const w = adRows[index].endCol - adRows[index].startCol
      result.width = colWidth + colWidth * w - rem(0.25)
    }
    return result
  }
  const getMask = (visibility: number): string => {
    return visibility === 0 || visibility === 1
      ? 'none'
      : `linear-gradient(-90deg, rgba(0, 0, 0, 1) ${visibility * 100}%, rgba(0, 0, 0, .4) ${visibility * 100}%)`
  }
  const getMore = (day: number): { top: number, left: number, width: number } => {
    const result = { top: 0, left: 0, width: 0 }
    const lastRow = (minimizedAD ? minAD : maxAD) - 1
    result.top = rem(0.125 + lastRow * (heightAD + 0.125))
    result.left = rem(0.125) + day * (colWidth + 0.125)
    result.width = colWidth - rem(0.25)
    return result
  }

  const getTimeZone = (): string => {
    return new Intl.DateTimeFormat([], { timeZoneName: 'short' }).format(Date.now()).split(' ')[1]
  }

  const getTopOffset = (date: Date): number => {
    const d = convertToTime(date)
    return (
      (showHeader ? rem(heightHeader) : 0) +
      styleAD +
      cellHeight * d.hours +
      (d.mins / 60) * cellHeight +
      (d.mins > 50 ? -getGridOffset(d.mins, true) : d.mins < 10 ? getGridOffset(d.mins) : 0)
    )
  }
  const renderNow = (): void => {
    const d = new Date()
    const n = convertToTime(d)
    nowLineStyle =
      n.hours <= startHour && n.mins < 10
        ? ' lowerLabel'
        : n.hours >= displayedHours + startHour - 1 && n.mins > 50
          ? ' upperLabel'
          : ''
    timeNow = getTimeFormat(n.hours, n.mins)
    nowLineTop = getTopOffset(d)
  }
  const checkNowLine = (): void => {
    if (timer) clearInterval(timer)
    let equal = false
    for (let i = 0; i < displayedDaysCount; i++) if (areDatesEqual(getDay(weekMonday, i), todayDate)) equal = true
    if (equal) {
      renderNow()
      timer = setInterval(() => {
        renderNow()
      }, 1000)
    } else nowLineTop = -1
  }
  export const scrollToTime = (date: Date): void => {
    const top = getTopOffset(date)
    const offset = (scroller.getBoundingClientRect().height - rem(heightAD + 0.25)) / 2
    scroller.scrollTo({ top: top - offset })
  }
  $: if (currentDate) checkNowLine()

  onMount(() => {
    minHeightAD = rem((heightAD + 0.125) * minAD + 0.25)
    if (container) {
      checkSizes(container)
      scrollToTime(todayDate)
    }
  })
  onDestroy(() => {
    if (timer) clearInterval(timer)
  })

  const calcColumnWidth = (calWidth: number): number => (calWidth - rem(3.5)) / displayedDaysCount
  const needDecColumnWidth = (calWidth: number): boolean => calcColumnWidth(calWidth) < 200 && displayedDaysCount > 1
  const needIncColumnWidth = (calWidth: number): boolean =>
    displayedDaysCount < initDisplayedDaysCount && (displayedDaysCount + 1) * 200 + rem(3.5) < calWidth

  const checkSizes = (element: HTMLElement | Element) => {
    calendarRect = element.getBoundingClientRect()
    calendarWidth = calendarRect.width
    while (needDecColumnWidth(calendarWidth) || needIncColumnWidth(calendarWidth)) {
      if (needDecColumnWidth(calendarWidth)) displayedDaysCount--
      else if (needIncColumnWidth(calendarWidth)) displayedDaysCount++
    }
    colWidth = calcColumnWidth(calendarWidth)
  }
  $: if (docHeight && calendarRect?.top) {
    const proc = ((docHeight - calendarRect.top) * 30) / 100
    const temp = rem((heightAD + 0.125) * Math.trunc(proc / rem(heightAD + 0.125)) + 0.25)
    maxHeightAD = Math.max(temp, minHeightAD)
    shownHeightAD = rem((heightAD + 0.125) * adMaxRow + 0.25)
  }
  $: styleAD = shownAD
    ? shownHeightAD > maxHeightAD
      ? maxHeightAD
      : shownHeightAD
    : rem((heightAD + 0.125) * adMaxRow + 0.25) > maxHeightAD && minimizedAD
      ? rem((heightAD + 0.125) * (adMaxRow <= minAD ? adMaxRow : minAD) + 0.25)
      : rem((heightAD + 0.125) * (adMaxRow <= maxAD ? adMaxRow : maxAD) + 0.25)
  $: showArrowAD = (!minimizedAD && adMaxRow > maxAD) || (minimizedAD && adMaxRow > minAD)
  $: headerHeight = (showHeader ? rem(heightHeader) : 0) + styleAD

  const getMinutes = (exactly: number): number => {
    const roundStep = 60 / stepsPerHour
    return Math.round(exactly / roundStep) * roundStep
  }

  const getExactly = (e: MouseEvent, correction: boolean = false): number => {
    return Math.round((e.offsetY * 60) / cellHeight) - (correction ? 15 : 0)
  }

  const getStickyMinutes = (
    minutes: number,
    exactly: number,
    day: Date,
    hour: number,
    skipId: Ref<Event> | null
  ): number => {
    const roundStep = 60 / stepsPerHour
    // try find event for stick
    if (exactly !== minutes) {
      const minMinutes = Math.floor(exactly / roundStep) * roundStep
      const maxMinutes = Math.ceil(exactly / roundStep) * roundStep
      const min = new Date(day).setHours(hour, minMinutes, 0, 0)
      const max = new Date(day).setHours(hour, maxMinutes, 0, 0)
      const target = new Date(day).setHours(hour, exactly, 0, 0)
      const events = newEvents.filter((ev) => ev._id !== skipId && !ev.allDay)
      if (events.length > 0) {
        const minutes: number[] = []
        for (const ev of events) {
          if (ev.date >= min && ev.date <= max) {
            minutes.push(convertToTime(ev.date).mins)
          }
          if (ev.dueDate >= min && ev.dueDate <= max) {
            minutes.push(convertToTime(ev.dueDate).mins)
          }
        }
        if (minutes.length > 0) {
          let nearest = minutes[0]
          for (let index = 1; index < minutes.length; index++) {
            const minute = minutes[index]
            if (Math.abs(minute - target) < Math.abs(nearest - target)) nearest = minute
          }
          return nearest
        }
      }
    }
    return minutes
  }

  let dragOnOld: CalendarCell | null = null
  let dragId: Ref<Event> | null = null
  let resizeId: Ref<Event> | null = null
  let directionResize: 'top' | 'bottom' | null
  let oldMins: number = 0
  let oldTime: number = -1
  let originDate: Timestamp = 0
  let originDueDate: Timestamp = 0
  let scrollTimer: any = null

  async function updateHandler (event: Event) {
    const update: DocumentUpdate<Event> = {}
    if (originDate !== event.date) update.date = event.date
    if (originDueDate !== event.dueDate) update.dueDate = event.dueDate
    if (Object.keys(update).length > 0) {
      if (event._class === calendar.class.ReccuringInstance) {
        const updated = await updateReccuringInstance(update, {
          ...event,
          date: originDate,
          dueDate: originDueDate
        } as unknown as ReccuringInstance)
        if (!updated) {
          event.date = originDate
          event.dueDate = originDueDate
          events = events
        }
      } else {
        await client.update(event, update)
      }
    }
  }
  async function mouseUpElement (e: MouseEvent): Promise<void> {
    window.removeEventListener('mouseup', mouseUpElement as any)
    const event = events.find((ev) => ev._id === resizeId)
    if (event !== undefined) await updateHandler(event)
    resizeId = directionResize = null
  }
  function mouseDownElement (e: MouseEvent, event: Event, direction: 'top' | 'bottom'): void {
    if (e.buttons !== 1) return
    e.stopPropagation()
    closeTooltip()
    resizeId = event._id
    directionResize = direction
    originDate = event.date
    originDueDate = event.dueDate
    containerRect = scroller.getBoundingClientRect()
    window.addEventListener('mouseup', mouseUpElement as any)
  }
  function mouseMoveElement (
    e: MouseEvent & { currentTarget: EventTarget & HTMLDivElement },
    day: Date,
    hour: number
  ): void {
    if (resizeId == null && directionResize == null) return
    const exactly = getExactly(e)
    const minutes = getMinutes(exactly)
    const mins: number = getStickyMinutes(minutes, exactly, day, hour + startHour, resizeId)
    if (oldMins === mins) return
    oldMins = mins
    const newDate = new Date(day).setHours(hour + startHour, mins, 0, 0)
    const index = events.findIndex((ev) => ev._id === resizeId)
    if (index === -1) return
    if (directionResize === 'top') {
      if (events[index].dueDate - newDate >= 15 * 60000) events[index].date = newDate
    } else {
      if (newDate - events[index].date >= 15 * 60000) events[index].dueDate = newDate
    }
    events = events

    if (!scrollTimer) {
      directionScroll(
        e.clientY < containerRect.y + headerHeight + 16 && scroller.scrollTop > 0
          ? 'top'
          : e.clientY > containerRect.bottom - 16 && scroller.scrollHeight - scroller.scrollTop > scroller.clientHeight
            ? 'bottom'
            : 'none'
      )
    }
  }
  const directionScroll = (direction: 'top' | 'bottom' | 'none'): void => {
    if (direction === 'none') return
    scrollTimer = setTimeout(() => (scrollTimer = null), 150)
    scroller.scrollBy({ top: direction === 'top' ? -cellHeight * 2 : cellHeight * 2, left: 0, behavior: 'smooth' })
  }

  const transparentImage = new Image(1, 1)
  transparentImage.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'

  function dragStartElement (e: DragEvent & { currentTarget: EventTarget & HTMLDivElement }, event: Event): void {
    if (isReadOnly(event) || event.allDay) return
    if (e.dataTransfer) {
      e.dataTransfer.setDragImage(transparentImage, 0, 0)
      e.dataTransfer.effectAllowed = 'move'
    }
    originDate = event.date
    originDueDate = event.dueDate
    dragOnOld = null
    oldTime = -1
    closeTooltip()
    setTimeout(() => (dragId = event._id), 50)
  }

  async function dragEndElement (e: DragEvent) {
    const event = events.find((ev) => ev._id === dragId)
    if (event !== undefined) await updateHandler(event)
    dragId = null
  }

  function dragDrop (e: DragEvent, day: Date, hourOfDay: number): void {
    const hour = hourOfDay + startHour
    const newTime = new Date(day).setHours(hour, getExactly(e, true), 0, 0)
    if (dragId) {
      if (oldTime === -1) oldTime = newTime
      const index = events.findIndex((ev) => ev._id === dragId)
      const diff = newTime - oldTime
      if (diff && index !== -1) {
        const diffTime = new Date(originDate + diff)
        const minutes = convertToTime(diffTime).mins
        const roundMinutes = getMinutes(minutes)
        const stickyMinutes = getStickyMinutes(roundMinutes, minutes, diffTime, diffTime.getHours(), dragId)
        const stickyTime = new Date(diffTime).setMinutes(stickyMinutes, 0, 0)
        const stickyDiff = stickyTime - originDate
        if (stickyDiff) {
          events[index].date = stickyTime
          events[index].dueDate = originDueDate + stickyDiff
        }
      }
      events.sort((a, b) => a.date - b.date)
    } else {
      const minutes = convertToTime(newTime).mins
      const roundMinutes = getMinutes(minutes)
      const stickyMinutes = getStickyMinutes(
        roundMinutes,
        minutes,
        new Date(newTime),
        new Date(newTime).getHours(),
        dragItemId
      )
      dispatch('dragDrop', {
        day,
        hour,
        date: new Date(new Date(newTime).setMinutes(stickyMinutes, 0, 0))
      })
    }
    dragOnOld = null
  }

  function dragOver (e: DragEvent, day: Date, hourOfDay: number): void {
    if (e.dataTransfer) {
      e.dataTransfer.setDragImage(transparentImage, 0, 0)
      e.dataTransfer.dropEffect = 'move'
    }
    e.preventDefault()
    const dragOn: CalendarCell = {
      day,
      hourOfDay,
      minutes: getExactly(e, true)
    }
    if (
      dragOnOld !== null &&
      areDatesEqual(dragOn.day, dragOnOld.day) &&
      dragOn.hourOfDay === dragOnOld.hourOfDay &&
      dragOn.minutes === dragOnOld.minutes
    ) {
      return
    }
    dragOnOld = dragOn
    const newTime = new Date(day).setHours(hourOfDay + startHour, dragOn.minutes, 0, 0)
    if (dragId) {
      if (oldTime === -1) oldTime = newTime
      const index = events.findIndex((ev) => ev._id === dragId)
      const diff = newTime - oldTime
      if (diff && index !== -1) {
        const diffTime = new Date(originDate + diff)
        const minutes = convertToTime(diffTime).mins
        const roundMinutes = getMinutes(minutes)
        const stickyMinutes = getStickyMinutes(roundMinutes, minutes, diffTime, diffTime.getHours(), dragId)
        const stickyTime = new Date(diffTime).setMinutes(stickyMinutes, 0, 0)
        const stickyDiff = stickyTime - originDate
        if (stickyDiff) {
          events[index].date = stickyTime
          events[index].dueDate = originDueDate + stickyDiff
        }
      }
      events.sort((a, b) => a.date - b.date)
    } else {
      const minutes = convertToTime(newTime).mins
      const roundMinutes = getMinutes(minutes)
      const stickyMinutes = getStickyMinutes(
        roundMinutes,
        minutes,
        new Date(newTime),
        new Date(newTime).getHours(),
        dragItemId
      )
      dispatch('dragEnter', { date: new Date(new Date(newTime).setMinutes(stickyMinutes, 0, 0)) })
    }
  }

  const dragLeave = (e: DragEvent): void => {
    dispatch('dragOut')
    e.preventDefault()
  }
</script>

<Scroller
  bind:divScroll={scroller}
  fade={{ multipler: { top: (showHeader ? heightHeader : 0) + styleAD / fontSize, bottom: 0 } }}
>
  <!-- svelte-ignore a11y-no-static-element-interactions -->
  <div
    bind:this={container}
    on:dragleave={dragLeave}
    on:dragleave
    class="calendar-container timeline-grid-bg"
    class:clearCells={clearCells || resizeId !== null || dragId !== null}
    class:cursor-row-resize={resizeId !== null && directionResize !== null}
    style:--calendar-ad-height={styleAD + 'px'}
    style:--calendar-header-height={heightHeader + 'rem'}
    style:grid={`${showHeader ? '[header] ' + heightHeader + 'rem ' : ''}[all-day] ${styleAD}px repeat(${
      (displayedHours - startHour) * 2
    }, [row-start] 2rem) / [time-col] 3.5rem repeat(${displayedDaysCount}, [col-start] 1fr)`}
    use:resizeObserver={(element) => {
      checkSizes(element)
    }}
  >
    {#if showHeader}
      <div class="sticky-header head center"><span class="zone">{getTimeZone()}</span></div>
      {#each [...Array(displayedDaysCount).keys()] as dayOfWeek}
        {@const day = getDay(weekMonday, dayOfWeek)}
        {@const tday = areDatesEqual(todayDate, day)}
        <div class="sticky-header head title" class:center={displayedDaysCount > 1}>
          <span class="day" class:today={tday}>{day.getDate()}</span>
          {#if tday}
            <div class="flex-col">
              <span class="today"><Label label={calendar.string.Today} /></span>
              <span class="weekday">{getWeekDayName(day, weekFormat)}</span>
            </div>
          {:else}
            <span class="weekday">{getWeekDayName(day, weekFormat)}</span>
          {/if}
        </div>
      {/each}
    {/if}

    <div class="sticky-header allday-header content-dark-color" class:top={!showHeader} class:opened={showArrowAD}>
      <div class="flex-center text-sm leading-3 text-center" style:height={`${heightAD + 0.25}rem`}>
        <Label label={calendar.string.AllDay} />
      </div>
      {#if showArrowAD}
        <ActionIcon
          icon={shownAD ? IconUpOutline : IconDownOutline}
          size={'small'}
          action={() => {
            shownAD = !shownAD
          }}
        />
      {/if}
    </div>
    <div
      class="sticky-header allday-container"
      class:top={!showHeader}
      style:grid-column={`col-start 1 / span ${displayedDaysCount}`}
    >
      {#if shownHeightAD > maxHeightAD && shownAD}
        <Scroller noFade={false}>
          {#key [styleAD, calendarWidth, displayedDaysCount, showHeader]}
            <div style:min-height={`${shownHeightAD - cellBorder * 2}px`} />
            {#each alldays as event, i}
              {@const rect = getADRect(event._id)}
              {@const ev = events.find((p) => p._id === event._id)}
              {#if ev}
                <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
                <div
                  class="calendar-element"
                  style:top={`${rect.top}px`}
                  style:height={`${rect.height}px`}
                  style:left={`${rect.left}px`}
                  style:width={`${rect.width}px`}
                  style:opacity={rect.visibility === 0 ? 0.4 : 1}
                  style:--mask-image={getMask(rect.visibility)}
                  tabindex={500 + i}
                >
                  <EventElement event={ev} size={{ width: rect.width, height: rect.height }} />
                </div>
              {/if}
            {/each}
          {/key}
        </Scroller>
      {:else if shownAD}
        {#key [styleAD, calendarWidth, displayedDaysCount, showHeader]}
          {#each alldays as event, i}
            {@const rect = getADRect(event._id)}
            {@const ev = events.find((p) => p._id === event._id)}
            {#if ev}
              <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
              <div
                class="calendar-element"
                style:top={`${rect.top}px`}
                style:height={`${rect.height}px`}
                style:left={`${rect.left}px`}
                style:width={`${rect.width}px`}
                style:opacity={rect.visibility === 0 ? 0.4 : 1}
                style:--mask-image={getMask(rect.visibility)}
                tabindex={500 + i}
              >
                <EventElement event={ev} size={{ width: rect.width, height: rect.height }} />
              </div>
            {/if}
          {/each}
        {/key}
      {:else}
        {#key [styleAD, calendarWidth, displayedDaysCount, showHeader]}
          {#each shortAlldays as event, i}
            {@const rect = getADRect(event.id, event.day, event.fixRow)}
            {@const ev = events.find((p) => p._id === event.id)}
            {#if ev}
              <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
              <div
                class="calendar-element"
                style:top={`${rect.top}px`}
                style:height={`${rect.height}px`}
                style:left={`${rect.left}px`}
                style:width={`${rect.width}px`}
                style:opacity={rect.visibility === 0 ? 0.4 : 1}
                style:--mask-image={getMask(rect.visibility)}
                tabindex={500 + i}
              >
                <EventElement event={ev} size={{ width: rect.width, height: rect.height }} />
              </div>
            {/if}
          {/each}
          {#each moreCounts as more, day}
            {@const addon = shortAlldays.length}
            {#if more !== 0}
              {@const rect = getMore(day)}
              <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
              <!-- svelte-ignore a11y-click-events-have-key-events -->
              <!-- svelte-ignore a11y-no-static-element-interactions -->
              <div
                class="calendar-element withPointer antiButton ghost medium accent cursor-pointer"
                style:top={`${rect.top}px`}
                style:height={`${heightAD}rem`}
                style:left={`${rect.left}px`}
                style:width={`${rect.width}px`}
                style:padding={'0 .5rem 0 1.25rem'}
                style:--mask-image={'none'}
                tabindex={500 + addon + day}
                on:click={() => (shownAD = true)}
              >
                <Label label={ui.string.MoreCount} params={{ count: more }} />
              </div>
            {/if}
          {/each}
        {/key}
      {/if}
    </div>

    {#each [...Array(displayedHours - startHour).keys()] as hourOfDay}
      {#if hourOfDay === 0}
        {#if showHeader}
          <div class="clear-cell" />
        {:else}
          <div class="sticky-header head center zm"><span class="zone mini">{getTimeZone()}</span></div>
        {/if}
      {:else if hourOfDay < displayedHours - startHour}
        <div
          class="time-cell"
          style:grid-column={'time-col 1 / col-start 1'}
          style:grid-row={`row-start ${hourOfDay * 2} / row-start ${hourOfDay * 2 + 2}`}
        >
          {getTimeFormat(hourOfDay + startHour)}
        </div>
      {/if}
      {#each [...Array(displayedDaysCount).keys()] as dayOfWeek}
        {@const day = getDay(weekMonday, dayOfWeek)}
        {@const weekend = isWeekend(day)}
        <!-- svelte-ignore a11y-click-events-have-key-events -->
        <div
          class="empty-cell"
          class:weekend
          style:width={`${colWidth}px`}
          style:grid-column={`col-start ${dayOfWeek + 1} / ${dayOfWeek + 2}`}
          style:grid-row={`row-start ${hourOfDay * 2 + 1} / row-start ${hourOfDay * 2 + 3}`}
          on:mousemove={(e) => {
            mouseMoveElement(e, day, hourOfDay)
          }}
          on:dragover={(e) => {
            dragOver(e, day, hourOfDay)
          }}
          on:drop|preventDefault={(e) => {
            dragDrop(e, day, hourOfDay)
          }}
          on:click|stopPropagation={() => {
            dispatch('create', {
              date: new Date(day.setHours(hourOfDay + startHour, 0, 0, 0)),
              withTime: true
            })
          }}
        />
      {/each}
      {#if hourOfDay === displayedHours - startHour - 1}
        {#if showFooter}
          <div
            class="time-cell"
            style:grid-column={'time-col 1 / col-start 1'}
            style:grid-row={`row-start ${hourOfDay * 2 + 2} / row-start ${hourOfDay * 2 + 4}`}
          >
            {getTimeFormat(displayedHours - startHour)}
          </div>
        {:else}
          <div class="clear-cell" />
        {/if}
      {/if}
    {/each}
    {#if showFooter}
      {#each [...Array(displayedDaysCount).keys()] as _}
        <div class="footer-cell" />
      {/each}
    {/if}

    {#key [styleAD, calendarWidth, displayedDaysCount, showHeader]}
      {#each newEvents.filter((ev) => !ev.allDay) as event, i}
        {@const rect = getRect(event)}
        {@const ev = events.find((p) => p._id === event._id)}
        {#if ev}
          <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
          <div
            class="calendar-element"
            class:past={rect.visibility === 0}
            style:top={`${rect.top}px`}
            style:bottom={`${rect.bottom}px`}
            style:left={`${rect.left}px`}
            style:right={`${rect.right}px`}
            style:--mask-image={'none'}
            draggable={!ev.allDay && !resizeId}
            tabindex={1000 + i}
            on:dragstart={(e) => {
              dragStartElement(e, ev)
            }}
            on:dragend={dragEndElement}
          >
            <div
              class="calendar-element-start"
              class:allowed={!resizeId && !dragId && !clearCells}
              class:hovered={resizeId === ev._id && directionResize === 'top'}
              on:mousedown={(e) => {
                mouseDownElement(e, ev, 'top')
              }}
              on:contextmenu={(e) => {
                showMenu(e, { object: ev })
              }}
            />
            <div
              class="calendar-element-end"
              class:allowed={!resizeId && !dragId && !clearCells}
              class:hovered={resizeId === ev._id && directionResize === 'bottom'}
              on:mousedown={(e) => {
                mouseDownElement(e, ev, 'bottom')
              }}
              on:contextmenu={(e) => {
                showMenu(e, { object: ev })
              }}
            />
            <EventElement
              event={ev}
              size={{
                width: rect.width,
                height: (calendarRect?.height ?? rect.top + rect.bottom) - rect.top - rect.bottom
              }}
            />
          </div>
        {/if}
      {/each}
    {/key}
    {#key currentDate}
      {#if nowLineTop !== -1}
        <div data-now={timeNow} class="now-line{nowLineStyle}" style:top={`${nowLineTop}px`} />
      {/if}
    {/key}
  </div>
</Scroller>

<style lang="scss">
  $timeline-bg-color: var(--theme-comp-header-color);
  $timeline-weekend-stroke-color: var(--theme-calendar-weekend-stroke-color);

  .timeline-grid-bg {
    background-image: linear-gradient(
      135deg,
      $timeline-weekend-stroke-color 10%,
      $timeline-bg-color 10%,
      $timeline-bg-color 50%,
      $timeline-weekend-stroke-color 50%,
      $timeline-weekend-stroke-color 60%,
      $timeline-bg-color 60%,
      $timeline-bg-color 100%
    );
    background-size: 7px 7px;
  }
  .calendar-container {
    will-change: transform;
    position: relative;
    display: grid;

    &.clearCells .empty-cell {
      position: relative;
      &::after {
        position: absolute;
        content: '';
        inset: -1px;
        z-index: 5;
      }
    }
    .now-line,
    .now-line::before {
      position: absolute;
      background-color: var(--highlight-red);
    }
    .now-line {
      left: 3rem;
      right: 0;
      width: 100%;
      height: 1px;
      min-height: 1px;
      max-height: 1px;
      pointer-events: none;
      z-index: 2;

      &::before {
        content: '';
        top: -0.5rem;
        left: -2.75rem;
        right: calc(100% - 0.25rem);
        height: 1rem;
        border-radius: 0.125rem;
      }
      &::after {
        position: absolute;
        content: attr(data-now);
        top: -0.5rem;
        left: -1.25rem;
        font-weight: 500;
        font-size: 0.75rem;
        color: var(--primary-button-color);
        transform: translateX(-50%);
      }
      &.upperLabel::before,
      &.upperLabel::after {
        top: -0.875rem;
      }
      &.lowerLabel::before,
      &.lowerLabel::after {
        top: -0.125rem;
      }
    }
    .footer-cell {
      height: 2rem;
      background-color: var(--theme-bg-color);
    }
  }
  .empty-cell {
    border-left: 1px solid var(--theme-divider-color);
    border-bottom: 1px solid var(--theme-divider-color);

    &:not(.weekend) {
      background-color: $timeline-bg-color;
    }
  }
  .time-cell {
    display: inline-flex;
    justify-content: center;
    align-items: center;
    font-size: 0.75rem;
    color: var(--theme-dark-color);
    background-color: $timeline-bg-color;
  }
  .clear-cell {
    background-color: $timeline-bg-color;
  }
  .calendar-element {
    position: absolute;
    mask-image: var(--mask-image, none);
    --webkit-mask-image: var(--mask-image, none);
    border-radius: 0.25rem;
    outline: none;

    &:not(.withPointer) {
      pointer-events: none;
    }
    &-start,
    &-end {
      position: absolute;
      left: 0;
      right: 0;
      height: 0.25rem;
      border-radius: 0.5rem;
      cursor: row-resize;

      &::after {
        position: absolute;
        content: '';
        left: -0.25rem;
        right: -0.25rem;
        height: 0.5rem;
        border: 1px solid transparent;
        transition-property: opacity, border-width, transform;
        transition-duration: 0.15s;
        transition-timing-function: var(--timing-main);
        transform: scale(0.9);
        opacity: 0;
        pointer-events: none;
        z-index: 10;
      }
      &.allowed::after {
        pointer-events: all;
        z-index: 100;
      }
      &.allowed:hover::after,
      &.hovered::after {
        border-width: 1px;
        transform: scale(1);
        opacity: 1;
      }
    }
    &-start {
      top: 0;
      &::after {
        top: -0.25rem;
        border-radius: 0.5rem 0.5rem 0 0;
        border-top-color: var(--primary-edit-border-color);
      }
    }
    &-end {
      bottom: 0;
      &::after {
        bottom: -0.25rem;
        border-radius: 0 0 0.5rem 0.5rem;
        border-bottom-color: var(--primary-edit-border-color);
      }
    }
  }
  :global(.calendar-element.past .event-container) {
    opacity: 0.4;
  }

  .sticky-header {
    position: sticky;
    background-color: var(--theme-comp-header-color);
    z-index: 15;

    &:not(.head) {
      top: calc(var(--calendar-header-height, 4.75rem) - 0.5px);
      border-top: 1px solid var(--theme-divider-color);
      border-bottom: 1px solid var(--theme-divider-color);
    }
    &.head,
    &.top {
      top: -0.5px;
    }
    &.zm {
      top: var(--calendar-ad-height, 2.25rem);
      mask-image: linear-gradient(to top, #0000, #000f 0.5rem, #000f calc(100% - 0.5rem), #0000 100%);
      z-index: 1;
    }
    &.head:not(.zm) {
      padding: var(--spacing-2_5) var(--spacing-3) var(--spacing-2);
    }
    &.center {
      justify-content: center;
    }
    &.title {
      gap: var(--spacing-2);
      font-weight: 500;
      font-size: 0.875rem;
      line-height: 1rem;
      color: var(--input-PlaceholderColor);

      &.center {
        justify-content: center;
      }
      &:not(.center) {
        padding-left: var(--spacing-3);
      }

      .today {
        font-weight: 700;
        color: var(--global-accent-TextColor);

        & + .weekday {
          color: var(--global-primary-TextColor);
        }
      }
      .day {
        display: flex;
        justify-content: center;
        align-items: center;
        width: var(--global-medium-Size);
        height: var(--global-medium-Size);
        font-weight: 500;
        font-size: 1.5rem;
        line-height: 1.75rem;
        border-radius: var(--medium-BorderRadius);

        &:not(.today) {
          color: var(--global-primary-TextColor);
        }
        &.today {
          font-weight: 500;
        }
      }
      .weekday::first-letter {
        text-transform: uppercase;
      }
    }
    &:not(.allday-container) {
      display: flex;
      align-items: center;
    }
    &.allday-header {
      flex-direction: column;
      justify-content: space-between;
      align-items: center;
      padding: 0 0.125rem;

      &.opened {
        padding: 0 0.125rem 0.625rem;
      }
    }
    &.allday-container {
      overflow: hidden;
    }

    .zone {
      padding: 0.375rem;
      font-size: 0.625rem;
      color: var(--theme-dark-color);
      background-color: rgba(64, 109, 223, 0.1);
      border-radius: 0.25rem;

      &.mini {
        padding: 0.125rem;
      }
    }
  }
</style>
