/***************************** 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"
#include "MvLog.h"

// =====================================================
// class BufrDecoderFactory - builds  decoders
//
class BufrDecoderFactory : public DecoderFactory
{
    // Virtual Constructor - Builds a new BufrDecoder
    Decoder* Build(const MvRequest& inRequest) override
    {
        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_{0};
    GroupInfoMap groupInfo_;
};
using BufrInfoMap = std::multimap<std::string, BufrInfo, std::less<std::string> >;
using BufrInfoIterator = BufrInfoMap::iterator;

// 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 = nullptr;
    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 {
        MvLog().popup().err() << "BufrDecoder-> File not OK: " << name;
        cleanMap();
    }
}

BufrDecoder::~BufrDecoder() = default;

// 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
    std::string datestr = (*groupIterator_).first;
    std::string yyyymmdd = datestr.substr(0, 8);
    std::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)
{
    std::string groupTime, groupFileName;
    MvObs obs;
    TDynamicTime obsTime;
    GroupInfoIterator ii;

    bool writeData = true;
    time_t mtime = 0;

    // 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) {
        MvLog().popup().err() << "BufrDecoder-> File not OK : " << path_;
        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()) {
            auto 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) {
                // Existing map entry too old, erasing
                bufrInfoMap.erase(currentBufr);
                break;
            }
            // Check that we have the same grouping interval
            else if ((*currentBufr).second.groupInterval_ == ObsGroupControl::groupPeriod()) {
                // Using existing files, new files not created
                writeData = false;
                groupInfo_ = (*currentBufr).second.groupInfo_;
                groupIterator_ = groupInfo_.begin();
                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
            std::pair<const std::string, GroupInfo*> pp(groupTime, new GroupInfo(groupFileName, writeData));
            std::pair<GroupInfoIterator, bool> retpair = groupInfo_.insert(pp);
            if (retpair.second == false) {
                MvLog().popup().err() << "BufrDecoder-> Can not insert grouping info for " << groupTime;
            }
            else {
                auto 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(std::pair<const std::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 ======================" << std::endl;
   for ( bufrii = bufrInfoMap.begin();  bufrii != bufrInfoMap.end(); bufrii++)
   {
      COUT << "ORIGINAL FILE : " << (*bufrii).first << std::endl;
      for (ii = (*bufrii).second.groupInfo_.begin(); ii != (*bufrii).second.groupInfo_.end(); ii++)
         COUT << "\t" << (*ii).second->path().c_str() << std::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) << std::endl;
}

// BufrDecoder::findTime
// Uses the TDynamicTime returned from the obs. to get the
// correct grouping time.
void BufrDecoder::findTime(TDynamicTime& tt, std::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(std::string& fname, const std::string& grouptime)
{
    std::string tmpdir(getenv("METVIEW_TMPDIR"));

    int myGroupingPeriod = ObsGroupControl::groupPeriod();

    // Does not handle trailing "/" properly.
    std::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()
{
    std::string curr_path;
    BufrInfo curr_info;
    GroupInfoIterator ii;
    struct stat bufrstat
    {
    };

    auto bufrii = bufrInfoMap.begin();
    while (bufrii != bufrInfoMap.end()) {
        auto 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();

            // Erasing entry for curr_path.c_str()
            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;
        MvLog().info() << "Obs grouping period is "
                       << groupPeriodHours_
                       << " hours, start minute is "
                       << groupPeriodStartMinute_
                       << ", values from "
                       << (useObsTime_ ? "observation" : "metadata (section 1)")
                       << std::ends;
    }
}

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.
std::string
ObsGroupControl::groupTimeString(const TDynamicTime& tim)
{
    groupingValues();

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

    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 std::string& groupTimeString)
{
    short yyyy = 0, mm = 0, dd = 0, hh = 0;
    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());

    MvLog().info() << message;
}

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

        MvLog().info() << "Obs grouping period changed to "
                       << groupPeriodHours_
                       << " hours"
                       << std::ends;
    }
}

void ObsGroupControl::setGroupingPeriodStart(int minutes)
{
    if (groupPeriodStartMinute_ != minutes) {
        groupPeriodStartMinute_ = minutes;
        MvLog().info() << "Obs grouping period start minute changed to "
                       << groupPeriodStartMinute_
                       << std::ends;
    }
}
