import * as cornerstoneTools from 'cornerstone-tools'
const external = cornerstoneTools.external

// State
const getToolState = cornerstoneTools.getToolState
const toolStyle = cornerstoneTools.toolStyle
const toolColors = cornerstoneTools.toolColors

// Drawing
const getNewContext = cornerstoneTools.import('drawing/getNewContext')
const draw = cornerstoneTools.import('drawing/draw')
const drawHandles = cornerstoneTools.import('drawing/drawHandles')
const drawRect = cornerstoneTools.import('drawing/drawRect')
const drawLinkedTextBox = cornerstoneTools.import('drawing/drawLinkedTextBox')
const setShadow = cornerstoneTools.import('drawing/setShadow')

// Util
const calculateSUV = cornerstoneTools.import('util/calculateSUV')
const getROITextBoxCoords = cornerstoneTools.import('util/getROITextBoxCoords')
const numbersWithCommas = cornerstoneTools.import('util/numbersWithCommas')
const throttle = cornerstoneTools.import('util/throttle')
const rectangleRoiCursor = cornerstoneTools.import('tools/cursors')
const getPixelSpacing = cornerstoneTools.import('util/getPixelSpacing')
const getModule = cornerstoneTools.getModule

/**
 * @public
 * @class RectangleRoiTool
 * @memberof Tools.Annotation
 * @classdesc Tool for drawing rectangular regions of interest, and measuring
 * the statistics of the enclosed pixels.
 * @extends Tools.Base.BaseAnnotationTool
 */
export default class RectangleRoiTool extends cornerstoneTools.RectangleRoiTool {
  constructor(props = {}) {
    const defaultProps = {
      name: 'RectangleRoi',
      supportedInteractionTypes: ['Mouse', 'Touch'],
      configuration: {
        drawHandles: true,
        drawHandlesOnHover: false,
        hideHandlesIfMoving: false,
        renderDashed: false,
        showStatsText: false
        // showMinMax: false,
        // showHounsfieldUnits: true
      },
      svgCursor: rectangleRoiCursor
    }

    super(props, defaultProps)

    this.throttledUpdateCachedStats = throttle(this.updateCachedStats, 110)
  }

  createNewMeasurement(eventData) {
    const goodEventData =
      eventData && eventData.currentPoints && eventData.currentPoints.image

    if (!goodEventData) {
      console.log(
        `required eventData not supplied to tool ${this.name}'s createNewMeasurement`
      )

      return
    }

    return {
      visible: true,
      active: true,
      color: undefined,
      invalidated: true,
      handles: {
        start: {
          x: eventData.currentPoints.image.x,
          y: eventData.currentPoints.image.y,
          highlight: true,
          active: false
        },
        end: {
          x: eventData.currentPoints.image.x,
          y: eventData.currentPoints.image.y,
          highlight: true,
          active: true
        },
        initialRotation: eventData.viewport.rotation,
        textBox: {
          active: false,
          hasMoved: false,
          movesIndependently: false,
          drawnIndependently: true,
          allowedOutsideImage: true,
          hasBoundingBox: true
        }
      }
    }
  }

  pointNearTool(element, data, coords, interactionType) {
    const hasStartAndEndHandles =
      data && data.handles && data.handles.start && data.handles.end
    const validParameters = hasStartAndEndHandles

    if (!validParameters) {
      console.log(
        `invalid parameters supplied to tool ${this.name}'s pointNearTool`
      )
    }

    if (!validParameters || data.visible === false) {
      return false
    }

    const distance = interactionType === 'mouse' ? 15 : 25
    const startCanvas = external.cornerstone.pixelToCanvas(
      element,
      data.handles.start
    )
    const endCanvas = external.cornerstone.pixelToCanvas(
      element,
      data.handles.end
    )

    const rect = {
      left: Math.min(startCanvas.x, endCanvas.x),
      top: Math.min(startCanvas.y, endCanvas.y),
      width: Math.abs(startCanvas.x - endCanvas.x),
      height: Math.abs(startCanvas.y - endCanvas.y)
    }

    const distanceToPoint = external.cornerstoneMath.rect.distanceToPoint(
      rect,
      coords
    )

    return distanceToPoint < distance
  }

