import {PDFViewerApplication} from '../pdfjs-legacy/js/viewer.mjs';
import UUID from './uuid.js';

let zoomScaleValue = 1.0;

let activeOverlay;
let drawingLayer = null;

const doNothing = () => {};

function addRedactedPagePreview(pagesSelector) {
  document.querySelectorAll(pagesSelector).forEach((page) => {
    let textLayer = page.querySelector('.textLayer');
    if (textLayer) textLayer.classList.add('redacted-page-preview');
  });
}

function addRedactedThumbnailPreview(sidebarPagesSelector) {
  document.querySelectorAll(sidebarPagesSelector).forEach((thumbnail) => {
    thumbnail.classList.add('redacted-thumbnail-preview');
    let thumbnailImage = thumbnail.querySelector('.thumbnailImage');
    if (thumbnailImage) thumbnailImage.classList.add('redacted-thumbnail-image-preview');
  });
}

function removeRedactedPagePreview() {
  document.querySelectorAll('.textLayer').forEach((textLayer) => textLayer.classList.remove('redacted-page-preview'));
  document.querySelectorAll('#thumbnailView > a > div.thumbnail').forEach((thumbnail) => {
    thumbnail.classList.remove('redacted-thumbnail-preview');
    let thumbnailImage = thumbnail.querySelector('.thumbnailImage');
    if (thumbnailImage) thumbnailImage.classList.remove('redacted-thumbnail-image-preview');
  });
}

function extractPagesDetailed(pagesInput, totalPageCount) {
  let parts = pagesInput.split(',').filter((s) => s);
  let pagesDetailed = {
    numbers: new Set(),
    functions: new Set(),
    ranges: new Set(),
    all: false,
  };
  for (let part of parts) {
    let trimmedPart = part.trim();
    if ('all' == trimmedPart) {
      pagesDetailed.all = true;
      return pagesDetailed;
    } else if (isValidFunction(trimmedPart)) {
      pagesDetailed.functions.add(formatNFunction(trimmedPart));
    } else if (trimmedPart.includes('-')) {
      let range = trimmedPart
        .replaceAll(' ', '')
        .split('-')
        .filter((s) => s);
      if (range && range.length == 2 && range[0].trim() > 0 && range[1].trim() > 0)
        pagesDetailed.ranges.add({
          low: range[0].trim(),
          high: range[1].trim(),
        });
    } else if (isPageNumber(trimmedPart)) {
      pagesDetailed.numbers.add(trimmedPart <= totalPageCount ? trimmedPart : totalPageCount);
    }
  }

  return pagesDetailed;
}

function formatNFunction(expression) {
  let result = insertMultiplicationBeforeN(expression.replaceAll(' ', ''));
  let multiplyByOpeningRoundBracketPattern = /([0-9n)])\(/g; // example: n(n-1), 9(n-1), (n-1)(n-2)
  result = result.replaceAll(multiplyByOpeningRoundBracketPattern, '$1*(');

  let multiplyByClosingRoundBracketPattern = /\)([0-9n)])/g; // example: (n-1)n, (n-1)9, (n-1)(n-2)
  result = result.replaceAll(multiplyByClosingRoundBracketPattern, ')*$1');
  return result;
}

function insertMultiplicationBeforeN(expression) {
  let result = expression.replaceAll(/(\d)n/g, '$1*n');
  while (result.match(/nn/)) {
    result = result.replaceAll(/nn/g, 'n*n'); // From nn -> n*n
  }
  return result;
}

function validatePages(pages) {
  let parts = pages.split(',').filter((s) => s);
  let errors = [];
  for (let part of parts) {
    let trimmedPart = part.trim();
    if ('all' == trimmedPart) continue;
    else if (trimmedPart.includes('n')) {
      if (!isValidFunction(trimmedPart))
        errors.push(`${trimmedPart} is an invalid function, it should consist of digits 0-9, n, *, -, /, (, ), \\.`);
    } else if (trimmedPart.includes('-')) {
      let range = trimmedPart.split('-').filter((s) => s);
      if (!range || range.length != 2)
        errors.push(`${trimmedPart} is an invalid range, it should consist of from-to, example: 1-5`);
      else if (range[0].trim() <= 0 || range[1].trim() <= 0)
        errors.push(`${trimmedPart} has invalid range(s), page numbers should be positive.`);
    } else if (!isPageNumber(trimmedPart)) {
      errors.push(`${trimmedPart} is invalid, it should either be a function, page number or a range.`);
    }
  }

  return {errors};
}

function isPageNumber(page) {
  return /^[0-9]*$/.test(page);
}

function isValidFunction(part) {
  return part.includes('n') && /[0-9n+\-*/() ]+$/.test(part);
}

function hideContainer(container) {
  container?.classList.add('d-none');
}

const RedactionModes = Object.freeze({
  DRAWING: Symbol('drawing'),
  TEXT: Symbol('text'),
  NONE: Symbol('none'),
});

