(function(root, factory) {
  const api = factory();
  if (typeof module === 'object' && module.exports) module.exports = api;
  else root.issueEvidenceReview = api;
})(typeof self !== 'undefined' ? self : this, function() {
  'use strict';

  const MAX_BYTES = 2 * 1024 * 1024;
  const MAX_PIXELS = 12 * 1024 * 1024;
  const MAX_DIMENSION = 8192;

  function namedBlob(blob, name) {
    if (typeof File === 'function') return new File([blob], name, { type: blob.type });
    Object.defineProperty(blob, 'name', { value: name, configurable: true });
    return blob;
  }

  async function optimizeImage(file, environment = {}) {
    const decode = environment.createImageBitmap || globalThis.createImageBitmap;
    const makeCanvas = environment.createCanvas || (() => document.createElement('canvas'));
    if (typeof decode !== 'function') throw new Error('This browser cannot safely inspect the screenshot. Try cropping it and choose it again.');
    let bitmap;
    try {
      bitmap = await decode(file);
      const width = Number(bitmap.width);
      const height = Number(bitmap.height);
      if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) {
        throw new Error('decode');
      }
      const pixelScale = Math.min(1, Math.sqrt(MAX_PIXELS / (width * height)));
      const dimensionScale = Math.min(1, MAX_DIMENSION / Math.max(width, height));
      const byteScale = file.size > MAX_BYTES ? Math.sqrt(MAX_BYTES / file.size) * 0.92 : 1;
      let scale = Math.min(pixelScale, dimensionScale, byteScale);
      const canvas = makeCanvas();
      const context = canvas && canvas.getContext && canvas.getContext('2d');
      if (!context) throw new Error('decode');
      for (let attempt = 0; attempt < 10; attempt += 1) {
        canvas.width = Math.max(1, Math.floor(width * scale));
        canvas.height = Math.max(1, Math.floor(height * scale));
        context.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
        const blob = await new Promise(resolve => canvas.toBlob(resolve, file.type, file.type === 'image/png' ? undefined : 0.86));
        if (!blob) throw new Error('encode');
        if (blob.size > 0 && blob.size <= MAX_BYTES) return namedBlob(blob, file.name);
        scale *= 0.8;
      }
    } catch (_error) {
      throw new Error('The screenshot could not be optimized. Try cropping it and choose it again.');
    } finally {
      if (bitmap && typeof bitmap.close === 'function') bitmap.close();
    }
    throw new Error('The screenshot could not be optimized below 2 MB. Try cropping it and choose it again.');
  }

  function create(options) {
    const controller = options.controller;
    const documentRef = options.document || document;
    let activeIndex = 0;
    let busy = false;
    let previewUrl = '';
    let thumbnailUrls = [];

    function revoke(url) {
      if (url && !url.startsWith('data:')) options.revokeObjectURL(url);
    }

    function attachmentUrl(attachment) {
      return attachment?.blob ? options.createObjectURL(attachment.blob) :
        'data:' + attachment.contentType + ';base64,' + attachment.data;
    }

    function count() {
      const value = controller.state();
      return Array.isArray(value) ? value.length : (value ? 1 : 0);
    }

    function setBusy(value) {
      busy = Boolean(value);
      options.earlier.disabled = busy || activeIndex <= 0;
      options.later.disabled = busy || activeIndex >= count() - 1;
      if (options.note) options.note.disabled = busy;
      if (options.edit) options.edit.disabled = busy || !count();
      Array.from(options.tray.children || []).forEach(button => { button.disabled = busy; });
    }

    function updateNote(values) {
      if (!options.note) return;
      options.note.value = controller.note(activeIndex);
      options.note.disabled = busy;
      if (options.noteLabel) {
        options.noteLabel.textContent = 'Evidence note for screenshot ' + (activeIndex + 1) + ' of ' +
          values.length + ' (optional)';
      }
    }

    function update(values, optimized = false) {
      if (!values.length) return;
      activeIndex = Math.max(0, Math.min(activeIndex, values.length - 1));
      revoke(previewUrl);
      previewUrl = attachmentUrl(values[activeIndex]);
      options.image.src = previewUrl;
      const current = values[activeIndex];
      const size = current.blob ? current.blob.size : Math.max(1, Math.floor(String(current.data || '').length * 3 / 4));
      options.meta.textContent = 'Screenshot ' + (activeIndex + 1) + ' of ' + values.length + ' · ' +
        current.filename + ' · ' + Math.ceil(size / 1024) + ' KB';
      options.preview.hidden = false;
      if (options.edit) options.edit.disabled = busy;
      Array.from(options.tray.children || []).forEach((button, index) =>
        button.setAttribute('aria-pressed', index === activeIndex ? 'true' : 'false'));
      options.earlier.disabled = busy || activeIndex === 0;
      options.later.disabled = busy || activeIndex === values.length - 1;
      updateNote(values);
      options.status.textContent = optimized ? 'Screenshots optimized and ready to file in this order.' :
        values.length + ' screenshots ready to file in this order.';
    }

    function render(value, index = activeIndex, optimized = false) {
      const values = (Array.isArray(value) ? value : [value]).filter(Boolean);
      activeIndex = Math.max(0, Math.min(index, values.length - 1));
      thumbnailUrls.forEach(revoke);
      thumbnailUrls = [];
      options.tray.replaceChildren(...values.map((item, itemIndex) => {
        const button = documentRef.createElement('button');
        button.type = 'button';
        button.className = 'issue-evidence-thumbnail';
        button.setAttribute('aria-label', 'Review screenshot ' + (itemIndex + 1) + ' of ' + values.length + ': ' + item.filename);
        const thumbnail = documentRef.createElement('img');
        thumbnail.alt = '';
        thumbnail.src = attachmentUrl(item);
        thumbnailUrls.push(thumbnail.src);
        button.appendChild(thumbnail);
        const position = documentRef.createElement('span');
        position.textContent = String(itemIndex + 1);
        button.appendChild(position);
        button.addEventListener('click', () => { activeIndex = itemIndex; update(values); });
        return button;
      }));
      options.tray.hidden = !values.length;
      if (values.length) update(values, optimized);
    }

    function clear() {
      revoke(previewUrl);
      thumbnailUrls.forEach(revoke);
      previewUrl = '';
      thumbnailUrls = [];
      activeIndex = 0;
      options.tray.replaceChildren();
      options.tray.hidden = true;
      options.earlier.disabled = true;
      options.later.disabled = true;
      if (options.note) {
        options.note.value = '';
        options.note.disabled = true;
      }
      if (options.edit) options.edit.disabled = true;
      if (options.noteLabel) options.noteLabel.textContent = 'Evidence note (optional)';
    }

    function afterRemoval(removedIndex, previousCount) {
      activeIndex = Math.min(removedIndex, previousCount - 2);
    }

    function move(offset) {
      const total = count();
      const destination = activeIndex + offset;
      if (destination < 0 || destination >= total) return Promise.resolve();
      controller.move(activeIndex, destination);
      activeIndex = destination;
      return controller.serialize().then(value => {
        render(value, activeIndex);
        options.status.textContent = 'Screenshot moved to position ' + (activeIndex + 1) + ' of ' + total + '.';
      });
    }

    options.earlier.addEventListener('click', () => move(-1));
    options.later.addEventListener('click', () => move(1));
    options.note?.addEventListener('input', event => {
      controller.setNote(activeIndex, event.target.value);
    });
    clear();
    return { activeIndex: () => activeIndex, afterRemoval, clear, render, setBusy };
  }

  return { create, optimizeImage, MAX_PIXELS };
});
