#include <algorithm> // remove_if
#include <string.h> 
#include <stdlib.h>
#include <math.h> 
#include <ctype.h>
#include <errno.h>
#include <stddef.h>  /* apparently needed to define size_t */
#include <stdexcept>
#include "fitsio2.h"
//#include "group.h"

#include "service_string.hpp" // to_cftsio_format() needed$
#include "ast.hpp"
#include "fitsfiles.hpp"
#include "imcopy.hpp"
#include "io.hpp"

#include "fitsio.h"

using namespace std;



int stream_cutout(string pathname, int extnum, string region)
{
	vector<uint_bounds> bnds;

	int ov_code = vlkb_overlap(pathname, region, bnds);

   if((ov_code >= 2) && (ov_code <= 5))
	{
		string pixfilter{ to_cfitsio_format(bnds) };

		imcopy(pathname, extnum, pixfilter);
      // pixfilter must be within NAXIS[] otherwise cfitsio-error
      return 0; // EXIT_SUCCESS
	}
	else if((ov_code == 1) || (ov_code == 6))
	{
		return 1; // no overlap EXIT_SUCCESS
	}
   else
   {
      throw runtime_error("overlap code invalid: " + to_string(ov_code));
   }
}


// ----------------------------------------------------------------------------------

int fits_copy_image_section2(
		fitsfile *fptr,  /* I - pointer to input image */
		fitsfile *newptr,  /* I - pointer to output image */
		char *expr,       /* I - Image section expression    */
		int *status);




void imcopy(std::string filename, int extnum, std::string pixfilter)
{
	LOG_trace(__func__);

	LOG_STREAM << filename << " EXT: " << extnum << endl;
	LOG_STREAM << "pixfilter: " << pixfilter << endl;

	int status = 0;

	pixfilter.erase( remove(pixfilter.begin(), pixfilter.end(), '['), pixfilter.end() );
	pixfilter.erase( remove(pixfilter.begin(), pixfilter.end(), ']'), pixfilter.end() );

	LOG_STREAM << "filter expr: " << pixfilter << endl;

	char *expr = (char*)pixfilter.c_str(); /* I - Image section expression */

	fitsfile *fptr;    /* I - pointer to input image */
	fitsfile *newfptr; /* I - pointer to output image */

	const char * cfilename = filename.c_str();

	fits_open_diskfile(&fptr, cfilename, READONLY, &status);
	if (status)
	{
		string errmsg{ fitsfiles::cfitsio_errmsg(__FILE__, __LINE__, status) };
		throw runtime_error("fits_open_file " + filename + " failed: " + errmsg);
	}

	/* cfitsio(v4.2.0) manual: 10.2.3 Notes about the stream filetype driver
	 *  "-" writes to stdout: but first loads all file into mem
	 *  "stream://" keeps NIOBUF=40 buffers, and header must fit into it */
	fits_create_file(&newfptr, "stream://", &status);
	if (status)
	{
		string errmsg{ fitsfiles::cfitsio_errmsg(__FILE__, __LINE__, status) };

		int tstatus = 0;
		fits_close_file(fptr, &tstatus);
		if(tstatus) LOG_STREAM << fitsfiles::cfitsio_errmsg(__FILE__, __LINE__, tstatus) << tstatus<< endl;

		throw runtime_error("fits_open_file to stream failed: " + errmsg);
	}

	/* err codes 1..1000 reserved by cfitsio */
#define HEADER_TOO_LONG (10000)
#define MAX_CARD_COUNT (1400)
	/* see notes about stream driver in cfitsio manual */
	fits_copy_image_section2(fptr, newfptr, expr, &status);
	if (status)
	{
		int tstatus = 0;
		fits_close_file(fptr, &tstatus);
		if(tstatus) LOG_STREAM << fitsfiles::cfitsio_errmsg(__FILE__, __LINE__, tstatus) << tstatus<< endl;
		tstatus = 0;
		fits_close_file(newfptr, &tstatus);
		if(tstatus) LOG_STREAM << fitsfiles::cfitsio_errmsg(__FILE__, __LINE__, tstatus) << tstatus<< endl;

		if(status == HEADER_TOO_LONG)
		{
			throw runtime_error("Cutout from " + filename
					+ " failed: header is too long. Direct streaming in current build support max "
					+ to_string(MAX_CARD_COUNT) + " cards in header.");
		}
		else
		{
			string errmsg{fitsfiles::cfitsio_errmsg(__FILE__, __LINE__, status)};
			throw runtime_error("fits_copy_image_section " + filename
					+ " to cut-file with " + string{expr} + " failed: " + errmsg);
		}
	}

	fits_close_file(fptr, &status);
	if (status)
	{
		string errmsg{fitsfiles::cfitsio_errmsg(__FILE__, __LINE__, status)};

		int tstatus = 0;
		fits_close_file(newfptr, &tstatus);
		if(tstatus) LOG_STREAM << fitsfiles::cfitsio_errmsg(__FILE__, __LINE__, tstatus) << tstatus<< endl;

		throw runtime_error("fits_close_file " + filename + " failed: " + errmsg);
	}

	fits_close_file(newfptr, &status);
	if (status)
	{
		string errmsg{fitsfiles::cfitsio_errmsg(__FILE__, __LINE__, status)};
		throw runtime_error("fits_close_file cut failed: " + errmsg);
	}
}


