import { useState, useRef, useEffect } from 'react'
import { useStore } from '@nanostores/react'
import { Square, Archive, Dices, Sparkles } from 'lucide-react'
import { PlaybackManager } from './services/PlaybackManager'
import { DownloadService } from './services/DownloadService'
import { generateTileGrid, generateRandomFormula } from './utils/bytebeatFormulas'
import { BytebeatTile } from './components/BytebeatTile'
import { EffectsBar } from './components/EffectsBar'
import { EngineControls } from './components/EngineControls'
import { FormulaEditor } from './components/FormulaEditor'
import { LFOPanel } from './components/LFOPanel'
import { AudioContextWarning } from './components/AudioContextWarning'
import { HelpModal } from './components/HelpModal'
import { getSampleRateFromIndex } from './config/effects'
import { engineSettings, effectSettings, lfoSettings } from './stores/settings'
import { exitMappingMode } from './stores/mappingMode'
import { useKeyboardShortcuts } from './hooks/useKeyboardShortcuts'
import { useTileParams } from './hooks/useTileParams'
import type { TileState } from './types/tiles'
import { createTileStateFromCurrent, loadTileParams, randomizeTileParams } from './utils/tileState'
import { DEFAULT_VARIABLES, PLAYBACK_ID, TILE_GRID, DEFAULT_DOWNLOAD_OPTIONS, LOOP_DURATION } from './constants/defaults'
import { getTileId, getTileFromGrid, type FocusedTile } from './utils/tileHelpers'

