# DatePicker — accessible headless date picker with calendar
#
# A popover calendar for selecting a single date or a date range.
# Set @range to true for range selection (value becomes [from, to]).
# Keyboard: Arrow keys navigate days, Enter selects, Escape closes.
# Ships zero CSS — style entirely via attribute selectors in your stylesheet.
#
# Usage:
#   DatePicker value <=> selectedDate, placeholder: "Pick a date"
#   DatePicker value <=> dateRange, range: true

dpFmt = (d) ->
  return '' unless d
  m = String(d.getMonth() + 1).padStart(2, '0')
  day = String(d.getDate()).padStart(2, '0')
  "#{m}/#{day}/#{d.getFullYear()}"

dpParse = (str) ->
  return null unless str?.length is 10
  parts = str.split('/')
  return null unless parts.length is 3
  [m, d, y] = parts.map Number
  return null if isNaN(m) or isNaN(d) or isNaN(y)
  dt = new Date(y, m - 1, d)
  return null if dt.getMonth() isnt m - 1
  dt

dpSameDay = (a, b) ->
  return false unless a and b
  a.getFullYear() is b.getFullYear() and a.getMonth() is b.getMonth() and a.getDate() is b.getDate()

dpInRange = (day, from, to) ->
  return false unless day and from and to
  t = day.getTime()
  lo = Math.min(from.getTime(), to.getTime())
  hi = Math.max(from.getTime(), to.getTime())
  t >= lo and t <= hi

export DatePicker = component
  @value := null
  @placeholder := 'mm/dd/yyyy'
  @disabled := false
  @range := false
  @firstDayOfWeek := 0

  open := false
  viewMonth := new Date()
  _rangeStart := null
  _hoveredDay := null
  _inputText := ''
  _id =! "dp-#{Math.random().toString(36).slice(2, 8)}"

  _dayNames =! ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']

  _daysInView ~=
    yr = viewMonth.getFullYear()
    mo = viewMonth.getMonth()
    firstOfMonth = new Date(yr, mo, 1)
    startDay = firstOfMonth.getDay()
    offset = (startDay - @firstDayOfWeek + 7) %% 7
    daysInMonth = new Date(yr, mo + 1, 0).getDate()
    prevMonthDays = new Date(yr, mo, 0).getDate()
    dayList = []
    for n in [0...offset]
      dayList.push { date: new Date(yr, mo - 1, prevMonthDays - offset + n + 1), outside: true }
    for n in [1..daysInMonth]
      dayList.push { date: new Date(yr, mo, n), outside: false }
    trailing = (7 - dayList.length %% 7) %% 7
    for n in [1..trailing]
      dayList.push { date: new Date(yr, mo + 1, n), outside: true }
    dayList

  _displayText ~=
    if @range
      if Array.isArray(@value) and @value[0]
        from = dpFmt(@value[0])
        to = if @value[1] then dpFmt(@value[1]) else '...'
        "#{from} – #{to}"
      else
        @placeholder
    else
      if @value then dpFmt(@value) else @placeholder

  _monthLabel ~=
    viewMonth.toLocaleDateString(undefined, { month: 'long', year: 'numeric' })

  _today =! new Date()

  _prevMonth: ->
    viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() - 1, 1)

  _nextMonth: ->
    viewMonth = new Date(viewMonth.getFullYear(), viewMonth.getMonth() + 1, 1)

  _selectDay: (day) ->
    return if @disabled
    if @range
      if _rangeStart and not dpSameDay(_rangeStart, day)
        from = if _rangeStart < day then _rangeStart else day
        to = if _rangeStart < day then day else _rangeStart
        @value = [from, to]
        _rangeStart = null
        @emit 'change', @value
        open = false
      else
        _rangeStart = day
        @value = [day, null]
    else
      @value = day
      @emit 'change', @value
      open = false
    _inputText = _displayText

  _onInputChange: (e) ->
    raw = e.target.value.replace(/[^\d\/]/g, '')
    if raw.length <= 10
      _inputText = raw
    if raw.length is 10
      dt = dpParse(raw)
      if dt
        @value = dt
        viewMonth = new Date(dt.getFullYear(), dt.getMonth(), 1)
        @emit 'change', @value

  toggle: ->
    return if @disabled
    if open then @close() else @openPicker()

  openPicker: ->
    open = true
    if @value and not @range
      viewMonth = new Date(@value.getFullYear(), @value.getMonth(), 1)
    else if @range and Array.isArray(@value) and @value[0]
      viewMonth = new Date(@value[0].getFullYear(), @value[0].getMonth(), 1)
    _inputText = _displayText
    setTimeout => @_position(), 0

  close: ->
    open = false
    _rangeStart = null
    _hoveredDay = null

  _position: ->
    return unless @_trigger and @_cal
    tr = @_trigger.getBoundingClientRect()
    @_cal.style.position = 'fixed'
    @_cal.style.left = "#{tr.left}px"
    @_cal.style.top = "#{tr.bottom + 4}px"
    fl = @_cal.getBoundingClientRect()
    if fl.bottom > window.innerHeight
      @_cal.style.top = "#{tr.top - fl.height - 4}px"

  _onKeydown: (e) ->
    switch e.key
      when 'Escape'
        e.preventDefault()
        @close()
        @_trigger?.focus()
      when 'Enter', ' '
        e.preventDefault()
        @toggle() unless open

  ~>
    if open
      onDown = (e) =>
        unless @_trigger?.contains(e.target) or @_cal?.contains(e.target)
          @close()
      document.addEventListener 'mousedown', onDown
      return -> document.removeEventListener 'mousedown', onDown

  render
    . $open: open?!, $disabled: @disabled?!, $range: @range?!

      # Trigger
      button ref: "_trigger", $trigger: true
        aria-haspopup: "dialog"
        aria-expanded: !!open
        disabled: @disabled
        @click: @toggle
        @keydown: @_onKeydown
        _displayText

      # Calendar dropdown
      if open
        div ref: "_cal", role: "dialog", aria-label: "Date picker", $calendar: true
          style: "position:fixed;z-index:50"

          # Month navigation
          . $header: true
            button $prev: true, aria-label: "Previous month", @click: @_prevMonth
              "‹"
            span $month-label: true
              _monthLabel
            button $next: true, aria-label: "Next month", @click: @_nextMonth
              "›"

          # Day-of-week headers
          . $weekdays: true
            for dayName in _dayNames
              span $weekday: true
                dayName

          # Day grid
          . $days: true, role: "grid"
            for entry, dIdx in _daysInView
              button role: "gridcell", tabindex: "-1"
                $outside: entry.outside?!
                $today: dpSameDay(entry.date, _today)?!
                $selected: (if @range then (Array.isArray(@value) and (dpSameDay(entry.date, @value[0]) or dpSameDay(entry.date, @value[1]))) else dpSameDay(entry.date, @value))?!
                $in-range: (if @range then (if _rangeStart then dpInRange(entry.date, _rangeStart, _hoveredDay or _rangeStart) else if Array.isArray(@value) and @value[0] and @value[1] then dpInRange(entry.date, @value[0], @value[1]) else false) else false)?!
                $range-start: (if @range and _rangeStart then dpSameDay(entry.date, _rangeStart) else false)?!
                @click: (=> @_selectDay(entry.date))
                @mouseenter: (=> _hoveredDay = entry.date)
                entry.date.getDate()
