import Phaser from "phaser";
import { LaserPuzzleGame } from "../../game/simulation/LaserPuzzleGame";
import { colorNameToHex } from "../../game/simulation/colors";
import type {
  BeamColorName,
  DetectorData,
  DeviceData,
  DeviceType,
  Direction,
  EmitterData,
  GameSnapshot,
  LevelData,
  Vec2
} from "../../game/simulation/types";
import type { HudController } from "../../ui/hud";
import { ensureBackgroundMusic } from "../audio/backgroundMusic";

type EditorTool = "wall" | "erase" | "player" | "portal" | "emitter" | "detector" | "mirror" | "prism" | "splitter";

interface LevelEditorSceneData {
  initialLevel?: LevelData;
}

interface EditableLevel {
  id: string;
  title: string;
  width: number;
  height: number;
  playerStart: Vec2;
  portal: Vec2;
  emitters: EmitterData[];
  detectors: DetectorData[];
  devices: DeviceData[];
  requiredDetectors: number;
  interiorWalls: Set<string>;
}

const TOOL_OPTIONS: Array<{ value: EditorTool; label: string }> = [
  { value: "wall", label: "Стена" },
  { value: "erase", label: "Ластик" },
  { value: "player", label: "Игрок" },
  { value: "portal", label: "Портал" },
  { value: "emitter", label: "Эмиттер" },
  { value: "detector", label: "Детектор" },
  { value: "mirror", label: "Зеркало" },
  { value: "prism", label: "Призма" },
  { value: "splitter", label: "Разделитель" }
];

const COLOR_OPTIONS: BeamColorName[] = ["white", "red", "green", "blue", "yellow", "magenta", "cyan"];
const DIRECTION_OPTIONS: Direction[] = ["right", "down", "left", "up"];

export class LevelEditorScene extends Phaser.Scene {
  private readonly hud: HudController;

  private readonly tileSize = 64;

  private boardGraphics!: Phaser.GameObjects.Graphics;

  private entityGraphics!: Phaser.GameObjects.Graphics;

  private beamGraphics!: Phaser.GameObjects.Graphics;

  private hoverGraphics!: Phaser.GameObjects.Graphics;

  private infoText!: Phaser.GameObjects.Text;

  private editor!: EditableLevel;

  private previewSnapshot!: GameSnapshot;

  private panelEl: HTMLDivElement | null = null;

  private panelDockEl: HTMLElement | null = null;

  private shellEl: HTMLElement | null = null;

  private toolSelect!: HTMLSelectElement;

  private levelIdInput!: HTMLInputElement;

  private levelTitleInput!: HTMLInputElement;

  private emitterColorSelect!: HTMLSelectElement;

  private emitterDirSelect!: HTMLSelectElement;

  private detectorColorSelect!: HTMLSelectElement;

  private detectorMinRaysInput!: HTMLInputElement;

  private deviceRotInput!: HTMLInputElement;

  private prismTintSelect!: HTMLSelectElement;

  private requiredDetectorsInput!: HTMLInputElement;

  private exportOutputEl!: HTMLTextAreaElement;

  private exportStatusEl!: HTMLDivElement;

  private hoverCell: Vec2 | null = null;

  private lastPaintedCellKey: string | null = null;

  private isPaintDragging = false;

  private readonly onGlobalPointerUp = (): void => {
    this.isPaintDragging = false;
    this.lastPaintedCellKey = null;
  };

  constructor(hud: HudController) {
    super("level-editor");
    this.hud = hud;
  }

  init(data: LevelEditorSceneData): void {
    if (data?.initialLevel) {
      this.editor = this.levelToEditable(data.initialLevel);
      return;
    }
    this.editor = this.createDefaultEditableLevel();
  }

