
import React from 'react'
import { BaseBoxShapeTool, BaseBoxShapeUtil, Edge2d, HTMLContainer, ShapeUtil, T, TLBaseBoxShape, Vec, toDomPrecision } from '@tldraw/tldraw'
import type { TLHandle } from '@tldraw/tldraw'
import { PAGE_WIDTH } from '../../../utils/exam-canvas/model'
import type { ExamCanvasRegionKind, ExamCanvasShapeKind } from '../../../utils/exam-canvas/model'
import type { ExamTemplateSource } from '../../../types/exam.types'

export const PDF_PAGE_SHAPE_TYPE = 'exam-pdf-page'

export const SHAPE_TYPES = {
  boundary: 'exam-boundary',
  part: 'exam-part',
  response: 'exam-region-response',
  context: 'exam-region-context',
  question_number: 'exam-region-question-number',
  mark_area: 'exam-region-mark-area',
  reference: 'exam-region-reference',
  furniture: 'exam-region-furniture',
} as const

export type ExamPdfPageTLShape = TLBaseBoxShape & {
  type: typeof PDF_PAGE_SHAPE_TYPE
  props: { w: number; h: number; src: string; pageNumber: number }
}

export type ExamCanvasTLShape = TLBaseBoxShape & {
  type: typeof SHAPE_TYPES[keyof typeof SHAPE_TYPES]
  props: {
    w: number
    h: number
    label: string
    kind: ExamCanvasShapeKind
    maxMarks?: number
    responseForm?: string
    contextType?: string
    questionId?: string | null
    parentId?: string | null
    isContainer?: boolean
    depth?: number
    description?: string
    linkLabel?: string
    metaJson?: string
    domainId?: string
    source?: 'manual' | 'ai'
    confirmed?: boolean
    confidence?: number | null
    derivation?: string | null
    reviewFlags?: string
  }
}

type CanvasPaletteEntry = {
  stroke: string
  fill: string
  darkStroke: string
  darkFill: string
  dash?: string
  label: string
  icon: string
  role: string
}

export const canvasShapePalette: Record<ExamCanvasShapeKind, CanvasPaletteEntry> = {
  boundary: { stroke: '#ef4444', fill: 'rgba(239,68,68,0.06)', darkStroke: '#f87171', darkFill: 'rgba(248,113,113,0.10)', dash: '8 6', label: 'Boundary', icon: '↕', role: 'start/end rule' },
  part: { stroke: '#f59e0b', fill: 'rgba(245,158,11,0.18)', darkStroke: '#fbbf24', darkFill: 'rgba(251,191,36,0.26)', label: 'Part', icon: '□', role: 'markable box' },
  response: { stroke: '#2563eb', fill: 'rgba(37,99,235,0.18)', darkStroke: '#60a5fa', darkFill: 'rgba(96,165,250,0.34)', label: 'Response', icon: '✎', role: 'student writing' },
  context: { stroke: '#7c3aed', fill: 'rgba(124,58,237,0.14)', darkStroke: '#a78bfa', darkFill: 'rgba(167,139,250,0.28)', dash: '6 5', label: 'Context', icon: '◉', role: 'stimulus' },
  question_number: { stroke: '#0f766e', fill: 'rgba(15,118,110,0.14)', darkStroke: '#2dd4bf', darkFill: 'rgba(45,212,191,0.24)', label: 'Question #', icon: '#', role: 'printed label' },
  mark_area: { stroke: '#16a34a', fill: 'rgba(22,163,74,0.14)', darkStroke: '#4ade80', darkFill: 'rgba(74,222,128,0.23)', label: 'Marks', icon: '[2]', role: 'printed marks' },
  reference: { stroke: '#0891b2', fill: 'rgba(8,145,178,0.14)', darkStroke: '#22d3ee', darkFill: 'rgba(34,211,238,0.24)', label: 'Reference', icon: '§', role: 'resource' },
  furniture: { stroke: '#64748b', fill: 'rgba(100,116,139,0.12)', darkStroke: '#cbd5e1', darkFill: 'rgba(148,163,184,0.18)', dash: '3 5', label: 'Furniture', icon: '×', role: 'ignore' },
}

