import { motion } from 'framer-motion'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Helmet } from 'react-helmet-async'
import { useParams } from 'react-router-dom'

import ErrorScreen from '@/components/ErrorScreen'
import PostEditForm from '@/components/PostEditForm'
import PostEmbed from '@/components/PostEmbed'
import PrefetchLink from '@/components/PrefetchLink'
import TagLink from '@/components/TagLink'
import FieldError from '@/components/common/FieldError'
import { useDialogue } from '@/components/dialogues/DialogueProvider'
import { Button } from '@/components/ui/button'
import { SITE_TITLE } from '@/config'
import { CATEGORIES, CATEGORY_NAMES } from '@/consts'
import { apiDelete, apiGet, apiPatch, apiPost, apiPut, isApiError } from '@/lib/api'
import { fetchPost } from '@/lib/posts'
import { canEditContent } from '@/lib/users'
import { cn, dateString, inputClass } from '@/lib/utils'
import { useValidationErrors } from '@/lib/useValidationErrors'

import type { FC, FormEvent, ReactNode } from 'react'

import type { NiconicoViewerHandle,
	      Post,
	      Category,
	      Tag,
	      Theatre,
	      TheatreComment,
	      TheatreInfo,
	      TheatrePostSelectionWeights,
	      TheatreProgramme,
	      User } from '@/types'

type TheatreCommentField = 'content'
type TheatreLayoutMode = 'threeColumns' | 'tagsBottom' | 'commentsBottom'
type TagFlow = 'vertical' | 'horizontal'

const INITIAL_THEATRE_INFO: TheatreInfo =
    { hostFlg: false,
      postId: null,
      postStartedAt: null,
      postElapsedMs: null,
      watchingUsers: [],
      skipVote: { votesCount: 0,
		  requiredCount: 1,
		  watchingUsersCount: 0,
		  voted: false } }

const INITIAL_WEIGHTS: TheatrePostSelectionWeights =
    { tagPenalties: [], lightestPosts: [], heaviestPosts: [] }

const LAYOUT_STORAGE_KEY = 'theatre-layout-mode'
const TAG_FLOW_STORAGE_KEY = 'theatre-tag-flow'

const LAYOUT_LABELS: Record<TheatreLayoutMode, string> = {
    threeColumns:   '3 列',
    tagsBottom:     '2 列 A 型',
    commentsBottom: '2 列 B 型' }

const TAG_FLOW_LABELS: Record<TagFlow, string> = {
    vertical:   '縦並び',
    horizontal: '横並び' }


const userName = (user: Pick<User, 'id' | 'name'> | null | undefined): string =>
    user ? (user.name || `名もなきニジラー（#${ user.id }）`) : '運営'


const commentBox = (
    comment: TheatreComment,
    programme: TheatreProgramme | null = null,
): ReactNode[] =>
    [(
	<div key={`${ comment.no }-content`} className="w-full">
	  {comment.deleted
	   ? <span className="text-sm font-bold">削除されました．</span>
	   : comment.content}
	</div>),
     (
	<div key={`${ comment.no }-user`} className="w-full text-sm text-right">
	  by {userName (comment.user)}
	</div>),
     (
	<div key={`${ comment.no }-createdAt`} className="w-full text-sm text-right">
	  {dateString (comment.createdAt)}
	</div>),
     (
	<div
	    key={`${ comment.no }-post`}
	    className="mt-1 w-full text-xs text-zinc-500 dark:text-zinc-400">
	  {programme && (
	      <>
		<PrefetchLink to={`/posts/${ programme.post.id }`} className="font-bold hover:underline">
		  {programme.post.title || programme.post.url}
		</PrefetchLink>
		&thinsp;へのコメント
	      </>)}
	</div>)]


const compareTagName = (a: Tag, b: Tag): number =>
    a.name === b.name ? 0 : (a.name < b.name ? -1 : 1)


const tagsByCategory = (tags: Tag[]): Partial<Record<Category, Tag[]>> => {
  const grouped: Partial<Record<Category, Tag[]>> = { }

  for (const tag of tags)
    {
      grouped[tag.category] ??= []
      grouped[tag.category]!.push (tag)
    }

  for (const cat of CATEGORIES)
    grouped[cat]?.sort (compareTagName)

  return grouped
}


