/***************************** 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 "BufrThermo.h"
#include "MvNetCDF.h"
#include "MvObsSet.h"
#include "MvRequestUtil.hpp"

const int TOBSTEMP     = 12001;
const int TOBSTD       = 12003;
const int TOBSDIR      = 11001;
const int TOBSSTRENGTH = 11002;
const double PI        = 3.14159265;

BufrThermo::BufrThermo() :
                   Thermo("BUFR_THERMO"),
                   counter_(0),
                   pCount_(0),
                   nlev_(0),
                   nTIME_STR_(13)
{ }

BufrThermo::BufrThermo(const char* kw) :
                   Thermo(kw)
{ }

void
BufrThermo::allocateDataMemory()
{
   pCount_ = 0;
   T_.clear(); TD_.clear();
   U_.clear(); V_.clear();
   P_.clear();

   // Allocate memory
   if ( nlev_ > 0 )
   {
      T_.resize(nlev_);
      TD_.resize(nlev_);
      U_.resize(nlev_);
      V_.resize(nlev_);
      P_.resize(nlev_);
   }
}

void
BufrThermo::initialiseDataMemory()
{
   pCount_ = 0;
   for ( int i = 0; i < nlev_; i++ )
   {
      T_[i]  = MissingValue;
      TD_[i] = MissingValue;
      U_[i]  = MissingValue;
      V_[i]  = MissingValue;
      P_[i]  = MissingValue;
   }
}

bool BufrThermo::getAppParameters( MvRequest& in )
{
   // It is expected that the input request is expanded

   // Get station input parameter
   station_ = in.getSubrequest("STATION");
   if ( !station_ )
   {
      setError(1,"Thermo ERROR: No Station info specified!");
      return false;
   }

   return true;
}

// The data structure contains vectors for Temp, Td, Wind and Pressure.
// There is only one Pressure vector related to the other 3 data. This
// means that these 3 data may contain different number of Missing 
// values, which will be allocated in those positions when there is no
// Pressure level defined.
bool BufrThermo::processData()
{
   // If it is not a BUFR data then it has been already processed
   if ( strcmp(dataRequest_.getVerb(),"BUFR") != 0 )
      return true;

   // Create a temporary netCDF file
   string ncFileName = this->ncFileName();
   MvNetCDF netcdf(ncFileName,'w');
   if ( !netcdf.isValid() )
   {
      setError(1,"Thermo ERROR: Can not open a new netCDF file");
      return false;
   }

   // Get location info from the station
   MvObsSet obsset(dataRequest_);
   MvObsSetIterator iter(obsset);
   if( !getIdent(station_,iter) )
   {
      if( !getLocation(station_,iter) )
     {
        setError(1, "Thermo ERROR: WMO station number and/or location missing...");
        return false;
     }
   }

   // Initialize obs variables
   MvBufrParam obsTemp(TOBSTEMP), obsTd(TOBSTD),
               obsDirection(TOBSDIR), obsStrength(TOBSSTRENGTH);

   iter.setMessageType(2); //-- Vertical soundings (other than satellite)

   // Evaluate dimensions
   // Remove this function after updating library MvNetCDF to create
   // files of type Netcdf4 (allowing multiple unlimited dimensions)
   nlev_ = this->evaluateNLevels(iter);
   if ( nlev_ <= 0 )
   {
      setError(0,"Thermo: No Levels found" );
      return false;
   }

   // Allocate memory
   this->allocateDataMemory();

   // Main loop
   // Compute data values
   MvObs obs;
   obsset.rewind();
   counter_ = 0;
   while ( obs = iter() )
   {
      // Skip PILOT
      if( obs.messageSubtype() !=  101 &&  obs.messageSubtype() !=  109 )  //obs.messageSubtype() == 91 )  //-- ECMWF code for PILOT!
      {
         setError(0,"Thermo: Skip PILOT soundings (full TEMP sounding needed)" );
         continue;
      }

      //obs.printAllValues();

      // Initialise data values storage with MissingValue
      this->initialiseDataMemory();

      // Get/compute data values for all pressure levels
      int ic = 0;
      float PrevLevel = FLT_MAX;
      float flevel = obs.firstPressureLevel();
      while( flevel != MissingValue )
      {
         //-- Pressure levels decrease in the main sounding data.
         //-- At the end of msg there can be data for "special levels"
         //-- ( e.g. 'Maximum wind level') which refer back to lower
         //-- levels (higher pressure) and which should not be used.
         if( flevel >= PrevLevel )
            break;

         float myT   = obs.valueByPressureLevel( flevel, obsTemp);
         float myDew = obs.valueByPressureLevel( flevel, obsTd);
         float myDir = obs.valueByPressureLevel( flevel, obsDirection);
         float myStr = obs.valueByPressureLevel( flevel, obsStrength);

         // Temperatura value
         if ( myT != MissingValue )
            T_[ic] = myT - K2C;

         // Dewpoint value
         if ( myDew != MissingValue )
         TD_[ic] = myDew - K2C;

         // Wind values
         if ( myDir != MissingValue )
         {
            U_[ic] = -myStr * sin(myDir*PI/180.0);
            V_[ic] = -myStr * cos(myDir*PI/180.0);
         }

         // Pressure value
         P_[ic] = flevel;
         pCount_++;

         ic++;
         PrevLevel = flevel;
         flevel = obs.nextPressureLevel();
      } // end while

      // Consistency check
      if ( pCount_ <= 1)
      {
         setError(1,"Thermo ERROR: Problems with BUFR data.....");
         return false;
      }

      // Write info to the netCDF file
      if ( !this->generate(netcdf,obs) )
      {
         setError(1,"Thermo ERROR: generate netCDF");
         return false;
      }

      counter_++;
   } // end main loop

   if ( counter_ == 0 )
   {
      setError(1,"Thermo ERROR: No Data found for given Station...");
      return false;
   }

   // Close netCDF file
   netcdf.close();

   return true;
}

bool BufrThermo::generate( MvNetCDF& netcdf, MvObs& obs )
{
   // Create netcdf variables, dimensions and global atributes
   if ( counter_ == 0 )
   {
      if ( !this->ncInit(netcdf,obs) )
      {
         setError(1,"Thermo ERROR: Failure to initilise a netCDF file");
         return false;
      }
   }

   // Get date and time info and generate time key (yyyymmddhhmm)
   TMetTime tmpdate = obs.obsTime();
   ostringstream oss;
   oss << setfill( '0' )
       << setw( 4 ) << tmpdate.GetYear()
       << setw( 2 ) << tmpdate.GetMonth()
       << setw( 2 ) << tmpdate.GetDay()
       << setw( 2 ) << tmpdate.GetHour()
       << setw( 2 ) << tmpdate.GetMin();

   // Get netcdf variables
   MvNcVar *time  = netcdf.getVariable(sTIME);
   MvNcVar *nlev  = netcdf.getVariable(sNLEV);
   MvNcVar *temp  = netcdf.getVariable(sTEMP);
   MvNcVar *dew   = netcdf.getVariable(sDEWPOINT);
   MvNcVar *press = netcdf.getVariable(sPRESSURE);
   MvNcVar *uwind = netcdf.getVariable(sU);
   MvNcVar *vwind = netcdf.getVariable(sV);
   MvNcVar *xwind = netcdf.getVariable(sXWIND);
   if ( !time || !temp || !dew || !press || !uwind || !vwind || !xwind )
   {
      setError(1,"Thermo ERROR: accessing netCDF variables");
      return false;
   }

   // Add data to the variables
   const char* ctime = oss.str().c_str();
   time->setCurrent(counter_);
   bool btime = time->put(ctime,1,strlen(ctime)+1);
   nlev->setCurrent(counter_);
   bool bnlev = nlev->put(&pCount_,1,1);
   temp->setCurrent(counter_);
   bool btemp = temp->put(T_,1,nlev_);
   dew->setCurrent(counter_);
   bool bdew = dew->put(TD_,1,nlev_);
   press->setCurrent(counter_);
   bool bpress = press->put(P_,1,nlev_);
   uwind->setCurrent(counter_);
   bool buwind = uwind->put(U_,1,nlev_);
   vwind->setCurrent(counter_);
   bool bvwind = vwind->put(V_,1,nlev_);
   xwind->setCurrent(counter_);
   vector <double> xv(nlev_,1000.5);
   bool bxwind = xwind->put(xv,1,nlev_);
   if ( !btime || !bnlev || !btemp || !bdew || !bpress || !buwind || !bvwind || !bxwind )
   {
      setError(1,"Thermo ERROR: writing a netCDF variable");
      return false;
   }

   return true;
}

bool BufrThermo::ncInit(MvNetCDF &netcdf, MvObs& obs)
{
   // Get geographical location
   MvLocation loc = obs.location();
   ostringstream oss;
   oss << loc.latitude() << '/' << loc.longitude();

   // Add global attributes to the netcdf file
   netcdf.addAttribute("_FILL_VALUE",MissingValue);
   netcdf.addAttribute("Station",obs.WmoIdentNumber());
   netcdf.addAttribute("Coordinates",oss.str().c_str());

   // Create variables
   vector<long> values_ndim;
   vector<string> values_sdim;
   values_ndim.push_back(0);    // unlimited
   values_sdim.push_back(sTIME);

   // Create variable for number of levels
   if ( !netcdf.addVariable(sNLEV,ncInt,values_ndim,values_sdim) )
      return false;

   // Create variable time
   values_ndim.push_back(nTIME_STR_);
   values_sdim.push_back(sTIME_STR);
   if ( !netcdf.addVariable(sTIME,ncChar,values_ndim,values_sdim) )
      return false;

   // Create variables temperature, dewpoint, pressure, wind
   values_ndim[1] = nlev_;
   values_sdim[1] = sNLEV;
   if ( !netcdf.addVariable(sTEMP,ncFloat,values_ndim,values_sdim) )
      return false;
   if ( !netcdf.addVariable(sDEWPOINT,ncFloat,values_ndim,values_sdim) )
      return false;
   if ( !netcdf.addVariable(sPRESSURE,ncFloat,values_ndim,values_sdim) )
      return false;
   if ( !netcdf.addVariable(sU,ncFloat,values_ndim,values_sdim) )
      return false;
   if ( !netcdf.addVariable(sV,ncFloat,values_ndim,values_sdim) )
      return false;
   if ( !netcdf.addVariable(sXWIND,ncFloat,values_ndim,values_sdim) )
      return false;

   return true;
}

bool BufrThermo::createOutputRequest(MvRequest& out)
{
   // Get default parameter to be plotted
   //ParamInfo* parInfo = plotVariable(params);
   //if ( !parInfo )
   //   return false;

   // Create netCDF data request
   MvRequest ncDataReq("NETCDF");
   ncDataReq("PATH") = this->ncFileName().c_str();
   ncDataReq("TEMPORARY") = 1;

   // Create Temperature output request
   int thermoId = rand() % 10000;
   MvRequest dReq("NETCDF_XY_POINTS");
   dReq("NETCDF_DATA") = ncDataReq;
   dReq("NETCDF_Y_VARIABLE") = sPRESSURE.c_str();
   dReq("NETCDF_X_VARIABLE") = sTEMP.c_str();
   dReq("_MODULEID") = thermoId;

   MvRequest gReq("MGRAPH");
   gReq("GRAPH_LINE_COLOUR") = "red";
   gReq("GRAPH_LINE_THICKNESS") = 8;
   gReq("GRAPH_MISSING_DATA_MODE") = "join";
   gReq("GRAPH_MISSING_DATA_STYLE") = "solid";
   gReq("GRAPH_MISSING_DATA_THICKNESS") = 8;
   gReq("_CLASS") = "MGRAPH";
   gReq("_MODULEID") = thermoId;
   gReq("_SKIP_MACRO") = 1;  // skip macro converter
   MvRequest dataReq = dReq + gReq;

   // Create Dewpoint output request
   dReq("NETCDF_X_VARIABLE") = sDEWPOINT.c_str();
   dReq("_SKIP_MACRO") = 1;  // skip macro converter

   gReq("GRAPH_LINE_STYLE") = "dash",
   gReq("GRAPH_MISSING_DATA_STYLE") = "dash";

   // Create wind output request
   MvRequest wdReq("NETCDF_XY_VECTORS");
   wdReq("NETCDF_DATA") = ncDataReq;
   wdReq("NETCDF_Y_POSITION_VARIABLE")  = sPRESSURE.c_str();
   wdReq("NETCDF_X_POSITION_VARIABLE")  = sXWIND.c_str();
   wdReq("NETCDF_X_COMPONENT_VARIABLE") = sU.c_str();
   wdReq("NETCDF_Y_COMPONENT_VARIABLE") = sV.c_str();
   wdReq("_MODULEID") = thermoId;
   wdReq("_SKIP_MACRO") = 1;  // skip macro converter

   MvRequest wReq("MWIND");
   wReq("WIND_FIELD_TYPE")  = "flags";
   wReq("WIND_FLAG_COLOUR") = "black";
   wReq("_CLASS") = "MWIND";
   wReq("_MODULEID") = thermoId;
   wReq("_SKIP_MACRO") = 1;  // skip macro converter

   dataReq = dataReq + dReq + gReq + wdReq + wReq;

   // Add information to help the Macro Converter to translate
   // the output request to a Macro code
   dataReq("_ORIGINAL_REQUEST") = this->buildMacroConverterRequest();

   // Create a default ThermoView request
   MvRequest viewReq("THERMOVIEW");
   if ( origReq_ && strcmp(origReq_.getVerb(),"THERMOVIEW") == 0 )
      viewReq = origReq_;
   else if ( (const char*)origReq_("_CONTEXT") )
   {
      MvRequest req = origReq_("_CONTEXT");
      if ( (const char*)req("_ORIGINAL_REQUEST") )
         viewReq = req("_ORIGINAL_REQUEST");
      // else   // use a default ThermoView request
   }
   else
   {
      // Initialize request from the Thermo Data module
      CopyAndRemoveParameters (origReq_,viewReq,"_");
      viewReq.unsetParam("DATA");
   }

   viewReq("_MODULEID") = thermoId;

   // Add title information.
   // It must be positioned before the Data+Visdef set of requests to avoid the
   // following problem in uPlot: if another visdef (MGRAPH or MWIND) is defined
   // after the GribThermo requests then it will not be associated to the GribThermo's
   // output data because the existence of a MTEXT request between them.
   MvRequest titleReq("MTEXT");
   titleReq("TEXT_LINE_1") = this->title().c_str();
   titleReq("TEXT_FONT_SIZE") = 0.5;
   titleReq("_MODULEID") = thermoId;
   titleReq("_SKIP_MACRO") = 1;  // skip macro converter

   // If action is not visualisation related then return the netcdf data.
   // Also, add the visualisation and original requests as hidden parameters.
   // These may be used later to help the visualisation of the netcdf data.
   if ( !this->isVisualise() )
   {
      out = ncDataReq;
      out("_VIEW") = "THERMOVIEW";
      out("_VISUALISE") = titleReq + dataReq;
      out("_VIEW_REQUEST") = viewReq;
      out("_MODULEID") = thermoId;

      return true;
   }

   // Final output request
   out = viewReq + titleReq + dataReq;

   return true;
}

int BufrThermo::evaluateNLevels(MvObsSetIterator& iter)
{
   MvObs obs;
   int nn;
   int nlev = -1;
   while ( obs = iter() )
   {
      nn = obs.numberOfPressureLevels();
      if ( nlev < nn )
         nlev = nn;
   }

   return nlev;
}

bool BufrThermo::getIdent(MvRequest& station, MvObsSetIterator& iter)
{
   long ident = station("IDENT");
   if (ident)
   {
      iter.setWmoStation(ident);
      return true;
   }

   return false;
}

bool BufrThermo::getLocation(MvRequest& station, MvObsSetIterator& iter)
{
   double lat = station("LATITUDE");
   double lon = station("LONGITUDE");
   double eps = 0.0001;   //-- to allow for BUFR rounding differences

   MvLocation loc1(lat+eps,lon-eps);
   MvLocation loc2(lat-eps,lon+eps);

   iter.setArea(loc1,loc2);

   return true;
}

string BufrThermo::title()
{
   // Get first time information from the netcdf file
   MvNetCDF netcdf(this->ncFileName(),'r');
   MvNcVar *time = netcdf.getVariable(sTIME);
   NcDim* dim = time->getDimension(1);
   int size = dim->size();
   vector <char> vtime;
   time->setCurrent(0L);
   time->get(vtime,1,size);

   // Decode time info
   string str(vtime.begin(),vtime.end());
   string sdate = str.substr(0,8);
   string stime = str.substr(8,4);

   // Create title
   char  title[200];
   MvNcAtt* tmpatt = netcdf.getAttribute("Coordinates");
   str = tmpatt->as_string(0);
   int ipos = str.find('/');
   float lat = atof(str.substr(0,ipos).c_str());
   float lon = atof(str.substr(ipos+1).c_str());
   sprintf(title,"%s %s [lat,lon: %.2f,%.2f]",
           sdate.c_str(), stime.c_str(), lat, lon );

   return string(title);
}