// Human-legible names for the extraction service's response forms — so the canvas SHOWS what was
// recognised (multiple-choice, draw-on, table…), not just a generic "Response" box.
const RESPONSE_FORM_LABEL: Record<string, string> = {
  lines: 'ruled lines',
  'answer-box': 'answer box',
  working: 'working space',
  diagram: 'draw-on',
  'tick-boxes': 'multiple choice',
  table: 'table',
  blanks: 'fill-in',
}

const shapeCss = `
.exam-canvas-shape { --exam-stroke: var(--exam-light-stroke); --exam-fill: var(--exam-light-fill); }
[data-color-mode="dark"] .exam-canvas-shape, .tl-theme__dark .exam-canvas-shape { --exam-stroke: var(--exam-dark-stroke); --exam-fill: var(--exam-dark-fill); }
.exam-canvas-shape__pill { background: rgba(255,255,255,.90); color: var(--exam-stroke); box-shadow: 0 1px 4px rgba(15,23,42,.14); }
.exam-canvas-shape__flag { background: rgba(251,191,36,.94); color: #78350f; box-shadow: 0 1px 4px rgba(120,53,15,.18); }
.exam-canvas-shape__confidence { background: rgba(15,23,42,.82); color: #fff; }
[data-color-mode="dark"] .exam-canvas-shape__pill, .tl-theme__dark .exam-canvas-shape__pill { background: rgba(15,23,42,.88); color: var(--exam-stroke); box-shadow: 0 1px 5px rgba(0,0,0,.35); }
[data-color-mode="dark"] .exam-canvas-shape__flag, .tl-theme__dark .exam-canvas-shape__flag { background: rgba(251,191,36,.88); color: #422006; }
`


function confidenceLabel(confidence: number | null | undefined) {
  return typeof confidence === 'number' ? `${Math.round(confidence * 100)}%` : null
}

// Unconfirmed AI ghosts fade by confidence (fainter = less certain); confirmed/manual render solid.
function ghostOpacity(confidence: number | null | undefined) {
  if (typeof confidence !== 'number') return 0.72
  return 0.5 + 0.4 * Math.max(0, Math.min(1, confidence))
}

function reviewFlags(shape: ExamCanvasTLShape): string[] {
  return (shape.props.reviewFlags ?? '').split('|').map((flag) => flag.trim()).filter(Boolean)
}

function provenanceTitle(shape: ExamCanvasTLShape, base: string) {
  const bits = [base]
  if (shape.props.source === 'ai') bits.push(shape.props.confirmed === false ? 'AI suggestion, unconfirmed' : 'AI, confirmed')
  const confidence = confidenceLabel(shape.props.confidence)
  if (confidence) bits.push(`confidence ${confidence}`)
  if (shape.props.derivation) bits.push(`derivation: ${shape.props.derivation}`)
  const flags = reviewFlags(shape)
  if (flags.length) bits.push(`review flags: ${flags.join(', ')}`)
  return bits.join(' • ')
}

function renderBoundaryLine(shape: ExamCanvasTLShape) {
  const p = canvasShapePalette.boundary
  const lineY = Math.max(1, Math.min(shape.props.h - 1, shape.props.h / 2))
  const isAi = shape.props.source === 'ai'
  const isGhost = isAi && shape.props.confirmed === false
  const confidence = confidenceLabel(shape.props.confidence)
  const flags = reviewFlags(shape)
  return (
    <HTMLContainer id={shape.id} style={{ width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h), overflow: 'visible', pointerEvents: 'all' }}>
      <style>{shapeCss}</style>
      <svg width={toDomPrecision(shape.props.w)} height={toDomPrecision(shape.props.h)} aria-label={`${p.label}: ${p.role}`} style={{ display: 'block', overflow: 'visible' }}>
        <line x1={0} x2={toDomPrecision(shape.props.w)} y1={lineY} y2={lineY} stroke="var(--exam-stroke)" strokeWidth={2.5} strokeDasharray={isGhost ? '4 6' : p.dash} strokeLinecap="round" opacity={isGhost ? ghostOpacity(shape.props.confidence) : 1} style={{ '--exam-light-stroke': p.stroke, '--exam-dark-stroke': p.darkStroke } as React.CSSProperties} />
      </svg>
      <span className="exam-canvas-shape__pill" style={{ position: 'absolute', left: 8, top: -24, fontSize: 11, fontWeight: 900, textTransform: 'uppercase', letterSpacing: 0.6, borderRadius: 999, padding: '2px 7px', display: 'inline-flex', alignItems: 'center', gap: 5, color: p.stroke }}>
        <span aria-hidden="true">{isGhost ? 'AI' : p.icon}</span>
        {shape.props.label || p.label}
      </span>
      {confidence && <span className="exam-canvas-shape__pill exam-canvas-shape__confidence" style={{ position: 'absolute', right: 8, top: -24, fontSize: 11, fontWeight: 900, borderRadius: 999, padding: '2px 7px' }}>{confidence}</span>}
      {flags.slice(0, 2).map((flag, index) => <span key={flag} className="exam-canvas-shape__pill exam-canvas-shape__flag" style={{ position: 'absolute', left: 8, top: 10 + index * 22, fontSize: 10, fontWeight: 900, borderRadius: 999, padding: '1px 6px' }}>{flag}</span>)}
    </HTMLContainer>
  )
}

