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

  function boundedRect(value, bounds) {
    const x = Math.max(bounds.x, Math.min(bounds.x + bounds.width - 1, Math.round(Number(value.x) || 0)));
    const y = Math.max(bounds.y, Math.min(bounds.y + bounds.height - 1, Math.round(Number(value.y) || 0)));
    const width = Math.max(1, Math.min(bounds.x + bounds.width - x, Math.round(Number(value.width) || 0)));
    const height = Math.max(1, Math.min(bounds.y + bounds.height - y, Math.round(Number(value.height) || 0)));
    return { x, y, width, height };
  }

  function boundedPoint(value, bounds) {
    return {
      x: Math.max(bounds.x, Math.min(bounds.x + bounds.width, Math.round(Number(value.x) || 0))),
      y: Math.max(bounds.y, Math.min(bounds.y + bounds.height, Math.round(Number(value.y) || 0))),
    };
  }

  function createModel(size) {
    const full = { x:0, y:0, width:Math.max(1, Math.round(size.width)), height:Math.max(1, Math.round(size.height)) };
    let crop = { ...full };
    let redactions = [];
    let annotations = [];
    let history = [];

    function remember() {
      history.push({
        crop:{...crop}, redactions:redactions.map(value => ({...value})),
        annotations:annotations.map(value => ({...value})),
      });
    }
    function intersects(annotation, bounds) {
      const left = annotation.type === 'arrow' ? Math.min(annotation.startX, annotation.endX) : annotation.x;
      const top = annotation.type === 'arrow' ? Math.min(annotation.startY, annotation.endY) : annotation.y;
      const right = annotation.type === 'arrow' ? Math.max(annotation.startX, annotation.endX) : annotation.x + annotation.width;
      const bottom = annotation.type === 'arrow' ? Math.max(annotation.startY, annotation.endY) : annotation.y + annotation.height;
      return left < bounds.x + bounds.width && top < bounds.y + bounds.height && right > bounds.x && bottom > bounds.y;
    }
    function clampAnnotation(annotation) {
      if (annotation.type === 'highlight') {
        const x = Math.max(crop.x, annotation.x);
        const y = Math.max(crop.y, annotation.y);
        const right = Math.min(crop.x + crop.width, annotation.x + annotation.width);
        const bottom = Math.min(crop.y + crop.height, annotation.y + annotation.height);
        return { type:'highlight', x, y, width:Math.max(1, right - x), height:Math.max(1, bottom - y) };
      }
      const start = boundedPoint({x:annotation.startX, y:annotation.startY}, crop);
      const end = boundedPoint({x:annotation.endX, y:annotation.endY}, crop);
      return { type:'arrow', startX:start.x, startY:start.y, endX:end.x, endY:end.y };
    }
    function setCrop(value) {
      remember();
      crop = boundedRect(value, crop);
      redactions = redactions.filter(rectangle =>
        rectangle.x < crop.x + crop.width && rectangle.y < crop.y + crop.height &&
        rectangle.x + rectangle.width > crop.x && rectangle.y + rectangle.height > crop.y
      ).map(rectangle => boundedRect(rectangle, crop));
      annotations = annotations.filter(annotation => intersects(annotation, crop)).map(clampAnnotation);
      return snapshot();
    }
    function addRedaction(value) {
      remember();
      redactions.push(boundedRect(value, crop));
      return snapshot();
    }
    function addHighlight(value) {
      remember();
      annotations.push({ type:'highlight', ...boundedRect(value, crop) });
      return snapshot();
    }
    function addArrow(value) {
      remember();
      const start = boundedPoint({x:value.startX, y:value.startY}, crop);
      const end = boundedPoint({x:value.endX, y:value.endY}, crop);
      annotations.push({ type:'arrow', startX:start.x, startY:start.y, endX:end.x, endY:end.y });
      return snapshot();
    }
    function undo() {
      const previous = history.pop();
      if (previous) {
        crop = previous.crop;
        redactions = previous.redactions;
        annotations = previous.annotations;
      }
      return snapshot();
    }
    function reset() {
      remember();
      crop = { ...full };
      redactions = [];
      annotations = [];
      return snapshot();
    }
    function snapshot() {
      return {
        crop:{...crop}, redactions:redactions.map(value => ({...value})),
        annotations:annotations.map(value => ({...value})),
      };
    }
    return { addArrow, addHighlight, addRedaction, reset, setCrop, snapshot, undo };
  }

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

  function paint(options) {
    const state = options.model.snapshot();
    const maxDimension = Number.isFinite(options.maxDimension) ? options.maxDimension : Infinity;
    const scale = Math.min(1, maxDimension / Math.max(state.crop.width, state.crop.height));
    const width = Math.max(1, Math.round(state.crop.width * scale));
    const height = Math.max(1, Math.round(state.crop.height * scale));
    options.canvas.width = width;
    options.canvas.height = height;
    const context = options.canvas.getContext('2d');
    if (!context) throw new Error('Screenshot editing is unavailable in this browser.');
    context.drawImage(options.source, state.crop.x, state.crop.y, state.crop.width, state.crop.height, 0, 0, width, height);
    context.fillStyle = '#000000';
    state.redactions.forEach(rectangle => context.fillRect(
      Math.round((rectangle.x - state.crop.x) * scale),
      Math.round((rectangle.y - state.crop.y) * scale),
      Math.round(rectangle.width * scale),
      Math.round(rectangle.height * scale),
    ));
    state.annotations.forEach(annotation => {
      if (annotation.type === 'highlight') {
        context.fillStyle = 'rgba(250, 204, 21, 0.38)';
        context.fillRect(
          Math.round((annotation.x - state.crop.x) * scale),
          Math.round((annotation.y - state.crop.y) * scale),
          Math.round(annotation.width * scale),
          Math.round(annotation.height * scale),
        );
        return;
      }
      const startX = (annotation.startX - state.crop.x) * scale;
      const startY = (annotation.startY - state.crop.y) * scale;
      const endX = (annotation.endX - state.crop.x) * scale;
      const endY = (annotation.endY - state.crop.y) * scale;
      const angle = Math.atan2(endY - startY, endX - startX);
      const head = Math.max(10, Math.min(24, 16 * scale));
      context.save();
      context.strokeStyle = '#facc15';
      context.lineWidth = Math.max(4, 6 * scale);
      context.lineCap = 'round';
      context.lineJoin = 'round';
      context.beginPath();
      context.moveTo(Math.round(startX), Math.round(startY));
      context.lineTo(Math.round(endX), Math.round(endY));
      context.lineTo(endX - head * Math.cos(angle - Math.PI / 6), endY - head * Math.sin(angle - Math.PI / 6));
      context.moveTo(Math.round(endX), Math.round(endY));
      context.lineTo(endX - head * Math.cos(angle + Math.PI / 6), endY - head * Math.sin(angle + Math.PI / 6));
      context.stroke();
      context.restore();
    });
    return state;
  }

  async function flatten(options) {
    paint(options);
    const type = ['image/png', 'image/jpeg', 'image/webp'].includes(options.type) ? options.type : 'image/png';
    const blob = await new Promise(resolve => options.canvas.toBlob(resolve, type, type === 'image/png' ? undefined : 0.9));
    if (!blob || !blob.size) throw new Error('The edited screenshot could not be saved. Try again.');
    return namedBlob(blob, options.name || 'edited-screenshot.png');
  }

  function attachmentBlob(value) {
    if (value.blob) return value.blob;
    const binary = atob(String(value.data || ''));
    return new Blob([Uint8Array.from(binary, character => character.charCodeAt(0))], {type:value.contentType});
  }

  function mount(options) {
    let source = null;
    let model = null;
    let mode = 'redact';
    let start = null;
    let selectedIndex = -1;
    let previousFocus = null;

    function status(message) { options.status.textContent = message; }
    function setMode(value) {
      mode = value;
      options.crop.setAttribute('aria-pressed', value === 'crop' ? 'true' : 'false');
      options.redact.setAttribute('aria-pressed', value === 'redact' ? 'true' : 'false');
      options.highlight.setAttribute('aria-pressed', value === 'highlight' ? 'true' : 'false');
      options.arrow.setAttribute('aria-pressed', value === 'arrow' ? 'true' : 'false');
      status({
        crop:'Drag around the part to keep.', redact:'Drag over private information to hide it.',
        highlight:'Drag around the detail to highlight.', arrow:'Drag toward the detail you want to point out.',
      }[value]);
    }
    function render() {
      if (source && model) paint({source, model, canvas:options.canvas, maxDimension:1600});
    }
    function close() {
      if (!source && !model) return;
      options.dialog.hidden = true;
      if (source && typeof source.close === 'function') source.close();
      source = null;
      model = null;
      start = null;
      (previousFocus || options.edit).focus();
    }
    async function open() {
      const value = await options.controller.serialize();
      const values = (Array.isArray(value) ? value : [value]).filter(Boolean);
      selectedIndex = options.getActiveIndex();
      const selected = values[selectedIndex];
      if (!selected) return;
      previousFocus = options.document.activeElement;
      status('Opening screenshot editor…');
      try {
        const decode = options.decode || globalThis.createImageBitmap;
        if (typeof decode !== 'function') throw new Error('decode');
        source = await decode(attachmentBlob(selected));
        model = createModel({width:source.width, height:source.height});
        options.dialog.hidden = false;
        render();
        setMode('redact');
        options.redact.focus();
      } catch (_error) {
        source = null;
        status('This browser cannot edit the screenshot. Your original is unchanged.');
      }
    }
    function point(event) {
      const box = options.canvas.getBoundingClientRect();
      const state = model.snapshot();
      return {
        x:state.crop.x + Math.round((event.clientX - box.left) * state.crop.width / box.width),
        y:state.crop.y + Math.round((event.clientY - box.top) * state.crop.height / box.height),
      };
    }
    options.canvas.addEventListener('pointerdown', event => {
      if (!model) return;
      start = point(event);
      options.canvas.setPointerCapture?.(event.pointerId);
    });
    options.canvas.addEventListener('pointerup', event => {
      if (!model || !start) return;
      const end = point(event);
      const dragStart = start;
      const rectangle = {
        x:Math.min(dragStart.x, end.x), y:Math.min(dragStart.y, end.y),
        width:Math.abs(end.x - dragStart.x), height:Math.abs(end.y - dragStart.y),
      };
      start = null;
      const tooSmall = mode === 'arrow' ? Math.hypot(end.x - dragStart.x, end.y - dragStart.y) < 8 :
        rectangle.width < 3 || rectangle.height < 3;
      if (tooSmall) {
        status('Drag a larger area on the screenshot.');
        return;
      }
      if (mode === 'crop') model.setCrop(rectangle);
      else if (mode === 'redact') model.addRedaction(rectangle);
      else if (mode === 'highlight') model.addHighlight(rectangle);
      else model.addArrow({startX:dragStart.x, startY:dragStart.y, endX:end.x, endY:end.y});
      render();
      status({
        crop:'Crop applied. Undo or reset if needed.', redact:'Private area hidden with an opaque redaction.',
        highlight:'Highlight added.', arrow:'Arrow added.',
      }[mode]);
    });
    options.edit.addEventListener('click', open);
    options.crop.addEventListener('click', () => setMode('crop'));
    options.redact.addEventListener('click', () => setMode('redact'));
    options.highlight.addEventListener('click', () => setMode('highlight'));
    options.arrow.addEventListener('click', () => setMode('arrow'));
    options.undo.addEventListener('click', () => { if (model) { model.undo(); render(); status('Last edit undone.'); } });
    options.reset.addEventListener('click', () => { if (model) { model.reset(); render(); status('Crop, redactions, and annotations reset.'); } });
    options.cancel.addEventListener('click', close);
    options.dialog.addEventListener('keydown', event => { if (event.key === 'Escape') { event.preventDefault(); close(); } });
    options.apply.addEventListener('click', async () => {
      if (!model || !source) return;
      options.apply.disabled = true;
      status('Applying flattened edit…');
      try {
        const current = await options.controller.serialize();
        const values = Array.isArray(current) ? current : [current];
        const selected = values[selectedIndex];
        let derivative = await flatten({
          source, model, canvas:options.exportCanvas, maxDimension:2048,
          name:selected.filename, type:selected.contentType,
        });
        if (options.optimizeImage) derivative = await options.optimizeImage(derivative);
        options.controller.replace(selectedIndex, derivative);
        await options.onApplied(selectedIndex);
        close();
      } catch (error) {
        status(error.message || 'The edited screenshot could not be applied. Your original is unchanged.');
      } finally {
        options.apply.disabled = false;
      }
    });
    return { close, open };
  }

  return { createModel, flatten, mount, paint };
});
