/*
 *  recording_device.cpp
 *
 *  This file is part of NEST.
 *
 *  Copyright (C) 2004 The NEST Initiative
 *
 *  NEST is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  NEST is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with NEST.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include "recording_device.h"
#include "network.h"
#include "dictutils.h"
#include "iostreamdatum.h"
#include "arraydatum.h"
#include "config.h"
#include "exceptions.h"
#include "sliexceptions.h"
#include <iostream> // using cerr for error message.
#include <iomanip>
#include "fdstream.h"

// nestmodule provides global access to the network, so we can
// issue warning messages. This is messy and needs cleaning up.
#include "nestmodule.h"


/* ----------------------------------------------------------------
 * Default constructors defining default parameters and state
 * ---------------------------------------------------------------- */

nest::RecordingDevice::Parameters_::Parameters_(const std::string& file_ext,
						bool withtime, bool withgid, bool withweight)
  : to_file_(false),
    to_screen_(false),
    to_memory_(true),
    to_accumulator_(false),
    time_in_steps_(false),
    precise_times_(false),
    withgid_(withgid),
    withtime_(withtime),
    withweight_(withweight),
    precision_(3),
    scientific_(false),
    binary_(false),
    fbuffer_size_(BUFSIZ), // default buffer size as defined in <cstdio>
    label_(),
    file_ext_(file_ext),
    filename_(),
    close_after_simulate_(false),
    flush_after_simulate_(true),
    flush_records_(false),
    close_on_reset_(true)
{}

nest::RecordingDevice::State_::State_()
  : events_(0),
    event_senders_(),
    event_times_ms_(),
    event_times_steps_(),
    event_times_offsets_()
{}

/* ----------------------------------------------------------------
 * Parameter extraction and manipulation functions
 * ---------------------------------------------------------------- */

void nest::RecordingDevice::Parameters_::get(const RecordingDevice& rd,
                                             DictionaryDatum &d) const
{
  (*d)[names::label] = label_;

  (*d)[names::withtime] = withtime_;
  (*d)[names::withgid]  = withgid_;
  (*d)[names::withweight] = withweight_;

  (*d)[names::time_in_steps]  = time_in_steps_;
  if ( rd.mode_ == RecordingDevice::SPIKE_DETECTOR )
    (*d)[names::precise_times]  = precise_times_;

  // We must maintain /to_file, /to_screen, and /to_memory, because
  // the new /record_to feature is not working in Pynest.
  (*d)[names::to_screen]= to_screen_;
  (*d)[names::to_memory]= to_memory_;
  (*d)[names::to_file]= to_file_;
  if ( rd.mode_ == RecordingDevice::MULTIMETER )
    (*d)[names::to_accumulator]= to_accumulator_;

  ArrayDatum ad;
  if ( to_file_   ) ad.push_back(LiteralDatum(names::file));
  if ( to_memory_ ) ad.push_back(LiteralDatum(names::memory));
  if ( to_screen_ ) ad.push_back(LiteralDatum(names::screen));
  if ( rd.mode_ == RecordingDevice::MULTIMETER )
    if ( to_accumulator_ ) ad.push_back(LiteralDatum(names::accumulator));
  (*d)[names::record_to] = ad;

  (*d)[names::file_extension] = file_ext_;
  (*d)[names::precision] = precision_;
  (*d)[names::scientific] = scientific_;

  (*d)[names::binary] = binary_;
  (*d)[names::fbuffer_size] = fbuffer_size_;

  (*d)[names::close_after_simulate] = close_after_simulate_;
  (*d)[names::flush_after_simulate] = flush_after_simulate_;
  (*d)[names::flush_records]  = flush_records_;
  (*d)[names::close_on_reset] = close_on_reset_;

  if ( to_file_ && !filename_.empty() )
  {
    initialize_property_array(d, names::filenames);
    append_property(d, names::filenames, filename_);
  }
}

