#include "MantidAlgorithms/CountEventsInPulses.h"
#include "MantidKernel/System.h"
#include "MantidAPI/WorkspaceProperty.h"
#include "MantidDataObjects/Workspace2D.h"
#include "MantidKernel/ListValidator.h"
#include "MantidKernel/TimeSeriesProperty.h"
#include "MantidDataObjects/EventList.h"
#include "MantidDataObjects/Events.h"
#include "MantidAPI/MatrixWorkspace.h"
#include "MantidKernel/UnitFactory.h"
#include "MantidAPI/WorkspaceFactory.h"

#include <sstream>

using namespace Mantid::Kernel;
using namespace Mantid::API;

namespace Mantid {
namespace Algorithms {

DECLARE_ALGORITHM(CountEventsInPulses)

//----------------------------------------------------------------------------------------------
/** Constructor
 */
CountEventsInPulses::CountEventsInPulses() {}

//----------------------------------------------------------------------------------------------
/** Destructor
 */
CountEventsInPulses::~CountEventsInPulses() {}

void CountEventsInPulses::init() {
  // Input workspace
  this->declareProperty(new API::WorkspaceProperty<DataObjects::EventWorkspace>(
                            "InputWorkspace", "", Direction::Input),
                        "Input EventWorkspace to count events");

  // Output workspace
  this->declareProperty(
      new API::WorkspaceProperty<DataObjects::EventWorkspace>(
          "OutputWorkspace", "", Direction::Output),
      "Output EventWorkspace for events count along run time.");

  // Bin size
  this->declareProperty("BinSize", EMPTY_DBL(),
                        "Bin size for output workspace in unit of time.  Left "
                        "empty will use default equal to length of 1 pulse.");

  // Tolerance (resolution)
  this->declareProperty("Tolerance", EMPTY_DBL(),
                        "Tolerance of events compressed in unit of second.  "
                        "Left empty disables.");

  // Sum spectra or not
  this->declareProperty("SumSpectra", true, "Whether to sum up all spectra.");

  // Run in parallel or not
  this->declareProperty("Parallel", true, "Make the code work in parallel");

  return;
}

/** Execute main body
 */
void CountEventsInPulses::exec() {
  // 1. Get input
  inpWS = this->getProperty("InputWorkspace");

  mBinSize = this->getProperty("BinSize");
  bool usedefaultbinsize = false;
  if (mBinSize == EMPTY_DBL()) {
    usedefaultbinsize = true;
  }

  mSumSpectra = this->getProperty("SumSpectra");

  double tolerance = this->getProperty("Tolerance");
  bool compressevent = true;
  if (tolerance == EMPTY_DBL()) {
    compressevent = false;
  }

  // 2. Some survey of
  Kernel::TimeSeriesProperty<double> *protonchargelog =
      dynamic_cast<Kernel::TimeSeriesProperty<double> *>(
          inpWS->run().getProperty("proton_charge"));
  mTimesInSecond = protonchargelog->timesAsVectorSeconds();

  // b) Check proton charge log to examine the pulses
  double dt1 = 0.0;
  double dt2 = 0.0;
  for (size_t i = 1; i < mTimesInSecond.size(); i++) {
    double dt = mTimesInSecond[i] - mTimesInSecond[i - 1];
    dt1 += dt;
    dt2 += dt * dt;
    if (dt > 1.5 / 60.0) {
      g_log.warning() << "From proton charge, delta T = " << dt
                      << "  : some pulse might be skipped" << std::endl;
    }
  }
  mPulseLength = dt1 / static_cast<double>(mTimesInSecond.size());
  double stddev = sqrt(dt2 / static_cast<double>(mTimesInSecond.size()) -
                       mPulseLength * mPulseLength);
  g_log.notice() << "For Each Pulse: Delta T = " << mPulseLength
                 << "  Standard deviation = " << stddev << std::endl;

  if (usedefaultbinsize) {
    mBinSize = mPulseLength;
  }

  // 3. Setup for parallelization
  bool useparallel = this->getProperty("Parallel");
  int nummaxcores = 1;
  if (useparallel)
    nummaxcores = PARALLEL_GET_MAX_THREADS;

  // 4.  Create and output EventWorkspace
  DataObjects::EventWorkspace_sptr outputWS =
      this->createEventWorkspace(inpWS, mSumSpectra);

  // 5. Switch Event's TOF and Pulse Time
  // Set the maximum cores to user
  PARALLEL_SET_NUM_THREADS(nummaxcores);

  this->convertEvents(outputWS, mSumSpectra);

  // Set the maximum cores back
  PARALLEL_SET_NUM_THREADS(PARALLEL_GET_MAX_THREADS);

  // 6. Rebin
  rebin(outputWS);

  // 7. Compress events
  if (compressevent) {
    outputWS = compressEvents(outputWS, tolerance);
  }

  // 8. Output
  this->setProperty("OutputWorkspace", outputWS);

  return;
}

/**
 * Create an output EventWorkspace w/o any events
 */
DataObjects::EventWorkspace_sptr CountEventsInPulses::createEventWorkspace(
    DataObjects::EventWorkspace_const_sptr parentws, bool sumspectrum) {
  size_t numspec;
  bool diffsize;
  if (sumspectrum) {
    numspec = 1;
    diffsize = true;
  } else {
    numspec = parentws->getNumberHistograms();
    diffsize = false;
  }

  DataObjects::EventWorkspace_sptr outputWS =
      boost::dynamic_pointer_cast<DataObjects::EventWorkspace>(
          API::WorkspaceFactory::Instance().create("EventWorkspace", numspec, 1,
                                                   1));
  API::WorkspaceFactory::Instance().initializeFromParent(parentws, outputWS,
                                                         diffsize);

  outputWS->getAxis(0)->unit() = Kernel::UnitFactory::Instance().create("Time");

  return outputWS;
}

/** Rebin output workspace
  */
void CountEventsInPulses::rebin(DataObjects::EventWorkspace_sptr outputWS) {
  const double xmin = outputWS->getTofMin();
  const double xmax = outputWS->getTofMax();
  if (xmax <= xmin) {
    std::stringstream ss;
    ss << "Tof_max " << xmax << " is less than Tof_min " << xmin;
    throw std::runtime_error(ss.str());
  }

  std::stringstream ss;
  ss << xmin << ", " << mPulseLength << ", " << xmax;
  std::string binparam = ss.str();

  g_log.debug() << "Binning parameter = " << binparam << std::endl;

  API::Algorithm_sptr rebin =
      this->createChildAlgorithm("Rebin", 0.8, 0.9, true);
  rebin->initialize();
  rebin->setProperty("InputWorkspace", outputWS);
  rebin->setProperty("OutputWorkspace", outputWS);
  rebin->setProperty("Params", binparam);
  rebin->setProperty("PreserveEvents", true);

  bool success = rebin->execute();

  if (!success) {
    g_log.warning() << "Rebin output event workspace failed! " << std::endl;
  }

  return;
}

/** Compress event
  */
DataObjects::EventWorkspace_sptr
CountEventsInPulses::compressEvents(DataObjects::EventWorkspace_sptr inputws,
                                    double tolerance) {
  API::Algorithm_sptr alg =
      this->createChildAlgorithm("CompressEvents", 0.9, 1.0, true);
  alg->initialize();

  alg->setProperty("InputWorkspace", inputws);
  alg->setProperty("OutputWorkspace", "TempWS");
  alg->setProperty("Tolerance", tolerance);

  bool successful = alg->execute();

  DataObjects::EventWorkspace_sptr outputws;

  if (!successful) {
    // Failed
    outputws = inputws;
    g_log.warning() << "CompressEvents() Failed!" << std::endl;
  } else {
    // Successful
    outputws = alg->getProperty("OutputWorkspace");
    if (!outputws) {
      g_log.error()
          << "CompressEvents failed as the output is not a MatrixWorkspace. "
          << std::endl;
      throw std::runtime_error(
          "SumSpectra failed as the output is not a MatrixWorkspace.");
    }

    /*
    API::MatrixWorkspace_sptr testws = alg->getProperty("OutputWorkspace");
    if (!testws)
    {
        g_log.error() << "CompressEvents failed as the output is not a
    MatrixWorkspace. " << std::endl;
        throw std::runtime_error("SumSpectra failed as the output is not a
    MatrixWorkspace.");
    }
    else
    {
        outputws =
    boost::dynamic_pointer_cast<DataObjects::EventWorkspace>(testws);
    }
   */
  }

  return outputws;
}

/** Convert events to "fake" events as counts in outWS
 */
void CountEventsInPulses::convertEvents(DataObjects::EventWorkspace_sptr outWS,
                                        bool sumspectra) {
  // 1. Get run_start time
  std::string runstartstr = inpWS->run().getProperty("run_start")->value();
  Kernel::DateAndTime runstart(runstartstr);
  int64_t runstarttime = runstart.totalNanoseconds();

  // 2. Convert TOF and add to new event workspace
  double margin_sec = mPulseLength * 0.01;
  g_log.information() << "Pulse length = " << mPulseLength
                      << " (sec).  Margine = " << margin_sec
                      << " For safe binning. " << std::endl;

  PARALLEL_FOR2(inpWS, outWS)
  for (int wsindex = 0;
       wsindex < static_cast<int>(inpWS->getNumberHistograms()); wsindex++) {

    DataObjects::EventList realevents = inpWS->getEventList(wsindex);
    DataObjects::EventList *fakeevents;
    if (sumspectra) {
      fakeevents = outWS->getEventListPtr(0);
    } else {
      fakeevents = outWS->getEventListPtr(wsindex);
    }

    size_t numevents = realevents.getNumberEvents();
    for (size_t ie = 0; ie < numevents; ie++) {
      DataObjects::WeightedEvent event = realevents.getEvent(ie);
      // Swap TOF and pulse time and add to new event list
      // a) TOF (ms) -> Pulse Time
      double oldtof = event.tof();
      int64_t oldtofns = int64_t(oldtof * 1000);
      Kernel::DateAndTime newpulsetime(oldtofns);

      // b) Pulse Time (ns) -> TOF (ms) -> second
      int64_t oldpulsetime =
          event.pulseTime().totalNanoseconds() - runstarttime;
      double newtofinsecond = double(oldpulsetime) * 1.0E-9 + margin_sec;

      DataObjects::TofEvent fakeevent(newtofinsecond, newpulsetime);

      fakeevents->addEventQuickly(fakeevent);

    } // ENDFOR events

  } // END FOR: Per Spectrum

  g_log.debug() << "DBx505 Input Events = " << inpWS->getNumberEvents()
                << "; Output Events = " << outWS->getNumberEvents()
                << std::endl;

  return;
}

} // namespace Mantid
} // namespace Algorithms