import Phaser from "phaser";
import { CAMPAIGN_LEVELS } from "../../game/content/levels";
import { LaserPuzzleGame } from "../../game/simulation/LaserPuzzleGame";
import { colorNameToHex } from "../../game/simulation/colors";
import { CodBloomPipeline } from "../pipelines/CodBloomPipeline";
import { ensureBackgroundMusic } from "../audio/backgroundMusic";
import { PORTAL_OPEN_SFX_KEY } from "./BootScene";
import type { Direction, GameSnapshot, LevelData, Vec2 } from "../../game/simulation/types";
import type { HudController } from "../../ui/hud";

interface Keymap {
  up: Phaser.Input.Keyboard.Key;
  down: Phaser.Input.Keyboard.Key;
  left: Phaser.Input.Keyboard.Key;
  right: Phaser.Input.Keyboard.Key;
  w: Phaser.Input.Keyboard.Key;
  a: Phaser.Input.Keyboard.Key;
  s: Phaser.Input.Keyboard.Key;
  d: Phaser.Input.Keyboard.Key;
  q: Phaser.Input.Keyboard.Key;
  e: Phaser.Input.Keyboard.Key;
  r: Phaser.Input.Keyboard.Key;
  z: Phaser.Input.Keyboard.Key;
  x: Phaser.Input.Keyboard.Key;
  enter: Phaser.Input.Keyboard.Key;
  space: Phaser.Input.Keyboard.Key;
  l: Phaser.Input.Keyboard.Key;
  m: Phaser.Input.Keyboard.Key;
  esc: Phaser.Input.Keyboard.Key;
}

interface GameSceneStartData {
  startLevelIndex?: number;
  customLevels?: LevelData[];
  returnSceneKey?: string;
  returnSceneData?: object;
  victorySceneEnabled?: boolean;
}

const ROTATION_SPEED_DEG = 135;
const LEVEL_OVERLAY_FADE_IN_MS = 460;
const LEVEL_OVERLAY_HOLD_MS = 1200;
const LEVEL_OVERLAY_FADE_OUT_MS = 700;
const LEVEL_OVERLAY_TARGET_VEIL_ALPHA = 0.84;
const LEVEL_OVERLAY_BOARD_ALPHA = 0.34;
const LEVEL_OVERLAY_ENTITY_ALPHA = 0.3;
const LEVEL_OVERLAY_BEAM_ALPHA = 0.2;
const PORTAL_OPEN_SHAKE_MS = 220;
const PORTAL_OPEN_SHAKE_INTENSITY = 0.0035;
const MC_LIGHT_SAMPLE_COUNT = 12;
const MC_LIGHT_INTENSITY = 4.0;
const MC_LIGHT_MIN_DIST_SQ = 8 * 8;
const MC_LIGHT_MAX_CONTRIB = 1.0;
const DEVICE_SHADOW_RADIUS = 18;
const EMITTER_SHADOW_RADIUS = 11;
const DETECTOR_SHADOW_RADIUS = 14;
const PORTAL_SHADOW_RADIUS = 19;
const PLAYER_SHADOW_RADIUS = 12;
const WALL_BOX_INSET = 0;
const PORTAL_OPEN_SFX_VOLUME = 1.2;

export class GameScene extends Phaser.Scene {
  private readonly tileSize = 64;

  private readonly hud: HudController;

  private sim!: LaserPuzzleGame;

  private levels: LevelData[] = CAMPAIGN_LEVELS;

  private boardGraphics!: Phaser.GameObjects.Graphics;

  private entityGraphics!: Phaser.GameObjects.Graphics;

  private beamGraphics!: Phaser.GameObjects.Graphics;

  private bloomPipeline: CodBloomPipeline | null = null;

  private cachedWallBoxKey: string | null = null;

  private cachedWallBoxes: number[] = [];

  private lightingVisibility = { value: 1 };

  private keys!: Keymap;

  private levelOverlay!: Phaser.GameObjects.Container;

  private levelOverlayVeil!: Phaser.GameObjects.Rectangle;

  private levelOverlayCardContainer!: Phaser.GameObjects.Container;

  private levelOverlayNumberText!: Phaser.GameObjects.Text;

  private levelOverlayTitleText!: Phaser.GameObjects.Text;

  private lastRenderedLevelIndex: number | null = null;

  private lastPortalOpen = false;

  private inputLockedByOverlay = false;

  private overlayHideTimer: Phaser.Time.TimerEvent | null = null;

  private startLevelIndex = 0;

  private lightingEnabled = true;

  private returnSceneKey: string | null = null;

  private returnSceneData: object | undefined = undefined;

  private victorySceneEnabled = true;

  private victoryOverlayActive = false;

  constructor(hud: HudController) {
    super("game");
    this.hud = hud;
  }

