import { motion } from 'framer-motion'
import { Helmet } from 'react-helmet-async'

import { clientAnimationTransition } from '@/lib/clientAnimation'
import { useClientBehaviourSettings } from '@/lib/useClientBehaviourSettings'
import { cn } from '@/lib/utils'

import type { CSSProperties, FC, MouseEvent, PointerEvent, ReactNode } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'

type SidebarSide = 'left' | 'right'

type Props = {
    children:      ReactNode
    className?:    string
    maxWidth?:     number
    onWidthChange?: (width: number) => void
    sidebarKey:    string
    side?:         SidebarSide }

type ResizeState = {
    pointerId:  number
    startWidth: number
    startX:     number
    side:       SidebarSide }

type PendingPressState = ResizeState & { startY: number }

const DEFAULT_SIDEBAR_WIDTH = 256
const MIN_SIDEBAR_WIDTH = 192
const MIN_MAIN_WIDTH = 360
const MAX_VIEWPORT_WIDTH_RATIO = .8
const LONG_PRESS_DELAY_MS = 350
const TOUCH_MOVE_THRESHOLD_PX = 8


const storageKeyForSidebar = (sidebarKey: string, side: SidebarSide): string =>
    `sidebar:${ sidebarKey }:${ side }:width`


const getSidebarMaxWidth = (containerWidth: number): number => {
  const viewportWidth =
      typeof window === 'undefined' ? containerWidth : window.innerWidth
  const viewportMaxWidth = Math.floor (viewportWidth * MAX_VIEWPORT_WIDTH_RATIO)
  const containerMaxWidth = Math.floor (containerWidth - MIN_MAIN_WIDTH)
  const dynamicMaxWidth = Math.min (viewportMaxWidth, containerMaxWidth)

  return Math.max (MIN_SIDEBAR_WIDTH, dynamicMaxWidth)
}


const readStoredSidebarWidth = (
    sidebarKey: string,
    side: SidebarSide): number => {
  if (typeof window === 'undefined')
    return DEFAULT_SIDEBAR_WIDTH

  const value = window.localStorage.getItem (storageKeyForSidebar (sidebarKey, side))
  const width = value == null ? Number.NaN : Number (value)

  return Number.isFinite (width) ? width : DEFAULT_SIDEBAR_WIDTH
}


