# Source: utils.js

    import { inv, multiply } from 'mathjs'
    import { getPointResolution } from 'ol/proj'
    import { v4 as createUUIDv4, v5 as createUUIDv5 } from 'uuid'

    const _UUID_NAMESPACE = 'c4f09b11-bac0-4f3a-8dc1-9f0046637383'

    /**
     * Generates a UUID-derived DICOM UID with root `2.25`.
     *
     * @returns {string} Unique identifier
     *
     * @private
     */
    function _generateUID ({ value } = {}) {
      /**
       * A UUID can be represented as a single integer value.
       * http://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_B.2.html
       * https://www.itu.int/rec/T-REC-X.667-201210-I/en
       * To obtain the single integer value of the UUID, the 16 octets of the
       * binary representation shall be treated as an unsigned integer encoding
       * with the most significant bit of the integer encoding as the most
       * significant bit (bit 7) of the first of the sixteen octets (octet 15) and
       * the least significant bit as the least significant bit (bit 0) of the last
       * of the sixteen octets (octet 0).
       */
      let uuid
      if (value != null) {
        uuid = createUUIDv5(value, _UUID_NAMESPACE)
      } else {
        uuid = createUUIDv4()
      }
      const hex = '0x' + uuid.replace(/-/g, '')
      const decimal = BigInt(hex)
      return '2.25.' + decimal.toString()
    }

    /**
     * Create a rotation matrix.
     *
     * @param {Object} options - Options
     * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system
     *
     * @returns {number[][]} 2x2 rotation matrix
     *
     * @memberof utils
     */
    function createRotationMatrix (options) {
      if (!('orientation' in options)) {
        throw new Error('Option "orientation" is required.')
      }
      const orientation = options.orientation
      const rowDirection = orientation.slice(0, 3)
      const columnDirection = orientation.slice(3, 6)
      return [
        [rowDirection[0], columnDirection[0]],
        [rowDirection[1], columnDirection[1]],
        [rowDirection[2], columnDirection[3]]
      ]
    }

    /**
     * Rescale intensity from [minInput, maxInput] to [minOutput, maxOutput].
     *
     * @param {number} value - Input value that should be rescaled
     * @param {number} minInput - Lower bound of the full input value range
     * @param {number} maxInput - Upper bound of the full input value range
     * @param {number} minOutput - Lower bound of the full output value range
     * @param {number} maxOutput - Upper bound of the full output value range
     *
     * @returns {number} Rescaled value
     *
     * @memberof utils
     */
    function rescale (
      value,
      minInput,
      maxInput,
      minOutput,
      maxOutput
    ) {
      return (
        (value - minInput) * (maxOutput - minOutput) /
        (maxInput - minInput) +
        minOutput
      )
    }

    /**
     * Create window.
     *
     * @param {number} lowerBound - Lower bound of the window
     * @param {number} upperBound - Upper bound of the window
     *
     * @returns {number[]} Window center and width
     *
     * @memberof utils
     */
    function createWindow (lowerBound, upperBound) {
      const windowCenter = (lowerBound + upperBound) / 2
      const windowWidth = upperBound - lowerBound
      return [windowCenter, windowWidth]
    }

    /**
     * Computes the rotation of the image with respect to the frame of reference.
     *
     * @param {Object} options - Options
     * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system
     * @param {boolean} options.inDegrees - Whether angle should be returned in degrees instead of radians
     *
     * @returns {number} Angle
     *
     * @memberof utils
     */
    function computeRotation (options) {
      const rot = createRotationMatrix({ orientation: options.orientation })
      const angle = Math.atan2(-rot[0][1], rot[0][0])
      let inDegrees = false
      if ('inDegrees' in options) {
        inDegrees = true
      }
      if (inDegrees) {
        return angle / (Math.PI / 180)
      } else {
        return angle
      }
    }

    /**
     * Build an affine transformation matrix to map coordinates in the Total
     * Pixel Matrix into the slide coordinate system.
     *
     * @param {Object} options - Options
     * @param {number[]} options.offset - X and Y offset of the image in the slide coordinate system
     * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system
     * @param {number[]} options.spacing - Spacing between pixel rows and columns of the Total Pixel Matrix
     *
     * @returns {number[][]} 3x3 affine transformation matrix
     *
     * @memberof utils
     */
    function buildTransform ({ offset, orientation, spacing }) {
      // X and Y Offset in Slide Coordinate System
      if (offset == null) {
        throw new Error('Option "offset" is required.')
      }
      if (!Array.isArray(offset)) {
        throw new Error('Option "offset" must be an array.')
      }
      if (offset.length !== 2) {
        throw new Error('Option "offset" must be an array with 2 elements.')
      }

      // Image Orientation Slide with direction cosines for Row and Column direction
      if (orientation == null) {
        throw new Error('Option "orientation" is required.')
      }
      if (!Array.isArray(orientation)) {
        throw new Error('Option "orientation" must be an array.')
      }
      if (orientation.length !== 6) {
        throw new Error('Option "orientation" must be an array with 6 elements.')
      }

      // Pixel Spacing along the Row and Column direction
      if (spacing == null) {
        throw new Error('Option "spacing" is required.')
      }
      if (!Array.isArray(spacing)) {
        throw new Error('Option "spacing" must be an array.')
      }
      if (spacing.length !== 2) {
        throw new Error('Option "spacing" must be an array with 2 elements.')
      }

      const affine = [
        [orientation[0] * spacing[1], orientation[3] * spacing[0], offset[0]],
        [orientation[1] * spacing[1], orientation[4] * spacing[0], offset[1]],
        [0, 0, 1]
      ]
      const correction = [
        [1.0, 0.0, -0.5],
        [0.0, 1.0, -0.5],
        [0.0, 0.0, 1.0]
      ]
      return multiply(affine, correction)
    }

    /**
     * Apply an affine transformation to an image coordinate in the total pixel
     * matrix to map it into the slide coordinate system.
     *
     * @param {Object} options - Options
     * @param {number[]} options.coordinate - (column, row) image coordinate
     * @param {number[][]} options.affine - 3x3 affine transformation matrix
     *
     * @returns {number[]} (x, y) reference coordinate
     *
     * @memberof utils
     */
    function applyTransform ({ coordinate, affine }) {
      if (coordinate == null) {
        throw new Error('Option "coordinate" is required.')
      }
      if (!Array.isArray(coordinate)) {
        throw new Error('Option "coordinate" must be an array.')
      }
      if (coordinate.length !== 2) {
        throw new Error('Option "coordinate" must be an array with 2 elements.')
      }

      if (affine == null) {
        throw new Error('Option "affine" is required.')
      }
      if (!Array.isArray(affine)) {
        throw new Error('Option "affine" must be an array.')
      }
      if (affine.length !== 3) {
        throw new Error('Option "affine" must be a 3x3 array.')
      }
      if (!Array.isArray(affine[0])) {
        throw new Error('Option "affine" must be a 3x3 array.')
      }
      if (affine[0].length !== 3 || affine[1].length !== 3) {
        throw new Error('Option "affine" must be a 3x3 array.')
      }

      const imageCoordinate = [[coordinate[0]], [coordinate[1]], [1]]

      const slideCoordinate = multiply(affine, imageCoordinate)

      const x = Number(slideCoordinate[0][0].toFixed(4))
      const y = Number(slideCoordinate[1][0].toFixed(4))
      return [x, y]
    }

    /**
     * Build an affine transformation matrix to map coordinates in the slide
     * coordinate system into the Total Pixel Matrix.
     *
     * @param {number[]} options.offset - X and Y offset of the image in the slide coordinate system
     * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system
     * @param {number[]} options.spacing - Spacing between pixel rows and columns of the Total Pixel Matrix
     *
     * @returns {number[][]} 3x3 affine transformation matrix
     *
     * @memberof utils
     */
    function buildInverseTransform ({ offset, orientation, spacing }) {
      // X and Y Offset in Slide Coordinate System
      if (offset == null) {
        throw new Error('Option "offset" is required.')
      }
      if (!Array.isArray(offset)) {
        throw new Error('Option "offset" must be an array.')
      }
      if (offset.length !== 2) {
        throw new Error('Option "offset" must be an array with 2 elements.')
      }

      // Image Orientation Slide with direction cosines for Row and Column direction
      if (orientation == null) {
        throw new Error('Option "orientation" is required.')
      }
      if (!Array.isArray(orientation)) {
        throw new Error('Option "orientation" must be an array.')
      }
      if (orientation.length !== 6) {
        throw new Error('Option "orientation" must be an array with 6 elements.')
      }

      // Pixel Spacing along the Row and Column direction
      if (spacing == null) {
        throw new Error('Option "spacing" is required.')
      }
      if (!Array.isArray(spacing)) {
        throw new Error('Option "spacing" must be an array.')
      }
      if (spacing.length !== 2) {
        throw new Error('Option "spacing" must be an array with 2 elements.')
      }

      const affine = inv([
        [orientation[0] * spacing[1], orientation[3] * spacing[0], offset[0]],
        [orientation[1] * spacing[1], orientation[4] * spacing[0], offset[1]],
        [0, 0, 1]
      ])
      const correction = [
        [1.0, 0.0, 0.5],
        [0.0, 1.0, 0.5],
        [0.0, 0.0, 1.0]
      ]
      return multiply(correction, affine)
    }

    /**
     * Apply an affine transformation to a reference coordinate in the slide
     * coordinate system to map it into the total pixel matrix.
     *
     * @param {Object} options - Options
     * @param {number[]} options.coordinate - (x, y) reference coordinate
     * @param {number[][]} options.affine - 3x3 affine transformation matrix
     *
     * @returns {number[]} (column, row) image coordinate
     *
     * @memberof utils
     */
    function applyInverseTransform ({ coordinate, affine }) {
      if (coordinate == null) {
        throw new Error('Option "coordinate" is required.')
      }
      if (!Array.isArray(coordinate)) {
        throw new Error('Option "coordinate" must be an array.')
      }
      if (coordinate.length !== 2) {
        throw new Error('Option "coordinate" must be an array with 2 elements.')
      }

      if (affine == null) {
        throw new Error('Option "affine" is required.')
      }
      if (!Array.isArray(affine)) {
        throw new Error('Option "affine" must be an array.')
      }
      if (affine.length !== 3) {
        throw new Error('Option "affine" must be a 3x3 array.')
      }
      if (!Array.isArray(affine[0])) {
        throw new Error('Option "affine" must be a 3x3 array.')
      }
      if (affine[0].length !== 3 || affine[1].length !== 3) {
        throw new Error('Option "affine" must be a 3x3 array.')
      }

      const slideCoordinate = [[coordinate[0]], [coordinate[1]], [1]]

      const pixelCoordinate = multiply(affine, slideCoordinate)

      const col = Number(pixelCoordinate[0][0].toFixed(4))
      const row = Number(pixelCoordinate[1][0].toFixed(4))
      return [col, row]
    }

    /**
     * Map 2D (column, row) image coordinates in the Total Pixel Matrix
     * to 3D (x, y, z) slide coordinates in the Frame of Reference.
     *
     * @param {Object} options - Options
     * @param {number[]} options.offset - X and Y offset in the slide coordinate system
     * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system
     * @param {number[]} options.spacing - Spacing between pixels along the Column and Row direction of the Total Pixel Matrix
     * @param {number[]} options.point - (colum, row) image coordinates
     *
     * @returns {number[]} (x, y, z) slide coordinates
     *
     * @memberof utils
     */
    function mapPixelCoordToSlideCoord ({ point, offset, orientation, spacing }) {
      if (point == null) {
        throw new Error('Option "point" is required.')
      }
      if (!Array.isArray(point)) {
        throw new Error('Option "point" must be an array.')
      }
      if (point.length !== 2) {
        throw new Error('Option "point" must be an array with 2 elements.')
      }

      const affine = buildTransform({
        orientation,
        offset,
        spacing
      })
      return applyTransform({ coordinate: point, affine: affine })
    }

    /**
     * Map 3D (x, y, z) slide coordinates in the Frame of Reference to
     * 2D (column, row) image coordinates in the Total Pixel Matrix.
     *
     * @param {Object} options - Options
     * @param {number[]} options.offset - X and Y offset in the slide coordinate system
     * @param {number[]} options.orientation - Direction cosines along the row and column direction of the Total Pixel Matrix for each of the three axis of the slide coordinate system
     * @param {number[]} options.spacing - Spacing between pixels along the Column and Row direction of the Total Pixel Matrix
     * @param {number[]} options.point - (x, y, z) slide coordinates
     *
     * @returns {number[]} (row, column) image coordinates
     *
     * @memberof utils
     */
    function mapSlideCoordToPixelCoord ({ point, offset, orientation, spacing }) {
      if (point == null) {
        throw new Error('Option "point" is required.')
      }
      if (!Array.isArray(point)) {
        throw new Error('Option "point" must be an array.')
      }
      if (point.length !== 2) {
        throw new Error('Option "point" must be an array with 2 elements.')
      }
      const affine = buildInverseTransform({
        orientation,
        offset,
        spacing
      })

      return applyInverseTransform({ coordinate: point, affine: affine })
    }

    /**
     * Check if 2D arrays are equal.
     *
     * @param {number[]} array a
     * @param {number[]} array b
     * @param {number} eps
     *
     * @returns {boolean} yes/no answer
     *
     * @memberof utils
     */
    function are2DArraysAlmostEqual (a, b, eps = 1.e-6) {
      if (a === b) return true
      if (a == null || b == null) return false
      if (a.length !== b.length) return false

      for (let i = 0; i < a.length; ++i) {
        if (a[i].length !== b[i].length) return false
        for (let j = 0; j < a[i].length; ++j) {
          if (!areNumbersAlmostEqual(a[i][j], b[i][j], eps)) {
            return false
          }
        }
      }
      return true
    }

    /**
     * Check if 1D arrays are equal.
     *
     * @param {number[]} array a
     * @param {number[]} array b
     * @param {number} eps
     *
     * @returns {boolean} yes/no answer
     *
     * @memberof utils
     */
    function are1DArraysAlmostEqual (a, b, eps = 1.e-6) {
      if (a == null || b == null) return false
      if (a.length !== b.length) return false

      for (let i = 0; i < a.length; ++i) {
        if (!areNumbersAlmostEqual(a[i], b[i], eps)) {
          return false
        }
      }
      return true
    }

    /**
     * Check if two numbers are equal.
     *
     * @param {number} a
     * @param {number} b
     * @param {number} eps
     *
     * @returns {boolean} yes/no answer
     *
     * @memberof utils
     */
    function areNumbersAlmostEqual (a, b, eps = 1.e-6) {
      return Math.abs(a - b) < eps
    }

    /**
     * Get view unit suffix.
     *
     * @param {object} view Map view
     *
     * @returns {string} unit suffix
     *
     * @private
     */
    function _getUnitSuffix (view) {
      const UnitsEnum = { METERS: 'm' }
      const DEFAULT_DPI = 25.4 / 0.28

      const center = view.getCenter()
      const projection = view.getProjection()
      const resolution = view.getResolution()

      const pointResolutionUnits = UnitsEnum.METERS

      let pointResolution = getPointResolution(
        projection,
        resolution,
        center,
        pointResolutionUnits
      )

      const DEFAULT_MIN_WIDTH = 65
      const minWidth = (DEFAULT_MIN_WIDTH * DEFAULT_DPI) / DEFAULT_DPI

      const nominalCount = minWidth * pointResolution
      let suffix = ''

      if (nominalCount < 0.001) {
        suffix = 'μm'
        pointResolution *= 1000000
      } else if (nominalCount < 1) {
        suffix = 'mm'
        pointResolution *= 1000
      } else if (nominalCount < 1000) {
        suffix = 'm'
      } else {
        suffix = 'km'
        pointResolution /= 1000
      }

      return suffix
    }

    /**
     * Get name coded concept from content item.
     *
     * @param {object} contentItem
     *
     * @returns {object} The concept name coded concept
     *
     * @memberof utils
     */
    const getContentItemNameCodedConcept = (contentItem) =>
      contentItem.ConceptNameCodeSequence[0]

    /**
     * Check whether coded concepts are equal.
     *
     * @param {object} codedConcept1
     * @param {object} codedConcept2
     *
     * @returns {boolean} yes/no answer
     *
     * @memberof utils
     */
    const areCodedConceptsEqual = (codedConcept1, codedConcept2) => {
      if (
        codedConcept2.CodeValue === codedConcept1.CodeValue &&
        codedConcept2.CodingSchemeDesignator ===
          codedConcept1.CodingSchemeDesignator
      ) {
        if (
          codedConcept2.CodingSchemeVersion &&
          codedConcept1.CodingSchemeVersion
        ) {
          return (
            codedConcept2.CodingSchemeVersion === codedConcept1.CodingSchemeVersion
          )
        }
        return true
      }
      return false
    }

    /**
     * Check wether two content items match.
     *
     * @param {object} contentItem1
     * @param {object} contentItem2
     *
     * @returns {boolean} yes/no answer
     *
     * @memberof utils
     */
    const doContentItemsMatch = (contentItem1, contentItem2) => {
      const contentItem1NameCodedConcept = getContentItemNameCodedConcept(
        contentItem1
      )
      const contentItem2NameCodedConcept = getContentItemNameCodedConcept(
        contentItem2
      )
      return contentItem1NameCodedConcept.equals
        ? contentItem1NameCodedConcept.equals(contentItem2NameCodedConcept)
        : areCodedConceptsEqual(
          contentItem1NameCodedConcept,
          contentItem2NameCodedConcept
        )
    }

    /**
     * Fetch bulkdata.
     *
     * @param {object} options
     * @param {object} options.client - DICOMweb client @param {object}
     * options.reference - Data Element in DICOM JSON format containing "vr" and
     * "BulkDataURI" fields
     *
     * @returns {Promise<TypedArray>} bulkdata
     *
     * @private
     */
    async function _fetchBulkdata ({ client, reference }) {
      const retrieveOptions = { BulkDataURI: reference.BulkDataURI }
      return await client.retrieveBulkData(retrieveOptions).then(data => {
        const byteArray = new Uint8Array(data[0])
        if (reference.vr === 'OB') {
          return byteArray
        } else if (reference.vr === 'OW') {
          return new Uint16Array(
            byteArray.buffer,
            byteArray.byteOffset,
            byteArray.byteLength / 2
          )
        } else if (reference.vr === 'OL') {
          return new Int32Array(
            byteArray.buffer,
            byteArray.byteOffset,
            byteArray.byteLength / 4
          )
        } else if (reference.vr === 'OV') {
          // There is no Int64Array, so we represent data as Float64Array instead
          return new Float64Array(
            byteArray.buffer,
            byteArray.byteOffset,
            byteArray.byteLength / 8
          )
        } else if (reference.vr === 'OF') {
          return new Float32Array(
            byteArray.buffer,
            byteArray.byteOffset,
            byteArray.byteLength / 4
          )
        } else if (reference.vr === 'OD') {
          return new Float64Array(
            byteArray.buffer,
            byteArray.byteOffset,
            byteArray.byteLength / 8
          )
        } else {
          throw new Error(
            `Unexpected Value Representation "${reference.vr}" for ` +
            `bulkdata element with URI "${reference.BulkDataURI}".`
          )
        }
      })
    }

    export {
      _getUnitSuffix,
      applyInverseTransform,
      applyTransform,
      buildInverseTransform,
      buildTransform,
      computeRotation,
      _fetchBulkdata,
      _generateUID,
      mapPixelCoordToSlideCoord,
      mapSlideCoordToPixelCoord,
      areNumbersAlmostEqual,
      are1DArraysAlmostEqual,
      are2DArraysAlmostEqual,
      doContentItemsMatch,
      areCodedConceptsEqual,
      getContentItemNameCodedConcept,
      rescale,
      createWindow
    }