import * as fs from "node:fs";
import { createCanvas, Image, loadImage } from "@napi-rs/canvas";
import type { B01DeviceStatus, B01MapData } from "../b01/types";
import { Q10AssetCatalog, resolveQ10PluginAssetPath } from "./Q10AssetCatalog";
import { Q10_CANVAS_SCALE, getQ10ExportCanvasScale, Q10MapGeometry } from "./Q10MapGeometry";
import type {
	Q10CreatorArea,
	Q10CreatorData,
	Q10CreatorLine,
	Q10CreatorObstacle,
	Q10CreatorPathPoint,
	Q10CreatorSuspectedPoint,
	Q10MapPixelPoint,
	Q10PixelPose
} from "./types";

const Q10_LAYOUT = {
	areaStrokeWidth: 2,
	areaMopDash: 3,
	forbidLineIconSize: 12,
	thresholdRowShiftRatio: 0.63
} as const;
const DARK_MAP_COLORS = {
	wall: 1836349183,
	inWall: 1940580863,
	rooms: [1940580863, 3854457599, 3648937983, 634505215] as const,
	roomTagBase: [4279123053, 4283645184, 4286455337, 4278537798] as const,
	roomTagStroke: [4278528336, 4281147648, 4284156949, 4278202925] as const,
	forbidLine: 4294919482,
	forbidFill: 872367418,
	eraseFill: 872387840,
	eraseBase: 4294939904,
	thresholdBase: 4292136800,
	text: 3426499651
} as const;

interface LoadedQ10Assets {
	device?: Image;
	power?: Image;
	forbidlineIcon?: Image;
	obstacle?: Image;
	tiaoGuoIcon?: Image;
	mapCarpetMaterial?: Image;
	mapThresholdMaterial?: Image;
	roomTags: Map<number, Image>;
	suspectedThreshold?: Image;
	suspectedEasycard?: Image;
	suspectedCliff?: Image;
}

interface Q10RenderMetrics {
	baseIconSize: number;
	roomFontSize: number;
	roomBubbleDiameter: number;
	roomGap: number;
	roomIconSize: number;
	roomBadgeRadius: number;
}

interface Q10PathLayerStyle {
	strokeStyle: string;
	lineWidth: number;
	dash?: number[];
	dashOffset?: number;
}

type Q10OriginalMaterialKind = "ceramicTile" | "horizontalFloorBoard" | "verticalFloorBoard";

interface Q10RenderedMaps {
	full: Buffer;
	clean: Buffer;
}

function packedColorToRgbaBytes(color: number): [number, number, number, number] {
	return [
		(color >>> 24) & 0xff,
		(color >>> 16) & 0xff,
		(color >>> 8) & 0xff,
		color & 0xff
	];
}

function packedArgbToCss(color: number, alphaOverride?: number): string {
	const a = ((color >>> 24) & 0xff) / 255;
	const r = (color >>> 16) & 0xff;
	const g = (color >>> 8) & 0xff;
	const b = color & 0xff;
	const alpha = alphaOverride ?? a;
	return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}

function getRenderMetrics(): Q10RenderMetrics {
	const kImgRate = Q10_CANVAS_SCALE;
	return {
		baseIconSize: 8 * kImgRate,
		roomFontSize: 10,
		roomBubbleDiameter: 12,
		roomGap: 4,
		roomIconSize: 6,
		roomBadgeRadius: 6
	};
}

function imageWidth(image?: Image): number {
	return Number(image?.width ?? image?.naturalWidth ?? 0);
}

function imageHeight(image?: Image): number {
	return Number(image?.height ?? image?.naturalHeight ?? 0);
}

function drawCenteredAsset(
	ctx: any,
	image: Image | undefined,
	x: number,
	y: number,
	drawWidth: number,
	rotationDeg = 0
): void {
	if (!image) return;
	const width = imageWidth(image);
	const height = imageHeight(image);
	if (width <= 0 || height <= 0) return;

	const scale = Math.min(drawWidth / width, drawWidth / height);
	const fittedWidth = width * scale;
	const fittedHeight = height * scale;

	ctx.save();
	ctx.translate(x, y);
	if (rotationDeg) ctx.rotate((rotationDeg * Math.PI) / 180);
	ctx.drawImage(image as any, -fittedWidth / 2, -fittedHeight / 2, fittedWidth, fittedHeight);
	ctx.restore();
}

function drawCenteredSpriteWidthScaled(
	ctx: any,
	image: Image | undefined,
	x: number,
	y: number,
	targetWidth: number,
	rotationDeg = 0
): void {
	if (!image) return;
	const width = imageWidth(image);
	const height = imageHeight(image);
	if (width <= 0 || height <= 0) return;

	const scale = targetWidth / width;
	const drawWidth = width * scale;
	const drawHeight = height * scale;

	ctx.save();
	ctx.translate(x, y);
	if (rotationDeg) ctx.rotate((rotationDeg * Math.PI) / 180);
	ctx.drawImage(image as any, -drawWidth / 2, -drawHeight / 2, drawWidth, drawHeight);
	ctx.restore();
}

function drawCenteredCoverSquareAsset(
	ctx: any,
	image: Image | undefined,
	x: number,
	y: number,
	size: number,
	rotationDeg = 0
): void {
	if (!image) return;
	const width = imageWidth(image);
	const height = imageHeight(image);
	if (width <= 0 || height <= 0) return;

	const scale = Math.max(size / width, size / height);
	const drawWidth = width * scale;
	const drawHeight = height * scale;

	ctx.save();
	ctx.translate(x, y);
	if (rotationDeg) ctx.rotate((rotationDeg * Math.PI) / 180);
	ctx.beginPath();
	ctx.rect(-size / 2, -size / 2, size, size);
	ctx.clip();
	ctx.drawImage(image as any, -drawWidth / 2, -drawHeight / 2, drawWidth, drawHeight);
	ctx.restore();
}

interface ImageOpaqueBounds {
	sx: number;
	sy: number;
	sw: number;
	sh: number;
}

function fillPolygon(ctx: any, points: Q10MapPixelPoint[]): void {
	if (!points.length) return;
	ctx.beginPath();
	ctx.moveTo(points[0].x, points[0].y);
	for (let index = 1; index < points.length; index++) {
		ctx.lineTo(points[index].x, points[index].y);
	}
	ctx.closePath();
}

function isMaterialMaskCellWalkable(mask: Uint8Array, width: number, height: number, x: number, y: number): boolean {
	return x >= 0 && x < width && y >= 0 && y < height && mask[y * width + x] === 1;
}

function buildRoomMaterialMaskGrid(
	baseGrid: Buffer,
	width: number,
	height: number,
	roomIds: number[]
): Uint8Array {
	const roomIdSet = new Set<number>(roomIds);
	const mask = new Uint8Array(width * height);
	for (let index = 0; index < width * height; index++) {
		if (roomIdSet.has(baseGrid[index] ?? 0)) {
			mask[index] = 1;
		}
	}
	return mask;
}

