import { useQuery } from '@tanstack/react-query'
import { useEffect, useRef, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'

import SidebarComponent from '@/components/layout/SidebarComponent'
import TagLink from '@/components/TagLink'
import { fetchMaterialTagTree, parseMaterialFilter } from '@/lib/materials'
import { materialsKeys } from '@/lib/queryKeys'
import { cn } from '@/lib/utils'

import type { CSSProperties, Dispatch, FC, ReactNode, SetStateAction } from 'react'

import type { MaterialFilter, MaterialSidebarTag, Tag } from '@/types'

const FILTERS: MaterialFilter[] = ['missing', 'present', 'any']
const FILTER_LABELS: Record<MaterialFilter, string> = { present: '有', missing: '無', any: '全' }


const px = (value: string): number => {
  const parsed = Number.parseFloat (value)
  return Number.isFinite (parsed) ? parsed : 0
}


const verticalChrome = (el: HTMLElement): number => {
  const style = window.getComputedStyle (el)

  return (
      px (style.paddingTop)
      + px (style.paddingBottom)
      + px (style.borderTopWidth)
      + px (style.borderBottomWidth))
}


const setChildrenById = (
    tags:     MaterialSidebarTag[],
    targetId: number,
    children: MaterialSidebarTag[],
): MaterialSidebarTag[] => (
    tags.map (tag => {
      if (tag.id === targetId)
	return { ...tag, children }

      if (tag.children.length === 0)
	return tag

      return { ...tag, children: setChildrenById (tag.children, targetId, children) }
    }))


const materialPath = (
    tagId: number,
    materialFilter: MaterialFilter,
): string =>
    `/materials?tag_id=${ tagId }&include_descendants=1&group_by=parent_tag`
    + `&material_filter=${ materialFilter }`


const sidebarTagToTag = (tag: MaterialSidebarTag): Tag => ({
    id:             tag.id,
    name:           tag.name,
    category:       tag.category,
    deprecatedAt:   tag.deprecated ? '' : null,
    aliases:        [],
    parents:        [],
    postCount:      0,
    createdAt:      '',
    updatedAt:      '',
    hasWiki:        false,
    materialId:     null,
    hasDeerjikists: false,
    matchedAlias:   null })


const tagSelectionShellClass = (selected: boolean): string =>
    cn (selected
	? ['rounded-md border border-sky-500 bg-sky-50 px-2 py-1 text-sky-700',
	   'dark:border-sky-400 dark:bg-sky-950 dark:text-sky-100']
	: 'px-2 py-1')


const updateMaterialFilterQuery = (
    pathname: string,
    locationSearch: string,
    navigate: ReturnType<typeof useNavigate>,
    materialFilter: MaterialFilter,
) => {
  const qs = new URLSearchParams (locationSearch)
  qs.set ('material_filter', materialFilter)
  navigate (`${ pathname }${ qs.toString () ? `?${ qs.toString () }` : '' }`)
}


const MaterialFilterButtons: FC<{ materialFilter: MaterialFilter
				  onChange:       (materialFilter: MaterialFilter) => void }> = (
    { materialFilter, onChange },
) => (
    <div className="flex flex-wrap gap-2 justify-end md:justify-start flex-center">
      <label className="my-auto text-sm font-bold">素材：</label>
      {FILTERS.map (value => (
	  <button
	      key={value}
	      type="button"
	      onClick={() => onChange (value)}
	      className={cn (
		  'rounded-full border px-3 py-1 text-sm',
		  (materialFilter === value
		   ? ['border-sky-500 bg-sky-50 text-sky-700 dark:border-sky-400',
		      'dark:bg-sky-950 dark:text-sky-100']
		   : ['border-neutral-300 bg-white text-neutral-700 dark:border-stone-700',
		      'dark:bg-stone-900 dark:text-stone-200']))}>
	    {FILTER_LABELS[value]}
	  </button>))}
    </div>)


const MaterialTreeNode: FC<{
    materialFilter: MaterialFilter
    selectedTagId:  number | null
    nestLevel?:     number
    onChildren:     (tagId: number, children: MaterialSidebarTag[]) => void
    openTags:       Record<number, boolean>
    setOpenTags:    Dispatch<SetStateAction<Record<number, boolean>>>
    tag:            MaterialSidebarTag
}> = ({ materialFilter, nestLevel = 0, onChildren, openTags, selectedTagId,
	 setOpenTags, tag }) => {
  const open = Boolean (openTags[tag.id])
  const { data } = useQuery ({
      queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }),
      queryFn:	() => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }),
      enabled:	open && tag.hasChildren && tag.children.length === 0})

  useEffect (() => {
    if (open && data && tag.children.length === 0)
      onChildren (tag.id, data)
  }, [data, onChildren, open, tag.children.length, tag.id])

  return (
      <>
	<li>
	  <div className="flex flex-center">
	    <div className="flex-none w-4 my-auto">
	      {tag.hasChildren && (
		  <button
		      type="button"
		      onClick={() => setOpenTags (prev => ({ ...prev, [tag.id]: !prev[tag.id] }))}
		      className="text-neutral-500 dark:text-stone-400">
		    {open ? <>&minus;</> : '+'}
		  </button>)}
	    </div>
	    <div className="min-w-0 flex-1 my-auto">
	      <div className={cn (tagSelectionShellClass (selectedTagId === tag.id),
				  'min-w-0 truncate')}>
		<TagLink
		    tag={sidebarTagToTag (tag)}
		    nestLevel={nestLevel}
		    title={tag.name}
		    withCount={false}
		    withWiki={false}
		    to={materialPath (tag.id, materialFilter)}/>
	      </div>
	    </div>
	  </div>
	</li>
	{open && tag.children.length > 0 && (
	    <ul>
	      {tag.children.map (child => (
		  <MaterialTreeNode
		      key={child.id}
		      tag={child}
		      nestLevel={nestLevel + 1}
		      materialFilter={materialFilter}
		      selectedTagId={selectedTagId}
		      openTags={openTags}
		      setOpenTags={setOpenTags}
		      onChildren={onChildren}/>))}
	    </ul>)}
      </>)
}


