import { ArrowDownOutlined, ArrowLeftOutlined, CloseOutlined, PauseOutlined, PlayCircleOutlined, ReloadOutlined, RotateRightOutlined, SettingOutlined, SoundOutlined, VerticalAlignTopOutlined } from '@ant-design/icons';
import { Button, Switch } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import type { PointerEvent as ReactPointerEvent } from 'react';
import { getRegisteredAccessToken } from '../../../../app/main/tokenAccess';
import './TetrisAppView.css';

type TetrisAppViewProps = {
  onBack: () => void;
  launchContext?: 'direct' | 'embedded';
};

type CellValue = '' | 'I' | 'O' | 'T' | 'S' | 'Z' | 'J' | 'L';
type PieceType = Exclude<CellValue, ''>;

type PieceState = {
  type: PieceType;
  rotation: number;
  x: number;
  y: number;
};

type TouchAction = 'left' | 'right' | 'rotate' | 'soft-drop';
type JoystickDirection = 'left' | 'right' | 'down';
type TetrisSoundEffect = 'move' | 'rotate' | 'soft-drop' | 'hard-drop' | 'line-clear' | 'game-over';
type InstantTouchAction = 'audio' | 'hard-drop' | 'pause-toggle' | 'restart';

const TETRIS_AUDIO_RESOURCE_ROOT = 'resource/Codex Live/Apps 보관함/테트리스 오디오/20260519/docs/audio';
const TETRIS_AUDIO_RESOURCES = {
  bgm: `${TETRIS_AUDIO_RESOURCE_ROOT}/tetris-bgm-loop.mp3`,
  move: `${TETRIS_AUDIO_RESOURCE_ROOT}/tetris-move.mp3`,
  rotate: `${TETRIS_AUDIO_RESOURCE_ROOT}/tetris-rotate.mp3`,
  'soft-drop': `${TETRIS_AUDIO_RESOURCE_ROOT}/tetris-soft-drop.mp3`,
  'hard-drop': `${TETRIS_AUDIO_RESOURCE_ROOT}/tetris-hard-drop.mp3`,
  'line-clear': `${TETRIS_AUDIO_RESOURCE_ROOT}/tetris-line-clear.mp3`,
  'game-over': `${TETRIS_AUDIO_RESOURCE_ROOT}/tetris-game-over.mp3`,
} as const;

const TOUCH_ACTIONS: TouchAction[] = ['left', 'right', 'rotate', 'soft-drop'];
const JOYSTICK_HIDE_DELAY_MS = 180;
const EFFECT_MIN_INTERVAL_MS: Record<TetrisSoundEffect, number> = {
  move: 70,
  rotate: 80,
  'soft-drop': 90,
  'hard-drop': 120,
  'line-clear': 180,
  'game-over': 240,
};

function createTouchActionState() {
  return {
    left: false,
    right: false,
    rotate: false,
    'soft-drop': false,
  } satisfies Record<TouchAction, boolean>;
}

const BOARD_WIDTH = 10;
const BOARD_HEIGHT = 20;
const EMPTY_BOARD = Array.from({ length: BOARD_HEIGHT }, () => Array<CellValue>(BOARD_WIDTH).fill(''));
const PIECE_ORDER: PieceType[] = ['I', 'O', 'T', 'S', 'Z', 'J', 'L'];

const PIECE_COLORS: Record<PieceType, string> = {
  I: '#50e3ff',
  O: '#ffd45c',
  T: '#c48bff',
  S: '#6dff9b',
  Z: '#ff7b6d',
  J: '#6d8cff',
  L: '#ffae63',
};

const PIECE_SHAPES: Record<PieceType, number[][][]> = {
  I: [
    [[0, 1], [1, 1], [2, 1], [3, 1]],
    [[2, 0], [2, 1], [2, 2], [2, 3]],
    [[0, 2], [1, 2], [2, 2], [3, 2]],
    [[1, 0], [1, 1], [1, 2], [1, 3]],
  ],
  O: [
    [[1, 0], [2, 0], [1, 1], [2, 1]],
    [[1, 0], [2, 0], [1, 1], [2, 1]],
    [[1, 0], [2, 0], [1, 1], [2, 1]],
    [[1, 0], [2, 0], [1, 1], [2, 1]],
  ],
  T: [
    [[1, 0], [0, 1], [1, 1], [2, 1]],
    [[1, 0], [1, 1], [2, 1], [1, 2]],
    [[0, 1], [1, 1], [2, 1], [1, 2]],
    [[1, 0], [0, 1], [1, 1], [1, 2]],
  ],
  S: [
    [[1, 0], [2, 0], [0, 1], [1, 1]],
    [[1, 0], [1, 1], [2, 1], [2, 2]],
    [[1, 1], [2, 1], [0, 2], [1, 2]],
    [[0, 0], [0, 1], [1, 1], [1, 2]],
  ],
  Z: [
    [[0, 0], [1, 0], [1, 1], [2, 1]],
    [[2, 0], [1, 1], [2, 1], [1, 2]],
    [[0, 1], [1, 1], [1, 2], [2, 2]],
    [[1, 0], [0, 1], [1, 1], [0, 2]],
  ],
  J: [
    [[0, 0], [0, 1], [1, 1], [2, 1]],
    [[1, 0], [2, 0], [1, 1], [1, 2]],
    [[0, 1], [1, 1], [2, 1], [2, 2]],
    [[1, 0], [1, 1], [0, 2], [1, 2]],
  ],
  L: [
    [[2, 0], [0, 1], [1, 1], [2, 1]],
    [[1, 0], [1, 1], [1, 2], [2, 2]],
    [[0, 1], [1, 1], [2, 1], [0, 2]],
    [[0, 0], [1, 0], [1, 1], [1, 2]],
  ],
};

function createPiece(type: PieceType): PieceState {
  return {
    type,
    rotation: 0,
    x: 3,
    y: 0,
  };
}

