/***************************** LICENSE START ***********************************

 Copyright 2012 ECMWF and INPE. This software is distributed under the terms
 of the Apache License version 2.0. In applying this license, ECMWF does not
 waive the privileges and immunities granted to it by virtue of its status as
 an Intergovernmental Organization or submit itself to any jurisdiction.

 ***************************** LICENSE END *************************************/

#include "BufrDecoder.h"
#include "MatchingInfo.h"
#include "PlotMod.h"

// =====================================================
// class BufrDecoderFactory - builds  decoders
//
class BufrDecoderFactory : public DecoderFactory
{
  // Virtual Constructor - Builds a new BufrDecoder
  virtual Decoder* Build ( const MvRequest& inRequest )
    { return new BufrDecoder ( inRequest ); }

public:
  BufrDecoderFactory() : DecoderFactory("BufrDecoder") {}
};

static BufrDecoderFactory bufrDecoderFactoryInstance;


//---------------------------------------------------------------
// Helper class to find out whether to rewrite the grouped files,
// or use existing ones. A static map keeps the information 
// about original files, and of which grouped files have been created
// for these files.
struct BufrInfo
{
  time_t mtime_;
  int    groupInterval_;
  GroupInfoMap groupInfo_;
};
typedef multimap<string,BufrInfo, less<string> > BufrInfoMap;
typedef BufrInfoMap::iterator BufrInfoIterator;

// Static map of existing grouped files.
static BufrInfoMap bufrInfoMap;
//--------------------------------------------------------


//=======================================================
//
// Methods for the BufrDecoder class
//
BufrDecoder::BufrDecoder( const MvRequest& dataUnitRequest ) :
  Decoder(dataUnitRequest)
{ 
  struct stat bufrstat;

  const char *name = 0;
  dataUnitRequest.getValue( name, "PATH" );

  if ( stat(name,&bufrstat ) == 0 )
  {
      MvObsSet obsSet(name);
      MvObsSetIterator   obsIterator(obsSet);
      path_ = name;

      // Group the obs. into files
      groupInfo(obsIterator);
  }
  else
  {
      cerr << "File : " << name << "  not OK." << endl;
      perror("BufrDecoder problem ");
      cleanMap();
  }
}

BufrDecoder::~BufrDecoder()
{
  // Empty
}

// BufrDecoder::ReadNextData
// Return true while there are more data. Uses the groupInfo_
// and offset_ members to determine when there's no more data.
bool BufrDecoder::ReadNextData ()
{

  if ( offset_++ < (signed int)groupInfo_.size() )
    return true;
  else
    return false;
}

// BufrDecoder::CreateMatchingInfo
// Create a MATCHING_INFO request and construct a
// MatchingInfo from it.
// Currently only match on time.
MatchingInfo BufrDecoder::CreateMatchingInfo ()
{
  MvRequest bufrRequest("MATCHING_INFO");

  bufrRequest("PATH") = (*groupIterator_).second->path().c_str();

  // Format the output to match the one for GRIB
  string datestr  = (*groupIterator_).first;
  string yyyymmdd = datestr.substr(0,8);
  string hhmm     = datestr.substr(8,4);

  bufrRequest("DATE") = yyyymmdd.c_str();
  bufrRequest("TIME") = hhmm.c_str();

  bufrRequest( "DATA_TYPE" ) = "BUFR";

  groupIterator_++;

  return MatchingInfo( bufrRequest );
}

