/* example how to get filename from fptr: 
   int fstatus = 0;
   char filename[FLEN_FILENAME];
   fits_file_name(fptr, filename, &fstatus);*/

#include "cutout.hpp" // struct fits_card needed
#include "fits_header.hpp"
#include "fix_header.hpp"

#include <stdlib.h> // malloc
#include <string.h> // strcpy
#include <fitsio.h>
#include <stdexcept>

using namespace std;


string fits::header::cfitsio_errmsg(const char * filename, int line_num, int status)
{
   char errmsg[32]; // cfitsio doc says errmsg is "30 chars max"
   errmsg[0] = 0;
   fits_get_errstatus(status, errmsg);
   string msg{"ERR["+to_string(status)+"] " + string{filename} + "." + to_string(line_num) + ":" + string{errmsg}};

   return msg;
}

string fits::header::cfitsio_errmsg(int status)
{
   char errmsg[32]; // cfitsio doc says errmsg is "30 chars max"
   errmsg[0] = 0;
   fits_get_errstatus(status, errmsg);
   string msg{"ERR["+to_string(status)+"] " + string{errmsg}};

   return msg;
}


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


void fits::header::parseStrCards(std::map<std::string, std::string>& strCards)
{
   LOG_trace(__func__);

   map<string,string>::iterator it = strCards.begin();
   while(it != strCards.end())
   {
      string key = it->first;

      char keyvalue[FLEN_VALUE];
      int status = 0;
      int rc = 0;
      if((rc = fits_read_key_str(fptr, key.c_str(), keyvalue, NULL, &status)))
      {
         if((rc == KEY_NO_EXIST) || (rc == VALUE_UNDEFINED))
         {
            status = 0; // reset error, we handle this one
            /*/ LOG_STREAM << "KEYS erase : " << key << " KEY_NO_EXIST or VALUE_UNDEFINED" << endl;*/
            strCards.erase(key);
         }
         else
         {
            LOG_STREAM << cfitsio_errmsg(__FILE__,__LINE__,status) << endl; // FIXME exception ?
         }
      }
      else
      {
         strCards[key] = keyvalue;// LOG_STREAM << "KEYS found : " << key << " : " << strCards[key] << endl;
      }

      it++;
   }
}

void fits::header::parseUIntCards(std::map<std::string, unsigned long>& uintCards)
{
   LOG_trace(__func__);

   map<string, unsigned long>::iterator it = uintCards.begin();
   while(it != uintCards.end())
   {
      string key = it->first;

      unsigned long keyvalue;
      int status = 0;
      int rc = 0;
      if((rc = fits_read_key_lng(fptr, key.c_str(), (long int*)&keyvalue, NULL, &status)))
      {
         if((rc == KEY_NO_EXIST) || (rc == VALUE_UNDEFINED))
         {  
            status = 0; // reset error, we handle this one
            /*/ LOG_STREAM << "KEYS erase : " << key << " KEY_NO_EXIST or VALUE_UNDEFINED" << endl;*/
            uintCards.erase(key);
         }
         else
         {  
            LOG_STREAM << cfitsio_errmsg(__FILE__,__LINE__,status) << endl;// FIXME exception ?
         }
      }
      else
      {  
         uintCards[key] = keyvalue;// LOG_STREAM << "KEYS found : " << key << " : " << uintCards[key] << endl;
      }
      it++;
   }
}

void fits::header::parseDoubleCards(std::map<std::string, double>& doubleCards)
{
   LOG_trace(__func__);

   map<string, double>::iterator it = doubleCards.begin();
   while(it != doubleCards.end())
   {
      string key = it->first;

      double keyvalue;
      int status = 0;
      int rc = 0;
      if((rc = fits_read_key_dbl(fptr, key.c_str(), &keyvalue, NULL, &status)))
      {
         if((rc == KEY_NO_EXIST) || (rc == VALUE_UNDEFINED))
         {
            status = 0; // reset error, we handle this one
            /*/ LOG_STREAM << "KEYS erase : " << key << " KEY_NO_EXIST or VALUE_UNDEFINED" << endl;*/
            doubleCards.erase(key);
         }
         else
         {
            LOG_STREAM << cfitsio_errmsg(__FILE__,__LINE__,status) << endl;            // FIXME exception?
         }
      }
      else
      {
         doubleCards[key] = keyvalue;         // LOG_STREAM << "KEYS found : " << key << " : " << doubleCards[key] << endl;
      }
      it++;
   }
}

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