function pairMaterialSegments(points: Q10MapPixelPoint[]): Q10MapPixelPoint[][] {
	const pairs: Q10MapPixelPoint[][] = [];
	const pairCount = Math.floor(points.length / 2);
	for (let index = 0; index < pairCount; index++) {
		pairs.push([
			points[index * 2]!,
			points[index * 2 + 1]!
		]);
	}
	return pairs;
}

function clipHorizontalMaterialPath(
	startX: number,
	endX: number,
	y: number,
	mask: Uint8Array,
	width: number,
	height: number
): Q10MapPixelPoint[][] {
	const subPoints: Q10MapPixelPoint[] = [];
	let last = 0;

	for (let x = startX; x <= endX; x++) {
		const point = { x, y };
		const isValid = isMaterialMaskCellWalkable(mask, width, height, x, y);
		if (!isValid) {
			if (last === 1 || last === 2) {
				subPoints.push(point);
				last = 0;
			}
			continue;
		}

		if (last === 0) {
			subPoints.push(point);
			last = 1;
		} else {
			last = 2;
		}
		if (x === endX) {
			subPoints.push(point);
		}
	}

	return pairMaterialSegments(subPoints);
}

function clipVerticalMaterialPath(
	x: number,
	startY: number,
	endY: number,
	mask: Uint8Array,
	width: number,
	height: number
): Q10MapPixelPoint[][] {
	const subPoints: Q10MapPixelPoint[] = [];
	let last = 0;

	for (let y = startY; y <= endY; y++) {
		const point = { x, y };
		const isValid = isMaterialMaskCellWalkable(mask, width, height, x, y);
		if (!isValid) {
			if (last === 1 || last === 2) {
				subPoints.push(point);
				last = 0;
			}
			continue;
		}

		if (last === 0) {
			subPoints.push(point);
			last = 1;
		} else {
			last = 2;
		}
		if (y === endY) {
			subPoints.push(point);
		}
	}

	return pairMaterialSegments(subPoints);
}

function buildCeramicTileMaterialPaths(
	mask: Uint8Array,
	width: number,
	height: number,
	resolution: number
): Q10MapPixelPoint[][] {
	const wStep = Math.max(1, Math.floor(0.8 / resolution));
	const hStep = Math.max(1, Math.floor(0.8 / resolution));
	const paths: Q10MapPixelPoint[][] = [];

	for (let x = 0; x <= width; x++) {
		if (x % wStep === 0) {
			paths.push(...clipVerticalMaterialPath(x, 0, height, mask, width, height));
		}
	}
	for (let y = 0; y <= height; y++) {
		if (y % hStep === 0) {
			paths.push(...clipHorizontalMaterialPath(0, width, y, mask, width, height));
		}
	}

	return paths;
}

function buildHorizontalFloorBoardMaterialPaths(
	mask: Uint8Array,
	width: number,
	height: number,
	resolution: number
): Q10MapPixelPoint[][] {
	const materialW = 1.2;
	const materialH = 0.3;
	let wStep = Math.max(1, Math.floor(materialW / resolution));
	const hStep = Math.max(1, Math.floor(materialH / resolution));
	const paths: Q10MapPixelPoint[][] = [];

	for (let y = 0; y <= height; y++) {
		if (y % hStep === 0) {
			paths.push(...clipHorizontalMaterialPath(0, width, y, mask, width, height));
		}
	}

	wStep = wStep / 2;
	let columnIndex = 0;
	for (let x = 0; x <= width; x++) {
		if (x % wStep !== 0) continue;
		columnIndex += 1;

		let points: Q10MapPixelPoint[] = [];
		for (let y = 0; y <= height; y++) {
			if (y % hStep !== 0) continue;
			points.push({ x, y });
		}

		if (columnIndex % 2 === 1) {
			if (Math.floor(points.length % 2) === 1) {
				points = points.slice(0, points.length - 1);
			}
		} else {
			if (points.length > 0) {
				points = points.slice(1);
			}
			if (Math.floor(points.length % 2) === 1) {
				points = points.slice(0, points.length - 1);
			}
		}

		for (let index = 0; index < points.length / 2; index++) {
			const start = points[index * 2]!;
			const end = points[index * 2 + 1]!;
			paths.push(...clipVerticalMaterialPath(start.x, start.y, end.y, mask, width, height));
		}
	}

	return paths;
}

function buildVerticalFloorBoardMaterialPaths(
	mask: Uint8Array,
	width: number,
	height: number,
	resolution: number
): Q10MapPixelPoint[][] {
	const materialW = 0.3;
	const materialH = 1.2;
	const wStep = Math.max(1, Math.floor(materialW / resolution));
	let hStep = Math.max(1, Math.floor(materialH / resolution));
	const paths: Q10MapPixelPoint[][] = [];

	for (let x = 0; x <= width; x++) {
		if (x % wStep === 0) {
			paths.push(...clipVerticalMaterialPath(x, 0, height, mask, width, height));
		}
	}

	hStep = hStep / 2;
	let rowIndex = 0;
	for (let y = 0; y <= height; y++) {
		if (y % hStep !== 0) continue;
		rowIndex += 1;

		let points: Q10MapPixelPoint[] = [];
		for (let x = 0; x <= width; x++) {
			if (x % wStep !== 0) continue;
			points.push({ x, y });
		}

		if (rowIndex % 2 === 1) {
			if (Math.floor(points.length % 2) === 1) {
				points = points.slice(0, points.length - 1);
			}
		} else {
			if (points.length > 0) {
				points = points.slice(1);
			}
			if (Math.floor(points.length % 2) === 1) {
				points = points.slice(0, points.length - 1);
			}
		}

		for (let index = 0; index < points.length / 2; index++) {
			const start = points[index * 2]!;
			const end = points[index * 2 + 1]!;
			paths.push(...clipHorizontalMaterialPath(start.x, end.x, start.y, mask, width, height));
		}
	}

	return paths;
}

function measureRoomText(
	ctx: any,
	text: string,
	fontSize: number
): { width: number; ascent: number; descent: number; height: number } {
	const metrics = ctx.measureText(text);
	const fontMetrics = metrics as TextMetrics & {
		fontBoundingBoxAscent?: number;
		fontBoundingBoxDescent?: number;
	};
	const ascent =
		fontMetrics.fontBoundingBoxAscent ||
		metrics.actualBoundingBoxAscent ||
		fontSize * 0.78;
	const descent =
		fontMetrics.fontBoundingBoxDescent ||
		metrics.actualBoundingBoxDescent ||
		fontSize * 0.22;
	return {
		width: metrics.width,
		ascent,
		descent,
		height: ascent + descent
	};
}

