import type Konva from 'konva';
import type { jsPDF as JsPDFType } from 'jspdf';
import { applyPdfUnicodeFont, PDF_FONT_FAMILY } from './pdfFonts';

// ── PNG Export Options ──

export interface PngExportOptions {
  readonly pixelRatio?: number;
  readonly mimeType?: 'image/png' | 'image/jpeg';
  readonly quality?: number;
}

// ── Stage-based PNG export ──

/**
 * Export a Konva stage to a data URL.
 * Optionally hides the grid layer before exporting.
 */
export function exportKonvaStageToDataUrl(
  stage: Konva.Stage,
  options: PngExportOptions & { readonly includeGrid?: boolean } = {},
): string {
  const { pixelRatio = 2, mimeType = 'image/png', quality = 1, includeGrid = false } = options;

  // Temporarily hide grid layer if requested (only for multi-layer stages like the 2D editor)
  const layers = stage.getLayers();
  // Grid is the first layer only when there are multiple layers (2D editor).
  // Single-layer stages (projections) should never have their layer hidden.
  const gridLayer = layers.length > 1 ? layers[0] : null;
  const gridWasVisible = gridLayer?.visible() ?? false;

  if (!includeGrid && gridLayer) {
    gridLayer.visible(false);
    stage.draw();
  }

  // Force a synchronous redraw before capture
  stage.draw();

  const dataUrl = stage.toDataURL({
    pixelRatio,
    mimeType,
    quality,
    x: 0,
    y: 0,
    width: stage.width(),
    height: stage.height(),
  });

  // Restore grid visibility
  if (!includeGrid && gridLayer && gridWasVisible) {
    gridLayer.visible(true);
    stage.draw();
  }

  return dataUrl;
}

/**
 * Export a Three.js canvas element to a data URL. Defaults to PNG but
 * callers targeting a PDF should pass `'image/jpeg'` with a quality to
 * cut file size by 10-20x on photographic 3D renders.
 *
 * For JPEG (which has no alpha channel), transparent pixels in the source
 * canvas would otherwise be filled with the browser's default of black —
 * which gives the 3D export a black sky/background. We composite the
 * source onto a white offscreen canvas first to avoid that.
 */
export function exportThreeCanvasToDataUrl(
  canvas: HTMLCanvasElement,
  mimeType: 'image/png' | 'image/jpeg' = 'image/png',
  quality = 0.85,
): string {
  if (mimeType === 'image/jpeg') {
    const composite = document.createElement('canvas');
    composite.width = canvas.width;
    composite.height = canvas.height;
    const ctx = composite.getContext('2d');
    if (ctx) {
      ctx.fillStyle = '#ffffff';
      ctx.fillRect(0, 0, composite.width, composite.height);
      ctx.drawImage(canvas, 0, 0);
      return composite.toDataURL('image/jpeg', quality);
    }
    return canvas.toDataURL('image/jpeg', quality);
  }
  return canvas.toDataURL('image/png');
}

// ── Download helpers ──

/**
 * Trigger browser download of a data URL as a file.
 */
