import { v4 as uuidv4 } from 'uuid';

const DEFAULT_SCALE_VALUE = 'page-fit';
const DEFAULT_SCALE = 1.0;
const DEFAULT_SCALE_DELTA = 1.1;
const MIN_SCALE = 0.05;
const MAX_SCALE = 10.0;
const UNKNOWN_SCALE = 0;
const MAX_AUTO_SCALE = 1.25;
const SCROLLBAR_PADDING = 40;
const VERTICAL_PADDING = 5;

const RenderingStates = {
  INITIAL: 0,
  RUNNING: 1,
  PAUSED: 2,
  FINISHED: 3,
};

const PresentationModeState = {
  UNKNOWN: 0,
  NORMAL: 1,
  CHANGING: 2,
  FULLSCREEN: 3,
};

const SidebarView = {
  UNKNOWN: -1,
  NONE: 0,
  THUMBS: 1, // Default value.
  OUTLINE: 2,
  ATTACHMENTS: 3,
  LAYERS: 4,
};

const RendererType = {
  CANVAS: "canvas",
  SVG: "svg",
};

const TextLayerMode = {
  DISABLE: 0,
  ENABLE: 1,
};

const ScrollMode = {
  UNKNOWN: -1,
  VERTICAL: 0, // Default value.
  HORIZONTAL: 1,
  WRAPPED: 2,
  PAGE: 3,
};

const SpreadMode = {
  UNKNOWN: -1,
  NONE: 0, // Default value.
  ODD: 1,
  EVEN: 2,
};

const userAgent = navigator.userAgent || "";
const platform = navigator.platform || "";
const maxTouchPoints = navigator.maxTouchPoints || 1;
// 限制canvas最大像素值
let maxCanvasPixels = 16777216
const isAndroid = /Android/.test(userAgent);
const isIOS =
  /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent) ||
  (platform === "MacIntel" && maxTouchPoints > 1);
(function checkCanvasSizeLimitation() {
  if (isIOS || isAndroid) {
    maxCanvasPixels = 5242880;
  }
})();
const isMobileDevice = isIOS || isAndroid || /webOS|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
const isMobile = window.innerWidth < 640;

// Used by `PDFViewerApplication`, and by the API unit-tests.
const AutoPrintRegExp = /\bprint\s*\(/;

/**
 * Scale factors for the canvas, necessary with HiDPI displays.
 */
class OutputScale {
  constructor() {
    const pixelRatio = window.devicePixelRatio || 1;

    /**
     * @type {number} Horizontal scale.
     */
    this.sx = pixelRatio;

    /**
     * @type {number} Vertical scale.
     */
    this.sy = pixelRatio;
  }

  /**
   * @type {boolean} Returns `true` when scaling is required, `false` otherwise.
   */
  get scaled() {
    return this.sx !== 1 || this.sy !== 1;
  }
}

export function convertToPDFTimestamp(date) {
  const timezoneOffset = date.getTimezoneOffset()
  const offsetSign = timezoneOffset < 0 ? '+' : '-'
  const offsetHours = ('00' + Math.abs(timezoneOffset / 60)).slice(-2)
  const offsetMinutes = ('00' + Math.abs(timezoneOffset % 60)).slice(-2)
  const sep = `${offsetSign}${offsetHours}'${offsetMinutes}'`
  const year = date.getFullYear()
  const month = ('00' + (date.getMonth() + 1)).slice(-2)
  const day = ('00' + date.getDate()).slice(-2)
  const hours = ('00' + date.getHours()).slice(-2)
  const minutes = ('00' + date.getMinutes()).slice(-2)
  const seconds = ('00' + date.getSeconds()).slice(-2)
  const timestamp = `D:${year}${month}${day}${hours}${minutes}${seconds}${sep}`

  return timestamp
}

export function round(number, precision) {
  const b = Math.pow(10, precision)
  return Math.round(number * b) / b
}

export function parseAdobePDFTimestamp(timestamp) {
  if (!timestamp) {
    return new Date()
  }
  // 使用正则表达式匹配时间戳格式
  const regex = /D:(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})([-+]\d{2}'\d{2}')?/;
  const match = timestamp.match(regex);

  if (match) {
    const [, year, month, day, hour, minute, second, offset] = match;
    // const offsetHours = parseInt(offset.slice(1, 3));
    // const offsetMinutes = parseInt(offset.slice(4, 6));
    // 构建 Date 对象，注意月份要减 1
    const date = new Date(
      parseInt(year),
      parseInt(month) - 1,
      parseInt(day),
      parseInt(hour),
      parseInt(minute),
      parseInt(second)
    );

    // 考虑时区偏移
    // date.setHours(date.getUTCHours());
    // date.setMinutes(date.getUTCMinutes());

    return date;
  } else {
    throw new Error('Invalid timestamp format');
  }
}

// 保留两位小数
export function roundToDecimalPlaces(number, decimalPlaces = 2) {
  const multiplier = Math.pow(10, decimalPlaces);
  return Math.round(number * multiplier) / multiplier;
}

export class PromiseExt {
  constructor() {
    this.promise = new Promise((resolve, reject) => {
      this.resolve = (data) => {
        resolve(data);
      };
      this.reject = (reason) => {
        reject(reason);
      };
    });
  }
}

// Ensure that the returned Object has a `null` prototype; hence why
// `Object.fromEntries(...)` is not used.
function objectFromMap(map) {
  const obj = Object.create(null);
  for (const [key, value] of map) {
    obj[key] = value;
  }
  return obj;
}

function BaseException(message, name) {
  if (this.constructor === BaseException) {
    unreachable("Cannot initialize BaseException.");
  }
  this.message = message;
  this.name = name;
}
BaseException.prototype = new Error();
BaseException.constructor = BaseException;

/**
 * Error used to indicate task cancellation.
 */
class AbortException extends BaseException {
  constructor(msg) {
    super(msg, "AbortException");
  }
}

class EventBus {
  constructor() {
    this._listeners = Object.create(null);
  }

  /**
   * @param {string} eventName
   * @param {function} listener
   * @param {Object} [options]
   */
  on(eventName, listener, options = null) {
    this._on(eventName, listener, {
      external: true,
      once: options?.once,
    });
  }

  /**
   * @param {string} eventName
   * @param {function} listener
   * @param {Object} [options]
   */
  off(eventName, listener, options = null) {
    this._off(eventName, listener, {
      external: true,
      once: options?.once,
    });
  }

  /**
   * @param {string} eventName
   * @param {Object} data
   */
  dispatch(eventName, data, action = '') {
    const eventListeners = this._listeners[eventName];
    if (!eventListeners || eventListeners.length === 0) {
      return;
    }
    let externalListeners;
    // Making copy of the listeners array in case if it will be modified
    // during dispatch.
    for (const { listener, external, once } of eventListeners.slice(0)) {
      if (once) {
        this._off(eventName, listener)
      }
      if (external) {
        (externalListeners ||= []).push(listener)
        continue;
      }
      listener(data, action)
    }
    // Dispatch any "external" listeners *after* the internal ones, to give the
    // viewer components time to handle events and update their state first.
    if (externalListeners) {
      for (const listener of externalListeners) {
        listener(data);
      }
      externalListeners = null;
    }
  }

  /**
   * @ignore
   */
  _on(eventName, listener, options = null) {
    const eventListeners = (this._listeners[eventName] ||= []);
    eventListeners.push({
      listener,
      external: options?.external === true,
      once: options?.once === true,
    });
  }