export class Q10MapBuilder {
	private assetsLoadedForModel: string | null = null;
	private assets: LoadedQ10Assets = this.createEmptyAssets();
	private opaqueBoundsCache = new WeakMap<Image, ImageOpaqueBounds>();

	constructor(private adapter?: any) {}

	private createEmptyAssets(): LoadedQ10Assets {
		return { roomTags: new Map<number, Image>() };
	}

	private async loadImageIfExists(filePath: string): Promise<Image | undefined> {
		if (!fs.existsSync(filePath)) return undefined;
		try {
			return await loadImage(fs.readFileSync(filePath));
		} catch {
			return undefined;
		}
	}

	private toImageBuffer(fileData: unknown): Buffer | undefined {
		if (!fileData) return undefined;
		if (Buffer.isBuffer(fileData)) return fileData;
		if (typeof fileData === "object" && fileData !== null && "file" in fileData) {
			const file = (fileData as { file: unknown }).file;
			if (Buffer.isBuffer(file)) return file;
			if (file instanceof Uint8Array) return Buffer.from(file);
			if (file instanceof ArrayBuffer) return Buffer.from(file);
			if (typeof file === "string") return Buffer.from(file);
			return undefined;
		}
		if (fileData instanceof Uint8Array) return Buffer.from(fileData);
		if (fileData instanceof ArrayBuffer) return Buffer.from(fileData);
		if (typeof fileData === "string") return Buffer.from(fileData);
		return undefined;
	}

	private async loadImageFromAdapterAssets(relativePath: string, robotModel?: string): Promise<Image | undefined> {
		if (
			!robotModel ||
			!this.adapter?.name ||
			typeof this.adapter.fileExistsAsync !== "function" ||
			typeof this.adapter.readFileAsync !== "function"
		) {
			return undefined;
		}

		const assetPath = `assets/${robotModel}/${relativePath}`;
		const namespaces = this.adapter.name.includes(".")
			? [this.adapter.name, this.adapter.name.split(".")[0]]
			: [this.adapter.name];

		for (const namespace of namespaces) {
			try {
				if (!(await this.adapter.fileExistsAsync(namespace, assetPath))) continue;
				const fileData = await this.adapter.readFileAsync(namespace, assetPath);
				const buffer = this.toImageBuffer(fileData);
				if (!buffer) continue;
				return await loadImage(buffer);
			} catch {
				// try next namespace
			}
		}

		return undefined;
	}

	private async loadImageAsset(relativePath: string, robotModel?: string): Promise<Image | undefined> {
		return (
			(await this.loadImageFromAdapterAssets(relativePath, robotModel)) ||
			(await this.loadImageIfExists(resolveQ10PluginAssetPath(relativePath)))
		);
	}

	private async ensureAssets(robotModel?: string): Promise<void> {
		const modelCacheKey = robotModel ?? "";
		if (this.assetsLoadedForModel === modelCacheKey) return;

		this.assets = this.createEmptyAssets();

		this.assets.device = await this.loadImageAsset(Q10AssetCatalog.device, robotModel);
		this.assets.power = await this.loadImageAsset(Q10AssetCatalog.power, robotModel);
		this.assets.forbidlineIcon = await this.loadImageAsset(Q10AssetCatalog.forbidlineIcon, robotModel);
		this.assets.obstacle = await this.loadImageAsset(Q10AssetCatalog.obstacle, robotModel);
		this.assets.tiaoGuoIcon = await this.loadImageAsset(Q10AssetCatalog.tiaoGuoIcon, robotModel);
		this.assets.mapCarpetMaterial = await this.loadImageAsset(Q10AssetCatalog.mapCarpetMaterial, robotModel);
		this.assets.mapThresholdMaterial = await this.loadImageAsset(Q10AssetCatalog.mapThresholdMaterial, robotModel);
		this.assets.suspectedThreshold = await this.loadImageAsset(Q10AssetCatalog.yisiMenkan, robotModel);
		this.assets.suspectedEasycard = await this.loadImageAsset(Q10AssetCatalog.yisiYika, robotModel);
		this.assets.suspectedCliff = await this.loadImageAsset(Q10AssetCatalog.yisiXuanya, robotModel);

		for (let roomType = 0; roomType < Q10AssetCatalog.roomTags.length; roomType++) {
			const image = await this.loadImageAsset(Q10AssetCatalog.roomTags[roomType], robotModel);
			if (image) this.assets.roomTags.set(roomType, image);
		}

		this.assetsLoadedForModel = modelCacheKey;
	}

	private getOpaqueBounds(image: Image | undefined): ImageOpaqueBounds | undefined {
		if (!image) return undefined;
		const cached = this.opaqueBoundsCache.get(image);
		if (cached) return cached;

		const width = imageWidth(image);
		const height = imageHeight(image);
		if (width <= 0 || height <= 0) return undefined;

		const canvas = createCanvas(width, height);
		const ctx = canvas.getContext("2d");
		ctx.drawImage(image as any, 0, 0, width, height);
		const pixels = ctx.getImageData(0, 0, width, height).data;
		let minX = width;
		let minY = height;
		let maxX = -1;
		let maxY = -1;

		for (let y = 0; y < height; y++) {
			for (let x = 0; x < width; x++) {
				const alpha = pixels[(y * width + x) * 4 + 3] ?? 0;
				if (alpha <= 8) continue;
				if (x < minX) minX = x;
				if (y < minY) minY = y;
				if (x > maxX) maxX = x;
				if (y > maxY) maxY = y;
			}
		}

		const bounds = maxX >= minX && maxY >= minY
			? { sx: minX, sy: minY, sw: maxX - minX + 1, sh: maxY - minY + 1 }
			: { sx: 0, sy: 0, sw: width, sh: height };
		this.opaqueBoundsCache.set(image, bounds);
		return bounds;
	}

	private drawCenteredOpaqueAsset(
		ctx: any,
		image: Image | undefined,
		x: number,
		y: number,
		drawWidth: number,
		rotationDeg = 0
	): void {
		if (!image) return;
		const bounds = this.getOpaqueBounds(image);
		if (!bounds || bounds.sw <= 0 || bounds.sh <= 0) {
			drawCenteredAsset(ctx, image, x, y, drawWidth, rotationDeg);
			return;
		}

		const scale = Math.min(drawWidth / bounds.sw, drawWidth / bounds.sh);
		const fittedWidth = bounds.sw * scale;
		const fittedHeight = bounds.sh * scale;

		ctx.save();
		ctx.translate(x, y);
		if (rotationDeg) ctx.rotate((rotationDeg * Math.PI) / 180);
		ctx.drawImage(
			image as any,
			bounds.sx,
			bounds.sy,
			bounds.sw,
			bounds.sh,
			-fittedWidth / 2,
			-fittedHeight / 2,
			fittedWidth,
			fittedHeight
		);
		ctx.restore();
	}

