# Source: metadata.js

    import { tagToKeyword } from './dictionary'
    import { SOPClassUIDs } from './enums'
    import { _groupFramesPerMapping } from './mapping'

    const _metadata = Symbol('metadata')
    const _bulkdataReferences = Symbol('bulkdataReferences')

    function _base64ToUint8Array (value) {
      const blob = window.atob(value)
      const array = new Uint8Array(blob.length)

      for (let i = 0; i < blob.length; i++) {
        array[i] = blob.charCodeAt(i)
      }

      return array
    }

    function _base64ToUint16Array (value) {
      const blob = window.atob(value)
      const n = Uint16Array.BYTES_PER_ELEMENT
      const length = blob.length / n
      const buffer = new ArrayBuffer(n)
      const view = new DataView(buffer)
      const array = new Uint16Array(length)

      let p = 0
      for (let i = 0; i < length; i++) {
        p = i * n
        for (let j = 0; j < n; j++) {
          view.setUint8(j, blob.charCodeAt(p + j))
        }
        array[i] = view.getUint16(0, true)
      }

      return array
    }

    function _base64ToUint32Array (value) {
      const blob = window.atob(value)
      const n = Uint32Array.BYTES_PER_ELEMENT
      const length = blob.length / n
      const buffer = new ArrayBuffer(n)
      const view = new DataView(buffer)
      const array = new Uint32Array(length)

      let p = 0
      for (let i = 0; i < length; i++) {
        p = i * n
        for (let j = 0; j < n; j++) {
          view.setUint8(j, blob.charCodeAt(p + j))
        }
        array[i] = view.getUint32(0, true)
      }

      return array
    }

    function _base64ToFloat32Array (value) {
      const blob = window.atob(value)
      const n = Float32Array.BYTES_PER_ELEMENT
      const length = blob.length / n
      const buffer = new ArrayBuffer(n)
      const view = new DataView(buffer)
      const array = new Float32Array(length)

      let p = 0
      for (let i = 0; i < length; i++) {
        p = i * n
        for (let j = 0; j < n; j++) {
          view.setUint8(j, blob.charCodeAt(p + j))
        }
        array[i] = view.getFloat32(0, true)
      }

      return array
    }

    function _base64ToFloat64Array (value) {
      const blob = window.atob(value)
      const n = Float64Array.BYTES_PER_ELEMENT
      const length = blob.length / n
      const buffer = new ArrayBuffer(n)
      const view = new DataView(buffer)
      const array = new Float64Array(length)

      let p = 0
      for (let i = 0; i < length; i++) {
        p = i * n
        for (let j = 0; j < n; j++) {
          view.setUint8(j, blob.charCodeAt(p + j))
        }
        array[i] = view.getFloat64(0, true)
      }

      return array
    }

    /** Determine the mapping of pyramid tile positions to frame numbers.
     *
     * @param {Object} Formatted metadata of a VL Whole Slide Microscopy Image instance
     * @returns {Object} Mapping of pyramid tile position (Row-Column) to frame URI
     * @private
     */
    function getFrameMapping (metadata) {
      const rows = metadata.Rows
      const columns = metadata.Columns
      const totalPixelMatrixColumns = metadata.TotalPixelMatrixColumns
      const totalPixelMatrixRows = metadata.TotalPixelMatrixRows
      const sopInstanceUID = metadata.SOPInstanceUID
      const numberOfFrames = Number(metadata.NumberOfFrames || 1)

      /**
       * Handle images that may contain multiple "planes"
       *  - z-planes (VL Whole Slide Microscopy Image)
       *  - optical paths (VL Whole Slide Microscopy Image)
       *  - segments (Segmentation)
       *  - mappings (Parametric Map)
       */
      const numberOfFocalPlanes = Number(metadata.NumberOfFocalPlanes || 1)
      if (numberOfFocalPlanes > 1) {
        throw new Error('Images with multiple focal planes are not yet supported.')
      }

      const {
        mappingNumberToFrameNumbers,
        frameNumberToMappingNumber
      } = _groupFramesPerMapping(metadata)
      let numberOfChannels = 0
      let numberOfOpticalPaths = 0
      let numberOfSegments = 0
      let numberOfMappings = 0
      if (metadata.OpticalPathSequence != null) {
        numberOfOpticalPaths = Number(metadata.NumberOfOpticalPaths || 1)
        numberOfChannels = numberOfOpticalPaths
      } else if (metadata.SegmentSequence != null) {
        numberOfSegments = metadata.SegmentSequence.length
        numberOfChannels = numberOfSegments
      } else if (Object.keys(mappingNumberToFrameNumbers).length > 0) {
        numberOfMappings = Object.keys(mappingNumberToFrameNumbers).length
        numberOfChannels = numberOfMappings
      } else {
        throw new Error('Could not determine the number of image channels.')
      }

      const tileColumns = Math.ceil(totalPixelMatrixColumns / columns)
      const tileRows = Math.ceil(totalPixelMatrixRows / rows)
      const frameMapping = {}
      /**
       * The values "TILED_SPARSE" and "TILED_FULL" were introduced in the 2018
       * edition of the standard. Older datasets are equivalent to "TILED_SPARSE".
       */
      const dimensionOrganizationType = (
        metadata.DimensionOrganizationType || 'TILED_SPARSE'
      )
      if (dimensionOrganizationType === 'TILED_FULL') {
        let n = 1
        // Forth, along "channels"
        for (let i = 0; i < numberOfChannels; i++) {
          // Third, along the depth direction from glass slide -> coverslip
          for (let p = 0; p < numberOfFocalPlanes; p++) {
            // Second, along the column direction from top -> bottom
            for (let r = 0; r < tileRows; r++) {
              // First, along the row direction from left -> right
              for (let c = 0; c < tileColumns; c++) {
                /*
                 * The standard currently only defines TILED_FULL for optical paths
                 * and not any other types of "channels" such as segments or
                 * parameter mappings.
                 */
                let channelIdentifier
                if (numberOfOpticalPaths > 0) {
                  const opticalPath = metadata.OpticalPathSequence[i]
                  channelIdentifier = opticalPath.OpticalPathIdentifier
                } else if (numberOfSegments > 0) {
                  const segment = metadata.SegmentIdentificationSequence[i]
                  channelIdentifier = String(segment.ReferencedSegmentNumber)
                } else if (numberOfMappings > 0) {
                  // TODO: ensure that frames are mapped accordingly
                  channelIdentifier = String(frameNumberToMappingNumber[n])
                } else {
                  throw new Error(`Could not determine channel of frame #${n}.`)
                }
                const index = `${r + 1}-${c + 1}-${channelIdentifier}`
                frameMapping[index] = `${sopInstanceUID}/frames/${n}`
                n += 1
              }
            }
          }
        }
      } else {
        const sharedFuncGroups = metadata.SharedFunctionalGroupsSequence
        const perframeFuncGroups = metadata.PerFrameFunctionalGroupsSequence
        for (let j = 0; j < numberOfFrames; j++) {
          const planePositions = perframeFuncGroups[j].PlanePositionSlideSequence[0]
          const rowPosition = planePositions.RowPositionInTotalImagePixelMatrix
          const columnPosition = planePositions.ColumnPositionInTotalImagePixelMatrix
          const rowIndex = Math.ceil(rowPosition / rows)
          const colIndex = Math.ceil(columnPosition / columns)
          let channelIdentifier
          if (numberOfOpticalPaths === 1) {
            try {
              channelIdentifier = String(
                sharedFuncGroups[0]
                  .OpticalPathIdentificationSequence[0]
                  .OpticalPathIdentifier
              )
            } catch {
              channelIdentifier = String(
                perframeFuncGroups[0]
                  .OpticalPathIdentificationSequence[0]
                  .OpticalPathIdentifier
              )
            }
          } else if (numberOfOpticalPaths > 1) {
            channelIdentifier = String(
              perframeFuncGroups[0]
                .OpticalPathIdentificationSequence[0]
                .OpticalPathIdentifier
            )
          } else if (numberOfSegments === 1) {
            try {
              channelIdentifier = String(
                sharedFuncGroups[0]
                  .SegmentIdentificationSequence[0]
                  .ReferencedSegmentNumber
              )
            } catch {
              channelIdentifier = String(
                perframeFuncGroups[0]
                  .SegmentIdentificationSequence[0]
                  .ReferencedSegmentNumber
              )
            }
          } else if (numberOfSegments > 1) {
            channelIdentifier = String(
              perframeFuncGroups[0]
                .SegmentIdentificationSequence[0]
                .ReferencedSegmentNumber
            )
          } else if (numberOfMappings > 0) {
            channelIdentifier = String(frameNumberToMappingNumber[j + 1])
          } else {
            throw new Error(`Could not determine channel of frame ${j}.`)
          }
          const index = `${rowIndex}-${colIndex}-${channelIdentifier}`
          const frameNumber = j + 1
          frameMapping[index] = `${sopInstanceUID}/frames/${frameNumber}`
        }
      }
      return {
        frameMapping,
        numberOfChannels
      }
    }

    /**
     * Format DICOM metadata structured according to the DICOM JSON model.
     *
     * Transforms the DICOM JSON representation into a more human friendly
     * representation, where values of data elements can be directly accessed via
     * their keyword (e.g., "SOPInstanceUID").
     * Bulkdata elements will be extracted and returned as a separate mapping.
     *
     * @param {Object} metadata - Metadata structured according to the DICOM JSON model
     *
     * @returns {Object} Formatted dataset and remaining bulkdata
     *
     * @memberof metadata
     */
    function formatMetadata (metadata) {
      const loadJSONDataset = (elements) => {
        const dataset = {}
        const bulkdataReferences = {}
        Object.keys(elements).forEach(tag => {
          const keyword = tagToKeyword[tag]
          const vr = elements[tag].vr
          if ('BulkDataURI' in elements[tag]) {
            bulkdataReferences[keyword] = elements[tag]
          } else if ('Value' in elements[tag]) {
            const value = elements[tag].Value
            if (vr === 'SQ') {
              dataset[keyword] = []
              const mappings = []
              value.forEach(item => {
                const loaded = loadJSONDataset(item)
                dataset[keyword].push(loaded.dataset)
                mappings.push(loaded.bulkdataReferences)
              })
              if (mappings.some(item => Object.keys(item).length > 0)) {
                bulkdataReferences[keyword] = mappings
              }
            } else {
              // Handle value multiplicity.
              if (value.length === 1) {
                if (vr === 'DS' || vr === 'IS') {
                  dataset[keyword] = Number(value[0])
                } else {
                  dataset[keyword] = value[0]
                }
              } else {
                if (vr === 'DS' || vr === 'IS') {
                  dataset[keyword] = value.map(v => Number(v))
                } else {
                  dataset[keyword] = value
                }
              }
            }
          } else if ('InlineBinary' in elements[tag]) {
            const value = elements[tag].InlineBinary
            if (vr === 'OB') {
              dataset[keyword] = _base64ToUint8Array(value)
            } else if (vr === 'OW') {
              dataset[keyword] = _base64ToUint16Array(value)
            } else if (vr === 'OL') {
              dataset[keyword] = _base64ToUint32Array(value)
            } else if (vr === 'OF') {
              dataset[keyword] = _base64ToFloat32Array(value)
            } else if (vr === 'OD') {
              dataset[keyword] = _base64ToFloat64Array(value)
            }
          } else {
            if (vr === 'SQ') {
              dataset[keyword] = []
            } else {
              dataset[keyword] = null
            }
          }
        })
        return { dataset, bulkdataReferences }
      }

      const { dataset, bulkdataReferences } = loadJSONDataset(metadata)

      // The top level (lowest resolution) image may be a single frame image in
      // which case the "NumberOfFrames" attribute is optional. We include it for
      // consistency.
      if (dataset === undefined) {
        throw new Error('Could not format metadata: ', metadata)
      }
      if (!('NumberOfFrames' in dataset) && (dataset.Modality === 'SM')) {
        dataset.NumberOfFrames = 1
      }

      return { dataset, bulkdataReferences }
    }

    /**
     * Group DICOM metadata of monochrome slides by Optical Path Identifier.
     *
     * @param {metadata.VLWholeSlideMicroscopyImage[]} images - DICOM VL Whole
     * Slide Microscopy Image instances.
     *
     * @returns {Object} Groups of DICOM VL Whole Slide Microscopy Image instances
     * @memberof metadata
     */
    function groupMonochromeInstances (images) {
      const channels = {}
      images.forEach(img => {
        if (
          img.SamplesPerPixel === 1 &&
          img.PhotometricInterpretation === 'MONOCHROME2' &&
          (img.ImageType[2] === 'VOLUME' || img.ImageType[2] === 'THUMBNAIL')
        ) {
          img.OpticalPathSequence.forEach((opticalPathItem, opticalPathIndex) => {
            const id = opticalPathItem.OpticalPathIdentifier
            if (id in channels) {
              channels[id].push(img)
            } else {
              channels[id] = [img]
            }
          })
        }
      })
      return channels
    }

    /**
     * Group DICOM metadata of color images slides by Optical Path Identifier.
     *
     * @param {metadata.VLWholeSlideMicroscopyImage[]} images - DICOM VL Whole
     * Slide Microscopy Image instances.
     *
     * @returns {Object} Groups of DICOM VL Whole Slide Microscopy Image instances
     * @memberof metadata
     */
    function groupColorInstances (images) {
      const channels = {}
      images.forEach(img => {
        if (
          img.SamplesPerPixel !== 1 &&
          (img.ImageType[2] === 'THUMBNAIL' || img.ImageType[2] === 'VOLUME') &&
          (
            img.PhotometricInterpretation === 'RGB' ||
            img.PhotometricInterpretation.includes('YBR')
          )
        ) {
          const id = img.OpticalPathSequence[0].OpticalPathIdentifier
          if (id in channels) {
            channels[id].push(img)
          } else {
            channels[id] = [img]
          }
        }
      })
      return channels
    }

    /**
     * DICOM Service Object Pair (SOP) Class.
     *
     * @class
     * @abstract
     * @memberof metadata
     */
    class SOPClass {
      /**
       * @param {Object} options
       * @param {Object} options.metadata - Metadata of a DICOM SOP instance in DICOM JSON format
       */
      constructor ({ metadata }) {
        if (metadata == null) {
          throw new Error(
            'Cannot construct SOP Instance because no metadata was provided.'
          )
        }
        const { dataset, bulkdataReferences } = formatMetadata(metadata)
        Object.assign(this, dataset)
        this[_metadata] = metadata
        this[_bulkdataReferences] = bulkdataReferences
        Object.freeze(this)
      }

      /**
       * Get metadata of instance in DICOM JSON format.
       *
       * The metadata may include bulkdata references via "BulkDataURI".
       *
       * @returns {Object} metadata in DICOM JSON format
       */
      get json () {
        return this[_metadata]
      }

      /**
       * Get references to bulkdata of instance in DICOM JSON format.
       *
       * @returns {Object} bulkdata references in DICOM JSON format
       */
      get bulkdataReferences () {
        return this[_bulkdataReferences]
      }
    }

    /**
     * DICOM VL Whole Slide Microscopy Image instance.
     *
     * @class
     * @extends metadata.SOPClass
     * @memberof metadata
     */
    class VLWholeSlideMicroscopyImage extends SOPClass {
      /**
       * @param {Object} options
       * @param {Object} options.metadata - Metadata of a VL Whole Slide Microscopy Image in DICOM JSON format
       */
      constructor ({ metadata }) {
        super({ metadata })
        if (this.SOPClassUID !== SOPClassUIDs.VL_WHOLE_SLIDE_MICROSCOPY_IMAGE) {
          throw new Error(
            'Cannot construct VL Whole Slide Microscopy Image instance ' +
            `given dataset with SOP Class UID "${this.SOPClassUID}"`
          )
        }
      }
    }

    /**
     * DICOM Comprehensive 3D SR instance.
     *
     * @class
     * @extends metadata.SOPClass
     * @memberof metadata
     */
    class Comprehensive3DSR extends SOPClass {
      /**
       * @param {Object} options
       * @param {Object} options.metadata - Metadata of DICOM Structured Report instance in DICOM JSON format
       */
      constructor ({ metadata }) {
        super({ metadata })
        if (this.SOPClassUID !== SOPClassUIDs.COMPREHENSIVE_3D_SR) {
          throw new Error(
            'Cannot construct Comprehensive 3D SR instance ' +
              `given dataset with SOP Class UID "${this.SOPClassUID}"`
          )
        }
      }
    }

    /**
     * DICOM Microscopy Bulk Simple Annotations instance.
     *
     * @class
     * @extends metadata.SOPClass
     * @memberof metadata
     */
    class MicroscopyBulkSimpleAnnotations extends SOPClass {
      /**
       * @param {Object} options
       * @param {Object} options.metadata - Metadata of a DICOM Microscopy Bulk Simple Annotations instance in DICOM JSON format
       */
      constructor ({ metadata }) {
        super({ metadata })
        if (this.SOPClassUID !== SOPClassUIDs.MICROSCOPY_BULK_SIMPLE_ANNOTATIONS) {
          throw new Error(
            'Cannot construct Microscopy Bulk Simple Annotations instance ' +
              `given dataset with SOP Class UID "${this.SOPClassUID}"`
          )
        }
      }
    }

    /**
     * DICOM Parametric Map instance.
     *
     * @class
     * @extends metadata.SOPClass
     * @memberof metadata
     */
    class ParametricMap extends SOPClass {
      /**
       * @param {Object} options
       * @param {Object} options.metadata - Metadata of a DICOM Parametric Map instance in DICOM JSON format
       */
      constructor ({ metadata }) {
        super({ metadata })
        if (this.SOPClassUID !== SOPClassUIDs.PARAMETRIC_MAP) {
          throw new Error(
            'Cannot construct Parametric Map instance ' +
              `given dataset with SOP Class UID "${this.SOPClassUID}"`
          )
        }
      }
    }

    /**
     * DICOM Segmentation instance.
     *
     * @class
     * @extends metadata.SOPClass
     * @memberof metadata
     */
    class Segmentation extends SOPClass {
      /**
       * @param {Object} options
       * @param {Object} options.metadata - Metadata of a DICOM Segmentation instance in DICOM JSON format
       */
      constructor ({ metadata }) {
        super({ metadata })
        if (this.SOPClassUID !== SOPClassUIDs.SEGMENTATION) {
          throw new Error(
            'Cannot construct Segmentation instance ' +
            `given dataset with SOP Class UID "${this.SOPClassUID}"`
          )
        }
      }
    }

    export {
      Comprehensive3DSR,
      formatMetadata,
      groupMonochromeInstances,
      groupColorInstances,
      getFrameMapping,
      MicroscopyBulkSimpleAnnotations,
      ParametricMap,
      Segmentation,
      VLWholeSlideMicroscopyImage
    }