  init(data: GameSceneStartData): void {
    if (typeof data?.startLevelIndex === "number" && Number.isFinite(data.startLevelIndex)) {
      this.startLevelIndex = data.startLevelIndex;
    } else {
      this.startLevelIndex = 0;
    }

    if (Array.isArray(data?.customLevels) && data.customLevels.length > 0) {
      this.levels = data.customLevels;
    } else {
      this.levels = CAMPAIGN_LEVELS;
    }

    this.returnSceneKey = typeof data?.returnSceneKey === "string" ? data.returnSceneKey : null;
    if (typeof data?.returnSceneData === "object" && data.returnSceneData !== null) {
      this.returnSceneData = data.returnSceneData;
    } else {
      this.returnSceneData = undefined;
    }
    this.victorySceneEnabled = data?.victorySceneEnabled !== false;
  }

  create(): void {
    this.hud.setVisible(true);
    this.cameras.main.setRoundPixels(true);
    this.lastRenderedLevelIndex = null;
    this.lastPortalOpen = false;
    this.inputLockedByOverlay = false;
    this.victoryOverlayActive = false;
    if (this.overlayHideTimer) {
      this.overlayHideTimer.remove(false);
      this.overlayHideTimer = null;
    }
    this.cachedWallBoxKey = null;
    this.cachedWallBoxes = [];
    this.lightingVisibility.value = 1;
    this.lightingEnabled = this.hud.isLightingEnabled();
    this.sim = new LaserPuzzleGame(this.levels);
    this.sim.startFromLevel(this.startLevelIndex);

    this.boardGraphics = this.add.graphics();
    this.entityGraphics = this.add.graphics();
    this.beamGraphics = this.add.graphics();
    this.beamGraphics.setBlendMode(Phaser.BlendModes.ADD);

    if (this.game.renderer.type === Phaser.WEBGL) {
      const webglRenderer = this.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer;
      webglRenderer.pipelines.addPostPipeline(CodBloomPipeline.KEY, CodBloomPipeline);
      this.cameras.main.setPostPipeline(CodBloomPipeline.KEY);
      this.bloomPipeline = this.resolveBloomPipeline();
    }

    this.keys = this.input.keyboard?.addKeys({
      up: Phaser.Input.Keyboard.KeyCodes.UP,
      down: Phaser.Input.Keyboard.KeyCodes.DOWN,
      left: Phaser.Input.Keyboard.KeyCodes.LEFT,
      right: Phaser.Input.Keyboard.KeyCodes.RIGHT,
      w: Phaser.Input.Keyboard.KeyCodes.W,
      a: Phaser.Input.Keyboard.KeyCodes.A,
      s: Phaser.Input.Keyboard.KeyCodes.S,
      d: Phaser.Input.Keyboard.KeyCodes.D,
      q: Phaser.Input.Keyboard.KeyCodes.Q,
      e: Phaser.Input.Keyboard.KeyCodes.E,
      r: Phaser.Input.Keyboard.KeyCodes.R,
      z: Phaser.Input.Keyboard.KeyCodes.Z,
      x: Phaser.Input.Keyboard.KeyCodes.X,
      enter: Phaser.Input.Keyboard.KeyCodes.ENTER,
      space: Phaser.Input.Keyboard.KeyCodes.SPACE,
      l: Phaser.Input.Keyboard.KeyCodes.L,
      m: Phaser.Input.Keyboard.KeyCodes.M,
      esc: Phaser.Input.Keyboard.KeyCodes.ESC
    }) as Keymap;

    this.createLevelOverlay();
    this.renderScene();
    ensureBackgroundMusic(this);
  }

  update(_time: number, delta: number): void {
    this.syncLightingViewState();

    if (this.victoryOverlayActive) {
      if (
        Phaser.Input.Keyboard.JustDown(this.keys.enter)
        || Phaser.Input.Keyboard.JustDown(this.keys.space)
        || Phaser.Input.Keyboard.JustDown(this.keys.r)
      ) {
        this.hideVictoryOverlay();
        this.sim.resetCampaign();
        this.renderScene();
        return;
      }

      if (Phaser.Input.Keyboard.JustDown(this.keys.l)) {
        this.hud.setVisible(false);
        this.scene.start("level-select");
        return;
      }

      if (Phaser.Input.Keyboard.JustDown(this.keys.m) || Phaser.Input.Keyboard.JustDown(this.keys.esc)) {
        this.hud.setVisible(false);
        this.scene.start("main-menu");
        return;
      }

      return;
    }

    if (Phaser.Input.Keyboard.JustDown(this.keys.esc)) {
      this.hud.setVisible(false);
      const targetScene = this.returnSceneKey ?? "main-menu";
      if (typeof this.returnSceneData !== "undefined") {
        this.scene.start(targetScene, this.returnSceneData);
      } else {
        this.scene.start(targetScene);
      }
      return;
    }

    if (this.inputLockedByOverlay) {
      return;
    }

    let changed = false;
    const dt = delta / 1000;

    const axis = this.readMovementAxis();
    changed = this.sim.movePlayerAxis(axis.x, axis.y, dt) || changed;

    let rotationDelta = 0;
    if (this.keys.q.isDown) {
      rotationDelta -= ROTATION_SPEED_DEG * dt;
    }
    if (this.keys.e.isDown || this.keys.r.isDown) {
      rotationDelta += ROTATION_SPEED_DEG * dt;
    }
    if (Math.abs(rotationDelta) > 0) {
      changed = this.sim.rotateAdjacent(rotationDelta) || changed;
    }

    if (this.isWorldPointerDrag(this.input.activePointer)) {
      changed = this.rotateActiveDeviceTowardPointer(this.input.activePointer) || changed;
    }

    if (Phaser.Input.Keyboard.JustDown(this.keys.z)) {
      changed = this.sim.repaintAdjacentPrism(-1) || changed;
    }
    if (Phaser.Input.Keyboard.JustDown(this.keys.x)) {
      changed = this.sim.repaintAdjacentPrism(1) || changed;
    }

    if (
      !this.victorySceneEnabled
      && Phaser.Input.Keyboard.JustDown(this.keys.enter)
      && this.sim.getSnapshot().campaignComplete
    ) {
      this.sim.resetCampaign();
      changed = true;
    }

    if (changed) {
      this.renderScene();
    }
  }