void nest::RecordingDevice::Parameters_::set(const RecordingDevice& rd,
                                             const Buffers_&,
                                             const DictionaryDatum& d)
{
  updateValue<std::string>(d, names::label, label_);
  updateValue<bool>(d, names::withgid, withgid_);
  updateValue<bool>(d, names::withtime, withtime_);
  updateValue<bool>(d, names::withweight, withweight_);
  updateValue<bool>(d, names::time_in_steps, time_in_steps_);
  if ( rd.mode_ == RecordingDevice::SPIKE_DETECTOR )
    updateValue<bool>(d, names::precise_times, precise_times_);
  updateValue<std::string>(d, names::file_extension, file_ext_);
  updateValue<long>(d, names::precision, precision_);
  updateValue<bool>(d, names::scientific, scientific_);

  updateValue<bool>(d, names::binary, binary_);

  long fbuffer_size;
  if (updateValue<long>(d, names::fbuffer_size, fbuffer_size))
  {  
    if (fbuffer_size < 0)
      throw BadProperty("/fbuffer_size must be <= 0");
    else
    {
      fbuffer_size_old_ = fbuffer_size_;
      fbuffer_size_ = fbuffer_size;
    }
  }
  
  updateValue<bool>(d, names::close_after_simulate, close_after_simulate_);
  updateValue<bool>(d, names::flush_after_simulate, flush_after_simulate_);
  updateValue<bool>(d, names::flush_records, flush_records_);
  updateValue<bool>(d, names::close_on_reset, close_on_reset_);

  // In Pynest we cannot use /record_to, because we have no way to pass
  // values as LiteralDatum. Thus, we must keep the boolean flags.
  // We must have || rec_change at the end, otherwise short-circuiting may
  // mean that some flags are not read.
  bool rec_change = false;
  rec_change = updateValue<bool>(d, names::to_screen, to_screen_) || rec_change;
  rec_change = updateValue<bool>(d, names::to_memory, to_memory_) || rec_change;
  rec_change = updateValue<bool>(d, names::to_file, to_file_)     || rec_change;
  if ( rd.mode_ == RecordingDevice::MULTIMETER )
    rec_change = updateValue<bool>(d, names::to_accumulator, to_accumulator_) || rec_change;

  const bool have_record_to = d->known(names::record_to);
  if ( have_record_to )
  {
    // clear all flags
    to_file_ = to_screen_ = to_memory_ = to_accumulator_ = false;

    // check for flags present in array, could be far more elegant ...
    ArrayDatum ad = getValue<ArrayDatum>(d, names::record_to);
    for ( Token* t = ad.begin() ; t != ad.end() ; ++t )
      if ( *t == LiteralDatum(names::file) || *t == Token(names::file.toString()) )
        to_file_ = true;
      else if ( *t == LiteralDatum(names::memory) || *t == Token(names::memory.toString()) )
        to_memory_ = true;
      else if ( *t == LiteralDatum(names::screen) || *t == Token(names::screen.toString()) )
        to_screen_ = true;
      else if ( rd.mode_ == RecordingDevice::MULTIMETER  &&
                ( *t == LiteralDatum(names::accumulator) || *t == Token(names::accumulator.toString()) ) )
        to_accumulator_ = true;
      else
      {
        if ( rd.mode_ == RecordingDevice::MULTIMETER )
          throw BadProperty("/to_record must be array, allowed entries: /file, /memory, /screen, /accumulator.");
        else
          throw BadProperty("/to_record must be array, allowed entries: /file, /memory, /screen.");
      }
}

  if ( ( rec_change || have_record_to ) && to_file_ && to_memory_ )
    NestModule::get_network().message(SLIInterpreter::M_INFO, "RecordingDevice::set_status",
                                   "Data will be recorded to file and to memory.");

  if ( to_accumulator_ && ( to_file_ || to_screen_ || to_memory_ || withgid_ || withweight_ ) )
  {
      to_file_ = to_screen_ = to_memory_ = withgid_ = withweight_ = false;
      Node::network()->message(SLIInterpreter::M_WARNING, "RecordingDevice::set_status()",
                               "Accumulator mode selected. All incompatible properties "
                               "(to_file, to_screen, to_memory, withgid, withweight) "
                               "have been set to false.");
  }
}