  /**
   * @ignore
   */
  _off(eventName, listener, options = null) {
    const eventListeners = this._listeners[eventName];
    if (!eventListeners) {
      return;
    }
    for (let i = 0, ii = eventListeners.length; i < ii; i++) {
      if (eventListeners[i].listener === listener) {
        eventListeners.splice(i, 1);
        return;
      }
    }
  }
}

/**
 * Scrolls specified element into view of its parent.
 * @param {Object} element - The element to be visible.
 * @param {Object} spot - An object with optional top and left properties,
 *   specifying the offset from the top left edge.
 * @param {boolean} [scrollMatches] - When scrolling search results into view,
 *   ignore elements that either: Contains marked content identifiers,
 *   or have the CSS-rule `overflow: hidden;` set. The default value is `false`.
 */
function scrollIntoView(element, spot, scrollMatches = false) {
  // Assuming offsetParent is available (it's not available when viewer is in
  // hidden iframe or object). We have to scroll: if the offsetParent is not set
  // producing the error. See also animationStarted.
  let parent = element.offsetParent;
  if (!parent) {
    console.error("offsetParent is not set -- cannot scroll");
    return;
  }
  let offsetY = element.offsetTop + element.clientTop;
  let offsetX = element.offsetLeft + element.clientLeft;
  while (
    (parent.clientHeight === parent.scrollHeight &&
      parent.clientWidth === parent.scrollWidth) ||
    (scrollMatches &&
      (parent.classList.contains("markedContent") ||
        getComputedStyle(parent).overflow === "hidden"))
  ) {
    offsetY += parent.offsetTop;
    offsetX += parent.offsetLeft;

    parent = parent.offsetParent;
    if (!parent) {
      return; // no need to scroll
    }
  }
  if (spot) {
    if (spot.top !== undefined) {
      offsetY += spot.top;
    }
    if (spot.left !== undefined) {
      offsetX += spot.left;
      parent.scrollLeft = offsetX;
    }
  }
  parent.scrollTop = offsetY;
}

/**
 * Helper function to start monitoring the scroll event and converting them into
 * PDF.js friendly one: with scroll debounce and scroll direction.
 */
function watchScroll(viewAreaElement, callback) {
  const debounceScroll = function (evt) {
    if (rAF) {
      return;
    }
    // schedule an invocation of scroll for next animation frame.
    rAF = window.requestAnimationFrame(function viewAreaElementScrolled() {
      rAF = null;

      const currentX = viewAreaElement.scrollLeft;
      const lastX = state.lastX;
      if (currentX !== lastX) {
        state.right = currentX > lastX;
      }
      state.lastX = currentX;
      const currentY = viewAreaElement.scrollTop;
      const lastY = state.lastY;
      if (currentY !== lastY) {
        state.down = currentY > lastY;
      }
      state.lastY = currentY;
      callback(state);
    });
  };

  const state = {
    right: true,
    down: true,
    lastX: viewAreaElement.scrollLeft,
    lastY: viewAreaElement.scrollTop,
    _eventHandler: debounceScroll,
  };

  let rAF = null;
  viewAreaElement.addEventListener("scroll", debounceScroll, true);
  return state;
}

/**
 * Helper function to parse query string (e.g. ?param1=value&param2=...).
 * @param {string}
 * @returns {Map}
 */
function parseQueryString(query) {
  const params = new Map();
  for (const [key, value] of new URLSearchParams(query)) {
    params.set(key.toLowerCase(), value);
  }
  return params;
}

const NullCharactersRegExp = /\x00/g;
const InvisibleCharactersRegExp = /[\x01-\x1F]/g;

/**
 * @param {string} str
 * @param {boolean} [replaceInvisible]
 */
function removeNullCharacters(str, replaceInvisible = false) {
  if (typeof str !== "string") {
    console.error(`The argument must be a string.`);
    return str;
  }
  if (replaceInvisible) {
    str = str.replace(InvisibleCharactersRegExp, " ");
  }
  return str.replace(NullCharactersRegExp, "");
}

/**
 * Use binary search to find the index of the first item in a given array which
 * passes a given condition. The items are expected to be sorted in the sense
 * that if the condition is true for one item in the array, then it is also true
 * for all following items.
 *
 * @returns {number} Index of the first array element to pass the test,
 *                   or |items.length| if no such element exists.
 */
function binarySearchFirstItem(items, condition, start = 0) {
  let minIndex = start;
  let maxIndex = items.length - 1;

  if (maxIndex < 0 || !condition(items[maxIndex])) {
    return items.length;
  }
  if (condition(items[minIndex])) {
    return minIndex;
  }

  while (minIndex < maxIndex) {
    const currentIndex = (minIndex + maxIndex) >> 1;
    const currentItem = items[currentIndex];
    if (condition(currentItem)) {
      maxIndex = currentIndex;
    } else {
      minIndex = currentIndex + 1;
    }
  }
  return minIndex; /* === maxIndex */
}

/**
 *  Approximates float number as a fraction using Farey sequence (max order
 *  of 8).
 *  @param {number} x - Positive float number.
 *  @returns {Array} Estimated fraction: the first array item is a numerator,
 *                   the second one is a denominator.
 */
function approximateFraction(x) {
  // Fast paths for int numbers or their inversions.
  if (Math.floor(x) === x) {
    return [x, 1];
  }
  const xinv = 1 / x;
  const limit = 8;
  if (xinv > limit) {
    return [1, limit];
  } else if (Math.floor(xinv) === xinv) {
    return [1, xinv];
  }

  const x_ = x > 1 ? xinv : x;
  // a/b and c/d are neighbours in Farey sequence.
  let a = 0,
    b = 1,
    c = 1,
    d = 1;
  // Limiting search to order 8.
  while (true) {
    // Generating next term in sequence (order of q).
    const p = a + c,
      q = b + d;
    if (q > limit) {
      break;
    }
    if (x_ <= p / q) {
      c = p;
      d = q;
    } else {
      a = p;
      b = q;
    }
  }
  let result;
  // Select closest of the neighbours to x.
  if (x_ - a / b < c / d - x_) {
    result = x_ === x ? [a, b] : [b, a];
  } else {
    result = x_ === x ? [c, d] : [d, c];
  }
  return result;
}

function roundToDivide(x, div) {
  const r = x % div;
  return r === 0 ? x : Math.round(x - r + div);
}

/**
 * @typedef {Object} GetPageSizeInchesParameters
 * @property {number[]} view
 * @property {number} userUnit
 * @property {number} rotate
 */

/**
 * @typedef {Object} PageSize
 * @property {number} width - In inches.
 * @property {number} height - In inches.
 */

/**
 * Gets the size of the specified page, converted from PDF units to inches.
 * @param {GetPageSizeInchesParameters} params
 * @returns {PageSize}
 */
function getPageSizeInches({ view, userUnit, rotate }) {
  const [x1, y1, x2, y2] = view;
  // We need to take the page rotation into account as well.
  const changeOrientation = rotate % 180 !== 0;

  const width = ((x2 - x1) / 72) * userUnit;
  const height = ((y2 - y1) / 72) * userUnit;

  return {
    width: changeOrientation ? height : width,
    height: changeOrientation ? width : height,
  };
}

/**
 * Helper function for getVisibleElements.
 *
 * @param {number} index - initial guess at the first visible element
 * @param {Array} views - array of pages, into which `index` is an index
 * @param {number} top - the top of the scroll pane
 * @returns {number} less than or equal to `index` that is definitely at or
 *   before the first visible element in `views`, but not by too much. (Usually,
 *   this will be the first element in the first partially visible row in
 *   `views`, although sometimes it goes back one row further.)
 */
