///
/// \class l1t::L1TStage1Layer2Producer
///
/// Description: Emulator for the stage 1 jet algorithms.
///
///
/// \author: R. Alex Barbieri MIT
///

// system include files
#include <memory>

// user include files

#include "FWCore/Framework/interface/EventSetup.h"
#include "FWCore/Framework/interface/Frameworkfwd.h"
#include "FWCore/Framework/interface/stream/EDProducer.h"
#include "FWCore/Framework/interface/ESHandle.h"
#include "FWCore/Framework/interface/Event.h"
#include "FWCore/Framework/interface/MakerMacros.h"
#include "FWCore/ParameterSet/interface/ParameterSet.h"
#include "FWCore/Utilities/interface/EDGetToken.h"
#include "FWCore/Utilities/interface/InputTag.h"
#include "FWCore/Utilities/interface/ESGetToken.h"
#include "FWCore/MessageLogger/interface/MessageLogger.h"

#include "CondFormats/L1TObjects/interface/L1CaloEtScale.h"
#include "CondFormats/DataRecord/interface/L1EmEtScaleRcd.h"
#include "CondFormats/DataRecord/interface/L1JetEtScaleRcd.h"
#include "CondFormats/DataRecord/interface/L1HtMissScaleRcd.h"
#include "CondFormats/DataRecord/interface/L1HfRingEtScaleRcd.h"

#include <vector>
#include "DataFormats/L1Trigger/interface/BXVector.h"

//#include "CondFormats/DataRecord/interface/CaloParamsRcd.h"
//#include "CondFormats/L1TCalorimeter/interface/CaloParams.h"
#include "CondFormats/L1TObjects/interface/CaloParams.h"
#include "CondFormats/DataRecord/interface/L1TCaloParamsRcd.h"
//#include "CondFormats/L1TObjects/interface/FirmwareVersion.h"
#include "L1Trigger/L1TCalorimeter/interface/CaloParamsHelper.h"

#include "L1Trigger/L1TCalorimeter/interface/CaloConfigHelper.h"
#include "CondFormats/DataRecord/interface/L1TCaloConfigRcd.h"

#include "DataFormats/L1TCalorimeter/interface/CaloRegion.h"
#include "DataFormats/L1TCalorimeter/interface/CaloEmCand.h"

#include "DataFormats/L1Trigger/interface/EGamma.h"
#include "DataFormats/L1Trigger/interface/Tau.h"
#include "DataFormats/L1Trigger/interface/Jet.h"
#include "DataFormats/L1Trigger/interface/EtSum.h"
#include "DataFormats/L1Trigger/interface/CaloSpare.h"

#include "L1Trigger/L1TCalorimeter/interface/Stage1Layer2MainProcessor.h"
#include "L1Trigger/L1TCalorimeter/interface/Stage1Layer2FirmwareFactory.h"

// print statements
//#include <stdio.h>

using namespace std;
using namespace edm;
using namespace l1t;

//
// class declaration
//

class L1TStage1Layer2Producer : public stream::EDProducer<> {
public:
  explicit L1TStage1Layer2Producer(const ParameterSet&);
  ~L1TStage1Layer2Producer() override;

  static void fillDescriptions(ConfigurationDescriptions& descriptions);

private:
  void produce(Event&, EventSetup const&) override;
  void beginRun(Run const& iR, EventSetup const& iE) override;

  // ----------member data ---------------------------
  unsigned long long m_paramsCacheId;  // Cache-ID from current parameters, to check if needs to be updated.
  unsigned long long m_configCacheId;  // Cache-ID from current parameters, to check if needs to be updated.
  CaloParamsHelper* m_params;
  CaloConfigHelper m_config;

  std::shared_ptr<Stage1Layer2MainProcessor> m_fw;  // Firmware to run per event, depends on database parameters.

  Stage1Layer2FirmwareFactory m_factory;  // Factory to produce algorithms based on DB parameters

  std::string m_conditionsLabel;

  // to be extended with other "consumes" stuff
  EDGetTokenT<BXVector<CaloRegion>> regionToken;
  EDGetTokenT<BXVector<CaloEmCand>> candsToken;
  edm::ESGetToken<CaloParams, L1TCaloParamsRcd> paramsToken;
  edm::ESGetToken<CaloConfig, L1TCaloConfigRcd> configToken;
  edm::ESGetToken<L1CaloEtScale, L1EmEtScaleRcd> emScaleToken;
  edm::ESGetToken<L1CaloEtScale, L1JetEtScaleRcd> jetScaleToken;
  edm::ESGetToken<L1CaloEtScale, L1HtMissScaleRcd> htMissScaleToken;
  edm::ESGetToken<L1CaloEtScale, L1HfRingEtScaleRcd> hfRingScaleToken;
};