function getCells(piece: PieceState) {
  return PIECE_SHAPES[piece.type][piece.rotation].map(([x, y]) => ({ x: piece.x + x, y: piece.y + y }));
}

function randomPieceType() {
  return PIECE_ORDER[Math.floor(Math.random() * PIECE_ORDER.length)] ?? 'T';
}

function cloneBoard(board: CellValue[][]) {
  return board.map((row) => [...row]);
}

function isCollision(board: CellValue[][], piece: PieceState) {
  return getCells(piece).some(({ x, y }) => {
    if (x < 0 || x >= BOARD_WIDTH || y >= BOARD_HEIGHT) {
      return true;
    }

    if (y < 0) {
      return false;
    }

    return board[y]?.[x] !== '';
  });
}

function mergePiece(board: CellValue[][], piece: PieceState) {
  const nextBoard = cloneBoard(board);
  getCells(piece).forEach(({ x, y }) => {
    if (y >= 0 && y < BOARD_HEIGHT && x >= 0 && x < BOARD_WIDTH) {
      nextBoard[y][x] = piece.type;
    }
  });
  return nextBoard;
}

function clearLines(board: CellValue[][]) {
  const remainingRows = board.filter((row) => row.some((cell) => cell === ''));
  const clearedCount = BOARD_HEIGHT - remainingRows.length;
  const clearedRows = Array.from({ length: clearedCount }, () => Array<CellValue>(BOARD_WIDTH).fill(''));
  return {
    board: [...clearedRows, ...remainingRows],
    clearedCount,
  };
}

function formatScore(lines: number) {
  return lines * 100;
}

function getLevel(lines: number) {
  return Math.floor(lines / 8) + 1;
}

function getTickMs(lines: number) {
  return Math.max(180, 720 - (getLevel(lines) - 1) * 60);
}

function clamp(value: number, min: number, max: number) {
  return Math.min(Math.max(value, min), max);
}

function buildManagedAudioUrl(resourcePath: string) {
  const relativePath = resourcePath.replace(/^resource\/+/, '').trim();
  const encodedPath = relativePath
    .split('/')
    .filter(Boolean)
    .map((segment) => encodeURIComponent(segment))
    .join('/');
  const previewUrl = `/api/resource-manager/preview/${encodedPath}`;
  const token = getRegisteredAccessToken();

  if (!token) {
    return previewUrl;
  }

  return `${previewUrl}?token=${encodeURIComponent(token)}`;
}

