<!-- Source: https://svelte.dev/repl/7f0042a186ee4d8e949c46ca663dbe6c?version=3.33.0 -->

<script lang="ts">
  import { createEventDispatcher } from "svelte";
  import { kbd } from "@melt-ui/svelte/internal/helpers";

  import { clamp } from "$lib/util/functions";

  // Props
  export let min = 0;
  export let max = 100;
  export let value = 0;
  export let ariaLabel: string | undefined = undefined;
  export let marginX = true;

  /**
   * Value to be displayed next to the slider
   * (current position while dragging, actual value otherwise)
   */
  export let displayValue = value;
  /** Update value in real time while dragging */
  export let liveUpdate = false;

  // Node bindings
  let element: HTMLElement;

  // Internal state
  let elementRect: DOMRect | null;
  let holding = false;
  let keydownAcceleration = 0;
  let accelerationTimer: null | number = null;
  let valueTmp = value;
  let percent = 0;

  // Dispatch 'change' events
  const dispatch = createEventDispatcher();

  function resizeSlider() {
    elementRect = element.getBoundingClientRect();
  }

  // Allows both bind:value and on:change for parent value retrieval
  function setValue(newValue: number) {
    value = newValue;
    dispatch("change", { value });
  }

  function onTrackEvent(e: Event) {
    // Update value immediately before beginning drag
    updateValueOnEvent(e);
    onDragStart();
  }

  function onDragStart() {
    holding = true;
  }

  function onDragEnd() {
    if (holding) {
      setValue(valueTmp);
      holding = false;
    }
  }

  // Accessible keypress handling
  function onKeyPress(e: KeyboardEvent) {
    // Max out at +/- 10 to value per event (50 events / 5)
    // 100 below is to increase the amount of events required to reach max velocity
    if (keydownAcceleration < 50) keydownAcceleration++;
    let throttled = Math.ceil(keydownAcceleration / 5);

    if (e.key === kbd.ARROW_UP || e.key === kbd.ARROW_RIGHT) {
      if (value + throttled > max || value >= max) {
        setValue(max);
      } else {
        setValue(value + throttled);
      }
    }
    if (e.key === kbd.ARROW_DOWN || e.key === kbd.ARROW_LEFT) {
      if (value - throttled < min || value <= min) {
        setValue(min);
      } else {
        setValue(value - throttled);
      }
    }

    // Reset acceleration after 100ms of no events
    if (accelerationTimer !== null) {
      clearTimeout(accelerationTimer);
    }
    accelerationTimer = window.setTimeout(() => (keydownAcceleration = 1), 100);
  }

  function calculateNewValue(clientX: number) {
    if (elementRect === null) return;

    // Find distance between cursor and element's left cord
    let delta = clientX - elementRect.x;

    let percent = (delta * 100) / elementRect.width;

    // Limit percent 0 -> 100
    percent = percent < 0 ? 0 : percent > 100 ? 100 : percent;

    // Limit value min -> max
    valueTmp = Math.round((percent * (max - min)) / 100 + min);

    if (liveUpdate) {
      setValue(valueTmp);
    }
  }

  // Handles both dragging of touch/mouse as well as simple one-off click/touches
  function updateValueOnEvent(e: Event) {
    // touchstart && mousedown are one-off updates, otherwise expect a currentPointer node
    if (!holding && e.type !== "touchstart" && e.type !== "mousedown") return false;

    if (e.stopPropagation) e.stopPropagation();
    if (e.preventDefault) e.preventDefault();

    // Get client's x cord either touch or mouse
    const coords =
      e.type === "touchmove" || e.type === "touchstart"
        ? (e as TouchEvent).touches[0]
        : (e as MouseEvent);

    calculateNewValue(coords.clientX);
  }

  // React to left position of element relative to window
  $: if (element) elementRect = element.getBoundingClientRect();

  $: displayValue = holding ? valueTmp : value;
  $: percent = ((clamp(displayValue, min, max) - min) * 100) / (max - min);
</script>

<svelte:window
  on:touchmove|nonpassive={updateValueOnEvent}
  on:touchcancel={onDragEnd}
  on:touchend={onDragEnd}
  on:mousemove={updateValueOnEvent}
  on:mouseup={onDragEnd}
  on:resize={resizeSlider}
/>
<div class="flex-1" class:mx-2={marginX}>
  <div
    class="slider-wrapper"
    class:holding
    tabindex="0"
    on:keydown={onKeyPress}
    bind:this={element}
    role="slider"
    aria-label={ariaLabel}
    aria-valuemin={min}
    aria-valuemax={max}
    aria-valuenow={value}
    on:mousedown={onTrackEvent}
    on:touchstart|passive={onTrackEvent}
    style={`--slider-transform:${percent}%;`}
  >
    <div class="slider-bg slider-shape">
      <div class="slider-shape overflow-hidden">
        <div class="slider-fill slider-shape" />
      </div>
      <div class="slider-thumb" />
    </div>
  </div>
</div>

<style lang="postcss">
  .slider-wrapper {
    min-width: 100%;
    height: 1em;
    position: relative;

    &:hover,
    &:focus-visible,
    &.holding {
      & .slider-fill {
        @apply bg-primary;
      }
      & .slider-thumb {
        display: block;
      }
    }
  }

  .slider-bg {
    @apply bg-base-content bg-opacity-40;
    display: flex;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
  }

  .slider-shape {
    width: 100%;
    height: 4px;
    border-radius: 2px;
  }

  .slider-fill {
    @apply bg-base-content;
    transform: translateX(calc(-100% + var(--slider-transform)));
  }

  .slider-thumb {
    @apply bg-base-content;
    display: none;

    position: absolute;
    top: 50%;
    transform: translateY(-50%);

    height: 12px;
    width: 12px;

    border: 0;
    border-radius: 50%;
    box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.5);

    left: var(--slider-transform);
    margin-left: -6px;
  }
</style>