function App() {
  const engineValues = useStore(engineSettings)
  const effectValues = useStore(effectSettings)

  const [tiles, setTiles] = useState<TileState[][]>(() =>
    generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity)
  )
  const [playing, setPlaying] = useState<string | null>(null)
  const [queued, setQueued] = useState<string | null>(null)
  const [playbackPosition, setPlaybackPosition] = useState<number>(0)
  const [downloading, setDownloading] = useState(false)
  const [focusedTile, setFocusedTile] = useState<FocusedTile>({ row: 0, col: 0 })
  const [customTile, setCustomTile] = useState<TileState>(() => createTileStateFromCurrent('t*(8&t>>9)'))
  const [showWarning, setShowWarning] = useState(true)
  const [showHelp, setShowHelp] = useState(false)
  const playbackManagerRef = useRef<PlaybackManager | null>(null)
  const downloadServiceRef = useRef<DownloadService>(new DownloadService())
  const switchTimerRef = useRef<number | null>(null)

  const { saveCurrentTileParams } = useTileParams({ tiles, setTiles, customTile, setCustomTile, focusedTile })

  useEffect(() => {
    if (playbackManagerRef.current) {
      playbackManagerRef.current.setPlaybackPositionCallback(setPlaybackPosition)
    }
  }, [])

  useEffect(() => {
    effectSettings.setKey('masterVolume', engineValues.masterVolume)
  }, [engineValues.masterVolume])

  const clearSwitchTimer = () => {
    if (switchTimerRef.current !== null) {
      clearTimeout(switchTimerRef.current)
      switchTimerRef.current = null
    }
  }

  const startSwitchTimer = (queuedId: string) => {
    clearSwitchTimer()

    switchTimerRef.current = window.setTimeout(() => {
      const [rowStr, colStr] = queuedId.split('-')
      const row = parseInt(rowStr, 10)
      const col = parseInt(colStr, 10)
      const tile = getTileFromGrid(tiles, row, col)

      if (tile) {
        playFormula(tile.formula, queuedId)
      }
    }, engineValues.loopCount * 1000)
  }

  useEffect(() => {
    return () => clearSwitchTimer()
  }, [])

  const handleRandom = () => {
    clearSwitchTimer()
    setTiles(generateTileGrid(TILE_GRID.SIZE, TILE_GRID.COLUMNS, engineValues.complexity))
    setQueued(null)
  }

  const handleRandomizeAllParams = () => {
    clearSwitchTimer()
    let newRandomized: TileState | null = null

    if (playing === PLAYBACK_ID.CUSTOM) {
      setCustomTile(prev => {
        const randomized = randomizeTileParams(prev)
        newRandomized = randomized
        return randomized
      })
    } else {
      setTiles(prevTiles => {
        const newTiles = prevTiles.map((row, rowIdx) =>
          row.map((tile, colIdx) => {
            const randomized = randomizeTileParams(tile)
            if (playing && focusedTile !== 'custom') {
              const tileId = getTileId(focusedTile.row, focusedTile.col)
              if (playing === tileId && rowIdx === focusedTile.row && colIdx === focusedTile.col) {
                newRandomized = randomized
              }
            }
            return randomized
          })
        )
        return newTiles
      })

      setCustomTile(prev => randomizeTileParams(prev))
    }

    if (newRandomized && playbackManagerRef.current) {
      const params = newRandomized as TileState
      loadTileParams(params)

      playbackManagerRef.current.setEffects(params.effectParams as any)
      playbackManagerRef.current.setVariables(
        params.engineParams.a ?? DEFAULT_VARIABLES.a,
        params.engineParams.b ?? DEFAULT_VARIABLES.b,
        params.engineParams.c ?? DEFAULT_VARIABLES.c,
        params.engineParams.d ?? DEFAULT_VARIABLES.d
      )
      playbackManagerRef.current.setPitch(params.engineParams.pitch ?? 1.0)

      if (params.lfoConfigs) {
        playbackManagerRef.current.setLFOConfig(0, params.lfoConfigs.lfo1)
        playbackManagerRef.current.setLFOConfig(1, params.lfoConfigs.lfo2)
        playbackManagerRef.current.setLFOConfig(2, params.lfoConfigs.lfo3)
        playbackManagerRef.current.setLFOConfig(3, params.lfoConfigs.lfo4)
      }
    }

    setQueued(null)
  }

  const playFormula = async (formula: string, id: string) => {
    const sampleRate = getSampleRateFromIndex(engineValues.sampleRate)
    const duration = LOOP_DURATION

    if (!playbackManagerRef.current) {
      playbackManagerRef.current = new PlaybackManager({ sampleRate, duration })
    } else {
      await playbackManagerRef.current.updateOptions({ sampleRate, duration })
    }

    playbackManagerRef.current.stop()
    playbackManagerRef.current.setEffects(effectValues)
    playbackManagerRef.current.setVariables(
      engineValues.a ?? DEFAULT_VARIABLES.a,
      engineValues.b ?? DEFAULT_VARIABLES.b,
      engineValues.c ?? DEFAULT_VARIABLES.c,
      engineValues.d ?? DEFAULT_VARIABLES.d
    )
    playbackManagerRef.current.setPitch(engineValues.pitch ?? 1.0)

    await playbackManagerRef.current.play(formula)
    setPlaying(id)
    setQueued(null)
    return true
  }

  const handleTileClick = (_formula: string, row: number, col: number, isDoubleClick: boolean = false) => {
    const id = getTileId(row, col)
    const tile = getTileFromGrid(tiles, row, col)

    if (!tile) return

    if (focusedTile === 'custom' || (focusedTile.row !== row || focusedTile.col !== col)) {
      saveCurrentTileParams()
    }

    if (tile) {
      loadTileParams(tile)
    }
    setFocusedTile({ row, col })

    if (playing === id) {
      handleStop()
      return
    }

    if (isDoubleClick || playing === null) {
      clearSwitchTimer()
      playFormula(tile.formula, id)
    } else {
      setQueued(id)
      startSwitchTimer(id)
    }
  }

  const handleTileDoubleClick = (formula: string, row: number, col: number) => {
    handleTileClick(formula, row, col, true)
  }


  const handleEngineChange = async (parameterId: string, value: number) => {
    engineSettings.setKey(parameterId as keyof typeof engineValues, value)
    saveCurrentTileParams()

    if (parameterId === 'masterVolume' && playbackManagerRef.current) {
      playbackManagerRef.current.setEffects({ ...effectValues, masterVolume: value })
    }

    if (parameterId === 'pitch' && playbackManagerRef.current && playing) {
      playbackManagerRef.current.setPitch(value)
    }

    if (['a', 'b', 'c', 'd'].includes(parameterId) && playbackManagerRef.current && playing) {
      const updatedValues = { ...engineValues, [parameterId]: value }
      playbackManagerRef.current.setVariables(
        updatedValues.a ?? DEFAULT_VARIABLES.a,
        updatedValues.b ?? DEFAULT_VARIABLES.b,
        updatedValues.c ?? DEFAULT_VARIABLES.c,
        updatedValues.d ?? DEFAULT_VARIABLES.d
      )
    }
  }

  const handleEffectChange = (parameterId: string, value: number | boolean | string) => {
    effectSettings.setKey(parameterId as any, value as any)
    saveCurrentTileParams()

    if (playbackManagerRef.current) {
      playbackManagerRef.current.setEffects(effectValues)
    }
  }

  const handleLFOChange = (lfoIndex: number, config: any) => {
    if (playbackManagerRef.current) {
      playbackManagerRef.current.setLFOConfig(lfoIndex, config)
    }
  }

  const handleParameterMapClick = (paramId: string, lfoIndex: number) => {
    const lfoKey = `lfo${lfoIndex + 1}` as 'lfo1' | 'lfo2' | 'lfo3' | 'lfo4'
    const currentLFO = lfoSettings.get()[lfoKey]

    const existingMappingIndex = currentLFO.mappings.findIndex(m => m.targetParam === paramId)

    let updatedMappings
    if (existingMappingIndex >= 0) {
      updatedMappings = currentLFO.mappings.filter((_, i) => i !== existingMappingIndex)
    } else {
      updatedMappings = [...currentLFO.mappings, { targetParam: paramId, depth: 50 }]
    }

    const updatedLFO = { ...currentLFO, mappings: updatedMappings }
    lfoSettings.setKey(lfoKey, updatedLFO)

    if (playbackManagerRef.current) {
      playbackManagerRef.current.setLFOConfig(lfoIndex, updatedLFO)
    }

    saveCurrentTileParams()

    if (updatedMappings.length === 0 || existingMappingIndex >= 0) {
      exitMappingMode()
    }
  }

  const handleUpdateMappingDepth = (lfoIndex: number, paramId: string, depth: number) => {
    const lfoKey = `lfo${lfoIndex + 1}` as 'lfo1' | 'lfo2' | 'lfo3' | 'lfo4'
    const currentLFO = lfoSettings.get()[lfoKey]

    const updatedMappings = currentLFO.mappings.map(m =>
      m.targetParam === paramId ? { ...m, depth } : m
    )

    const updatedLFO = { ...currentLFO, mappings: updatedMappings }
    lfoSettings.setKey(lfoKey, updatedLFO)

    if (playbackManagerRef.current) {
      playbackManagerRef.current.setLFOConfig(lfoIndex, updatedLFO)
    }

    saveCurrentTileParams()
  }

  const handleRemoveMapping = (lfoIndex: number, paramId: string) => {
    const lfoKey = `lfo${lfoIndex + 1}` as 'lfo1' | 'lfo2' | 'lfo3' | 'lfo4'
    const currentLFO = lfoSettings.get()[lfoKey]

    const updatedMappings = currentLFO.mappings.filter(m => m.targetParam !== paramId)

    const updatedLFO = { ...currentLFO, mappings: updatedMappings }
    lfoSettings.setKey(lfoKey, updatedLFO)

    if (playbackManagerRef.current) {
      playbackManagerRef.current.setLFOConfig(lfoIndex, updatedLFO)
    }

    saveCurrentTileParams()
  }

  const getMappedLFOs = (paramId: string): number[] => {
    const lfos = lfoSettings.get()
    const mapped: number[] = []

    Object.entries(lfos).forEach(([, lfo], index) => {
      if (lfo.mappings.some((m: { targetParam: string }) => m.targetParam === paramId)) {
        mapped.push(index)
      }
    })

    return mapped
  }

  const handleDownloadAll = async () => {
    setDownloading(true)
    const formulas = tiles.map(row => row.map(tile => tile.formula))
    await downloadServiceRef.current.downloadAll(formulas, {
      duration: DEFAULT_DOWNLOAD_OPTIONS.DURATION,
      bitDepth: DEFAULT_DOWNLOAD_OPTIONS.BIT_DEPTH
    })
    setDownloading(false)
  }

  const handleDownloadFormula = (formula: string, filename: string) => {
    downloadServiceRef.current.downloadFormula(formula, filename, {
      duration: DEFAULT_DOWNLOAD_OPTIONS.DURATION,
      bitDepth: DEFAULT_DOWNLOAD_OPTIONS.BIT_DEPTH
    })
  }

  const handleRegenerate = (row: number, col: number) => {
    const newFormula = generateRandomFormula(engineValues.complexity)
    const newTile = createTileStateFromCurrent(newFormula)

    setTiles(prevTiles => {
      const newTiles = [...prevTiles]
      newTiles[row] = [...newTiles[row]]
      newTiles[row][col] = newTile
      return newTiles
    })
  }

  const handleStop = () => {
    clearSwitchTimer()
    playbackManagerRef.current?.stop()
    setPlaying(null)
    setQueued(null)
    setPlaybackPosition(0)
  }

  const handleCustomEvaluate = (formula: string) => {
    if (focusedTile !== 'custom') {
      saveCurrentTileParams()
      loadTileParams(customTile)
    }

    setFocusedTile('custom')
    setCustomTile({ ...customTile, formula })
    playFormula(formula, PLAYBACK_ID.CUSTOM)
  }

  const handleCustomStop = () => {
    if (playing === PLAYBACK_ID.CUSTOM) {
      handleStop()
    }
  }

  const handleCustomRandom = () => {
    return generateRandomFormula(engineValues.complexity)
  }

  const moveFocus = (direction: 'up' | 'down' | 'left' | 'right', step: number = 1) => {
    saveCurrentTileParams()

    setFocusedTile(prev => {
      if (prev === 'custom') return prev

      let { row, col } = prev
      const maxRow = tiles.length - 1
      const maxCol = (tiles[row]?.length || 1) - 1

      switch (direction) {
        case 'up':
          row = Math.max(0, row - step)
          break
        case 'down':
          row = Math.min(maxRow, row + step)
          break
        case 'left':
          col = Math.max(0, col - step)
          break
        case 'right':
          col = Math.min(maxCol, col + step)
          break
      }

      const newTile = tiles[row]?.[col]
      if (newTile) {
        loadTileParams(newTile)
        return { row, col }
      }

      return prev
    })
  }

  const handleKeyboardSpace = () => {
    if (playing) {
      handleStop()
    } else if (focusedTile !== 'custom') {
      const tile = tiles[focusedTile.row]?.[focusedTile.col]
      if (tile) {
        handleTileClick(tile.formula, focusedTile.row, focusedTile.col, true)
      }
    }
  }

  const handleKeyboardEnter = () => {
    if (focusedTile !== 'custom') {
      const tile = tiles[focusedTile.row]?.[focusedTile.col]
      if (tile) {
        handleTileClick(tile.formula, focusedTile.row, focusedTile.col, false)
      }
    }
  }

  const handleKeyboardDoubleEnter = () => {
    if (focusedTile !== 'custom') {
      const tile = tiles[focusedTile.row]?.[focusedTile.col]
      if (tile) {
        handleTileClick(tile.formula, focusedTile.row, focusedTile.col, true)
      }
    }
  }

  const handleKeyboardR = () => {
    if (focusedTile !== 'custom') {
      handleRegenerate(focusedTile.row, focusedTile.col)
    }
  }

  const handleKeyboardShiftR = () => {
    handleRandom()
  }

  const handleEscape = () => {
    exitMappingMode()
  }

  const handleKeyboardC = () => {
    if (focusedTile === 'custom') {
      setCustomTile(prev => {
        const randomized = randomizeTileParams(prev)
        loadTileParams(randomized)

        if (playing === PLAYBACK_ID.CUSTOM && playbackManagerRef.current) {
          playbackManagerRef.current.setEffects(randomized.effectParams as any)
          playbackManagerRef.current.setVariables(
            randomized.engineParams.a ?? DEFAULT_VARIABLES.a,
            randomized.engineParams.b ?? DEFAULT_VARIABLES.b,
            randomized.engineParams.c ?? DEFAULT_VARIABLES.c,
            randomized.engineParams.d ?? DEFAULT_VARIABLES.d
          )
          playbackManagerRef.current.setPitch(randomized.engineParams.pitch ?? 1.0)

          if (randomized.lfoConfigs) {
            playbackManagerRef.current.setLFOConfig(0, randomized.lfoConfigs.lfo1)
            playbackManagerRef.current.setLFOConfig(1, randomized.lfoConfigs.lfo2)
            playbackManagerRef.current.setLFOConfig(2, randomized.lfoConfigs.lfo3)
            playbackManagerRef.current.setLFOConfig(3, randomized.lfoConfigs.lfo4)
          }
        }

        return randomized
      })
    } else {
      const tileId = getTileId(focusedTile.row, focusedTile.col)
      setTiles(prevTiles => {
        const newTiles = [...prevTiles]
        newTiles[focusedTile.row] = [...newTiles[focusedTile.row]]
        const randomized = randomizeTileParams(newTiles[focusedTile.row][focusedTile.col])
        newTiles[focusedTile.row][focusedTile.col] = randomized

        loadTileParams(randomized)

        if (playing === tileId && playbackManagerRef.current) {
          playbackManagerRef.current.setEffects(randomized.effectParams as any)
          playbackManagerRef.current.setVariables(
            randomized.engineParams.a ?? DEFAULT_VARIABLES.a,
            randomized.engineParams.b ?? DEFAULT_VARIABLES.b,
            randomized.engineParams.c ?? DEFAULT_VARIABLES.c,
            randomized.engineParams.d ?? DEFAULT_VARIABLES.d
          )
          playbackManagerRef.current.setPitch(randomized.engineParams.pitch ?? 1.0)

          if (randomized.lfoConfigs) {
            playbackManagerRef.current.setLFOConfig(0, randomized.lfoConfigs.lfo1)
            playbackManagerRef.current.setLFOConfig(1, randomized.lfoConfigs.lfo2)
            playbackManagerRef.current.setLFOConfig(2, randomized.lfoConfigs.lfo3)
            playbackManagerRef.current.setLFOConfig(3, randomized.lfoConfigs.lfo4)
          }
        }

        return newTiles
      })
    }
  }

  const handleKeyboardShiftC = () => {
    handleRandomizeAllParams()
  }

  const handleDismissWarning = () => {
    setShowWarning(false)
  }

  useKeyboardShortcuts({
    onSpace: handleKeyboardSpace,
    onArrowUp: (shift) => moveFocus('up', shift ? 10 : 1),
    onArrowDown: (shift) => moveFocus('down', shift ? 10 : 1),
    onArrowLeft: (shift) => moveFocus('left', shift ? 10 : 1),
    onArrowRight: (shift) => moveFocus('right', shift ? 10 : 1),
    onEnter: handleKeyboardEnter,
    onDoubleEnter: handleKeyboardDoubleEnter,
    onR: handleKeyboardR,
    onShiftR: handleKeyboardShiftR,
    onC: handleKeyboardC,
    onShiftC: handleKeyboardShiftC,
    onEscape: handleEscape
  })

  useEffect(() => {
    if (focusedTile !== 'custom') {
      const element = document.querySelector(`[data-tile-id="${focusedTile.row}-${focusedTile.col}"]`)
      if (element) {
        element.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
      }
    }
  }, [focusedTile])

  return (
    <div className="w-screen h-screen flex flex-col bg-black overflow-hidden">
      {showWarning && <AudioContextWarning onDismiss={handleDismissWarning} />}
      {showHelp && <HelpModal onClose={() => setShowHelp(false)} />}
      <header className="bg-black border-b-2 border-white px-6 py-3">
        <div className="flex items-center justify-between gap-6">
          <h1
            onClick={() => setShowHelp(true)}
            className="font-mono text-sm tracking-[0.3em] text-white flex-shrink-0 cursor-pointer hover:opacity-70 transition-opacity"
          >
            BRUITISTE
          </h1>
          <EngineControls
            values={engineValues}
            onChange={handleEngineChange}
            onMapClick={handleParameterMapClick}
            getMappedLFOs={getMappedLFOs}
          />
          <div className="flex gap-4 flex-shrink-0">
            <button
              onClick={handleStop}
              disabled={!playing}
              className="px-6 py-2 bg-black text-white border-2 border-white font-mono text-[11px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center gap-2"
            >
              <Square size={14} strokeWidth={2} fill="currentColor" />
              STOP
            </button>
            <button
              onClick={handleRandom}
              className="px-6 py-2 bg-white text-black font-mono text-[11px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all flex items-center gap-2"
            >
              <Dices size={14} strokeWidth={2} />
              RANDOM
            </button>
            <button
              onClick={handleRandomizeAllParams}
              className="px-6 py-2 bg-white text-black font-mono text-[11px] tracking-[0.2em] hover:bg-black hover:text-white border-2 border-white transition-all flex items-center gap-2"
            >
              <Sparkles size={14} strokeWidth={2} />
              CHAOS
            </button>
            <button
              onClick={handleDownloadAll}
              disabled={downloading}
              className="px-6 py-2 bg-black text-white border-2 border-white font-mono text-[11px] tracking-[0.2em] hover:bg-white hover:text-black transition-all disabled:opacity-30 disabled:cursor-not-allowed flex items-center gap-2"
            >
              <Archive size={14} strokeWidth={2} />
              {downloading ? 'DOWNLOADING...' : 'PACK'}
            </button>
          </div>
        </div>
      </header>

      <LFOPanel onChange={handleLFOChange} onUpdateDepth={handleUpdateMappingDepth} onRemoveMapping={handleRemoveMapping} />

      <div className="flex-1 flex flex-col overflow-auto bg-white">
        <div className="grid grid-cols-4 gap-[1px] bg-white p-[1px]">
          <div className="col-span-4">
            <FormulaEditor
              formula={customTile.formula}
              isPlaying={playing === PLAYBACK_ID.CUSTOM}
              isFocused={focusedTile === 'custom'}
              playbackPosition={playing === PLAYBACK_ID.CUSTOM ? playbackPosition : 0}
              onEvaluate={handleCustomEvaluate}
              onStop={handleCustomStop}
              onRandom={handleCustomRandom}
            />
          </div>
        </div>

        <div className="flex-1 grid grid-cols-4 auto-rows-min gap-[1px] bg-white p-[1px]">
          {tiles.map((row, i) =>
            row.map((tile, j) => {
              const id = getTileId(i, j)
              return (
                <BytebeatTile
                  key={id}
                  formula={tile.formula}
                  row={i}
                  col={j}
                  isPlaying={playing === id}
                  isQueued={queued === id}
                  isFocused={focusedTile !== 'custom' && focusedTile.row === i && focusedTile.col === j}
                  playbackPosition={playing === id ? playbackPosition : 0}
                  a={tile.engineParams.a ?? 8}
                  b={tile.engineParams.b ?? 16}
                  c={tile.engineParams.c ?? 32}
                  d={tile.engineParams.d ?? 64}
                  onPlay={handleTileClick}
                  onDoubleClick={handleTileDoubleClick}
                  onDownload={handleDownloadFormula}
                  onRegenerate={handleRegenerate}
                />
              )
            })
          )}
        </div>
      </div>

      <EffectsBar
        values={effectValues}
        onChange={handleEffectChange}
        onMapClick={handleParameterMapClick}
        getMappedLFOs={getMappedLFOs}
      />
    </div>
  )
}

export default App