//
// constructors and destructor
//
L1TStage1Layer2Producer::L1TStage1Layer2Producer(const ParameterSet& iConfig) {
  // register what you produce
  produces<BXVector<EGamma>>();
  produces<BXVector<Tau>>("rlxTaus");
  produces<BXVector<Tau>>("isoTaus");
  produces<BXVector<Jet>>();
  produces<BXVector<Jet>>("preGtJets");
  produces<BXVector<EtSum>>();
  produces<BXVector<CaloSpare>>("HFRingSums");
  produces<BXVector<CaloSpare>>("HFBitCounts");

  // register what you consume and keep token for later access:
  regionToken = consumes<BXVector<CaloRegion>>(iConfig.getParameter<InputTag>("CaloRegions"));
  candsToken = consumes<BXVector<CaloEmCand>>(iConfig.getParameter<InputTag>("CaloEmCands"));
  //int ifwv=iConfig.getParameter<unsigned>("FirmwareVersion");  // LenA  make configurable for now

  m_conditionsLabel = iConfig.getParameter<std::string>("conditionsLabel");

  m_params = new CaloParamsHelper;

  paramsToken =
      esConsumes<CaloParams, L1TCaloParamsRcd, edm::Transition::BeginRun>(edm::ESInputTag("", m_conditionsLabel));
  configToken =
      esConsumes<CaloConfig, L1TCaloConfigRcd, edm::Transition::BeginRun>(edm::ESInputTag("", m_conditionsLabel));
  emScaleToken =
      esConsumes<L1CaloEtScale, L1EmEtScaleRcd, edm::Transition::BeginRun>(edm::ESInputTag("", m_conditionsLabel));
  jetScaleToken =
      esConsumes<L1CaloEtScale, L1JetEtScaleRcd, edm::Transition::BeginRun>(edm::ESInputTag("", m_conditionsLabel));
  htMissScaleToken =
      esConsumes<L1CaloEtScale, L1HtMissScaleRcd, edm::Transition::BeginRun>(edm::ESInputTag("", m_conditionsLabel));
  hfRingScaleToken =
      esConsumes<L1CaloEtScale, L1HfRingEtScaleRcd, edm::Transition::BeginRun>(edm::ESInputTag("", m_conditionsLabel));

  // set cache id to zero, will be set at first beginRun:
  m_paramsCacheId = 0;
  m_configCacheId = 0;
}

L1TStage1Layer2Producer::~L1TStage1Layer2Producer() {}

//
// member functions
//