void nest::RecordingDevice::State_::get(DictionaryDatum& d, const Parameters_& p) const
{
  // if we already have the n_events entry, we add to it, otherwise we create it
    if (d->known(names::n_events))
	(*d)[names::n_events] = getValue<long>(d, names::n_events) + events_;
    else 
	(*d)[names::n_events] = events_;

  DictionaryDatum dict;

  // if we already have the events dict, we use it, otherwise we create it
  if (!d->known(names::events))
    dict = DictionaryDatum(new Dictionary);
  else
    dict = getValue<DictionaryDatum>(d, names::events);

  if ( p.withgid_ )
  {
    assert(not p.to_accumulator_);
    initialize_property_intvector(dict, names::senders);
    append_property(dict, names::senders, std::vector<long>(event_senders_));
  }

  if ( p.withweight_ )
  {
    assert(not p.to_accumulator_);
    initialize_property_doublevector(dict, names::weights);
    append_property(dict, names::weights, std::vector<double_t>(event_weights_));
  }

  if ( p.withtime_ )
  {
    if ( p.time_in_steps_ )
    {
      initialize_property_intvector(dict, names::times);
      // When not accumulating, we just add time data. When accumulating, we must add
      // time data only from one thread and ensure that time data from other threads
      // is either empty of identical to what is present.
      if ( not p.to_accumulator_ ) 
	append_property(dict, names::times, std::vector<long>(event_times_steps_));
      else
	provide_property(dict, names::times, std::vector<long>(event_times_steps_));

      if ( p.precise_times_ )
      {
        initialize_property_doublevector(dict, names::offsets);
	if ( not p.to_accumulator_ )
	  append_property(dict, names::offsets, std::vector<double_t>(event_times_offsets_));
	else
	  provide_property(dict, names::offsets, std::vector<double_t>(event_times_offsets_));
      }
    }
    else
    {
      initialize_property_doublevector(dict, names::times);
      if ( not p.to_accumulator_ )
	append_property(dict, names::times, std::vector<double_t>(event_times_ms_));
      else
	provide_property(dict, names::times, std::vector<double_t>(event_times_ms_));
    }
  }

  (*d)[names::events] = dict;
}

void nest::RecordingDevice::State_::set(const DictionaryDatum& d)
{
  long_t ne = 0;
  if ( updateValue<long_t>(d, names::n_events, ne) )
  {
    if ( ne == 0 )
      events_ = 0;
    else
      throw BadProperty("n_events can only be set to 0.");
  }
}

 /* ----------------------------------------------------------------
  * Default and copy constructor for device
  * ---------------------------------------------------------------- */