  private readMovementAxis(): Vec2 {
    const x = (this.keys.right.isDown || this.keys.d.isDown ? 1 : 0)
      - (this.keys.left.isDown || this.keys.a.isDown ? 1 : 0);
    const y = (this.keys.down.isDown || this.keys.s.isDown ? 1 : 0)
      - (this.keys.up.isDown || this.keys.w.isDown ? 1 : 0);
    return { x, y };
  }

  private rotateActiveDeviceTowardPointer(pointer: Phaser.Input.Pointer): boolean {
    const snapshot = this.sim.getSnapshot();
    if (!snapshot.activeDeviceId) {
      return false;
    }

    const device = snapshot.devices.find((entry) => entry.id === snapshot.activeDeviceId);
    if (!device) {
      return false;
    }

    const center = this.toWorldCellCenter(device);
    const dx = pointer.worldX - center.x;
    const dy = pointer.worldY - center.y;
    if (dx * dx + dy * dy < 9) {
      return false;
    }

    const angle = (Math.atan2(dy, dx) * 180) / Math.PI;
    return this.sim.setAdjacentRotation(angle);
  }

  private isWorldPointerDrag(pointer: Phaser.Input.Pointer): boolean {
    if (!pointer.isDown) {
      return false;
    }

    const canvas = this.game.canvas as HTMLCanvasElement | null;
    if (!canvas) {
      return false;
    }

    const pointerWithDownElement = pointer as Phaser.Input.Pointer & { downElement?: EventTarget | null };
    const downElement = pointerWithDownElement.downElement;
    if (downElement) {
      return downElement === canvas;
    }

    // Fallback path for engines/browsers that don't expose downElement.
    const elementUnderPointer = document.elementFromPoint(pointer.x, pointer.y);
    return elementUnderPointer === canvas;
  }

  private renderScene(): void {
    const snapshot = this.sim.getSnapshot();
    const previousLevelIndex = this.lastRenderedLevelIndex;
    const levelChanged = previousLevelIndex !== null && previousLevelIndex !== snapshot.levelIndex;
    const portalJustOpened = previousLevelIndex !== null
      && !levelChanged
      && snapshot.portalOpen
      && !this.lastPortalOpen;

    this.drawBoard(snapshot);
    this.drawBeams(snapshot);
    this.syncLightingData(snapshot);
    this.hud.update(snapshot);

    if (portalJustOpened) {
      this.cameras.main.shake(PORTAL_OPEN_SHAKE_MS, PORTAL_OPEN_SHAKE_INTENSITY, true);
      if (this.cache.audio.exists(PORTAL_OPEN_SFX_KEY)) {
        this.sound.stopByKey(PORTAL_OPEN_SFX_KEY);
        this.sound.play(PORTAL_OPEN_SFX_KEY, {
          volume: PORTAL_OPEN_SFX_VOLUME
        });
      }
    }

    if (levelChanged) {
      this.bloomPipeline?.resetTemporalHistory();
      this.showLevelOverlay(snapshot);
    }

    if (this.victorySceneEnabled && snapshot.campaignComplete && !this.victoryOverlayActive) {
      this.showVictoryOverlay(snapshot);
    }

    this.lastPortalOpen = snapshot.portalOpen;
    this.lastRenderedLevelIndex = snapshot.levelIndex;
  }