export function downloadDataUrl(dataUrl: string, filename: string): void {
  const link = document.createElement('a');
  link.download = filename;
  link.href = dataUrl;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

/**
 * Trigger browser download of a Blob.
 */
export function downloadBlob(blob: Blob, filename: string): void {
  const url = URL.createObjectURL(blob);
  downloadDataUrl(url, filename);
  URL.revokeObjectURL(url);
}

// ── PDF Export ──

export interface PdfExportOptions {
  readonly title: string;
  readonly includeGrid?: boolean;
  readonly pixelRatio?: number;
}

/**
 * Lazy-load jsPDF for code splitting.
 */
async function loadJsPDF(): Promise<typeof import('jspdf')> {
  return import('jspdf');
}

/**
 * Add an image inside a bounding box, preserving its native aspect ratio and
 * centering it inside the box. Uses jsPDF's getImageProperties to read the
 * source dimensions from the PNG data URL.
 */
function addImageFit(
  pdf: JsPDFType,
  dataUrl: string,
  boxX: number,
  boxY: number,
  boxW: number,
  boxH: number,
): void {
  if (boxW <= 0 || boxH <= 0) return;
  let drawW = boxW;
  let drawH = boxH;
  try {
    const props = pdf.getImageProperties(dataUrl);
    if (props.width > 0 && props.height > 0) {
      const aspect = props.width / props.height;
      drawW = boxW;
      drawH = boxW / aspect;
      if (drawH > boxH) {
        drawH = boxH;
        drawW = boxH * aspect;
      }
    }
  } catch {
    // Fall back to stretched-fit if properties can't be read.
  }
  const offX = boxX + (boxW - drawW) / 2;
  // Top-align so the image sits directly under the page title instead of
  // getting pushed to the vertical centre of the page (which looks like a
  // big empty strip above the image for wide walls on A4 landscape).
  const offY = boxY;
  pdf.addImage(dataUrl, 'PNG', offX, offY, drawW, drawH);
}

/**
 * Create a single-room PDF with:
 * - Page 1: 3D top-down view (default camera) filling the page
 * - Page 2: 2D floor plan (default view)
 * - One page per wall projection (default view)
 *
 * Images are embedded with their native aspect ratio preserved.
 */
export async function createRoomPdf(
  roomName: string,
  topDownDataUrl: string | null,
  projectionDataUrls: readonly { readonly label: string; readonly dataUrl: string }[],
  view3dDataUrl: string | null,
): Promise<JsPDFType> {
  const { jsPDF } = await loadJsPDF();
  // `compress: true` enables Flate compression for all streams (including
  // raster images), which is the single biggest file-size win for PDFs
  // dominated by Konva PNG exports.
  const pdf = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4', compress: true });
  await applyPdfUnicodeFont(pdf);
  const pageWidth = pdf.internal.pageSize.getWidth();
  const pageHeight = pdf.internal.pageSize.getHeight();

  const MARGIN = 12;
  const TITLE_Y = 12;
  const IMAGE_TOP = 18;

  let pageIndex = 0;

  const addTitledPage = (title: string, fontSize: number): void => {
    if (pageIndex > 0) pdf.addPage('a4', 'landscape');
    pageIndex += 1;
    pdf.setFont(PDF_FONT_FAMILY, 'normal');
    pdf.setFontSize(fontSize);
    pdf.text(title, pageWidth / 2, TITLE_Y, { align: 'center' });
  };

  // ── Page 1: 3D top view (JPEG — far smaller than PNG for photographic 3D) ──
  if (view3dDataUrl) {
    addTitledPage(roomName, 18);
    const boxH = pageHeight - IMAGE_TOP - MARGIN;
    const boxW = pageWidth - MARGIN * 2;
    addImageFit(pdf, view3dDataUrl, MARGIN, IMAGE_TOP, boxW, boxH);
  }

  // ── Page 2: 2D floor plan ──
  if (topDownDataUrl) {
    addTitledPage(`${roomName} — 2D`, 16);
    const boxH = pageHeight - IMAGE_TOP - MARGIN;
    const boxW = pageWidth - MARGIN * 2;
    addImageFit(pdf, topDownDataUrl, MARGIN, IMAGE_TOP, boxW, boxH);
  }

  // ── One page per wall projection ──
  for (const proj of projectionDataUrls) {
    addTitledPage(`${roomName} — ${proj.label}`, 16);
    const boxH = pageHeight - IMAGE_TOP - MARGIN;
    const boxW = pageWidth - MARGIN * 2;
    addImageFit(pdf, proj.dataUrl, MARGIN, IMAGE_TOP, boxW, boxH);
  }

  // Fallback: if nothing at all was captured, emit a placeholder first page.
  if (pageIndex === 0) {
    pdf.setFont(PDF_FONT_FAMILY, 'normal');
    pdf.setFontSize(18);
    pdf.text(roomName, pageWidth / 2, TITLE_Y, { align: 'center' });
  }

  return pdf;
}

/**
 * Create a multi-room apartment PDF with a cover page.
 */
export async function createApartmentPdf(
  apartmentName: string,
  rooms: readonly {
    readonly roomName: string;
    readonly topDownDataUrl: string | null;
    readonly projectionDataUrls: readonly { readonly label: string; readonly dataUrl: string }[];
  }[],
): Promise<JsPDFType> {
  const { jsPDF } = await loadJsPDF();
  const pdf = new jsPDF({ orientation: 'landscape', unit: 'mm', format: 'a4' });
  await applyPdfUnicodeFont(pdf);
  const pageWidth = pdf.internal.pageSize.getWidth();
  const pageHeight = pdf.internal.pageSize.getHeight();

  const MARGIN = 12;
  const TITLE_Y = 12;
  const IMAGE_TOP = 18;
  const boxW = pageWidth - MARGIN * 2;
  const boxH = pageHeight - IMAGE_TOP - MARGIN;

  // ── Cover page ──
  pdf.setFont(PDF_FONT_FAMILY, 'normal');
  pdf.setFontSize(28);
  pdf.text(apartmentName, pageWidth / 2, pageHeight / 3, { align: 'center' });
  pdf.setFontSize(14);
  pdf.text('House Plan', pageWidth / 2, pageHeight / 3 + 12, { align: 'center' });
  pdf.setFontSize(10);
  pdf.text(`Generated: ${new Date().toLocaleDateString()}`, pageWidth / 2, pageHeight / 3 + 22, { align: 'center' });
  pdf.setFontSize(11);
  pdf.text(
    `${rooms.length} room${rooms.length === 1 ? '' : 's'}`,
    pageWidth / 2,
    pageHeight / 3 + 30,
    { align: 'center' },
  );

  const addTitledPage = (title: string, fontSize: number): void => {
    pdf.addPage('a4', 'landscape');
    pdf.setFont(PDF_FONT_FAMILY, 'normal');
    pdf.setFontSize(fontSize);
    pdf.text(title, pageWidth / 2, TITLE_Y, { align: 'center' });
  };

  // ── Room pages ──
  for (const room of rooms) {
    if (room.topDownDataUrl) {
      addTitledPage(`${room.roomName} — 2D`, 16);
      addImageFit(pdf, room.topDownDataUrl, MARGIN, IMAGE_TOP, boxW, boxH);
    }

    for (const proj of room.projectionDataUrls) {
      addTitledPage(`${room.roomName} — ${proj.label}`, 16);
      addImageFit(pdf, proj.dataUrl, MARGIN, IMAGE_TOP, boxW, boxH);
    }
  }

  return pdf;
}

/**
 * Sanitize a filename for download.
 */
export function sanitizeFilename(name: string): string {
  return name.replace(/[^a-zA-Z0-9_\-\s]/g, '').replace(/\s+/g, '_');
}

// ── Default-view helpers ──

export interface Point2D {
  readonly x: number;
  readonly y: number;
}

/**
 * Compute fit-to-room zoom and pan for a given canvas size and room shape,
 * mirroring the auto-fit logic used by the 2D editor (RoomEditorLayout).
 */
export function computeFitView(
  shape: readonly Point2D[],
  canvasWidth: number,
  canvasHeight: number,
  padding = 80,
): { readonly zoom: number; readonly panX: number; readonly panY: number } | null {
  if (shape.length === 0) return null;
  if (canvasWidth <= 0 || canvasHeight <= 0) return null;
  let minX = Infinity;
  let minY = Infinity;
  let maxX = -Infinity;
  let maxY = -Infinity;
  for (const p of shape) {
    if (p.x < minX) minX = p.x;
    if (p.x > maxX) maxX = p.x;
    if (p.y < minY) minY = p.y;
    if (p.y > maxY) maxY = p.y;
  }
  const roomW = maxX - minX;
  const roomH = maxY - minY;
  if (roomW <= 0 || roomH <= 0) return null;
  const scaleX = (canvasWidth - padding * 2) / roomW;
  const scaleY = (canvasHeight - padding * 2) / roomH;
  const zoom = Math.min(scaleX, scaleY, 300);
  const cx = (minX + maxX) / 2;
  const cy = (minY + maxY) / 2;
  return {
    zoom,
    panX: canvasWidth / 2 - cx * zoom,
    panY: canvasHeight / 2 - cy * zoom,
  };
}

/** Await N animation frames. */
export function waitFrames(n = 2): Promise<void> {
  return new Promise((resolve) => {
    let count = 0;
    const tick = (): void => {
      count += 1;
      if (count >= n) {
        resolve();
      } else {
        requestAnimationFrame(tick);
      }
    };
    requestAnimationFrame(tick);
  });
}