function backtrackBeforeAllVisibleElements(index, views, top) {
  // binarySearchFirstItem's assumption is that the input is ordered, with only
  // one index where the conditions flips from false to true: [false ...,
  // true...]. With vertical scrolling and spreads, it is possible to have
  // [false ..., true, false, true ...]. With wrapped scrolling we can have a
  // similar sequence, with many more mixed true and false in the middle.
  //
  // So there is no guarantee that the binary search yields the index of the
  // first visible element. It could have been any of the other visible elements
  // that were preceded by a hidden element.

  // Of course, if either this element or the previous (hidden) element is also
  // the first element, there's nothing to worry about.
  if (index < 2) {
    return index;
  }

  // That aside, the possible cases are represented below.
  //
  //     ****  = fully hidden
  //     A*B*  = mix of partially visible and/or hidden pages
  //     CDEF  = fully visible
  //
  // (1) Binary search could have returned A, in which case we can stop.
  // (2) Binary search could also have returned B, in which case we need to
  // check the whole row.
  // (3) Binary search could also have returned C, in which case we need to
  // check the whole previous row.
  //
  // There's one other possibility:
  //
  //     ****  = fully hidden
  //     ABCD  = mix of fully and/or partially visible pages
  //
  // (4) Binary search could only have returned A.

  // Initially assume that we need to find the beginning of the current row
  // (case 1, 2, or 4), which means finding a page that is above the current
  // page's top. If the found page is partially visible, we're definitely not in
  // case 3, and this assumption is correct.
  let elt = views[index].div;
  let pageTop = elt.offsetTop + elt.clientTop;

  if (pageTop >= top) {
    // The found page is fully visible, so we're actually either in case 3 or 4,
    // and unfortunately we can't tell the difference between them without
    // scanning the entire previous row, so we just conservatively assume that
    // we do need to backtrack to that row. In both cases, the previous page is
    // in the previous row, so use its top instead.
    elt = views[index - 1].div;
    pageTop = elt.offsetTop + elt.clientTop;
  }

  // Now we backtrack to the first page that still has its bottom below
  // `pageTop`, which is the top of a page in the first visible row (unless
  // we're in case 4, in which case it's the row before that).
  // `index` is found by binary search, so the page at `index - 1` is
  // invisible and we can start looking for potentially visible pages from
  // `index - 2`. (However, if this loop terminates on its first iteration,
  // which is the case when pages are stacked vertically, `index` should remain
  // unchanged, so we use a distinct loop variable.)
  for (let i = index - 2; i >= 0; --i) {
    elt = views[i].div;
    if (elt.offsetTop + elt.clientTop + elt.clientHeight <= pageTop) {
      // We have reached the previous row, so stop now.
      // This loop is expected to terminate relatively quickly because the
      // number of pages per row is expected to be small.
      break;
    }
    index = i;
  }
  return index;
}

/**
 * @typedef {Object} GetVisibleElementsParameters
 * @property {HTMLElement} scrollEl - A container that can possibly scroll.
 * @property {Array} views - Objects with a `div` property that contains an
 *   HTMLElement, which should all be descendants of `scrollEl` satisfying the
 *   relevant layout assumptions.
 * @property {boolean} sortByVisibility - If `true`, the returned elements are
 *   sorted in descending order of the percent of their padding box that is
 *   visible. The default value is `false`.
 * @property {boolean} horizontal - If `true`, the elements are assumed to be
 *   laid out horizontally instead of vertically. The default value is `false`.
 * @property {boolean} rtl - If `true`, the `scrollEl` container is assumed to
 *   be in right-to-left mode. The default value is `false`.
 */

/**
 * Generic helper to find out what elements are visible within a scroll pane.
 *
 * Well, pretty generic. There are some assumptions placed on the elements
 * referenced by `views`:
 *   - If `horizontal`, no left of any earlier element is to the right of the
 *     left of any later element.
 *   - Otherwise, `views` can be split into contiguous rows where, within a row,
 *     no top of any element is below the bottom of any other element, and
 *     between rows, no bottom of any element in an earlier row is below the
 *     top of any element in a later row.
 *
 * (Here, top, left, etc. all refer to the padding edge of the element in
 * question. For pages, that ends up being equivalent to the bounding box of the
 * rendering canvas. Earlier and later refer to index in `views`, not page
 * layout.)
 *
 * @param {GetVisibleElementsParameters}
 * @returns {Object} `{ first, last, views: [{ id, x, y, view, percent }] }`
 */
function getVisibleElements({
  scrollEl,
  views,
  sortByVisibility = false,
  horizontal = false,
  rtl = false,
}) {
  const top = scrollEl.scrollTop,
    bottom = top + scrollEl.clientHeight;
  const left = scrollEl.scrollLeft,
    right = left + scrollEl.clientWidth;

  // Throughout this "generic" function, comments will assume we're working with
  // PDF document pages, which is the most important and complex case. In this
  // case, the visible elements we're actually interested is the page canvas,
  // which is contained in a wrapper which adds no padding/border/margin, which
  // is itself contained in `view.div` which adds no padding (but does add a
  // border). So, as specified in this function's doc comment, this function
  // does all of its work on the padding edge of the provided views, starting at
  // offsetLeft/Top (which includes margin) and adding clientLeft/Top (which is
  // the border). Adding clientWidth/Height gets us the bottom-right corner of
  // the padding edge.
  function isElementBottomAfterViewTop(view) {
    const element = view.div;
    const elementBottom =
      element.offsetTop + element.clientTop + element.clientHeight;
    return elementBottom > top;
  }
  function isElementNextAfterViewHorizontally(view) {
    const element = view.div;
    const elementLeft = element.offsetLeft + element.clientLeft;
    const elementRight = elementLeft + element.clientWidth;
    return rtl ? elementLeft < right : elementRight > left;
  }

  let visible = []
  const ids = new Set(),
    numViews = views.length;
  let firstVisibleElementInd = binarySearchFirstItem(
    views,
    horizontal
      ? isElementNextAfterViewHorizontally
      : isElementBottomAfterViewTop
  );

  // Please note the return value of the `binarySearchFirstItem` function when
  // no valid element is found (hence the `firstVisibleElementInd` check below).
  if (
    firstVisibleElementInd > 0 &&
    firstVisibleElementInd < numViews &&
    !horizontal
  ) {
    // In wrapped scrolling (or vertical scrolling with spreads), with some page
    // sizes, isElementBottomAfterViewTop doesn't satisfy the binary search
    // condition: there can be pages with bottoms above the view top between
    // pages with bottoms below. This function detects and corrects that error;
    // see it for more comments.
    firstVisibleElementInd = backtrackBeforeAllVisibleElements(
      firstVisibleElementInd,
      views,
      top
    );
  }

  // lastEdge acts as a cutoff for us to stop looping, because we know all
  // subsequent pages will be hidden.
  //
  // When using wrapped scrolling or vertical scrolling with spreads, we can't
  // simply stop the first time we reach a page below the bottom of the view;
  // the tops of subsequent pages on the same row could still be visible. In
  // horizontal scrolling, we don't have that issue, so we can stop as soon as
  // we pass `right`, without needing the code below that handles the -1 case.
  let lastEdge = horizontal ? right : -1;

  for (let i = firstVisibleElementInd; i < numViews; i++) {
    const view = views[i],
      element = view.div;
    const currentWidth = element.offsetLeft + element.clientLeft;
    const currentHeight = element.offsetTop + element.clientTop;
    const viewWidth = element.clientWidth,
      viewHeight = element.clientHeight;
    const viewRight = currentWidth + viewWidth;
    const viewBottom = currentHeight + viewHeight;

    if (lastEdge === -1) {
      // As commented above, this is only needed in non-horizontal cases.
      // Setting lastEdge to the bottom of the first page that is partially
      // visible ensures that the next page fully below lastEdge is on the
      // next row, which has to be fully hidden along with all subsequent rows.
      if (viewBottom >= bottom) {
        lastEdge = viewBottom;
      }
    } else if ((horizontal ? currentWidth : currentHeight) > lastEdge) {
      break;
    }

    if (
      viewBottom <= top ||
      currentHeight >= bottom ||
      viewRight <= left ||
      currentWidth >= right
    ) {
      continue;
    }

    const hiddenHeight =
      Math.max(0, top - currentHeight) + Math.max(0, viewBottom - bottom);
    const hiddenWidth =
      Math.max(0, left - currentWidth) + Math.max(0, viewRight - right);

    const fractionHeight = (viewHeight - hiddenHeight) / viewHeight,
      fractionWidth = (viewWidth - hiddenWidth) / viewWidth;
    const percent = (fractionHeight * fractionWidth * 100) | 0;

    visible.push({
      id: view.id,
      x: currentWidth,
      y: currentHeight,
      view,
      percent,
      widthPercent: (fractionWidth * 100) | 0,
    });
    ids.add(view.id);
  }

  const first = visible[0],
    last = visible.at(-1);

  if (sortByVisibility) {
    visible.sort(function (a, b) {
      const pc = a.percent - b.percent;
      if (Math.abs(pc) > 0.001) {
        return -pc;
      }
      return a.id - b.id; // ensure stability
    });
  }

  getExpandKeepRange(visible, ids, views, 2)

  return { first, last, views: visible, ids };
}