  private createLevelOverlay(): void {
    const camera = this.cameras.main;
    const width = camera.width;
    const height = camera.height;

    this.levelOverlayVeil = this.add.rectangle(width / 2, height / 2, width, height, 0x020a12, 0);
    this.levelOverlayVeil.setScrollFactor(0);

    const cardWidth = Math.min(width - 120, 560);
    const cardHeight = 156;
    const cardGlow = this.add.rectangle(0, 0, cardWidth + 40, cardHeight + 30, 0x7befff, 0.12);
    cardGlow.setScrollFactor(0);

    const card = this.add.rectangle(0, 0, cardWidth, cardHeight, 0x102333, 0.9);
    card.setStrokeStyle(2, 0x95f3ff, 0.45);
    card.setScrollFactor(0);

    this.levelOverlayNumberText = this.add.text(0, -30, "", {
      fontFamily: '"Chakra Petch", "Trebuchet MS", sans-serif',
      fontSize: "28px",
      color: "#e7fcff",
      align: "center"
    });
    this.levelOverlayNumberText.setOrigin(0.5);
    this.levelOverlayNumberText.setScrollFactor(0);

    this.levelOverlayTitleText = this.add.text(0, 20, "", {
      fontFamily: '"Chakra Petch", "Trebuchet MS", sans-serif',
      fontSize: "40px",
      color: "#8ef8ff",
      align: "center",
      wordWrap: { width: cardWidth - 60 }
    });
    this.levelOverlayTitleText.setOrigin(0.5);
    this.levelOverlayTitleText.setScrollFactor(0);

    this.levelOverlayCardContainer = this.add.container(width / 2, height / 2 + 16, [
      cardGlow,
      card,
      this.levelOverlayNumberText,
      this.levelOverlayTitleText
    ]);
    this.levelOverlayCardContainer.setAlpha(0);
    this.levelOverlayCardContainer.setScale(0.96);

    this.levelOverlay = this.add.container(0, 0, [this.levelOverlayVeil, this.levelOverlayCardContainer]);
    this.levelOverlay.setDepth(3000);
    this.levelOverlay.setVisible(false);
  }

  private showLevelOverlay(snapshot: GameSnapshot): void {
    this.levelOverlayNumberText.setFontSize("28px");
    this.levelOverlayTitleText.setFontSize("40px");
    this.levelOverlayNumberText.setText(`Уровень ${snapshot.levelIndex + 1}`);
    this.levelOverlayTitleText.setText(snapshot.level.title);

    this.inputLockedByOverlay = true;
    if (this.overlayHideTimer) {
      this.overlayHideTimer.remove(false);
      this.overlayHideTimer = null;
    }

    this.tweens.killTweensOf(this.levelOverlay);
    this.tweens.killTweensOf(this.levelOverlayVeil);
    this.tweens.killTweensOf(this.levelOverlayCardContainer);

    this.levelOverlay.setVisible(true);
    this.levelOverlayVeil.setAlpha(1);
    this.levelOverlayCardContainer.setAlpha(0);
    this.levelOverlayCardContainer.setScale(0.96);
    this.levelOverlayCardContainer.setY(this.cameras.main.height / 2 + 16);

    this.setWorldOverlayDimming(true, LEVEL_OVERLAY_FADE_IN_MS);

    this.tweens.add({
      targets: this.levelOverlayVeil,
      alpha: LEVEL_OVERLAY_TARGET_VEIL_ALPHA,
      duration: LEVEL_OVERLAY_FADE_IN_MS,
      ease: "Sine.easeOut",
    });

    this.tweens.add({
      targets: this.levelOverlayCardContainer,
      alpha: 1,
      scaleX: 1,
      scaleY: 1,
      y: this.cameras.main.height / 2,
      duration: LEVEL_OVERLAY_FADE_IN_MS,
      ease: "Cubic.easeOut"
    });

    this.overlayHideTimer = this.time.delayedCall(LEVEL_OVERLAY_FADE_IN_MS + LEVEL_OVERLAY_HOLD_MS, () => {
      this.setWorldOverlayDimming(false, LEVEL_OVERLAY_FADE_OUT_MS + 90);

      this.tweens.add({
        targets: this.levelOverlayVeil,
        alpha: 0,
        duration: LEVEL_OVERLAY_FADE_OUT_MS,
        ease: "Sine.easeInOut"
      });

      this.tweens.add({
        targets: this.levelOverlayCardContainer,
        alpha: 0,
        scaleX: 0.985,
        scaleY: 0.985,
        y: this.cameras.main.height / 2 - 10,
        duration: LEVEL_OVERLAY_FADE_OUT_MS,
        ease: "Cubic.easeIn",
        onComplete: () => {
          this.levelOverlay.setVisible(false);
          this.inputLockedByOverlay = false;
          this.overlayHideTimer = null;
        }
      });
    });
  }