std::map<std::string, std::string> fits::header::parse_string_cards(std::set<std::string> str_keys)
{
   map<std::string, std::string> key_value_map;

   set<string>::iterator it = str_keys.begin();
   while(it != str_keys.end())
   {
      string key = *it;

      char keyvalue[FLEN_VALUE];
      int status = 0;
      int rc = 0;
      if((rc = fits_read_key_str(fptr, key.c_str(), keyvalue, NULL, &status)))
      {
         if((rc == KEY_NO_EXIST) || (rc == VALUE_UNDEFINED))
         {
            status = 0; // reset expected error, we handle this one
         }
         else
         {
            throw runtime_error("error reading card: " + key); 
         }
      }
      else
      {
         key_value_map.insert(pair<string, string>(key, keyvalue));
      }

      it++;
   }

   return key_value_map;
}

std::map<std::string, unsigned long int> fits::header::parse_uint_cards(std::set<std::string> str_keys)
{
   map<std::string, unsigned long int> key_value_map;

   set<string>::iterator it = str_keys.begin();
   while(it != str_keys.end())
   {
      string key = *it;

      long int keyvalue;
      int status = 0;
      int rc = 0;
      if((rc = fits_read_key_lng(fptr, key.c_str(), (long int*)&keyvalue, NULL, &status)))
      {
         if((rc == KEY_NO_EXIST) || (rc == VALUE_UNDEFINED))
         {
            status = 0; // reset expected error, we handle this one
         }
         else
         {
            throw runtime_error("error reading card: " + key); 
         }
      }
      else
      {
         if(keyvalue < 0) throw invalid_argument("card value must not be negative: " + key + " " + to_string(keyvalue));
         unsigned long int ulong_keyvalue = (unsigned long int)keyvalue;
         key_value_map.insert(pair<string, unsigned long int>(key, ulong_keyvalue));
      }

      it++;
   }

   return key_value_map;
}

std::map<std::string, double> fits::header::parse_double_cards(std::set<std::string> str_keys)
{
   map<std::string, double> key_value_map;

   set<string>::iterator it = str_keys.begin();
   while(it != str_keys.end())
   {
      string key = *it;

      double keyvalue;
      int status = 0;
      int rc = 0;
      if((rc = fits_read_key_dbl(fptr, key.c_str(), &keyvalue, NULL, &status)))
      {
         if((rc == KEY_NO_EXIST) || (rc == VALUE_UNDEFINED))
         {
            status = 0; // reset expected error, we handle this one
         }
         else
         {
            throw runtime_error("error reading card: " + key); 
         }
      }
      else
      {
         key_value_map.insert(pair<string, double>(key, keyvalue));
      }

      it++;
   }

   return key_value_map;
}


fitsfiles::key_values_by_type fits::header::parse_cards(fitsfiles::keys_by_type keys)
{
   return fitsfiles::key_values_by_type{
      parse_string_cards(keys.strKeys),
         parse_uint_cards(keys.uintKeys),
         parse_double_cards(keys.doubleKeys)};
}

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

bool fits::header::contains_card(std::string keyname)
{
   int status = 0;
   char card[FLEN_CARD];
   bool found = false;

   fits_read_card(fptr, keyname.c_str(), card, &status);
   switch(status)
   {
      case 0:            found = true; break;
      case KEY_NO_EXIST: found = false; break;
      default: throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status) + " key: " + keyname);
   }

   return found;
}


string fits::header::read_card(std::string keyname)
{
   int status = 0;
   char card[FLEN_CARD];

   fits_read_card(fptr, keyname.c_str(), card, &status);

   switch(status)
   {
      case 0:
         ; break;
      case KEY_NO_EXIST:
         throw invalid_argument("keyname '" + keyname + "' not found in the given FITS-file's HDU[" + to_string(hdunum) + "]");
      default: 
         throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status) + " keyname: " + keyname);
   }

   return string{card};
}