  create(): void {
    this.hud.setVisible(false);
    ensureBackgroundMusic(this);
    this.cameras.main.setRoundPixels(true);
    this.input.mouse?.disableContextMenu();
    this.input.keyboard?.disableGlobalCapture();

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

    this.infoText = this.add.text(10, this.scale.height - 12, "", {
      fontFamily: '"Chakra Petch", "Trebuchet MS", sans-serif',
      fontSize: "14px",
      color: "#9de1ee"
    }).setOrigin(0, 1);

    this.createEditorPanel();
    this.refreshPreviewAndRender();

    this.input.on("pointermove", this.onPointerMove, this);
    this.input.on("pointerdown", this.onPointerDown, this);
    this.input.on("pointerup", this.onGlobalPointerUp);
    window.addEventListener("pointerup", this.onGlobalPointerUp);

    this.events.once(Phaser.Scenes.Events.SHUTDOWN, this.cleanupScene, this);
    this.events.once(Phaser.Scenes.Events.DESTROY, this.cleanupScene, this);
  }

  private cleanupScene(): void {
    this.input.off("pointermove", this.onPointerMove, this);
    this.input.off("pointerdown", this.onPointerDown, this);
    this.input.off("pointerup", this.onGlobalPointerUp);
    window.removeEventListener("pointerup", this.onGlobalPointerUp);
    this.onGlobalPointerUp();
    this.input.keyboard?.enableGlobalCapture();
    this.cleanupPanel();
  }

  private createEditorPanel(): void {
    const gameRoot = this.game.canvas.parentElement;
    if (!gameRoot) {
      return;
    }

    this.cleanupPanel();

    const panel = document.createElement("div");
    panel.className = "editor-panel";
    panel.innerHTML = `
      <h3>Редактор Уровня</h3>
      <div class="editor-grid">
        <label>ID <input class="editor-id" type="text" /></label>
        <label>Название <input class="editor-title" type="text" /></label>
        <label>Инструмент
          <select class="editor-tool"></select>
        </label>
        <label>Эмиттер: цвет
          <select class="editor-emitter-color"></select>
        </label>
        <label>Эмиттер: направление
          <select class="editor-emitter-dir"></select>
        </label>
        <label>Детектор: цвет
          <select class="editor-detector-color"></select>
        </label>
        <label>Детектор: мин. лучей
          <input class="editor-detector-rays" type="number" min="1" max="8" step="1" />
        </label>
        <label>Поворот устройства
          <input class="editor-device-rot" type="number" min="-3" max="3" step="1" />
        </label>
        <label>Призма: цвет
          <select class="editor-prism-color"></select>
        </label>
        <label>Нужно детекторов
          <input class="editor-required-detectors" type="number" min="1" max="16" step="1" />
        </label>
      </div>
      <div class="editor-actions">
        <button class="editor-preview">Предпросмотр</button>
        <button class="editor-export">Экспорт TS</button>
        <button class="editor-clear">Очистить</button>
        <button class="editor-menu">В меню</button>
      </div>
      <div class="editor-export-status"></div>
      <textarea class="editor-export-output" readonly></textarea>
      <p class="editor-tip">ЛКМ по сетке размещает объект выбранного инструмента. Для стен и ластика можно вести мышью с зажатой ЛКМ.</p>
    `;

    const shell = gameRoot.closest(".game-shell") as HTMLElement | null;
    if (shell) {
      const dock = document.createElement("aside");
      dock.className = "editor-root";
      panel.classList.add("is-docked");
      dock.append(panel);
      shell.append(dock);
      shell.classList.add("editor-layout");
      this.panelDockEl = dock;
      this.shellEl = shell;
    } else {
      gameRoot.append(panel);
    }
    this.panelEl = panel;

    this.toolSelect = panel.querySelector(".editor-tool") as HTMLSelectElement;
    this.levelIdInput = panel.querySelector(".editor-id") as HTMLInputElement;
    this.levelTitleInput = panel.querySelector(".editor-title") as HTMLInputElement;
    this.emitterColorSelect = panel.querySelector(".editor-emitter-color") as HTMLSelectElement;
    this.emitterDirSelect = panel.querySelector(".editor-emitter-dir") as HTMLSelectElement;
    this.detectorColorSelect = panel.querySelector(".editor-detector-color") as HTMLSelectElement;
    this.detectorMinRaysInput = panel.querySelector(".editor-detector-rays") as HTMLInputElement;
    this.deviceRotInput = panel.querySelector(".editor-device-rot") as HTMLInputElement;
    this.prismTintSelect = panel.querySelector(".editor-prism-color") as HTMLSelectElement;
    this.requiredDetectorsInput = panel.querySelector(".editor-required-detectors") as HTMLInputElement;
    this.exportOutputEl = panel.querySelector(".editor-export-output") as HTMLTextAreaElement;
    this.exportStatusEl = panel.querySelector(".editor-export-status") as HTMLDivElement;

    for (const option of TOOL_OPTIONS) {
      this.toolSelect.add(new Option(option.label, option.value));
    }
    for (const color of COLOR_OPTIONS) {
      this.emitterColorSelect.add(new Option(color, color));
      this.detectorColorSelect.add(new Option(color, color));
      this.prismTintSelect.add(new Option(color, color));
    }
    for (const dir of DIRECTION_OPTIONS) {
      this.emitterDirSelect.add(new Option(dir, dir));
    }

    this.toolSelect.addEventListener("change", () => {
      this.exportStatusEl.textContent = "";
    });
    this.levelIdInput.addEventListener("input", () => {
      this.editor.id = this.levelIdInput.value;
    });
    this.levelTitleInput.addEventListener("input", () => {
      this.editor.title = this.levelTitleInput.value.trim() || "Новый уровень";
      this.refreshPreviewAndRender();
    });
    this.requiredDetectorsInput.addEventListener("input", () => {
      const value = Number.parseInt(this.requiredDetectorsInput.value, 10);
      const maxDetectors = Math.max(1, this.editor.detectors.length || 1);
      this.editor.requiredDetectors = clampInt(value, 1, maxDetectors);
      this.requiredDetectorsInput.value = String(this.editor.requiredDetectors);
      this.refreshPreviewAndRender();
    });

    panel.querySelector(".editor-preview")?.addEventListener("click", () => this.startPreview());
    panel.querySelector(".editor-export")?.addEventListener("click", () => this.exportAsTypeScript());
    panel.querySelector(".editor-clear")?.addEventListener("click", () => this.clearInteriorWalls());
    panel.querySelector(".editor-menu")?.addEventListener("click", () => this.scene.start("main-menu"));

    this.syncPanelInputs();
  }

