import OHIF from '@ohif/core';
import dcmjs from 'dcmjs';
import cornerstone from 'cornerstone-core';
import cornerstoneTools from 'cornerstone-tools';
import transformPointsToImagePlane from './utils/transformPointsToImagePlane';
import TOOL_NAMES from './utils/toolNames';
import { vec3 } from 'gl-matrix';

const dicomlab2RGB = dcmjs.data.Colors.dicomlab2RGB;
const globalImageIdSpecificToolStateManager =
  cornerstoneTools.globalImageIdSpecificToolStateManager;
const { DicomLoaderService } = OHIF.utils;

export default async function loadRTStruct(
  rtStructDisplaySet,
  referencedDisplaySet,
  studies
) {
  const rtStructModule = cornerstoneTools.getModule('rtstruct');
  // Set here is loading is asynchronous.
  // If this function throws its set back to false.
  rtStructDisplaySet.isLoaded = true;

  const { StudyInstanceUID, SeriesInstanceUID } = referencedDisplaySet;

  const segArrayBuffer = await DicomLoaderService.findDicomDataPromise(
    rtStructDisplaySet,
    studies
  );

  const dicomData = dcmjs.data.DicomMessage.readFile(segArrayBuffer);
  const rtStructDataset = dcmjs.data.DicomMetaDictionary.naturalizeDataset(
    dicomData.dict
  );

  rtStructDataset._meta = dcmjs.data.DicomMetaDictionary.namifyDataset(
    dicomData.meta
  );

  // global cornerstone tools state to attach measurements to.
  const toolState = globalImageIdSpecificToolStateManager.saveToolState();

  const {
    StructureSetROISequence,
    ROIContourSequence,
    RTROIObservationsSequence,
    StructureSetLabel,
  } = rtStructDataset;

  // Define our structure set entry and add it to the rtstruct module state.
  const structureSet = {
    StructureSetLabel,
    SeriesInstanceUID: rtStructDataset.SeriesInstanceUID,
    ROIContours: [],
    referencedSeriesSequence:
      rtStructDisplaySet.metadata.ReferencedSeriesSequence,
    visible: true,
  };

  rtStructModule.setters.structureSet(structureSet);

  const imageIdSopInstanceUidPairs = _getImageIdSopInstanceUidPairsForDisplaySet(
    studies,
    StudyInstanceUID,
    SeriesInstanceUID
  );

  const rtStructDisplayToolName = TOOL_NAMES.RTSTRUCT_DISPLAY_TOOL;

  for (let i = 0; i < ROIContourSequence.length; i++) {
    const ROIContour = ROIContourSequence[i];
    const { ReferencedROINumber, ContourSequence } = ROIContour;

    if (!ContourSequence) {
      continue;
    }

    const isSupported = false;

    const ContourSequenceArray = _toArray(ContourSequence);

    for (let c = 0; c < ContourSequenceArray.length; c++) {
      const {
        ContourImageSequence,
        ContourData,
        NumberOfContourPoints,
        ContourGeometricType,
      } = ContourSequenceArray[c];

      const sopInstanceUID = ContourImageSequence
        ? ContourImageSequence.ReferencedSOPInstanceUID
        : _getClosestSOPInstanceUID(
            ContourData,
            ContourGeometricType,
            NumberOfContourPoints,
            imageIdSopInstanceUidPairs
          );
      const imageId = _getImageId(imageIdSopInstanceUidPairs, sopInstanceUID);

      if (!imageId) {
        continue;
      }

      const imageIdSpecificToolData = _getOrCreateImageIdSpecificToolData(
        toolState,
        imageId,
        rtStructDisplayToolName
      );

      const imagePlane = cornerstone.metaData.get('imagePlaneModule', imageId);
      const points = [];
      let measurementData;

      switch (ContourGeometricType) {
        case 'CLOSED_PLANAR':
        case 'OPEN_PLANAR':
        case 'POINT':
          isSupported = true;

          for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
            points.push({
              x: ContourData[p],
              y: ContourData[p + 1],
              z: ContourData[p + 2],
            });
          }

          transformPointsToImagePlane(points, imagePlane);

          measurementData = {
            handles: {
              points,
            },
            type: ContourGeometricType,
            structureSetSeriesInstanceUid: rtStructDataset.SeriesInstanceUID,
            ROINumber: ReferencedROINumber,
          };

          imageIdSpecificToolData.push(measurementData);
          break;
        default:
          continue;
      }
    }

    _setROIContourMetadata(
      structureSet,
      StructureSetROISequence,
      RTROIObservationsSequence,
      ROIContour,
      isSupported
    );
  }

  _setToolEnabledIfNotEnabled(rtStructDisplayToolName);

  /*
   * TODO: Improve the way we notify parts of the app that depends on rts to be loaded.
   *
   * Currently we are using a non-ideal implementation through a custom event to notify the rtstruct panel
   * or other components that could rely on loaded rtstructs that
   * the first batch of structs were loaded so that e.g. when the user opens the panel
   * before the structs are fully loaded, the panel can subscribe to this custom event
   * and update itself with the new structs.
   *
   * This limitation is due to the fact that the rtmodule is an object (which will be
   * updated after the structs are loaded) that React its not aware of its changes
   * because the module object its not passed in to the panel component as prop but accessed externally.
   *
   * Improving this event approach to something reactive that can be tracked inside the react lifecycle,
   * allows us to easily watch the module or the rtstruct loading process in any other component
   * without subscribing to external events.
   */
  const event = new CustomEvent('extensiondicomrtrtloaded', {
    detail: {
      rtStructDisplaySet,
      referencedDisplaySet,
      studies,
    },
  });
  document.dispatchEvent(event);
}