int fits::header::get_nkeys()
{
   int status = 0;
   int nkeys;
   if(!fits_get_hdrspace(fptr, &nkeys, NULL, &status))
   {
      return nkeys;
   }
   else
   {
      throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status)); 
   }
}


// opens for write pathname[hdunum]
// update() and fix() operate on this opened file
void fits::header::open_fitsfile(std::string pathname, unsigned long hdunum, int iomode)
{
   LOG_trace(__func__);

   int status = 0;
   string header;

   if ( !fits_open_file(&fptr, pathname.c_str(), iomode, &status) )
   {
      if( !fits_movabs_hdu(fptr, hdunum, NULL, &status) )
      {
         this->hdunum = hdunum;
         int hdutype = -1; 
         if( !fits_get_hdu_type(fptr, &hdutype, &status) )
         {
            switch(hdutype)
            {
               case IMAGE_HDU:

                  int nkeys;
                  if(!fits_get_hdrspace(fptr, &nkeys, NULL, &status))
                  {
                     if(nkeys > 0)
                     {
                        ;//this->nkeys = nkeys; // ok
                     }
                     else
                     {
                        fits_close_file(fptr, &status);
                        if(status) LOG_STREAM << "fits_close_file: " << cfitsio_errmsg(__FILE__,__LINE__,status) << endl;
                        throw runtime_error(__func__ + string(" : unexpected empty header in file ") + pathname );
                     }
                  }
                  break;

               default:
                  fits_close_file(fptr, &status);
                  LOG_STREAM << "fits_close_file: " << cfitsio_errmsg(__FILE__,__LINE__,status) << endl;
                  throw runtime_error(__func__
                        + string{" : HDU["}+to_string(hdunum)+"] has incorrect type : "
                        + to_string(hdutype) + " should be IMAGE" );
                  break;
            }
         }
      }
      else
      {
         LOG_STREAM << cfitsio_errmsg(__FILE__,__LINE__,status)
            + " : cannot move to hdunum: " + to_string(hdunum) + " in file " + pathname << endl;
         throw invalid_argument(cfitsio_errmsg(status)
               + " : cannot move to hdunum: " + to_string(hdunum) + " in file " + pathname);
      }
   }
   else
   {
      LOG_STREAM << cfitsio_errmsg(__FILE__,__LINE__,status) + " : " + pathname << endl;
      throw invalid_argument(cfitsio_errmsg(status) + " : " + pathname);
   }

   if(status) throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status) + " : " + pathname);
}



int fits::header::read_record(int keynum, char *card, int *status)
{
   return fits_read_record(fptr, keynum, card, status);
}

string keytype_to_string(int keytype)
{
   string keytype_text;

   // ref: cfitsio manual, fits_parse_template() func description
   switch(keytype)
   {
      case -2: keytype_text = "rename keyword"; break;
      case -1: keytype_text = "delete keyword"; break;
      case  0: keytype_text = "append/update key-record"; break;
      case  1: keytype_text = "append/update HISTORY or COMMENT key-record"; break;
      case  2: keytype_text = "END record (not written explicitely)"; break;
      default: throw runtime_error(string{__FILE__} + ":" + to_string(__LINE__)
                     + ": unknown keytype returned from fits_parse_template");
   }

   return keytype_text;
}