function removePDFJSButtons() {
  document.getElementById('print')?.remove();
  document.getElementById('download')?.remove();
  document.getElementById('editorStamp')?.remove();
  document.getElementById('editorFreeText')?.remove();
  document.getElementById('editorInk')?.remove();
  document.getElementById('secondaryToolbarToggle')?.remove();
  document.getElementById('openFile')?.remove();
}

function hideInitialPage() {
  document.body.style.overflowY = 'hidden';
  let redactionsFormContainer = document.getElementById('redactionFormContainer');
  for (
    let el = redactionsFormContainer.previousElementSibling;
    el && el instanceof HTMLBRElement;
    el = el.previousElementSibling
  ) {
    el.classList.add('d-none');
  }
  redactionsFormContainer.classList.add('d-none');
  const footer = document.getElementsByTagName('footer')[0];

  // Check if the parent of the footer has the id "viewerContainer"
  if (footer.parentElement && footer.parentElement.id !== 'viewerContainer') {
    footer.classList.add('d-none');
  }
}

window.addEventListener('load', (e) => {
  let isChromium =
    !!window.chrome ||
    (!!navigator.userAgentData && navigator.userAgentData.brands.some((data) => data.brand == 'Chromium'));

  let isSafari =
    /constructor/i.test(window.HTMLElement) ||
    (function (p) {
      return p.toString() === '[object SafariRemoteNotification]';
    })(!window['safari'] || (typeof safari !== 'undefined' && window['safari'].pushNotification));
  let isWebkit = navigator.userAgent.search(/webkit/i) > 0;
  let isGecko = navigator.userAgent.search(/gecko/i) > 0;
  let isFirefox = typeof InstallTrigger !== 'undefined';

  let hiddenInput = document.getElementById('fileInput');
  let outerContainer = document.getElementById('outerContainer');
  let printContainer = document.getElementById('printContainer');

  let toolbarViewerRight = document.getElementById('toolbarViewerRight');
  let showMoreBtn = document.getElementById('showMoreBtn');

  window.onresize = (e) => {
    if (window.innerWidth > 1125 && showMoreBtn.classList.contains('toggled')) {
      showMoreBtn.click();
    } else if (window.innerWidth > 1125 && toolbarViewerRight.hasAttribute('style')) {
      toolbarViewerRight.style.removeProperty('display');
    }
  };

  showMoreBtn.onclick = (e) => {
    if (showMoreBtn.classList.contains('toggled')) {
      toolbarViewerRight.style.display = 'none';
      showMoreBtn.classList.remove('toggled');
    } else {
      toolbarViewerRight.style.display = 'flex';
      showMoreBtn.classList.add('toggled');
    }
  };

  let viewer = document.getElementById('viewer');

  hiddenInput.files = undefined;
  let redactionMode = RedactionModes.NONE;

  let redactions = [];

  let redactionsInput = document.getElementById('redactions-input');

  let redactionsPalette = document.getElementById('redactions-palette');
  let redactionsPaletteInput = redactionsPalette.querySelector('input');

  let redactionsPaletteContainer = document.getElementById('redactionsPaletteContainer');

  let applyRedactionBtn = document.getElementById('apply-redaction');

  let redactedPagesDetails = {
    numbers: new Set(),
    ranges: new Set(),
    functions: new Set(),
    all: false,
  };
  let pageBasedRedactionBtn = document.getElementById('pageBasedRedactionBtn');
  let pageBasedRedactionOverlay = document.getElementById('pageBasedRedactionOverlay');
  pageBasedRedactionBtn.onclick = (e) => pageBasedRedactionOverlay.classList.remove('d-none');

  pageBasedRedactionOverlay.querySelector('input[type=text]').onchange = (e) => {
    let input = e.target;
    let parentElement = input.parentElement;

    resetFieldFeedbackMessages(input, parentElement);

    let value = input.value.trim();
    let {errors} = validatePages(value);
    if (errors && errors.length > 0) {
      applyPageRedactionBtn.disabled = 'true';
      displayFieldErrorMessages(input, errors);
    } else {
      applyPageRedactionBtn.removeAttribute('disabled');
      input.classList.add('is-valid');
    }
  };

  let applyPageRedactionBtn = document.getElementById('applyPageRedactionBtn');
  applyPageRedactionBtn.onclick = (e) => {
    pageBasedRedactionOverlay.querySelectorAll('input').forEach((input) => {
      const id = input.getAttribute('data-for');
      if (id == 'pageNumbers') {
        let {errors} = validatePages(input.value);

        resetFieldFeedbackMessages(input, input.parentElement);

        if (errors?.length > 0) {
          applyPageRedactionBtn.disabled = true;
          displayFieldErrorMessages(input, errors);
        } else {
          pageBasedRedactionOverlay.classList.add('d-none');
          applyRedactionBtn.removeAttribute('disabled');
          input.classList.remove('is-valid');

          let totalPagesCount = PDFViewerApplication.pdfViewer.pagesCount;
          let pagesDetailed = extractPagesDetailed(input.value, totalPagesCount);
          redactedPagesDetails = pagesDetailed;
          addPageRedactionPreviewToPages(pagesDetailed, totalPagesCount);
        }
      } else if (id == 'pageRedactColor') setPageRedactionColor(input.value);
      let formInput = document.getElementById(id);
      if (formInput) formInput.value = input.value;
    });
  };

  let closePageRedactionBtn = document.getElementById('closePageRedactionBtn');
  closePageRedactionBtn.onclick = (e) => {
    pageBasedRedactionOverlay.classList.add('d-none');
    pageBasedRedactionOverlay.querySelectorAll('input').forEach((input) => {
      const id = input.getAttribute('data-for');
      if (id == 'pageNumbers') {
        resetFieldFeedbackMessages(input, input.parentElement);
      }
      let formInput = document.getElementById(id);
      if (formInput) input.value = formInput.value;
    });
  };

  let pdfToImageCheckbox = document.getElementById('convertPDFToImage');

  let pdfToImageBtn = document.getElementById('pdfToImageBtn');
  pdfToImageBtn.onclick = (e) => {
    pdfToImageBtn.classList.toggle('btn-success');
    pdfToImageBtn.classList.toggle('btn-danger');
    pdfToImageCheckbox.checked = !pdfToImageCheckbox.checked;
  };

  let fileChooser = document.getElementsByClassName('custom-file-chooser')[0];
  let fileChooserInput = fileChooser.querySelector(`#${fileChooser.getAttribute('data-bs-element-id')}`);

  let uploadButton = document.getElementById('uploadBtn');
  uploadButton.onclick = (e) => fileChooserInput.click();

  document.addEventListener('file-input-change', (e) => {
    redactions = [];
    _setRedactionsInput(redactions);
  });

  let submitBtn = document.getElementById('submitBtn');

  let downloadBtn = document.getElementById('downloadBtn');
  let downloadBtnIcon = document.getElementById('downloadBtnIcon');

  downloadBtn.onclick = (e) => {
    submitBtn.click();
    setTimeout(_showOrHideLoadingSpinner, 100); // wait 100 milliseconds so that submitBtn would be disabled
  };

  function _showOrHideLoadingSpinner() {
    if (!submitBtn.disabled) {
      downloadBtnIcon.innerHTML = 'download';
      downloadBtnIcon.classList.remove('spin-animation');
      return;
    }

    downloadBtnIcon.innerHTML = 'progress_activity';
    downloadBtnIcon.classList.add('spin-animation');
    setTimeout(_showOrHideLoadingSpinner, 500);
  }

  redactionsPaletteContainer.onclick = (e) => redactionsPalette.click();

  viewer.onmouseup = (e) => {
    if (redactionMode !== RedactionModes.TEXT) return;
    const containsText = window.getSelection() && window.getSelection().toString() != '';
    applyRedactionBtn.disabled = !containsText;
  };

  applyRedactionBtn.onclick = (e) => {
    if (redactionMode !== RedactionModes.TEXT) {
      applyRedactionBtn.disabled = true;
      return;
    }
    redactTextSelection();
    applyRedactionBtn.disabled = true;
  };

  redactionsPaletteInput.onchange = (e) => {
    let color = e.target.value;
    redactionsPalette.style.setProperty('--palette-color', color);
  };

  document.addEventListener('file-input-change', (e) => {
    let fileChooser = document.getElementsByClassName('custom-file-chooser')[0];
    let fileChooserInput = fileChooser.querySelector(`#${fileChooser.getAttribute('data-bs-element-id')}`);

    hiddenInput.files = fileChooserInput.files;
    if (!hiddenInput.files || hiddenInput.files.length === 0) {
      hideContainer(outerContainer);
      hideContainer(printContainer);
    } else {
      outerContainer?.classList.remove('d-none');
      printContainer?.classList.remove('d-none');
      hideInitialPage();
    }

    hiddenInput.dispatchEvent(new Event('change', {bubbles: true}));
  });

  PDFViewerApplication.downloadOrSave = doNothing;
  PDFViewerApplication.triggerPrinting = doNothing;

  let redactionContainersDivs = {};
  PDFViewerApplication.eventBus.on('pagerendered', (e) => {
    removePDFJSButtons();

    let textSelectionRedactionBtn = document.getElementById('man-text-select-redact');
    let drawRedactionBtn = document.getElementById('man-shape-redact');

    textSelectionRedactionBtn.onclick = _handleTextSelectionRedactionBtnClick;
    drawRedactionBtn.onclick = _handleDrawRedactionBtnClick;

    let layer = e.source.textLayer.div;
    layer.setAttribute('data-page', e.pageNumber);
    if (redactedPagesDetails.all || redactedPagesDetails.numbers.has(e.pageNumber)) {
      layer.classList.add('redacted-page-preview');
    } else {
      layer.classList.remove('redacted-page-preview');
    }

    zoomScaleValue = e.source.scale ? e.source.scale : e.source.pageScale;
    document.documentElement.style.setProperty('--zoom-scale', zoomScaleValue);

    let redactionsContainer = document.getElementById(`redactions-container-${e.pageNumber}`);
    if (!redactionsContainer && !redactionContainersDivs[`${e.pageNumber}`]) {
      redactionsContainer = document.createElement('div');
      redactionsContainer.style.position = 'relative';
      redactionsContainer.style.height = '100%';
      redactionsContainer.style.width = '100%';
      redactionsContainer.id = `redactions-container-${e.pageNumber}`;
      redactionsContainer.style.setProperty('z-index', 'unset');

      layer.appendChild(redactionsContainer);
      redactionContainersDivs[`${e.pageNumber}`] = redactionsContainer;
    } else if (!redactionsContainer && redactionContainersDivs[`${e.pageNumber}`]) {
      redactionsContainer = redactionContainersDivs[`${e.pageNumber}`];

      layer.appendChild(redactionsContainer);
      // Dispatch event to update text layer references for elements' events
      redactionsContainer.querySelectorAll('.selected-wrapper').forEach((area) =>
        area.dispatchEvent(
          new CustomEvent('textLayer-reference-changed', {
            bubbles: true,
            detail: {textLayer: layer},
          })
        )
      );
    }

    document.onpointerup = (e) => {
      if (drawingLayer && e.target != drawingLayer && e.button == 0)
        drawingLayer.dispatchEvent(new Event('external-pointerup'));
    };

    initDraw(layer, redactionsContainer);

    function _handleTextSelectionRedactionBtnClick(e) {
      if (textSelectionRedactionBtn.classList.contains('toggled')) {
        resetTextSelection();
      } else {
        resetDrawRedactions();
        textSelectionRedactionBtn.classList.add('toggled');
        redactionMode = RedactionModes.TEXT;
        const containsText = window.getSelection() && window.getSelection().toString() != '';
        applyRedactionBtn.disabled = !containsText;
        applyRedactionBtn.classList.remove('d-none');
      }
    }

    function resetTextSelection() {
      textSelectionRedactionBtn.classList.remove('toggled');
      redactionMode = RedactionModes.NONE;
      clearSelection();
      applyRedactionBtn.disabled = true;
      applyRedactionBtn.classList.add('d-none');
    }

    function clearSelection() {
      if (window.getSelection) {
        if (window.getSelection().empty) {
          // Chrome
          window.getSelection().empty();
        } else if (window.getSelection().removeAllRanges) {
          // Firefox
          window.getSelection().removeAllRanges();
        }
      } else if (document.selection) {
        // IE?
        document.selection.empty();
      }
    }

    function _handleDrawRedactionBtnClick(e) {
      if (drawRedactionBtn.classList.contains('toggled')) {
        resetDrawRedactions();
      } else {
        resetTextSelection();
        drawRedactionBtn.classList.add('toggled');
        document.documentElement.style.setProperty('--textLayer-pointer-events', 'none');
        document.documentElement.style.setProperty('--textLayer-user-select', 'none');
        redactionMode = RedactionModes.DRAWING;
      }
    }

    function resetDrawRedactions() {
      redactionMode = RedactionModes.NONE;
      drawRedactionBtn.classList.remove('toggled');
      document.documentElement.style.setProperty('--textLayer-pointer-events', 'auto');
      document.documentElement.style.setProperty('--textLayer-user-select', 'auto');
      window.dispatchEvent(new CustomEvent('reset-drawing', {bubbles: true}));
    }

    function initDraw(canvas, redactionsContainer) {
      let mouse = {
        x: 0,
        y: 0,
        startX: 0,
        startY: 0,
      };
      let element = null;
      let drawnRedaction = null;

      window.addEventListener('reset-drawing', (e) => {
        _clearDrawing();
        canvas.style.cursor = 'default';
        document.documentElement.style.setProperty('--textLayer-pointer-events', 'auto');
        document.documentElement.style.setProperty('--textLayer-user-select', 'auto');
      });

      window.addEventListener('drawing-entered', (e) => {
        let target = e.detail?.target;
        if (canvas === target) return;
        _clearDrawing();
      });

      window.addEventListener('cancel-drawing', (e) => {
        _clearDrawing();
        canvas.style.cursor = 'default';
      });

      function setMousePosition(e) {
        if (isChromium || isSafari || isWebkit) {
          mouse.x = e.offsetX;
          mouse.y = e.offsetY;
        } else if (isFirefox || isGecko) {
          mouse.x = e.layerX;
          mouse.y = e.layerY;
        } else {
          let rect = (e.target || e.srcElement).getBoundingClientRect();
          mouse.x = e.clientX - rect.left;
          mouse.y = e.clientY - rect.top;
        }
      }

      window.onkeydown = (e) => {
        if (e.key === 'Escape' && redactionMode === RedactionModes.DRAWING) {
          window.dispatchEvent(new CustomEvent('cancel-drawing', {bubbles: true}));
        }
      };

      canvas.onpointerenter = (e) => {
        window.dispatchEvent(
          new CustomEvent('drawing-entered', {
            bubbles: true,
            detail: {target: canvas},
          })
        );
      };

      canvas.onpointerup = (e) => {
        let isLeftClick = e.button == 0;
        if (!isLeftClick) return;

        if (element !== null) {
          _saveAndResetDrawnRedaction();
          console.log('finished.');
        }
      };

      canvas.addEventListener('external-pointerup', (e) => {
        if (element != null) {
          _saveAndResetDrawnRedaction();
        }
      });

      canvas.onpointerleave = (e) => {
        let ev = copyEvent(e, 'pointerleave');
        let {left, top} = calculateMouseCoordinateToRotatedBox(canvas, e);

        ev.layerX = left;
        ev.offsetX = left;

        ev.layerY = top;
        ev.offsetY = top;

        setMousePosition(ev);
        if (element !== null) {
          draw();
        }
      };

      canvas.onpointerdown = (e) => {
        let isLeftClick = e.button == 0;
        if (!isLeftClick) return;

        if (element == null) {
          if (redactionMode !== RedactionModes.DRAWING) {
            console.warn('Drawing attempt when redaction mode is', redactionMode.description);
            return;
          }
          console.log('begun.');
          _captureAndDrawStartingPointOfDrawnRedaction();
        }
      };

      canvas.onpointermove = function (e) {
        setMousePosition(e);
        if (element !== null) {
          draw();
        }
      };

      function draw() {
        let scaleFactor = _getScaleFactor();

        let width = Math.abs(mouse.x - mouse.startX);
        element.style.width = _toCalcZoomPx(_scaleToDisplay(width));

        let height = Math.abs(mouse.y - mouse.startY);
        element.style.height = _toCalcZoomPx(_scaleToDisplay(height));

        let left = mouse.x - mouse.startX < 0 ? mouse.x : mouse.startX;
        element.style.left = _toCalcZoomPx(_scaleToDisplay(left));

        let top = mouse.y - mouse.startY < 0 ? mouse.y : mouse.startY;
        element.style.top = _toCalcZoomPx(_scaleToDisplay(top));

        if (drawnRedaction) {
          drawnRedaction.left = _scaleToPDF(left, scaleFactor);
          drawnRedaction.top = _scaleToPDF(top, scaleFactor);
          drawnRedaction.width = _scaleToPDF(width, scaleFactor);
          drawnRedaction.height = _scaleToPDF(height, scaleFactor);
        }
      }

      function _clearDrawing() {
        if (element) element.remove();
        if (drawingLayer == canvas) drawingLayer = null;
        element = null;
        drawnRedaction = null;
      }

      function _saveAndResetDrawnRedaction() {
        if (!element) return;
        if (
          !element.style.height ||
          element.style.height.includes('(0px * var') ||
          !element.style.width ||
          element.style.width.includes('(0px * var')
        ) {
          element.remove();
        } else {
          element.classList.add('selected-wrapper');
          element.classList.remove('rectangle');

          addRedactionOverlay(element, drawnRedaction, canvas);
          redactions.push(drawnRedaction);
          _setRedactionsInput(redactions);
        }
        drawingLayer = null;
        element = null;
        drawnRedaction = null;
        canvas.style.cursor = 'default';
      }

      function _captureAndDrawStartingPointOfDrawnRedaction() {
        mouse.startX = mouse.x;
        mouse.startY = mouse.y;

        element = document.createElement('div');
        element.className = 'rectangle';
        drawingLayer = canvas;

        let left = mouse.x;
        let top = mouse.y;

        element.style.left = _toCalcZoomPx(_scaleToDisplay(left));
        element.style.top = _toCalcZoomPx(_scaleToDisplay(top));

        let scaleFactor = _getScaleFactor();
        let color = redactionsPalette.style.getPropertyValue('--palette-color');

        element.style.setProperty('--palette-color', color);

        drawnRedaction = {
          left: _scaleToPDF(left, scaleFactor),
          top: _scaleToPDF(top, scaleFactor),
          width: 0.0,
          height: 0.0,
          color: color,
          pageNumber: parseInt(canvas.getAttribute('data-page')),
          element: element,
          id: UUID.uuidv4(),
        };

        redactionsContainer.appendChild(element);
        canvas.style.cursor = 'crosshair';
      }
    }
  });

  PDFViewerApplication.eventBus.on('rotationchanging', (e) => {
    if (!activeOverlay) return;
    hideOverlay();
  });

  function _getScaleFactor() {
    return parseFloat(viewer.style.getPropertyValue('--scale-factor'));
  }

  function getTextLayer(element) {
    let current = element;
    while (current) {
      if (current instanceof HTMLDivElement && current.classList.contains('textLayer')) return current;
      current = current.parentElement;
    }

    return current;
  }

  document.onclick = (e) => {
    if (
      (e.target && e.target.classList.contains('selected-wrapper') && e.target.firstChild == activeOverlay) ||
      e.target == activeOverlay
    )
      return;
    if (activeOverlay) hideOverlay();
  };

  document.addEventListener('keydown', (e) => {
    if (e.key === 'Delete' && activeOverlay) {
      activeOverlay.querySelector('.delete-icon')?.dispatchEvent(new Event('click', {bubbles: true}));
      return;
    }
    const isRedactionShortcut = e.ctrlKey && (e.key == 's' || e.key == 'S' || e.code == 'KeyS');
    if (!isRedactionShortcut || redactionMode !== RedactionModes.TEXT) return;

    redactTextSelection();
  });

  function rotateTextBox(rect, textLayerRect, angle) {
    let left, top, width, height;
    if (!angle || angle == 0) {
      left = rect.left - textLayerRect.left;
      top = rect.top - textLayerRect.top;
      width = rect.width;
      height = rect.height;
    } else if (angle == 90) {
      left = rect.top - textLayerRect.top;
      top = textLayerRect.right - rect.right;
      width = rect.height;
      height = rect.width;
    } else if (angle == 180) {
      left = textLayerRect.right - rect.right;
      top = textLayerRect.bottom - rect.bottom;
      width = rect.width;
      height = rect.height;
    } else if (angle == 270) {
      left = textLayerRect.bottom - rect.bottom;
      top = rect.left - textLayerRect.left;
      width = rect.height;
      height = rect.width;
    }

    return {left, top, width, height};
  }

  function redactTextSelection() {
    let selection = window.getSelection();
    if (!selection || selection.rangeCount <= 0) return;
    let range = selection.getRangeAt(0);

    let textLayer = getTextLayer(range.startContainer);
    if (!textLayer) return;

    const pageNumber = textLayer.getAttribute('data-page');
    let redactionsArea = textLayer.querySelector(`#redactions-container-${pageNumber}`);
    let textLayerRect = textLayer.getBoundingClientRect();

    let rects = range.getClientRects();
    let scaleFactor = _getScaleFactor();

    let color = redactionsPalette.style.getPropertyValue('--palette-color');

    let angle = textLayer.getAttribute('data-main-rotation');
    for (const rect of rects) {
      if (!rect || !rect.width || !rect.height) continue;
      let redactionElement = document.createElement('div');
      redactionElement.classList.add('selected-wrapper');

      let {left, top, width, height} = rotateTextBox(rect, textLayerRect, angle);

      let leftDisplayScaled = _scaleToDisplay(left);
      let topDisplayScaled = _scaleToDisplay(top);
      let widthDisplayScaled = _scaleToDisplay(width);
      let heightDisplayScaled = _scaleToDisplay(height);

      let redactionInfo = {
        left: _scaleToPDF(left, scaleFactor),
        top: _scaleToPDF(top, scaleFactor),
        width: _scaleToPDF(width, scaleFactor),
        height: _scaleToPDF(height, scaleFactor),
        pageNumber: parseInt(pageNumber),
        color: color,
        element: redactionElement,
        id: UUID.uuidv4(),
      };

      redactions.push(redactionInfo);

      redactionElement.style.left = _toCalcZoomPx(leftDisplayScaled);
      redactionElement.style.top = _toCalcZoomPx(topDisplayScaled);

      redactionElement.style.width = _toCalcZoomPx(widthDisplayScaled);
      redactionElement.style.height = _toCalcZoomPx(heightDisplayScaled);
      redactionElement.style.setProperty('--palette-color', color);

      redactionsArea.appendChild(redactionElement);

      addRedactionOverlay(redactionElement, redactionInfo, textLayer);
    }

    _setRedactionsInput(redactions);
    applyRedactionBtn.disabled = true;
  }

  function _scaleToDisplay(value) {
    return value / zoomScaleValue;
  }

  function _scaleToPDF(value, scaleFactor) {
    if (!scaleFactor) scaleFactor = document.documentElement.getPropertyValue('--scale-factor');
    return value / scaleFactor;
  }

  function _toCalcZoomPx(val) {
    return `calc(${val}px * var(--zoom-scale))`;
  }

  function _setRedactionsInput(redactions) {
    let stringifiedRedactions = JSON.stringify(
      redactions.filter(_nonEmptyRedaction).map((red) => ({
        x: red.left,
        y: red.top,
        width: red.width,
        height: red.height,
        color: red.color,
        page: red.pageNumber,
      }))
    );
    redactionsInput.value = stringifiedRedactions;
  }

  function addRedactionOverlay(redactionElement, redactionInfo, textLayer) {
    let redactionOverlay = document.createElement('div');

    let deleteBtn = $(
      `<svg class="delete-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="#e8eaed"><path d="M312-144q-29.7 0-50.85-21.15Q240-186.3 240-216v-480h-48v-72h192v-48h192v48h192v72h-48v479.57Q720-186 698.85-165T648-144H312Zm336-552H312v480h336v-480ZM384-288h72v-336h-72v336Zm120 0h72v-336h-72v336ZM312-696v480-480Z"/></svg>`
    )[0];

    deleteBtn.onclick = (e) => {
      redactions = redactions.filter((red) => redactionInfo.id != red.id);
      redactionElement.remove();
      _setRedactionsInput(redactions);
      activeOverlay = null;
    };

    let colorPaletteLabel = $(
      `<label class="material-symbols-rounded palette-color position-relative">
         palette
       </label>`
    )[0];

    let colorPaletteInput = $(`
      <input type="color" name="color-picker" class="overlay-colorpicker-window">
      `)[0];

    colorPaletteLabel.appendChild(colorPaletteInput);

    colorPaletteLabel.onclick = (e) => {
      if (colorPaletteLabel === e.target) {
        e.stopPropagation();
      }
    };

    colorPaletteInput.onchange = (e) => {
      let color = e.target.value;
      redactionElement.style.setProperty('--palette-color', color);
      let redactionIdx = redactions.findIndex((red) => redactionInfo.id === red.id);
      if (redactionIdx < 0) return;
      redactions[redactionIdx].color = color;
      _setRedactionsInput(redactions);
    };

    redactionOverlay.appendChild(deleteBtn);
    redactionOverlay.appendChild(colorPaletteLabel);

    redactionOverlay.classList.add('redaction-overlay');
    redactionOverlay.style.display = 'none';

    redactionElement.addEventListener('textLayer-reference-changed', (e) => {
      textLayer = e.detail.textLayer;
    });

    redactionElement.onclick = (e) => {
      if (e.target != redactionElement) return;
      if (activeOverlay) hideOverlay();
      redactionElement.classList.add('active-redaction');
      activeOverlay = redactionOverlay;
      _adjustActiveOverlayCoordinates();
    };

    redactionElement.appendChild(redactionOverlay);

    // Adjust active overlay coordinates to avoid placing the overlay out of page bounds
    function _adjustActiveOverlayCoordinates() {
      activeOverlay.style.visibility = 'hidden';
      activeOverlay.style.display = 'flex';
      textLayer = textLayer || getTextLayer(redactionElement);
      let angle = parseInt(textLayer.getAttribute('data-main-rotation'));
      if (textLayer) redactionOverlay.style.transform = `rotate(${angle * -1}deg)`;

      activeOverlay.style.removeProperty('left');
      activeOverlay.style.removeProperty('top');

      let textRect = textLayer.getBoundingClientRect();
      let overlayRect = redactionOverlay.getBoundingClientRect();

      let leftOffset = 0,
        topOffset = 0;
      if (overlayRect.right > textRect.right) {
        leftOffset = textRect.right - overlayRect.right;
      } else if (overlayRect.left < textRect.left) {
        leftOffset = textRect.left - overlayRect.left;
      }

      if (overlayRect.top < textRect.top) {
        topOffset = textRect.top - overlayRect.top;
      } else if (overlayRect.bottom > textRect.bottom) {
        topOffset = textRect.bottom - overlayRect.bottom;
      }

      switch (angle) {
        case 90:
          [leftOffset, topOffset] = [topOffset, -leftOffset];
          break;
        case 180:
          [leftOffset, topOffset] = [-leftOffset, -topOffset];
          break;
        case 270:
          [leftOffset, topOffset] = [-topOffset, leftOffset];
          break;
      }

      if (leftOffset != 0) activeOverlay.style.left = `calc(50% + ${leftOffset}px`;
      if (topOffset != 0) activeOverlay.style.top = `calc(100% + ${topOffset}px`;
      activeOverlay.style.visibility = 'unset';
    }
  }
});

