/**
 * Sum counts from the input workspace in lambda along lines of constant Q by
 * projecting to "virtual lambda" at a reference angle.
 *
 * @param detectorWS [in] :: the input workspace in wavelength
 * @param indices [in] :: an index set defining the foreground histograms
 * @return :: the single histogram output workspace in wavelength
 */
API::MatrixWorkspace_sptr
ReflectometrySumInQ::sumInQ(const API::MatrixWorkspace &detectorWS,
                            const Indexing::SpectrumIndexSet &indices) {

  const auto spectrumInfo = detectorWS.spectrumInfo();
  const auto refAngles = referenceAngles(spectrumInfo);
  // Construct the output workspace in virtual lambda
  API::MatrixWorkspace_sptr IvsLam =
      constructIvsLamWS(detectorWS, indices, refAngles);
  auto &outputE = IvsLam->dataE(0);
  // Loop through each spectrum in the detector group
  for (auto spIdx : indices) {
    if (spectrumInfo.isMasked(spIdx) || spectrumInfo.isMonitor(spIdx)) {
      continue;
    }
    // Get the size of this detector in twoTheta
    const auto twoThetaRange = twoThetaWidth(spIdx, spectrumInfo);
    // Check X length is Y length + 1
    const auto inputBinEdges = detectorWS.binEdges(spIdx);
    const auto inputCounts = detectorWS.counts(spIdx);
    const auto inputStdDevs = detectorWS.countStandardDeviations(spIdx);
    // Create a vector for the projected errors for this spectrum.
    // (Output Y values can simply be accumulated directly into the output
    // workspace, but for error values we need to create a separate error
    // vector for the projected errors from each input spectrum and then
    // do an overall sum in quadrature.)
    std::vector<double> projectedE(outputE.size(), 0.0);
    // Process each value in the spectrum
    const int ySize = static_cast<int>(inputCounts.size());
    for (int inputIdx = 0; inputIdx < ySize; ++inputIdx) {
      // Do the summation in Q
      processValue(inputIdx, twoThetaRange, refAngles, inputBinEdges,
                   inputCounts, inputStdDevs, *IvsLam, projectedE);
    }
    // Sum errors in quadrature
    const int eSize = static_cast<int>(outputE.size());
    for (int outIdx = 0; outIdx < eSize; ++outIdx) {
      outputE[outIdx] += projectedE[outIdx] * projectedE[outIdx];
    }
  }

  // Take the square root of all the accumulated squared errors for this
  // detector group. Assumes Gaussian errors
  double (*rs)(double) = std::sqrt;
  std::transform(outputE.begin(), outputE.end(), outputE.begin(), rs);

  return IvsLam;
}
/**
 * Return the wavelength range of the output histogram.
 * @param detectorWS [in] :: the input workspace
 * @param indices [in] :: the workspace indices of foreground histograms
 * @param refAngles [in] :: the reference angles
 * @return :: the minimum and maximum virtual wavelengths
 */
ReflectometrySumInQ::MinMax ReflectometrySumInQ::findWavelengthMinMax(
    const API::MatrixWorkspace &detectorWS,
    const Indexing::SpectrumIndexSet &indices, const Angles &refAngles) {
  const API::SpectrumInfo &spectrumInfo = detectorWS.spectrumInfo();
  // Get the new max and min X values of the projected (virtual) lambda range
  const bool includePartialBins = getProperty(Prop::PARTIAL_BINS);
  // Find minimum and maximum 2thetas and the corresponding indices.
  // It cannot be assumed that 2theta increases with indices, check for example
  // D17 at ILL
  MinMax inputLambdaRange;
  MinMax inputTwoThetaRange;
  for (const auto i : indices) {
    const auto twoThetas = twoThetaWidth(i, spectrumInfo);
    inputTwoThetaRange.testAndSetMin(includePartialBins ? twoThetas.min
                                                        : twoThetas.max);
    inputTwoThetaRange.testAndSetMax(includePartialBins ? twoThetas.max
                                                        : twoThetas.min);
    const auto &edges = detectorWS.binEdges(i);
    for (size_t xIndex = 0; xIndex < edges.size(); ++xIndex) {
      // It is common for the wavelength to have negative values at ILL.
      const auto x = edges[xIndex + (includePartialBins ? 0 : 1)];
      if (x > 0.) {
        inputLambdaRange.testAndSet(x);
        break;
      }
    }
    if (includePartialBins) {
      inputLambdaRange.testAndSet(edges.back());
    } else {
      inputLambdaRange.testAndSet(edges[edges.size() - 2]);
    }
  }

  MinMax outputLambdaRange;
  outputLambdaRange.min = projectToReference(inputLambdaRange.min,
                                             inputTwoThetaRange.max, refAngles);
  outputLambdaRange.max = projectToReference(inputLambdaRange.max,
                                             inputTwoThetaRange.min, refAngles);
  if (outputLambdaRange.min > outputLambdaRange.max) {
    throw std::runtime_error(
        "Error projecting lambda range to reference line; projected range (" +
        std::to_string(outputLambdaRange.min) + "," +
        std::to_string(outputLambdaRange.max) + ") is negative.");
  }
  return outputLambdaRange;
}
/**
 * Construct an "empty" output workspace in virtual-lambda for summation in Q.
 *
 * @param detectorWS [in] :: the input workspace
 * @param indices [in] :: the workspace indices of the foreground histograms
 * @param refAngles [in] :: the reference angles
 * @return :: a 1D workspace where y values are all zero
 */
API::MatrixWorkspace_sptr ReflectometrySumInQ::constructIvsLamWS(
    const API::MatrixWorkspace &detectorWS,
    const Indexing::SpectrumIndexSet &indices, const Angles &refAngles) {

  // Calculate the number of bins based on the min/max wavelength, using
  // the same bin width as the input workspace
  const auto &edges = detectorWS.binEdges(refAngles.referenceWSIndex);
  const double binWidth =
      (edges.back() - edges.front()) / static_cast<double>(edges.size());
  const auto wavelengthRange =
      findWavelengthMinMax(detectorWS, indices, refAngles);
  if (std::abs(wavelengthRange.max - wavelengthRange.min) < binWidth) {
    throw std::runtime_error("Given wavelength range too small.");
  }
  const int numBins = static_cast<int>(
      std::ceil((wavelengthRange.max - wavelengthRange.min) / binWidth));
  // Construct the histogram with these X values. Y and E values are zero.
  const HistogramData::BinEdges bins(
      numBins + 1,
      HistogramData::LinearGenerator(wavelengthRange.min, binWidth));
  const HistogramData::Counts counts(numBins, 0.);
  const HistogramData::Histogram modelHistogram(std::move(bins),
                                                std::move(counts));
  // Create the output workspace
  API::MatrixWorkspace_sptr outputWS =
      DataObjects::create<DataObjects::Workspace2D>(detectorWS, 1,
                                                    std::move(modelHistogram));

  // Set the detector IDs and specturm number from the twoThetaR detector.
  const auto &thetaSpec = detectorWS.getSpectrum(refAngles.referenceWSIndex);
  auto &outSpec = outputWS->getSpectrum(0);
  outSpec.clearDetectorIDs();
  outSpec.addDetectorIDs(thetaSpec.getDetectorIDs());
  outSpec.setSpectrumNo(thetaSpec.getSpectrumNo());

  return outputWS;
}