# Slider — accessible headless range slider
#
# Supports single and range (multi-thumb) modes, pointer drag with capture,
# keyboard stepping, and CSS custom properties for thumb/indicator positioning.
# Ships zero CSS.
#
# Usage:
#   Slider value <=> volume
#   Slider value <=> volume, min: 0, max: 100, step: 5
#   Slider value <=> range, min: 0, max: 100   (pass array for range mode)

export Slider = component
  @value:: number := 0
  @min:: number := 0
  @max:: number := 100
  @step:: number := 1
  @largeStep:: number := 10
  @orientation:: "horizontal" | "vertical" := "horizontal"
  @disabled:: boolean := false
  @name:: any := null
  @valueText:: any := null

  dragging := false
  activeThumb := -1
  _thumbOffset = 0
  _id =! "sld-#{Math.random().toString(36).slice(2, 8)}"

  isRange ~= Array.isArray(@value)
  values  ~= if isRange then @value else [@value]
  horiz   ~= @orientation is 'horizontal'

  _clamp: (v) -> Math.min(@max, Math.max(@min, v))

  _roundToStep: (v) ->
    rounded = Math.round((v - @min) / @step) * @step + @min
    precision = String(@step).split('.')[1]?.length or 0
    parseFloat rounded.toFixed(precision)

  _percentOf: (v) -> ((@_clamp(v) - @min) / (@max - @min)) * 100

  _valueFromPointer: (e) ->
    rect = @_track.getBoundingClientRect()
    if horiz
      ratio = (e.clientX - _thumbOffset - rect.left) / rect.width
    else
      ratio = 1 - (e.clientY - _thumbOffset - rect.top) / rect.height
    ratio = Math.max(0, Math.min(1, ratio))
    @_roundToStep(@min + ratio * (@max - @min))

  _closestThumb: (e) ->
    return 0 unless isRange
    rect = @_track.getBoundingClientRect()
    pos = if horiz then (e.clientX - rect.left) / rect.width else 1 - (e.clientY - rect.top) / rect.height
    best = 0
    bestDist = Infinity
    for v, i in values
      pct = @_percentOf(v) / 100
      dist = Math.abs(pos - pct)
      if dist < bestDist
        bestDist = dist
        best = i
    best

  _setValue: (idx, val) ->
    val = @_clamp(@_roundToStep(val))
    if isRange
      arr = [...values]
      arr[idx] = val
      arr.sort (a, b) -> a - b
      @value = arr
    else
      @value = val
    @emit 'input', @value

  _commitValue: ->
    @emit 'change', @value

  _onPointerDown: (e) ->
    return if @disabled or e.button isnt 0
    e.preventDefault()
    idx = @_closestThumb(e)
    activeThumb = idx
    dragging = true

    thumb = @_track.querySelectorAll('[data-thumb]')[idx]
    if thumb
      tr = thumb.getBoundingClientRect()
      if horiz
        _thumbOffset = e.clientX - (tr.left + tr.width / 2)
      else
        _thumbOffset = e.clientY - (tr.top + tr.height / 2)
    else
      _thumbOffset = 0

    newVal = @_valueFromPointer(e)
    @_setValue idx, newVal

    @_track.setPointerCapture e.pointerId

  _onPointerMove: (e) ->
    return unless dragging
    newVal = @_valueFromPointer(e)
    @_setValue activeThumb, newVal

  _onPointerUp: (e) ->
    return unless dragging
    dragging = false
    activeThumb = -1
    _thumbOffset = 0
    @_track.releasePointerCapture e.pointerId
    @_commitValue()

  _onKeydown: (e, idx) ->
    s = if e.shiftKey then @largeStep else @step
    v = values[idx]
    newVal = switch e.key
      when 'ArrowRight', 'ArrowUp' then v + s
      when 'ArrowLeft', 'ArrowDown' then v - s
      when 'PageUp' then v + @largeStep
      when 'PageDown' then v - @largeStep
      when 'Home' then @min
      when 'End' then @max
      else null
    if newVal?
      e.preventDefault()
      @_setValue idx, newVal
      @_commitValue()

  render
    div role: "group", $orientation: @orientation, $disabled: @disabled?!, $dragging: dragging?!
      style: "--slider-min: #{@min}; --slider-max: #{@max}"

      # Track
      div ref: "_track", $track: true
        style: "position:relative"
        @pointerdown: @_onPointerDown
        @pointermove: @_onPointerMove
        @pointerup: @_onPointerUp

        # Indicator (filled portion)
        if isRange
          div $indicator: true
            style: "position:absolute; #{if horiz then 'left' else 'bottom'}: #{@_percentOf(values[0])}%; #{if horiz then 'width' else 'height'}: #{@_percentOf(values[1]) - @_percentOf(values[0])}%"
        else
          div $indicator: true
            style: "position:absolute; #{if horiz then 'left: 0; width' else 'bottom: 0; height'}: #{@_percentOf(values[0])}%"

        # Thumbs
        for val, idx in values
          div $thumb: true, $active: (idx is activeThumb)?!
            style: "position:absolute; #{if horiz then 'left' else 'bottom'}: #{@_percentOf(val)}%; z-index: #{if idx is activeThumb then 2 else 1}"
            @keydown: (e) => @_onKeydown(e, idx)
            input type: "range", style: "position:absolute;opacity:0;width:0;height:0;pointer-events:none"
              id: "#{_id}-thumb-#{idx}"
              name: @name?!
              min: @min, max: @max, step: @step
              value: val
              aria-valuenow: val
              aria-valuemin: @min
              aria-valuemax: @max
              aria-valuetext: if @valueText then @valueText(val, idx) else undefined
              aria-orientation: @orientation
              aria-disabled: @disabled?!

      slot
