
#include "SqlSchema_INSERT.hpp"

#include <iomanip>
#include <sstream> // to_string(int precision, double value)
#include <string>
#include <stdexcept>
#include <math.h> // fabs needed

#include <algorithm>
#include <vector>

#include "dataset_id.hpp"
#include "ObsCoreTime.hpp"
#include "io.hpp"
#include "ast4vl.hpp"
#include "my_assert.hpp"



using namespace std;


//----------------------------------------------------------------
// utils
//----------------------------------------------------------------

string to_string(int precision, double value)
{
   stringstream stream;
   stream << fixed << setprecision(precision) << value;
   return stream.str();
}


string authPolicyToSQLEnum(string csvPolicy)
{
   if((csvPolicy.compare("FREE") == 0) ||
      (csvPolicy.compare("PRIV") == 0)) return csvPolicy;


   if(csvPolicy.compare("PUBLIC") == 0)
   {
      return "FREE";
   }
   else if(csvPolicy.compare("PRIVATE") == 0)
   {
      return "PRIV";
   }
   else
   {
      LOG_STREAM << "authPolicyToSQLEnum: unrecoginzed csv-Policy: " + csvPolicy << endl;
      throw std::invalid_argument( "unrecognized auth-Policy string: " + csvPolicy );
   }
}

/* order vertices for polygon */

bool comp_vertex(point2d a, point2d b)
{
   return ( atan2(a.lat, a.lon) < atan2(b.lat, b.lon) );
}

vector<point2d> to_point2dvec(int len, double * vertlon, double * vertlat)
{
   vector<point2d> vertex;
   int ii;
   for(ii=0; ii<len; ii++) {vertex.push_back(point2d{vertlon[ii], vertlat[ii]});}
   return vertex;
}

/* re-orders points in lon,lat arrays to form a convex polygon (no lines cross) */
/* FIXME this re-ordering is not needed if: 
 * algorithm to generate vertices in 'ast::frameset::sky_vertices(void)' would
 * generate them in ordered for polygons ; 
 * it is not expected that projection changes the angular order of vertices */
void reorder_vertices(vector<point2d>& vertex)
{
   /* calc center */

   double clon=0.0, clat=0.0;
   for(point2d vert : vertex)
   {
      clon += vert.lon;
      clat += vert.lat;
   }
   double count = (double)vertex.size();
   clon /= count;
   clat /= count;

   /* shif vertices to coord-system at center, sort, and shift back */
   
   int ii;
   for(ii=0; ii<count; ii++) {vertex[ii].lon -= clon; vertex[ii].lat -= clat;}

   sort(vertex.begin(), vertex.end(), comp_vertex);

   for(ii=0; ii<count; ii++) {vertex[ii].lon += clon; vertex[ii].lat += clat;}
   
   return;
}

string region_spoly(vector<point2d> vert)
{
   my_assert((vert.size()==4), __FILE__,__LINE__, "expected 4 vertices, but found " + to_string(vert.size()) );

   string spoly = 
      "{ " 
      "(" + to_string(vert[0].lon) + "d," + to_string(vert[0].lat) + "d),"
      "(" + to_string(vert[1].lon) + "d," + to_string(vert[1].lat) + "d),"
      "(" + to_string(vert[2].lon) + "d," + to_string(vert[2].lat) + "d),"
      "(" + to_string(vert[3].lon) + "d," + to_string(vert[3].lat) + "d)"
      " }";

   LOG_STREAM << "spoly: " << spoly << endl;

   return spoly;
}

string region_stcs(vector<point2d> vert)
{
   my_assert((vert.size()==4), __FILE__,__LINE__, "expected 4 vertices, but found " + to_string(vert.size()) );

   string stcs = 
      "Polygon ICRS " 
      + to_string(vert[0].lon) + " " + to_string(vert[0].lat) + " " 
      + to_string(vert[1].lon) + " " + to_string(vert[1].lat) + " " 
      + to_string(vert[2].lon) + " " + to_string(vert[2].lat) + " " 
      + to_string(vert[3].lon) + " " + to_string(vert[3].lat) + " " 
      " unit deg";

   LOG_STREAM << "stcs:" << stcs << endl;

   return stcs;
}

