import { PDFDocument } from '@cantoo/pdf-lib';
import { registerFeature } from '../registry';
import { featureModal, footerButtons, deliver } from '../common';
import { bufferOf, readBytes, formatBytes } from '../../core/util';
import { toast } from '../../ui/toast';
import { dropzone } from '../../ui/dropzone';
import { field, select, segmented, numberInput } from '../../ui/controls';
import { icon } from '../../ui/icons';
import { h } from '../../core/dom';

interface ImageFile {
  name: string;
  bytes: Uint8Array;
  type: string;
}

type PageSize = 'fit' | 'a4' | 'letter';
type Orientation = 'auto' | 'portrait' | 'landscape';

const FIXED_SIZES: Record<Exclude<PageSize, 'fit'>, { w: number; h: number }> = {
  a4: { w: 595, h: 842 },
  letter: { w: 612, h: 792 },
};

/** Decode an image to a canvas and re-encode as JPEG (opaque) or PNG (has alpha). */
async function reencode(
  file: ImageFile,
): Promise<{ data: Uint8Array; format: 'jpg' | 'png'; width: number; height: number }> {
  const bmp = await createImageBitmap(new Blob([bufferOf(file.bytes)], { type: file.type || 'image/*' }));
  const width = bmp.width;
  const height = bmp.height;
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const cx = canvas.getContext('2d');
  if (!cx) throw new Error('Canvas is unavailable.');
  cx.drawImage(bmp, 0, 0);
  bmp.close();

  // Detect whether the image actually uses transparency.
  let hasAlpha = false;
  try {
    const px = cx.getImageData(0, 0, width, height).data;
    for (let i = 3; i < px.length; i += 4) {
      if (px[i] < 255) { hasAlpha = true; break; }
    }
  } catch {
    // getImageData can throw on very large canvases; fall back to PNG to be safe.
    hasAlpha = true;
  }

  const format: 'jpg' | 'png' = hasAlpha ? 'png' : 'jpg';
  const blob = await new Promise<Blob | null>((resolve) =>
    canvas.toBlob(resolve, format === 'jpg' ? 'image/jpeg' : 'image/png', 0.92),
  );
  if (!blob) throw new Error('Could not encode the image.');
  const data = await readBytes(blob);
  return { data, format, width, height };
}