  private cleanupPanel(): void {
    if (this.panelDockEl) {
      this.panelDockEl.remove();
      this.panelDockEl = null;
    }
    if (this.panelEl) {
      this.panelEl.remove();
      this.panelEl = null;
    }
    if (this.shellEl) {
      this.shellEl.classList.remove("editor-layout");
      this.shellEl = null;
    }
  }

  private onPointerMove(pointer: Phaser.Input.Pointer): void {
    const cell = this.pointerToCell(pointer.worldX, pointer.worldY);
    this.hoverCell = cell;
    this.drawHover();
    if (!this.isPaintDragging || !cell) {
      return;
    }
    if (this.currentTool() !== "wall" && this.currentTool() !== "erase") {
      return;
    }
    this.applyToolToCell(cell, true);
  }

  private onPointerDown(pointer: Phaser.Input.Pointer): void {
    const cell = this.pointerToCell(pointer.worldX, pointer.worldY);
    const tool = this.currentTool();
    this.isPaintDragging = tool === "wall" || tool === "erase";
    if (!cell) {
      return;
    }
    this.applyToolToCell(cell, false);
  }

  private applyToolToCell(cell: Vec2, fromDrag: boolean): void {
    if (!this.isInteriorCell(cell)) {
      return;
    }

    const cellKey = keyOf(cell);
    if (fromDrag && this.lastPaintedCellKey === cellKey) {
      return;
    }
    this.lastPaintedCellKey = cellKey;

    const tool = this.currentTool();
    if (tool !== "emitter" && this.isOnlyEmitterAtCell(cell)) {
      this.setEditorStatus("Нельзя удалить последний эмиттер. Сначала добавь другой эмиттер.");
      return;
    }

    if (tool === "wall") {
      if (sameCell(cell, this.editor.playerStart) || sameCell(cell, this.editor.portal)) {
        return;
      }
      this.removeObjectsAtCell(cell);
      this.editor.interiorWalls.add(cellKey);
      this.refreshPreviewAndRender();
      return;
    }

    if (tool === "erase") {
      this.editor.interiorWalls.delete(cellKey);
      this.removeObjectsAtCell(cell);
      this.refreshPreviewAndRender();
      return;
    }

    this.editor.interiorWalls.delete(cellKey);
    this.removeObjectsAtCell(cell);

    if (tool === "player") {
      if (!sameCell(cell, this.editor.portal)) {
        this.editor.playerStart = { ...cell };
      }
      this.refreshPreviewAndRender();
      return;
    }

    if (tool === "portal") {
      if (!sameCell(cell, this.editor.playerStart)) {
        this.editor.portal = { ...cell };
      }
      this.refreshPreviewAndRender();
      return;
    }

    if (tool === "emitter") {
      this.upsertEmitter(cell);
      this.refreshPreviewAndRender();
      return;
    }

    if (tool === "detector") {
      this.upsertDetector(cell);
      this.refreshPreviewAndRender();
      return;
    }

    this.upsertDevice(cell, tool);
    this.refreshPreviewAndRender();
  }