function getExpandKeepRange (visible, ids, views, size) {
  if (!visible.length) return
  if (visible) {
    const firstPageIndex = visible[0].view.pageIndex
    const endPageIndex = visible[visible.length - 1].view.pageIndex
    let start = Math.min(firstPageIndex, endPageIndex)
    let end = Math.max(firstPageIndex, endPageIndex)
    const pagesCount = views.length
    if (start > 1) {
      for (let i = Math.max(1, start - size); i < start && i < pagesCount; i++) {
        const view = views[i]
        visible.push({
          id: view.id,
          view,
          percent: 0,
          widthPercent: 0,
        })
        ids.add(view.id);
      }
    }
    if (end < pagesCount - 1) {
      for (let i = end + 1; i < pagesCount && i <= end + size; i++) {
        const view = views[i]
        visible.push({
          id: view.id,
          view,
          percent: 0,
          widthPercent: 0,
        })
        ids.add(view.id);
      }
    }
  }
}

/**
 * Event handler to suppress context menu.
 */
function noContextMenuHandler(evt) {
  evt.preventDefault();
}

function normalizeWheelEventDirection(evt) {
  let delta = Math.hypot(evt.deltaX, evt.deltaY);
  const angle = Math.atan2(evt.deltaY, evt.deltaX);
  if (-0.25 * Math.PI < angle && angle < 0.75 * Math.PI) {
    // All that is left-up oriented has to change the sign.
    delta = -delta;
  }
  return delta;
}

function normalizeWheelEventDelta(evt) {
  let delta = normalizeWheelEventDirection(evt);

  const MOUSE_DOM_DELTA_PIXEL_MODE = 0;
  const MOUSE_DOM_DELTA_LINE_MODE = 1;
  const MOUSE_PIXELS_PER_LINE = 30;
  const MOUSE_LINES_PER_PAGE = 30;

  // Converts delta to per-page units
  if (evt.deltaMode === MOUSE_DOM_DELTA_PIXEL_MODE) {
    delta /= MOUSE_PIXELS_PER_LINE * MOUSE_LINES_PER_PAGE;
  } else if (evt.deltaMode === MOUSE_DOM_DELTA_LINE_MODE) {
    delta /= MOUSE_LINES_PER_PAGE;
  }
  return delta;
}

function isValidRotation(angle) {
  return Number.isInteger(angle) && angle % 90 === 0;
}

function isValidScrollMode(mode) {
  return (
    Number.isInteger(mode) &&
    Object.values(ScrollMode).includes(mode) &&
    mode !== ScrollMode.UNKNOWN
  );
}

function waitOnEventOrTimeout({ target, name, delay = 0 }) {
  return new Promise(function (resolve, reject) {
    if (
      typeof target !== "object" ||
      !(name && typeof name === "string") ||
      !(Number.isInteger(delay) && delay >= 0)
    ) {
      throw new Error("waitOnEventOrTimeout - invalid parameters.");
    }

    function handler(type) {
      if (target instanceof EventBus) {
        target._off(name, eventHandler);
      } else {
        target.removeEventListener(name, eventHandler);
      }

      if (timeout) {
        clearTimeout(timeout);
      }
      resolve(type);
    }

    const eventHandler = handler.bind(null, WaitOnType.EVENT);
    if (target instanceof EventBus) {
      target._on(name, eventHandler);
    } else {
      target.addEventListener(name, eventHandler);
    }

    const timeoutHandler = handler.bind(null, WaitOnType.TIMEOUT);
    const timeout = setTimeout(timeoutHandler, delay);
  });
}

function isValidSpreadMode(mode) {
  return (
    Number.isInteger(mode) &&
    Object.values(SpreadMode).includes(mode) &&
    mode !== SpreadMode.UNKNOWN
  );
}

function isPortraitOrientation(size) {
  return size.width <= size.height;
}

const docStyle =
  typeof document === "undefined"
    ? null
    : document.documentElement.style;

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

class ProgressBar {
  #classList = null;

  #percent = 0;

  #visible = true;

  constructor(id) {
    const bar = document.getElementById(id);
    this.#classList = bar.classList;
  }

  get percent() {
    return this.#percent;
  }

  set percent(val) {
    this.#percent = clamp(val, 0, 100);

    if (isNaN(val)) {
      this.#classList.add("indeterminate");
      return;
    }
    this.#classList.remove("indeterminate");

    docStyle.setProperty("--progressBar-percent", `${this.#percent}%`);
  }

  setWidth(viewer) {
    if (!viewer) {
      return;
    }
    const container = viewer.parentNode;
    const scrollbarWidth = container.offsetWidth - viewer.offsetWidth;
    if (scrollbarWidth > 0) {
      docStyle.setProperty("--progressBar-end-offset", `${scrollbarWidth}px`);
    }
  }

  hide() {
    if (!this.#visible) {
      return;
    }
    this.#visible = false;
    this.#classList.add("hidden");
  }

  show() {
    if (this.#visible) {
      return;
    }
    this.#visible = true;
    this.#classList.remove("hidden");
  }
}

/**
 * Get the active or focused element in current DOM.
 *
 * Recursively search for the truly active or focused element in case there are
 * shadow DOMs.
 *
 * @returns {Element} the truly active or focused element.
 */
function getActiveOrFocusedElement() {
  let curRoot = document;
  let curActiveOrFocused =
    curRoot.activeElement || curRoot.querySelector(":focus");

  while (curActiveOrFocused?.shadowRoot) {
    curRoot = curActiveOrFocused.shadowRoot;
    curActiveOrFocused =
      curRoot.activeElement || curRoot.querySelector(":focus");
  }

  return curActiveOrFocused;
}

