import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import ScriptEditor from './editor/ScriptEditor.tsx'
import Timeline from './viewer/Timeline.tsx'
import ReportPanel from './viewer/ReportPanel.tsx'
import BlockInspector from './viewer/BlockInspector.tsx'
import { runScript, type RunHandle } from './engine/runner.ts'
import { parseSeq, SeqParseError } from './seq/parseSeq.ts'
import { reconstruct, type Reconstructed } from './seq/reconstruct.ts'
import type { ParsedSeq } from './seq/types.ts'
import { EXAMPLES, findExample } from './examples/index.ts'
import ExamplesModal from './examples/ExamplesModal.tsx'

interface ConsoleLine {
  kind: 'out' | 'err' | 'ok'
  text: string
}

interface LoadedSeq {
  name: string
  text: string
  seq: ParsedSeq
  rec: Reconstructed
  source: 'run' | 'upload'
}

type RunStatus = 'idle' | 'booting' | 'running'

function loadSeqFile(name: string, text: string, source: 'run' | 'upload'): LoadedSeq {
  const seq = parseSeq(text)
  return { name, text, seq, rec: reconstruct(seq), source }
}

function download(name: string, text: string) {
  const url = URL.createObjectURL(new Blob([text], { type: 'text/plain' }))
  const a = document.createElement('a')
  a.href = url
  a.download = name
  a.click()
  URL.revokeObjectURL(url)
}