  updateCachedStats(image, element, data) {
    const seriesModule =
      external.cornerstone.metaData.get('generalSeriesModule', image.imageId) ||
      {}
    const modality = seriesModule.modality
    const pixelSpacing = getPixelSpacing(image)

    const stats = _calculateStats(
      image,
      element,
      data.handles,
      modality,
      pixelSpacing
    )

    data.cachedStats = stats
    data.invalidated = false
  }

  renderToolData(evt) {
    const toolData = getToolState(evt.currentTarget, this.name)

    if (!toolData) {
      return
    }

    const eventData = evt.detail
    const { image, element } = eventData
    const lineWidth = toolStyle.getToolWidth()
    const lineDash = getModule('globalConfiguration').configuration.lineDash
    const {
      handleRadius,
      drawHandlesOnHover,
      hideHandlesIfMoving,
      renderDashed
    } = this.configuration
    const context = getNewContext(eventData.canvasContext.canvas)
    const { rowPixelSpacing, colPixelSpacing } = getPixelSpacing(image)

    // Meta
    const seriesModule =
      external.cornerstone.metaData.get('generalSeriesModule', image.imageId) ||
      {}

    // Pixel Spacing
    const modality = seriesModule.modality
    const hasPixelSpacing = rowPixelSpacing && colPixelSpacing

    draw(context, context => {
      // If we have tool data for this element - iterate over each set and draw it
      for (let i = 0; i < toolData.data.length; i++) {
        const data = toolData.data[i]

        if (data.visible === false) {
          continue
        }

        // Configure
        const color = toolColors.getColorIfActive(data)
        const handleOptions = {
          color,
          handleRadius,
          drawHandlesIfActive: drawHandlesOnHover,
          hideHandlesIfMoving
        }

        setShadow(context, this.configuration)

        const rectOptions = { color }

        if (renderDashed) {
          rectOptions.lineDash = lineDash
        }

        // Draw
        drawRect(
          context,
          element,
          data.handles.start,
          data.handles.end,
          rectOptions,
          'pixel',
          data.handles.initialRotation
        )

        if (this.configuration.drawHandles) {
          drawHandles(context, eventData, data.handles, handleOptions)
        }

        // Update textbox stats
        if (data.invalidated === true) {
          if (data.cachedStats) {
            this.throttledUpdateCachedStats(image, element, data)
          } else {
            this.updateCachedStats(image, element, data)
          }
        }

        // Default to textbox on right side of ROI
        if (!data.handles.textBox.hasMoved) {
          const defaultCoords = getROITextBoxCoords(
            eventData.viewport,
            data.handles
          )

          Object.assign(data.handles.textBox, defaultCoords)
        }

        const textBoxAnchorPoints = handles =>
          _findTextBoxAnchorPoints(handles.start, handles.end)
        const textBoxContent = _createTextBoxContent(
          context,
          image.color,
          data.cachedStats,
          modality,
          hasPixelSpacing,
          this.configuration,
          data
        )

        data.unit = _getUnit(modality, this.configuration.showHounsfieldUnits)

        drawLinkedTextBox(
          context,
          element,
          data.handles.textBox,
          textBoxContent,
          data.handles,
          textBoxAnchorPoints,
          color,
          lineWidth,
          10,
          true
        )
      }
    })
  }
}

/**
 * TODO: This is the same method (+ GetPixels) for the other ROIs
 * TODO: The pixel filtering is the unique bit
 *
 * @param {*} startHandle
 * @param {*} endHandle
 * @returns {{ left: number, top: number, width: number, height: number}}
 */
function _getRectangleImageCoordinates(startHandle, endHandle) {
  return {
    left: Math.min(startHandle.x, endHandle.x),
    top: Math.min(startHandle.y, endHandle.y),
    width: Math.abs(startHandle.x - endHandle.x),
    height: Math.abs(startHandle.y - endHandle.y)
  }
}