/**
 * Converts API PageLayout values to the format used by `BaseViewer`.
 * NOTE: This is supported to the extent that the viewer implements the
 *       necessary Scroll/Spread modes (since SinglePage, TwoPageLeft,
 *       and TwoPageRight all suggests using non-continuous scrolling).
 * @param {string} mode - The API PageLayout value.
 * @returns {Object}
 */
function apiPageLayoutToViewerModes(layout) {
  let scrollMode = ScrollMode.VERTICAL,
    spreadMode = SpreadMode.NONE;

  switch (layout) {
    case "SinglePage":
      scrollMode = ScrollMode.PAGE;
      break;
    case "OneColumn":
      break;
    case "TwoPageLeft":
      scrollMode = ScrollMode.PAGE;
    /* falls through */
    case "TwoColumnLeft":
      spreadMode = SpreadMode.ODD;
      break;
    case "TwoPageRight":
      scrollMode = ScrollMode.PAGE;
    /* falls through */
    case "TwoColumnRight":
      spreadMode = SpreadMode.EVEN;
      break;
  }
  return { scrollMode, spreadMode };
}

/**
 * Converts API PageMode values to the format used by `PDFSidebar`.
 * NOTE: There's also a "FullScreen" parameter which is not possible to support,
 *       since the Fullscreen API used in browsers requires that entering
 *       fullscreen mode only occurs as a result of a user-initiated event.
 * @param {string} mode - The API PageMode value.
 * @returns {number} A value from {SidebarView}.
 */
function apiPageModeToSidebarView(mode) {
  switch (mode) {
    case "UseNone":
      return SidebarView.NONE;
    case "UseThumbs":
      return SidebarView.THUMBS;
    case "UseOutlines":
      return SidebarView.OUTLINE;
    case "UseAttachments":
      return SidebarView.ATTACHMENTS;
    case "UseOC":
      return SidebarView.LAYERS;
  }
  return SidebarView.NONE; // Default value.
}

const xmlParser = (xmlString) => {
  const parser = new window.DOMParser();
  const xmlDoc = parser.parseFromString(xmlString, 'text/xml');
  return xmlDoc;
};

const getElementsByTagName = (
  elements,
  tagname,
) => {
  const arr = Array.prototype.slice.call(elements.childNodes);
  let element = null;

  arr.forEach((ele) => {
    if (ele.tagName === tagname) {
      element = ele.childNodes;
    }
  });
  return element;
};

function bindEvents(obj, element, names) {
  for (const name of names) {
    element.addEventListener(name, obj[name].bind(obj));
  }
}

function getFontAttribute(element) {
  let font = null
  const nodes = Array.prototype.slice.call(element.childNodes);
  nodes.forEach((ele) => {
    if (ele.tagName === 'defaultappearance') {
      const appearanceString = ele.innerHTML || ele.textContent || '';
      const arr = appearanceString.split(' ');

      font = {
        'font-size': parseInt(arr[5], 10),
        'font-name': arr[4].substr(1),
        'font-color': rgbToHex(
          parseFloat(arr[0]),
          parseFloat(arr[1]),
          parseFloat(arr[2]),
        ),
      }
    } else if (ele.tagName === 'contents-richtext') {
      let richtext = ele.innerHTML || ele.textContent || ''
      richtext = richtext.replaceAll("&lt;", "<")
        .replaceAll("&gt;", ">")
        .replaceAll("&quot;", '"')
        .replaceAll("&#39;", "'")
        .replaceAll("&amp;", "&");
      
      const parser = new window.DOMParser();
      const xmlDoc = parser.parseFromString(richtext, 'text/xml');
      const bodyElement = xmlDoc.getElementsByTagName("body")[0];

      // 获取 style 属性的值
      const styleAttribute = bodyElement.getAttribute("style");

      // 解析 style 属性中的样式信息
      const styleObject = {};
      styleAttribute.split(";").forEach(style => {
        const [key, value] = style.split(":").map(s => s.trim());
        if (key && value) {
          styleObject[key] = value;
        }
      })
    }
  })

  return font && {
    fontName: font['font-name'],
    fontSize: parseFloat(font['font-size']),
    fontColor: font.color
  }
};

const ANNOTATION_TYPE = {
  ink: 'ink',
  freetext: 'freetext',
  text: 'text',
  square: 'square',
  circle: 'circle',
  line: 'line',
  arrow: 'line',
  image: 'image',
  highlight: 'highlight',
  underline: 'underline',
  strikeout: 'strikeout',
  squiggly: 'squiggly',
  stamp: 'stamp',
  redact: 'redact',
};

const findIndex = (name, items) => {
  const index = items.findIndex((item) => {
    return item?.name === name
  })
  return index
}

const parseAnnotationFromXml = (xmlString) => {
  if (!xmlString) return [];
  const xmlDoc = xmlParser(xmlString);
  const elements = xmlDoc.firstElementChild || xmlDoc.firstChild;
  const element = getElementsByTagName(elements, 'annots') || [];
  const annotations = Array.prototype.slice.call(element);
  const annotationsArray = [];
  for (let i = 0; i < annotations.length; i++) {
    const cur = annotations[i]
    const type = ANNOTATION_TYPE[cur.tagName];

    const attributes = cur.attributes;

    if (type) {
      const annotation = {
        date: attributes.date ? attributes.date.value : (attributes.creationdate ? attributes.creationdate.value : undefined),
        color: attributes.color ? attributes.color.value : undefined,
        flags: attributes.flags ? attributes.flags.value : undefined,
        rect: attributes.rect ? attributes.rect.value : undefined,
        name: attributes.name ? attributes.name.value : uuidv4(),
        width: attributes.width ? attributes.width.value : undefined,
        'interior-color': attributes['interior-color'] ? attributes['interior-color'].value : undefined,
        page: attributes.page ? attributes.page.value : undefined,
        opacity: attributes.opacity ? attributes.opacity.value : 1,
        type
      }
      if (annotation.flags && annotation.flags.indexOf('hidden') > -1) {
        annotation.isHidden = 1
      }
      const markupType = ['highlight', 'underline', 'strikeout', 'squiggly']
      if (markupType.includes(type)) {
        const content = getContent(cur)
        if (content) {
          annotation.content = content
        }
        annotation.coords = attributes.coords ? attributes.coords.value : undefined
      }
      if (type === 'line') {
        if (!annotation.rect) continue
        
        annotation.start = attributes.start ? attributes.start.value : undefined
        annotation.end = attributes.end ? attributes.end.value : undefined
      }
      if (type === 'line' && attributes.tail && attributes.tail.value === 'OpenArrow') {
        annotation.tail = 'OpenArrow'
        annotation.head = 'None'
      }
      if (type === 'square' || type === 'circle' || type === 'text' || type === 'line') {
        if (!annotation.rect) continue
      }
      if (type === 'ink') {
        const gesture = getPosition(cur)
        if (!gesture) continue
        !annotation.inklist && (annotation.inklist = {})
        annotation.inklist.gesture = gesture
      }

      if (type === 'text' || type === 'freetext') {
        if (!annotation.rect) continue
        const content = getContent(cur)
        if (content) {
          annotation.content = content
        }
      }
      if (type === 'freetext') {
        annotation.justification = attributes.justification ? attributes.justification.value : undefined
        const font = getFontAttribute(cur)
        if (font) {
          annotation.textStyle = font
        }
      }
      if (type === 'stamp') {
        // 图片签名不显示
        if (!attributes['stamp-type'] || attributes['stamp-type'].value === 'image') {
          continue
        }
        annotation['stamp-type'] = attributes['stamp-type'] ? attributes['stamp-type'].value : undefined
        annotation.icon = attributes.icon ? attributes.icon.value : undefined
      }
      annotationsArray.push(annotation)
    }
  }
  return annotationsArray;
}