export default function App() {
  const [script, setScript] = useState(() => {
    const hashExample = readExampleFromHash()
    return (hashExample ?? EXAMPLES[0]).source
  })
  const [scriptName, setScriptName] = useState(() => {
    const hashExample = readExampleFromHash()
    return `${(hashExample ?? EXAMPLES[0]).id}.m`
  })
  const [consoleLines, setConsoleLines] = useState<ConsoleLine[]>([])
  const [runStatus, setRunStatus] = useState<RunStatus>('idle')
  const [loaded, setLoaded] = useState<LoadedSeq[]>([])
  const [activeSeq, setActiveSeq] = useState(0)
  const [selectedBlock, setSelectedBlock] = useState<number | null>(null)
  const [examplesOpen, setExamplesOpen] = useState(false)
  const [dropActive, setDropActive] = useState(false)
  const [leftWidth, setLeftWidth] = useState(46) // percent
  const runRef = useRef<RunHandle | null>(null)
  const consoleBodyRef = useRef<HTMLDivElement>(null)
  const fileInputRef = useRef<HTMLInputElement>(null)

  const appendConsole = useCallback((kind: ConsoleLine['kind'], text: string) => {
    setConsoleLines((lines) => [...lines, { kind, text }])
  }, [])

  useEffect(() => {
    const el = consoleBodyRef.current
    if (el) el.scrollTop = el.scrollHeight
  }, [consoleLines])

  // doRun reads the current script through a ref so its identity is stable
  // (the CodeMirror keymap captures it once).
  const scriptRef = useRef(script)
  scriptRef.current = script

  const doRun = useCallback((src?: unknown) => {
    if (runRef.current) return
    // May be called from the Run button (MouseEvent), the editor keymap (no
    // arg), or loadExample (explicit source string). Only a string overrides
    // the ref, avoiding a race with the setScript state update.
    const source = typeof src === 'string' ? src : scriptRef.current
    setConsoleLines([])
    setRunStatus('booting')
    const handle = runScript(source, {
      onOutput: (text) => {
        setRunStatus('running')
        appendConsole('out', text)
      },
    })
    runRef.current = handle
    handle.promise
      .then((result) => {
        if (result.aborted) {
          appendConsole('err', '— run cancelled —\n')
          return
        }
        if (!result.ok) {
          appendConsole('err', (result.error ?? 'unknown error') + '\n')
        }
        if (result.seqFiles.length > 0) {
          const files: LoadedSeq[] = []
          for (const f of result.seqFiles) {
            try {
              files.push(loadSeqFile(f.name, f.text, 'run'))
            } catch (err) {
              appendConsole(
                'err',
                `failed to parse ${f.name}: ${err instanceof SeqParseError ? err.message : String(err)}\n`,
              )
            }
          }
          setLoaded(files)
          setActiveSeq(0)
          setSelectedBlock(null)
        } else if (result.ok) {
          appendConsole('err', 'The script finished without writing any .seq file — call seq.write(...).\n')
        }
        if (result.ok) {
          appendConsole('ok', `— finished in ${(result.elapsedMs / 1000).toFixed(1)} s —\n`)
        }
      })
      .catch((err) => {
        appendConsole('err', `engine error: ${err instanceof Error ? err.message : String(err)}\n`)
      })
      .finally(() => {
        runRef.current = null
        setRunStatus('idle')
      })
  }, [appendConsole])

  const doCancel = useCallback(() => {
    runRef.current?.cancel()
  }, [])

  const loadExample = useCallback((id: string) => {
    const ex = findExample(id)
    if (!ex) return
    // Load into the editor only; the user presses Run to execute it.
    setScript(ex.source)
    setScriptName(`${ex.id}.m`)
    window.location.hash = `example/${ex.id}`
    setExamplesOpen(false)
  }, [])

  const openSeqFile = useCallback(
    (file: File) => {
      file.text().then(
        (text) => {
          try {
            const f = loadSeqFile(file.name, text, 'upload')
            setLoaded([f])
            setActiveSeq(0)
            setSelectedBlock(null)
          } catch (err) {
            appendConsole(
              'err',
              `failed to parse ${file.name}: ${err instanceof Error ? err.message : String(err)}\n`,
            )
          }
        },
        () => appendConsole('err', `could not read ${file.name}\n`),
      )
    },
    [appendConsole],
  )

  // Drag & drop anywhere
  useEffect(() => {
    let depth = 0
    const onDragEnter = (e: DragEvent) => {
      if (!e.dataTransfer?.types.includes('Files')) return
      depth++
      setDropActive(true)
    }
    const onDragLeave = () => {
      depth = Math.max(0, depth - 1)
      if (depth === 0) setDropActive(false)
    }
    const onDragOver = (e: DragEvent) => {
      if (e.dataTransfer?.types.includes('Files')) e.preventDefault()
    }
    const onDrop = (e: DragEvent) => {
      depth = 0
      setDropActive(false)
      const file = e.dataTransfer?.files?.[0]
      if (file) {
        e.preventDefault()
        openSeqFile(file)
      }
    }
    window.addEventListener('dragenter', onDragEnter)
    window.addEventListener('dragleave', onDragLeave)
    window.addEventListener('dragover', onDragOver)
    window.addEventListener('drop', onDrop)
    return () => {
      window.removeEventListener('dragenter', onDragEnter)
      window.removeEventListener('dragleave', onDragLeave)
      window.removeEventListener('dragover', onDragOver)
      window.removeEventListener('drop', onDrop)
    }
  }, [openSeqFile])

  // Divider drag
  const onDividerDown = useCallback((e: React.MouseEvent) => {
    e.preventDefault()
    const onMove = (ev: MouseEvent) => {
      const pct = (ev.clientX / window.innerWidth) * 100
      setLeftWidth(Math.min(70, Math.max(25, pct)))
    }
    const onUp = () => {
      window.removeEventListener('mousemove', onMove)
      window.removeEventListener('mouseup', onUp)
    }
    window.addEventListener('mousemove', onMove)
    window.addEventListener('mouseup', onUp)
  }, [])

  const active = loaded[activeSeq]
  const running = runStatus !== 'idle'
  const consoleEmpty = consoleLines.length === 0

  const statusLabel = useMemo(() => {
    if (runStatus === 'booting') return 'starting engine…'
    if (runStatus === 'running') return 'running…'
    return null
  }, [runStatus])

  return (
    <div className={`app${dropActive ? ' drop-active' : ''}`}>
      <header className="header">
        <div className="logo">
          <b>seqlab</b>
          <span className="tagline">
            powered by{' '}
            <a href="https://pulseq.github.io/" target="_blank" rel="noopener noreferrer">
              Pulseq
            </a>{' '}
            +{' '}
            <a href="https://numbl.org" target="_blank" rel="noopener noreferrer">
              numbl
            </a>
          </span>
        </div>
        <div className="spacer" />
        <button onClick={() => fileInputRef.current?.click()}>Open .seq…</button>
        <input
          ref={fileInputRef}
          type="file"
          accept=".seq,text/plain"
          style={{ display: 'none' }}
          onChange={(e) => {
            const file = e.target.files?.[0]
            if (file) openSeqFile(file)
            e.target.value = ''
          }}
        />
      </header>

      <div className="main">
        <div className="left" style={{ width: `${leftWidth}%` }}>
          <div className="editor-toolbar">
            {running ? (
              <button className="danger" onClick={doCancel}>
                ■ Cancel
              </button>
            ) : (
              <button className="primary" onClick={doRun} title="Ctrl+Enter">
                ▶ Run
              </button>
            )}
            <button className="examples-btn" onClick={() => setExamplesOpen(true)}>
              ☰ Examples
            </button>
            <span className="name">{scriptName}</span>
            <div className="spacer" style={{ flex: 1 }} />
            <button onClick={() => download(scriptName, scriptRef.current)}>Download .m</button>
          </div>
          <ScriptEditor value={script} onChange={setScript} onRun={doRun} />
          <div className="console">
            <div className="console-head">
              Console
              {statusLabel && <span>· {statusLabel}</span>}
            </div>
            <div className="console-body" ref={consoleBodyRef}>
              {consoleEmpty && !running ? (
                <span className="muted" style={{ color: 'var(--text-dim)' }}>
                  Press Run (Ctrl+Enter) to execute the script with pulseq on the numbl MATLAB
                  runtime — entirely in this tab.
                </span>
              ) : (
                consoleLines.map((l, i) => (
                  <span key={i} className={l.kind === 'out' ? undefined : l.kind}>
                    {l.text}
                  </span>
                ))
              )}
            </div>
          </div>
        </div>

        <div className="divider" onMouseDown={onDividerDown} />

        <div className="right">
          {!active ? (
            <div className="viewer-empty">
              <h2>No sequence loaded</h2>
              <p className="hint">
                Run the script on the left to generate a <code>.seq</code> file, pick an example
                from the gallery, or drop / open an existing <code>.seq</code> file to explore it.
              </p>
            </div>
          ) : (
            <>
              {loaded.length > 1 && (
                <div className="seq-tabs">
                  {loaded.map((f, i) => (
                    <button
                      key={f.name + i}
                      className={i === activeSeq ? 'active' : undefined}
                      onClick={() => {
                        setActiveSeq(i)
                        setSelectedBlock(null)
                      }}
                    >
                      {f.name}
                    </button>
                  ))}
                </div>
              )}
              <div className="viewer-section">
                <h3>
                  {active.name}
                  <span className="free" />
                  <span className="fine">
                    {active.source === 'run' ? 'generated by script' : 'opened file'}
                  </span>
                  <button onClick={() => download(active.name, active.text)}>Download .seq</button>
                </h3>
                <ReportPanel seq={active.seq} rec={active.rec} />
              </div>
              <div className="viewer-section">
                <h3>
                  Timeline
                  <span className="free" />
                  <span className="fine">scroll to zoom · drag to pan · click a block · double-click resets</span>
                </h3>
                <Timeline
                  rec={active.rec}
                  selectedBlock={selectedBlock}
                  onSelectBlock={setSelectedBlock}
                />
              </div>
              <div className="viewer-section">
                <h3>
                  Block inspector
                  <span className="free" />
                  {selectedBlock !== null && (
                    <button onClick={() => setSelectedBlock(null)}>Close</button>
                  )}
                </h3>
                {selectedBlock !== null ? (
                  <BlockInspector
                    seq={active.seq}
                    rec={active.rec}
                    blockIndex={selectedBlock}
                    onNavigate={(i) =>
                      setSelectedBlock(Math.min(Math.max(i, 0), active.seq.blocks.length - 1))
                    }
                  />
                ) : (
                  <p className="inspector-hint">
                    Click a block in the timeline above to inspect its events.
                  </p>
                )}
              </div>
            </>
          )}
        </div>
      </div>

      {examplesOpen && (
        <ExamplesModal
          examples={EXAMPLES}
          onPick={loadExample}
          onClose={() => setExamplesOpen(false)}
        />
      )}
    </div>
  )
}

function readExampleFromHash() {
  const m = window.location.hash.match(/^#example\/([\w-]+)/)
  return m ? findExample(m[1]) : undefined
}