/**
 *
 *
 * @param {*} image
 * @param {*} element
 * @param {*} handles
 * @param {*} modality
 * @param {*} pixelSpacing
 * @returns {Object} The Stats object
 */
function _calculateStats(image, element, handles, modality, pixelSpacing) {
  // Retrieve the bounds of the rectangle in image coordinates
  const roiCoordinates = _getRectangleImageCoordinates(
    handles.start,
    handles.end
  )

  // Retrieve the array of pixels that the rectangle bounds cover
  const pixels = external.cornerstone.getPixels(
    element,
    roiCoordinates.left,
    roiCoordinates.top,
    roiCoordinates.width,
    roiCoordinates.height
  )

  // Calculate the mean & standard deviation from the pixels and the rectangle details
  const roiMeanStdDev = _calculateRectangleStats(pixels, roiCoordinates)

  let meanStdDevSUV

  if (modality === 'PT') {
    meanStdDevSUV = {
      mean: calculateSUV(image, roiMeanStdDev.mean, true) || 0,
      stdDev: calculateSUV(image, roiMeanStdDev.stdDev, true) || 0
    }
  }

  // Calculate the image area from the rectangle dimensions and pixel spacing
  const area =
    roiCoordinates.width *
    (pixelSpacing.colPixelSpacing || 1) *
    (roiCoordinates.height * (pixelSpacing.rowPixelSpacing || 1))

  const perimeter =
    roiCoordinates.width * 2 * (pixelSpacing.colPixelSpacing || 1) +
    roiCoordinates.height * 2 * (pixelSpacing.rowPixelSpacing || 1)

  return {
    area: area || 0,
    perimeter,
    count: roiMeanStdDev.count || 0,
    mean: roiMeanStdDev.mean || 0,
    variance: roiMeanStdDev.variance || 0,
    stdDev: roiMeanStdDev.stdDev || 0,
    min: roiMeanStdDev.min || 0,
    max: roiMeanStdDev.max || 0,
    meanStdDevSUV
  }
}

/**
 *
 *
 * @param {*} sp
 * @param {*} rectangle
 * @returns {{ count, number, mean: number,  variance: number,  stdDev: number,  min: number,  max: number }}
 */
function _calculateRectangleStats(sp, rectangle) {
  let sum = 0
  let sumSquared = 0
  let count = 0
  let index = 0
  let min = sp ? sp[0] : null
  let max = sp ? sp[0] : null

  for (let y = rectangle.top; y < rectangle.top + rectangle.height; y++) {
    for (let x = rectangle.left; x < rectangle.left + rectangle.width; x++) {
      sum += sp[index]
      sumSquared += sp[index] * sp[index]
      min = Math.min(min, sp[index])
      max = Math.max(max, sp[index])
      count++ // TODO: Wouldn't this just be sp.length?
      index++
    }
  }

  if (count === 0) {
    return {
      count,
      mean: 0.0,
      variance: 0.0,
      stdDev: 0.0,
      min: 0.0,
      max: 0.0
    }
  }

  const mean = sum / count
  const variance = sumSquared / count - mean * mean

  return {
    count,
    mean,
    variance,
    stdDev: Math.sqrt(variance),
    min,
    max
  }
}

/**
 *
 *
 * @param {*} startHandle
 * @param {*} endHandle
 * @returns {Array.<{x: number, y: number}>}
 */
function _findTextBoxAnchorPoints(startHandle, endHandle) {
  const { left, top, width, height } = _getRectangleImageCoordinates(
    startHandle,
    endHandle
  )

  return [
    {
      // Top middle point of rectangle
      x: left + width / 2,
      y: top
    },
    {
      // Left middle point of rectangle
      x: left,
      y: top + height / 2
    },
    {
      // Bottom middle point of rectangle
      x: left + width / 2,
      y: top + height
    },
    {
      // Right middle point of rectangle
      x: left + width,
      y: top + height / 2
    }
  ]
}