  private showVictoryOverlay(snapshot: GameSnapshot): void {
    this.levelOverlayNumberText.setFontSize("44px");
    this.levelOverlayTitleText.setFontSize("27px");

    const completed = snapshot.levelCount > 1
      ? `Пройдены все ${snapshot.levelCount} уровней.`
      : "Уровень завершён.";
    this.levelOverlayNumberText.setText("ПОБЕДА");
    this.levelOverlayTitleText.setText(`${completed}\nEnter - снова, L - уровни, Esc - меню`);

    this.victoryOverlayActive = true;
    this.inputLockedByOverlay = true;
    if (this.overlayHideTimer) {
      this.overlayHideTimer.remove(false);
      this.overlayHideTimer = null;
    }

    this.tweens.killTweensOf(this.levelOverlay);
    this.tweens.killTweensOf(this.levelOverlayVeil);
    this.tweens.killTweensOf(this.levelOverlayCardContainer);

    this.levelOverlay.setVisible(true);
    this.levelOverlayVeil.setAlpha(1);
    this.levelOverlayCardContainer.setAlpha(0);
    this.levelOverlayCardContainer.setScale(0.96);
    this.levelOverlayCardContainer.setY(this.cameras.main.height / 2 + 16);

    this.setWorldOverlayDimming(true, LEVEL_OVERLAY_FADE_IN_MS);

    this.tweens.add({
      targets: this.levelOverlayVeil,
      alpha: LEVEL_OVERLAY_TARGET_VEIL_ALPHA,
      duration: LEVEL_OVERLAY_FADE_IN_MS,
      ease: "Sine.easeOut"
    });

    this.tweens.add({
      targets: this.levelOverlayCardContainer,
      alpha: 1,
      scaleX: 1,
      scaleY: 1,
      y: this.cameras.main.height / 2,
      duration: LEVEL_OVERLAY_FADE_IN_MS,
      ease: "Cubic.easeOut"
    });
  }

  private hideVictoryOverlay(): void {
    if (!this.victoryOverlayActive) {
      return;
    }

    this.victoryOverlayActive = false;
    this.inputLockedByOverlay = false;

    this.tweens.killTweensOf(this.levelOverlay);
    this.tweens.killTweensOf(this.levelOverlayVeil);
    this.tweens.killTweensOf(this.levelOverlayCardContainer);
    this.levelOverlay.setVisible(false);
    this.levelOverlayVeil.setAlpha(0);
    this.levelOverlayCardContainer.setAlpha(0);
    this.levelOverlayCardContainer.setScale(0.96);
    this.levelOverlayCardContainer.setY(this.cameras.main.height / 2 + 16);

    this.setWorldOverlayDimming(false, LEVEL_OVERLAY_FADE_OUT_MS);
  }

  private setWorldOverlayDimming(active: boolean, duration: number): void {
    this.tweens.killTweensOf(this.boardGraphics);
    this.tweens.killTweensOf(this.entityGraphics);
    this.tweens.killTweensOf(this.beamGraphics);

    this.tweens.add({
      targets: this.boardGraphics,
      alpha: active ? LEVEL_OVERLAY_BOARD_ALPHA : 1,
      duration,
      ease: active ? "Sine.easeOut" : "Sine.easeInOut"
    });

    this.tweens.add({
      targets: this.entityGraphics,
      alpha: active ? LEVEL_OVERLAY_ENTITY_ALPHA : 1,
      duration,
      ease: active ? "Sine.easeOut" : "Sine.easeInOut"
    });

    this.tweens.add({
      targets: this.beamGraphics,
      alpha: active ? LEVEL_OVERLAY_BEAM_ALPHA : 1,
      duration,
      ease: active ? "Sine.easeOut" : "Sine.easeInOut"
    });

    this.tweens.killTweensOf(this.lightingVisibility);
    this.tweens.add({
      targets: this.lightingVisibility,
      value: active ? LEVEL_OVERLAY_BEAM_ALPHA : 1,
      duration,
      ease: active ? "Sine.easeOut" : "Sine.easeInOut"
    });
  }

  private drawBoard(snapshot: GameSnapshot): void {
    this.boardGraphics.clear();
    this.entityGraphics.clear();

    const wallSet = new Set(snapshot.level.walls.map((wall) => `${wall.x},${wall.y}`));

    for (let y = 0; y < snapshot.level.height; y += 1) {
      for (let x = 0; x < snapshot.level.width; x += 1) {
        const px = x * this.tileSize;
        const py = y * this.tileSize;
        const isBoundary = x === 0 || y === 0 || x === snapshot.level.width - 1 || y === snapshot.level.height - 1;
        const isWall = isBoundary || wallSet.has(`${x},${y}`);

        const floorColor = (x + y) % 2 === 0 ? 0x122a3b : 0x0f2231;
        this.boardGraphics.fillStyle(isWall ? 0x2a3f52 : floorColor, 1);
        this.boardGraphics.fillRoundedRect(px + 1, py + 1, this.tileSize - 2, this.tileSize - 2, 10);

        this.boardGraphics.lineStyle(1, isWall ? 0x43607a : 0x224257, 0.55);
        this.boardGraphics.strokeRoundedRect(px + 0.5, py + 0.5, this.tileSize - 1, this.tileSize - 1, 10);
      }
    }

    this.drawEmitters(snapshot);
    this.drawDetectors(snapshot);
    this.drawPortal(snapshot);
    this.drawDevices(snapshot);
    this.drawPlayer(snapshot.player);
  }