// BufrDecoder::groupInfo
// Is called the first time ReadNextData is called. 
// Groups the obs. into files, which will be given
// to the view in MatchingInfo's later. 
void BufrDecoder::groupInfo(MvObsSetIterator &iter)
{
   string groupTime,groupFileName;
   MvObs obs;
   TDynamicTime obsTime;
   GroupInfoIterator ii;

   bool writeData = true;
   time_t mtime;

   // Only for timing during development
   //time_t clock;
   //time(&clock);

   // Find status and last modification of file
   struct stat bufrstat;
   if ( stat(path_.c_str(),&bufrstat) < 0  )
   {
      cerr << "File not OK : " << path_.c_str()  << endl;
      perror("BufrDecoder problem ");
      cleanMap();
      return;
   }
   else
      mtime = bufrstat.st_mtime;

   // Try to find entries in static map
   BufrInfoIterator bufrii;
   if ( ( bufrii = bufrInfoMap.find(path_) ) != bufrInfoMap.end() )
   {
      // Find the correct entry
      while( bufrii != bufrInfoMap.end() )
      {
         BufrInfoIterator currentBufr = bufrii;
         ++bufrii;  //-- advance before possible erasing

         // No more entries for this file
         if ( (*currentBufr).first != path_ )
            break;

         // Check modification time
         if ( (*currentBufr).second.mtime_ < mtime )
         {
            cout << "Existing map entry too old, erasing" << endl;
            bufrInfoMap.erase(currentBufr);
            break;
         }
         // Check that we have the same grouping interval
         else if ( (*currentBufr).second.groupInterval_ == ObsGroupControl::groupPeriod() )
         {
            writeData = false;
            groupInfo_ = (*currentBufr).second.groupInfo_;
            groupIterator_ = groupInfo_.begin();
            cout << "Using existing files, new files not created" << endl;
            break;
         }
      }
   }

  // Return here if we use data files produced previously.
  if ( ! writeData )
    return;

   // Files not found, need to process the file
   // FAMI20171017 : VERY IMPORTANT : grouping observations by time is disabled.
   // Consenquently, the intermediate BUFR files (one for each group of observations)
   // will not be generated. Therefore, only the input BUFR file will be sent to MAGICS.
#if 0 //FAMI20171017
   while ( (obs = iter(NR_returnMsg)) )
#else
   writeData = false;
   if ( (obs = iter(NR_returnMsg)) )
#endif
   {
      // Find the correct time to use for grouping
      // Check if this time exists, if so write the
      // obs to the file for the right time, else
      // create a new entry and write the obs.
      if( ObsGroupControl::useObsTime() )
         obsTime = obs.obsTime();       //-- slow: needs to decode the msg
      else
         obsTime = obs.msgTime();       //-- fast: use metadata, no decoding

      findTime(obsTime,groupTime);
      
      if ( (ii = groupInfo_.find(groupTime)) != groupInfo_.end() )
         (*ii).second->write(obs);
      else
      {
#if 0 //FAMI20171017
         generateGroupFileName(groupFileName,groupTime);
#else
         groupFileName = path_;
#endif

         // Insert a pair into map
         pair<const string,GroupInfo*> pp(groupTime, new GroupInfo(groupFileName,writeData ) );
         pair<GroupInfoIterator,bool> retpair = groupInfo_.insert(pp);
         if ( retpair.second == false )
            cerr << "Can not insert grouping info for " << groupTime.c_str() << endl;
         else
         {
            GroupInfoIterator newii = retpair.first;
            (*newii).second->write(obs);
            ObsGroupControl::printGroupingLimits(groupTime);
         }
      }
   }

   // All obs. are written. Now go through the map and close all files
   for (groupIterator_ = groupInfo_.begin(); groupIterator_ != groupInfo_.end(); groupIterator_++)
      (*groupIterator_).second->close();

   // Add file info to static map
   BufrInfo bufrinfo;
   bufrinfo.mtime_         = mtime;
   bufrinfo.groupInterval_ = ObsGroupControl::groupPeriod();
   bufrinfo.groupInfo_     = groupInfo_;
   bufrInfoMap.insert(pair<const string,BufrInfo>(path_,bufrinfo));

   // Rewind the iterator
   groupIterator_ = groupInfo_.begin();

#if 0
   // Print out all info in static map for debugging purposes
   cout << "================= STATIC MAP ======================" << endl;
   for ( bufrii = bufrInfoMap.begin();  bufrii != bufrInfoMap.end(); bufrii++)
   {
      cout << "ORIGINAL FILE : " << (*bufrii).first << endl;
      for (ii = (*bufrii).second.groupInfo_.begin(); ii != (*bufrii).second.groupInfo_.end(); ii++)
         cout << "\t" << (*ii).second->path().c_str() << endl;
   }
#endif

   // Clean up any files where the source file does not exist any more
   cleanMap();

   // For testing
   //time(&clock);
   //cout << " After grouping " << ctime(&clock) << endl;
}