nest::RecordingDevice::RecordingDevice(const Node& n, Mode mode, const std::string& file_ext,
				       bool withtime, bool withgid, bool withweight)
   : Device(),
     node_(n),
     mode_(mode),
     P_(file_ext, withtime, withgid, withweight),
     S_()
 {}

 nest::RecordingDevice::RecordingDevice(const Node& n, const RecordingDevice& d)
   : Device(d),
     node_(n),
     mode_(d.mode_),
     P_(d.P_),
     S_(d.S_)
 {}


 /* ----------------------------------------------------------------
  * Device initialization functions
  * ---------------------------------------------------------------- */

 void nest::RecordingDevice::init_parameters(const RecordingDevice& pr)
 {
   Device::init_parameters(pr);

   P_ = pr.P_;
   S_ = pr.S_;
 }

 void nest::RecordingDevice::init_state(const RecordingDevice& pr)
 {
   Device::init_state(pr);
   S_ = pr.S_;
 }

 void nest::RecordingDevice::init_buffers()
 {
   Device::init_buffers();

   // we only close files here, opening is left to calibrate()
   if ( P_.close_on_reset_ && B_.fs_.is_open() )
   {
     B_.fs_.close();
     P_.filename_.clear();  // filename_ only visible while file open
   }

 }

 void nest::RecordingDevice::calibrate()
 {
   Device::calibrate();

   if ( P_.to_file_ )
   {
     // do we need to (re-)open the file
     bool newfile = false;

     if ( !B_.fs_.is_open() )
     {
       newfile = true;   // no file from before
       P_.filename_ = build_filename_();
     }
     else
     {
       std::string newname = build_filename_();
       if ( newname != P_.filename_ )
       {
         Node::network()->message(SLIInterpreter::M_INFO,
				  "RecordingDevice::calibrate()",
				  "Closing file " + P_.filename_ +
				  ", opening file " + newname);

         B_.fs_.close(); // close old file
         P_.filename_ = newname;
         newfile = true;
       }
     }

     if ( newfile )
     {
       assert(!B_.fs_.is_open());

       if ( Node::network()->overwrite_files() )
       {
         if ( P_.binary_ )
           B_.fs_.open(P_.filename_.c_str(), std::ios::out | std::ios::binary);
         else
           B_.fs_.open(P_.filename_.c_str());
       }
       else
       {
         // try opening for reading
         std::ifstream test(P_.filename_.c_str());
         if ( test.good() )
         {
           Node::network()->message(SLIInterpreter::M_ERROR, "RecordingDevice::calibrate()",
             "The device file " + P_.filename_ + " exists already and will not be overwritten.\n"
             "Please change data_path, data_prefix or label, or set /overwrite_files to true in the root node." );
           throw IOError();
         }
         else
           test.close();

         // file does not exist, so we can open
         if ( P_.binary_ )
           B_.fs_.open(P_.filename_.c_str(), std::ios::out | std::ios::binary);
         else
           B_.fs_.open(P_.filename_.c_str());
       }

       if (P_.fbuffer_size_ != P_.fbuffer_size_old_)
       {
         if (P_.fbuffer_size_ == 0)
           B_.fs_.rdbuf()->pubsetbuf(0, 0);
         else
         {
           std::vector<char>* buffer = new std::vector<char>(P_.fbuffer_size_);
           B_.fs_.rdbuf()->pubsetbuf(reinterpret_cast<char*>(&buffer[0]), P_.fbuffer_size_);
         }
         
         P_.fbuffer_size_old_ = P_.fbuffer_size_;
       }
     }

     if ( !B_.fs_.good() )
     {
       Node::network()->message(SLIInterpreter::M_ERROR, "RecordingDevice::calibrate()",
                              "I/O error while opening file " + P_.filename_);
       if ( B_.fs_.is_open() )
         B_.fs_.close();
       P_.filename_.clear();
       throw IOError();
     }

     /* Set formatting
        Formatting is not applied to std::cout for screen output,
        since different devices may have different settings and
        this would lead to a mess.
      */
     if ( P_.scientific_ )
       B_.fs_ << std::scientific;
     else
       B_.fs_ << std::fixed;

     B_.fs_ << std::setprecision(P_.precision_);

     if (P_.fbuffer_size_ != P_.fbuffer_size_old_)
     {
       std::string msg = String::compose("Cannot set file buffer size, as the file is already "
                                         "openeded with a buffer size of %1. Please close the "
                                         "file first.", P_.fbuffer_size_old_);
       Node::network()->message(SLIInterpreter::M_ERROR, "RecordingDevice::calibrate()", msg);
       throw IOError();       
     }
   }

 }

 void nest::RecordingDevice::finalize()
 {
   if ( B_.fs_.is_open() )
   {
     if ( P_.close_after_simulate_ )
     {
       B_.fs_.close();
       return;
     }

     if ( P_.flush_after_simulate_ )
       B_.fs_.flush();

     if ( !B_.fs_.good() )
     {
       Node::network()->message(SLIInterpreter::M_ERROR, "RecordingDevice::finalize()",
                              "I/O error while writing to file " + P_.filename_);
       throw IOError();
     }
   }
 }

 /* ----------------------------------------------------------------
  * Other functions
  * ---------------------------------------------------------------- */

void nest::RecordingDevice::set_status(const DictionaryDatum &d)
{
  Parameters_ ptmp = P_;    // temporary copy in case of errors
  ptmp.set(*this, B_, d);   // throws if BadProperty
  State_      stmp = S_;
  stmp.set(d);

  // We now know that (ptmp, stmp) are consistent. We do not
  // write them back to (P_, S_) before we are also sure that
  // the properties to be set in the parent class are internally
  // consistent.
  Device::set_status(d);

  // if we get here, temporaries contain consistent set of properties
  P_ = ptmp;
  S_ = stmp;

  if ( !P_.to_file_ && B_.fs_.is_open() )
  {
    B_.fs_.close();
    P_.filename_.clear();
  }

  if ( S_.events_ == 0 )
    S_.clear_events();
}