  private removeObjectsAtCell(cell: Vec2): void {
    this.editor.emitters = this.editor.emitters.filter((entry) => !sameCell(entry, cell));
    this.editor.detectors = this.editor.detectors.filter((entry) => !sameCell(entry, cell));
    this.editor.devices = this.editor.devices.filter((entry) => !sameCell(entry, cell));
    this.editor.requiredDetectors = clampInt(this.editor.requiredDetectors, 1, Math.max(1, this.editor.detectors.length || 1));
    this.syncPanelInputs();
  }

  private upsertEmitter(cell: Vec2): void {
    const color = this.emitterColorSelect.value as BeamColorName;
    const dir = this.emitterDirSelect.value as Direction;
    const existing = this.editor.emitters.find((entry) => sameCell(entry, cell));
    if (existing) {
      existing.color = color;
      existing.dir = dir;
      return;
    }
    this.editor.emitters.push({ x: cell.x, y: cell.y, color, dir });
  }

  private upsertDetector(cell: Vec2): void {
    const requiredColor = this.detectorColorSelect.value as BeamColorName;
    const minRays = clampInt(Number.parseInt(this.detectorMinRaysInput.value, 10), 1, 8);
    const existing = this.editor.detectors.find((entry) => sameCell(entry, cell));
    if (existing) {
      existing.requiredColor = requiredColor;
      existing.minRays = minRays;
    } else {
      this.editor.detectors.push({ x: cell.x, y: cell.y, requiredColor, minRays });
    }
    const maxDetectors = Math.max(1, this.editor.detectors.length);
    this.editor.requiredDetectors = clampInt(this.editor.requiredDetectors, 1, maxDetectors);
    this.syncPanelInputs();
  }

  private upsertDevice(cell: Vec2, tool: Exclude<EditorTool, "wall" | "erase" | "player" | "portal" | "emitter" | "detector">): void {
    const type = tool as DeviceType;
    const rot = clampInt(Number.parseInt(this.deviceRotInput.value, 10), -3, 3);
    const tint = this.prismTintSelect.value as BeamColorName;

    const existing = this.editor.devices.find((entry) => sameCell(entry, cell));
    if (existing) {
      existing.type = type;
      existing.rot = rot;
      existing.tint = type === "prism" ? tint : undefined;
      if (!existing.id || !existing.id.trim()) {
        existing.id = this.nextDeviceId(type);
      }
      return;
    }

    this.editor.devices.push({
      id: this.nextDeviceId(type),
      type,
      x: cell.x,
      y: cell.y,
      rot,
      tint: type === "prism" ? tint : undefined
    });
  }

  private nextDeviceId(type: DeviceType): string {
    let index = 1;
    while (this.editor.devices.some((entry) => entry.id === `${type}-${index}`)) {
      index += 1;
    }
    return `${type}-${index}`;
  }

