
#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("SECURE") == 0)
   {
      return "FREE";
   }
   else if(csvPolicy.compare("PROPRIETARY") == 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(string coord_sys, vector<point2d> vert)
{
   my_assert((vert.size()==4), __FILE__,__LINE__, "expected 4 vertices, but found " + to_string(vert.size()) );

   string stcs = 
      "Polygon " + coord_sys + " "
      + 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 to_sql_value(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(), [](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(), [](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)
{
   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
      throw invalid_argument("parse_transition failed");
}


//----------------------------------------------------------------------
// public API: ObsCore row
//----------------------------------------------------------------------
void SqlSchema_INSERT::appendRow(
      const int calib_level,
      const string& obs_collection,
      const string& obs_id,
      const string& obs_publisher_did,

      const string& obs_title,
      const string& bib_reference,
      const string& dataproduct_type,
      const string& o_ucd,

      const string& access_url,
      const string& access_format,
      const uintmax_t access_estsize,

      const fitsfiles::Hdu& hdu,

      const double rest_frequency,
      const string& auth_policy,
      const string& auth_groups)
{
   LOG_trace(__func__);

   // mandatory

   obscoreRow[SqlSchema::obscoreColId::calib_level]      = to_string(calib_level);
   obscoreRow[SqlSchema::obscoreColId::obs_collection]   = to_sqlstring(obs_collection);
   obscoreRow[SqlSchema::obscoreColId::obs_id]           = to_sqlstring(obs_id);
   obscoreRow[SqlSchema::obscoreColId::obs_publisher_id] = to_sqlstring(obs_publisher_did);

   // optional

   obscoreRow[SqlSchema::obscoreColId::obs_title]     = to_sqlstring(obs_title);
   obscoreRow[SqlSchema::obscoreColId::bib_reference] = to_sqlstring(bib_reference);
   obscoreRow[SqlSchema::obscoreColId::data_rights]   = to_sqlstring(auth_policy);
   obscoreRow[SqlSchema::obscoreColId::dataproduct_type] = to_sqlstring(dataproduct_type);
   obscoreRow[SqlSchema::obscoreColId::o_ucd]            = to_sqlstring(o_ucd);

   obscoreRow[SqlSchema::obscoreColId::access_url]     = to_sqlstring(access_url);
   obscoreRow[SqlSchema::obscoreColId::access_format]  = to_sqlstring(access_format);
   obscoreRow[SqlSchema::obscoreColId::access_estsize] = to_string(access_estsize);

   obscoreRow[facility_name]   = to_sql_value(hdu.key_values, "TELESCOP");
   obscoreRow[instrument_name] = to_sql_value(hdu.key_values, "INSTRUME");
   obscoreRow[target_name]     = to_sql_value(hdu.key_values, "OBJECT");

   // sky & spectrum

   const string VELOLSRK{"System=VELO,StdOfRest=LSRK,Unit=km/s"};
   vector<struct Bounds> galBounds  = calc_bounds(hdu.m_header, "GALACTIC", VELOLSRK);
   const string WAVELSRK{"System=WAVE,StdOfRest=LSRK,Unit=m"};
   vector<struct Bounds> icrsBounds = calc_bounds(hdu.m_header, "ICRS", WAVELSRK);
   double ds_ra  = (icrsBounds[0].low + icrsBounds[0].up)/2.0;
   double ds_dec = (icrsBounds[1].low + icrsBounds[1].up)/2.0;
   double ra_len  = fabs(icrsBounds[0].low - icrsBounds[0].up);
   double dec_len = fabs(icrsBounds[1].low - icrsBounds[1].up);
   double ds_fov  = (ra_len > dec_len) ? ra_len : dec_len;
   obscoreRow[s_ra]   = to_string(ds_ra);
   obscoreRow[s_dec]  = to_string(ds_dec);
   obscoreRow[s_fov]  = to_string(ds_fov);
   obscoreRow[s_xel1] = to_sql_value(hdu.key_values, "NAXIS1");
   obscoreRow[s_xel2] = to_sql_value(hdu.key_values, "NAXIS2");
   obscoreRow[s_resolution] = "NULL"; // double FIXME CDELT+CUNIT->arcsec ? which axis?

   vector<point2d> icrsVerts = calc_skyvertices(hdu.m_header, "ICRS");
   reorder_vertices(icrsVerts);
   obscoreRow[s_region]          = to_sqlstring(region_stcs("ICRS", icrsVerts));

   const int EM_PRECISION{10}; // nanometers
   if(icrsBounds.size() >= 3)  // 3D cubes
   {
      obscoreRow[vel_min]      = to_string(EM_PRECISION, galBounds[2].low);// VLKB extension
      obscoreRow[vel_max]      = to_string(EM_PRECISION, galBounds[2].up); // VLKB extension
      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";//to_sql_value(hdu.key_values, "CDELT3");
      obscoreRow[em_xel]       = to_sql_value(hdu.key_values, "NAXIS3");
   }
   else if(icrsBounds.size() == 2) // 2D images
   {
      try
      {
         const double SPEED_OF_LIGHT{299792458};// [m/s]
         double val{SPEED_OF_LIGHT / rest_frequency};
         obscoreRow[em_min] = (rest_frequency==0.0) ? "NULL" : to_string(val); //get_wavelen(EM_PRECISION, hdu.key_values);
      }
      catch(const std::exception&  ex)
      {
         LOG_STREAM << "calc em_min failed: " << ex.what() << endl;
         obscoreRow[em_min] = "NULL";
      }
      obscoreRow[em_max] = obscoreRow[em_min];
      obscoreRow[vel_min]      = "NULL";
      obscoreRow[vel_max]      = "NULL";
      obscoreRow[em_res_power] = "NULL";
      obscoreRow[em_xel]       = "NULL";
   }
   else
   {
      obscoreRow[em_min]       = "NULL";
      obscoreRow[em_max]       = "NULL";
      obscoreRow[vel_min]      = "NULL";
      obscoreRow[vel_max]      = "NULL";
      obscoreRow[em_res_power] = "NULL";
      obscoreRow[em_xel]       = "NULL";
   }

   // time

   /* FIXME review
      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]        = "NULL";//time.t_min_str; //to_string(time.t_min);
   obscoreRow[t_max]        = "NULL";//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] = "NULL";// HiGAL SQL error '-nan'; to_string(time.t_resolution);
   obscoreRow[t_xel]        = "NULL";// HiGAL Mosaic gives SQL error: 'too big integer'; to_string(time.t_xel);

   // polarization

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


   // VLKB extensions

   // GALACTIC coords
   vector<point2d> galVerts  = calc_skyvertices(hdu.m_header, "GALACTIC");
   reorder_vertices(galVerts);
   obscoreRow[s_region_galactic]       = to_sqlstring(region_stcs("GALACTIC", galVerts));
   obscoreRow[polygon_region_galactic] = to_sqlstring(region_spoly(galVerts));
   // pgSphere-types
   obscoreRow[polygon_region] = to_sqlstring(region_spoly(icrsVerts));
   obscoreRow[coordinates]    = to_sqlstring("(" + obscoreRow[s_ra] + "," + obscoreRow[s_dec] + ")");
   // authorization
   obscoreRow[policy]               = to_sqlstring(authPolicyToSQLEnum(auth_policy)) + "::auth_policy";
   obscoreRow[obscoreColId::groups] = to_sqlstring("{" + auth_groups + "}");
   // ?
   obscoreRow[proposal_id] = "NULL";

   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;
}