function calculateMouseCoordinateToRotatedBox(canvas, e) {
  let textRect = canvas.getBoundingClientRect();
  let left,
    top = 0;
  let angle = parseInt(canvas.getAttribute('data-main-rotation'));
  switch (angle) {
    case 0:
      left = clamp(e.pageX - textRect.left, 0, textRect.width);
      top = clamp(e.pageY - textRect.top, 0, textRect.height);
      break;

    case 90:
      left = clamp(e.pageY - textRect.top, 0, textRect.height);
      top = clamp(textRect.right - e.pageX, 0, textRect.width);
      break;
    case 180:
      left = clamp(textRect.right - e.pageX, 0, textRect.width);
      top = clamp(textRect.bottom - e.pageY, 0, textRect.width);
      break;
    case 270:
      left = clamp(textRect.bottom - e.pageY, 0, textRect.height);
      top = clamp(e.pageX - textRect.left, 0, textRect.width);
      break;
  }
  return {left, top};
}

function clamp(value, min, max) {
  return Math.max(min, Math.min(value, max));
}

function addPageRedactionPreviewToPages(pagesDetailed, totalPagesCount) {
  if (pagesDetailed.all) {
    addRedactedPagePreview('#viewer > .page');
    addRedactedThumbnailPreview('#thumbnailView > a > div.thumbnail');
  } else {
    removeRedactedPagePreview();

    setPageNumbersFromRange(pagesDetailed, totalPagesCount);
    setPageNumbersFromNFunctions(pagesDetailed, totalPagesCount);

    let pageNumbers = Array.from(pagesDetailed.numbers);
    if (pageNumbers?.length > 0) {
      let pagesSelector = pageNumbers.map((number) => `#viewer > .page[data-page-number="${number}"]`).join(',');
      addRedactedPagePreview(pagesSelector);
      let thumbnailSelector = pageNumbers
        .map((number) => `#thumbnailView > a > div.thumbnail[data-page-number="${number}"]`)
        .join(',');
      addRedactedThumbnailPreview(thumbnailSelector);
    }
  }
}