// Distinct look for a container box (main question / intermediate part). It frames its children rather than
// filling over them: transparent interior + a coloured border whose hue shifts by nesting depth, so
// question ⊃ part ⊃ subpart reads as nested frames on the paper.
const CONTAINER_DEPTH_STROKE = ['#4f46e5', '#0d9488', '#b45309']
function containerStroke(depth: number) { return CONTAINER_DEPTH_STROKE[Math.min(depth, CONTAINER_DEPTH_STROKE.length - 1)] }

function marksPill(maxMarks: number | undefined) {
  if (typeof maxMarks !== 'number' || maxMarks <= 0) return null
  return (
    <span className="exam-canvas-shape__pill" style={{ fontSize: 11, fontWeight: 900, borderRadius: 999, padding: '2px 7px', display: 'inline-flex', alignItems: 'center', gap: 3 }} title={`${maxMarks} mark${maxMarks === 1 ? '' : 's'}`}>
      <span aria-hidden="true" style={{ opacity: 0.7 }}>◆</span>{maxMarks}
    </span>
  )
}

function parseMeta(shape: ExamCanvasTLShape): Record<string, any> | null {
  try { return shape.props.metaJson ? JSON.parse(shape.props.metaJson) : null } catch { return null }
}

// Draw the recognised answer sub-structure INSIDE a response box, using the box-relative [0,1] coords the
// contract now carries: numbered sub-answer bands ("suggest two" → 1, 2), per-option MC selection boxes, and
// the final-answer line's expected quantity/unit.
function renderResponseSubstructure(shape: ExamCanvasTLShape): React.ReactNode {
  const m = parseMeta(shape)
  if (!m) return null
  const pct = (v: any, d = 0) => `${Math.max(0, Math.min(1, Number(v ?? d))) * 100}%`
  const els: React.ReactNode[] = []
  const comps: any[] = Array.isArray(m.components) ? m.components : []
  comps.forEach((c, i) => {
    if (i === 0) return   // first band starts at the box top; a divider there is noise
    els.push(<div key={`cd${i}`} style={{ position: 'absolute', left: 0, right: 0, top: pct(c.rel_y0), borderTop: '1px dashed var(--exam-stroke)', opacity: 0.45 }} />)
  })
  comps.forEach((c, i) => els.push(
    <span key={`cl${i}`} className="exam-canvas-shape__pill" style={{ position: 'absolute', left: 4, top: `calc(${pct(c.rel_y0)} + 2px)`, fontSize: 10, fontWeight: 900, borderRadius: 6, padding: '0 5px', opacity: 0.95 }}>{c.label ?? i + 1}</span>
  ))
  const boxes: any[] = Array.isArray(m.boxes) ? m.boxes : []
  boxes.forEach((b, i) => {
    const rel = b.rel || {}
    const filled = Number(b.fill ?? 0) > 0.35
    els.push(<div key={`bx${i}`} title={`option ${i + 1}${filled ? ' (marked)' : ''}`} style={{ position: 'absolute', left: pct(rel.x0), top: pct(rel.y0), width: pct(Number(rel.x1) - Number(rel.x0)), height: pct(Number(rel.y1) - Number(rel.y0)), border: '1.5px solid var(--exam-stroke)', borderRadius: 3, background: filled ? 'var(--exam-stroke)' : 'transparent', opacity: filled ? 0.5 : 0.85, boxSizing: 'border-box' }} />)
  })
  // "tick N" instruction for multiple-choice (select_n = how many boxes to tick)
  const selectN = Number(m.select_n ?? 0)
  if (selectN > 0 && boxes.length) {
    els.push(<span key="tickn" className="exam-canvas-shape__pill" style={{ position: 'absolute', left: 4, top: 2, fontSize: 10, fontWeight: 900, borderRadius: 6, padding: '0 5px', opacity: 0.95 }}>tick {selectN}</span>)
  }
  const fal = m.final_answer_line
  if (fal && (fal.unit || fal.quantity)) {
    els.push(<span key="fal" className="exam-canvas-shape__pill" style={{ position: 'absolute', right: 4, top: `calc(${pct(fal.rel_y, 0.9)} - 8px)`, maxWidth: 'calc(100% - 8px)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontSize: 10, fontWeight: 800, borderRadius: 6, padding: '0 5px', opacity: 0.95 }} title="expected final answer">{[fal.quantity, fal.unit && `(${fal.unit})`].filter(Boolean).join(' ')}</span>)
  }
  return els.length ? <>{els}</> : null
}

// A container question is a BACKDROP, not a big box over the content: the main question (depth 0) relies on
// its start/end boundary rules for extent and shows only a small corner tab (label + total marks); an
// intermediate part-group (depth ≥ 1) also gets a faint dashed frame to convey nesting. Non-interactive so it
// never blocks a hover on the figures/responses on top.
function renderContainerTab(shape: ExamCanvasTLShape) {
  const depth = shape.props.depth ?? 0
  const stroke = containerStroke(depth)
  const isAiSuggestion = shape.props.source === 'ai' && shape.props.confirmed === false
  const marks = shape.props.maxMarks
  const title = provenanceTitle(shape, depth === 0 ? 'Question (container)' : 'Part group (container)')
  return (
    <HTMLContainer id={shape.id} style={{ width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h), pointerEvents: 'none', overflow: 'visible' }}>
      <style>{shapeCss}</style>
      {depth > 0 && <div style={{ position: 'absolute', inset: 0, border: `1px dashed ${stroke}`, borderRadius: 12, opacity: 0.3, boxSizing: 'border-box' }} />}
      <span className="exam-canvas-shape__pill" title={title} style={{ position: 'absolute', left: 0, top: -3, display: 'inline-flex', alignItems: 'center', gap: 5, fontSize: 11, fontWeight: 900, borderRadius: 8, padding: '2px 7px', color: stroke, border: `1.5px solid ${stroke}`, background: 'rgba(255,255,255,0.92)', whiteSpace: 'nowrap' }}>
        <span aria-hidden="true">{isAiSuggestion ? 'AI' : (depth === 0 ? '▣' : '▢')}</span>
        {shape.props.label}
        {typeof marks === 'number' && marks > 0 && <span style={{ opacity: 0.85 }}>· {marks}m</span>}
      </span>
    </HTMLContainer>
  )
}

function renderShape(shape: ExamCanvasTLShape) {
  const kind = shape.props.kind
  const p = canvasShapePalette[kind] ?? canvasShapePalette.response
  const isBoundary = kind === 'boundary'
  const isAiSuggestion = shape.props.source === 'ai' && shape.props.confirmed === false
  if (isBoundary) return renderBoundaryLine(shape)
  const isContainer = shape.props.isContainer === true && kind === 'part'
  if (isContainer) return renderContainerTab(shape)
  const isPreamble = kind === 'context' && shape.props.contextType === 'preamble'
  const depth = shape.props.depth ?? 0
  const stroke = isContainer ? containerStroke(depth) : p.stroke
  const darkStroke = isContainer ? containerStroke(depth) : p.darkStroke
  const confidence = confidenceLabel(shape.props.confidence)
  const flags = reviewFlags(shape)
  const roleLabel = isContainer ? (depth === 0 ? 'Question: container' : 'Part: container') : `${p.label}: ${p.role}`
  const title = shape.props.description ? `${provenanceTitle(shape, roleLabel)} • ${shape.props.description}` : provenanceTitle(shape, roleLabel)
  return (
    <HTMLContainer id={shape.id} style={{ width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h), pointerEvents: isPreamble ? 'none' : 'all' }}>
      <style>{shapeCss}</style>
      <div
        className={`exam-canvas-shape exam-canvas-shape--${kind}${isContainer ? ' exam-canvas-shape--container' : ''}`}
        style={{
          '--exam-light-stroke': stroke,
          '--exam-light-fill': p.fill,
          '--exam-dark-stroke': darkStroke,
          '--exam-dark-fill': p.darkFill,
          width: '100%', height: '100%', boxSizing: 'border-box', border: `${isContainer ? 2.5 : 1.5}px ${isContainer ? 'solid' : (isAiSuggestion ? 'dashed' : p.dash ? 'dashed' : 'solid')} var(--exam-stroke)`,
          borderRadius: isContainer ? 14 : 10,
          background: isContainer ? 'transparent' : 'var(--exam-fill)', opacity: isAiSuggestion ? ghostOpacity(shape.props.confidence) : 1, color: 'var(--exam-stroke)', fontFamily: 'Inter, system-ui, sans-serif',
          display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between',
          padding: 8, boxShadow: isContainer ? 'none' : '0 10px 22px rgba(15,23,42,0.10)', overflow: 'hidden', gap: 6,
        } as React.CSSProperties}
        aria-label={title}
        title={title}
      >
        {kind === 'response' && renderResponseSubstructure(shape)}
        <span className="exam-canvas-shape__pill" style={{ fontSize: 12, fontWeight: 900, textTransform: 'uppercase', letterSpacing: 0.6, borderRadius: 999, padding: '2px 7px', display: 'inline-flex', alignItems: 'center', gap: 5, maxWidth: 'calc(100% - 60px)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', zIndex: 1 }}>
          <span aria-hidden="true">{isAiSuggestion ? 'AI' : (isContainer ? (depth === 0 ? '▣' : '▢') : p.icon)}</span>
          {shape.props.label || p.label}
        </span>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 4, flexShrink: 0 }}>
          {marksPill(shape.props.maxMarks)}
          {confidence && <span className="exam-canvas-shape__pill exam-canvas-shape__confidence" style={{ fontSize: 11, fontWeight: 900, borderRadius: 999, padding: '2px 7px' }}>{confidence}</span>}
          {!confidence && !marksPill(shape.props.maxMarks) && shape.props.questionId && kind !== 'context' && <span className="exam-canvas-shape__pill" style={{ fontSize: 11, fontWeight: 800, borderRadius: 999, padding: '2px 7px' }}>Attached</span>}
        </span>
        {flags.length > 0 && <span className="exam-canvas-shape__pill exam-canvas-shape__flag" style={{ position: 'absolute', left: 8, bottom: 8, fontSize: 10, fontWeight: 900, borderRadius: 999, padding: '1px 6px', maxWidth: 'calc(100% - 16px)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{flags.slice(0, 2).join(' · ')}</span>}
        {kind === 'response' && shape.props.responseForm && <span className="exam-canvas-shape__pill" style={{ position: 'absolute', right: 8, bottom: 8, fontSize: 10, fontWeight: 800, borderRadius: 999, padding: '1px 6px', opacity: 0.9 }}>{RESPONSE_FORM_LABEL[shape.props.responseForm] ?? shape.props.responseForm}</span>}
        {/* Context region: a visible tether to its owning question + the figure kind. */}
        {kind === 'context' && shape.props.linkLabel && <span className="exam-canvas-shape__pill" style={{ position: 'absolute', left: 8, bottom: 8, fontSize: 10, fontWeight: 800, borderRadius: 999, padding: '1px 6px', opacity: 0.95 }}>→ {shape.props.linkLabel}</span>}
        {kind === 'context' && shape.props.contextType && shape.props.contextType !== 'generic' && <span className="exam-canvas-shape__pill" style={{ position: 'absolute', right: 8, bottom: 8, fontSize: 10, fontWeight: 800, borderRadius: 999, padding: '1px 6px', opacity: 0.9 }}>{shape.props.contextType}</span>}
      </div>
    </HTMLContainer>
  )
}

function defaultProps(kind: ExamCanvasShapeKind, w: number, h: number) {
  const p = canvasShapePalette[kind]
  return { w, h, label: p.label, kind, responseForm: kind === 'response' ? 'lines' : undefined, contextType: kind === 'context' ? 'generic' : undefined, source: 'manual' as const, confirmed: true }
}

const sharedProps = { w: T.number, h: T.number, label: T.string, kind: T.string, maxMarks: T.optional(T.number), responseForm: T.optional(T.string), contextType: T.optional(T.string), questionId: T.optional(T.string), parentId: T.optional(T.string), isContainer: T.optional(T.boolean), depth: T.optional(T.number), description: T.optional(T.string), linkLabel: T.optional(T.string), metaJson: T.optional(T.string), domainId: T.optional(T.string), source: T.optional(T.string), confirmed: T.optional(T.boolean), confidence: T.optional(T.number), derivation: T.optional(T.string), reviewFlags: T.optional(T.string) }
const ind = (s: ExamCanvasTLShape | ExamPdfPageTLShape) => <rect width={toDomPrecision(s.props.w)} height={toDomPrecision(s.props.h)} />

class PdfPageUtil extends BaseBoxShapeUtil<ExamPdfPageTLShape> {
  static override type = PDF_PAGE_SHAPE_TYPE
  static override props = { w: T.number, h: T.number, src: T.string, pageNumber: T.number }
  override getDefaultProps() { return { w: 780, h: 1100, src: '', pageNumber: 1 } }
  override canEdit() { return false }
  override component(shape: ExamPdfPageTLShape) {
    return (
      <HTMLContainer id={shape.id} style={{ width: toDomPrecision(shape.props.w), height: toDomPrecision(shape.props.h), pointerEvents: 'none' }}>
        <img src={shape.props.src} alt={'PDF page ' + shape.props.pageNumber} draggable={false} style={{ width: '100%', height: '100%', display: 'block', userSelect: 'none', pointerEvents: 'none', boxShadow: '0 2px 16px rgba(15,23,42,0.18)', background: '#fff' }} />
      </HTMLContainer>
    )
  }
  override indicator(shape: ExamPdfPageTLShape) { return ind(shape) }
}
class BoundaryUtil extends ShapeUtil<ExamCanvasTLShape> {
  static override type = SHAPE_TYPES.boundary
  static override props = sharedProps

  override getDefaultProps() { return defaultProps('boundary', PAGE_WIDTH, 8) }
  override canEdit() { return false }
  override canResize() { return false }
  override canBind() { return false }
  override hideResizeHandles() { return true }
  override hideRotateHandle() { return true }
  override hideSelectionBoundsBg() { return true }

  private pageSpanForY(y: number) {
    const pages = this.editor.getCurrentPageShapes().filter((shape): shape is ExamPdfPageTLShape => shape.type === PDF_PAGE_SHAPE_TYPE)
    const hit = pages.find((page) => y >= page.y && y <= page.y + page.props.h)
    const nearest = hit ?? pages.reduce<ExamPdfPageTLShape | null>((best, page) => {
      if (!best) return page
      const pageDy = Math.min(Math.abs(y - page.y), Math.abs(y - (page.y + page.props.h)))
      const bestDy = Math.min(Math.abs(y - best.y), Math.abs(y - (best.y + best.props.h)))
      return pageDy < bestDy ? page : best
    }, null)
    return nearest ? { x: nearest.x, w: nearest.props.w } : { x: 0, w: PAGE_WIDTH }
  }

  private normalize(shape: ExamCanvasTLShape): ExamCanvasTLShape {
    const span = this.pageSpanForY(shape.y + shape.props.h / 2)
    return { ...shape, x: span.x, rotation: 0, props: { ...shape.props, w: span.w, h: 8, kind: 'boundary' } }
  }

  override getGeometry(shape: ExamCanvasTLShape) {
    const y = shape.props.h / 2
    return new Edge2d({ start: new Vec(0, y), end: new Vec(shape.props.w, y) })
  }

  override getHandles(shape: ExamCanvasTLShape): TLHandle[] {
    return [{ id: 'y', type: 'vertex', index: 'a1' as any, x: shape.props.w / 2, y: shape.props.h / 2, canSnap: false }]
  }

  override onBeforeCreate(next: ExamCanvasTLShape) { return this.normalize(next) }
  override onBeforeUpdate(_prev: ExamCanvasTLShape, next: ExamCanvasTLShape) { return this.normalize(next) }
  override onTranslate(initial: ExamCanvasTLShape, current: ExamCanvasTLShape): any {
    return this.normalize({ ...current, x: initial.x })
  }
  override onHandleDrag(shape: ExamCanvasTLShape, { handle }: { handle: TLHandle }): any {
    return this.normalize({ ...shape, y: shape.y + handle.y - shape.props.h / 2 })
  }
  override component(shape: ExamCanvasTLShape) { return renderShape(shape) }
  override indicator(shape: ExamCanvasTLShape) { return <path d={`M 0 ${toDomPrecision(shape.props.h / 2)} L ${toDomPrecision(shape.props.w)} ${toDomPrecision(shape.props.h / 2)}`} /> }
}
class PartUtil extends BaseBoxShapeUtil<ExamCanvasTLShape> { static override type = SHAPE_TYPES.part; static override props = sharedProps; override getDefaultProps(){ return defaultProps('part', 420, 170) }; override component(shape: ExamCanvasTLShape){ return renderShape(shape) }; override indicator(shape: ExamCanvasTLShape){ return ind(shape) } }
function regionUtil(type: string, kind: ExamCanvasRegionKind, w = 360, h = 120) { return class extends BaseBoxShapeUtil<ExamCanvasTLShape> { static override type = type; static override props = sharedProps; override getDefaultProps(){ return defaultProps(kind, w, h) }; override component(shape: ExamCanvasTLShape){ return renderShape(shape) }; override indicator(shape: ExamCanvasTLShape){ return ind(shape) } } }

class BoundaryTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.boundary; static override initial = 'pointing'; shapeType = SHAPE_TYPES.boundary }
class PartTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.part; static override initial = 'pointing'; shapeType = SHAPE_TYPES.part }
class ResponseTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.response; static override initial = 'pointing'; shapeType = SHAPE_TYPES.response }
class ContextTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.context; static override initial = 'pointing'; shapeType = SHAPE_TYPES.context }
class QuestionNumberTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.question_number; static override initial = 'pointing'; shapeType = SHAPE_TYPES.question_number }
class MarkAreaTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.mark_area; static override initial = 'pointing'; shapeType = SHAPE_TYPES.mark_area }
class ReferenceTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.reference; static override initial = 'pointing'; shapeType = SHAPE_TYPES.reference }
class FurnitureTool extends BaseBoxShapeTool { static override id = SHAPE_TYPES.furniture; static override initial = 'pointing'; shapeType = SHAPE_TYPES.furniture }

export const examCanvasShapeUtils = [PdfPageUtil, BoundaryUtil, PartUtil, regionUtil(SHAPE_TYPES.response, 'response'), regionUtil(SHAPE_TYPES.context, 'context'), regionUtil(SHAPE_TYPES.question_number, 'question_number', 170, 80), regionUtil(SHAPE_TYPES.mark_area, 'mark_area', 170, 80), regionUtil(SHAPE_TYPES.reference, 'reference'), regionUtil(SHAPE_TYPES.furniture, 'furniture')] as const
export const examCanvasTools = [BoundaryTool, PartTool, ResponseTool, ContextTool, QuestionNumberTool, MarkAreaTool, ReferenceTool, FurnitureTool] as const

export function isPdfPageShape(type: string): boolean {
  return type === PDF_PAGE_SHAPE_TYPE
}

export function shapeTypeToKind(type: string): ExamCanvasShapeKind | null {
  const entry = Object.entries(SHAPE_TYPES).find(([, v]) => v === type)
  return (entry?.[0] as ExamCanvasShapeKind | undefined) ?? null
}