string to_sqlstring(string str) {return "\'" + str + "\'";}

// FIXME replace this
string asSqlString(fitsfiles::key_values_by_type key_values, string key)
{
   if(key_values.strValues.count(key)>0)
   {
      return "\'" + key_values.strValues[key] + "\'";
   }
   else if(key_values.uintValues.count(key)>0)
   {
      return to_string(key_values.uintValues[key]);
   }
   else if(key_values.doubleValues.count(key)>0)
   {
      return to_string(key_values.doubleValues[key]);
   }
   else
   {
      return "NULL";
   }
}


string get_wavelen(int precision, fitsfiles::key_values_by_type key_values)
{
   const double UNIT_CONVERT = 0.001; // FIXME header-card unit [mm] but ObsCore em_ is [m]
   if(key_values.doubleValues.find("WAVELEN") == key_values.doubleValues.end() )
   {
      if(key_values.doubleValues.find("WAVELENG") == key_values.doubleValues.end() )
      {
         if(key_values.doubleValues.find("WAVELNTH") == key_values.doubleValues.end() )
         {
            return "NULL";
         }
         else
         {
            return to_string(precision, UNIT_CONVERT * key_values.doubleValues["WAVELNTH"]);
         }
      }
      else
      {
         return to_string(precision, UNIT_CONVERT * key_values.doubleValues["WAVELENG"]);
      }
   }
   else
   {
      return to_string(precision, UNIT_CONVERT * key_values.doubleValues["WAVELEN"]);
   }
}


// trim from start (in place)
inline void ltrim(std::string &s) {
    s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
        return !std::isspace(ch);
    }));
}

// trim from end (in place)
inline void rtrim(std::string &s) {
    s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
        return !std::isspace(ch);
    }).base(), s.end());
}

// trim from both ends (in place)
inline void trim(std::string &s) {
    rtrim(s);
    ltrim(s);
}


// returns band in wavelen [m]
double parse_transition(string trans)
{
   // Surveys::survTransition "num unit" <-- for images

   trim(trans);

   if(!trans.empty())
   {
      string::size_type pos = trans.find(' ');
      std::string num  = trans.substr(0, pos);
      std::string unit = trans.substr(pos+1);

      LOG_STREAM << "parse trans: " << num << " " << unit << endl;

      const double speed_of_light = 299792458.0; // [m/s] in vacuum

      double value = stod(num);
      if(unit.compare("um") == 0) return value * 1e-6;
      else if(unit.compare("mm") == 0) return value * 1e-3;
      else if(unit.compare("m") == 0) return value;
      else if(unit.compare("GHz") == 0) return (speed_of_light / (value * 1e9));
      else if(unit.compare("MHz") == 0) return (speed_of_light / (value * 1e6));
      else if(unit.compare("kHz") == 0) return (speed_of_light / (value * 1e3));
      else if(unit.compare("Hz") == 0) return (speed_of_light / (value));
      else
         return 0.0; // ignore
   }
   else
      return 0.0;
}