const MobileMaterialTreeNode: FC<{ depth?: number
				   availableInlineSizePx?: number | null
				   materialFilter: MaterialFilter
				   selectedTagId: number | null
				   onChildren: (tagId: number, children: MaterialSidebarTag[]) =>
						   void
				   openTags: Record<number, boolean>
				   setOpenTags: Dispatch<SetStateAction<Record<number, boolean>>>
				   tag: MaterialSidebarTag }> = (
    {
      depth = 0,
      availableInlineSizePx = null,
      materialFilter,
      onChildren,
      openTags,
      selectedTagId,
      setOpenTags,
      tag,
    },
) => {
  const open = Boolean (openTags[tag.id])
  const tagColumnRef = useRef<HTMLDivElement | null> (null)
  const chipRef = useRef<HTMLDivElement | null> (null)
  const buttonRef = useRef<HTMLButtonElement | null> (null)
  const expansionSlotRef = useRef<HTMLDivElement | null> (null)
  const expansionBorderRef = useRef<HTMLDivElement | null> (null)
  const [tagChipInlineSizePx, setTagChipInlineSizePx] = useState<number | null> (null)
  const [tagLinkInlineSizePx, setTagLinkInlineSizePx] = useState<number | null> (null)
  const [childAvailableInlineSizePx, setChildAvailableInlineSizePx] = useState<number | null> (null)
  const { data } = useQuery ({
      queryKey: materialsKeys.tree ({ parentId: tag.id, materialFilter }),
      queryFn:  () => fetchMaterialTagTree ({ parentId: tag.id, materialFilter }),
      enabled:  open && tag.hasChildren && tag.children.length === 0})

  useEffect (() => {
    if (open && data && tag.children.length === 0)
      onChildren (tag.id, data)
  }, [data, onChildren, open, tag.children.length, tag.id])

  useEffect (() => {
    const tagColumn = tagColumnRef.current
    const chip = chipRef.current
    if (!(tagColumn) || !(chip))
      return

    const updateTagInlineSize = () => {
      const buttonHeight = buttonRef.current?.offsetHeight ?? 0
      const gap = tag.hasChildren ? px (window.getComputedStyle (tagColumn).rowGap) : 0
      const columnHeight =
	  availableInlineSizePx == null
	  ? tagColumn.clientHeight
	  : Math.min (tagColumn.clientHeight, availableInlineSizePx)
      const nextChipInlineSize = Math.max (24, columnHeight - buttonHeight - gap)
      const nextLinkInlineSize = Math.max (
	  16,
	  nextChipInlineSize - verticalChrome (chip),
      )

      setTagChipInlineSizePx (prev => prev === nextChipInlineSize ? prev : nextChipInlineSize)
      setTagLinkInlineSizePx (prev => prev === nextLinkInlineSize ? prev : nextLinkInlineSize)
    }

    updateTagInlineSize ()

    const resizeObserver = new ResizeObserver (() => {
      updateTagInlineSize ()
    })

    resizeObserver.observe (tagColumn)
    resizeObserver.observe (chip)

    if (buttonRef.current)
      resizeObserver.observe (buttonRef.current)

    return () => {
      resizeObserver.disconnect ()
    }
  }, [availableInlineSizePx, open, tag.hasChildren, tag.children.length])

  useEffect (() => {
    const expansionSlot = expansionSlotRef.current
    const expansionBorder = expansionBorderRef.current
    if (!(expansionSlot) || !(expansionBorder))
      return

    const updateChildInlineSize = () => {
      const base =
	  availableInlineSizePx == null
	  ? expansionSlot.clientHeight
	  : availableInlineSizePx
      const chrome = verticalChrome (expansionSlot) + verticalChrome (expansionBorder)
      const nextInlineSize = Math.max (24, base - chrome)
      setChildAvailableInlineSizePx (prev => prev === nextInlineSize ? prev : nextInlineSize)
    }

    updateChildInlineSize ()

    const resizeObserver = new ResizeObserver (() => {
      updateChildInlineSize ()
    })

    resizeObserver.observe (expansionSlot)
    resizeObserver.observe (expansionBorder)

    return () => {
      resizeObserver.disconnect ()
    }
  }, [availableInlineSizePx, open, tag.children.length])

  return (
      <div className="flex h-full min-h-0 max-h-full flex-row-reverse items-start gap-2
		      overflow-hidden">
	<div
	    ref={tagColumnRef}
	    className="flex h-full min-h-0 max-h-full flex-col items-center gap-1
		       overflow-hidden">
	  <div
	      ref={chipRef}
	      className={cn (
		  tagSelectionShellClass (selectedTagId === tag.id),
		  'box-border rounded-xl border px-3 py-2 text-sm shadow-sm',
		  'min-h-0 overflow-hidden [max-inline-size:var(--tag-chip-inline-size)]',
		  '[max-height:var(--tag-chip-inline-size)]',
	      )}
	      style={{
		writingMode: 'vertical-rl',
		'--tag-chip-inline-size': (
		    tagChipInlineSizePx == null
		    ? undefined
		    : `${ tagChipInlineSizePx }px`),
		'--tag-link-inline-size': (
		    tagLinkInlineSizePx == null
		    ? undefined
		    : `${ tagLinkInlineSizePx }px`),
	      } as CSSProperties}>
	    <TagLink
		tag={sidebarTagToTag (tag)}
		title={tag.name}
		withCount={false}
		withWiki={false}
		to={materialPath (tag.id, materialFilter)}
		className="block overflow-hidden text-ellipsis whitespace-nowrap
			   [max-inline-size:var(--tag-link-inline-size)]
			   [max-height:var(--tag-link-inline-size)]"/>
	  </div>
	  {tag.hasChildren && (
	      <button
		  ref={buttonRef}
		  type="button"
		  onClick={() => setOpenTags (prev => ({ ...prev, [tag.id]: !prev[tag.id] }))}
		  className="flex-none rounded-full border border-stone-300 bg-white
			     px-2 py-0.5 text-sm text-stone-700 dark:border-stone-700
			     dark:bg-stone-900 dark:text-stone-100">
		{open ? <>&minus;</> : '+'}
	      </button>)}
	</div>
	{open && tag.children.length > 0 && (
	    <div
		ref={expansionSlotRef}
		className={cn (
		    'h-full min-h-0 max-h-full overflow-hidden box-border',
		    depth === 0 ? 'pt-5' : 'pt-3')}>
	      <div
		  ref={expansionBorderRef}
		  className="relative max-h-full overflow-hidden rounded-2xl border border-stone-200
			     bg-stone-100/70 py-2 pl-2 pr-2 text-stone-900
			     dark:border-stone-700 dark:bg-stone-900/70 dark:text-stone-100">
		<div className="flex h-full min-h-0 max-h-full flex-row-reverse items-start gap-2
				overflow-hidden">
		  <span
		      aria-hidden="true"
		      className="absolute -right-3 top-4 h-px w-3 bg-stone-300 dark:bg-stone-600"/>
		  {tag.children.map (child => (
		      <div
			  key={child.id}
			  className="relative h-full min-h-0 max-h-full overflow-hidden">
			<MobileMaterialTreeNode
			    tag={child}
			    depth={depth + 1}
			    availableInlineSizePx={childAvailableInlineSizePx}
			    materialFilter={materialFilter}
			    selectedTagId={selectedTagId}
			    openTags={openTags}
			    setOpenTags={setOpenTags}
			    onChildren={onChildren}/>
		      </div>))}
		</div>
	      </div>
	    </div>)}
      </div>)
}