// ------------ method called to produce the data ------------
void L1TStage1Layer2Producer::produce(Event& iEvent, const EventSetup& iSetup) {
  LogDebug("l1t|stage 1 jets") << "L1TStage1Layer2Producer::produce function called...\n";

  //return;

  //inputs
  Handle<BXVector<CaloRegion>> caloRegions;
  iEvent.getByToken(regionToken, caloRegions);

  Handle<BXVector<CaloEmCand>> caloEmCands;
  iEvent.getByToken(candsToken, caloEmCands);

  int bxFirst = caloRegions->getFirstBX();
  int bxLast = caloRegions->getLastBX();

  //outputs
  std::unique_ptr<EGammaBxCollection> egammas(new EGammaBxCollection);
  std::unique_ptr<TauBxCollection> taus(new TauBxCollection);
  std::unique_ptr<TauBxCollection> isoTaus(new TauBxCollection);
  std::unique_ptr<JetBxCollection> jets(new JetBxCollection);
  std::unique_ptr<JetBxCollection> preGtJets(new JetBxCollection);
  std::unique_ptr<EtSumBxCollection> etsums(new EtSumBxCollection);
  std::unique_ptr<CaloSpareBxCollection> hfSums(new CaloSpareBxCollection);
  std::unique_ptr<CaloSpareBxCollection> hfCounts(new CaloSpareBxCollection);

  egammas->setBXRange(bxFirst, bxLast);
  taus->setBXRange(bxFirst, bxLast);
  isoTaus->setBXRange(bxFirst, bxLast);
  jets->setBXRange(bxFirst, bxLast);
  preGtJets->setBXRange(bxFirst, bxLast);
  etsums->setBXRange(bxFirst, bxLast);
  hfSums->setBXRange(bxFirst, bxLast);
  hfCounts->setBXRange(bxFirst, bxLast);

  //producer is responsible for splitting the BXVector into pieces for
  //the firmware to handle
  for (int i = bxFirst; i <= bxLast; ++i) {
    //make local inputs
    std::vector<CaloRegion>* localRegions = new std::vector<CaloRegion>();
    std::vector<CaloEmCand>* localEmCands = new std::vector<CaloEmCand>();

    //make local outputs
    std::vector<EGamma>* localEGammas = new std::vector<EGamma>();
    std::vector<Tau>* localTaus = new std::vector<Tau>();
    std::vector<Tau>* localIsoTaus = new std::vector<Tau>();
    std::vector<Jet>* localJets = new std::vector<Jet>();
    std::vector<Jet>* localPreGtJets = new std::vector<Jet>();
    std::vector<EtSum>* localEtSums = new std::vector<EtSum>();
    CaloSpare* localHfSums = new CaloSpare();
    localHfSums->setType(CaloSpare::HFRingSum);
    CaloSpare* localHfCounts = new CaloSpare();
    localHfCounts->setType(CaloSpare::HFBitCount);

    // copy over the inputs -> there must be a better way to do this
    for (std::vector<CaloRegion>::const_iterator region = caloRegions->begin(i); region != caloRegions->end(i);
         ++region)
      localRegions->push_back(*region);
    for (std::vector<CaloEmCand>::const_iterator emcand = caloEmCands->begin(i); emcand != caloEmCands->end(i);
         ++emcand)
      localEmCands->push_back(*emcand);

    //run the firmware on one event
    m_fw->processEvent(*localEmCands,
                       *localRegions,
                       localEGammas,
                       localTaus,
                       localIsoTaus,
                       localJets,
                       localPreGtJets,
                       localEtSums,
                       localHfSums,
                       localHfCounts);

    // copy the output into the BXVector -> there must be a better way
    for (std::vector<EGamma>::const_iterator eg = localEGammas->begin(); eg != localEGammas->end(); ++eg)
      egammas->push_back(i, *eg);
    for (std::vector<Tau>::const_iterator tau = localTaus->begin(); tau != localTaus->end(); ++tau)
      taus->push_back(i, *tau);
    for (std::vector<Tau>::const_iterator isotau = localIsoTaus->begin(); isotau != localIsoTaus->end(); ++isotau)
      isoTaus->push_back(i, *isotau);
    for (std::vector<Jet>::const_iterator jet = localJets->begin(); jet != localJets->end(); ++jet)
      jets->push_back(i, *jet);
    for (std::vector<Jet>::const_iterator jet = localPreGtJets->begin(); jet != localPreGtJets->end(); ++jet)
      preGtJets->push_back(i, *jet);
    for (std::vector<EtSum>::const_iterator etsum = localEtSums->begin(); etsum != localEtSums->end(); ++etsum)
      etsums->push_back(i, *etsum);
    hfSums->push_back(i, *localHfSums);
    hfCounts->push_back(i, *localHfCounts);

    delete localRegions;
    delete localEmCands;
    delete localEGammas;
    delete localTaus;
    delete localIsoTaus;
    delete localJets;
    delete localPreGtJets;
    delete localEtSums;
    delete localHfSums;
    delete localHfCounts;
  }

  iEvent.put(std::move(egammas));
  iEvent.put(std::move(taus), "rlxTaus");
  iEvent.put(std::move(isoTaus), "isoTaus");
  iEvent.put(std::move(jets));
  iEvent.put(std::move(preGtJets), "preGtJets");
  iEvent.put(std::move(etsums));
  iEvent.put(std::move(hfSums), "HFRingSums");
  iEvent.put(std::move(hfCounts), "HFBitCounts");
}

// ------------ method called when starting to processes a run ------------