	private drawBaseMap(ctx: any, data: B01MapData, creator: Q10CreatorData): void {
		const width = data.header.sizeX;
		const height = data.header.sizeY;
		const tempCanvas = createCanvas(width, height);
		const tempCtx = tempCanvas.getContext("2d");
		const imageData = tempCtx.createImageData(width, height);
		const buffer = imageData.data;
		const roomColorMap = new Map<number, number>();

		for (const room of creator.roomModels) roomColorMap.set(room.gridValue, room.colorID);

		for (let index = 0; index < data.mapGrid.length; index++) {
			const value = data.mapGrid[index];
			const offset = index * 4;

			if (value === 127) {
				buffer[offset] = 0;
				buffer[offset + 1] = 0;
				buffer[offset + 2] = 0;
				buffer[offset + 3] = 0;
				continue;
			}

			let color: number = DARK_MAP_COLORS.inWall;
			if (value >= 128) color = DARK_MAP_COLORS.wall;
			else if (value > 1) {
				const roomColor = roomColorMap.get(value) ?? (value - 1) % DARK_MAP_COLORS.rooms.length;
				color = DARK_MAP_COLORS.rooms[roomColor] ?? DARK_MAP_COLORS.rooms[0];
			}

			const [r, g, b, a] = packedColorToRgbaBytes(color);
			buffer[offset] = r;
			buffer[offset + 1] = g;
			buffer[offset + 2] = b;
			buffer[offset + 3] = a || 255;
		}

		tempCtx.putImageData(imageData, 0, 0);
		ctx.drawImage(tempCanvas as any, 0, 0);
	}

	private withOutputSpace(ctx: any, geometry: Q10MapGeometry, draw: (outputCtx: any) => void): void {
		ctx.save();
		const scale = Math.max(geometry.canvasScaleValue(), 0.001);
		ctx.scale(1 / scale, 1 / scale);
		ctx.imageSmoothingEnabled = true;
		draw(ctx);
		ctx.restore();
	}

	private withMapSpace(ctx: any, geometry: Q10MapGeometry, draw: (mapCtx: any) => void): void {
		ctx.save();
		const scale = Math.max(geometry.canvasScaleValue(), 0.001);
		ctx.scale(scale, scale);
		draw(ctx);
		ctx.restore();
	}

	public buildSelfIdentifiedCarpetSourceCanvas(carpet: Q10CreatorData["selfIdentifiedCarpets"][number]): any | null {
		if (carpet.width <= 0 || carpet.height <= 0 || !carpet.mask.length) return null;

		const sourceWidth = carpet.width * 3;
		const sourceHeight = carpet.height * 3;
		const canvas = createCanvas(sourceWidth, sourceHeight);
		const carpetCtx = canvas.getContext("2d");
		const imageData = carpetCtx.createImageData(sourceWidth, sourceHeight);
		const pixels = imageData.data;

		const setMaskPixel = (x: number, y: number): void => {
			const offset = (y * sourceWidth + x) * 4;
			pixels[offset] = 0;
			pixels[offset + 1] = 0;
			pixels[offset + 2] = 0;
			pixels[offset + 3] = 120;
		};

		for (let index = 0; index < carpet.mask.length; index++) {
			if (carpet.mask[index] !== 1) continue;

			const localX = index % carpet.width;
			const localY = Math.floor(index / carpet.width);
			const pixelX = localX * 3;
			const pixelY = localY * 3;
			setMaskPixel(pixelX + 2, pixelY);
			setMaskPixel(pixelX + 1, pixelY + 1);
			setMaskPixel(pixelX, pixelY + 2);
		}

		carpetCtx.putImageData(imageData, 0, 0);
		return canvas;
	}

	private drawSelfIdentifiedCarpets(ctx: any, data: B01MapData, creator: Q10CreatorData): void {
		if (!creator.selfIdentifiedCarpets.length) return;
		const renderScale = getQ10ExportCanvasScale(data.header.sizeX, data.header.sizeY);
		for (const carpet of creator.selfIdentifiedCarpets) {
			this.drawSelfIdentifiedCarpetToExport(ctx, carpet, renderScale);
		}
	}

	private drawSelfIdentifiedCarpetToExport(
		ctx: any,
		carpet: Q10CreatorData["selfIdentifiedCarpets"][number],
		renderScale: number
	): void {
		if (carpet.width <= 0 || carpet.height <= 0 || !carpet.mask.length) return;
		const sourceCanvas = this.buildSelfIdentifiedCarpetSourceCanvas(carpet);
		if (!sourceCanvas) return;
		const destX = carpet.lt.x * renderScale;
		const destY = carpet.lt.y * renderScale;
		const destWidth = (carpet.rb.x - carpet.lt.x) * renderScale;
		const destHeight = (carpet.rb.y - carpet.lt.y) * renderScale;
		if (destWidth <= 0 || destHeight <= 0) return;

		ctx.save();
		ctx.imageSmoothingEnabled = false;
		ctx.drawImage(sourceCanvas as any, destX, destY, destWidth, destHeight);
		ctx.restore();
	}

	private buildOriginalMaterialPaths(
		data: B01MapData,
		creator: Q10CreatorData,
		roomIds: number[],
		kind: Q10OriginalMaterialKind
	): Q10MapPixelPoint[][] {
		if (!roomIds.length) return [];

		const width = data.header.sizeX;
		const height = data.header.sizeY;
		const baseGrid =
			creator.clipEraseMapGrid && creator.clipEraseMapGrid.length === width * height
				? creator.clipEraseMapGrid
				: data.mapGrid;
		const mask = buildRoomMaterialMaskGrid(baseGrid, width, height, roomIds);

		if (kind === "ceramicTile") {
			return buildCeramicTileMaterialPaths(mask, width, height, data.header.resolution);
		}
		if (kind === "horizontalFloorBoard") {
			return buildHorizontalFloorBoardMaterialPaths(mask, width, height, data.header.resolution);
		}
		return buildVerticalFloorBoardMaterialPaths(mask, width, height, data.header.resolution);
	}

	private drawRoomMaterials(
		ctx: any,
		geometry: Q10MapGeometry,
		data: B01MapData,
		creator: Q10CreatorData
	): void {
		const ceramicTilePaths = creator.materialPaths.ceramicTile.length
			? creator.materialPaths.ceramicTile
			: this.buildOriginalMaterialPaths(data, creator, creator.roomMaterialRoomIds.ceramicTile, "ceramicTile");
		const horizontalFloorBoardPaths = creator.materialPaths.horizontalFloorBoard.length
			? creator.materialPaths.horizontalFloorBoard
			: this.buildOriginalMaterialPaths(data, creator, creator.roomMaterialRoomIds.horizontalFloorBoard, "horizontalFloorBoard");
		const verticalFloorBoardPaths = creator.materialPaths.verticalFloorBoard.length
			? creator.materialPaths.verticalFloorBoard
			: this.buildOriginalMaterialPaths(data, creator, creator.roomMaterialRoomIds.verticalFloorBoard, "verticalFloorBoard");

		const materialMapRate = geometry.canvasScaleValue();

		this.drawMaterialPathGroup(ctx, ceramicTilePaths, materialMapRate);
		this.drawMaterialPathGroup(ctx, horizontalFloorBoardPaths, materialMapRate);
		this.drawMaterialPathGroup(ctx, verticalFloorBoardPaths, materialMapRate);
	}