void fits::header::update_card(const struct fits_card new_card)
{
   LOG_trace(__func__);

   int status = 0;

   char errtxt[1024];
   char card[FLEN_CARD];
   card[0] = 0;

   const char * keystr = new_card.key.c_str();

   bool is_card_in_header = ( 0 == fits_read_card(fptr, keystr, card, &status) );

   if (is_card_in_header)
   {
      LOG_STREAM << __func__ << " found card[" << keystr  << "]: " << card << endl;
      return;
   }

   // try to continue even if real error -> fits_update_card will fail
   fits_get_errstatus(status, errtxt);
   LOG_STREAM << __func__ << " fits_read_card status: " << string(errtxt) << endl;
   status = 0; // reset after expected error

   // add card

   const string newcard_template{ new_card.key + " = " + new_card.value + " / " + new_card.comment };

   // reformat the keyword string to conform to FITS rules
   int keytype;
   char buff[1024];
   strcpy(buff, newcard_template.c_str());
   if(!fits_parse_template(buff, card, &keytype, &status))
   {
      LOG_STREAM << __func__ << " adding card[" << keytype  << "] >" << card << "< " << keytype_to_string(keytype) << endl;

      fits_update_card(fptr, keystr, card, &status);
   }

   if(status)
      throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status) + " key: " + new_card.key);
}


void fits::header::update(const std::string card)
{
   LOG_trace(__func__);

   int status = 0;
   int keylength;
   char ccard[FLEN_CARD];
   char keyname[FLEN_KEYWORD];

   strcpy(ccard ,card.c_str());

   if(fits_get_keyname(ccard, keyname, &keylength, &status))
      throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status) + " card: " + card);

   if(fits_update_card(fptr, keyname, ccard, &status))
      throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status) + " card: " + card);
}



void fits::header::update(const vector<fits_card> additional_cards)
{
   LOG_trace(__func__);

   for(fits_card new_card : additional_cards)
   {
      update_card(new_card);
   }
}





string fits::header::get_header(bool apply_fixes)
{
   LOG_trace(__func__);

   string header;

   int status = 0;
   char * header_cstr;
   const int nocomments = 0;
   int nkeys;

   if(fits_hdr2str(fptr, nocomments, NULL, 0, &header_cstr, &nkeys, &status))
   {
      throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status));
   }
   else
   {
      if(header_cstr == NULL)
      {
         throw runtime_error(__func__ + string(" : fits_hdr2str returned NULL header string"));
      }
      else
      {
         LOG_STREAM << "header length: " << strlen(header_cstr) << endl;

         if(apply_fixes)
         {
            /* FIXME verify header modifs:
             * GRS FCRAO:  VELOCITY -> VELO____
             * HI_VGPS:    VELO-LSR -> VELO____
             * and:        M/S      -> m/s
             */
            fix_header(header_cstr);
         }

         header = header_cstr;

         fits_free_memory(header_cstr, &status);
         if(status) LOG_STREAM << cfitsio_errmsg(__FILE__,__LINE__,status) << endl;
      }
   }

   return header;
}




/* FIXME the only data-unit access -> consider renaming  fits_header -> fits_hdu  ? */