  private drawEmitters(snapshot: GameSnapshot): void {
    const emitters = snapshot.level.emitters && snapshot.level.emitters.length > 0
      ? snapshot.level.emitters
      : [snapshot.level.emitter];

    for (const emitter of emitters) {
      const center = this.toWorldCellCenter(emitter);
      this.entityGraphics.fillStyle(this.hexToNumber(colorNameToHex(emitter.color)), 1);
      this.entityGraphics.fillCircle(center.x, center.y, 11);

      const angle = emitter.angle ?? this.angleFromDirection(emitter.dir ?? "right");
      const dir = this.unitFromAngle(angle);

      this.entityGraphics.lineStyle(3, 0xfff3f3, 1);
      this.entityGraphics.beginPath();
      this.entityGraphics.moveTo(center.x, center.y);
      this.entityGraphics.lineTo(center.x + dir.x * 18, center.y + dir.y * 18);
      this.entityGraphics.strokePath();
    }
  }

  private drawDetectors(snapshot: GameSnapshot): void {
    for (const detector of snapshot.detectors) {
      const center = this.toWorldCellCenter(detector);
      const requiredColor = this.hexToNumber(colorNameToHex(detector.requiredColor));
      const currentColor = detector.currentColor === "none"
        ? 0x1d2b36
        : this.hexToNumber(colorNameToHex(detector.currentColor));
      const frameColor = detector.matched ? 0x9bff7d : requiredColor;

      this.entityGraphics.lineStyle(4, frameColor, 1);
      this.entityGraphics.strokeCircle(center.x, center.y, 13);
      this.entityGraphics.fillStyle(currentColor, detector.currentColor === "none" ? 0.45 : 0.88);
      this.entityGraphics.fillCircle(center.x, center.y, 8);
      if (detector.matched) {
        this.entityGraphics.lineStyle(2, 0xd7ff9e, 1);
        this.entityGraphics.strokeCircle(center.x, center.y, 17);
      }
    }
  }

  private drawPortal(snapshot: GameSnapshot): void {
    const center = this.toWorldCellCenter(snapshot.level.portal);
    const glow = snapshot.portalOpen ? 0x68f0ff : 0x586376;
    const fill = snapshot.portalOpen ? 0x3a8ca3 : 0x3f4a58;

    this.entityGraphics.fillStyle(fill, 0.95);
    this.entityGraphics.fillEllipse(center.x, center.y, 28, 38);
    this.entityGraphics.lineStyle(4, glow, 1);
    this.entityGraphics.strokeEllipse(center.x, center.y, 30, 40);
  }

  private drawDevices(snapshot: GameSnapshot): void {
    for (const device of snapshot.devices) {
      const center = this.toWorldCellCenter(device);
      const frameColor = snapshot.activeDeviceId === device.id ? 0xfef08a : 0x83a6b8;

      this.entityGraphics.fillStyle(0x1b303f, 1);
      this.entityGraphics.fillRoundedRect(center.x - 18, center.y - 18, 36, 36, 8);
      this.entityGraphics.lineStyle(2, frameColor, 1);
      this.entityGraphics.strokeRoundedRect(center.x - 18, center.y - 18, 36, 36, 8);

      if (device.type === "mirror") {
        const axis = this.unitFromAngle(device.rot);
        const half = 15;
        this.entityGraphics.lineStyle(4, 0xe6f4ff, 1);
        this.entityGraphics.beginPath();
        this.entityGraphics.moveTo(center.x - axis.x * half, center.y - axis.y * half);
        this.entityGraphics.lineTo(center.x + axis.x * half, center.y + axis.y * half);
        this.entityGraphics.strokePath();
      } else if (device.type === "prism") {
        const points = this.triangleForAngle(center, device.rot, 11);
        this.entityGraphics.fillStyle(this.hexToNumber(colorNameToHex(device.tint ?? "white")), 0.92);
        this.entityGraphics.fillPoints(points, true);
        this.entityGraphics.lineStyle(2, 0xc8fdff, 1);
        this.entityGraphics.strokePoints(points, true);
      } else {
        const main = this.unitFromAngle(device.rot);
        const branch = this.unitFromAngle(device.rot + 90);

        this.entityGraphics.fillStyle(0x12344b, 1);
        this.entityGraphics.fillRect(center.x - 10, center.y - 3, 20, 6);
        this.entityGraphics.fillRect(center.x - 3, center.y - 10, 6, 20);

        this.entityGraphics.lineStyle(3, 0x8de6ff, 1);
        this.entityGraphics.beginPath();
        this.entityGraphics.moveTo(center.x, center.y);
        this.entityGraphics.lineTo(center.x + main.x * 12, center.y + main.y * 12);
        this.entityGraphics.moveTo(center.x, center.y);
        this.entityGraphics.lineTo(center.x + branch.x * 12, center.y + branch.y * 12);
        this.entityGraphics.strokePath();
      }
    }
  }