	private drawMaterialPathGroup(ctx: any, polygons: Q10MapPixelPoint[][], mapRate: number): void {
		if (!polygons.length) return;

		ctx.save();
		ctx.strokeStyle = packedArgbToCss(419430400);
		ctx.lineWidth = 2 / Math.max(mapRate, 1);
		ctx.lineJoin = "round";
		ctx.lineCap = "round";

		for (const polygon of polygons) {
			if (polygon.length < 2) continue;
			ctx.beginPath();
			ctx.moveTo(polygon[0].x, polygon[0].y);
			for (let index = 1; index < polygon.length; index++) {
				ctx.lineTo(polygon[index].x, polygon[index].y);
			}
			ctx.stroke();
		}

		ctx.restore();
	}

	private drawThresholdArea(
		ctx: any,
		geometry: Q10MapGeometry,
		area: Q10CreatorArea
	): void {
		if (area.points.length < 4) return;

		const placement = geometry.areaPlacement(area, "map");
		const { centerX, centerY, width, height, angleRad } = placement;
		if (width <= 0 || height <= 0) return;

		let tileWidth = geometry.quarterMeterTileLengthInMap();
		let tileHeight = tileWidth;
		const image = this.assets.mapThresholdMaterial;
		const sourceWidth = imageWidth(image) || tileWidth;
		const sourceHeight = imageHeight(image) || tileHeight;
		let imageScale = tileWidth / sourceWidth;
		if (imageScale <= 0.02) {
			imageScale = 0.02;
			tileWidth = sourceWidth * imageScale;
			tileHeight = sourceHeight * imageScale;
		} else {
			tileHeight = sourceHeight * imageScale;
		}

		const columns = Math.floor(width / tileWidth) + 2;
		const rows = Math.floor(height / tileHeight) + 2;
		if (columns <= 0 || rows <= 0) return;

		ctx.save();
		fillPolygon(ctx, area.points);
		ctx.clip();
		ctx.translate(centerX, centerY);
		ctx.rotate(angleRad);

		if (!image) {
			ctx.fillStyle = packedArgbToCss(DARK_MAP_COLORS.thresholdBase, 0.82);
			ctx.fillRect(-width / 2, -height / 2, width, height);
			ctx.restore();
			return;
		}

		for (let row = 0; row < rows; row++) {
			const drawY = -height / 2 + row * tileHeight;
			const rowShift =
				(tileWidth * Q10_LAYOUT.thresholdRowShiftRatio * row) % tileWidth;
			for (let column = 0; column < columns; column++) {
				const drawX = -width / 2 + column * tileWidth - rowShift;
				ctx.drawImage(image as any, drawX, drawY, tileWidth, tileHeight);
			}
		}

		ctx.restore();
	}

	private drawAreas(
		ctx: any,
		geometry: Q10MapGeometry,
		areas: Q10CreatorArea[],
		mode: "erase" | "forbid" | "mop" | "threshold"
	): void {
		if (!areas.length) return;
		const strokeWidth = geometry.layoutLengthInMap(Q10_LAYOUT.areaStrokeWidth);
		const dashLength = geometry.layoutLengthInMap(Q10_LAYOUT.areaMopDash);
		const fillColor = packedArgbToCss(
			mode === "erase" ? DARK_MAP_COLORS.eraseFill : DARK_MAP_COLORS.forbidFill
		);
		const strokeColor = packedArgbToCss(
			mode === "erase" ? DARK_MAP_COLORS.eraseBase : DARK_MAP_COLORS.forbidLine
		);
		const dashPattern = mode === "mop" ? [dashLength, dashLength] : [];

		for (const area of areas) {
			if (area.points.length < 3) continue;

			if (mode === "threshold") {
				this.drawThresholdArea(ctx, geometry, area);
				continue;
			}

			ctx.save();
			fillPolygon(ctx, area.points);
			ctx.fillStyle = fillColor;
			ctx.fill();
			ctx.restore();

			ctx.save();
			ctx.strokeStyle = strokeColor;
			ctx.lineWidth = strokeWidth;
			ctx.lineJoin = "round";
			ctx.lineCap = "round";
			ctx.setLineDash(dashPattern);
			fillPolygon(ctx, area.points);
			ctx.stroke();
			ctx.restore();
		}
	}

	private drawAreaMaterialAtlas(
		ctx: any,
		geometry: Q10MapGeometry,
		area: Q10CreatorArea,
		image: Image | undefined,
		rowShiftRatio = 0
	): void {
		if (area.points.length < 4) return;

		const placement = geometry.areaPlacement(area, "canvas");
		const { centerX, centerY, width, height, angleRad } = placement;
		if (width <= 0 || height <= 0) return;

		let tileWidth = Math.max(1, geometry.quarterMeterTileLength());
		let tileHeight = tileWidth;
		const sourceWidth = imageWidth(image) || tileWidth;
		const sourceHeight = imageHeight(image) || tileHeight;
		let imageScale = tileWidth / sourceWidth;
		if (imageScale <= 0.02) {
			imageScale = 0.02;
			tileWidth = sourceWidth * imageScale;
			tileHeight = sourceHeight * imageScale;
		} else {
			tileHeight = sourceHeight * imageScale;
		}

		const columns = Math.floor(width / tileWidth) + 2;
		const rows = Math.floor(height / tileHeight) + 2;
		if (columns <= 0 || rows <= 0) return;

		ctx.save();
		ctx.translate(centerX, centerY);
		ctx.rotate(angleRad);
		ctx.beginPath();
		ctx.rect(-width / 2, -height / 2, width, height);
		ctx.clip();

		if (!image) {
			ctx.fillStyle = "rgba(255, 255, 255, 0.22)";
			ctx.fillRect(-width / 2, -height / 2, width, height);
			ctx.restore();
			return;
		}

		for (let row = 0; row < rows; row++) {
			const drawY = -height / 2 + row * tileHeight;
			const rowShift = rowShiftRatio > 0
				? (tileWidth * rowShiftRatio * row) % tileWidth
				: 0;
			for (let column = 0; column < columns; column++) {
				const drawX = -width / 2 + column * tileWidth - rowShift;
				ctx.drawImage(image as any, drawX, drawY, tileWidth, tileHeight);
			}
		}

		ctx.restore();
	}