registerFeature({
  id: 'image-to-pdf',
  title: 'Images to PDF',
  desc: 'Turn JPG, PNG, GIF, WebP or BMP images into a PDF.',
  icon: 'imageToPdf',
  category: 'convert-in',
  run() {
    const images: ImageFile[] = [];
    let pageSize: PageSize = 'fit';
    let orientation: Orientation = 'auto';
    let margin = 0;

    const m = featureModal('Images to PDF', 'Build a PDF from your images — nothing is uploaded.', 'imageToPdf', 560);

    const count = h('div.hint');
    const list = h('div.source-list');

    const renderList = () => {
      list.replaceChildren();
      images.forEach((f, i) => {
        list.appendChild(
          h('div.source-file',
            h('span.source-file-icon', icon('image', 16)),
            h('span.source-file-name', f.name),
            h('span.source-file-size', formatBytes(f.bytes.length)),
            h('button.source-file-x', {
              title: 'Remove',
              onclick: () => { images.splice(i, 1); renderList(); },
            }, '×'),
          ),
        );
      });
      count.textContent = images.length
        ? `${images.length} image${images.length === 1 ? '' : 's'} — one page each, in the order shown.`
        : 'Add one or more images to begin.';
      primary.disabled = images.length === 0;
    };

    const dz = dropzone({
      accept: 'image/*',
      multiple: true,
      compact: true,
      label: 'Add images',
      hint: 'JPG, PNG, GIF, WebP or BMP',
      onFiles: async (fs) => {
        for (const f of fs) {
          if (f.type && !f.type.startsWith('image/')) { toast(`${f.name} isn’t an image.`, { type: 'error' }); continue; }
          const bytes = await readBytes(f);
          images.push({ name: f.name, bytes, type: f.type });
        }
        renderList();
      },
    });

    // Options: orientation only applies to a fixed page size.
    const orientationField = field('Orientation', segmented({
      options: [
        { value: 'auto', label: 'Auto' },
        { value: 'portrait', label: 'Portrait' },
        { value: 'landscape', label: 'Landscape' },
      ],
      value: orientation,
      onchange: (v) => { orientation = v as Orientation; },
    }));
    const syncOrientationVisibility = () => {
      orientationField.style.display = pageSize === 'fit' ? 'none' : '';
    };

    const sizeField = field('Page size', select({
      options: [
        { value: 'fit', label: 'Fit to image' },
        { value: 'a4', label: 'A4 (595 × 842)' },
        { value: 'letter', label: 'US Letter (612 × 792)' },
      ],
      value: pageSize,
      onchange: (v) => { pageSize = v as PageSize; syncOrientationVisibility(); },
    }), 'Fit to image makes each page match its image exactly.');

    const marginField = field('Margin (points)', numberInput({
      value: margin,
      min: 0,
      max: 200,
      step: 1,
      onchange: (v) => { margin = Number.isFinite(v) ? Math.max(0, v) : 0; },
    }), 'Ignored when fitting to the image.');

    m.body.append(
      dz,
      list,
      count,
      h('div.field-grid', sizeField, orientationField, marginField),
    );
    syncOrientationVisibility();

    const { primary } = footerButtons(m, 'Build PDF', async () => {
      if (!images.length) return;
      const t = toast('Building PDF…', { type: 'progress' });
      m.setBusy(true, 'Building PDF…');
      try {
        const doc = await PDFDocument.create();
        let placed = 0;

        for (let i = 0; i < images.length; i++) {
          const f = images[i];
          t.update(`Adding image ${i + 1} of ${images.length}…`);
          t.progress(i / images.length);

          let enc: Awaited<ReturnType<typeof reencode>>;
          try {
            enc = await reencode(f);
          } catch (err) {
            console.error(err);
            toast(`Skipped ${f.name} — couldn’t decode it.`, { type: 'error' });
            continue;
          }

          const embedded = enc.format === 'jpg'
            ? await doc.embedJpg(bufferOf(enc.data))
            : await doc.embedPng(bufferOf(enc.data));

          if (pageSize === 'fit') {
            // Page matches image pixels treated as points.
            const page = doc.addPage([enc.width, enc.height]);
            page.drawImage(embedded, { x: 0, y: 0, width: enc.width, height: enc.height });
          } else {
            const base = FIXED_SIZES[pageSize];
            let pw = base.w;
            let ph = base.h;
            if (orientation === 'landscape' || (orientation === 'auto' && enc.width > enc.height)) {
              pw = base.h;
              ph = base.w;
            }
            const page = doc.addPage([pw, ph]);
            const availW = Math.max(1, pw - margin * 2);
            const availH = Math.max(1, ph - margin * 2);
            const scale = Math.min(availW / enc.width, availH / enc.height);
            const drawW = enc.width * scale;
            const drawH = enc.height * scale;
            page.drawImage(embedded, {
              x: (pw - drawW) / 2,
              y: (ph - drawH) / 2,
              width: drawW,
              height: drawH,
            });
          }
          placed++;
        }

        if (placed === 0) {
          m.setBusy(false);
          t.close();
          toast('No images could be added.', { type: 'error' });
          return;
        }

        t.progress(1);
        const bytes = await doc.save({ useObjectStreams: true });
        t.close();
        m.close();
        deliver(bytes, 'images.pdf');
      } catch (e) {
        m.setBusy(false);
        t.close();
        toast('Couldn’t build the PDF.', { type: 'error' });
        console.error(e);
      }
    }, 'imageToPdf');

    renderList();
  },
});