  private drawPlayer(position: Vec2): void {
    const center = this.toWorldPoint(position);
    this.entityGraphics.fillStyle(0x95ff8f, 1);
    this.entityGraphics.fillCircle(center.x, center.y, 12);
    this.entityGraphics.fillStyle(0x16301f, 1);
    this.entityGraphics.fillCircle(center.x + 4, center.y - 3, 2.5);
  }

  private drawBeams(snapshot: GameSnapshot): void {
    this.beamGraphics.clear();

    for (const segment of snapshot.beams) {
      const from = this.toWorldPoint(segment.from);
      const to = this.toWorldPoint(segment.to);
      const color = Number.parseInt(segment.color.slice(1), 16);

      this.beamGraphics.lineStyle(10, color, 0.2);
      this.beamGraphics.beginPath();
      this.beamGraphics.moveTo(from.x, from.y);
      this.beamGraphics.lineTo(to.x, to.y);
      this.beamGraphics.strokePath();

      this.beamGraphics.lineStyle(4, color, 1);
      this.beamGraphics.beginPath();
      this.beamGraphics.moveTo(from.x, from.y);
      this.beamGraphics.lineTo(to.x, to.y);
      this.beamGraphics.strokePath();
    }
  }

  private resolveBloomPipeline(): CodBloomPipeline | null {
    const pipeline = this.cameras.main.getPostPipeline(CodBloomPipeline.KEY);
    if (Array.isArray(pipeline)) {
      return (pipeline[0] as CodBloomPipeline | undefined) ?? null;
    }
    return (pipeline as CodBloomPipeline | undefined) ?? null;
  }

  private syncLightingData(snapshot: GameSnapshot): void {
    if (this.game.renderer.type !== Phaser.WEBGL) {
      return;
    }

    const pipeline = this.bloomPipeline ?? this.resolveBloomPipeline();
    if (!pipeline) {
      return;
    }
    this.bloomPipeline = pipeline;

    const segmentData: number[] = [];
    const colorData: number[] = [];

    for (const segment of snapshot.beams) {
      const from = this.toWorldPoint(segment.from);
      const to = this.toWorldPoint(segment.to);
      segmentData.push(from.x, from.y, to.x, to.y);

      const rgb = this.hexToRgb01(segment.color);
      colorData.push(rgb.r, rgb.g, rgb.b);
    }

    const boxes = this.getMergedWallBoxes(snapshot);
    const circles: number[] = [];

    for (const device of snapshot.devices) {
      const center = this.toWorldCellCenter(device);
      circles.push(center.x, center.y, DEVICE_SHADOW_RADIUS);
    }

    const emitters = snapshot.level.emitters && snapshot.level.emitters.length > 0
      ? snapshot.level.emitters
      : [snapshot.level.emitter];
    for (const emitter of emitters) {
      const center = this.toWorldCellCenter(emitter);
      circles.push(center.x, center.y, EMITTER_SHADOW_RADIUS);
    }

    for (const detector of snapshot.detectors) {
      const center = this.toWorldCellCenter(detector);
      circles.push(center.x, center.y, DETECTOR_SHADOW_RADIUS);
    }

    const portalCenter = this.toWorldCellCenter(snapshot.level.portal);
    circles.push(portalCenter.x, portalCenter.y, PORTAL_SHADOW_RADIUS);

    const playerCenter = this.toWorldPoint(snapshot.player);
    circles.push(playerCenter.x, playerCenter.y, PLAYER_SHADOW_RADIUS);

    pipeline.setLightingData({
      segments: segmentData,
      segmentColors: colorData,
      boxes,
      circles,
      sceneRect: [0, 0, snapshot.level.width * this.tileSize, snapshot.level.height * this.tileSize],
      sampleCount: MC_LIGHT_SAMPLE_COUNT,
      intensity: MC_LIGHT_INTENSITY,
      minDistanceSq: MC_LIGHT_MIN_DIST_SQ,
      maxContribution: MC_LIGHT_MAX_CONTRIB,
      frameSeed: 1000 + snapshot.levelIndex * 131
    });
  }

  private syncLightingViewState(): void {
    if (this.game.renderer.type !== Phaser.WEBGL) {
      return;
    }

    const pipeline = this.bloomPipeline ?? this.resolveBloomPipeline();
    if (!pipeline) {
      return;
    }
    this.bloomPipeline = pipeline;

    const shake = (this.cameras.main.shakeEffect as unknown as { _offsetX?: number; _offsetY?: number; isRunning?: boolean });
    const offsetX = Number(shake._offsetX ?? 0);
    const offsetY = Number(shake._offsetY ?? 0);
    const lightingEnabled = this.hud.isLightingEnabled();

    if (lightingEnabled !== this.lightingEnabled) {
      this.lightingEnabled = lightingEnabled;
      pipeline.resetTemporalHistory();
    }

    pipeline.setLightingEnabled(this.lightingEnabled);

    if (shake.isRunning) {
      pipeline.resetTemporalHistory();
    }

    const visibility = this.lightingEnabled ? this.lightingVisibility.value : 0;
    pipeline.setLightingView(offsetX, offsetY, visibility);
  }