const getPosition = (element) => {
  let gesture = [];
  const nodes = element.children[0].children;
  const nodeList = Array.prototype.slice.call(nodes);
  nodeList.forEach((ele) => {
    if (ele.tagName === 'gesture') {
      gesture.push(ele.innerHTML || ele.textContent || '');
    }
  });
  if (gesture.length === 0) {
    gesture = ''
  }

  return gesture;
}

const getContent = (element) => {
  let content = '';
  const nodes = Array.prototype.slice.call(element.childNodes);

  nodes.forEach((ele) => {
    if (ele.tagName === 'contents') {
      content = ele.innerHTML || ele.textContent || '';
    } else if (ele.tagName === 'contents-richtext') {
      let richtext = ele.innerHTML || ele.textContent || ''
      richtext = richtext.replaceAll("&lt;", "<")
        .replaceAll("&gt;", ">")
        .replaceAll("&quot;", '"')
        .replaceAll("&#39;", "'")
        .replaceAll("&amp;", "&");
      const parser = new window.DOMParser();
      const xmlDoc = parser.parseFromString(richtext, 'text/xml');
      content = xmlDoc.firstElementChild.innerText
    }
  });
  return content;
};


function unreachable(msg) {
  throw new Error(msg);
}

function assert(cond, msg) {
  if (!cond) {
    unreachable(msg);
  }
}

class FeatureTest {
  static get isLittleEndian() {
    return shadow(this, "isLittleEndian", isLittleEndian());
  }

  static get isEvalSupported() {
    return shadow(this, "isEvalSupported", isEvalSupported());
  }

  static get isOffscreenCanvasSupported() {
    return shadow(
      this,
      "isOffscreenCanvasSupported",
      typeof OffscreenCanvas !== "undefined"
    );
  }

  static get platform() {
    if (
      (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) &&
      typeof navigator === "undefined"
    ) {
      return shadow(this, "platform", { isWin: false, isMac: false });
    }
    return shadow(this, "platform", {
      isWin: navigator.platform.includes("Win"),
      isMac: navigator.platform.includes("Mac"),
    });
  }
}

function shadow(obj, prop, value, nonSerializable = false) {
  if (
    typeof PDFJSDev === "undefined" ||
    PDFJSDev.test("!PRODUCTION || TESTING")
  ) {
    assert(
      prop in obj,
      `shadow: Property "${prop && prop.toString()}" not found in object.`
    );
  }
  Object.defineProperty(obj, prop, {
    value,
    enumerable: !nonSerializable,
    configurable: true,
    writable: false,
  });
  return value;
}



class Util {
  static makeHexColor(r, g, b) {
    return `#${hexNumbers[r]}${hexNumbers[g]}${hexNumbers[b]}`;
  }

  // Apply a scaling matrix to some min/max values.
  // If a scaling factor is negative then min and max must be
  // swaped.
  static scaleMinMax(transform, minMax) {
    let temp;
    if (transform[0]) {
      if (transform[0] < 0) {
        temp = minMax[0];
        minMax[0] = minMax[1];
        minMax[1] = temp;
      }
      minMax[0] *= transform[0];
      minMax[1] *= transform[0];

      if (transform[3] < 0) {
        temp = minMax[2];
        minMax[2] = minMax[3];
        minMax[3] = temp;
      }
      minMax[2] *= transform[3];
      minMax[3] *= transform[3];
    } else {
      temp = minMax[0];
      minMax[0] = minMax[2];
      minMax[2] = temp;
      temp = minMax[1];
      minMax[1] = minMax[3];
      minMax[3] = temp;

      if (transform[1] < 0) {
        temp = minMax[2];
        minMax[2] = minMax[3];
        minMax[3] = temp;
      }
      minMax[2] *= transform[1];
      minMax[3] *= transform[1];

      if (transform[2] < 0) {
        temp = minMax[0];
        minMax[0] = minMax[1];
        minMax[1] = temp;
      }
      minMax[0] *= transform[2];
      minMax[1] *= transform[2];
    }
    minMax[0] += transform[4];
    minMax[1] += transform[4];
    minMax[2] += transform[5];
    minMax[3] += transform[5];
  }

  // Concatenates two transformation matrices together and returns the result.
  static transform(m1, m2) {
    return [
      m1[0] * m2[0] + m1[2] * m2[1],
      m1[1] * m2[0] + m1[3] * m2[1],
      m1[0] * m2[2] + m1[2] * m2[3],
      m1[1] * m2[2] + m1[3] * m2[3],
      m1[0] * m2[4] + m1[2] * m2[5] + m1[4],
      m1[1] * m2[4] + m1[3] * m2[5] + m1[5],
    ];
  }

  // For 2d affine transforms
  static applyTransform(p, m) {
    const xt = p[0] * m[0] + p[1] * m[2] + m[4];
    const yt = p[0] * m[1] + p[1] * m[3] + m[5];
    return [xt, yt];
  }

  static applyInverseTransform(p, m) {
    const d = m[0] * m[3] - m[1] * m[2];
    const xt = (p[0] * m[3] - p[1] * m[2] + m[2] * m[5] - m[4] * m[3]) / d;
    const yt = (-p[0] * m[1] + p[1] * m[0] + m[4] * m[1] - m[5] * m[0]) / d;
    return [xt, yt];
  }

  // Applies the transform to the rectangle and finds the minimum axially
  // aligned bounding box.
  static getAxialAlignedBoundingBox(r, m) {
    const p1 = Util.applyTransform(r, m);
    const p2 = Util.applyTransform(r.slice(2, 4), m);
    const p3 = Util.applyTransform([r[0], r[3]], m);
    const p4 = Util.applyTransform([r[2], r[1]], m);
    return [
      Math.min(p1[0], p2[0], p3[0], p4[0]),
      Math.min(p1[1], p2[1], p3[1], p4[1]),
      Math.max(p1[0], p2[0], p3[0], p4[0]),
      Math.max(p1[1], p2[1], p3[1], p4[1]),
    ];
  }

  static inverseTransform(m) {
    const d = m[0] * m[3] - m[1] * m[2];
    return [
      m[3] / d,
      -m[1] / d,
      -m[2] / d,
      m[0] / d,
      (m[2] * m[5] - m[4] * m[3]) / d,
      (m[4] * m[1] - m[5] * m[0]) / d,
    ];
  }

  // This calculation uses Singular Value Decomposition.
  // The SVD can be represented with formula A = USV. We are interested in the
  // matrix S here because it represents the scale values.
  static singularValueDecompose2dScale(m) {
    const transpose = [m[0], m[2], m[1], m[3]];

    // Multiply matrix m with its transpose.
    const a = m[0] * transpose[0] + m[1] * transpose[2];
    const b = m[0] * transpose[1] + m[1] * transpose[3];
    const c = m[2] * transpose[0] + m[3] * transpose[2];
    const d = m[2] * transpose[1] + m[3] * transpose[3];

    // Solve the second degree polynomial to get roots.
    const first = (a + d) / 2;
    const second = Math.sqrt((a + d) ** 2 - 4 * (a * d - c * b)) / 2;
    const sx = first + second || 1;
    const sy = first - second || 1;

    // Scale values are the square roots of the eigenvalues.
    return [Math.sqrt(sx), Math.sqrt(sy)];
  }