/* ---- version 3.49 ------- */
/* Currently this is a direct copy from cfitsio-lib.
 * Ensures read-by-rows on systems (CentOS7) with old cfitsio lib.
 * Also consider to duplicate the buffer and run reader
 * on parallel-thread to avoid reader-write wait for each other. */
int fits_copy_image_section2(
		fitsfile *fptr,  /* I - pointer to input image */
		fitsfile *newptr,  /* I - pointer to output image */
		char *expr,       /* I - Image section expression    */
		int *status)
{
	/* copies an image section from the input file to a new output HDU */

	int bitpix, naxis, numkeys, nkey;
	long naxes[] = {1,1,1,1,1,1,1,1,1}, smin, smax, sinc;
	long fpixels[] = {1,1,1,1,1,1,1,1,1};
	long lpixels[] = {1,1,1,1,1,1,1,1,1};
	long incs[] = {1,1,1,1,1,1,1,1,1};
	char *cptr, keyname[FLEN_KEYWORD], card[FLEN_CARD];
	int ii, tstatus, anynull;
	long minrow, maxrow, minslice, maxslice, mincube, maxcube;
	long firstpix;
	long ncubeiter, nsliceiter, nrowiter, kiter, jiter, iiter;
	int klen, kk, jj;
	long outnaxes[9], outsize, buffsize;
	double *buffer, crpix, cdelt;

	if (*status > 0)
		return(*status);

	/* get the size of the input image */
	fits_get_img_type(fptr, &bitpix, status);
	fits_get_img_dim(fptr, &naxis, status);
	if (fits_get_img_size(fptr, naxis, naxes, status) > 0)
		return(*status);

	if (naxis < 1 || naxis > 4)
	{
		ffpmsg(
				"Input image either had NAXIS = 0 (NULL image) or has > 4 dimensions");
		return(*status = BAD_NAXIS);
	}

	/* create output image with same size and type as the input image */
	/*  Will update the size later */
	fits_create_img(newptr, bitpix, naxis, naxes, status);

	/* copy all other non-structural keywords from the input to output file */
	fits_get_hdrspace(fptr, &numkeys, NULL, status);

	int cards_written = 4;
	for (nkey = 4; nkey <= numkeys; nkey++) /* skip the first few keywords */
	{
		fits_read_record(fptr, nkey, card, status);

		/* cfitsio(v4.2.0) manual: 10.2.3 Notes about the stream filetype driver
		 * stream:// driver has internal buffer NIOBUF=40 blocks e.g. 1400 cards
		 * TYP_COMM_KEY: skip HISTORY COMMENT and empty keys */
		if(numkeys > MAX_CARD_COUNT)
			if (fits_get_keyclass(card) == TYP_COMM_KEY) continue;

		if (fits_get_keyclass(card) > TYP_CMPRS_KEY)
		{
			if(++cards_written > MAX_CARD_COUNT) return(HEADER_TOO_LONG);

			/* write the record to the output file */
			fits_write_record(newptr, card, status);
		}
	}

	if (*status > 0)
	{
		ffpmsg("error copying header from input image to output image");
		return(*status);
	}

	/* parse the section specifier to get min, max, and inc for each axis */
	/* and the size of each output image axis */

	cptr = expr;
	for (ii=0; ii < naxis; ii++)
	{
		if (fits_get_section_range(&cptr, &smin, &smax, &sinc, status) > 0)
		{
			ffpmsg("error parsing the following image section specifier:");
			ffpmsg(expr);
			return(*status);
		}

		if (smax == 0)
			smax = naxes[ii];   /* use whole axis  by default */
		else if (smin == 0)
			smin = naxes[ii];   /* use inverted whole axis */

		if (smin > naxes[ii] || smax > naxes[ii])
		{
			ffpmsg("image section exceeds dimensions of input image:");
			ffpmsg(expr);
			return(*status = BAD_NAXIS);
		}

		fpixels[ii] = smin;
		lpixels[ii] = smax;
		incs[ii] = sinc;

		if (smin <= smax)
			outnaxes[ii] = (smax - smin + sinc) / sinc;
		else
			outnaxes[ii] = (smin - smax + sinc) / sinc;

		/* modify the NAXISn keyword */
		fits_make_keyn("NAXIS", ii + 1, keyname, status);
		fits_modify_key_lng(newptr, keyname, outnaxes[ii], NULL, status);

		/* modify the WCS keywords if necessary */

		if (fpixels[ii] != 1 || incs[ii] != 1)
		{
			for (kk=-1;kk<26; kk++)  /* modify any alternate WCS keywords */
			{
				/* read the CRPIXn keyword if it exists in the input file */
				fits_make_keyn("CRPIX", ii + 1, keyname, status);

				if (kk != -1) {
					klen = (int)strlen(keyname);
					keyname[klen]= (char)((int)'A' + kk);
					keyname[klen + 1] = '\0';
				}

				tstatus = 0;
				if (fits_read_key(fptr, TDOUBLE, keyname,
							&crpix, NULL, &tstatus) == 0)
				{
					/* calculate the new CRPIXn value */
					if (fpixels[ii] <= lpixels[ii]) {
						crpix = (crpix - double(fpixels[ii])) / double(incs[ii]) + 1.0;
						/*  crpix = (crpix - (fpixels[ii] - 1.0) - .5) / incs[ii] + 0.5; */
					} else {
						crpix = (double(fpixels[ii]) - crpix)  / double(incs[ii]) + 1.0;
						/* crpix = (fpixels[ii] - (crpix - 1.0) - .5) / incs[ii] + 0.5; */
					}

					/* modify the value in the output file */
					fits_modify_key_dbl(newptr, keyname, crpix, 15, NULL, status);

					if (incs[ii] != 1 || fpixels[ii] > lpixels[ii])
					{
						/* read the CDELTn keyword if it exists in the input file */
						fits_make_keyn("CDELT", ii + 1, keyname, status);

						if (kk != -1) {
							klen = (int)strlen(keyname);
							keyname[klen]=(char)((int)'A' + kk);
							keyname[klen + 1] = '\0';
						}

						tstatus = 0;
						if (fits_read_key(fptr, TDOUBLE, keyname,
									&cdelt, NULL, &tstatus) == 0)
						{
							/* calculate the new CDELTn value */
							if (fpixels[ii] <= lpixels[ii])
								cdelt = cdelt * double(incs[ii]);
							else
								cdelt = cdelt * double(-incs[ii]);

							/* modify the value in the output file */
							fits_modify_key_dbl(newptr, keyname, cdelt, 15, NULL, status);
						}

						/* modify the CDi_j keywords if they exist in the input file */

						fits_make_keyn("CD1_", ii + 1, keyname, status);

						if (kk != -1) {
							klen = (int)strlen(keyname);
							keyname[klen]=(char)((int)'A' + kk);
							keyname[klen + 1] = '\0';
						}

						for (jj=0; jj < 9; jj++)   /* look for up to 9 dimensions */
						{
							keyname[2] = (char)((int)'1' + jj);

							tstatus = 0;
							if (fits_read_key(fptr, TDOUBLE, keyname,
										&cdelt, NULL, &tstatus) == 0)
							{
								/* calculate the new CDi_j value */
								if (fpixels[ii] <= lpixels[ii])
									cdelt = cdelt * double(incs[ii]);
								else
									cdelt = cdelt * double(-incs[ii]);

								/* modify the value in the output file */
								fits_modify_key_dbl(newptr, keyname, cdelt, 15, NULL, status);
							}
						}

					} /* end of if (incs[ii]... loop */
				}   /* end of fits_read_key loop */
			}    /* end of for (kk  loop */
		}
	}  /* end of main NAXIS loop */

	if (ffrdef(newptr, status) > 0)  /* force the header to be scanned */
	{
		return(*status);
	}

	/* turn off any scaling of the pixel values */
	fits_set_bscale(fptr,  1.0, 0.0, status);
	fits_set_bscale(newptr, 1.0, 0.0, status);

	/* to reduce memory foot print, just read/write image 1 row at a time */

	outsize = outnaxes[0];
	buffsize = (abs(bitpix) / 8) * outsize;

	buffer = (double *) malloc(buffsize); /* allocate memory for the image row */
	if (!buffer)
	{
		ffpmsg("fits_copy_image_section: no memory for image section");
		return(*status = MEMORY_ALLOCATION);
	}
	/* read the image section then write it to the output file */

	minrow = fpixels[1];
	maxrow = lpixels[1];
	if (minrow > maxrow) {
		nrowiter = (minrow - maxrow + incs[1]) / incs[1];
	} else {
		nrowiter = (maxrow - minrow + incs[1]) / incs[1];
	}

	minslice = fpixels[2];
	maxslice = lpixels[2];
	if (minslice > maxslice) {
		nsliceiter = (minslice - maxslice + incs[2]) / incs[2];
	} else {
		nsliceiter = (maxslice - minslice + incs[2]) / incs[2];
	}

	mincube = fpixels[3];
	maxcube = lpixels[3];
	if (mincube > maxcube) {
		ncubeiter = (mincube - maxcube + incs[3]) / incs[3];
	} else {
		ncubeiter = (maxcube - mincube + incs[3]) / incs[3];
	}

	firstpix = 1;
	for (kiter = 0; kiter < ncubeiter; kiter++)
	{
		if (mincube > maxcube) {
			fpixels[3] = mincube - (kiter * incs[3]);
		} else {
			fpixels[3] = mincube + (kiter * incs[3]);
		}

		lpixels[3] = fpixels[3];

		for (jiter = 0; jiter < nsliceiter; jiter++)
		{
			if (minslice > maxslice) {
				fpixels[2] = minslice - (jiter * incs[2]);
			} else {
				fpixels[2] = minslice + (jiter * incs[2]);
			}

			lpixels[2] = fpixels[2];

			for (iiter = 0; iiter < nrowiter; iiter++)
			{
				if (minrow > maxrow) {
					fpixels[1] = minrow - (iiter * incs[1]);
				} else {
					fpixels[1] = minrow + (iiter * incs[1]);
				}

				lpixels[1] = fpixels[1];

				if (bitpix == 8)
				{
					ffgsvb(fptr, 1, naxis, naxes, fpixels, lpixels, incs, 0,
							(unsigned char *) buffer, &anynull, status);

					ffpprb(newptr, 1, firstpix, outsize, (unsigned char *) buffer, status);
				}
				else if (bitpix == 16)
				{
					ffgsvi(fptr, 1, naxis, naxes, fpixels, lpixels, incs, 0,
							(short *) buffer, &anynull, status);

					ffppri(newptr, 1, firstpix, outsize, (short *) buffer, status);
				}
				else if (bitpix == 32)
				{
					ffgsvk(fptr, 1, naxis, naxes, fpixels, lpixels, incs, 0,
							(int *) buffer, &anynull, status);

					ffpprk(newptr, 1, firstpix, outsize, (int *) buffer, status);
				}
				else if (bitpix == -32)
				{
					ffgsve(fptr, 1, naxis, naxes, fpixels, lpixels, incs, FLOATNULLVALUE,
							(float *) buffer, &anynull, status);

					ffppne(newptr, 1, firstpix, outsize, (float *) buffer, FLOATNULLVALUE, status);
				}
				else if (bitpix == -64)
				{
					ffgsvd(fptr, 1, naxis, naxes, fpixels, lpixels, incs, DOUBLENULLVALUE,
							buffer, &anynull, status);

					ffppnd(newptr, 1, firstpix, outsize, buffer, DOUBLENULLVALUE,
							status);
				}
				else if (bitpix == 64)
				{
					ffgsvjj(fptr, 1, naxis, naxes, fpixels, lpixels, incs, 0,
							(LONGLONG *) buffer, &anynull, status);

					ffpprjj(newptr, 1, firstpix, outsize, (LONGLONG *) buffer, status);
				}


				firstpix += outsize;
			}
		}
	}

	free(buffer);  /* finished with the memory */

	if (*status > 0)
	{
		ffpmsg("fits_copy_image_section: error copying image section");
		return(*status);
	}

	return(*status);
}