const MaterialSidebar: FC = () => {
  const location = useLocation ()
  const navigate = useNavigate ()
  const qs = new URLSearchParams (location.search)
  const materialFilter = parseMaterialFilter (qs.get ('material_filter'), 'any')
  const selectedTagId = Number (qs.get ('tag_id') ?? 0) || null

  const [desktopTags, setDesktopTags] = useState<MaterialSidebarTag[]> ([])
  const [openTags, setOpenTags] = useState<Record<number, boolean>> ({ })
  const mobileRailRef = useRef<HTMLDivElement | null> (null)
  const [mobileAvailableInlineSizePx, setMobileAvailableInlineSizePx] =
      useState<number | null> (null)

  const { data: rootTags = [], isLoading, isError } = useQuery ({
      queryKey: materialsKeys.tree ({ parentId: null, materialFilter }),
      queryFn:  () => fetchMaterialTagTree ({ parentId: null, materialFilter })})

  useEffect (() => {
    setDesktopTags (rootTags)
  }, [rootTags])

  useEffect (() => {
    const el = mobileRailRef.current
    if (!(el))
      return

    requestAnimationFrame (() => {
      el.scrollLeft = el.scrollWidth
    })
  }, [rootTags, materialFilter])

  useEffect (() => {
    const el = mobileRailRef.current
    if (!(el))
      return

    const updateAvailableInlineSize = () => {
      const nextInlineSize = Math.max (24, el.clientHeight)
      setMobileAvailableInlineSizePx (prev => prev === nextInlineSize ? prev : nextInlineSize)
    }

    updateAvailableInlineSize ()

    const resizeObserver = new ResizeObserver (() => {
      updateAvailableInlineSize ()
    })

    resizeObserver.observe (el)

    return () => {
      resizeObserver.disconnect ()
    }
  }, [rootTags, materialFilter])

  const visibleRootTags = desktopTags.length > 0 ? desktopTags : rootTags

  const setChildren = (tagId: number, children: MaterialSidebarTag[]) => {
    setDesktopTags (prev => {
      const base = prev.length > 0 ? prev : rootTags
      return setChildrenById (base, tagId, children)
    })
  }

  const handleFilterChange = (value: MaterialFilter) => {
    setDesktopTags ([])
    setOpenTags ({ })
    updateMaterialFilterQuery (location.pathname, location.search, navigate, value)
  }

  const renderDesktopTree = (tags: MaterialSidebarTag[]): ReactNode => (
      tags.map (tag => (
	  <MaterialTreeNode
	      key={tag.id}
	      tag={tag}
	      materialFilter={materialFilter}
	      selectedTagId={selectedTagId}
	      openTags={openTags}
	      setOpenTags={setOpenTags}
	      onChildren={setChildren}/>)))

  return (
      <>
	<div className="border-b bg-stone-50 p-3 dark:border-stone-700 dark:bg-stone-950
			dark:text-stone-100 md:hidden flex h-[25dvh] min-h-0 flex-col
			overflow-hidden">
	  <MaterialFilterButtons
	      materialFilter={materialFilter}
	      onChange={handleFilterChange}/>
	  <div
	      ref={mobileRailRef}
	      className="mt-3 min-h-0 flex-1 overflow-x-auto overflow-y-hidden">
	    <div className="flex min-w-max flex-row-reverse items-start gap-3 pb-1 h-full
			    min-h-0 max-h-full">
	      {visibleRootTags.map (tag => (
		  <MobileMaterialTreeNode
		      key={tag.id}
		      tag={tag}
		      availableInlineSizePx={mobileAvailableInlineSizePx}
		      materialFilter={materialFilter}
		      selectedTagId={selectedTagId}
		      openTags={openTags}
		      setOpenTags={setOpenTags}
		      onChildren={setChildren}/>))}
	    </div>
	  </div>
	</div>

	<div className="hidden md:block">
	  <SidebarComponent>
	    <div className="space-y-4">
	      <MaterialFilterButtons
		  materialFilter={materialFilter}
		  onChange={handleFilterChange}/>
	      {isLoading && (
		  <p className="text-sm text-neutral-500 dark:text-stone-400">読込中……</p>)}
	      {isError && (
		  <p className="text-sm text-red-600 dark:text-red-300">
		    タグ一覧の取得に失敗しました．
		  </p>)}
	      {(!isLoading && !isError) && (
		  <ul>
		    {renderDesktopTree (visibleRootTags)}
		  </ul>)}
	    </div>
	  </SidebarComponent>
	</div>
      </>)
}


export default MaterialSidebar