const TagList: FC<{ tags: Tag[]; compact?: boolean; flow?: TagFlow }> = (
    { tags, compact, flow = 'vertical' },
) => {
  const grouped = tagsByCategory (tags)

  if (flow === 'horizontal')
    {
      return (
	  <ul className={cn ('flex flex-wrap gap-x-3 gap-y-1', compact && 'text-sm')}>
	    {CATEGORIES.flatMap (cat => grouped[cat] ?? []).map (tag => (
		<li key={tag.id} className="text-left leading-tight">
		  <TagLink tag={tag} withCount={false}/>
		</li>))}
	  </ul>)
    }

  return (
      <div className="space-y-3">
	{CATEGORIES.map (cat => {
	  const rows = grouped[cat] ?? []
	  if (rows.length === 0)
	    return null

	  return (
	      <div key={cat}>
		<div className="mb-1 shrink-0 text-xs font-bold text-zinc-500 dark:text-zinc-400">
		  {CATEGORY_NAMES[cat]}
		</div>
		<ul className={cn ('space-y-1', compact && 'text-sm')}>
		  {rows.map (tag => (
		      <li key={tag.id} className="text-left leading-tight">
			<TagLink tag={tag} withCount={false}/>
		      </li>))}
		</ul>
	      </div>)
	})}
      </div>)
}


type Props = { user: User | null }