	private drawManualCarpetAreas(ctx: any, geometry: Q10MapGeometry, areas: Q10CreatorArea[]): void {
		if (!areas.length) return;
		this.withOutputSpace(ctx, geometry, (outputCtx) => {
			for (const area of areas) {
				this.drawAreaMaterialAtlas(outputCtx, geometry, area, this.assets.mapCarpetMaterial);
			}
		});
	}

	private drawForbidEndpoint(
		ctx: any,
		geometry: Q10MapGeometry,
		point: Q10MapPixelPoint,
		rotationDeg: number
	): void {
		const canvasPoint = geometry.mapPoint(point);
		const endpointSize = geometry.layoutLength(Q10_LAYOUT.forbidLineIconSize);
		if (this.assets.forbidlineIcon) {
			drawCenteredAsset(
				ctx,
				this.assets.forbidlineIcon,
				canvasPoint.x,
				canvasPoint.y,
				endpointSize,
				rotationDeg
			);
			return;
		}

		ctx.save();
		ctx.translate(canvasPoint.x, canvasPoint.y);
		ctx.rotate((rotationDeg * Math.PI) / 180);
		ctx.fillStyle = packedArgbToCss(DARK_MAP_COLORS.forbidLine);
		ctx.beginPath();
		ctx.arc(0, 0, endpointSize / 2, 0, Math.PI * 2);
		ctx.fill();
		ctx.fillStyle = "rgba(255,255,255,1)";
		ctx.fillRect(
			-endpointSize * 0.28,
			-endpointSize * 0.08,
			endpointSize * 0.56,
			endpointSize * 0.16
		);
		ctx.restore();
	}

	private drawVirtualWalls(ctx: any, geometry: Q10MapGeometry, walls: Q10CreatorLine[]): void {
		if (!walls.length) return;
		const lineWidth = geometry.layoutLength(Q10_LAYOUT.areaStrokeWidth);
		ctx.save();
		ctx.lineCap = "round";
		ctx.lineJoin = "round";
		ctx.strokeStyle = packedArgbToCss(DARK_MAP_COLORS.forbidLine);
		ctx.lineWidth = lineWidth;

		for (const wall of walls) {
			const start = geometry.mapPoint(wall.points[0]);
			const end = geometry.mapPoint(wall.points[1]);
			ctx.beginPath();
			ctx.moveTo(start.x, start.y);
			ctx.lineTo(end.x, end.y);
			ctx.stroke();
			const rotationDeg = (Math.atan2(end.y - start.y, end.x - start.x) * 180) / Math.PI;
			this.drawForbidEndpoint(ctx, geometry, wall.points[0], rotationDeg);
			this.drawForbidEndpoint(ctx, geometry, wall.points[1], rotationDeg);
		}
		ctx.restore();
	}

	private historyUpdateToPathKind(update: number | undefined): number {
		if (update === 6) return 0;
		if (update === 4) return 1;
		if (update === 5) return 2;
		return 0;
	}

	private normalizeNativePathType(type: number | undefined): number {
		if (type === 0 || type === 1 || type === 2 || type === 3 || type === 4) return type;
		return 0;
	}

	private packagePathPointsLikeNative(points: Array<Q10MapPixelPoint & { type: number }>): Q10MapPixelPoint[][][] {
		const paths: Q10MapPixelPoint[][][] = [[], [], [], [], []];
		let previous: (Q10MapPixelPoint & { type: number }) | null = null;

		for (const point of points) {
			const bucket = paths[point.type] ?? paths[0]!;

			const changedType = previous?.type !== point.type;
			if (changedType) {
				const subPath: Q10MapPixelPoint[] = [];
				if (previous && previous.type !== -1) {
					subPath.push({ x: previous.x, y: previous.y });
				} else {
					subPath.push({ x: point.x, y: point.y });
				}
				subPath.push({ x: point.x, y: point.y });
				bucket.push(subPath);
			} else if (bucket.length > 0) {
				bucket[bucket.length - 1]!.push({ x: point.x, y: point.y });
			}
			previous = point;
		}

		return paths;
	}

	private hasDrawablePathSegments(segments: Q10MapPixelPoint[][]): boolean {
		return segments.some((segment) => segment.length > 1);
	}

	private createPathCanvas(
		geometry: Q10MapGeometry,
		data: B01MapData,
		points: Array<Q10MapPixelPoint & { type: number }>
	): any | null {
		if (points.length < 2) return null;

		// Draw the path directly at final PNG resolution. The previous
		// implementation rendered on the coarse map grid first and then scaled
		// the bitmap up by the canvas scale, which made the path look visibly soft.
		const { width, height } = geometry.mapCanvasSize();
		const canvas = createCanvas(width, height);
		const pathCtx = canvas.getContext("2d");
		const paths = this.packagePathPointsLikeNative(points);
		const primaryWidth = width / 375;
		const glowWidth = geometry.mapLength(0.3 / Math.max(data.header.resolution, 0.001));

		const drawPath = (
			segments: Q10MapPixelPoint[][],
			strokeStyle: string,
			lineWidth: number,
			dash?: number[],
			dashOffset = 0
		): void => {
			const drawableSegments = segments.filter((segment) => segment.length >= 2);
			if (!drawableSegments.length) return;
			pathCtx.beginPath();
			pathCtx.strokeStyle = strokeStyle;
			pathCtx.lineWidth = lineWidth;
			pathCtx.lineJoin = "round";
			pathCtx.lineCap = "round";
			pathCtx.setLineDash(dash ?? []);
			pathCtx.lineDashOffset = dashOffset;
			for (const segment of drawableSegments) {
				const start = geometry.mapPoint(segment[0]!);
				pathCtx.moveTo(start.x, start.y);
				for (let index = 1; index < segment.length; index++) {
					const point = geometry.mapPoint(segment[index]!);
					pathCtx.lineTo(point.x, point.y);
				}
			}
			pathCtx.stroke();
			pathCtx.setLineDash([]);
			pathCtx.lineDashOffset = 0;
		};

		pathCtx.clearRect(0, 0, width, height);
		pathCtx.imageSmoothingEnabled = true;

		// Path paints use Skia.Color() in the original bundle, which interprets
		// packed integers as AARRGGBB. Base-map raster colors in this file use a
		// different packing, so path colors must be decoded separately.
		const wideGlowColor = packedArgbToCss(1728053247);
		const solidWhite = packedArgbToCss(4294967295);
		const thinGlowColor = packedArgbToCss(1728053247);
		const dashedColor = packedArgbToCss(2583691263);

		const pathStyles: Array<{ segments: Q10MapPixelPoint[][]; layers: Q10PathLayerStyle[] }> = [
			{
				segments: paths[0]!,
				layers: [
					{ strokeStyle: wideGlowColor, lineWidth: glowWidth },
					{ strokeStyle: solidWhite, lineWidth: primaryWidth }
				]
			},
			{
				segments: paths[1]!,
				layers: [
					{ strokeStyle: wideGlowColor, lineWidth: glowWidth },
					{ strokeStyle: thinGlowColor, lineWidth: primaryWidth }
				]
			},
			{
				segments: paths[2]!,
				layers: [
					{ strokeStyle: solidWhite, lineWidth: primaryWidth }
				]
			},
			{
				segments: paths[3]!,
				layers: [
					{
						strokeStyle: dashedColor,
						lineWidth: primaryWidth,
						dash: [primaryWidth, primaryWidth * 3],
						dashOffset: primaryWidth * 3
					}
				]
			}
		] as const;

		for (const pathStyle of pathStyles) {
			if (!this.hasDrawablePathSegments(pathStyle.segments)) continue;
			for (const layer of pathStyle.layers) {
				drawPath(
					pathStyle.segments,
					layer.strokeStyle,
					layer.lineWidth,
					layer.dash,
					layer.dashOffset
				);
			}
		}

		return canvas;
	}