function resetFieldFeedbackMessages(input, parentElement) {
  if (parentElement) parentElement.querySelectorAll('.invalid-feedback').forEach((feedback) => feedback.remove());
  if (input) {
    input.classList.remove('is-invalid');
    input.classList.remove('is-valid');
  }
}

function displayFieldErrorMessages(input, errors) {
  input.classList.add('is-invalid');
  errors.forEach((error) => {
    let element = document.createElement('div');
    element.classList.add('invalid-feedback');
    element.classList.add('list-styling');
    element.textContent = error;
    input.parentElement.appendChild(element);
  });
}

function setPageRedactionColor(color) {
  document.documentElement.style.setProperty('--page-redaction-color', color);
}

function setPageNumbersFromNFunctions(pagesDetailed, totalPagesCount) {
  pagesDetailed.functions.forEach((fun) => {
    if (!isValidFunction(fun)) return;
    for (let n = 1; n <= totalPagesCount; n++) {
      let pageNumber = eval(fun);
      if (!pageNumber || pageNumber <= 0 || pageNumber > totalPagesCount) continue;
      pagesDetailed.numbers.add(pageNumber);
    }
  });
}

function setPageNumbersFromRange(pagesDetailed, totalPagesCount) {
  pagesDetailed.ranges.forEach((range) => {
    for (let i = range.low; i <= range.high && i <= totalPagesCount; i++) {
      pagesDetailed.numbers.add(i);
    }
  });
}