void L1TStage1Layer2Producer::beginRun(Run const& iR, EventSetup const& iE) {
  unsigned long long id = 0;

  id = iE.get<L1TCaloParamsRcd>().cacheIdentifier();
  if (id != m_paramsCacheId) {
    m_paramsCacheId = id;

    edm::ESHandle<CaloParams> paramsHandle = iE.getHandle(paramsToken);

    // replace our local copy of the parameters with a new one using placement new
    m_params->~CaloParamsHelper();
    m_params = new (m_params) CaloParamsHelper(*paramsHandle.product());

    LogDebug("L1TDebug") << *m_params << std::endl;

    if (!m_params) {
      edm::LogError("l1t|caloStage1") << "Could not retrieve params from Event Setup" << std::endl;
    }
  }

  id = iE.get<L1TCaloConfigRcd>().cacheIdentifier();
  if (id != m_configCacheId) {
    m_configCacheId = id;

    edm::ESHandle<CaloConfig> configHandle = iE.getHandle(configToken);

    if (!configHandle.product()) {
      edm::LogError("l1t|caloStage1") << "Could not retrieve config from Event Setup" << std::endl;
    } else {
      //update our DB payload in CaloConfigHelper:
      m_config.UpdatePayload(configHandle.product());
      //cout << "DEBUG:  L1T Calo Config reports the Layer2 firmware version is " << m_config.fwv() << "\n";
    }
  }

  LogDebug("l1t|stage 1 jets") << "L1TStage1Layer2Producer::beginRun function called...\n";

  //get the proper scales for conversion to physical et AND gt scales
  edm::ESHandle<L1CaloEtScale> emScale = iE.getHandle(emScaleToken);
  m_params->setEmScale(*emScale);

  edm::ESHandle<L1CaloEtScale> jetScale = iE.getHandle(jetScaleToken);
  m_params->setJetScale(*jetScale);

  edm::ESHandle<L1CaloEtScale> HtMissScale = iE.getHandle(jetScaleToken);
  m_params->setHtMissScale(*HtMissScale);

  //not sure if I need this one
  edm::ESHandle<L1CaloEtScale> HfRingScale = iE.getHandle(hfRingScaleToken);
  m_params->setHfRingScale(*HfRingScale);

  //unsigned long long id = iE.get<CaloParamsRcd>().cacheIdentifier();

  //if (id != m_paramsCacheId)
  {  // Need to update:
    //m_paramsCacheId = id;

    //ESHandle<CaloParams> parameters;
    //iE.get<CaloParamsRcd>().get(parameters);

    // LenA move the setting of the firmware version to the L1TStage1Layer2Producer constructor

    //m_params = std::shared_ptr<const CaloParams>(parameters.product());
    //m_fwv = std::shared_ptr<const FirmwareVersion>(new FirmwareVersion());
    //printf("Begin.\n");
    //m_fwv = std::make_shared<FirmwareVersion>(); //not const during testing
    //printf("Success m_fwv.\n");
    //m_fwv->setFirmwareVersion(1); //hardcode for now, 1=HI, 2=PP
    //printf("Success m_fwv version set.\n");

    // if (! m_params){
    //   LogError("l1t|stage 1 jets") << "L1TStage1Layer2Producer: could not retreive DB params from Event Setup\n";
    // }

    // Set the current algorithm version based on DB pars from database:
    //m_fw = m_factory.create(*m_fwv /*,*m_params*/);
    //printf("Success create.\n");

    //if (! m_fw) {
    //  // we complain here once per run
    //  LogError("l1t|stage 1 jets") << "L1TStage1Layer2Producer: firmware could not be configured.\n";
    //}
  }

  int ifwv = m_config.fwv();
  //cout << "DEBUG:  ifwv is " << ifwv << "\n";
  //m_fwv = std::make_shared<FirmwareVersion>(); //not const during testing
  if (ifwv == 1) {
    LogDebug("l1t|stage1firmware") << "L1TStage1Layer2Producer -- Running HI implementation\n";
    //std::cout << "L1TStage1Layer2Producer -- Running HI implementation\n";
  } else if (ifwv == 2) {
    LogDebug("l1t|stage1firmware") << "L1TStage1Layer2Producer -- Running pp implementation\n";
    //std::cout << "L1TStage1Layer2Producer -- Running pp implementation\n";
  } else if (ifwv == 3) {
    LogDebug("l1t|stage1firmware") << "L1TStage1Layer2Producer -- Running SimpleHW implementation\n";
    //std::cout << "L1TStage1Layer2Producer -- Running SimpleHW implementation -- for testing only\n";
  } else {
    LogError("l1t|stage1firmware") << "L1TStage1Layer2Producer -- Unknown implementation.\n";
    //std::cout << "L1TStage1Layer2Producer -- Unknown implementation.\n";
  }
  //m_fwv->setFirmwareVersion(ifwv); // =1 HI, =2 PP
  // m_fw = m_factory.create(*m_fwv /*,*m_params*/);
  //m_fwv = ifwv;
  m_fw = m_factory.create(ifwv, m_params);
  //printf("Success create.\n");
  if (!m_fw) {
    // we complain here once per job
    LogError("l1t|stage1firmware") << "L1TStage1Layer2Producer: firmware could not be configured.\n";
  }
}

// ------------ method fills 'descriptions' with the allowed parameters for the module ------------
void L1TStage1Layer2Producer::fillDescriptions(ConfigurationDescriptions& descriptions) {
  //The following says we do not know what parameters are allowed so do no validation
  // Please change this to state exactly what you do use, even if it is no parameters
  ParameterSetDescription desc;
  desc.add<InputTag>("CaloRegions");
  desc.add<InputTag>("CaloEmCands");
  desc.add<std::string>("conditionsLabel");

  descriptions.addDefault(desc);
}

//define this as a plug-in
DEFINE_FWK_MODULE(L1TStage1Layer2Producer);