	private drawPath(ctx: any, geometry: Q10MapGeometry, data: B01MapData, creator: Q10CreatorData): void {
		const sourcePath = data.q10SourceData?.pathPoints ?? [];
		const nativePath = creator.pathPixels ?? [];
		if (!sourcePath.length && !nativePath.length && !data.history?.length) return;

		const pixelPoints = sourcePath.length
			? sourcePath.map((point) => ({
				x: data.q10SourceData!.xMin + point.x,
				y: data.q10SourceData!.yMin - point.y,
				type: this.normalizeNativePathType(point.type)
			}))
			: nativePath.length
				? nativePath.map((point: Q10CreatorPathPoint) => ({
					x: point.x,
					y: point.y,
					// Native Q10 path rendering in the original app is driven by the
					// decoded raw `type` from parserPathData/yx_getPathPointWith.
					// Do not synthesize alternate types from `update` here, otherwise
					// we may draw segments that the original leaves hidden or styles
					// differently.
					type: this.normalizeNativePathType(point.type)
				}))
				: (data.history ?? []).map((point) => ({
					x: point.x,
					y: point.y,
					type: this.historyUpdateToPathKind(point.update)
				}));

		const pathCanvas = this.createPathCanvas(geometry, data, pixelPoints);
		if (!pathCanvas) return;

		ctx.save();
		ctx.imageSmoothingEnabled = false;
		ctx.drawImage(pathCanvas as any, 0, 0);
		ctx.restore();
	}

	private drawPose(
		ctx: any,
		geometry: Q10MapGeometry,
		pose: Q10PixelPose | undefined,
		image: Image | undefined,
		drawWidth: number,
		rotationOffset = 0,
		fallback: "charger" | "robot" = "robot"
	): void {
		if (!pose) return;
		const canvasPose = geometry.mapPose(pose);
		if (!canvasPose) return;
		const targetWidth = geometry.mapLength(drawWidth);
		if (image) {
			drawCenteredAsset(ctx, image, canvasPose.x, canvasPose.y, targetWidth, (canvasPose.phi ?? 0) + rotationOffset);
			return;
		}

		const radius = targetWidth * 0.28;
		ctx.save();
		ctx.translate(canvasPose.x, canvasPose.y);
		ctx.rotate((((canvasPose.phi ?? 0) + rotationOffset) * Math.PI) / 180);
		if (fallback === "charger") {
			ctx.fillStyle = "rgba(255,255,255,0.95)";
			ctx.beginPath();
			ctx.arc(0, 0, radius, 0, Math.PI * 2);
			ctx.fill();
			ctx.strokeStyle = "rgba(20,31,48,0.9)";
			ctx.lineWidth = Math.max(1, radius * 0.22);
			ctx.beginPath();
			ctx.moveTo(-radius * 0.22, -radius * 0.55);
			ctx.lineTo(radius * 0.05, -radius * 0.1);
			ctx.lineTo(-radius * 0.02, -radius * 0.1);
			ctx.lineTo(radius * 0.22, radius * 0.55);
			ctx.lineTo(-radius * 0.05, radius * 0.08);
			ctx.lineTo(radius * 0.02, radius * 0.08);
			ctx.stroke();
		} else {
			ctx.fillStyle = "rgba(255,255,255,0.95)";
			ctx.beginPath();
			ctx.arc(0, 0, radius, 0, Math.PI * 2);
			ctx.fill();
			ctx.fillStyle = "rgba(90,120,150,0.95)";
			ctx.beginPath();
			ctx.moveTo(0, -radius * 0.7);
			ctx.lineTo(radius * 0.28, -radius * 0.1);
			ctx.lineTo(0, radius * 0.1);
			ctx.lineTo(-radius * 0.28, -radius * 0.1);
			ctx.closePath();
			ctx.fill();
		}
		ctx.restore();
	}

	private drawObstacleIcons(
		ctx: any,
		geometry: Q10MapGeometry,
		entries: Q10CreatorObstacle[],
		image: Image | undefined,
		fallbackColor: string
	): void {
		const targetWidth = geometry.imgRateLength(6);
		for (const entry of entries) {
			const point = geometry.mapPoint(entry.point);
			if (image) {
				drawCenteredSpriteWidthScaled(ctx, image, point.x, point.y, targetWidth);
				continue;
			}
			ctx.fillStyle = fallbackColor;
			ctx.beginPath();
			ctx.arc(point.x, point.y, targetWidth * 0.25, 0, Math.PI * 2);
			ctx.fill();
		}
	}

	private drawSuspectedPoints(ctx: any, geometry: Q10MapGeometry, entries: Q10CreatorSuspectedPoint[]): void {
		const targetSize = geometry.layoutLength(16);
		for (const entry of entries) {
			const point = geometry.mapPoint(entry.point);
			const image =
				entry.type === "threshold" ? this.assets.suspectedThreshold :
					entry.type === "easycard" ? this.assets.suspectedEasycard :
						this.assets.suspectedCliff;
			if (image) {
				drawCenteredCoverSquareAsset(ctx, image, point.x, point.y, targetSize);
				continue;
			}
			ctx.fillStyle = "rgba(255, 196, 0, 0.9)";
			ctx.beginPath();
			ctx.arc(point.x, point.y, targetSize * 0.25, 0, Math.PI * 2);
			ctx.fill();
		}
	}

