#ifndef __JDB__JDETECTORINTEGRATION_T__
#define __JDB__JDETECTORINTEGRATION_T__

#include <istream>
#include <ostream>
#include <sstream>
#include <fstream>
#include <vector>
#include <map>
#include <algorithm>

#include "JTools/JRange.hh"
#include "JDB/JDB.hh"
#include "JDB/JProductIntegration.hh"
#include "JDB/JPBS_t.hh"
#include "JDB/JUPI_t.hh"
#include "JDB/JDate_t.hh"
#include "JLang/JException.hh"
#include "JLang/JWhiteSpacesFacet.hh"
#include "JLang/JLangToolkit.hh"
#include "JLang/JPredicate.hh"

#include "dbclient/KM3NeTDBClient.h"


/**
 * \author mdejong
 */
namespace JDATABASE {}
namespace JPP { using namespace JDATABASE; }

namespace JDATABASE {

  using JLANG::JDatabaseException;
  using JLANG::JException;


  /**
   * Type definition of period.
   */
  typedef JTOOLS::JRange<JDate_t>  JPeriod_t;


  /**
   * Write period to output stream.
   *
   * \param  out        output stream
   * \param  object     period
   * \return            output stream
   */
  inline std::ostream& operator<<(std::ostream& out, const JPeriod_t& object)
  {
    return out << "(" << object.getLowerLimit() << "," << object.getUpperLimit() << ")";
  }
  

  /**
   * Auxiliary class for product integration data.
   *
   * This class is primarily used to 
   * - convert the string data from JDATABASE::JProductIntegration to corresponding data structures;
   * - implement efficient (in)equality operations;
   */
  struct JProductIntegration_t {
    /**
     * Default constructor.
     */
    JProductIntegration_t() :
      status(false)
    {}


    /**
     * Constructor.
     *
     * \param  input        product integration data
     */
    JProductIntegration_t(const JProductIntegration& input) :
      status(false)
    {
      using namespace std;

      this->operation = input.OPERATIONID;
      this->position  = input.POSITION;
      this->container = input.CONTAINER_UPI;
      this->content   = input.CONTENT_UPI;

      istringstream(input.STARTD) >> this->start_date;
      istringstream(input.ENDD)   >> this->end_date;
    }


    /**
     * Check product status.
     *
     * \return              true if set; else false
     */
    bool is_set() const
    {
      return status;
    }


    /**
     * Set product status.
     */
    void set()
    {
      status = true;
    }


    /**
     * Unset product status.
     */
    void unset()
    {
      status = false;
    }


    /**
     * Read product integration from input stream.
     *
     * \param  in           input stream
     * \param  object       product integration
     * \return              input stream
     */
    friend inline std::istream& operator>>(std::istream& in, JProductIntegration_t& object)
    {
      using namespace std;

      in >> object.operation
	 >> object.container
	 >> object.content
	 >> object.position
	 >> object.start_date
	 >> object.end_date;

      return in;
    }


    /**
     * Write product integration to output stream.
     *
     * \param  out          output stream
     * \param  object       product integration
     * \return              output stream
     */
    friend inline std::ostream& operator<<(std::ostream& out, const JProductIntegration_t& object)
    {
      using namespace std;

      out << object.operation  << ' '
	  << object.container  << ' '
	  << object.content    << ' '
	  << object.position   << ' '
	  << object.start_date << ' '
	  << object.end_date;

      return out;
    }


    std::string    operation;
    int            position;
    JUPI_t         container;
    JUPI_t         content;
    JDateAndTime_t start_date;
    JDateAndTime_t end_date;
    bool           status;
  };