const SidebarComponent: FC<Props> = ({
    children,
    className,
    maxWidth,
    onWidthChange,
    sidebarKey,
    side = 'left',
}) => {
  const behaviourSettings = useClientBehaviourSettings ()
  const animationMode = behaviourSettings.animation ?? 'normal'
  const layoutTransition = clientAnimationTransition (
      animationMode,
      { normal: { duration: .2, ease: 'easeOut' as const } },
  )
  const rootRef = useRef<HTMLDivElement | null> (null)
  const maxWidthRef = useRef (maxWidth)
  const getContainerWidth = useCallback (() => {
    const containerWidth = rootRef.current
	?.closest ('[data-sidebar-container]')
	?.clientWidth
    ?? window.innerWidth
    return containerWidth
  }, [])
  const getClampedWidth = useCallback ((
      nextWidth: number,
      containerWidth: number): number => {
    const viewportWidth =
	typeof window === 'undefined' ? containerWidth : window.innerWidth
    const viewportMaxWidth = Math.floor (viewportWidth * MAX_VIEWPORT_WIDTH_RATIO)
    const maxAllowedWidth =
	maxWidthRef.current == null
	? getSidebarMaxWidth (containerWidth)
	: Math.max (
	    MIN_SIDEBAR_WIDTH,
	    Math.min (viewportMaxWidth, maxWidthRef.current))

    return Math.min (maxAllowedWidth, Math.max (MIN_SIDEBAR_WIDTH, nextWidth))
  }, [])

  const [width, setWidth] = useState (() => (
      typeof window === 'undefined'
      ? DEFAULT_SIDEBAR_WIDTH
      : getClampedWidth (
	  readStoredSidebarWidth (sidebarKey, side),
	  window.innerWidth)))
  const [resizing, setResizing] = useState (false)
  const widthRef = useRef (width)
  const longPressTimerRef = useRef<ReturnType<typeof setTimeout> | null> (null)
  const pendingPressRef = useRef<PendingPressState | null> (null)
  const resizeRef = useRef<ResizeState | null> (null)
  const bodyStyleRef = useRef<{ cursor: string, userSelect: string } | null> (null)

  const clearLongPressTimer = useCallback (() => {
    if (longPressTimerRef.current == null)
      return

    clearTimeout (longPressTimerRef.current)
    longPressTimerRef.current = null
  }, [])

  const restoreBodyStyle = useCallback (() => {
    if (bodyStyleRef.current == null)
      return

    document.body.style.cursor = bodyStyleRef.current.cursor
    document.body.style.userSelect = bodyStyleRef.current.userSelect
    bodyStyleRef.current = null
  }, [])

  const startResize = useCallback ((state: ResizeState) => {
    clearLongPressTimer ()
    pendingPressRef.current = null
    resizeRef.current = state
    setResizing (true)

    if (bodyStyleRef.current == null)
    {
      bodyStyleRef.current = {
	  cursor: document.body.style.cursor,
	  userSelect: document.body.style.userSelect }
    }

    document.body.style.cursor = 'col-resize'
    document.body.style.userSelect = 'none'
  }, [clearLongPressTimer])

  const finishResize = useCallback (() => {
    clearLongPressTimer ()
    pendingPressRef.current = null

    const state = resizeRef.current
    resizeRef.current = null

    if (state != null)
      window.localStorage.setItem (
	  storageKeyForSidebar (sidebarKey, state.side),
	  String (widthRef.current))

    setResizing (false)
    restoreBodyStyle ()
  }, [clearLongPressTimer, restoreBodyStyle, sidebarKey])

  const applyWidth = useCallback ((nextWidth: number) => {
    if (nextWidth === widthRef.current)
      return

    widthRef.current = nextWidth
    setWidth (nextWidth)
  }, [])

  useEffect (() => {
    maxWidthRef.current = maxWidth
    const containerWidth = getContainerWidth ()
    const nextWidth = getClampedWidth (widthRef.current, containerWidth)
    applyWidth (nextWidth)
  }, [applyWidth, getClampedWidth, getContainerWidth, maxWidth])

  const updateWidth = useCallback ((clientX: number) => {
    const state = resizeRef.current
    if (state == null)
      return

    const containerWidth = getContainerWidth ()
    const delta = state.side === 'left'
		  ? clientX - state.startX
		  : state.startX - clientX
    const nextWidth = getClampedWidth (state.startWidth + delta, containerWidth)
    applyWidth (nextWidth)
  }, [applyWidth, getClampedWidth, getContainerWidth])

  const handlePointerDown = (ev: PointerEvent<HTMLDivElement>) => {
    if (ev.pointerType !== 'mouse' && ev.pointerType !== 'touch')
      return

    if (ev.pointerType === 'mouse' && ev.button !== 0)
      return

    ev.preventDefault ()
    ev.stopPropagation ()
    ev.currentTarget.setPointerCapture (ev.pointerId)

    const state = {
	pointerId: ev.pointerId,
	side,
	startWidth: widthRef.current,
	startX: ev.clientX }

    if (ev.pointerType === 'mouse')
    {
      startResize (state)
      return
    }

    pendingPressRef.current = { ...state, startY: ev.clientY }
    clearLongPressTimer ()
    longPressTimerRef.current = setTimeout (() => {
      if (pendingPressRef.current != null)
	startResize (pendingPressRef.current)
    }, LONG_PRESS_DELAY_MS)
  }

  const handlePointerMove = (ev: PointerEvent<HTMLDivElement>) => {
    ev.stopPropagation ()

    if (resizeRef.current != null)
    {
      ev.preventDefault ()
      updateWidth (ev.clientX)
      return
    }

    const pendingPress = pendingPressRef.current
    if (pendingPress == null || pendingPress.pointerId !== ev.pointerId)
      return

    const deltaX = ev.clientX - pendingPress.startX
    const deltaY = ev.clientY - pendingPress.startY
    const distance = Math.hypot (deltaX, deltaY)

    if (distance >= TOUCH_MOVE_THRESHOLD_PX)
    {
      clearLongPressTimer ()
      pendingPressRef.current = null
    }
  }

  const handlePointerEnd = (ev: PointerEvent<HTMLDivElement>) => {
    ev.stopPropagation ()

    if (ev.currentTarget.hasPointerCapture (ev.pointerId))
      ev.currentTarget.releasePointerCapture (ev.pointerId)

    finishResize ()
  }

  const resetWidth = useCallback (() => {
    const containerWidth = getContainerWidth ()
    const nextWidth = getClampedWidth (DEFAULT_SIDEBAR_WIDTH, containerWidth)
    applyWidth (nextWidth)
    window.localStorage.removeItem (storageKeyForSidebar (sidebarKey, side))
  }, [applyWidth, getClampedWidth, getContainerWidth, sidebarKey, side])

  const handleDoubleClick = (ev: MouseEvent<HTMLDivElement>) => {
    ev.preventDefault ()
    ev.stopPropagation ()
    resetWidth ()
  }

  useEffect (() => {
    const containerWidth = getContainerWidth ()
    const nextWidth = getClampedWidth (
	readStoredSidebarWidth (sidebarKey, side),
	containerWidth)
    applyWidth (nextWidth)
  }, [applyWidth, getClampedWidth, getContainerWidth, sidebarKey, side])

  useEffect (() => {
    const containerWidth = getContainerWidth ()
    const nextWidth = getClampedWidth (widthRef.current, containerWidth)
    applyWidth (nextWidth)
  }, [applyWidth, getClampedWidth, getContainerWidth])

  useEffect (() => {
    if (typeof window === 'undefined')
      return

    const handleResize = () => {
      const containerWidth = getContainerWidth ()
      const nextWidth = getClampedWidth (widthRef.current, containerWidth)
      applyWidth (nextWidth)
    }

    window.addEventListener ('resize', handleResize)
    return () => window.removeEventListener ('resize', handleResize)
  }, [applyWidth, getClampedWidth, getContainerWidth])

  useEffect (() => {
    onWidthChange?.(width)
  }, [onWidthChange, width])

  useEffect (() => () => {
    clearLongPressTimer ()
    restoreBodyStyle ()
  }, [clearLongPressTimer, restoreBodyStyle])

  const style = {
      '--sidebar-width': `${ width }px` } as CSSProperties

  return (
      <motion.div
	  ref={rootRef}
	  layout={animationMode === 'off' ? false : 'position'}
	  transition={{ layout: layoutTransition }}
	  style={style}
	  className={cn (
	      'relative w-full md:w-[var(--sidebar-width)] md:shrink-0 md:h-full',
	      className)}>
	<Helmet>
	  <style>
	    {`
	      .sidebar
	      {
		direction: rtl;
	      }

	      .sidebar > *
	      {
		direction: ltr;
	      }`}
	  </style>
	</Helmet>

	<div className="h-full overflow-y-auto p-4 sidebar">
	  {children}
	</div>

	<div
	    role="separator"
	    aria-orientation="vertical"
	    aria-label="サイドバー幅"
	    onPointerDown={handlePointerDown}
	    onPointerMove={handlePointerMove}
	    onPointerUp={handlePointerEnd}
	    onPointerCancel={handlePointerEnd}
	    onDoubleClick={handleDoubleClick}
	    className={cn (
		'hidden md:block absolute top-0 bottom-0 z-20 w-4 cursor-col-resize',
		'touch-none select-none',
		side === 'left' ? '-right-2' : '-left-2',
		'before:absolute before:top-0 before:bottom-0 before:w-px',
		'before:bg-neutral-500/0 hover:before:bg-neutral-500/30',
		'before:transition-colors before:duration-150',
		side === 'left'
		? 'before:left-1/2'
		: 'before:right-1/2',
		resizing && 'before:bg-neutral-500/40')}/>
      </motion.div>)
}

export default SidebarComponent