// BufrDecoder::findTime
// Uses the TDynamicTime returned from the obs. to get the
// correct grouping time.
void BufrDecoder::findTime(TDynamicTime &tt,string &gtime)
{
  gtime = ObsGroupControl::groupTimeString( tt );
}

// Generate a file name in TMPDIR. Build the name from the name of
// original BUFR file, grouptime and groupinginterval;
void BufrDecoder::generateGroupFileName(string &fname,const string &grouptime)
{
  string tmpdir(getenv("METVIEW_TMPDIR"));

  int myGroupingPeriod = ObsGroupControl::groupPeriod();

  // Does not handle trailing "/" properly.
  string base_name = path_.substr(path_.find_last_of("/") + 1);

  fname = tmpdir + '/' + base_name + '_' + grouptime + '_';
  char ctime[5];
  sprintf( ctime, "%d", myGroupingPeriod );
  fname += ctime;
}

// Clean all entries in static map where the original file does not exist
// any more. This will also delete any grouped files created from the 
// original one.
void BufrDecoder::cleanMap()
{
  string curr_path;
  BufrInfo curr_info;
  GroupInfoIterator ii;
  struct stat bufrstat;

  BufrInfoIterator bufrii = bufrInfoMap.begin();
  while( bufrii != bufrInfoMap.end() )
    {
      BufrInfoIterator currentBufr = bufrii;
      ++bufrii;	  //-- advance before possible erase!

      curr_path = (*currentBufr).first;
      curr_info = (*currentBufr).second;

      if ( stat(curr_path.c_str(),&bufrstat)  < 0 )
	{
	  for ( ii = curr_info.groupInfo_.begin();
		ii != curr_info.groupInfo_.end(); ii++)
	    (*ii).second->deleteFile();

	  cout << "Erasing entry for " << curr_path.c_str() << endl;
	  bufrInfoMap.erase(currentBufr);
	}
    }
}

//__________________________________________________________________

//-- default obs grouping parameters, valid for ECMWF --//
int  ObsGroupControl::groupPeriodHours_       = 6;
int  ObsGroupControl::groupPeriodStartMinute_ = 1;
bool ObsGroupControl::useObsTime_             = false;
bool ObsGroupControl::firstTime_              = true;

void
ObsGroupControl::groupingValues()
{
  if( firstTime_ )
    {
      firstTime_ = false;

      std::ostringstream sbuf;
      sbuf << "Obs grouping period is "
	   << groupPeriodHours_
	   << " hours, start minute is "
	   << groupPeriodStartMinute_
	   << ", values from "
	   << ( useObsTime_ ? "observation" : "metadata (section 1)" )
	   << ends;
      PlotMod::Instance().MetviewError( sbuf.str(), "Info" );
    }
}

int
ObsGroupControl::groupPeriod()
{
  //  groupingValues();
  return groupPeriodHours_;
}

int
ObsGroupControl::groupPeriodStartMinute()
{
  //  groupingValues();
  return groupPeriodStartMinute_;
}