//----------------------------------------------------------------------
// public API
//----------------------------------------------------------------------
void SqlSchema_INSERT::appendRow(/*const int hid, const int sid,*/
      const string& obscore_publisher,
      const string& obscore_access_format,
      const string& obscore_access_url,
      const Survey& surv, const string& authGroups,
      /*const*/ fitsfiles::Hdu& hdu,
      //n  ObsCoreKeys ocKeys,
      const std::string& filename, const uintmax_t filesize)
{
   LOG_trace(__func__);

   // header based computations

#if 0
   const int specSystem = 1;// LSRK
   vector<struct Bounds> galBounds = 
      legacy::calcBounds(hdu.m_header, "GALACTIC", specSystem);
   vector<struct Bounds> icrsBounds = 
      legacy::calcBounds(hdu.m_header, "ICRS", specSystem);
   struct Vertices galVerts  = legacy::calcVertices(hdu.m_header, "GALACTIC");
   struct Vertices icrsVerts = legacy::calcVertices(hdu.m_header, "ICRS");
#else
   const string VELOLSRK{"System=VELO,StdOfRest=LSRK,Unit=km/s"};
   const string WAVELSRK{"System=WAVE,StdOfRest=LSRK,Unit=m"};
   // FIXME StdOfRest <--- SPECSYS

   vector<struct Bounds> galBounds  = calc_bounds(hdu.m_header, "GALACTIC", VELOLSRK);
   vector<struct Bounds> icrsBounds = calc_bounds(hdu.m_header, "ICRS", WAVELSRK);

   vector<point2d> galVerts  = calc_skyvertices(hdu.m_header, "GALACTIC");
   vector<point2d> icrsVerts = calc_skyvertices(hdu.m_header, "ICRS");

   reorder_vertices(galVerts);
   reorder_vertices(icrsVerts);
#endif

   // construct publisherDID

   bool scramble = false;
   const std::string pubdid(dataset_id::create(surv.storagePath,filename,hdu.m_hdunum, scramble));

   ////////////////////////////////////////////////////////////////////////

   // set table values
   /*
      headersRow[headersColId::header_id] = to_string(hid);
      headersRow[headersColId::filename] = to_sqlstring(filename);
      headersRow[headersColId::hdunum]   = to_string(hdu.m_hdunum);
      headersRow[headersColId::pubdid]   = to_sqlstring(pubdid);
      headersRow[headersColId::header]   = "$$" + hdu.m_header + "$$";
      headersRow[headersColId::survey_id] = to_string(sid);

   // cubebounds - GALACTIC

   boundsgalRow[bgheader_id] = to_string(hid);
   boundsgalRow[lfrom] = to_string(galBounds[0].low);//.x_from);
   boundsgalRow[lto]   = to_string(galBounds[0].up);//.x_to);
   boundsgalRow[lunit] = to_sqlstring(galBounds[0].unit);//x_unit);
   boundsgalRow[bfrom] = to_string(galBounds[1].low);//y_from);
   boundsgalRow[bto]   = to_string(galBounds[1].up);//y_to);
   boundsgalRow[bunit] = to_sqlstring(galBounds[1].unit);//y_unit);
   if(galBounds.size() >2)
   {
   boundsgalRow[vfrom] = to_string(galBounds[2].low);//vel_from);
   boundsgalRow[vto]   = to_string(galBounds[2].up);//vel_to);
   boundsgalRow[vunit] = to_sqlstring(galBounds[2].unit);//vel_unit);
   }
   else
   {
   boundsgalRow[vfrom] = "NULL";
   boundsgalRow[vto]   = "NULL";
   boundsgalRow[vunit] = "NULL";
   }

   // cubebounds - ICRS

   boundsicrsRow[biheader_id] = to_string(hid);
   boundsicrsRow[rfrom] = to_string(icrsBounds[0].low);//x_from);
   boundsicrsRow[rto]   = to_string(icrsBounds[0].up);//x_to);
   boundsicrsRow[runit] = to_sqlstring(icrsBounds[0].unit);//x_unit);
   boundsicrsRow[dfrom] = to_string(icrsBounds[1].low);//y_from);
   boundsicrsRow[dto]   = to_string(icrsBounds[1].up);//y_to);
   boundsicrsRow[dunit] = to_sqlstring(icrsBounds[1].unit);//y_unit);

   // vertices - GALACTIC

   verticesgalRow[vgheader_id] = to_string(hid);
   verticesgalRow[p1lon] = to_string(galVerts[0].lon);
   verticesgalRow[p1lat] = to_string(galVerts[0].lat);
   verticesgalRow[p2lon] = to_string(galVerts[1].lon);
   verticesgalRow[p2lat] = to_string(galVerts[1].lat);
   verticesgalRow[p3lon] = to_string(galVerts[2].lon);
   verticesgalRow[p3lat] = to_string(galVerts[2].lat);
   verticesgalRow[p4lon] = to_string(galVerts[3].lon);
   verticesgalRow[p4lat] = to_string(galVerts[3].lat);


   // vertices - ICRS

   verticesicrsRow[viheader_id] = to_string(hid);
   verticesicrsRow[p1ra]  = to_string(icrsVerts[0].lon);
   verticesicrsRow[p1dec] = to_string(icrsVerts[0].lat);
   verticesicrsRow[p2ra]  = to_string(icrsVerts[1].lon);
   verticesicrsRow[p2dec] = to_string(icrsVerts[1].lat);
   verticesicrsRow[p3ra]  = to_string(icrsVerts[2].lon);
   verticesicrsRow[p3dec] = to_string(icrsVerts[2].lat);
   verticesicrsRow[p4ra]  = to_string(icrsVerts[3].lon);
   verticesicrsRow[p4dec] = to_string(icrsVerts[3].lat);
   */

   // obscore - values from Surveys table

   string keyFacility      = surv.fitskeyFacilityName;
   string keyInstrument    = surv.fitskeyInstrumentName;

   obscoreRow[dataproduct_type] = to_sqlstring(surv.dataproductType);
   obscoreRow[calib_level]      = to_string(surv.calibLevel);
   obscoreRow[obs_collection]   = to_sqlstring(surv.getObsCollection());
   obscoreRow[o_ucd]            = to_sqlstring(surv.oUcd);
   obscoreRow[policy]           = to_sqlstring(authPolicyToSQLEnum(surv.authPolicy)) + "::auth_policy";


   // obscore - values derived from filename

   obscoreRow[obs_id]           = to_sqlstring(filename.substr(0, filename.find_last_of(".")));
   obscoreRow[obs_publisher_id] = obscore_publisher.empty() ? to_sqlstring(pubdid) : to_sqlstring(obscore_publisher + "?" + pubdid);
   obscoreRow[access_url]       = obscore_access_url.empty() ? "" : to_sqlstring(
         obscore_access_url + "/" + surv.storagePath + "/" + filename);
   obscoreRow[access_format]    = to_sqlstring(obscore_access_format);
   obscoreRow[access_estsize]   = to_string(filesize/1024); // [KB]


   // obscore - values taken from header cards

   obscoreRow[facility_name]    = asSqlString(hdu.key_values, keyFacility);
   obscoreRow[instrument_name]  = asSqlString(hdu.key_values, keyInstrument);
   obscoreRow[target_name]      = asSqlString(hdu.key_values, "OBJECT");
   obscoreRow[s_xel1]           = asSqlString(hdu.key_values, "NAXIS1");
   obscoreRow[s_xel2]           = asSqlString(hdu.key_values, "NAXIS2");
   obscoreRow[s_resolution]     = "NULL"; // double FIXME CDELT+CUNIT->arcsec ? which axis?


   // obscore - vals computed from header

#define max(a,b) (a>b ? a : b)
   /*   double ds_ra  = (icrsBounds.x_from + icrsBounds.x_to)/2.0;
        double ds_dec = (icrsBounds.y_from + icrsBounds.y_to)/2.0;
        double ds_fov = max( fabs(icrsBounds.x_from - icrsBounds.x_to),
        fabs(icrsBounds.y_from - icrsBounds.y_to) );
        */
   double ds_ra  = (icrsBounds[0].low + icrsBounds[0].up)/2.0;
   double ds_dec = (icrsBounds[1].low + icrsBounds[1].up)/2.0;
   double ds_fov = max( fabs(icrsBounds[0].low - icrsBounds[0].up),
         fabs(icrsBounds[1].low - icrsBounds[1].up) );


   obscoreRow[s_ra]  = to_string(ds_ra);
   obscoreRow[s_dec] = to_string(ds_dec);
   obscoreRow[s_fov] = to_string(ds_fov);

   obscoreRow[s_region] = to_sqlstring(region_stcs(icrsVerts));

   // polygon_region & coordinates are IA2-extensions
   obscoreRow[polygon_region_galactic] = to_sqlstring(region_spoly(galVerts));
   obscoreRow[polygon_region] = to_sqlstring(region_spoly(icrsVerts));
   obscoreRow[coordinates]    = to_sqlstring("(" + obscoreRow[s_ra] + "," + obscoreRow[s_dec] + ")");


   // obscore - spectral axis

   const int EM_PRECISION{10}; // nanometers

   if(icrsBounds.size() >= 3) // 3D cubes
   {
      //obscoreRow[em_min]        = to_string(galBounds[2].low);
      //obscoreRow[em_max]        = to_string(galBounds[2].up);
      obscoreRow[em_min]        = to_string(EM_PRECISION, icrsBounds[2].low);
      obscoreRow[em_max]        = to_string(EM_PRECISION, icrsBounds[2].up);

      obscoreRow[em_res_power]  = "NULL";//asSqlString(hdu.key_values, "CDELT3");
      obscoreRow[em_xel]        = asSqlString(hdu.key_values, "NAXIS3");
   }
   else if(icrsBounds.size() == 2) // 2D images
   {
      // surv.restFrequency [Hz] <-- is empty for 2D images: take from survTransition ?
      // surv.survTransition "num unit" <-- for images

      double val = parse_transition(surv.survTransition);
      obscoreRow[em_min] = ((val==0.0) ? "NULL" : to_string(val)); //get_wavelen(EM_PRECISION, hdu.key_values);
      obscoreRow[em_max] = obscoreRow[em_min];
      obscoreRow[em_res_power] = "NULL";
      obscoreRow[em_xel]       = "NULL";
   }
   else
   {
      obscoreRow[em_min]       = "NULL";
      obscoreRow[em_max]       = "NULL";
      obscoreRow[em_res_power] = "NULL";
      obscoreRow[em_xel]       = "NULL";
   }


   // obscore - time

   string DATEOBS;
   string DATEEND;

   if(hdu.key_values.strValues["DATE-OBS"].size() > 0) DATEOBS = hdu.key_values.strValues["DATE-OBS"];
   if(hdu.key_values.strValues["DATE-END"].size() > 0) DATEEND = hdu.key_values.strValues["DATE-END"];

   obscore::ObsCoreTime time =
      obscore::calcObsCoreTime(hdu.key_values.strValues["DATE-OBS"], hdu.key_values.strValues["DATE-END"]);

   obscoreRow[t_min]        = time.t_min_str; //to_string(time.t_min);
   obscoreRow[t_max]        = time.t_max_str; //to_string(time.t_max);
   obscoreRow[t_exptime]    = "NULL";// FIXME MALT gives 'nan' to_string(time.t_exptime);
   obscoreRow[t_resolution] = to_string(time.t_resolution);
   obscoreRow[t_xel]        = to_string(time.t_xel);


   // obscore - stokes params

   obscoreRow[pol_states] = "NULL";
   obscoreRow[pol_xel]    = "NULL";


   // obscore - misc

   obscoreRow[proposal_id] = "NULL";
   obscoreRow[obscoreColId::groups] = to_sqlstring("{" + authGroups + "}");

   // end: all data set


   SqlSchema::appendRow();
}




vector<string> SqlSchema_INSERT::getINSERT(void)
{
   vector<string> vCmds{
      /*    {"INSERT INTO headers " + m_columns[headers]        + " VALUES "             + skl(m_values[headers])},
            {"INSERT INTO cubeboundsgalactic " + m_columns[boundsgal]      + " VALUES "  + skl(m_values[boundsgal])},
            {"INSERT INTO cubeboundsicrs " + m_columns[boundsicrs]     + " VALUES "      + skl(m_values[boundsicrs])},
            {"INSERT INTO verticesgalactic " + m_columns[verticesgal]    + " VALUES "    + skl(m_values[verticesgal])},
            {"INSERT INTO verticesicrs " + m_columns[verticesicrs]   + " VALUES "        + skl(m_values[verticesicrs])},
            */	 
      {"INSERT INTO obscore " + m_columns[obscore]        + " VALUES "             + skl(m_values[obscore])},
   };
   reset();
   return vCmds;
}