function _setROIContourMetadata(
  structureSet,
  StructureSetROISequence,
  RTROIObservationsSequence,
  ROIContour,
  isSupported
) {
  const StructureSetROI = StructureSetROISequence.find(
    structureSetROI =>
      structureSetROI.ROINumber === ROIContour.ReferencedROINumber
  );

  const ROIContourData = {
    ROINumber: StructureSetROI.ROINumber,
    ROIName: StructureSetROI.ROIName,
    ROIGenerationAlgorithm: StructureSetROI.ROIGenerationAlgorithm,
    ROIDescription: StructureSetROI.ROIDescription,
    isSupported,
    visible: true,
  };

  _setROIContourDataColor(ROIContour, ROIContourData);

  if (RTROIObservationsSequence) {
    // If present, add additional RTROIObservations metadata.
    _setROIContourRTROIObservations(
      ROIContourData,
      RTROIObservationsSequence,
      ROIContour.ReferencedROINumber
    );
  }

  structureSet.ROIContours.push(ROIContourData);
}

function _setROIContourDataColor(ROIContour, ROIContourData) {
  let { ROIDisplayColor, RecommendedDisplayCIELabValue } = ROIContour;

  if (!ROIDisplayColor && RecommendedDisplayCIELabValue) {
    // If ROIDisplayColor is absent, try using the RecommendedDisplayCIELabValue color.
    ROIDisplayColor = dicomlab2RGB(RecommendedDisplayCIELabValue);
  }

  if (ROIDisplayColor) {
    ROIContourData.colorArray = [...ROIDisplayColor];
  } else {
    //Choose a color from the cornerstoneTools colorLUT
    // We sample from the default color LUT here (i.e. 0), as we have nothing else to go on.
    const { getters } = cornerstoneTools.getModule('segmentation');
    const color = getters.colorForSegmentIndexColorLUT(
      0,
      ROIContourData.ROINumber
    );

    ROIContourData.colorArray = [...color];
  }
}

function _setROIContourRTROIObservations(
  ROIContourData,
  RTROIObservationsSequence,
  ROINumber
) {
  const RTROIObservations = RTROIObservationsSequence.find(
    RTROIObservations => RTROIObservations.ReferencedROINumber === ROINumber
  );

  if (RTROIObservations) {
    // Deep copy so we don't keep the reference to the dcmjs dataset entry.
    const {
      ObservationNumber,
      ROIObservationDescription,
      RTROIInterpretedType,
      ROIInterpreter,
    } = RTROIObservations;

    ROIContourData.RTROIObservations = {
      ObservationNumber,
      ROIObservationDescription,
      RTROIInterpretedType,
      ROIInterpreter,
    };
  }
}

function _setToolEnabledIfNotEnabled(toolName) {
  cornerstone.getEnabledElements().forEach(enabledElement => {
    const { element, image } = enabledElement;
    const tool = cornerstoneTools.getToolForElement(element, toolName);

    if (tool.mode !== 'enabled') {
      // If not already active or passive, set passive so contours render.
      cornerstoneTools.setToolEnabled(toolName);
    }

    if (image) {
      cornerstone.updateImage(element);
    }
  });
}

