text: import { parsingUtils } from '../lib/parsingUtils';
  
  const FUNCTION = 'function';
  
  class MetadataProvider {
    constructor() {
      // Define the main "metadataLookup" private property as an immutable property.
      Object.defineProperty(this, 'metadataLookup', {
        configurable: false,
        enumerable: false,
        writable: false,
        value: new Map(),
      });
  
      // Local reference to provider function bound to current instance.
      Object.defineProperty(this, '_provider', {
        configurable: false,
        enumerable: false,
        writable: true,
        value: null,
      });
    }
  
    /**
     * Cornerstone Metadata provider to store image meta data
     * Data from instances, series, and studies are associated with
     * imageIds to facilitate usage of this information by Cornerstone's Tools
     *
     * e.g. the imagePlane metadata object contains instance information about
     * row/column pixel spacing, patient position, and patient orientation. It
     * is used in CornerstoneTools to position reference lines and orientation markers.
     *
     * @param {String} imageId The Cornerstone ImageId
     * @param {Object} data An object containing instance, series, and study metadata
     */
    addMetadata(imageId, data) {
      const instanceMetadata = data.instance;
      const seriesMetadata = data.series;
      const studyMetadata = data.study;
      const numImages = data.numImages;
      const metadata = {};
  
      metadata.frameNumber = data.frameNumber;
  
      metadata.study = {
        accessionNumber: studyMetadata.accessionNumber,
        patientId: studyMetadata.patientId,
        stationName: studyMetadata.stationName,
        operatorsName: studyMetadata.operatorsName,
        studyInstanceUid: studyMetadata.studyInstanceUid,
        studyDate: studyMetadata.studyDate,
        studyTime: studyMetadata.studyTime,
        studyDescription: studyMetadata.studyDescription,
        patientHistory: studyMetadata.patientHistory,
      };
  
      metadata.series = {
        seriesDescription: seriesMetadata.seriesDescription,
        seriesNumber: seriesMetadata.seriesNumber,
        seriesDate: seriesMetadata.seriesDate,
        seriesTime: seriesMetadata.seriesTime,
        modality: seriesMetadata.modality,
        stationName: studyMetadata.stationName,
        operatorsName: studyMetadata.operatorsName,
        seriesInstanceUid: seriesMetadata.seriesInstanceUid,
        numImages: numImages,
      };
  
      metadata.instance = instanceMetadata;
  
      metadata.patient = {
        name: studyMetadata.patientName,
        id: studyMetadata.patientId,
        birthDate: studyMetadata.patientBirthDate,
        sex: studyMetadata.patientSex,
        age: studyMetadata.patientAge,
      };
  
      // If there is sufficient information, populate
      // the imagePlane object for easier use in the Viewer
      metadata.imagePlane = this.getImagePlane(instanceMetadata);
  
      // Add the metadata to the imageId lookup object
      this.metadataLookup.set(imageId, metadata);
    }
  
    /**
     * Return the metadata for the given imageId
     * @param {String} imageId The Cornerstone ImageId
     * @returns image metadata
     */
    getMetadata(imageId) {
      return this.metadataLookup.get(imageId);
    }
  
    /**
     * Adds a set of metadata to the Cornerstone metadata provider given a specific
     * imageId, type, and dataset
     *
     * @param imageId
     * @param type (e.g. series, instance, tagDisplay)
     * @param data
     */
    addSpecificMetadata(imageId, type, data) {
      const metadata = {};
      metadata[type] = data;
  
      const oldMetadata = this.metadataLookup.get(imageId);
      this.metadataLookup.set(imageId, Object.assign(oldMetadata, metadata));
    }
  
    getFromImage(image, type, tag, attrName, defaultValue) {
      let value;
  
      if (image.data) {
        value = this.getFromDataSet(image.data, type, tag);
      } else {
        value = image.instance[attrName];
      }
  
      return value === null ? defaultValue : value;
    }
  
    getFromDataSet(dataSet, type, tag) {
      if (!dataSet) {
        return;
      }
  
      const fn = dataSet[type];
      if (!fn) {
        return;
      }
  
      return fn.call(dataSet, tag);
    }
  
    getFrameIncrementPointer(image) {
      const dataSet = image.data;
      let frameInstancePointer = '';
  
      if (parsingUtils.isValidDataSet(dataSet)) {
        const frameInstancePointerNames = {
          x00181063: 'frameTime',
          x00181065: 'frameTimeVector',
        };
  
        // (0028,0009) = Frame Increment Pointer
        const frameInstancePointerTag = parsingUtils.attributeTag(
          dataSet,
          'x00280009'
        );
        frameInstancePointer = frameInstancePointerNames[frameInstancePointerTag];
      } else {
        frameInstancePointer = image.instance.frameIncrementPointer;
      }
  
      return frameInstancePointer || '';
    }
  
    getFrameTimeVector(image) {
      const dataSet = image.data;
  
      if (parsingUtils.isValidDataSet(dataSet)) {
        // Frame Increment Pointer points to Frame Time Vector (0018,1065) field
        return parsingUtils.floatArray(dataSet, 'x00181065');
      }
  
      return image.instance.frameTimeVector;
    }
  
    getFrameTime(image) {
      const dataSet = image.data;
  
      if (parsingUtils.isValidDataSet(dataSet)) {
        // Frame Increment Pointer points to Frame Time (0018,1063) field or is not defined (for addtional flexibility).
        // Yet another value is possible for this field (5200,9230 for Multi-frame Functional Groups)
        // but that case is currently not supported.
        return dataSet.floatString('x00181063', -1);
      }
  
      return image.instance.frameTime;
    }
  
    /**
     * Updates the related metadata for missing fields given a specified image
     *
     * @param image
     */
    updateMetadata(image) {
      const imageMetadata = this.metadataLookup.get(image.imageId);
      if (!imageMetadata) {
        return;
      }
  
      imageMetadata.patient.age =
        imageMetadata.patient.age ||
        this.getFromDataSet(image.data, 'string', 'x00101010');
  
      imageMetadata.instance.rows = imageMetadata.instance.rows || image.rows;
      imageMetadata.instance.columns =
        imageMetadata.instance.columns || image.columns;
  
      imageMetadata.instance.sopClassUid =
        imageMetadata.instance.sopClassUid ||
        this.getFromDataSet(image.data, 'string', 'x00080016');
      imageMetadata.instance.sopInstanceUid =
        imageMetadata.instance.sopInstanceUid ||
        this.getFromDataSet(image.data, 'string', 'x00080018');
  
      imageMetadata.instance.pixelSpacing =
        imageMetadata.instance.pixelSpacing ||
        this.getFromDataSet(image.data, 'string', 'x00280030');
      imageMetadata.instance.frameOfReferenceUID =
        imageMetadata.instance.frameOfReferenceUID ||
        this.getFromDataSet(image.data, 'string', 'x00200052');
      imageMetadata.instance.imageOrientationPatient =
        imageMetadata.instance.imageOrientationPatient ||
        this.getFromDataSet(image.data, 'string', 'x00200037');
      imageMetadata.instance.imagePositionPatient =
        imageMetadata.instance.imagePositionPatient ||
        this.getFromDataSet(image.data, 'string', 'x00200032');
  
      imageMetadata.instance.sliceThickness =
        imageMetadata.instance.sliceThickness ||
        this.getFromDataSet(image.data, 'string', 'x00180050');
      imageMetadata.instance.sliceLocation =
        imageMetadata.instance.sliceLocation ||
        this.getFromDataSet(image.data, 'string', 'x00201041');
      imageMetadata.instance.tablePosition =
        imageMetadata.instance.tablePosition ||
        this.getFromDataSet(image.data, 'string', 'x00189327');
      imageMetadata.instance.spacingBetweenSlices =
        imageMetadata.instance.spacingBetweenSlices ||
        this.getFromDataSet(image.data, 'string', 'x00180088');
  
      imageMetadata.instance.lossyImageCompression =
        imageMetadata.instance.lossyImageCompression ||
        this.getFromDataSet(image.data, 'string', 'x00282110');
      imageMetadata.instance.lossyImageCompressionRatio =
        imageMetadata.instance.lossyImageCompressionRatio ||
        this.getFromDataSet(image.data, 'string', 'x00282112');
  
      imageMetadata.instance.frameIncrementPointer =
        imageMetadata.instance.frameIncrementPointer ||
        this.getFromDataSet(image.data, 'string', 'x00280009');
      imageMetadata.instance.frameTime =
        imageMetadata.instance.frameTime ||
        this.getFromDataSet(image.data, 'string', 'x00181063');
      imageMetadata.instance.frameTimeVector =
        imageMetadata.instance.frameTimeVector ||
        this.getFromDataSet(image.data, 'string', 'x00181065');
  
      if (
        (image.data || image.instance) &&
        !imageMetadata.instance.multiframeMetadata
      ) {
        imageMetadata.instance.multiframeMetadata = this.getMultiframeModuleMetadata(
          image
        );
      }
  
      imageMetadata.imagePlane =
        imageMetadata.imagePlane || this.getImagePlane(imageMetadata.instance);
    }
  
    /**
     * Constructs and returns the imagePlane given the metadata instance
     *
     * @param metadataInstance The metadata instance (InstanceMetadata class) containing information to construct imagePlane
     * @returns imagePlane The constructed imagePlane to be used in viewer easily
     */
    getImagePlane(instance) {
      if (
        !instance.rows ||
        !instance.columns ||
        !instance.pixelSpacing ||
        !instance.frameOfReferenceUID ||
        !instance.imageOrientationPatient ||
        !instance.imagePositionPatient
      ) {
        return;
      }
  
      const imageOrientation = instance.imageOrientationPatient.split('\\');
      const imagePosition = instance.imagePositionPatient.split('\\');
  
      let columnPixelSpacing = 1.0;
      let rowPixelSpacing = 1.0;
      if (instance.pixelSpacing) {
        const split = instance.pixelSpacing.split('\\');
        rowPixelSpacing = parseFloat(split[0]);
        columnPixelSpacing = parseFloat(split[1]);
      }
  
      return {
        frameOfReferenceUID: instance.frameOfReferenceUID,
        rows: instance.rows,
        columns: instance.columns,
        rowCosines: [
          parseFloat(imageOrientation[0]),
          parseFloat(imageOrientation[1]),
          parseFloat(imageOrientation[2]),
        ],
        columnCosines: [
          parseFloat(imageOrientation[3]),
          parseFloat(imageOrientation[4]),
          parseFloat(imageOrientation[5]),
        ],
        imagePositionPatient: [
          parseFloat(imagePosition[0]),
          parseFloat(imagePosition[1]),
          parseFloat(imagePosition[2]),
        ],
        rowPixelSpacing,
        columnPixelSpacing,
      };
    }
  
    /**
     * This function extracts miltiframe information from a dicomParser.DataSet object.
     *
     * @param dataSet {Object} An instance of dicomParser.DataSet object where multiframe information can be found.
     * @return {Object} An object containing multiframe image metadata (frameIncrementPointer, frameTime, frameTimeVector, etc).
     */
    getMultiframeModuleMetadata(image) {
      const imageInfo = {
        isMultiframeImage: false,
        frameIncrementPointer: null,
        numberOfFrames: 0,
        frameTime: 0,
        frameTimeVector: null,
        averageFrameRate: 0, // backwards compatibility only... it might be useless in the future
      };
  
      let frameTime;
  
      const numberOfFrames = this.getFromImage(
        image,
        'intString',
        'x00280008',
        'numberOfFrames',
        -1
      );
  
      if (numberOfFrames > 0) {
        // set multi-frame image indicator
        imageInfo.isMultiframeImage = true;
        imageInfo.numberOfFrames = numberOfFrames;
  
        // (0028,0009) = Frame Increment Pointer
        const frameIncrementPointer = this.getFrameIncrementPointer(image);
  
        if (frameIncrementPointer === 'frameTimeVector') {
          // Frame Increment Pointer points to Frame Time Vector (0018,1065) field
          const frameTimeVector = this.getFrameTimeVector(image);
  
          if (frameTimeVector instanceof Array && frameTimeVector.length > 0) {
            imageInfo.frameIncrementPointer = frameIncrementPointer;
            imageInfo.frameTimeVector = frameTimeVector;
            frameTime =
              frameTimeVector.reduce((a, b) => a + b) / frameTimeVector.length;
            imageInfo.averageFrameRate = 1000 / frameTime;
          }
        } else if (
          frameIncrementPointer === 'frameTime' ||
          frameIncrementPointer === ''
        ) {
          frameTime = this.getFrameTime(image);
  
          if (frameTime > 0) {
            imageInfo.frameIncrementPointer = frameIncrementPointer;
            imageInfo.frameTime = frameTime;
            imageInfo.averageFrameRate = 1000 / frameTime;
          }
        }
      }
  
      return imageInfo;
    }
  
    /**
     * Get a bound reference to the provider function.
     */
    getProvider() {
      let provider = this._provider;
      if (typeof this._provider !== FUNCTION) {
        provider = this.provider.bind(this);
        this._provider = provider;
      }
  
      return provider;
    }
  
    /**
     * Looks up metadata for Cornerstone Tools given a specified type and imageId
     * A type may be, e.g. 'study', or 'patient', or 'imagePlaneModule'. These types
     * are keys in the stored metadata objects.
     *
     * @param type
     * @param imageId
     * @returns {Object} Relevant metadata of the specified type
     */
    provider(type, imageId) {
      const imageMetadata = this.metadataLookup.get(imageId);
      if (!imageMetadata) {
        return;
      }
  
      if (imageMetadata.hasOwnProperty(type)) {
        return imageMetadata[type];
      }
    }
  }
  
  export default MetadataProvider;
  