  private refreshPreviewAndRender(): void {
    const runtimeLevel = this.buildRuntimeLevel();
    const sim = new LaserPuzzleGame([runtimeLevel]);
    this.previewSnapshot = sim.getSnapshot();

    this.drawBoard();
    this.drawBeams();
    this.drawHover();

    this.infoText.setText(
      `Объекты: стены ${this.editor.interiorWalls.size}, эмиттеры ${this.editor.emitters.length}, детекторы ${this.editor.detectors.length}, устройства ${this.editor.devices.length}`
    );
  }

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

    const snapshot = this.previewSnapshot;
    const wallSet = new Set(snapshot.level.walls.map((wall) => keyOf(wall)));

    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 isWall = 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, 8);

        this.boardGraphics.lineStyle(1, isWall ? 0x45657f : 0x214155, 0.6);
        this.boardGraphics.strokeRoundedRect(px + 0.5, py + 0.5, this.tileSize - 1, this.tileSize - 1, 8);
      }
    }

    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, 0xfff7f7, 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);
    }
  }

  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);

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

      if (device.type === "mirror") {
        const axis = this.unitFromAngle(this.deviceRotationDeg(device.type, 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, this.deviceRotationDeg(device.type, 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(this.deviceRotationDeg(device.type, device.rot));
        const branch = this.unitFromAngle(this.deviceRotationDeg(device.type, 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(): void {
    this.beamGraphics.clear();
    for (const segment of this.previewSnapshot.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 drawHover(): void {
    this.hoverGraphics.clear();
    if (!this.hoverCell || !this.isInteriorCell(this.hoverCell)) {
      return;
    }

    const px = this.hoverCell.x * this.tileSize;
    const py = this.hoverCell.y * this.tileSize;
    this.hoverGraphics.lineStyle(2, 0xfff398, 0.9);
    this.hoverGraphics.strokeRoundedRect(px + 2, py + 2, this.tileSize - 4, this.tileSize - 4, 6);
  }

  private pointerToCell(worldX: number, worldY: number): Vec2 | null {
    const x = Math.floor(worldX / this.tileSize);
    const y = Math.floor(worldY / this.tileSize);
    if (x < 0 || y < 0 || x >= this.editor.width || y >= this.editor.height) {
      return null;
    }
    return { x, y };
  }

  private isInteriorCell(cell: Vec2): boolean {
    return cell.x > 0 && cell.y > 0 && cell.x < this.editor.width - 1 && cell.y < this.editor.height - 1;
  }

  private currentTool(): EditorTool {
    return (this.toolSelect?.value as EditorTool) || "wall";
  }

  private buildRuntimeLevel(): LevelData {
    const interiorWalls = [...this.editor.interiorWalls].map(parseKey).sort(sortByCell);
    const walls = this.withBoundaryWalls(interiorWalls);
    const emitters = this.editor.emitters.length > 0
      ? this.editor.emitters.map((entry) => ({ ...entry }))
      : [{ x: 1, y: Math.floor(this.editor.height / 2), dir: "right" as Direction, color: "white" as BeamColorName }];

    return {
      id: sanitizeId(this.editor.id),
      title: this.editor.title,
      width: this.editor.width,
      height: this.editor.height,
      playerStart: { ...this.editor.playerStart },
      emitter: { ...emitters[0] },
      emitters,
      detectors: this.editor.detectors.map((entry) => ({ ...entry })),
      requiredDetectors: clampInt(this.editor.requiredDetectors, 1, Math.max(1, this.editor.detectors.length || 1)),
      portal: { ...this.editor.portal },
      devices: this.editor.devices.map((entry) => ({ ...entry })),
      walls
    };
  }

  private withBoundaryWalls(interiorWalls: Vec2[]): Vec2[] {
    const all = new Map<string, Vec2>();
    for (let x = 0; x < this.editor.width; x += 1) {
      all.set(`${x},0`, { x, y: 0 });
      all.set(`${x},${this.editor.height - 1}`, { x, y: this.editor.height - 1 });
    }
    for (let y = 1; y < this.editor.height - 1; y += 1) {
      all.set(`0,${y}`, { x: 0, y });
      all.set(`${this.editor.width - 1},${y}`, { x: this.editor.width - 1, y });
    }
    for (const wall of interiorWalls) {
      all.set(keyOf(wall), wall);
    }
    return [...all.values()].sort(sortByCell);
  }

  private startPreview(): void {
    const runtimeLevel = this.buildRuntimeLevel();
    this.scene.start("game", {
      startLevelIndex: 0,
      customLevels: [runtimeLevel],
      victorySceneEnabled: false,
      returnSceneKey: "level-editor",
      returnSceneData: { initialLevel: runtimeLevel }
    });
  }

  private exportAsTypeScript(): void {
    const code = this.createLevelTsSnippet();
    this.exportOutputEl.value = code;
    this.exportOutputEl.classList.add("is-visible");

    const clipboard = navigator.clipboard;
    if (!clipboard || typeof clipboard.writeText !== "function") {
      this.exportStatusEl.textContent = "TS-код готов. Скопируй его из поля ниже.";
      return;
    }

    void clipboard.writeText(code)
      .then(() => {
        this.exportStatusEl.textContent = "TS-код уровня скопирован в буфер.";
      })
      .catch(() => {
        this.exportStatusEl.textContent = "TS-код готов. Скопируй его из поля ниже.";
      });
  }

  private isOnlyEmitterAtCell(cell: Vec2): boolean {
    if (this.editor.emitters.length !== 1) {
      return false;
    }
    return sameCell(this.editor.emitters[0], cell);
  }

  private setEditorStatus(message: string): void {
    if (!this.exportStatusEl) {
      return;
    }
    this.exportStatusEl.textContent = message;
  }

  private createLevelTsSnippet(): string {
    const runtime = this.buildRuntimeLevel();
    const interiorWalls = [...this.editor.interiorWalls].map(parseKey).sort(sortByCell);
    const emitters = runtime.emitters && runtime.emitters.length > 0 ? runtime.emitters : [runtime.emitter];
    const emitterLine = formatEmitterObject(emitters[0]);
    const emittersLine = emitters.map((entry) => `    ${formatEmitterObject(entry)}`).join(",\n");
    const detectorsLine = runtime.detectors.map((entry) => `    ${formatDetectorObject(entry)}`).join(",\n");
    const devicesLine = runtime.devices.map((entry) => `    ${formatDeviceObject(entry)}`).join(",\n");
    const wallsLine = interiorWalls.map((entry) => `    ${formatVec(entry)}`).join(",\n");

    return `withBoundaryWalls({
  id: "${runtime.id}",
  title: "${escapeTs(runtime.title)}",
  width: ${runtime.width},
  height: ${runtime.height},
  playerStart: ${formatVec(runtime.playerStart)},
  emitter: ${emitterLine},
  emitters: [
${emittersLine}
  ],
  detectors: [
${detectorsLine}
  ],
  requiredDetectors: ${runtime.requiredDetectors ?? runtime.detectors.length},
  portal: ${formatVec(runtime.portal)},
  devices: [
${devicesLine}
  ],
  interiorWalls: [
${wallsLine}
  ]
})`;
  }

  private clearInteriorWalls(): void {
    this.editor.interiorWalls.clear();
    this.refreshPreviewAndRender();
  }

  private syncPanelInputs(): void {
    this.levelIdInput.value = this.editor.id;
    this.levelTitleInput.value = this.editor.title;
    this.requiredDetectorsInput.value = String(this.editor.requiredDetectors);
    if (!this.toolSelect.value) {
      this.toolSelect.value = "wall";
    }
    if (!this.emitterColorSelect.value) {
      this.emitterColorSelect.value = "white";
    }
    if (!this.emitterDirSelect.value) {
      this.emitterDirSelect.value = "right";
    }
    if (!this.detectorColorSelect.value) {
      this.detectorColorSelect.value = "red";
    }
    if (!this.detectorMinRaysInput.value) {
      this.detectorMinRaysInput.value = "1";
    }
    if (!this.deviceRotInput.value) {
      this.deviceRotInput.value = "0";
    }
    if (!this.prismTintSelect.value) {
      this.prismTintSelect.value = "white";
    }
  }

  private levelToEditable(level: LevelData): EditableLevel {
    const emitters = (level.emitters && level.emitters.length > 0 ? level.emitters : [level.emitter]).map((entry) => ({ ...entry }));
    const interiorWalls = new Set<string>();
    for (const wall of level.walls) {
      if (wall.x <= 0 || wall.y <= 0 || wall.x >= level.width - 1 || wall.y >= level.height - 1) {
        continue;
      }
      interiorWalls.add(keyOf(wall));
    }

    return {
      id: sanitizeId(level.id || "custom-level"),
      title: level.title || "Новый уровень",
      width: level.width,
      height: level.height,
      playerStart: { ...level.playerStart },
      portal: { ...level.portal },
      emitters,
      detectors: level.detectors.map((entry) => ({ ...entry })),
      devices: level.devices.map((entry) => ({ ...entry })),
      requiredDetectors: clampInt(level.requiredDetectors ?? level.detectors.length, 1, Math.max(1, level.detectors.length || 1)),
      interiorWalls
    };
  }

  private createDefaultEditableLevel(): EditableLevel {
    const width = 13;
    const height = 9;
    return {
      id: "custom-level",
      title: "Новый уровень",
      width,
      height,
      playerStart: { x: 2, y: 7 },
      portal: { x: width - 2, y: height - 2 },
      emitters: [{ x: 1, y: 4, dir: "right", color: "white" }],
      detectors: [{ x: 4, y: 1, requiredColor: "red", minRays: 1 }],
      devices: [],
      requiredDetectors: 1,
      interiorWalls: new Set<string>()
    };
  }

  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 deviceRotationDeg(type: DeviceType, rawRot: number): number {
    if (Math.abs(rawRot) <= 3 && Number.isInteger(rawRot)) {
      if (type === "mirror") {
        return 45 + rawRot * 90;
      }
      return rawRot * 90;
    }
    return rawRot;
  }

  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 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];
  }
}

function keyOf(point: Vec2): string {
  return `${point.x},${point.y}`;
}

function parseKey(key: string): Vec2 {
  const [xRaw, yRaw] = key.split(",");
  return {
    x: Number.parseInt(xRaw, 10),
    y: Number.parseInt(yRaw, 10)
  };
}

function sortByCell(a: Vec2, b: Vec2): number {
  return a.y - b.y || a.x - b.x;
}

function sameCell(a: Vec2, b: Vec2): boolean {
  return a.x === b.x && a.y === b.y;
}

function clampInt(value: number, min: number, max: number): number {
  if (!Number.isFinite(value)) {
    return min;
  }
  return Math.max(min, Math.min(max, Math.floor(value)));
}

function sanitizeId(id: string): string {
  const trimmed = id.trim().toLowerCase();
  const clean = trimmed
    .replace(/[^a-z0-9_-]+/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-|-$/g, "");
  return clean || "custom-level";
}

function formatVec(point: Vec2): string {
  return `{ x: ${point.x}, y: ${point.y} }`;
}

function formatEmitterObject(entry: EmitterData): string {
  const dirPart = entry.dir ? `, dir: "${entry.dir}"` : "";
  const anglePart = typeof entry.angle === "number" ? `, angle: ${entry.angle}` : "";
  return `{ x: ${entry.x}, y: ${entry.y}${dirPart}${anglePart}, color: "${entry.color}" }`;
}

function formatDetectorObject(entry: DetectorData): string {
  const minRays = typeof entry.minRays === "number" ? `, minRays: ${entry.minRays}` : "";
  return `{ x: ${entry.x}, y: ${entry.y}, requiredColor: "${entry.requiredColor}"${minRays} }`;
}

function formatDeviceObject(entry: DeviceData): string {
  const tintPart = entry.tint ? `, tint: "${entry.tint}"` : "";
  return `{ id: "${escapeTs(entry.id)}", type: "${entry.type}", x: ${entry.x}, y: ${entry.y}, rot: ${entry.rot}${tintPart} }`;
}

function escapeTs(value: string): string {
  return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