function _getOrCreateImageIdSpecificToolData(toolState, imageId, toolName) {
  if (toolState.hasOwnProperty(imageId) === false) {
    toolState[imageId] = {};
  }

  const imageIdToolState = toolState[imageId];

  // If we don't have tool state for this type of tool, add an empty object
  if (imageIdToolState.hasOwnProperty(toolName) === false) {
    imageIdToolState[toolName] = {
      data: [],
    };
  }

  return imageIdToolState[toolName].data;
}

const _getImageId = (imageIdSopInstanceUidPairs, sopInstanceUID) => {
  const imageIdSopInstanceUidPairsEntry = imageIdSopInstanceUidPairs.find(
    imageIdSopInstanceUidPairsEntry =>
      imageIdSopInstanceUidPairsEntry.sopInstanceUID === sopInstanceUID
  );

  return imageIdSopInstanceUidPairsEntry
    ? imageIdSopInstanceUidPairsEntry.imageId
    : null;
};

function _getImageIdSopInstanceUidPairsForDisplaySet(
  studies,
  StudyInstanceUID,
  SeriesInstanceUID
) {
  const study = studies.find(
    study => study.StudyInstanceUID === StudyInstanceUID
  );

  const displaySets = study.displaySets.filter(set => {
    return set.SeriesInstanceUID === SeriesInstanceUID;
  });

  if (displaySets.length > 1) {
    console.warn(
      'More than one display set with the same SeriesInstanceUID. This is not supported yet...'
    );
    // TODO -> We could make check the instance list and see if any match?
    // Do we split the segmentation into two cornerstoneTools segmentations if there are images in both series?
    // ^ Will that even happen?
  }

  const referencedDisplaySet = displaySets[0];

  return referencedDisplaySet.images.map(image => {
    return {
      imageId: image.getImageId(),
      sopInstanceUID: image.getSOPInstanceUID(),
    };
  });
}

function _toArray(objOrArray) {
  return Array.isArray(objOrArray) ? objOrArray : [objOrArray];
}

function _getClosestSOPInstanceUID(
  ContourData,
  ContourGeometricType,
  NumberOfContourPoints,
  imageIdSopInstanceUidPairs
) {
  const closest = {
    distance: Infinity,
    sopInstanceUID: null,
  };

  let point;

  switch (ContourGeometricType) {
    case 'POINT':
      point = ContourData;
      break;
    case 'CLOSED_PLANAR':
    case 'OPEN_PLANAR':
      // These are defined as planar, so get the of the region to get the
      // Best mapping to a plane even if its potentially off center.

      point = [0, 0, 0];
      for (let p = 0; p < NumberOfContourPoints * 3; p += 3) {
        point[0] += ContourData[p];
        point[1] += ContourData[p + 1];
        point[2] += ContourData[p + 2];
      }

      point[0] /= NumberOfContourPoints;
      point[1] /= NumberOfContourPoints;
      point[2] /= NumberOfContourPoints;
  }

  imageIdSopInstanceUidPairs.forEach(pair => {
    const { imageId } = pair;

    const imagePlaneModule = cornerstone.metaData.get(
      'imagePlaneModule',
      imageId
    );

    const distance = distanceFromPointToPlane(point, imagePlaneModule);

    if (distance < closest.distance) {
      closest.distance = distance;
      closest.sopInstanceUID = pair.sopInstanceUID;
    }
  });

  return closest.sopInstanceUID;
}

/**
 *
 * @param {number[3]} P - The point
 * @param {object} imagePlaneModule The cornerstone metadata object for the imagePlane
 */
function distanceFromPointToPlane(P, imagePlaneModule) {
  const {
    rowCosines,
    columnCosines,
    imagePositionPatient: Q,
  } = imagePlaneModule;

  let N = [];
  vec3.cross(N, rowCosines, columnCosines);

  const [A, B, C] = N;

  const D = -A * Q[0] - B * Q[1] - C * Q[2];

  return Math.abs(A * P[0] + B * P[1] + C * P[2] + D); // Denominator is sqrt(A**2 + B**2 + C**2) which is 1 as its a normal vector
}