	private drawRoomTags(ctx: any, geometry: Q10MapGeometry, creator: Q10CreatorData): void {
		if (!creator.roomModels.length) return;

		const metrics = getRenderMetrics();
		const referenceKImgRate = geometry.roomTagReferenceKImgRate();
		const exportScale = geometry.roomTagExportScale();
		const logicalFontSize = referenceKImgRate <= 3 ? 10 : 10 + 0.8 * (referenceKImgRate - 3);
		const fontSize = logicalFontSize * exportScale;
		const bubbleSize = metrics.roomBubbleDiameter * exportScale;
		const iconSize = metrics.roomIconSize * exportScale;
		const gap = metrics.roomGap * exportScale;

		ctx.font = `700 ${fontSize}px "Segoe UI", sans-serif`;

		for (const room of creator.roomModels) {
			const label = room.roomName?.trim();
			if (!label) continue;

			const bubbleColor = packedArgbToCss(DARK_MAP_COLORS.roomTagBase[room.colorID] ?? DARK_MAP_COLORS.roomTagBase[0]);
			const borderColor = packedArgbToCss(DARK_MAP_COLORS.roomTagStroke[room.colorID] ?? DARK_MAP_COLORS.roomTagStroke[0]);
			const textColor = bubbleColor;
			const icon = this.assets.roomTags.get(room.roomType) ?? this.assets.roomTags.get(0);
			const textMetrics = measureRoomText(ctx, label, fontSize);
			const paragraphWidth = textMetrics.width + exportScale;
			const paragraphHeight = textMetrics.height + exportScale;
			const totalWidth = bubbleSize + gap + paragraphWidth;
			const center = geometry.mapPoint(room.transCenterPoint);
			const centerX = center.x;
			const centerY = center.y;
			const startX = centerX - totalWidth / 2;
			const bubbleCenterX = startX + bubbleSize / 2;

			ctx.beginPath();
			ctx.arc(bubbleCenterX, centerY, bubbleSize / 2, 0, Math.PI * 2);
			ctx.fillStyle = bubbleColor;
			ctx.fill();
			ctx.strokeStyle = borderColor;
			ctx.lineWidth = Math.max(1, 0.5 * exportScale);
			ctx.stroke();

			if (icon) this.drawCenteredOpaqueAsset(ctx, icon, bubbleCenterX, centerY, iconSize);

			const textX = startX + bubbleSize + gap;
			ctx.textAlign = "left";
			ctx.textBaseline = "top";
			ctx.fillStyle = textColor;
			ctx.fillText(label, textX, centerY - paragraphHeight / 2);

			if (room.cleanOrder > 0) {
				const badgeRadius = metrics.roomBadgeRadius * exportScale;
				const badgeCenterX = startX + badgeRadius + exportScale;
				const badgeCenterY = centerY + paragraphHeight / 2 + 2 * exportScale + badgeRadius;
				ctx.beginPath();
				ctx.arc(badgeCenterX, badgeCenterY, badgeRadius, 0, Math.PI * 2);
				ctx.fillStyle = "rgba(111,111,116,0.95)";
				ctx.fill();
				ctx.fillStyle = "rgba(255,255,255,1)";
				ctx.textAlign = "center";
				ctx.textBaseline = "middle";
				ctx.font = `700 ${(room.cleanOrder < 10 ? 10 : 8) * exportScale}px "Segoe UI", sans-serif`;
				ctx.fillText(String(room.cleanOrder), badgeCenterX, badgeCenterY);
				ctx.font = `700 ${fontSize}px "Segoe UI", sans-serif`;
			}
		}
	}

	private initializeCanvas(ctx: any, width: number, height: number): void {
		ctx.imageSmoothingEnabled = false;
		ctx.clearRect(0, 0, width, height);
	}

	private drawBaseLayers(ctx: any, geometry: Q10MapGeometry, data: B01MapData, creator: Q10CreatorData): void {
		this.withMapSpace(ctx, geometry, (mapCtx) => {
			this.drawBaseMap(mapCtx, data, creator);
			this.drawRoomMaterials(mapCtx, geometry, data, creator);
		});
	}

	private drawCleanOverlayLayers(ctx: any, geometry: Q10MapGeometry, data: B01MapData, creator: Q10CreatorData): void {
		this.drawSelfIdentifiedCarpets(ctx, data, creator);
		this.withMapSpace(ctx, geometry, (mapCtx) => {
			this.drawManualCarpetAreas(mapCtx, geometry, creator.carpetAreas);
			this.drawAreas(mapCtx, geometry, creator.forbidAreas, "forbid");
			this.drawAreas(mapCtx, geometry, creator.mopAreas, "mop");
		});

		ctx.imageSmoothingEnabled = true;
		this.drawVirtualWalls(ctx, geometry, creator.virtualWalls);
		this.withMapSpace(ctx, geometry, (mapCtx) => {
			this.drawAreas(mapCtx, geometry, creator.thresholdAreas, "threshold");
		});
		this.withMapSpace(ctx, geometry, (mapCtx) => {
			this.drawAreas(mapCtx, geometry, creator.eraseAreas, "erase");
		});
	}

	private drawInteractiveOverlayLayers(ctx: any, geometry: Q10MapGeometry, creator: Q10CreatorData): void {
		this.drawPose(ctx, geometry, creator.chargerPixel, this.assets.power, 8, -90, "charger");
		this.drawPose(ctx, geometry, creator.robotPixel, this.assets.device, 8, 90, "robot");
		this.drawObstacleIcons(ctx, geometry, creator.obstaclePixels, this.assets.obstacle, "rgba(255,100,80,0.9)");
		this.drawObstacleIcons(ctx, geometry, creator.skipPixels, this.assets.tiaoGuoIcon, "rgba(255,220,60,0.92)");
		this.drawSuspectedPoints(ctx, geometry, creator.suspectedPoints);
		this.drawRoomTags(ctx, geometry, creator);
	}

	public async buildMaps(data: B01MapData, deviceStatus?: B01DeviceStatus, robotModel?: string): Promise<Q10RenderedMaps> {
		void deviceStatus;
		await this.ensureAssets(robotModel);

		const creator = data.q10CreatorData;
		if (!creator?.q10Detected) {
			throw new Error("Q10 creator data missing for Q10 builder");
		}

		const renderScale = getQ10ExportCanvasScale(data.header.sizeX, data.header.sizeY);
		const geometry = new Q10MapGeometry(data, 1, renderScale);
		const { width, height } = geometry.mapCanvasSize();
		const canvas = createCanvas(width, height);
		const ctx = canvas.getContext("2d");

		this.initializeCanvas(ctx, width, height);
		this.drawBaseLayers(ctx, geometry, data, creator);
		this.drawCleanOverlayLayers(ctx, geometry, data, creator);
		const clean = canvas.toBuffer("image/png");
		this.drawPath(ctx, geometry, data, creator);
		this.drawInteractiveOverlayLayers(ctx, geometry, creator);

		return {
			full: canvas.toBuffer("image/png"),
			clean
		};
	}

	public async buildMap(data: B01MapData, deviceStatus?: B01DeviceStatus, robotModel?: string): Promise<Buffer> {
		const rendered = await this.buildMaps(data, deviceStatus, robotModel);
		return rendered.full;
	}
}