function hideOverlay() {
  activeOverlay.style.display = 'none';
  activeOverlay.parentElement.classList.remove('active-redaction');
  activeOverlay = null;
}

function _isEmptyRedaction(redaction) {
  return (
    redaction.left == null ||
    redaction.top == null ||
    redaction.width == null ||
    redaction.height == null ||
    redaction.pageNumber == null
  );
}

function _nonEmptyRedaction(redaction) {
  return !_isEmptyRedaction(redaction);
}

function copyEvent(e, type) {
  if (type == 'pointerleave')
    return {
      layerX: e.layerX,
      layerY: e.layerY,
      pageX: e.pageX,
      pageY: e.pageY,
      clientX: e.clientX,
      clientY: e.clientY,
      button: e.button,
      height: e.height,
      width: e.width,
      offsetX: e.offsetX,
      offsetY: e.offsetY,
      pointerId: e.pointerId,
      pointerType: e.pointerType,
      type: e.type,
      screenX: e.screenX,
      screenY: e.screenY,
      tiltX: e.tiltX,
      tiltY: e.tiltY,
      x: e.x,
      y: e.y,
      altKey: e.altKey,
      ctrlKey: e.ctrlKey,
      isPrimary: e.isPrimary,
      isTrusted: e.isTrusted,
      metaKey: e.metaKey,
      pressure: e.pressure,
      returnValue: e.returnValue,
      shiftKey: e.shiftKey,
      timeStamp: e.timeStamp,
      which: e.which,
      twist: e.twist,
      tangentialPressure: e.tangentialPressure,
      target: e.target,
      srcElement: e.srcElement,
      relatedTarget: e.relatedTarget,
      rangeOffset: e.rangeOffset,
      rangeParent: e.rangeParent,
      explicitOriginalTarget: e.explicitOriginalTarget,
      eventPhase: e.eventPhase,
      detail: e.detail,
      defaultPrevented: e.defaultPrevented,
      currentTarget: e.currentTarget,
      buttons: e.buttons,
      azimuthAngle: e.azimuthAngle,
      altitudeAngle: e.altitudeAngle,
    };

  return {};
}