double fits::header::calc_nullvals(unsigned long long & null_cnt, unsigned long long & tot_cnt)
{
   LOG_trace(__func__);

   int ii, status = 0;

   /* informative only, not used */
   int equivbitpix = 0;
   if(fits_get_img_equivtype(fptr, &equivbitpix, &status))
   {
      throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status));
   }
   LOG_STREAM << " BITPIX (equivalent): " << equivbitpix << endl;


   /* get BLANK for int-HDU's */

   int bitpix = 0;
   if(fits_get_img_type(fptr, &bitpix, &status))
   {
      throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status));
   }
   LOG_STREAM << " BITPIX: " << bitpix << endl;

   long long blank;
   //bool blank_found = false;
   switch(bitpix)
   {
      case BYTE_IMG:
      case SHORT_IMG:
      case LONG_IMG:
      case LONGLONG_IMG:
         if(fits_read_key(fptr,TLONGLONG,"BLANK", &blank,NULL,&status))
         {
            status = 0;
            //blank_found = true;
            LOG_STREAM << "BLANK not found in header" << endl;
         }
         else
         {
            //blank_found = false;
            LOG_STREAM << "BLANK: " << blank << endl;
         }
         break;
      case FLOAT_IMG:
      case DOUBLE_IMG:
         break;
      default:
         throw runtime_error(string{string(__FILE__) + to_string(__LINE__) + "Unknown BITPIX value" });
   }


   /* get image size */

   int anaxis;
   if(fits_get_img_dim(fptr, &anaxis, &status))
   {
      throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status));
   }

   const int MAXAXES = 5;
   if (anaxis > MAXAXES)
   {
      throw runtime_error(string{string(__FILE__) + to_string(__LINE__)
            + "This build supports max " + to_string(MAXAXES) + " axes, but this HDU has " + to_string(anaxis) });
   }

   long int firstpix[MAXAXES] = {1,1,1,1,1};
   long int anaxes[MAXAXES]   = {1,1,1,1,1};
   if(fits_get_img_size(fptr, MAXAXES, (long int*) anaxes, &status))// FIXME explicit conversion
   {
      throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status));
   }

   LOG_STREAM << "NDIM(" << anaxis << "):";
   for (ii=0;ii<anaxis;ii++) LOG_STREAM << " " << anaxes[ii];
   LOG_STREAM << endl;


   /* alloc buffers for one row */

   double *apix = NULL;   // converted pixels
   char   *anul = NULL;   // true if pixel undefined

   long npixels = anaxes[0];  /* no. of pixels to read in each row */

   /* FIXME review: sizeof(double) matches with TDOUBLE in fits_read_pixnull */

   my_assert(sizeof(double) == 8, __FILE__, __LINE__, "malloc assumes 64bit floating point but sizeof(double) is " + to_string(sizeof(double)));

   apix = (double *) malloc(npixels * sizeof(double)); // mem for 1 row: pixels
   if (apix == NULL)
   {
      throw runtime_error(string{string(__FILE__) + to_string(__LINE__)
            + " failed malloc for one row of pixels" });
   }
   anul = (char *) malloc(npixels * sizeof(char));   // mem for 1 row: nulls
   if (anul == NULL)
   {
      free(apix);
      throw runtime_error(string{string(__FILE__) + to_string(__LINE__)
            + " failed malloc for one row of nulls" });
   }


   /* loop throu each pixel and count undefined values */

   unsigned long long nulcnt = 0; // count undefined pixels
   unsigned long long totcnt = 0; // count all pixels
                                  // 5th dim
   for (firstpix[4] = 1; firstpix[4] <= anaxes[4]; firstpix[4]++)
   {
      // 4th dim
      for (firstpix[3] = 1; firstpix[3] <= anaxes[3]; firstpix[3]++)
      {
         // 3rd dim
         for (firstpix[2] = 1; firstpix[2] <= anaxes[2]; firstpix[2]++)
         {
            /* loop over all rows of the plane */
            for (firstpix[1] = 1; firstpix[1] <= anaxes[1]; firstpix[1]++)
            {
               // Give starting pixel coordinate and no. of pixels to read.
               // nulval = 0.0; // 0 = don't convert, return nan & don't return anynul
               // Other than zero: convert nans to nullval, return anynul set to 0/1

               int anynul = -1;

               // apix: one row of data, npixels long.
               // All values (including BLANK defined NULL values) are scaled by
               // BZERO * rawval*BSCALE

               // RBu somewhere on the net: (ref guide does not explain anynul)
               //  The location of any blank pixels (as determined by the header keywords) is stored
               //  in nullarray (set to 1). Also anynul will be set to 1 to indicate the presence of
               //  blank pixels. If anynul==1 then all pixels are non-blank.
               //  if(anynul==0)... // no blank pixels, so don't bother with any trimming or checking...

               /* TDOUBLE biggest in cfitsio 64 bit */
               if(fits_read_pixnull(fptr, TDOUBLE, firstpix, npixels,
                        apix, anul,
                        &anynul, &status))
               {
                  free(anul);
                  free(apix);   
                  throw runtime_error(cfitsio_errmsg(__FILE__,__LINE__,status));
               }

               for(ii=0; ii< npixels; ii++)
               {
                  totcnt++;
                  if(anul[ii]) nulcnt++;
               }
            }
         }// end 3rd dim
      }// end 4th dim
   }// end 5th dim

   free(anul);
   free(apix);


   /* format result string */

   double nullfill = 100.0*(double)nulcnt/(double)totcnt;

   null_cnt = nulcnt;
   tot_cnt  = totcnt;
   return nullfill;
}