  // Normalize rectangle rect=[x1, y1, x2, y2] so that (x1,y1) < (x2,y2)
  // For coordinate systems whose origin lies in the bottom-left, this
  // means normalization to (BL,TR) ordering. For systems with origin in the
  // top-left, this means (TL,BR) ordering.
  static normalizeRect(rect) {
    const r = rect.slice(0); // clone rect
    if (rect[0] > rect[2]) {
      r[0] = rect[2];
      r[2] = rect[0];
    }
    if (rect[1] > rect[3]) {
      r[1] = rect[3];
      r[3] = rect[1];
    }
    return r;
  }

  // Returns a rectangle [x1, y1, x2, y2] corresponding to the
  // intersection of rect1 and rect2. If no intersection, returns 'null'
  // The rectangle coordinates of rect1, rect2 should be [x1, y1, x2, y2]
  static intersect(rect1, rect2) {
    const xLow = Math.max(
      Math.min(rect1[0], rect1[2]),
      Math.min(rect2[0], rect2[2])
    );
    const xHigh = Math.min(
      Math.max(rect1[0], rect1[2]),
      Math.max(rect2[0], rect2[2])
    );
    if (xLow > xHigh) {
      return null;
    }
    const yLow = Math.max(
      Math.min(rect1[1], rect1[3]),
      Math.min(rect2[1], rect2[3])
    );
    const yHigh = Math.min(
      Math.max(rect1[1], rect1[3]),
      Math.max(rect2[1], rect2[3])
    );
    if (yLow > yHigh) {
      return null;
    }

    return [xLow, yLow, xHigh, yHigh];
  }

  // From https://github.com/adobe-webplatform/Snap.svg/blob/b365287722a72526000ac4bfcf0ce4cac2faa015/src/path.js#L852
  static bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3) {
    const tvalues = [],
      bounds = [[], []];
    let a, b, c, t, t1, t2, b2ac, sqrtb2ac;
    for (let i = 0; i < 2; ++i) {
      if (i === 0) {
        b = 6 * x0 - 12 * x1 + 6 * x2;
        a = -3 * x0 + 9 * x1 - 9 * x2 + 3 * x3;
        c = 3 * x1 - 3 * x0;
      } else {
        b = 6 * y0 - 12 * y1 + 6 * y2;
        a = -3 * y0 + 9 * y1 - 9 * y2 + 3 * y3;
        c = 3 * y1 - 3 * y0;
      }
      if (Math.abs(a) < 1e-12) {
        if (Math.abs(b) < 1e-12) {
          continue;
        }
        t = -c / b;
        if (0 < t && t < 1) {
          tvalues.push(t);
        }
        continue;
      }
      b2ac = b * b - 4 * c * a;
      sqrtb2ac = Math.sqrt(b2ac);
      if (b2ac < 0) {
        continue;
      }
      t1 = (-b + sqrtb2ac) / (2 * a);
      if (0 < t1 && t1 < 1) {
        tvalues.push(t1);
      }
      t2 = (-b - sqrtb2ac) / (2 * a);
      if (0 < t2 && t2 < 1) {
        tvalues.push(t2);
      }
    }

    let j = tvalues.length,
      mt;
    const jlen = j;
    while (j--) {
      t = tvalues[j];
      mt = 1 - t;
      bounds[0][j] =
        mt * mt * mt * x0 +
        3 * mt * mt * t * x1 +
        3 * mt * t * t * x2 +
        t * t * t * x3;
      bounds[1][j] =
        mt * mt * mt * y0 +
        3 * mt * mt * t * y1 +
        3 * mt * t * t * y2 +
        t * t * t * y3;
    }

    bounds[0][jlen] = x0;
    bounds[1][jlen] = y0;
    bounds[0][jlen + 1] = x3;
    bounds[1][jlen + 1] = y3;
    bounds[0].length = bounds[1].length = jlen + 2;

    return [
      Math.min(...bounds[0]),
      Math.min(...bounds[1]),
      Math.max(...bounds[0]),
      Math.max(...bounds[1]),
    ];
  }

  static getBoundingBox(x0, y0, x1, y1) {
    const width = Math.abs(x0 - x1)
    const height = Math.abs(y0 - y1)
    return [width, height]
  }
}

function getColorFormat(colorString) {
  if (colorString.startsWith("#")) {
    return "hex";
  } else if (colorString.startsWith("rgb(")) {
    return "rgb";
  } else if (colorString.startsWith("hsl")) {
    return "hsl";
  } else {
    return "unknown";
  }
}

// 返回的RGB值范围：0-1
function convertColorToCppFormat (value) {
  const format = getColorFormat(value)
  if (format === 'hex') {
    const { R, G, B } = hexToRgb(value)
    return {
      R: roundToDecimalPlaces(R / 255),
      G: roundToDecimalPlaces(G / 255),
      B: roundToDecimalPlaces(B / 255)
    }
  } else if (format === 'rgb') {
    const match = value.match(/(\d+),\s*(\d+),\s*(\d+)/)
    const R = roundToDecimalPlaces(parseInt(match[1]) / 255)
    const G = roundToDecimalPlaces(parseInt(match[2]) / 255)
    const B = roundToDecimalPlaces(parseInt(match[3]) / 255)
    return {
      R,
      G,
      B
    }
  } else if (format === 'hsl') {
    const match = value.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/)
    if (!match) return { error: 'Invalid HSL format' }
    const H = parseInt(match[1])
    const S = parseInt(match[2]) / 100
    const L = parseInt(match[3]) / 100
    const { R, G, B } = hslToRgb(H, S, L)
    return {
      R: roundToDecimalPlaces(R / 255),
      G: roundToDecimalPlaces(G / 255),
      B: roundToDecimalPlaces(B / 255)
    }
  } else {
    console.error({
      message: 'Invalid color value'
    })
  }
}

// 返回的RGB值范围：0-255
function convertColorToRGB (value) {
  const format = getColorFormat(value)

  if (format === 'hex') {
    return hexToRgb(value)
  } else if (format === 'rgb') {
    const match = value.match(/(\d+),\s*(\d+),\s*(\d+)/)
    if (!match) return { error: 'Invalid RGB format' }
    return {
      R: parseInt(match[1]),
      G: parseInt(match[2]),
      B: parseInt(match[3])
    }
  } else if (format === 'hsl') {
    const match = value.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/)
    if (!match) return { error: 'Invalid HSL format' }
    const H = parseInt(match[1])
    const S = parseInt(match[2]) / 100
    const L = parseInt(match[3]) / 100
    return hslToRgb(H, S, L)
  } else {
    console.error({
      message: 'Invalid color value'
    })
  }
}

function rgbToHex(r, g, b) {
  const hexString = ((1 << 24) | (r << 16) | (g << 8) | b).toString(16).slice(1).toUpperCase()
  return `#${hexString}`;
}

function convertCppRGBToHex (R, G, B) {
  const r = Math.round(R * 255)
  const g = Math.round(G * 255)
  const b = Math.round(B * 255)
  return rgbToHex(r, g, b)
}