function useTetrisAudio(isPaused: boolean, isGameOver: boolean, isAudioEnabled: boolean) {
  const isPausedRef = useRef(isPaused);
  const isGameOverRef = useRef(isGameOver);
  const isAudioEnabledRef = useRef(isAudioEnabled);
  const audioUnlockedRef = useRef(false);
  const audioPrimingRef = useRef<Promise<boolean> | null>(null);
  const [isAudioReady, setIsAudioReady] = useState(false);
  const bgmAudioRef = useRef<HTMLAudioElement | null>(null);
  const effectPoolRef = useRef<Record<TetrisSoundEffect, HTMLAudioElement[]>>({
    move: [],
    rotate: [],
    'soft-drop': [],
    'hard-drop': [],
    'line-clear': [],
    'game-over': [],
  });
  const effectIndexRef = useRef<Record<TetrisSoundEffect, number>>({
    move: 0,
    rotate: 0,
    'soft-drop': 0,
    'hard-drop': 0,
    'line-clear': 0,
    'game-over': 0,
  });
  const lastEffectPlayedAtRef = useRef<Record<TetrisSoundEffect, number>>({
    move: 0,
    rotate: 0,
    'soft-drop': 0,
    'hard-drop': 0,
    'line-clear': 0,
    'game-over': 0,
  });

  useEffect(() => {
    isPausedRef.current = isPaused;
  }, [isPaused]);

  useEffect(() => {
    isGameOverRef.current = isGameOver;
  }, [isGameOver]);

  useEffect(() => {
    isAudioEnabledRef.current = isAudioEnabled;
  }, [isAudioEnabled]);

  useEffect(() => {
    if (!isAudioEnabled) {
      setIsAudioReady(false);
    }
  }, [isAudioEnabled]);

  useEffect(() => {
    if (typeof Audio === 'undefined') {
      return undefined;
    }

    bgmAudioRef.current = new Audio(buildManagedAudioUrl(TETRIS_AUDIO_RESOURCES.bgm));
    bgmAudioRef.current.loop = true;
    bgmAudioRef.current.preload = 'metadata';
    bgmAudioRef.current.volume = 0.24;

    (Object.keys(effectPoolRef.current) as TetrisSoundEffect[]).forEach((key) => {
      const sourceUrl = buildManagedAudioUrl(TETRIS_AUDIO_RESOURCES[key]);
      effectPoolRef.current[key] = Array.from({ length: key === 'move' ? 2 : 1 }, () => {
        const audio = new Audio(sourceUrl);
        audio.preload = 'metadata';
        audio.volume = key === 'move' ? 0.2 : key === 'soft-drop' ? 0.18 : 0.3;
        return audio;
      });
    });

    return () => {
      if (bgmAudioRef.current) {
        bgmAudioRef.current.pause();
        bgmAudioRef.current.src = '';
        bgmAudioRef.current = null;
      }

      (Object.keys(effectPoolRef.current) as TetrisSoundEffect[]).forEach((key) => {
        effectPoolRef.current[key].forEach((audio) => {
          audio.pause();
          audio.src = '';
        });
        effectPoolRef.current[key] = [];
      });
    };
  }, []);

  useEffect(() => {
    const bgm = bgmAudioRef.current;
    if (!bgm || !audioUnlockedRef.current || !isAudioEnabled) {
      if (bgm) {
        bgm.pause();
      }
      return;
    }

    if (isPaused || isGameOver) {
      bgm.pause();
      return;
    }

    void bgm.play().catch(() => undefined);
  }, [isAudioEnabled, isGameOver, isPaused]);

  const syncBackgroundMusic = () => {
    const bgm = bgmAudioRef.current;

    if (!bgm || !audioUnlockedRef.current || !isAudioEnabledRef.current) {
      if (bgm) {
        bgm.pause();
      }
      return;
    }

    if (isPausedRef.current || isGameOverRef.current) {
      bgm.pause();
      return;
    }

    void bgm.play().catch(() => undefined);
  };

  const primeAudio = (options?: { allowWhenDisabled?: boolean; markUnlocked?: boolean }) => {
    const shouldMarkUnlocked = options?.markUnlocked ?? true;
    const canPrimeWhileDisabled = options?.allowWhenDisabled ?? false;

    if (!isAudioEnabledRef.current && !canPrimeWhileDisabled) {
      return Promise.resolve(false);
    }

    if (shouldMarkUnlocked) {
      audioUnlockedRef.current = true;
    }

    const bgm = bgmAudioRef.current;

    if (audioUnlockedRef.current && isAudioReady) {
      return Promise.resolve(true);
    }

    if (audioPrimingRef.current) {
      return audioPrimingRef.current;
    }

    audioPrimingRef.current = (async () => {
      let didUnlockAnyAudio = false;

      if (bgm) {
        try {
          if (isPausedRef.current || isGameOverRef.current) {
            const previousMuted = bgm.muted;
            bgm.muted = true;
            bgm.currentTime = 0;
            await bgm.play();
            didUnlockAnyAudio = true;
            bgm.pause();
            bgm.currentTime = 0;
            bgm.muted = previousMuted;
          } else {
            await bgm.play();
            didUnlockAnyAudio = true;
          }
        } catch {
          // Ignore autoplay failures; the next user gesture can retry.
        }
      }

      const primingEffect = effectPoolRef.current.move[0];
      if (primingEffect) {
        const previousMuted = primingEffect.muted;
        try {
          primingEffect.muted = true;
          primingEffect.currentTime = 0;
          await primingEffect.play();
          didUnlockAnyAudio = true;
          primingEffect.pause();
          primingEffect.currentTime = 0;
        } catch {
          // Ignore autoplay failures; a later gesture can still unlock audio.
        } finally {
          primingEffect.muted = previousMuted;
        }
      }

      setIsAudioReady(didUnlockAnyAudio);
      if (didUnlockAnyAudio) {
        syncBackgroundMusic();
      }
      return didUnlockAnyAudio;
    })().finally(() => {
      audioPrimingRef.current = null;
    });
    return audioPrimingRef.current;
  };

  useEffect(() => {
    const handleVisibilityRestore = () => {
      if (document.visibilityState !== 'visible') {
        return;
      }

      if (audioUnlockedRef.current) {
        syncBackgroundMusic();
      }
    };

    const handlePageShow = () => {
      handleVisibilityRestore();
    };

    document.addEventListener('visibilitychange', handleVisibilityRestore);
    window.addEventListener('pageshow', handlePageShow);
    return () => {
      document.removeEventListener('visibilitychange', handleVisibilityRestore);
      window.removeEventListener('pageshow', handlePageShow);
    };
  }, []);

  const playEffect = (effect: TetrisSoundEffect) => {
    if (!isAudioEnabled) {
      return;
    }

    if (!audioUnlockedRef.current || !isAudioReady) {
      void primeAudio();
    }

    const pool = effectPoolRef.current[effect];

    if (!pool.length) {
      return;
    }

    const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
    const lastPlayedAt = lastEffectPlayedAtRef.current[effect];
    if (now - lastPlayedAt < EFFECT_MIN_INTERVAL_MS[effect]) {
      return;
    }
    lastEffectPlayedAtRef.current[effect] = now;

    const nextIndex = effectIndexRef.current[effect] % pool.length;
    effectIndexRef.current[effect] = nextIndex + 1;
    const audio = pool[nextIndex];
    audio.currentTime = 0;
    void audio.play().catch(() => undefined);
  };

  return {
    isAudioReady,
    primeAudio,
    playEffect,
  };
}

