<!--
// Copyright © 2022 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 { fly } from 'svelte/transition'
  import { Timestamp } from '@hcengineering/core'
  import { TimelinePoint, TimelineRow, TimelineState } from '../types'
  import ui, {
    CheckBox,
    Icon,
    Scroller,
    Button,
    resizeObserver,
    MILLISECONDS_IN_DAY,
    MILLISECONDS_IN_WEEK,
    IconArrowLeft,
    IconArrowRight,
    IconAdd
  } from '..'
  import { createEventDispatcher, onMount } from 'svelte'

  export let selectedRows: number[] = []
  export let selectedRow: number | undefined = undefined
  export let lines: TimelineRow[] | undefined = undefined
  export let currentTime: Timestamp = new Date().setHours(0, 0, 0, 0)

  const dispatch = createEventDispatcher()
  const NOT_ENDED = MILLISECONDS_IN_WEEK * 4
  let currentDate: Date = new Date(currentTime)
  $: currentDate = new Date(currentTime)

  export const onObjectChecked = (row: number, value: boolean) => {
    dispatch('check', { row, value })
  }
  export const selectRow = (row: number) => {
    selectedRow = row
  }
  const handleRowFocused = (row: number) => {
    dispatch('row-focus', row)
  }

  let panelWidth: number = 320
  const dayWidth: number = 5
  let container: HTMLElement
  let viewbox: HTMLElement
  let scroller: Scroller
  let scrollDir: 'horizontal' | 'vertical' | 'none' = 'none'

  const locale = new Intl.NumberFormat().resolvedOptions().locale
  const nillPoint: TimelinePoint = { label: '', date: currentDate, x: 0 }
  const nillRect: DOMRect = { x: 0, y: 0, width: 0, height: 0, left: 0, right: 0, top: 0, bottom: 0, toJSON: () => {} }
  const time: TimelineState = {
    todayMarker: nillPoint,
    offsetView: 0,
    renderedRange: { left: nillPoint, right: nillPoint, firstDays: [] },
    rows: undefined,
    months: [],
    days: [],
    timelineBox: nillRect,
    viewBox: nillRect
  }

  const checkRange = (reverse: boolean) => {
    if (reverse) {
      if (time.offsetView * -1 - time.viewBox.width <= time.renderedRange.left.x) renderPrevMonth()
    } else {
      if (time.offsetView * -1 + time.viewBox.width * 2 >= time.renderedRange.right.x) renderNextMonth()
    }
  }

  const getDateByOffset = (x: number): { date: Date; delta: number } => {
    const deltaDays = Math.floor(x / dayWidth)
    const calcDay = new Date(currentTime + deltaDays * MILLISECONDS_IN_DAY)
    return { date: calcDay, delta: deltaDays }
  }
  const getOffsetByDate = (date: Timestamp | Date): number => {
    const tempDay = new Date(date).setHours(0, 0, 0, 0)
    const deltaDays = Math.floor((tempDay - currentTime) / MILLISECONDS_IN_DAY)
    return deltaDays * dayWidth
  }
  const getNextMonth = (date: Date): TimelinePoint => {
    const fDate = new Date(date.getFullYear(), date.getMonth() + 1, 1, 0, 0)
    const offDate = getOffsetByDate(fDate)
    const lDate = Intl.DateTimeFormat(locale, { month: 'long' }).format(fDate)
    return { date: fDate, x: offDate, label: lDate }
  }
  const getNextWeek = (date: Date, reverse?: boolean): TimelinePoint => {
    const fDate = new Date(date.getTime() + MILLISECONDS_IN_WEEK * (reverse ? -1 : 1))
    const offDate = getOffsetByDate(fDate)
    const lDate = fDate.getDate().toString()
    return { date: fDate, x: offDate, label: lDate }
  }

  const renderPrevMonth = () => {
    const oldRange: TimelinePoint = time.renderedRange.left
    const newDate: Date = new Date(oldRange.date.getFullYear(), oldRange.date.getMonth() - 1, 1, 0, 0)
    const newRange: number = getOffsetByDate(newDate)
    const newLabel: string = Intl.DateTimeFormat(locale, { month: 'long' }).format(newDate)
    const newPoint: TimelinePoint = {
      x: newRange,
      date: newDate,
      label: newLabel
    }
    time.renderedRange.left = newPoint
    time.months = [newPoint, ...time.months]
    while (getNextWeek(time.days[0].date, true).x > newPoint.x) {
      const prevDay: TimelinePoint = getNextWeek(time.days[0].date, true)
      time.days = [prevDay, ...time.days]
    }
  }
  const renderNextMonth = () => {
    const oldRange: TimelinePoint = time.renderedRange.right
    const newDate: Date = new Date(oldRange.date.getFullYear(), oldRange.date.getMonth() + 1, 1, 0, 0)
    const newRange: number = getOffsetByDate(newDate)
    const newLabel: string = Intl.DateTimeFormat(locale, { month: 'long' }).format(newDate)
    const newPoint: TimelinePoint = {
      x: newRange,
      date: newDate,
      label: newLabel
    }
    time.renderedRange.right = newPoint
    time.months = [...time.months, newPoint]
    while (getNextWeek(time.days[time.days.length - 1].date).x < newPoint.x) {
      const nextDay: TimelinePoint = getNextWeek(time.days[time.days.length - 1].date)
      time.days = [...time.days, nextDay]
    }
  }

  const wheelEvent = (e: WheelEvent) => {
    e = e || window.event
    const deltaX = -e.deltaX
    const deltaY = e.deltaY
    if (scrollDir === 'none' && (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2)) {
      if (Math.abs(deltaX) > Math.abs(deltaY)) scrollDir = 'horizontal'
      else scrollDir = 'vertical'
    } else if (Math.abs(deltaX) <= 4 && Math.abs(deltaY) <= 4) scrollDir = 'none'
    time.offsetView += deltaX
    if (scrollDir === 'horizontal') {
      mouseMoveEvent(e)
      checkRange(deltaX > 0)
    }
    if (scrollDir === 'vertical') scroller.scrollBy(deltaY)
    e.preventDefault ? e.preventDefault() : (e.returnValue = false)
  }
  const mouseMoveEvent = (e: MouseEvent) => {
    const cur = e.x - time.viewBox.left
    if (cur >= 0 && cur <= time.viewBox.width) {
      const offset = cur - time.offsetView
      const t = getDateByOffset(offset)
      time.cursorMarker = {
        label: t.date.getDate().toString(),
        x: offset,
        date: t.date
      }
    }
  }
  const mouseOutEvent = (e: MouseEvent) => {
    time.cursorMarker = undefined
  }
  const clickEvent = (e: MouseEvent) => {
    console.log('[Timeline] Cursor: ', time.cursorMarker)
  }

  onMount(() => {
    container.addEventListener('wheel', wheelEvent)
    container.addEventListener('mousemove', mouseMoveEvent)
    container.addEventListener('mouseout', mouseOutEvent)
    container.addEventListener('click', clickEvent)

    time.timelineBox = container.getBoundingClientRect()
    time.viewBox = viewbox.getBoundingClientRect()
    time.offsetView = Math.floor(time.viewBox.width / 2)
    time.todayMarker.x = 0
    time.todayMarker.date = currentDate
    time.todayMarker.label = Intl.DateTimeFormat(locale, { day: 'numeric', month: 'short' }).format(currentDate)

    let mass: number[] = [currentTime]
    lines?.forEach((line) => {
      if (line.items !== undefined) {
        let tr: number[] = []
        line.items.forEach((it) => {
          if (it.startDate) tr = [...tr, it.startDate]
          if (it.targetDate) tr = [...tr, it.targetDate]
          else if (it.startDate) tr = [...tr, it.startDate + NOT_ENDED]
        })
        if (tr.length > 0) {
          mass = [...mass, ...tr]
          tr.sort()
          const minD: Date = new Date(tr[0])
          const maxD: Date = new Date(tr[tr.length - 1])
          const r = {
            min: {
              date: minD,
              x: getOffsetByDate(minD)
            },
            max: {
              date: maxD,
              x: getOffsetByDate(maxD)
            }
          }
          time.rows ? time.rows.push(r) : (time.rows = [r])
        } else time.rows ? time.rows.push(null) : (time.rows = [null])
      } else time.rows ? time.rows.push(null) : (time.rows = [null])
    })
    mass.sort()

    let leftRange: number = getOffsetByDate(mass[0]) - time.viewBox.width * 1.5
    const leftDate: Date = new Date(getDateByOffset(leftRange).date.setDate(1))
    leftRange = getOffsetByDate(leftDate)
    time.renderedRange.left = {
      x: leftRange,
      date: leftDate,
      label: Intl.DateTimeFormat(locale, { month: 'long' }).format(leftDate)
    }
    let rightRange: number = getOffsetByDate(mass[mass.length - 1]) + time.viewBox.width * 1.5
    const tr: Date = new Date(getDateByOffset(rightRange).date)
    const rightDate: Date = new Date(new Date(tr.getFullYear(), tr.getMonth() + 1, 1, 0, 0).getTime() - 1)
    rightRange = getOffsetByDate(rightDate)
    time.renderedRange.right = {
      x: rightRange,
      date: rightDate,
      label: Intl.DateTimeFormat(locale, { month: 'long' }).format(rightDate)
    }

    time.months = [time.renderedRange.left]
    let i = 0
    do {
      const nextMonth: TimelinePoint = getNextMonth(time.months[i].date)
      time.months = [...time.months, nextMonth]
      i++
    } while (getNextMonth(time.months[i].date).x <= time.renderedRange.right.x)
    time.days = [
      {
        x: time.renderedRange.left.x,
        date: time.renderedRange.left.date,
        label: '1'
      }
    ]
    i = 0
    do {
      const nextWeek: TimelinePoint = getNextWeek(time.days[i].date)
      time.days = [...time.days, nextWeek]
      i++
    } while (getNextWeek(time.days[i].date).x <= time.renderedRange.right.x)
  })

  let moving: boolean = false
  let sX: number
  const splitterStart = (e: MouseEvent) => {
    if (time.timelineBox.width <= 450) return
    sX = (e.x - time.viewBox.left) * -1
    document.addEventListener('mouseup', splitterEnd)
    document.addEventListener('mousemove', splitterMove)
    moving = true
  }
  const splitterMove = (e: MouseEvent) => {
    if (e.x - time.timelineBox.left + sX < 300) panelWidth = 300
    else if (time.timelineBox.right - e.x + sX < 150) panelWidth = time.timelineBox.width - 150
    else panelWidth = e.x - time.timelineBox.left + sX
  }
  const splitterEnd = (e: MouseEvent) => {
    document.removeEventListener('mousemove', splitterMove)
    document.removeEventListener('mouseup', splitterEnd)
    time.viewBox = viewbox.getBoundingClientRect()
    moving = false
  }