const onClickOutside = (el, cb) => {
  const listener = (event) => {
    let clickOutside = true
    if (Array.isArray(el)) {
      el.forEach((item) => {
        if (!item || item.contains(event.target)) {
          clickOutside = false
        }
      })
    } else {
      if (!el || el.contains(event.target)) {
        return
      }
    }
    if (!clickOutside) return
    cb && cb(event)
    document.removeEventListener(isMobileDevice ? 'touchstart' : 'mousedown', listener);
  }

  document.addEventListener(isMobileDevice ? 'touchstart' : 'mousedown', listener);
  return listener;
}

const onClickOutsideUp = (el, cb) => {
  const listener = (event) => {
    let clickOutside = true
    if (Array.isArray(el)) {
      el.forEach((item) => {
        if (!item || item.contains(event.target)) {
          clickOutside = false
        }
      })
    } else {
      if (!el || el.contains(event.target)) {
        return
      }
    }
    if (!clickOutside) return
    cb && cb(event)
    document.removeEventListener(isMobileDevice ? 'touchend' : 'mouseup', listener);
  }

  document.addEventListener(isMobileDevice ? 'touchend' : 'mouseup', listener);

  return listener
}

const pdfDateStringRegex = new RegExp(
  "^D:" + // Prefix (required)
    "(\\d{4})" + // Year (required)
    "(\\d{2})?" + // Month (optional)
    "(\\d{2})?" + // Day (optional)
    "(\\d{2})?" + // Hour (optional)
    "(\\d{2})?" + // Minute (optional)
    "(\\d{2})?" + // Second (optional)
    "([Z|+|-])?" + // Universal time relation (optional)
    "(\\d{2})?" + // Offset hour (optional)
    "'?" + // Splitting apostrophe (optional)
    "(\\d{2})?" + // Offset minute (optional)
    "'?" // Trailing apostrophe (optional)
);

const toDateObject = (input) => {
  if (!input || typeof input !== "string") {
    return new Date();
  }

  // Lazily initialize the regular expression.
  if (!pdfDateStringRegex) {
    pdfDateStringRegex = new RegExp(
      "^D:" + // Prefix (required)
        "(\\d{4})" + // Year (required)
        "(\\d{2})?" + // Month (optional)
        "(\\d{2})?" + // Day (optional)
        "(\\d{2})?" + // Hour (optional)
        "(\\d{2})?" + // Minute (optional)
        "(\\d{2})?" + // Second (optional)
        "([Z|+|-])?" + // Universal time relation (optional)
        "(\\d{2})?" + // Offset hour (optional)
        "'?" + // Splitting apostrophe (optional)
        "(\\d{2})?" + // Offset minute (optional)
        "'?" // Trailing apostrophe (optional)
    );
  }

  // Optional fields that don't satisfy the requirements from the regular
  // expression (such as incorrect digit counts or numbers that are out of
  // range) will fall back the defaults from the specification.
  const matches = pdfDateStringRegex.exec(input);
  if (!matches) {
    return new Date();
  }

  // JavaScript's `Date` object expects the month to be between 0 and 11
  // instead of 1 and 12, so we have to correct for that.
  const year = parseInt(matches[1], 10);
  let month = parseInt(matches[2], 10);
  month = month >= 1 && month <= 12 ? month - 1 : 0;
  let day = parseInt(matches[3], 10);
  day = day >= 1 && day <= 31 ? day : 1;
  let hour = parseInt(matches[4], 10);
  hour = hour >= 0 && hour <= 23 ? hour : 0;
  let minute = parseInt(matches[5], 10);
  minute = minute >= 0 && minute <= 59 ? minute : 0;
  let second = parseInt(matches[6], 10);
  second = second >= 0 && second <= 59 ? second : 0;
  const universalTimeRelation = matches[7] || "Z";
  let offsetHour = parseInt(matches[8], 10);
  offsetHour = offsetHour >= 0 && offsetHour <= 23 ? offsetHour : 0;
  let offsetMinute = parseInt(matches[9], 10) || 0;
  offsetMinute = offsetMinute >= 0 && offsetMinute <= 59 ? offsetMinute : 0;

  // Universal time relation 'Z' means that the local time is equal to the
  // universal time, whereas the relations '+'/'-' indicate that the local
  // time is later respectively earlier than the universal time. Every date
  // is normalized to universal time.
  if (universalTimeRelation === "-") {
    hour += offsetHour;
    minute += offsetMinute;
  } else if (universalTimeRelation === "+") {
    hour -= offsetHour;
    minute -= offsetMinute;
  }

  return new Date(Date.UTC(year, month, day, hour, minute, second));
}

export function hexToRgb(hexColor) {
  const hex = hexColor.slice(1); // 去除 "#" 符号
  const bigint = parseInt(hex, 16);
  const R = (bigint >> 16) & 255;
  const G = (bigint >> 8) & 255;
  const B = bigint & 255;
  return {
    R,
    G,
    B
  }
}

const hslToRgb = (h, s, l) => {
  let R, G, B;

  h = h / 360; // 将 H 值从 0-360 转换到 0-1

  if (s === 0) {
    R = G = B = Math.round(l * 255); // 当饱和度为0时，直接返回灰度值
  } else {
    const hue2rgb = function hue2rgb(p, q, t) {
      if (t < 0) t += 1;
      if (t > 1) t -= 1;
      if (t < 1 / 6) return p + (q - p) * 6 * t;
      if (t < 1 / 2) return q;
      if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
      return p;
    }

    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    const p = 2 * l - q;
    R = Math.round(hue2rgb(p, q, h + 1 / 3) * 255);
    G = Math.round(hue2rgb(p, q, h) * 255);
    B = Math.round(hue2rgb(p, q, h - 1 / 3) * 255);
  }

  return { R, G, B };
}

const setCss = (ele, cssText) => {
  if (!ele) return
  if (cssText) {
    for (let key in cssText) {
      ele.style[key] = cssText[key]
    }
  }
}

export {
  apiPageLayoutToViewerModes,
  apiPageModeToSidebarView,
  approximateFraction,
  AutoPrintRegExp,
  backtrackBeforeAllVisibleElements, // only exported for testing
  binarySearchFirstItem,
  DEFAULT_SCALE,
  DEFAULT_SCALE_DELTA,
  DEFAULT_SCALE_VALUE,
  docStyle,
  parseAnnotationFromXml,
  getActiveOrFocusedElement,
  getPageSizeInches,
  getVisibleElements,
  isPortraitOrientation,
  isValidRotation,
  isValidScrollMode,
  isValidSpreadMode,
  MAX_AUTO_SCALE,
  MAX_SCALE,
  MIN_SCALE,
  noContextMenuHandler,
  normalizeWheelEventDelta,
  normalizeWheelEventDirection,
  OutputScale,
  parseQueryString,
  PresentationModeState,
  ProgressBar,
  removeNullCharacters,
  RendererType,
  RenderingStates,
  roundToDivide,
  SCROLLBAR_PADDING,
  scrollIntoView,
  ScrollMode,
  SidebarView,
  SpreadMode,
  TextLayerMode,
  UNKNOWN_SCALE,
  VERTICAL_PADDING,
  watchScroll,
  maxCanvasPixels,
  AbortException,
  EventBus,
  waitOnEventOrTimeout,
  objectFromMap,
  xmlParser,
  getElementsByTagName,
  getPosition,
  getContent,
  bindEvents,
  FeatureTest,
  shadow,
  Util,
  convertColorToCppFormat,
  convertCppRGBToHex,
  onClickOutside,
  onClickOutsideUp,
  findIndex,
  toDateObject,
  setCss,
  isMobileDevice,
  convertColorToRGB,
  isMobile
};