export function TetrisAppView({ onBack, launchContext = 'direct' }: TetrisAppViewProps) {
  const [board, setBoard] = useState<CellValue[][]>(() => cloneBoard(EMPTY_BOARD));
  const [activePiece, setActivePiece] = useState<PieceState>(() => createPiece(randomPieceType()));
  const [nextPieceType, setNextPieceType] = useState<PieceType>(() => randomPieceType());
  const [lineCount, setLineCount] = useState(0);
  const [isPaused, setIsPaused] = useState(false);
  const [isGameOver, setIsGameOver] = useState(false);
  const [isSettingsOpen, setIsSettingsOpen] = useState(false);
  const [isAudioEnabled, setIsAudioEnabled] = useState(true);
  const isEmbeddedLaunch = launchContext === 'embedded';
  const [activeTouchActions, setActiveTouchActions] = useState<Record<TouchAction, boolean>>(() => createTouchActionState());
  const [isJoystickVisible, setIsJoystickVisible] = useState(false);
  const [joystickBaseOffset, setJoystickBaseOffset] = useState({ x: 0, y: 0 });
  const [joystickOffset, setJoystickOffset] = useState({ x: 0, y: 0 });
  const boardRef = useRef(board);
  const activePieceRef = useRef(activePiece);
  const isPausedRef = useRef(isPaused);
  const isGameOverRef = useRef(isGameOver);
  const softDropRef = useRef(false);
  const joystickTouchZoneRef = useRef<HTMLDivElement | null>(null);
  const joystickPointerIdRef = useRef<number | null>(null);
  const joystickDirectionRef = useRef<JoystickDirection | null>(null);
  const joystickMoveIntervalRef = useRef<number | null>(null);
  const joystickHideTimeoutRef = useRef<number | null>(null);
  const keyboardHorizontalDirectionRef = useRef<'left' | 'right' | null>(null);
  const keyboardHorizontalIntervalRef = useRef<number | null>(null);
  const joystickOriginClientRef = useRef({ x: 0, y: 0 });
  const joystickSoftDropActiveRef = useRef(false);
  const touchButtonPointerMapRef = useRef(new Map<number, TouchAction>());
  const instantButtonPointerMapRef = useRef(new Map<number, InstantTouchAction>());
  const { isAudioReady, primeAudio, playEffect } = useTetrisAudio(isPaused, isGameOver, isAudioEnabled);

  const visibleBoard = useMemo(() => mergePiece(board, activePiece), [activePiece, board]);
  const score = useMemo(() => formatScore(lineCount), [lineCount]);
  const level = useMemo(() => getLevel(lineCount), [lineCount]);

  const preventPointerSelection = (event: ReactPointerEvent<HTMLElement>) => {
    if (event.pointerType !== 'mouse') {
      event.preventDefault();
    }
  };

  useEffect(() => {
    boardRef.current = board;
  }, [board]);

  useEffect(() => {
    activePieceRef.current = activePiece;
  }, [activePiece]);

  useEffect(() => {
    isPausedRef.current = isPaused;
  }, [isPaused]);

  useEffect(() => {
    isGameOverRef.current = isGameOver;
  }, [isGameOver]);

  const setTouchActionActive = (action: TouchAction, isActive: boolean) => {
    setActiveTouchActions((current) => (current[action] === isActive ? current : { ...current, [action]: isActive }));
  };

  const clearAllTouchActions = () => {
    setActiveTouchActions((current) => {
      if (TOUCH_ACTIONS.every((action) => !current[action])) {
        return current;
      }
      return createTouchActionState();
    });
  };

  const syncSoftDropState = () => {
    const nextSoftDrop = joystickSoftDropActiveRef.current || Array.from(touchButtonPointerMapRef.current.values()).includes('soft-drop');
    softDropRef.current = nextSoftDrop;
    setTouchActionActive('soft-drop', nextSoftDrop);
  };

  const clearJoystickTouchActions = () => {
    setTouchActionActive('left', false);
    setTouchActionActive('right', false);
    setTouchActionActive('rotate', false);
    joystickSoftDropActiveRef.current = false;
    syncSoftDropState();
  };

  const resetGame = () => {
    const nextBoard = cloneBoard(EMPTY_BOARD);
    const nextPiece = createPiece(randomPieceType());
    boardRef.current = nextBoard;
    activePieceRef.current = nextPiece;
    isPausedRef.current = false;
    isGameOverRef.current = false;
    setBoard(nextBoard);
    setActivePiece(nextPiece);
    setNextPieceType(randomPieceType());
    setLineCount(0);
    setIsPaused(false);
    setIsGameOver(false);
    touchButtonPointerMapRef.current.clear();
    clearJoystickInterval();
    clearJoystickHideTimeout();
    clearKeyboardHorizontalInterval();
    clearAllTouchActions();
    softDropRef.current = false;
    hideJoystickVisual();
    setJoystickOffset({ x: 0, y: 0 });
  };

  const spawnNextPiece = (nextBoard: CellValue[][]) => {
    const spawnedPiece = createPiece(nextPieceType);
    const upcomingType = randomPieceType();
    activePieceRef.current = spawnedPiece;
    setActivePiece(spawnedPiece);
    setNextPieceType(upcomingType);
    if (isCollision(nextBoard, spawnedPiece)) {
      isGameOverRef.current = true;
      isPausedRef.current = false;
      setIsGameOver(true);
      setIsPaused(false);
      playEffect('game-over');
    }
  };

  const lockPiece = (piece: PieceState) => {
    const mergedBoard = mergePiece(boardRef.current, piece);
    const { board: clearedBoard, clearedCount } = clearLines(mergedBoard);
    boardRef.current = clearedBoard;
    setBoard(clearedBoard);
    if (clearedCount > 0) {
      setLineCount((current) => current + clearedCount);
      playEffect('line-clear');
    }
    spawnNextPiece(clearedBoard);
  };

  const tryMove = (nextPiece: PieceState) => {
    if (isGameOverRef.current || isPausedRef.current) {
      return false;
    }

    if (isCollision(boardRef.current, nextPiece)) {
      return false;
    }

    activePieceRef.current = nextPiece;
    setActivePiece(nextPiece);
    return true;
  };

  const moveHorizontal = (delta: number) => {
    const currentPiece = activePieceRef.current;
    if (tryMove({ ...currentPiece, x: currentPiece.x + delta })) {
      playEffect('move');
    }
  };

  const rotatePiece = () => {
    if (isPausedRef.current || isGameOverRef.current) {
      return;
    }

    const currentPiece = activePieceRef.current;
    const nextRotation = (currentPiece.rotation + 1) % 4;
    const rotationCandidate = { ...currentPiece, rotation: nextRotation };
    const kickOffsets = [0, -1, 1, -2, 2];
    const landedCandidate = kickOffsets
      .map((offset) => ({ ...rotationCandidate, x: rotationCandidate.x + offset }))
      .find((candidate) => !isCollision(boardRef.current, candidate));

    if (landedCandidate) {
      activePieceRef.current = landedCandidate;
      setActivePiece(landedCandidate);
      playEffect('rotate');
    }
  };

  const stepDown = () => {
    if (isPausedRef.current || isGameOverRef.current) {
      return;
    }

    const currentPiece = activePieceRef.current;
    const nextPiece = { ...currentPiece, y: currentPiece.y + 1 };
    if (isCollision(boardRef.current, nextPiece)) {
      lockPiece(currentPiece);
      return;
    }
    activePieceRef.current = nextPiece;
    setActivePiece(nextPiece);
  };

  const hardDrop = () => {
    if (isPausedRef.current || isGameOverRef.current) {
      return;
    }

    let droppedPiece = activePieceRef.current;
    while (!isCollision(boardRef.current, { ...droppedPiece, y: droppedPiece.y + 1 })) {
      droppedPiece = { ...droppedPiece, y: droppedPiece.y + 1 };
    }
    activePieceRef.current = droppedPiece;
    setActivePiece(droppedPiece);
    playEffect('hard-drop');
    lockPiece(droppedPiece);
  };

  const clearJoystickInterval = () => {
    if (joystickMoveIntervalRef.current !== null) {
      window.clearInterval(joystickMoveIntervalRef.current);
      joystickMoveIntervalRef.current = null;
    }
  };

  const clearJoystickHideTimeout = () => {
    if (joystickHideTimeoutRef.current !== null) {
      window.clearTimeout(joystickHideTimeoutRef.current);
      joystickHideTimeoutRef.current = null;
    }
  };

  const hideJoystickVisual = () => {
    setIsJoystickVisible(false);
    setJoystickBaseOffset({ x: 0, y: 0 });
    joystickOriginClientRef.current = { x: 0, y: 0 };
  };

  const clearKeyboardHorizontalInterval = () => {
    if (keyboardHorizontalIntervalRef.current !== null) {
      window.clearInterval(keyboardHorizontalIntervalRef.current);
      keyboardHorizontalIntervalRef.current = null;
    }
    keyboardHorizontalDirectionRef.current = null;
  };

  const clearSoftDrop = () => {
    joystickSoftDropActiveRef.current = false;
    syncSoftDropState();
  };

  const hasPriorityTouchButton = () => touchButtonPointerMapRef.current.size > 0 || instantButtonPointerMapRef.current.size > 0;

  const applyJoystickDirection = (direction: JoystickDirection | null) => {
    if (direction === joystickDirectionRef.current) {
      return;
    }

    clearJoystickInterval();

    if (joystickDirectionRef.current === 'left') {
      setTouchActionActive('left', false);
    } else if (joystickDirectionRef.current === 'right') {
      setTouchActionActive('right', false);
    } else if (joystickDirectionRef.current === 'down') {
      clearSoftDrop();
    }

    joystickDirectionRef.current = direction;

    if (direction === null) {
      return;
    }

    if (direction === 'left' || direction === 'right') {
      const delta = direction === 'left' ? -1 : 1;
      setTouchActionActive(direction, true);
      moveHorizontal(delta);
      joystickMoveIntervalRef.current = window.setInterval(() => {
        moveHorizontal(delta);
      }, 110);
      return;
    }

    joystickSoftDropActiveRef.current = true;
    syncSoftDropState();
    stepDown();
  };

  const updateJoystick = (clientX: number, clientY: number) => {
    const joystickTouchZone = joystickTouchZoneRef.current;
    if (!joystickTouchZone) {
      return;
    }

    const rect = joystickTouchZone.getBoundingClientRect();
    const origin = joystickOriginClientRef.current;
    const rawX = clientX - origin.x;
    const rawY = clientY - origin.y;
    const maxDistanceX = Math.min(Math.max(rect.width * 0.12, 44), 58);
    const maxDistanceY = Math.min(Math.max(rect.height * 0.12, 52), 72);
    const normalizedDistance = Math.hypot(rawX / maxDistanceX, rawY / maxDistanceY);
    const ratio = normalizedDistance > 1 ? 1 / normalizedDistance : 1;
    const nextOffset = {
      x: rawX * ratio,
      y: rawY * ratio,
    };
    setJoystickOffset(nextOffset);

    if (hasPriorityTouchButton()) {
      applyJoystickDirection(null);
      return;
    }

    const horizontalThreshold = Math.min(Math.max(rect.width * 0.045, 18), 28);
    const verticalThreshold = Math.min(Math.max(rect.height * 0.05, 22), 34);

    if (Math.abs(rawX) > Math.abs(rawY) && Math.abs(rawX) > horizontalThreshold) {
      applyJoystickDirection(rawX < 0 ? 'left' : 'right');
      return;
    }

    if (rawY > verticalThreshold) {
      applyJoystickDirection('down');
      return;
    }

    applyJoystickDirection(null);
  };

  const releaseJoystick = () => {
    joystickPointerIdRef.current = null;
    joystickDirectionRef.current = null;
    clearJoystickInterval();
    clearJoystickTouchActions();
    setJoystickOffset({ x: 0, y: 0 });
    clearJoystickHideTimeout();
    joystickHideTimeoutRef.current = window.setTimeout(() => {
      hideJoystickVisual();
      joystickHideTimeoutRef.current = null;
    }, JOYSTICK_HIDE_DELAY_MS);
  };

  const startKeyboardHorizontalRepeat = (direction: 'left' | 'right') => {
    if (keyboardHorizontalDirectionRef.current === direction) {
      return;
    }

    clearKeyboardHorizontalInterval();

    const delta = direction === 'left' ? -1 : 1;
    keyboardHorizontalDirectionRef.current = direction;
    moveHorizontal(delta);
    keyboardHorizontalIntervalRef.current = window.setInterval(() => {
      moveHorizontal(delta);
    }, 110);
  };

  useEffect(() => {
    if (isPaused || isGameOver) {
      clearKeyboardHorizontalInterval();
    }
  }, [isGameOver, isPaused]);

  useEffect(() => {
    if (isPaused || isGameOver) {
      return undefined;
    }

    const tickMs = softDropRef.current ? 70 : getTickMs(lineCount);
    const timer = window.setInterval(() => {
      stepDown();
    }, tickMs);

    return () => {
      window.clearInterval(timer);
    };
  }, [activePiece, board, isGameOver, isPaused, lineCount]);

  useEffect(() => {
    const handleKeyDown = (event: KeyboardEvent) => {
      primeAudio();
      if (event.key === 'ArrowLeft') {
        event.preventDefault();
        startKeyboardHorizontalRepeat('left');
      } else if (event.key === 'ArrowRight') {
        event.preventDefault();
        startKeyboardHorizontalRepeat('right');
      } else if (event.key === 'ArrowUp' || event.key.toLowerCase() === 'x') {
        event.preventDefault();
        rotatePiece();
      } else if (event.key === 'ArrowDown') {
        event.preventDefault();
        const wasSoftDropping = softDropRef.current;
        softDropRef.current = true;
        if (!wasSoftDropping) {
          playEffect('soft-drop');
        }
        stepDown();
      } else if (event.key === ' ') {
        event.preventDefault();
        hardDrop();
      } else if (event.key.toLowerCase() === 'p') {
        event.preventDefault();
        setIsPaused((current) => !current);
      } else if (event.key.toLowerCase() === 'r') {
        event.preventDefault();
        resetGame();
      }
    };

    const handleKeyUp = (event: KeyboardEvent) => {
      if (event.key === 'ArrowDown') {
        softDropRef.current = false;
        return;
      }

      if (
        (event.key === 'ArrowLeft' && keyboardHorizontalDirectionRef.current === 'left')
        || (event.key === 'ArrowRight' && keyboardHorizontalDirectionRef.current === 'right')
      ) {
        clearKeyboardHorizontalInterval();
      }
    };

    window.addEventListener('keydown', handleKeyDown);
    window.addEventListener('keyup', handleKeyUp);
    return () => {
      clearKeyboardHorizontalInterval();
      window.removeEventListener('keydown', handleKeyDown);
      window.removeEventListener('keyup', handleKeyUp);
    };
  });

  useEffect(() => {
    return () => {
      clearJoystickInterval();
      clearJoystickHideTimeout();
      clearKeyboardHorizontalInterval();
      touchButtonPointerMapRef.current.clear();
      instantButtonPointerMapRef.current.clear();
    };
  }, []);

  const handleTouchButtonPointerDown = (action: TouchAction, event: ReactPointerEvent<HTMLButtonElement>) => {
    preventPointerSelection(event);
    primeAudio();
    touchButtonPointerMapRef.current.set(event.pointerId, action);
    event.currentTarget.setPointerCapture(event.pointerId);
    applyJoystickDirection(null);
    setTouchActionActive(action, true);

    if (action === 'rotate') {
      rotatePiece();
      return;
    }

    if (action === 'soft-drop') {
      syncSoftDropState();
      playEffect('soft-drop');
      stepDown();
      return;
    }

  };

  const executeInstantTouchAction = (action: InstantTouchAction) => {
    primeAudio();

    if (action === 'audio') {
      playEffect('rotate');
      return;
    }

    if (action === 'hard-drop') {
      hardDrop();
      return;
    }

    if (action === 'pause-toggle') {
      setIsPaused((current) => !current);
      return;
    }

    resetGame();
  };

  const handleInstantTouchButtonPointerDown = (action: InstantTouchAction, event: ReactPointerEvent<HTMLButtonElement>) => {
    preventPointerSelection(event);
    instantButtonPointerMapRef.current.set(event.pointerId, action);
    event.currentTarget.setPointerCapture(event.pointerId);
    applyJoystickDirection(null);
    executeInstantTouchAction(action);
  };

  const releaseTouchButtonPointer = (pointerId: number) => {
    const action = touchButtonPointerMapRef.current.get(pointerId);
    if (!action) {
      return;
    }

    touchButtonPointerMapRef.current.delete(pointerId);
    if (!Array.from(touchButtonPointerMapRef.current.values()).includes(action)) {
      setTouchActionActive(action, false);
    }

    if (action === 'soft-drop') {
      syncSoftDropState();
    }
  };

  const releaseInstantTouchButtonPointer = (pointerId: number) => {
    instantButtonPointerMapRef.current.delete(pointerId);
  };

  return (
    <section className="tetris-app" data-testid="tetris-app">
      <header className="tetris-app__topbar">
        <div className="tetris-app__topbar-main">
          <Button type="text" icon={<ArrowLeftOutlined />} className="tetris-app__back" onClick={onBack}>
            앱 보관함
          </Button>
          <h2>Tetris</h2>
        </div>
        <div className="tetris-app__topbar-side">
          {isEmbeddedLaunch ? (
            <Button type="text" icon={<CloseOutlined />} className="tetris-app__exit" onClick={onBack}>
              종료
            </Button>
          ) : null}
          <div className="tetris-app__info-cards">
            <div className="tetris-app__next-preview" aria-label="다음 블록">
              <span className="tetris-app__panel-label">다음 블록</span>
              <div className="tetris-app__next-grid">
                {Array.from({ length: 16 }, (_, index) => {
                  const x = index % 4;
                  const y = Math.floor(index / 4);
                  const isActive = PIECE_SHAPES[nextPieceType][0].some(([cellX, cellY]) => cellX === x && cellY === y);
                  return (
                    <span
                      key={`${nextPieceType}-${index}`}
                      className={`tetris-app__next-cell${isActive ? ' tetris-app__next-cell--active' : ''}`}
                      style={isActive ? ({ ['--tetris-cell-color' as string]: PIECE_COLORS[nextPieceType] }) : undefined}
                    />
                  );
                })}
              </div>
            </div>

            <div className="tetris-app__stats" aria-label="게임 상태">
              <div className="tetris-app__stat">
                <span>점수</span>
                <strong>{score}</strong>
              </div>
              <div className="tetris-app__stat">
                <span>라인</span>
                <strong>{lineCount}</strong>
              </div>
              <div className="tetris-app__stat">
                <span>레벨</span>
                <strong>{level}</strong>
              </div>
            </div>
          </div>
          <div className="tetris-app__mobile-utility-actions" aria-label="모바일 상단 보조 제어">
            {!isAudioReady && isAudioEnabled ? (
              <Button
                type="default"
                aria-label="사운드 켜기"
                className="tetris-app__audio-cta tetris-app__utility-action tetris-app__utility-action--meta"
                icon={<SoundOutlined />}
                onPointerDown={(event) => handleInstantTouchButtonPointerDown('audio', event)}
                onPointerUp={(event) => {
                  event.currentTarget.releasePointerCapture(event.pointerId);
                  releaseInstantTouchButtonPointer(event.pointerId);
                }}
                onPointerCancel={(event) => releaseInstantTouchButtonPointer(event.pointerId)}
              />
            ) : null}
            <div className="tetris-app__settings-wrap tetris-app__settings-wrap--mobile">
              <Button
                type="text"
                aria-label="앱 설정 열기"
                icon={<SettingOutlined />}
                className={`tetris-app__settings-trigger tetris-app__utility-action tetris-app__utility-action--meta${isSettingsOpen ? ' is-active' : ''}`}
                onClick={() => setIsSettingsOpen((current) => !current)}
              />
              {isSettingsOpen ? (
                <div className="tetris-app__settings-panel" role="group" aria-label="앱 설정">
                  <div className="tetris-app__settings-row">
                    <span className="tetris-app__settings-label">
                      <SoundOutlined />
                      오디오
                    </span>
                    <Switch
                      size="small"
                      checked={isAudioEnabled}
                      checkedChildren="ON"
                      unCheckedChildren="OFF"
                      onChange={(checked) => {
                        setIsAudioEnabled(checked);
                      }}
                    />
                  </div>
                </div>
              ) : null}
            </div>
            <Button
              type="default"
              aria-label="다시 시작"
              icon={<ReloadOutlined />}
              className="tetris-app__utility-action tetris-app__utility-action--meta"
              onPointerDown={(event) => handleInstantTouchButtonPointerDown('restart', event)}
              onPointerUp={(event) => {
                event.currentTarget.releasePointerCapture(event.pointerId);
                releaseInstantTouchButtonPointer(event.pointerId);
              }}
              onPointerCancel={(event) => releaseInstantTouchButtonPointer(event.pointerId)}
            />
            <Button
              type="default"
              aria-label={isPaused ? '재개' : '일시정지'}
              icon={isPaused ? <PlayCircleOutlined /> : <PauseOutlined />}
              className="tetris-app__utility-action tetris-app__utility-action--meta"
              onPointerDown={(event) => handleInstantTouchButtonPointerDown('pause-toggle', event)}
              onPointerUp={(event) => {
                event.currentTarget.releasePointerCapture(event.pointerId);
                releaseInstantTouchButtonPointer(event.pointerId);
              }}
              onPointerCancel={(event) => releaseInstantTouchButtonPointer(event.pointerId)}
              disabled={isGameOver}
            />
          </div>
          <div className="tetris-app__icon-actions" aria-label="게임 제어">
            <Button
              type="text"
              aria-label="회전"
              icon={<RotateRightOutlined />}
              className={`tetris-app__icon-button tetris-app__icon-button--rotate${activeTouchActions.rotate ? ' is-active' : ''}`}
              onPointerDown={(event) => handleTouchButtonPointerDown('rotate', event)}
              onPointerUp={(event) => {
                event.currentTarget.releasePointerCapture(event.pointerId);
                releaseTouchButtonPointer(event.pointerId);
              }}
              onPointerCancel={(event) => releaseTouchButtonPointer(event.pointerId)}
            />
            <Button
              type="text"
              aria-label="즉시내리기"
              icon={<VerticalAlignTopOutlined rotate={180} />}
              className="tetris-app__icon-button"
              onClick={() => {
                primeAudio();
                hardDrop();
              }}
              disabled={isPaused || isGameOver}
            />
            <Button
              type="text"
              aria-label={isPaused ? '재개' : '일시정지'}
              icon={isPaused ? <PlayCircleOutlined /> : <PauseOutlined />}
              className="tetris-app__icon-button"
              onClick={() => {
                primeAudio();
                setIsPaused((current) => !current);
              }}
              disabled={isGameOver}
            />
            <Button
              type="text"
              aria-label="다시 시작"
              icon={<ReloadOutlined />}
              className="tetris-app__icon-button"
              onClick={() => {
                primeAudio();
                resetGame();
              }}
            />
          </div>
          <div className="tetris-app__settings-wrap tetris-app__settings-wrap--desktop">
            <Button
              type="text"
              aria-label="앱 설정 열기"
              icon={<SettingOutlined />}
              className={`tetris-app__settings-trigger${isSettingsOpen ? ' is-active' : ''}`}
              onClick={() => setIsSettingsOpen((current) => !current)}
            />
            {isSettingsOpen ? (
              <div className="tetris-app__settings-panel" role="group" aria-label="앱 설정">
                <div className="tetris-app__settings-row">
                  <span className="tetris-app__settings-label">
                    <SoundOutlined />
                    오디오
                  </span>
                  <Switch
                    size="small"
                    checked={isAudioEnabled}
                    checkedChildren="ON"
                    unCheckedChildren="OFF"
                    onChange={(checked) => {
                      setIsAudioEnabled(checked);
                    }}
                  />
                </div>
              </div>
            ) : null}
          </div>
        </div>
      </header>

      <div className="tetris-app__body">
        <section className="tetris-app__board-shell">
          <div className="tetris-app__board" role="img" aria-label="테트리스 보드">
            {visibleBoard.map((row, rowIndex) =>
              row.map((cell, colIndex) => (
                <span
                  key={`${rowIndex}-${colIndex}`}
                  className={`tetris-app__cell${cell ? ` tetris-app__cell--${cell}` : ''}`}
                  style={cell ? ({ ['--tetris-cell-color' as string]: PIECE_COLORS[cell] }) : undefined}
                />
              )),
            )}
          </div>

          <div className="tetris-app__overlay">
            {isPaused ? <div className="tetris-app__banner">일시정지</div> : null}
            {isGameOver ? <div className="tetris-app__banner tetris-app__banner--danger">게임 오버</div> : null}
          </div>
          <div className="tetris-app__touch-layer" aria-label="모바일 조작 패널">
            <div className="tetris-app__mobile-control-deck tetris-app__mobile-control-deck--joystick" aria-hidden="true" />
            <div className="tetris-app__mobile-control-deck tetris-app__mobile-control-deck--actions" aria-hidden="true" />
            <div
              ref={joystickTouchZoneRef}
              className="tetris-app__joystick-touch-zone"
              aria-label="모바일 방향 조작 영역"
              onPointerDown={(event) => {
                preventPointerSelection(event);
                primeAudio();
                clearJoystickHideTimeout();
                joystickPointerIdRef.current = event.pointerId;
                event.currentTarget.setPointerCapture(event.pointerId);
                const rect = event.currentTarget.getBoundingClientRect();
                const insetX = 62;
                const insetY = 62;
                const nextBaseOffset = {
                  x: clamp(event.clientX - rect.left, insetX, rect.width - insetX),
                  y: clamp(event.clientY - rect.top, insetY, rect.height - insetY),
                };
                joystickOriginClientRef.current = {
                  x: rect.left + nextBaseOffset.x,
                  y: rect.top + nextBaseOffset.y,
                };
                setJoystickBaseOffset(nextBaseOffset);
                setJoystickOffset({ x: 0, y: 0 });
                setIsJoystickVisible(true);
                applyJoystickDirection(null);
              }}
              onPointerMove={(event) => {
                if (joystickPointerIdRef.current !== event.pointerId) {
                  return;
                }
                preventPointerSelection(event);
                updateJoystick(event.clientX, event.clientY);
              }}
              onPointerUp={(event) => {
                if (joystickPointerIdRef.current !== event.pointerId) {
                  return;
                }
                event.currentTarget.releasePointerCapture(event.pointerId);
                releaseJoystick();
              }}
              onPointerCancel={releaseJoystick}
            >
              <div className="tetris-app__joystick-touch-zone-glow" aria-hidden="true" />
              <div
                className={`tetris-app__joystick-wrap${isJoystickVisible ? ' is-visible' : ''}`}
                style={{
                  left: `${joystickBaseOffset.x}px`,
                  top: `${joystickBaseOffset.y}px`,
                }}
              >
                <div className="tetris-app__joystick-pad">
                  <div className={`tetris-app__joystick${activeTouchActions.left || activeTouchActions.right || activeTouchActions['soft-drop'] ? ' is-active' : ''}`}>
                    <span className="tetris-app__joystick-stem" aria-hidden="true" />
                    <span className="tetris-app__joystick-ring" aria-hidden="true" />
                    <span className="tetris-app__joystick-gate" aria-hidden="true" />
                    <span className="tetris-app__joystick-label tetris-app__joystick-label--left">LEFT</span>
                    <span className="tetris-app__joystick-label tetris-app__joystick-label--right">RIGHT</span>
                    <span className="tetris-app__joystick-label tetris-app__joystick-label--down">DROP</span>
                    <span
                      className="tetris-app__joystick-knob"
                      aria-hidden="true"
                      style={{
                        transform: `translate(${joystickOffset.x}px, ${joystickOffset.y}px)`,
                      }}
                    />
                  </div>
                </div>
              </div>
            </div>
            <div className="tetris-app__mobile-main-buttons" aria-label="모바일 빠른 제어">
              <Button
                type="default"
                aria-label="회전"
                icon={<RotateRightOutlined />}
                className={`tetris-app__mobile-action tetris-app__mobile-action--rotate${activeTouchActions.rotate ? ' is-active' : ''}`}
                onPointerDown={(event) => handleTouchButtonPointerDown('rotate', event)}
                onPointerUp={(event) => {
                  event.currentTarget.releasePointerCapture(event.pointerId);
                  releaseTouchButtonPointer(event.pointerId);
                }}
                onPointerCancel={(event) => releaseTouchButtonPointer(event.pointerId)}
              />
              <Button
                type="default"
                aria-label="내리기"
                icon={<ArrowDownOutlined />}
                className={`tetris-app__mobile-action tetris-app__mobile-action--drop${activeTouchActions['soft-drop'] ? ' is-active' : ''}`}
                onPointerDown={(event) => handleTouchButtonPointerDown('soft-drop', event)}
                onPointerUp={(event) => {
                  event.currentTarget.releasePointerCapture(event.pointerId);
                  releaseTouchButtonPointer(event.pointerId);
                }}
                onPointerCancel={(event) => releaseTouchButtonPointer(event.pointerId)}
              />
              <Button
                type="default"
                aria-label="즉시내리기"
                icon={<VerticalAlignTopOutlined rotate={180} />}
                className="tetris-app__mobile-action tetris-app__mobile-action--hard-drop"
                onPointerDown={(event) => handleInstantTouchButtonPointerDown('hard-drop', event)}
                onPointerUp={(event) => {
                  event.currentTarget.releasePointerCapture(event.pointerId);
                  releaseInstantTouchButtonPointer(event.pointerId);
                }}
                onPointerCancel={(event) => releaseInstantTouchButtonPointer(event.pointerId)}
                disabled={isPaused || isGameOver}
              />
            </div>
          </div>
        </section>

        <aside className="tetris-app__sidebar">
          <div className="tetris-app__sidebar-spacer" aria-hidden="true" />
        </aside>
      </div>

    </section>
  );
}