const TheatreDetailPage: FC<Props> = ({ user }: Props) => {
  const { id } = useParams ()
  const dialogue = useDialogue ()

  const commentsRef = useRef<HTMLDivElement> (null)
  const embedRef = useRef<NiconicoViewerHandle> (null)
  const loadingRef = useRef (false)
  const theatreInfoRef = useRef<TheatreInfo> (INITIAL_THEATRE_INFO)
  const theatreInfoReceivedAtRef = useRef (performance.now ())
  const videoLengthRef = useRef (0)
  const lastCommentNoRef = useRef (0)

  const [comments, setComments] = useState<TheatreComment[]> ([])
  const [content, setContent] = useState ('')
  const [editingPost, setEditingPost] = useState<Post | null> (null)
  const [loading, setLoading] = useState (false)
  const [programmes, setProgrammes] = useState<TheatreProgramme[]> ([])
  const [sending, setSending] = useState (false)
  const [status, setStatus] = useState (200)
  const [theatre, setTheatre] = useState<Theatre | null> (null)
  const [theatreInfo, setTheatreInfo] = useState<TheatreInfo> (INITIAL_THEATRE_INFO)
  const [post, setPost] = useState<Post | null> (null)
  const [videoLength, setVideoLength] = useState (0)
  const [weights, setWeights] = useState<TheatrePostSelectionWeights> (INITIAL_WEIGHTS)
  const [layoutMode, setLayoutMode] = useState<TheatreLayoutMode> (() => {
    const stored = localStorage.getItem (LAYOUT_STORAGE_KEY)
    return (
	((['threeColumns', 'tagsBottom', 'commentsBottom'] as TheatreLayoutMode[])
	 .includes (stored as TheatreLayoutMode))
	? (stored as TheatreLayoutMode)
	: 'threeColumns')
  })
  const [tagFlow, setTagFlow] = useState<TagFlow> (() => {
    const stored = localStorage.getItem (TAG_FLOW_STORAGE_KEY)
    return (
	(['vertical', 'horizontal'] as TagFlow[]).includes (stored as TagFlow)
	? (stored as TagFlow)
	: 'vertical')
  })
  const { fieldErrors, clearValidationErrors, applyValidationError } =
      useValidationErrors<TheatreCommentField> ()

  const changeLayoutMode = (mode: TheatreLayoutMode) => {
    setLayoutMode (mode)
    localStorage.setItem (LAYOUT_STORAGE_KEY, mode)
  }

  const changeTagFlow = (flow: TagFlow) => {
    setTagFlow (flow)
    localStorage.setItem (TAG_FLOW_STORAGE_KEY, flow)
  }

  const applyTheatreInfo = useCallback ((nextInfo: TheatreInfo) => {
    theatreInfoReceivedAtRef.current = performance.now ()
    setTheatreInfo (nextInfo)
  }, [])

  const currentPostElapsedMs = useCallback (
      (info: TheatreInfo = theatreInfoRef.current): number => {
	if (info.postElapsedMs == null)
	  return 0

	return Math.max (
	    info.postElapsedMs + performance.now () - theatreInfoReceivedAtRef.current,
	    0)
      }, [])

  const refreshProgrammes = useCallback (async () => {
    if (!(id))
      return

    setProgrammes (await apiGet<TheatreProgramme[]> (
	`/theatres/${ id }/programmes`, { params: { limit: '100' } }))
  }, [id])

  const refreshWeights = useCallback (async () => {
    if (!(id))
      return

    setWeights (await apiGet<TheatrePostSelectionWeights> (
	`/theatres/${ id }/post_selection_weights`))
  }, [id])

  const advancePost = useCallback (async () => {
    if (!(id))
      return

    setLoading (true)
    try
    {
      await apiPatch<void> (`/theatres/${ id }/next_post`)
      await refreshProgrammes ()
      await refreshWeights ()
    }
    catch (error)
    {
      console.error (error)
    }
    finally
    {
      setLoading (false)
    }
  }, [id, refreshProgrammes, refreshWeights])

  useEffect (() => {
    loadingRef.current = loading
  }, [loading])

  useEffect (() => {
    theatreInfoRef.current = theatreInfo
  }, [theatreInfo])

  useEffect (() => {
    videoLengthRef.current = videoLength
  }, [videoLength])

  useEffect (() => {
    lastCommentNoRef.current = comments[0]?.no ?? 0
  }, [comments])

  useEffect (() => {
    if (!(id))
      return

    let cancelled = false

    setComments ([])
    setEditingPost (null)
    setPost (null)
    setProgrammes ([])
    setTheatre (null)
    theatreInfoReceivedAtRef.current = performance.now ()
    setTheatreInfo (INITIAL_THEATRE_INFO)
    setVideoLength (0)
    setWeights (INITIAL_WEIGHTS)
    lastCommentNoRef.current = 0

    void (async () => {
      try
      {
	const data = await apiGet<Theatre> (`/theatres/${ id }`)
	if (!(cancelled))
	  setTheatre (data)
      }
      catch (error)
      {
	setStatus (isApiError (error) ? error.response?.status ?? 200 : 200)
      }
    }) ()

    void refreshProgrammes ()
    void refreshWeights ()

    return () => {
      cancelled = true
    }
  }, [id, refreshProgrammes, refreshWeights])

  useEffect (() => {
    if (!(id))
      return

    let cancelled = false
    let running = false

    const tick = async () => {
      if (running)
	return

      running = true

      try
      {
	const newComments = await apiGet<TheatreComment[]> (
	    `/theatres/${ id }/comments`,
	    { params: { no_gt: lastCommentNoRef.current, limit: '20' } })

	if (!(cancelled) && newComments.length > 0)
	  {
	    lastCommentNoRef.current = newComments[0].no
	    setComments (prev => [...newComments, ...prev])
	  }

	const currentInfo = theatreInfoRef.current
	const ended =
	    currentInfo.hostFlg
	    && currentInfo.postStartedAt
	    && videoLengthRef.current > 0
	    && currentPostElapsedMs (currentInfo) > videoLengthRef.current + 3_000

	if (ended)
	  {
	    if (!(cancelled))
	      {
		setTheatreInfo (prev => ({
		    ...prev,
		    postId: null,
		    postStartedAt: null,
		    postElapsedMs: null }))
	      }

	    return
	  }

	const nextInfo = await apiPut<TheatreInfo> (`/theatres/${ id }/watching`)
	if (!(cancelled))
	  applyTheatreInfo (nextInfo)
      }
      catch (error)
      {
	console.error (error)
      }
      finally
      {
	running = false
      }
    }

    tick ()
    const interval = setInterval (() => tick (), 1_500)

    return () => {
      cancelled = true
      clearInterval (interval)
    }
  }, [applyTheatreInfo, currentPostElapsedMs, id])

  useEffect (() => {
    if (!(id) || !(theatreInfo.hostFlg) || loadingRef.current || theatreInfo.postId != null)
      return

    let cancelled = false

    void (async () => {
      await advancePost ()
      if (cancelled)
	setLoading (false)
    }) ()

    return () => {
      cancelled = true
    }
  }, [advancePost, id, theatreInfo.hostFlg, theatreInfo.postId])

  useEffect (() => {
    setVideoLength (0)

    if (theatreInfo.postId == null)
      {
	setPost (null)
	return
      }

    let cancelled = false

    void (async () => {
      try
      {
	const nextPost = await fetchPost (String (theatreInfo.postId))
	if (!(cancelled))
	  setPost (nextPost)
      }
      catch (error)
      {
	console.error (error)
      }
    }) ()

    return () => {
      cancelled = true
    }
  }, [theatreInfo.postId])

  useEffect (() => {
    void refreshProgrammes ()
  }, [refreshProgrammes, theatreInfo.postId])

  const syncPlaybackTime = (currentTimeMs: number): number | void => {
    if (!(theatreInfo.postStartedAt))
      return

    const currentVideoLength = videoLengthRef.current
    if (currentVideoLength <= 0)
      return

    const targetTime = Math.min (
	currentPostElapsedMs (theatreInfo),
	currentVideoLength)

    const drift = Math.abs (currentTimeMs - targetTime)

    if (drift > 5_000)
      embedRef.current?.seek (targetTime)

    return targetTime
  }

  const handlePlaybackError = async () => {
    if (!(theatreInfoRef.current.hostFlg) || loadingRef.current)
      return

    loadingRef.current = true
    try
    {
      await advancePost ()
    }
    finally
    {
      loadingRef.current = false
    }
  }

  const handleVideoReady = (durationMs: number) => {
    const playableDurationMs =
	Number.isFinite (durationMs)
	? durationMs
	: 0

    setVideoLength (playableDurationMs)
    videoLengthRef.current = playableDurationMs

    if (playableDurationMs <= 0)
      {
	void handlePlaybackError ()
	return
      }

    embedRef.current?.play ()
  }

  const handleSkipVote = async () => {
    if (!(id) || !(post))
      return

    setLoading (true)
    try
    {
      const nextInfo =
	  theatreInfo.skipVote.voted
	  ? await apiDelete<TheatreInfo> (
	      `/theatres/${ id }/skip_vote`, { params: { post_id: post.id } })
	  : await apiPut<TheatreInfo> (
	      `/theatres/${ id }/skip_vote`, { post_id: post.id })

      applyTheatreInfo (nextInfo)

      if (nextInfo.skipped)
	{
	  setPost (null)
	  await refreshProgrammes ()
	  await refreshWeights ()
	}
    }
    catch (error)
    {
      if (isApiError (error) && error.response?.status === 409)
	applyTheatreInfo (await apiPut<TheatreInfo> (`/theatres/${ id }/watching`))

      console.error (error)
    }
    finally
    {
      setLoading (false)
    }
  }

  const handleDelete = async (commentNo: number) => {
    try
    {
      await apiDelete (`/theatres/${ id }/comments/${ commentNo }`)
      setComments (prev => {
	const rtn = [...prev]
	const idx = rtn.findIndex (x => x.no === commentNo)
	if (idx >= 0)
	  rtn[idx] = { ...rtn[idx], deleted: true, content: null }
	return rtn
      })
    }
    catch
    {
      ;
    }
  }

  const handleCommentSubmit = async (e: FormEvent) => {
    e.preventDefault ()

    if (!(content))
      return

    try
    {
      setSending (true)
      clearValidationErrors ()
      await apiPost (`/theatres/${ id }/comments`, { content })
      setContent ('')
      commentsRef.current?.scrollTo ({ top: 0, behavior: 'smooth' })
    }
    catch (error)
    {
      applyValidationError (error)
    }
    finally
    {
      setSending (false)
    }
  }

  const skipVote = theatreInfo.skipVote
  const theatreTitle = theatre?.name ? `上映会場『${ theatre.name }』` : '上映会場'
  const postTags = post?.tags ?? []
  const programmesAsc = useMemo (
      () => [...programmes].sort (
	  (a, b) => Date.parse (a.createdAt) - Date.parse (b.createdAt)),
      [programmes])

  const programmeForComment = useCallback ((comment: TheatreComment): TheatreProgramme | null => {
    const commentedAt = Date.parse (comment.createdAt)
    let found: TheatreProgramme | null = null

    for (const programme of programmesAsc)
      {
	const startedAt = Date.parse (programme.createdAt)
	if (startedAt > commentedAt)
	  break

	found = programme
      }

    return found
  }, [programmesAsc])

  if (status >= 400)
    return <ErrorScreen status={status}/>

  const tagPanel = (
      <section className="rounded border-zinc-300 p-4 dark:border-zinc-800">
	<div className="mb-3 flex items-center justify-between gap-3">
	  <h2 className="font-bold">タグ</h2>
	  {layoutMode === 'tagsBottom' && (
	      <div className="hidden gap-2 md:flex">
		{(Object.keys (TAG_FLOW_LABELS) as TagFlow[]).map (flow => (
		    <Button
			key={flow}
			type="button"
			size="sm"
			variant={tagFlow === flow ? 'default' : 'outline'}
			onClick={() => changeTagFlow (flow)}>
		      {TAG_FLOW_LABELS[flow]}
		    </Button>))}
	      </div>)}
	</div>
	{postTags.length === 0
	 ? <div className="text-sm text-zinc-500">タグはありません。</div>
	 : <TagList tags={postTags} flow={layoutMode === 'tagsBottom' ? tagFlow : 'vertical'}/>}
      </section>)

  const commentsPanel = (
      <section className="rounded border-zinc-300 p-4 dark:border-zinc-800">
	<h2 className="mb-3 font-bold">コメント</h2>
	<form onSubmit={handleCommentSubmit}>
	  <input
	      className={inputClass ((fieldErrors.content ?? []).length > 0)}
	      type="text"
	      placeholder="ここにコメントを入力"
	      value={content}
	      onChange={e => setContent (e.target.value)}
	      disabled={sending}/>
	  <FieldError messages={fieldErrors.content}/>
	</form>

	<div
	    ref={commentsRef}
	    className="mt-3 max-h-[48vh] overflow-y-auto rounded border border-zinc-200
		       dark:border-zinc-800">
	  {comments.map (comment => {
	    const commentProgramme = programmeForComment (comment)
	    return (
		<div
		    key={comment.no}
		    className="group relative border-t border-zinc-100 p-2 first:border-t-0
			       hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800">
		  {(user && comment.user?.id === user.id && !(comment.deleted)) && (
		      <button
			  type="button"
			  className="absolute left-1 top-1 hidden rounded px-1 text-red-600
				     hover:bg-red-100 group-hover:inline-block dark:text-red-300
				     dark:hover:bg-red-950"
			  aria-label="コメントを削除"
			  onClick={async e => {
			    e.stopPropagation ()

			    if (!(await dialogue.confirm ({
				title: 'このコメントを削除しますか?',
				description: (
				    <div className="my-3 w-120 rounded border border-black p-2
						    dark:border-white">
				      {commentBox (comment, commentProgramme)}
				    </div>),
				confirmText: '削除',
				variant: 'danger' })))
			      return

			    await handleDelete (comment.no)
			  }}>
			&times;
		      </button>)}
		  {commentBox (comment, commentProgramme)}
		</div>)
	  })}
	</div>
      </section>)

  const participantsPanel = (
      <section className="rounded border-zinc-300 p-4 dark:border-zinc-800">
	<h2 className="mb-3 font-bold">参加者</h2>
	<div className="space-y-1">
	  {theatreInfo.watchingUsers.map (watchingUser => (
	      <div key={watchingUser.id} className="flex justify-between gap-2 text-sm">
		<span>{userName (watchingUser)}</span>
		{watchingUser.id === user?.id && <span className="text-zinc-500">お前</span>}
	      </div>))}
	</div>
      </section>)

  const historyPanel = (
      <section className="rounded border-zinc-300 p-4 dark:border-zinc-800">
	<h2 className="mb-3 font-bold">再生履歴</h2>
	<div className="rounded border border-zinc-300 dark:border-zinc-800 max-h-72
			overflow-y-auto">
	  {programmes.length === 0
	   ? <div className="text-sm text-zinc-500">まだ履歴はありません。</div>
	   : (
	      programmes.map (programme => (
		  <div
		      key={`${ programme.theatreId }-${ programme.position }`}
		      className="border-zinc-100 p-2 text-sm first:border-t-0
				 dark:border-zinc-800">
		    <PrefetchLink
			to={`/posts/${ programme.post.id }`}
			className="font-bold hover:underline">
		      {programme.post.title || programme.post.url}
		    </PrefetchLink>
		    <div className="text-xs text-zinc-500">
		      {dateString (programme.createdAt)}
		    </div>
		  </div>)))}
	</div>
      </section>)

  const weightsPanel = (
      <section className="rounded border-zinc-300 p-4 dark:border-zinc-800">
	<div className="mb-3 flex items-center justify-between gap-3">
	  <h2 className="font-bold">抽選重み</h2>
	  <Button type="button" variant="outline" size="sm" onClick={() => void refreshWeights ()}>
	    更新
	  </Button>
	</div>

	<div className="mx-4 grid gap-16 xl:grid-cols-3">
	  <div>
	    <h3 className="mb-2 text-sm font-bold">出にくいタグ</h3>
	    <div className="space-y-1 text-sm">
	      {weights.tagPenalties.length === 0
	       ? <div className="text-zinc-500">まだ減点はありません。</div>
	       : (
		  weights.tagPenalties.slice (0, 12).map (row => (
		      <div
			  key={row.tag.id}
			  className="grid grid-cols-[minmax(0,1fr)_auto] items-baseline gap-2
				     text-left">
			<div className="min-w-0 text-left">
			  <TagLink tag={row.tag} withCount={false}/>
			</div>
			<span className="font-mono">{row.penalty}</span>
		      </div>)))}
	    </div>
	  </div>

	  <div>
	    <h3 className="mb-2 text-sm font-bold">出にくい候補</h3>
	    <WeightRows rows={weights.lightestPosts}/>
	  </div>

	  <div>
	    <h3 className="mb-2 text-sm font-bold">出やすい候補</h3>
	    <WeightRows rows={weights.heaviestPosts}/>
	  </div>
	</div>
      </section>)

  return (
      <motion.div
	  layout="position"
	  transition={{ layout: { duration: .2, ease: 'easeOut' } }}
	  className="min-h-0 flex-1 overflow-y-auto bg-zinc-50 text-zinc-950
		     md:overflow-hidden dark:bg-zinc-950 dark:text-zinc-50">
	<Helmet>
	  <meta name="robots" content="noindex"/>
	  {theatre && <title>{`${ theatreTitle } | ${ SITE_TITLE }`}</title>}
	</Helmet>

	<div className={cn (
	    'grid min-h-full gap-4 overflow-visible md:h-full md:overflow-hidden',
	    (layoutMode === 'threeColumns'
	     && ['md:grid-cols-[16rem_minmax(0,1fr)_22rem]',
		 'xl:grid-cols-[18rem_minmax(0,1fr)_24rem]']),
	    (layoutMode === 'tagsBottom'
	     && 'md:grid-cols-[minmax(0,1fr)_22rem] xl:grid-cols-[minmax(0,1fr)_24rem]'),
	    (layoutMode === 'commentsBottom'
	     && 'md:grid-cols-[16rem_minmax(0,1fr)] xl:grid-cols-[18rem_minmax(0,1fr)]'))}>
	  {layoutMode !== 'tagsBottom' && (
	      <motion.aside
		  layout="position"
		  className="hidden min-w-0 space-y-4 md:order-none md:block md:overflow-y-auto
			     md:[direction:rtl]">
		<div className="md:[direction:ltr]">
		  {tagPanel}
		</div>
	      </motion.aside>)}

	  <motion.main
	      layout="position"
	      className={cn ('order-1 min-w-0 space-y-4 md:order-none md:overflow-y-auto',
			     layoutMode === 'tagsBottom' && 'md:[direction:rtl]')}>
	    <div className={cn ('space-y-4', layoutMode === 'tagsBottom' && 'md:[direction:ltr]')}>
	      <section className="overflow-hidden rounded border-zinc-300
				  dark:border-zinc-800">
		<div className="flex flex-wrap items-center justify-between gap-3
				border-zinc-200 px-4 py-3 dark:border-zinc-800">
		  <div>
		    <h1 className="text-lg font-bold">{theatreTitle}</h1>
		    <p className="text-sm text-zinc-500 dark:text-zinc-400">
		      同接 {theatreInfo.watchingUsers.length} 人
		    </p>
		  </div>

		  <div className="flex flex-wrap gap-2">
		    <div className="hidden flex-wrap gap-2 md:flex">
		      {(Object.keys (LAYOUT_LABELS) as TheatreLayoutMode[]).map (mode => (
			  <Button
			      key={mode}
			      type="button"
			      size="sm"
			      variant={layoutMode === mode ? 'default' : 'outline'}
			      onClick={() => changeLayoutMode (mode)}>
			    {LAYOUT_LABELS[mode]}
			  </Button>))}
		    </div>

		    <Button
			type="button"
			size="sm"
			variant={skipVote.voted ? 'secondary' : 'destructive'}
			disabled={loading || !(post)}
			onClick={handleSkipVote}>
		      {skipVote.voted ? 'スキップ取消' : 'スキップ'}
		      {` ${ skipVote.votesCount } / ${ skipVote.requiredCount }`}
		    </Button>

		  </div>
		</div>

		<div className="flex justify-center bg-black mx-4">
		  {post ? (
		      <PostEmbed
			  key={post.id}
			  ref={embedRef}
			  post={post}
			  onVideoReady={handleVideoReady}
			  onPlaybackChange={syncPlaybackTime}
			  onError={handlePlaybackError}/>) : (
		      <div className="grid min-h-72 place-items-center text-zinc-400">
			{loading ? '次の投稿を選んでゐます……' : '上映待機中'}
		      </div>)}
		</div>

		<div className="flex flex-wrap items-center justify-between gap-3 px-4 py-3">
		  <div className="min-w-0">
		    <div className="text-xs font-bold text-zinc-500 dark:text-zinc-400">
		      再生中
		    </div>
		    {post ? (
			<PrefetchLink
			    to={`/posts/${ post.id }`}
			    className="font-bold hover:underline">
			  {post.title || post.url}
			</PrefetchLink>) : (
			<span className="text-zinc-500">未選択</span>)}
		  </div>

		  {(post && canEditContent (user)) && (
		      <Button
			  type="button"
			  size="sm"
			  variant="outline"
			  disabled={!(post)}
			  onClick={() => post && setEditingPost (ep => ep ? null : post)}>
			{editingPost ? '閉じる' : '編輯'}
		      </Button>)}
		</div>
	      </section>

	      {editingPost && (
		  <section className="rounded border border-amber-300 bg-amber-50 mx-4 p-4
				      dark:border-amber-800 dark:bg-amber-950/30">
		    <div className="mb-3">
		      <h2 className="font-bold">編輯中の投稿</h2>
		      <p className="text-sm text-amber-900 dark:text-amber-100">
			<PrefetchLink
			    to={`/posts/${ editingPost.id }`}
			    className="mx-1 font-bold underline">
			  {editingPost.title || editingPost.url}
			</PrefetchLink>
			を編輯中……
		      </p>
		    </div>

		    <PostEditForm
			post={editingPost}
			onSave={newPost => {
			  setEditingPost (newPost)
			  if (post?.id === newPost.id)
			    setPost (newPost)
			  void refreshWeights ()
			}}/>
		  </section>)}

	      <div className="md:hidden">
		{commentsPanel}
	      </div>

	      {layoutMode === 'commentsBottom' && (
		  <div className="hidden md:block">
		    {commentsPanel}
		  </div>)}

	      <div className="md:hidden">
		{tagPanel}
	      </div>

	      {layoutMode === 'tagsBottom' && (
		  <div className="hidden md:block">
		    {tagPanel}
		  </div>)}

	      {historyPanel}
	      {weightsPanel}

	      <div className="md:hidden">
		{participantsPanel}
	      </div>

	      {layoutMode === 'commentsBottom' && (
		  <div className="hidden md:block">
		    {participantsPanel}
		  </div>)}
	    </div>
	  </motion.main>

	  {layoutMode !== 'commentsBottom' && (
	      <motion.aside
		  layout="position"
		  className="hidden min-w-0 space-y-4 md:order-none md:block md:overflow-y-auto">
		{commentsPanel}
		{participantsPanel}
	      </motion.aside>)}
	</div>
      </motion.div>)
}


const WeightRows: FC<{ rows: TheatrePostSelectionWeights['lightestPosts'] }> = ({ rows }) => (
    <div className="space-y-2 text-sm">
      {rows.length === 0
       ? <div className="text-zinc-500">候補はありません。</div>
       : (
	  rows.slice (0, 8).map (row => (
	      <div
		  key={row.post.id}
		  className="border-zinc-100 pt-2 first:border-t-0 first:pt-0
			     dark:border-zinc-800">
		<PrefetchLink
		    to={`/posts/${ row.post.id }`}
		    className="line-clamp-1 font-bold hover:underline">
		  {row.post.title || row.post.url}
		</PrefetchLink>
		<div className="flex justify-between gap-2 text-xs text-zinc-500">
		  <span>penalty {row.penalty}</span>
		  <span>weight {row.weight.toFixed (3)}</span>
		</div>
	      </div>)))}
    </div>)


export default TheatreDetailPage
