import * as importedActions from './actions';
import { utilities, Enums } from '@cornerstonejs/tools';
import { cache } from '@cornerstonejs/core';

const LABELMAP = Enums.SegmentationRepresentations.Labelmap;

const commandsModule = ({ commandsManager, servicesManager }: withAppTypes) => {
  const services = servicesManager.services;
  const { displaySetService, viewportGridService, segmentationService } = services;

  const actions = {
    ...importedActions,
    getDynamic4DDisplaySet: () => {
      const displaySets = displaySetService.getActiveDisplaySets();

      const dynamic4DDisplaySet = displaySets.find(displaySet => {
        const anInstance = displaySet.instances?.[0];

        if (anInstance) {
          return (
            anInstance.FrameReferenceTime !== undefined ||
            anInstance.NumberOfTimeSlices !== undefined ||
            anInstance.TemporalPositionIdentifier !== undefined
          );
        }

        return false;
      });

      return dynamic4DDisplaySet;
    },
    getComputedDisplaySets: () => {
      const displaySetCache = displaySetService.getDisplaySetCache();
      const cachedDisplaySets = [...displaySetCache.values()];
      const computedDisplaySets = cachedDisplaySets.filter(displaySet => {
        return displaySet.isDerived;
      });
      return computedDisplaySets;
    },
    exportTimeReportCSV: ({ segmentations, config, options, summaryStats }) => {
      const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();

      const volumeId = dynamic4DDisplaySet?.displaySetInstanceUID;

      // cache._volumeCache is a map that has a key that includes the volumeId
      // it is not exactly the volumeId, but it is the key that includes the volumeId
      // so we can't do cache._volumeCache.get(volumeId) we should iterate
      // over the keys and find the one that includes the volumeId
      let volumeCacheKey: string | undefined;

      for (const [key] of cache._volumeCache) {
        if (key.includes(volumeId)) {
          volumeCacheKey = key;
          break;
        }
      }

      let dynamicVolume;
      if (volumeCacheKey) {
        dynamicVolume = cache.getVolume(volumeCacheKey);
      }

      const instance = dynamic4DDisplaySet.instances[0];

      const csv = [];

      // CSV header information with placeholder empty values for the metadata lines
      csv.push(`Patient ID,${instance.PatientID},`);
      csv.push(`Study Date,${instance.StudyDate},`);
      csv.push(`StudyInstanceUID,${instance.StudyInstanceUID},`);
      csv.push(`StudyDescription,${instance.StudyDescription},`);
      csv.push(`SeriesInstanceUID,${instance.SeriesInstanceUID},`);

      // empty line
      csv.push('');
      csv.push('');

      // Helper function to calculate standard deviation
      function calculateStandardDeviation(data) {
        const n = data.length;
        const mean = data.reduce((acc, value) => acc + value, 0) / n;
        const squaredDifferences = data.map(value => (value - mean) ** 2);
        const variance = squaredDifferences.reduce((acc, value) => acc + value, 0) / n;
        const stdDeviation = Math.sqrt(variance);
        return stdDeviation;
      }
      // Iterate through each segmentation to get the timeData and ijkCoords
      segmentations.forEach(segmentation => {
        const volume = segmentationService.getLabelmapVolume(segmentation.segmentationId);
        const [timeData, ijkCoords] = utilities.dynamicVolume.getDataInTime(dynamicVolume, {
          maskVolumeId: volume.volumeId,
        }) as number[][];

        if (summaryStats) {
          // Adding column headers for pixel identifier and segmentation label ids
          let headers = 'Operation,Segmentation Label ID';
          const maxLength = dynamicVolume.numTimePoints;
          for (let t = 0; t < maxLength; t++) {
            headers += `,Time Point ${t}`;
          }
          csv.push(headers);
          // // perform summary statistics on the timeData including for each time point, mean, median, min, max, and standard deviation for
          // // all the voxels in the ROI
          const mean = [];
          const min = [];
          const minIJK = [];
          const max = [];
          const maxIJK = [];
          const std = [];

          const numVoxels = timeData.length;
          // Helper function to calculate standard deviation
          for (let timeIndex = 0; timeIndex < maxLength; timeIndex++) {
            // for each voxel in the ROI, get the value at the current time point
            const voxelValues = [];
            let sum = 0;
            let minValue = Infinity;
            let maxValue = -Infinity;
            let minIndex = 0;
            let maxIndex = 0;

            // Single pass through the data to collect all needed values
            for (let voxelIndex = 0; voxelIndex < numVoxels; voxelIndex++) {
              const value = timeData[voxelIndex][timeIndex];
              voxelValues.push(value);
              sum += value;

              if (value < minValue) {
                minValue = value;
                minIndex = voxelIndex;
              }
              if (value > maxValue) {
                maxValue = value;
                maxIndex = voxelIndex;
              }
            }

            mean.push(sum / numVoxels);
            min.push(minValue);
            minIJK.push(ijkCoords[minIndex]);
            max.push(maxValue);
            maxIJK.push(ijkCoords[maxIndex]);
            std.push(calculateStandardDeviation(voxelValues));
          }

          let row = `Mean,${segmentation.label}`;
          // Generate separate rows for each statistic
          for (let t = 0; t < maxLength; t++) {
            row += `,${mean[t]}`;
          }

          csv.push(row);

          row = `Standard Deviation,${segmentation.label}`;
          for (let t = 0; t < maxLength; t++) {
            row += `,${std[t]}`;
          }

          csv.push(row);

          row = `Min,${segmentation.label}`;
          for (let t = 0; t < maxLength; t++) {
            row += `,${min[t]}`;
          }

          csv.push(row);

          row = `Max,${segmentation.label}`;
          for (let t = 0; t < maxLength; t++) {
            row += `,${max[t]}`;
          }

          csv.push(row);
        } else {
          // Adding column headers for pixel identifier and segmentation label ids
          let headers = 'Pixel Identifier (IJK),Segmentation Label ID';
          const maxLength = dynamicVolume.numTimePoints;
          for (let t = 0; t < maxLength; t++) {
            headers += `,Time Point ${t}`;
          }
          csv.push(headers);
          // Assuming timeData and ijkCoords are of the same length
          for (let i = 0; i < timeData.length; i++) {
            // Generate the pixel identifier
            const pixelIdentifier = `${ijkCoords[i][0]}_${ijkCoords[i][1]}_${ijkCoords[i][2]}`;

            // Start a new row for the current pixel
            let row = `${pixelIdentifier},${segmentation.label}`;

            // Add time data points for this pixel
            for (let t = 0; t < timeData[i].length; t++) {
              row += `,${timeData[i][t]}`;
            }

            // Append the row to the CSV array
            csv.push(row);
          }
        }
      });

      // Convert to CSV string
      const csvContent = csv.join('\n');

      // Generate filename and trigger download
      const filename = `${instance.PatientID}.csv`;
      const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
      const link = document.createElement('a');
      const url = URL.createObjectURL(blob);
      link.setAttribute('href', url);
      link.setAttribute('download', filename);
      link.style.visibility = 'hidden';
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
    },
    swapDynamicWithComputedDisplaySet: ({ displaySet }) => {
      const computedDisplaySet = displaySet;

      const displaySetCache = displaySetService.getDisplaySetCache();
      const cachedDisplaySetKeys = [displaySetCache.keys()];
      const { displaySetInstanceUID } = computedDisplaySet;
      // Check to see if computed display set is already in cache
      if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
        displaySetCache.set(displaySetInstanceUID, computedDisplaySet);
      }

      // Get all viewports and their corresponding indices
      const { viewports } = viewportGridService.getState();

      // get the viewports in the grid
      // iterate over them and find the ones that are showing a dynamic
      // volume (displaySet), and replace that exact displaySet with the
      // computed displaySet

      const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();

      const viewportsToUpdate = [];

      for (const [key, value] of viewports) {
        const viewport = value;
        const viewportOptions = viewport.viewportOptions;
        const { displaySetInstanceUIDs } = viewport;
        const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
          dynamic4DDisplaySet.displaySetInstanceUID
        );
        if (displaySetInstanceUIDIndex !== -1) {
          const newViewport = {
            viewportId: viewport.viewportId,
            // merge the other displaySetInstanceUIDs with the new one
            displaySetInstanceUIDs: [
              ...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
              displaySetInstanceUID,
              ...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
            ],
            viewportOptions: {
              initialImageOptions: viewportOptions.initialImageOptions,
              viewportType: 'volume',
              orientation: viewportOptions.orientation,
              background: viewportOptions.background,
            },
          };
          viewportsToUpdate.push(newViewport);
        }
      }

      viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
    },
    swapComputedWithDynamicDisplaySet: () => {
      // Todo: this assumes there is only one dynamic display set in the viewer
      const dynamicDisplaySet = actions.getDynamic4DDisplaySet();

      const displaySetCache = displaySetService.getDisplaySetCache();
      const cachedDisplaySetKeys = [...displaySetCache.keys()]; // Fix: Spread to get the array
      const { displaySetInstanceUID } = dynamicDisplaySet;

      // Check to see if dynamic display set is already in cache
      if (!cachedDisplaySetKeys.includes(displaySetInstanceUID)) {
        displaySetCache.set(displaySetInstanceUID, dynamicDisplaySet);
      }

      // Get all viewports and their corresponding indices
      const { viewports } = viewportGridService.getState();

      // Get the computed 4D display set
      const computed4DDisplaySet = actions.getComputedDisplaySets()[0];

      const viewportsToUpdate = [];

      for (const [key, value] of viewports) {
        const viewport = value;
        const viewportOptions = viewport.viewportOptions;
        const { displaySetInstanceUIDs } = viewport;
        const displaySetInstanceUIDIndex = displaySetInstanceUIDs.indexOf(
          computed4DDisplaySet.displaySetInstanceUID
        );
        if (displaySetInstanceUIDIndex !== -1) {
          const newViewport = {
            viewportId: viewport.viewportId,
            // merge the other displaySetInstanceUIDs with the new one
            displaySetInstanceUIDs: [
              ...displaySetInstanceUIDs.slice(0, displaySetInstanceUIDIndex),
              displaySetInstanceUID,
              ...displaySetInstanceUIDs.slice(displaySetInstanceUIDIndex + 1),
            ],
            viewportOptions: {
              initialImageOptions: viewportOptions.initialImageOptions,
              viewportType: 'volume',
              orientation: viewportOptions.orientation,
              background: viewportOptions.background,
            },
          };
          viewportsToUpdate.push(newViewport);
        }
      }

      viewportGridService.setDisplaySetsForViewports(viewportsToUpdate);
    },
    createNewLabelMapForDynamicVolume: async ({ label }) => {
      const { viewports, activeViewportId } = viewportGridService.getState();

      // get the dynamic 4D display set
      const dynamic4DDisplaySet = actions.getDynamic4DDisplaySet();
      const dynamic4DDisplaySetInstanceUID = dynamic4DDisplaySet.displaySetInstanceUID;

      // check if the dynamic 4D display set is in the display, if not we might have
      // the computed volumes and we should choose them for the segmentation
      // creation

      let referenceDisplaySet;

      const activeViewport = viewports.get(activeViewportId);
      const activeDisplaySetInstanceUIDs = activeViewport.displaySetInstanceUIDs;
      const dynamicIsInActiveViewport = activeDisplaySetInstanceUIDs.includes(
        dynamic4DDisplaySetInstanceUID
      );

      if (dynamicIsInActiveViewport) {
        referenceDisplaySet = dynamic4DDisplaySet;
      }

      if (!referenceDisplaySet) {
        // try to see if there is any derived displaySet in the active viewport
        // which is referencing the dynamic 4D display set

        // Todo: this is wrong but I don't have time to fix it now
        const cachedDisplaySets = displaySetService.getDisplaySetCache();
        for (const [key, displaySet] of cachedDisplaySets) {
          if (displaySet.referenceDisplaySetUID === dynamic4DDisplaySetInstanceUID) {
            referenceDisplaySet = displaySet;
            break;
          }
        }
      }

      if (!referenceDisplaySet) {
        throw new Error('No reference display set found based on the dynamic data');
      }

      const displaySet = displaySetService.getDisplaySetByUID(
        referenceDisplaySet.displaySetInstanceUID
      );

      const segmentationId = await segmentationService.createLabelmapForDisplaySet(displaySet, {
        label,
      });

      const firstViewport = viewports.values().next().value;

      await segmentationService.addSegmentationRepresentation(firstViewport.viewportId, {
        segmentationId,
      });

      return segmentationId;
    },
  };

  const definitions = {
    updateSegmentationsChartDisplaySet: {
      commandFn: actions.updateSegmentationsChartDisplaySet,
      storeContexts: [],
      options: {},
    },
    exportTimeReportCSV: {
      commandFn: actions.exportTimeReportCSV,
      storeContexts: [],
      options: {},
    },
    swapDynamicWithComputedDisplaySet: {
      commandFn: actions.swapDynamicWithComputedDisplaySet,
      storeContexts: [],
      options: {},
    },
    createNewLabelMapForDynamicVolume: {
      commandFn: actions.createNewLabelMapForDynamicVolume,
      storeContexts: [],
      options: {},
    },
    swapComputedWithDynamicDisplaySet: {
      commandFn: actions.swapComputedWithDynamicDisplaySet,
      storeContexts: [],
      options: {},
    },
  };

  return {
    actions,
    definitions,
    defaultContext: 'DYNAMIC-VOLUME:CORNERSTONE',
  };
};

export default commandsModule;