void nest::RecordingDevice::record_event(const Event& event, bool endrecord)
{
  ++S_.events_;
  const index  sender = event.get_sender_gid();
  const Time   stamp  = event.get_stamp();
  const double offset = event.get_offset();
  const double weight = event.get_weight();

  //std::cout << "recording device sender: " << sender << std::endl;

  if ( P_.to_screen_ )
  {
    print_id_(std::cout, sender);
    print_time_(std::cout, stamp, offset);
    print_weight_(std::cout, weight);
    if ( endrecord )
      std::cout << '\n';
  }

  if ( P_.to_file_ )
  {
    print_id_(B_.fs_, sender);
    print_time_(B_.fs_, stamp, offset);
    print_weight_(B_.fs_, weight);
    if ( endrecord )
    {
      B_.fs_ << '\n';
      if ( P_.flush_records_ )
        B_.fs_.flush();
    }
  }

  // storing data when recording to accumulator relies on the fact
  // that multimeter will call us only once per accumulation step
  if ( P_.to_memory_ || P_.to_accumulator_ )
      store_data_(sender, stamp, offset, weight);
}

void nest::RecordingDevice::print_id_(std::ostream& os, index gid)
{
  if ( P_.withgid_ )
    os << gid << '\t';
}

void nest::RecordingDevice::print_time_(std::ostream& os, const Time& t, double offs)
{
  if ( !P_.withtime_ )
    return;

  if ( P_.time_in_steps_ )
  {
    os << t.get_steps() << '\t';
    if ( P_.precise_times_ )
      os << offs << '\t';
  }
  else if ( P_.precise_times_ )
    os << t.get_ms() - offs << '\t';
  else
    os << t.get_ms() << '\t';
}

void nest::RecordingDevice::print_weight_(std::ostream& os, double weight)
{
  if ( P_.withweight_ )
    os << weight << '\t';
}


void nest::RecordingDevice::store_data_(index sender, const Time& t, double offs, double weight)
{
  if ( P_.withgid_ )
    S_.event_senders_.push_back(sender);

  if ( P_.withtime_ )
  {
    if ( P_.time_in_steps_ )
    {
      S_.event_times_steps_.push_back(t.get_steps());
      if ( P_.precise_times_ )
        S_.event_times_offsets_.push_back(offs);
    }
    else if ( P_.precise_times_ )
      S_.event_times_ms_.push_back(t.get_ms()-offs);
    else
      S_.event_times_ms_.push_back(t.get_ms());
  }

  if ( P_.withweight_ )
    S_.event_weights_.push_back(weight);
}

const std::string nest::RecordingDevice::build_filename_() const
{
  // number of digits in number of virtual processes
  const int vpdigits = static_cast<int>(std::floor(std::log10(static_cast<float>(Communicator::get_num_virtual_processes()))) + 1);
  const int gidigits = static_cast<int>(std::floor(std::log10(static_cast<float>(Node::network()->size()))) + 1);

  std::ostringstream basename;
  const std::string& path = Node::network()->get_data_path();
  if ( !path.empty() )
    basename << path << '/';
  basename << Node::network()->get_data_prefix();


  if ( !P_.label_.empty() )
    basename << P_.label_;
  else
    basename << node_.get_name();

  basename << "-" << std::setfill('0') << std::setw(gidigits) << node_.get_gid()
           << "-" << std::setfill('0') << std::setw(vpdigits) << node_.get_vp();
  return basename.str() + '.' + P_.file_ext_;
}

void nest::RecordingDevice::State_::clear_events()
{
  events_ = 0;
  event_senders_.clear();
  event_times_ms_.clear();
  event_times_steps_.clear();
  event_times_offsets_.clear();
  event_weights_.clear();
}