</script>

<div
  class="timeline-container"
  bind:this={container}
  use:resizeObserver={() => {
    time.timelineBox = container.getBoundingClientRect()
    time.viewBox = viewbox.getBoundingClientRect()
  }}
>
  <div class="timeline-header">
    <div class="timeline-header__title" style:width={`${panelWidth}px`}>
      <Button
        label={ui.string.Today}
        on:click={() => {
          time.offsetView = Math.floor(time.viewBox.width / 2)
        }}
      />
    </div>
    <div class="timeline-header__time" bind:this={viewbox}>
      <div class="timeline-header__time-content" style:transform={`translateX(${time.offsetView}px)`}>
        {#if time.months}
          {#each time.months as month}
            <div class="month firstLetter" style:left={`${month.x}px`}>
              {#if month.date.getMonth() === 0}
                <b class="caption-color">{month.date.getFullYear()}</b>
              {/if}
              <span style="firstLetter">{month.label}</span>
            </div>
          {/each}
        {/if}
        {#if time.days}
          {#each time.days as day}
            <div class="day" style:left={`${day.x}px`}>{day.label}</div>
          {/each}
        {/if}
        <div class="cursor" style:left={`${time.todayMarker.x}px`}>{time.todayMarker.date.getDate()}</div>
        <!-- {#if time.cursorMarker}
          <div class="cursor" style:left={`${time.cursorMarker.x}px`}>{time.cursorMarker.label}</div>
        {/if} -->
      </div>
    </div>
  </div>
  <div class="timeline-background__headers" style:width={`${panelWidth}px`} />
  <div class="timeline-background__viewbox" style:left={`${panelWidth}px`}>
    <div class="timeline-wrapped_content" style:transform={`translateX(${time.offsetView}px)`}>
      {#if time.months}
        {#each time.months as month}
          <div class="monthMarker" style:left={`${month.x}px`} />
        {/each}
      {/if}
    </div>
  </div>
  {#if lines}
    <Scroller bind:this={scroller}>
      {#each lines as line, row}
        {@const rangeRow = time.rows ? time.rows[row] : null}
        <div
          class="listGrid"
          class:mListGridChecked={selectedRows.find((x) => x === row) !== undefined}
          class:mListGridSelected={selectedRow === row}
          on:focus={() => {}}
          on:mousemove={(ev) => {
            if (row !== selectedRow) {
              handleRowFocused(row)
            }
            ev.preventDefault()
          }}
        >
          <div class="headerWrapper" style:width={`${panelWidth}px`}>
            <div class="gridElement">
              <div class="eListGridCheckBox">
                <CheckBox
                  checked={selectedRows.filter((i) => i === row).length > 0}
                  on:value={(event) => onObjectChecked(row, event.detail)}
                />
              </div>
            </div>
            <slot {row} />
          </div>
          <div class="contentWrapper" class:nullRow={rangeRow === null && !moving}>
            <div class="timeline-wrapped_content" style:transform={`translateX(${time.offsetView}px)`}>
              {#if line.items}
                {#each line.items as item}
                  {#if item.startDate}
                    {@const target = item.targetDate ?? item.startDate + NOT_ENDED}
                    <div
                      class="project-item"
                      class:noTarget={item.targetDate === null}
                      style:left={`${getOffsetByDate(item.startDate)}px`}
                      style:right={`${getOffsetByDate(target) + dayWidth - 1}px`}
                      style:width={`${getOffsetByDate(target) - getOffsetByDate(item.startDate) + dayWidth - 1}px`}
                    >
                      <div class="project-presenter gap-2">
                        {#if item.icon}<Icon
                            icon={item.icon}
                            size={item.iconSize ?? 'small'}
                            iconProps={item.iconProps}
                          />{/if}
                        {#if item.presenter}<svelte:component this={item.presenter} {...item.props} />{/if}
                        {#if item.label}<span>{item.label}</span>{/if}
                      </div>
                    </div>
                  {/if}
                {/each}
              {/if}
            </div>
            {#if line.items}
              {#if rangeRow !== null && -time.offsetView + time.viewBox.width < rangeRow.min.x}
                <button
                  transition:fly={{ duration: 150, x: 50, opacity: 0 }}
                  class="timeline-action__button right"
                  on:click={() => {
                    if (rangeRow !== null) time.offsetView = -getOffsetByDate(rangeRow.min.date) + dayWidth * 5
                  }}
                >
                  <IconArrowRight size={'small'} />
                </button>
              {/if}
              {#if rangeRow !== null && -time.offsetView > rangeRow.max.x}
                <button
                  transition:fly={{ duration: 150, x: -50, opacity: 0 }}
                  class="timeline-action__button left"
                  on:click={() => {
                    if (rangeRow !== null) time.offsetView = -getOffsetByDate(rangeRow.min.date) + dayWidth * 5
                  }}
                >
                  <IconArrowLeft size={'small'} />
                </button>
              {/if}
            {/if}
            {#if rangeRow === null && selectedRow === row && time.cursorMarker && !moving}
              <button class="timeline-action__button add" style:left={`${time.offsetView + time.cursorMarker.x}px`}>
                <IconAdd size={'small'} />
              </button>
            {/if}
          </div>
        </div>
      {/each}
    </Scroller>
    <div class="timeline-foreground__viewbox" style:left={`${panelWidth}px`}>
      <div class="timeline-wrapped_content" style:transform={`translateX(${time.offsetView}px)`}>
        <div class="todayMarker" style:left={`${time.todayMarker.x}px`} />
      </div>
    </div>
  {/if}
  <div class="timeline-splitter" class:moving style:left={`${panelWidth}px`} on:mousedown={splitterStart} />
</div>

<style lang="scss">
  .timeline-container {
    overflow: hidden;
    position: relative;
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
    min-width: 0;
    min-height: 0;

    & > * {
      overscroll-behavior-x: contain;
    }
  }
  .timeline-header {
    display: flex;
    align-items: center;
    min-height: 4rem;
    border-bottom: 1px solid var(--divider-color);
  }
  .timeline-header__title {
    display: flex;
    align-items: center;
    flex-shrink: 0;
    padding: 0 2.25rem;
    height: 100%;
    background-color: var(--body-accent);
    box-shadow: var(--accent-shadow);
    // z-index: 2;
  }
  .timeline-header__time {
    // overflow: hidden;
    position: relative;
    flex-grow: 1;
    height: 100%;
    background-color: var(--body-color);
    mask-image: linear-gradient(
      90deg,
      rgba(0, 0, 0, 0) 0,
      rgba(0, 0, 0, 1) 2rem,
      rgba(0, 0, 0, 1) calc(100% - 2rem),
      rgba(0, 0, 0, 0) 100%
    );

    &-content {
      width: 100%;
      height: 100%;
      will-change: transform;

      .day,
      .month {
        position: absolute;
        pointer-events: none;
      }
      .month {
        width: max-content;
        top: 0.25rem;
        font-size: 1rem;
        color: var(--accent-color);

        &:first-letter {
          text-transform: uppercase;
        }
      }
      .day {
        bottom: 0.5rem;
        font-size: 1rem;
        color: var(--content-color);
        transform: translateX(-50%);
      }
      .cursor {
        position: absolute;
        display: flex;
        justify-content: center;
        align-items: center;
        padding-bottom: 1px;
        width: 1.75rem;
        height: 1.75rem;
        bottom: 0.375rem;
        font-size: 1rem;
        font-weight: 600;
        color: #fff;
        background-color: var(--primary-bg-color);
        border-radius: 50%;
        transform: translateX(-50%);
        pointer-events: none;
      }
    }
  }
  .todayMarker,
  .monthMarker {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 0;
    height: 100%;
    pointer-events: none;
  }
  .monthMarker {
    border-left: 1px dashed var(--highlight-select);
  }
  .todayMarker {
    border-left: 1px solid var(--primary-bg-color);
  }

  .timeline-background__headers,
  .timeline-background__viewbox,
  .timeline-foreground__viewbox {
    overflow: hidden;
    position: absolute;
    top: 4rem;
    bottom: 0;
    height: 100%;
    z-index: -1;
  }
  .timeline-background__headers {
    left: 0;
    background-color: var(--body-accent);
  }
  .timeline-background__viewbox,
  .timeline-foreground__viewbox {
    right: 0;
    mask-image: linear-gradient(
      90deg,
      rgba(0, 0, 0, 0) 0,
      rgba(0, 0, 0, 1) 2rem,
      rgba(0, 0, 0, 1) calc(100% - 2rem),
      rgba(0, 0, 0, 0) 100%
    );
  }
  .timeline-foreground__viewbox {
    z-index: 1;
    pointer-events: none;
  }

  .timeline-splitter,
  .timeline-splitter::before {
    position: absolute;
    top: 0;
    bottom: 0;
    height: 100%;
    transform: translateX(-50%);
  }
  .timeline-splitter {
    width: 1px;
    background-color: var(--divider-color);
    cursor: col-resize;
    z-index: 3;
    transition-property: width, background-color;
    transition-timing-function: var(--timing-main);
    transition-duration: 0.1s;
    transition-delay: 0s;

    &:hover {
      width: 3px;
      background-color: var(--button-border-hover);
      transition-duration: 0.15s;
      transition-delay: 0.3s;
    }
    &::before {
      content: '';
      width: 10px;
      left: 50%;
    }
    &.moving {
      width: 2px;
      background-color: var(--primary-edit-border-color);
      transition-duration: 0.1s;
      transition-delay: 0s;
    }
  }

  .headerWrapper {
    display: flex;
    align-items: center;
    height: 100%;
    min-width: 0;
    padding-left: 0.75rem;
    padding-right: 1.15rem;
    // border-bottom: 1px solid var(--accent-bg-color);
  }
  .contentWrapper {
    overflow: hidden;
    position: relative;
    display: flex;
    align-items: center;
    flex-grow: 1;
    height: 100%;
    min-width: 0;
    min-height: 0;
    mask-image: linear-gradient(
      90deg,
      rgba(0, 0, 0, 0) 0,
      rgba(0, 0, 0, 1) 2rem,
      rgba(0, 0, 0, 1) calc(100% - 2rem),
      rgba(0, 0, 0, 0) 100%
    );

    &.nullRow {
      cursor: pointer;
    }
  }
  .timeline-wrapped_content {
    width: 100%;
    height: 100%;
    min-width: 0;
    min-height: 0;
    will-change: transform;
  }

  .timeline-action__button,
  .project-item {
    position: absolute;
    display: flex;
    align-items: center;
    padding: 0.5rem;
    box-shadow: var(--button-shadow);
  }
  .project-item {
    top: 0.25rem;
    bottom: 0.25rem;
    background-color: var(--button-bg-color);
    border: 1px solid var(--button-border-color);
    border-radius: 0.75rem;

    &:hover {
      background-color: var(--button-bg-hover);
      border-color: var(--button-border-hover);
    }
    &.noTarget {
      mask-image: linear-gradient(to left, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 1) 2rem);
      border-right-color: transparent;
    }

    .project-presenter {
      display: flex;
      align-items: center;

      .space {
        flex-shrink: 0;
        width: 0.25rem;
        min-width: 0.25rem;
        max-width: 0.25rem;
      }
    }
  }
  .timeline-action__button {
    top: 0.625rem;
    bottom: 0.625rem;
    width: 2rem;
    color: var(--content-color);
    background-color: var(--button-bg-color);
    border: 1px solid var(--button-border-color);
    border-radius: 0.5rem;

    // color: var(--caption-color);
    // font-size: 0.65rem;
    // font-weight: 600;

    &:hover {
      color: var(--accent-color);
      background-color: var(--button-bg-hover);
      border-color: var(--button-border-hover);
    }

    &.left {
      left: 1rem;
    }
    &.right {
      right: 1rem;
    }
    &.add {
      transform: translateX(-50%);
      pointer-events: none;
    }
  }

  .listGrid {
    display: flex;
    justify-content: stretch;
    align-items: center;
    flex-shrink: 0;
    width: 100%;
    height: 3.25rem;
    min-height: 0;
    color: var(--caption-color);
    z-index: 2;

    &.mListGridChecked {
      .headerWrapper {
        background-color: var(--highlight-select);
      }
      .contentWrapper {
        background-color: var(--trans-content-05);
      }
      .eListGridCheckBox {
        opacity: 1;
      }
    }

    &.mListGridSelected {
      .headerWrapper {
        background-color: var(--highlight-select-hover);
      }
      .contentWrapper {
        background-color: var(--trans-content-10);
      }
    }

    .eListGridCheckBox {
      display: flex;
      align-items: center;
      justify-content: center;
      opacity: 0;
    }

    &:hover .eListGridCheckBox {
      opacity: 1;
    }
  }

  .filler {
    display: flex;
    flex-grow: 1;
  }

  .gridElement {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    margin-left: 0.5rem;

    &:first-child {
      margin-left: 0;
    }
  }

  .iconPresenter {
    padding-left: 0.45rem;
  }

  .projectPresenter {
    display: flex;
    align-items: center;
    flex-shrink: 0;
    width: 5.5rem;
    margin-left: 0.5rem;
  }
</style>