// Please consult printGroupingLimits::groupTimeString() - if the code
// changes in there, then this function may also have to be updated.
string
ObsGroupControl::groupTimeString( const TDynamicTime& tim )
{
  groupingValues();

  short yy,mm,dd;
  short hh,min,sec;

  TDynamicTime tt( tim );

  //-- ECMWF uses grouping XX01...YY00, UKMO uses XX00...(YY-1)59
  //-- If grouping does not start XX00 then use a trick to subtract extra minute(s) --//
  if( ObsGroupControl::groupPeriodStartMinute() )
    {
      tt.ChangeByMinutes( -ObsGroupControl::groupPeriodStartMinute() );
    }

  tt.GetDate(yy,mm,dd);
  tt.GetTime(hh,min,sec);

  //-- calculate the correct group time hour --//
  int myGroupSize = ObsGroupControl::groupPeriod();
  hh = ( (hh  + myGroupSize/2 ) / myGroupSize ) * myGroupSize;
  if ( hh >= 24 ) 
    {
      hh -= 24;
      tt.ChangeByDays(1);
      tt.SetTime(hh,min,sec);
      tt.GetDate(yy,mm,dd);
    }

  //-- build the group time string --//
  static char buf[20];
  sprintf(buf,"%04d%02d%02d%02d00",yy,mm,dd,hh);

  return buf;
}

// Prints a user message to indicate the range of times that
// will be included within the given time group.
// Please consult ObsGroupControl::groupTimeString() - if the code
// changes in there, then this function may also have to be updated.
void
ObsGroupControl::printGroupingLimits( const string &groupTimeString )
{
    short yyyy,mm,dd, hh;
    char  message[512];
    int   groupHours       = ObsGroupControl::groupPeriod();
    int   groupStartMinute = ObsGroupControl::groupPeriodStartMinute();
    int   minutesAtStart   = ((groupHours / 2) * 60);
    int   minutesAtEnd     = minutesAtStart;

    // the variable 'minutes' stores the number of minutes between the
    // 'official' group period hour (e.g. 06:00, i.e. midpoint of the period
    //  03:00 to 09:00) and the period's limits (in this case 180) if we exclude
    // groupStartMinute for the moment (normally we use 03:01, not 03:00).
    // However, for odd-numbered group hours, our mid-point hour is not
    // evenly between its 'limit' hours, e.g. 02:00 to 05:00 has a group
    // hour of 3. So we must adjust for this in our calculations.

    if (groupHours%2 == 1)
    {
        minutesAtEnd += 60;
    }

    // parse the grouping time string into its components
    sscanf (groupTimeString.c_str(), "%4hd%2hd%2hd%2hd00", &yyyy,&mm,&dd,&hh);

    // turn that into a proper 'time' structure
    TDynamicTime groupTime(yyyy,mm,dd,hh);

    // find the start of this time period
    TDynamicTime startGroupTime (groupTime);
    startGroupTime.ChangeByMinutes (-minutesAtStart + groupStartMinute);

    // find the end of this time period
    TDynamicTime endGroupTime (groupTime);
    endGroupTime.ChangeByMinutes (minutesAtEnd + groupStartMinute - 1);

    // create a message to tell the user of these period limits
    sprintf (message, "New obs grouping period: %04d%02d%02d-%02d:%02d to %04d%02d%02d-%02d:%02d",
                       startGroupTime.GetYear(),
                       startGroupTime.GetMonth(),
                       startGroupTime.GetDay(),
                       startGroupTime.GetHour(),
                       startGroupTime.GetMin(),
                       endGroupTime.GetYear(),
                       endGroupTime.GetMonth(),
                       endGroupTime.GetDay(),
                       endGroupTime.GetHour(),
                       endGroupTime.GetMin());

    PlotMod::Instance().MetviewError( message, "Info" );
}

void
ObsGroupControl::setGroupingPeriod( int hours )
{
  if( groupPeriodHours_ != hours )
    {
      groupPeriodHours_ = hours;

      std::ostringstream sbuf;
      sbuf << "Obs grouping period changed to "
	   << groupPeriodHours_
	   << " hours"
	   << ends;
      PlotMod::Instance().MetviewError( sbuf.str(), "Info" );
    }
}

void
ObsGroupControl::setGroupingPeriodStart( int minutes )
{
  if( groupPeriodStartMinute_ != minutes )
    {
      groupPeriodStartMinute_ = minutes;

      std::ostringstream sbuf;
      sbuf << "Obs grouping period start minute changed to "
	   << groupPeriodStartMinute_
	   << ends;
      PlotMod::Instance().MetviewError( sbuf.str(), "Info" );
    }
}