/**
 *
 *
 * @param {*} area
 * @param {*} hasPixelSpacing
 * @returns {string} The formatted label for showing area
 */
// function _formatArea(area, hasPixelSpacing) {
//   // This uses Char code 178 for a superscript 2
//   const suffix = hasPixelSpacing
//     ? ` mm${String.fromCharCode(178)}`
//     : ` px${String.fromCharCode(178)}`

//   return `Area: ${numbersWithCommas(area.toFixed(2))}${suffix}`
// }

function _getUnit(modality, showHounsfieldUnits) {
  return modality === 'CT' && showHounsfieldUnits !== false ? 'HU' : ''
}

/**
 * TODO: This is identical to EllipticalROI's same fn
 * TODO: We may want to make this a utility for ROIs with these values?
 *
 * @param {*} context
 * @param {*} isColorImage
 * @param {*} { area, mean, stdDev, min, max, meanStdDevSUV }
 * @param {*} modality
 * @param {*} hasPixelSpacing
 * @param {*} [options={}]
 * @returns {string[]}
 */
function _createTextBoxContent(
  context,
  isColorImage,
  { area, mean, stdDev, min, max, meanStdDevSUV },
  modality,
  hasPixelSpacing,
  options = {},
  data
) {
  const showMinMax = options.showMinMax || false
  const textLines = []

  const otherLines = []

  if (!isColorImage) {
    const hasStandardUptakeValues = meanStdDevSUV && meanStdDevSUV.mean !== 0
    const unit = _getUnit(modality, options.showHounsfieldUnits)

    let meanString = `Mean: ${numbersWithCommas(mean.toFixed(2))} ${unit}`
    const stdDevString = `Std Dev: ${numbersWithCommas(
      stdDev.toFixed(2)
    )} ${unit}`

    // If this image has SUV values to display, concatenate them to the text line
    if (hasStandardUptakeValues) {
      const SUVtext = ' SUV: '

      const meanSuvString = `${SUVtext}${numbersWithCommas(
        meanStdDevSUV.mean.toFixed(2)
      )}`
      const stdDevSuvString = `${SUVtext}${numbersWithCommas(
        meanStdDevSUV.stdDev.toFixed(2)
      )}`

      const targetStringLength = Math.floor(
        context.measureText(`${stdDevString}     `).width
      )

      while (context.measureText(meanString).width < targetStringLength) {
        meanString += ' '
      }

      otherLines.push(`${meanString}${meanSuvString}`)
      otherLines.push(`${stdDevString}     ${stdDevSuvString}`)
    } else {
      otherLines.push(`${meanString}`)
      otherLines.push(`${stdDevString}`)
    }

    if (showMinMax) {
      let minString = `Min: ${min} ${unit}`
      const maxString = `Max: ${max} ${unit}`
      const targetStringLength = hasStandardUptakeValues
        ? Math.floor(context.measureText(`${stdDevString}     `).width)
        : Math.floor(context.measureText(`${meanString}     `).width)

      while (context.measureText(minString).width < targetStringLength) {
        minString += ' '
      }

      otherLines.push(`${minString}`)
      otherLines.push(`${maxString}`)
    }
  }
  if (options.showStatsText) {
    textLines.push(_formatArea(area, hasPixelSpacing))
    otherLines.forEach(x => textLines.push(x))
  }
  
  if (data.hasOwnProperty('remark')) {
    if (data.hasOwnProperty('status') && data.status) {
      textLines.push(`${data.remark}(${data.status})`)
    } else {
      textLines.push(data.remark)
    }
  }
  return textLines
}
function _formatArea(area, hasPixelSpacing) {
  // This uses Char code 178 for a superscript 2
  const suffix = hasPixelSpacing
    ? ` mm${String.fromCharCode(178)}`
    : ` px${String.fromCharCode(178)}`;

  return `Area: ${numbersWithCommas(area.toFixed(2))}${suffix}`;
}