  private toWorldCellCenter(position: Vec2): Vec2 {
    return {
      x: (position.x + 0.5) * this.tileSize,
      y: (position.y + 0.5) * this.tileSize
    };
  }

  private toWorldPoint(position: Vec2): Vec2 {
    return {
      x: position.x * this.tileSize,
      y: position.y * this.tileSize
    };
  }

  private angleFromDirection(direction: Direction): number {
    if (direction === "up") {
      return 270;
    }
    if (direction === "right") {
      return 0;
    }
    if (direction === "down") {
      return 90;
    }
    return 180;
  }

  private unitFromAngle(deg: number): Vec2 {
    const rad = (deg * Math.PI) / 180;
    return { x: Math.cos(rad), y: Math.sin(rad) };
  }

  private hexToNumber(hex: string): number {
    return Number.parseInt(hex.slice(1), 16);
  }

  private hexToRgb01(hex: string): { r: number; g: number; b: number } {
    const value = this.hexToNumber(hex);
    return {
      r: ((value >> 16) & 255) / 255,
      g: ((value >> 8) & 255) / 255,
      b: (value & 255) / 255
    };
  }

  private getMergedWallBoxes(snapshot: GameSnapshot): number[] {
    const wallKey = `${snapshot.level.id}:${snapshot.level.width}x${snapshot.level.height}`;
    if (this.cachedWallBoxKey === wallKey && this.cachedWallBoxes.length > 0) {
      return this.cachedWallBoxes;
    }

    const width = snapshot.level.width;
    const height = snapshot.level.height;
    const wallSet = new Set(snapshot.level.walls.map((wall) => `${wall.x},${wall.y}`));
    const wallGrid: boolean[][] = [];

    for (let y = 0; y < height; y += 1) {
      const row: boolean[] = [];
      for (let x = 0; x < width; x += 1) {
        const isBoundary = x === 0 || y === 0 || x === width - 1 || y === height - 1;
        row.push(isBoundary || wallSet.has(`${x},${y}`));
      }
      wallGrid.push(row);
    }

    type RowRun = { x0: number; x1: number };
    type ActiveRect = { x0: number; x1: number; y0: number; y1: number };

    const mergedRects: ActiveRect[] = [];
    let activeBySpan = new Map<string, ActiveRect>();

    for (let y = 0; y < height; y += 1) {
      const runs: RowRun[] = [];
      let x = 0;
      while (x < width) {
        if (!wallGrid[y][x]) {
          x += 1;
          continue;
        }
        const startX = x;
        x += 1;
        while (x < width && wallGrid[y][x]) {
          x += 1;
        }
        runs.push({ x0: startX, x1: x });
      }

      const nextActiveBySpan = new Map<string, ActiveRect>();
      for (const run of runs) {
        const key = `${run.x0}:${run.x1}`;
        const existing = activeBySpan.get(key);
        if (existing) {
          existing.y1 = y + 1;
          nextActiveBySpan.set(key, existing);
        } else {
          nextActiveBySpan.set(key, { x0: run.x0, x1: run.x1, y0: y, y1: y + 1 });
        }
      }

      for (const [key, rect] of activeBySpan) {
        if (!nextActiveBySpan.has(key)) {
          mergedRects.push(rect);
        }
      }
      activeBySpan = nextActiveBySpan;
    }

    for (const rect of activeBySpan.values()) {
      mergedRects.push(rect);
    }

    const worldBoxes: number[] = [];
    for (const rect of mergedRects) {
      const minX = rect.x0 * this.tileSize + WALL_BOX_INSET;
      const minY = rect.y0 * this.tileSize + WALL_BOX_INSET;
      const maxX = rect.x1 * this.tileSize - WALL_BOX_INSET;
      const maxY = rect.y1 * this.tileSize - WALL_BOX_INSET;
      worldBoxes.push(minX, minY, maxX, maxY);
    }

    this.cachedWallBoxKey = wallKey;
    this.cachedWallBoxes = worldBoxes;
    return this.cachedWallBoxes;
  }

  private triangleForAngle(center: Vec2, angleDeg: number, radius: number): Vec2[] {
    const rad = (angleDeg * Math.PI) / 180;
    const front = { x: center.x + Math.cos(rad) * radius, y: center.y + Math.sin(rad) * radius };
    const left = {
      x: center.x + Math.cos(rad + (2 * Math.PI) / 3) * radius,
      y: center.y + Math.sin(rad + (2 * Math.PI) / 3) * radius
    };
    const right = {
      x: center.x + Math.cos(rad - (2 * Math.PI) / 3) * radius,
      y: center.y + Math.sin(rad - (2 * Math.PI) / 3) * radius
    };
    return [front, left, right];
  }
}