  /**
   * Detector integration.
   *
   * This class is used
   * - to read product integration data from the data base;  
   * - to organise detector specific integration data in memory;
   * - to efficiently %trace a product;
   * - to efficiently %find product(s);
   *
   * Note that the method JDetectorIntegration_t::configure should be used to set up the integration data for a specific detector.
   */
  struct JDetectorIntegration_t :
    public std::vector<JProductIntegration_t>
  {
    static const char* const getName() { return JProductIntegration::getName(); } //!< Table name

    typedef  std::multimap<JUPI_t, int>                             map_type;
    typedef  std::pair<map_type::const_iterator,
		       map_type::const_iterator>                    range_type;
    typedef  map_type::const_iterator                               range_const_iterator;
    typedef  map_type::const_iterator                               range_iterator;


    /**
     * Default constructor.
     */
    JDetectorIntegration_t()
    {}


    /**
     * Configure detector integration for given detector identifier.
     *
     * The components of the given detector are selected based on the following rules:
     *   -# the component should have a start date before that of the given detector;
     *   -# if multiple components appear at the same place in the same container, 
     *      the one with the latest start date is taken.
     *
     * The option can be used to retain only those products that are part of the given detector.
     *
     * \param  detid         detector identifier
     * \param  option        option
     */
    void configure(const std::string& detid, const bool option = false)
    {
      using namespace std;
      using namespace JPP;

      up  .clear();
      down.clear();
      out .clear();

      for (size_t index = 0; index != this->size(); ++index) {

        JProductIntegration_t& product = (*this)[index];

	product.unset();

	if (product.content != JUPI_t()) {

	  up  .insert(make_pair(product.content  .getUPI(), index));
	  down.insert(make_pair(product.container.getUPI(), index));

	} else {

	  out .insert(make_pair(product.container.getUPI(), index));
	}
      }

      for (iterator product = this->begin(); product != this->end(); ++product) {
	if (product->operation == detid) {
	  configure(*product, JPeriod_t(product->start_date, product->end_date));
	}
      }

      if (option) {	
	this->erase(partition(this->begin(), this->end(), make_predicate(&JProductIntegration_t::is_set, true)), this->end());
      }

      up  .clear();
      down.clear();

      for (size_t index = 0; index != this->size(); ++index) {

	const JProductIntegration_t& product = (*this)[index];

	if (product.is_set()) {
	  up  .insert(make_pair(product.content  .getUPI(), index));
	  down.insert(make_pair(product.container.getUPI(), index));
	}
      }
    }


    /**
     * Trace product up to given integration level.
     *
     * \param  upi           %UPI
     * \param  pbs           %PBS
     * \return               product
     */
    const JProductIntegration_t& trace(const JUPI_t& upi,
				       const JPBS_t& pbs = PBS::DETECTOR) const
    {
      using namespace std;
      
      if (upi.getPBS() >= pbs) {

	const range_type range = find(upi);

	switch (distance(range.first, range.second)) {

	case 1:
	  {
	    const JProductIntegration_t& product = (*this)[range.first->second];

	    if (product.container.getPBS() == pbs) 
	      return product;
	    else
	      return trace(product.container.getUPI(), pbs);
	  }
	  
	case 0:
	  THROW(JDatabaseException, "Invalid UPI " << upi);

	default:
	  THROW(JDatabaseException, "Ambiguous integration of UPI " << upi);
	}

      } else {

	THROW(JDatabaseException, "Invalid PBS " << upi.getPBS() << " < " << pbs);
      }
    }


    /**
     * Print product trace.
     *
     * \param  out           output stream
     * \param  upi           %UPI
     */
    void print(std::ostream& out, const JUPI_t& upi) const
    {
      using namespace std;
      
      range_type range;

      range = find(upi);

      for (map_type::const_iterator i = range.first; i != range.second; ++i) {

	out << upi << " -> ";

	print(out, (*this)[i->second]);
      }

      range = down.equal_range(upi);

      for (range_iterator i = range.first; i != range.second; ++i) {
	out << "<- " << (*this)[i->second] << endl; 
      }
    }


    /**
     * Print product trace.
     *
     * \param  out           output stream
     * \param  product       product
     */
    void print(std::ostream& out, const JProductIntegration_t& product) const
    {
      using namespace std;
      
      out << product.container.getUPI()                      << ' '
	//<< JPeriod_t(product.start_date, product.end_date) << ' '
	  << "-> ";

      const range_type range = find(product.container.getUPI());

      for (map_type::const_iterator i = range.first; i != range.second; ++i) {
	print(out, (*this)[i->second]);
      }

      if (distance(range.first, range.second) != 1) {
	out << product.operation << std::endl;
      }
    }


    /**
     * Read detector integration from result set.
     *
     * \param  rs            result set
     * \param  detector      detector
     * \return               true if read; else false
     */
    friend inline bool operator>>(ResultSet& rs, JDetectorIntegration_t& detector)
    {
      using namespace std;

      for (JProductIntegration parameters; rs >> parameters; ) {

	const JProductIntegration_t product(parameters);

	detector.up  .insert(make_pair(product.content  .getUPI(), detector.size()));
	detector.down.insert(make_pair(product.container.getUPI(), detector.size()));

	detector.push_back(product);
      }

      rs.Close();

      return true;
    }


    /**
     * Load detector integration from CSV formatted input file.
     *
     * The input should be conform output of <tt>JPrintDB -q "integration" -c ";" -W1</tt>.
     *
     * \param  file_name    file name
     * \param  separator    separator
     */
    void load(const char* const file_name, const std::string& separator = ";")
    {
      using namespace std;
      using namespace JPP;

      ifstream in(file_name);

      in.ignore(numeric_limits<streamsize>::max(), '\n');

      for (string buffer; getline(in, buffer); ) {

	istringstream is(buffer);

	const locale loc(is.getloc(), new JWhiteSpacesFacet(is.getloc(), separator));

	is.imbue(loc);

        JProductIntegration parameters;

        is >> buffer;  istringstream(trim(buffer)) >> parameters.OPERATIONID;
        is >> buffer;  istringstream(trim(buffer)) >> parameters.CITY;
        is >> buffer;  istringstream(trim(buffer)) >> parameters.LOCATIONID;
        is >> buffer;  istringstream(trim(buffer)) >> parameters.LOGIN;
        is >> buffer;  istringstream(trim(buffer)) >> parameters.USERID;
        is >> buffer;  istringstream(trim(buffer)) >> parameters.STARTD;
        is >> buffer;  istringstream(trim(buffer)) >> parameters.ENDD;
        is >> buffer;  istringstream(trim(buffer)) >> parameters.CONTAINER_UPI;
        is >> buffer;  istringstream(trim(buffer)) >> parameters.POSITION;
        is >> buffer;  istringstream(trim(buffer)) >> parameters.CONTENT_UPI;

        const JProductIntegration_t product(parameters);

        this->up  .insert(make_pair(product.content  .getUPI(), this->size()));
        this->down.insert(make_pair(product.container.getUPI(), this->size()));

        this->push_back(product);
      }

      in.close();
    }


    /**
     * Read detector integration from input stream.
     *
     * \param  in           input stream
     * \param  object       detector integration
     * \return              input stream
     */
    friend inline std::istream& operator>>(std::istream& in, JDetectorIntegration_t& object)
    {
      using namespace std;

      for (JProductIntegration_t product; in >> product; ) {

        object.up  .insert(make_pair(product.content  .getUPI(), object.size()));
        object.down.insert(make_pair(product.container.getUPI(), object.size()));

        object.push_back(product);
      }

      return in;
    }


    /**
     * Write detector integration to output stream.
     *
     * \param  out          output stream
     * \param  object       detector integration
     * \return              output stream
     */
    friend inline std::ostream& operator<<(std::ostream& out, const JDetectorIntegration_t& object)
    {
      for (const_iterator i = object.begin(); i != object.end(); ++i) {
	out << *i << std::endl;
      }

      return out;
    }


    /**
     * Find range of products with given %UPI.\n
     * The returned range corresponds to the usual begin and end iterators,
     * each pointing to an STL pair consisting of a %UPI and index.
     *
     * \param  upi           %UPI
     * \return               index
     */
    range_type find(const JUPI_t& upi) const
    {
      return up.equal_range(upi);
    }


    /**
     * Find range of products with given %PBS.\n
     * The returned range corresponds to the usual begin and end iterators,
     * each pointing to an STL pair consisting of a %UPI and index.
     *
     * \param  pbs           %PBS
     * \return               range
     */
    range_type find(const JPBS_t& pbs) const
    {
      range_const_iterator p = up.lower_bound(JUPI_t(pbs));
      range_const_iterator q = p;

      while (q != up.end() && (*this)[q->second].content.getPBS() <= pbs) {
	++q;
      }

      return range_type(p,q);
    }


    /**
     * Get components of product with given %UPI.\n
     * The returned range corresponds to the usual begin and end iterators,
     * each pointing to an STL pair consisting of a %UPI and index.
     *
     * \param  upi           %UPI
     * \return               index
     */
    range_type get(const JUPI_t& upi) const
    {
      return down.equal_range(upi);
    }


  protected:
    /**
     * Configure detector integration for given detector identifier.\n
     * This method recursively sets the status all related products.
     *
     * \param  product       product
     * \param  period        validity period
     */
    void configure(JProductIntegration_t& product, const JPeriod_t& period)
    {
      using namespace std;

      range_type range;

      range = out.equal_range(product.container);

      for (range_iterator i = range.first; i != range.second; ++i) {

	const JProductIntegration_t& object = (*this)[i->second];
	    
	if (object.position   == product.position        &&
	    object.start_date <= period.getLowerLimit()  &&
	    object.start_date >  product.start_date) {
	  return;
	}
      }

      product.set();

      map<int, int>    zmap;    // position -> index
      
      range = down.equal_range(product.content);

      for (range_iterator i = range.first; i != range.second; ++i) {

	const JProductIntegration_t& object = (*this)[i->second];

	if (object.start_date <= period.getLowerLimit()) {
	  if (zmap.count(object.position) == 0 || object.start_date > (*this)[zmap[object.position]].start_date) {
	    zmap[object.position] = i->second;
	  }
	}
      }

      for (map<int, int>::iterator i = zmap.begin(); i != zmap.end(); ++i) {
	configure((*this)[i->second], period);
      }
    }


    map_type   up;     //!< up   link %UPI to integration data
    map_type   down;   //!< down link %UPI to integration data
    map_type   out;    //!<      link %UPI to disintegrated data
  };
}

#endif