/*
 * This file is part of Siril, an astronomy image processor.
 * Copyright (C) 2005-2011 Francois Meyer (dulle at free.fr)
 * Copyright (C) 2012-2023 team free-astro (see more in AUTHORS file)
 * Reference site is https://free-astro.org/index.php/Siril
 *
 * Siril is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Siril is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Siril. If not, see <http://www.gnu.org/licenses/>.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <math.h>

#include "core/siril.h"
#include "algos/astrometry_solver.h"
#include "io/image_format_fits.h"

#ifdef HAVE_WCSLIB
#include <wcslib.h>
#include <wcsfix.h>
#endif

#include "siril_wcs.h"

#ifdef HAVE_WCSLIB
static GMutex wcs_mutex;
#endif

/* we force naxis to 2 */
#define NAXIS 2

gboolean has_wcs(fits *fit) {
#ifdef HAVE_WCSLIB
	return fit->wcslib != NULL;
#endif
	return FALSE;
}

// deal with cases where wcsdata is not NULL but members are set to 0
gboolean has_wcsdata(fits *fit) {
	if ((fit->wcsdata.crval[0] == 0.0 && fit->wcsdata.crval[1] == 0.0))
		return FALSE;
	return TRUE;
}


void free_wcs(fits *fit, gboolean keep_RADEC) {
#ifdef HAVE_WCSLIB
	if (fit->wcslib) {
		if (!wcsfree(fit->wcslib))
			free(fit->wcslib);
		fit->wcslib = NULL;
	}
#endif
	if (keep_RADEC) {
		memset(&fit->wcsdata.cdelt, 0, sizeof(fit->wcsdata.cdelt));
		memset(&fit->wcsdata.crpix, 0, sizeof(fit->wcsdata.crpix));
		memset(&fit->wcsdata.crval, 0, sizeof(fit->wcsdata.crval));
		memset(&fit->wcsdata.pc, 0, sizeof(fit->wcsdata.pc));
		memset(&fit->wcsdata.pltsolvd, 0, sizeof(fit->wcsdata.pltsolvd));
		memset(&fit->wcsdata.pltsolvd_comment, 0, sizeof(fit->wcsdata.pltsolvd_comment));
	} else {
		memset(&fit->wcsdata, 0, sizeof(fit->wcsdata));
	}
}

gboolean load_WCS_from_memory(fits *fit) {
#ifdef HAVE_WCSLIB
	int status;
	if (!fit->wcslib) {
		fit->wcslib = calloc(1, sizeof(struct wcsprm));
		if(!fit->wcslib) {
			PRINT_ALLOC_ERR;
			return FALSE;
		}
		fit->wcslib->flag = -1;
	}
	wcsinit(1, NAXIS, fit->wcslib, 0, 0, 0);

	const char CTYPE[2][9] = { "RA---TAN", "DEC--TAN" };
	const char CUNIT[2][9] = { "deg", "deg" };

	for (int i = 0; i < NAXIS; i++) {
		strncpy(fit->wcslib->cunit[i], &CUNIT[i][0], 71); // 72 char fixed buffer, keep 1 for the NULL
	}

	double *pcij = fit->wcslib->pc;
	for (int i = 0; i < NAXIS; i++) {
		for (int j = 0; j < NAXIS; j++) {
			*(pcij++) = fit->wcsdata.pc[i][j];
		}
	}

	for (int i = 0; i < NAXIS; i++) {
		fit->wcslib->crval[i] = fit->wcsdata.crval[i];
	}

	for (int i = 0; i < NAXIS; i++) {
		fit->wcslib->crpix[i] = fit->wcsdata.crpix[i];
	}

	for (int i = 0; i < NAXIS; i++) {
		fit->wcslib->cdelt[i] = fit->wcsdata.cdelt[i];
	}

	for (int i = 0; i < NAXIS; i++) {
		strncpy(fit->wcslib->ctype[i], &CTYPE[i][0], 71); // 72 byte buffer, leave 1 byte for the NULL
	}

	fit->wcslib->equinox = fit->wcsdata.equinox;
//	fit->wcslib->lonpole = 180;
	fit->wcslib->latpole = fit->wcsdata.crval[1];

	if ((status = wcsset(fit->wcslib)) != 0) {
		/* here we do not want to use free_wcs because
		 * we want to keep original header, just in case */
		if (fit->wcslib) {
			if (!wcsfree(fit->wcslib))
				free(fit->wcslib);
			fit->wcslib = NULL;
		}
		siril_debug_print("wcsset error %d: %s.\n", status, wcs_errmsg[status]);
		return FALSE;
	}
	return TRUE;
#else
	return FALSE;
#endif
}


gboolean load_WCS_from_file(fits* fit) {
#ifdef HAVE_WCSLIB
	int status = 0, wcs_status = 0;
	char *header;
	struct wcsprm *data = NULL;
	int nkeyrec, nreject, nwcs;

	/* sanity check to avoid error in some strange files */
	if ((fit->wcsdata.crpix[0] == 0) && (fit->wcsdata.crpix[1] == 0)) {
		return FALSE;
	}
	/* another sanity check, because at this stage we must have a valid pc matrix
	 * hence this function must be called in load_wcs_keywords
	 */
	if ((fit->wcsdata.pc[0][0] * fit->wcsdata.pc[1][1] - fit->wcsdata.pc[1][0] * fit->wcsdata.pc[0][1]) == 0.0) {
		return FALSE;
	}

	if (fit->wcslib) {
		free_wcs(fit, FALSE);
	}

	ffhdr2str(fit->fptr, 1, NULL, 0, &header, &nkeyrec, &status);
	if (status) {
		report_fits_error(status);
		return FALSE;
	}

	/** There is a bug with wcspih that it is not really thread-safe for wcslib version < 7.5.
	 * We need to lock it.
	 * TODO: check wcslib version ?*/
	g_mutex_lock(&wcs_mutex);
	wcs_status = wcspih(header, nkeyrec, 0, 0, &nreject, &nwcs, &data);

	if (wcs_status == 0) {
		for (int i = 0; i < nwcs; i++) {
			/* Find the master celestial WCS coordinates */
			struct wcsprm *prm = data + i;
//			/* ctype3 = 'RGB' fix */
//			if (prm->naxis == 3) {
//				cdfix(prm);
//			}
			wcsset(prm);
			if (prm->lng >= 0 && prm->lat >= 0
					&& (prm->alt[0] == '\0' || prm->alt[0] == ' ')) {
				int axes[2], nsub;
				nsub = 2;
				axes[0] = WCSSUB_LONGITUDE;
				axes[1] = WCSSUB_LATITUDE;
				fit->wcslib = (struct wcsprm*) calloc(1, sizeof(struct wcsprm));
				fit->wcslib->flag = -1;
				status = wcssub(1, prm, &nsub, axes, fit->wcslib);
				if (status == 0) {
					break;
				} else {
					siril_debug_print("wcssub error %d: %s.\n", status, wcs_errmsg[status]);
					wcsvfree(&nwcs, &fit->wcslib);
					fit->wcslib = NULL;
				}
			}
		}
	}
	wcsvfree(&nwcs, &data);
	g_mutex_unlock(&wcs_mutex);
	free(header);

	if (!fit->wcslib) {
		siril_debug_print("No world coordinate systems found.\n");
		wcsvfree(&nwcs, &fit->wcslib);
		fit->wcslib = NULL;
		return FALSE;
	}

	return TRUE;
#else
	return FALSE;
#endif
}

#ifdef HAVE_WCSLIB
void pix2wcs2(struct wcsprm *wcslib, double x, double y, double *r, double *d) {
	*r = 0.0;
	*d = 0.0;
	int status, stat[NWCSFIX];
	double imgcrd[NWCSFIX], phi, pixcrd[NWCSFIX], theta, world[NWCSFIX];

	pixcrd[0] = x;
	pixcrd[1] = y;

	status = wcsp2s(wcslib, 1, 2, pixcrd, imgcrd, &phi, &theta, world, stat);
	if (status != 0)
		return;

	*r = world[0];
	*d = world[1];
}
#endif

void pix2wcs(fits *fit, double x, double y, double *r, double *d) {
	*r = 0.0;
	*d = 0.0;
#ifdef HAVE_WCSLIB
	if (fit->wcslib)
		pix2wcs2(fit->wcslib, x, y, r, d);
#endif
}

// ra in degrees
int wcs2pix(fits *fit, double ra, double dec, double *x, double *y) {
	if (x) *x = -1.0;
	if (y) *y = -1.0;
#ifndef HAVE_WCSLIB
	return 1;
#else
	int status, stat[NWCSFIX];
	double imgcrd[NWCSFIX], phi, pixcrd[NWCSFIX], theta, world[NWCSFIX];
	world[0] = ra;
	world[1] = dec;

	status = wcss2p(fit->wcslib, 1, 2, world, &phi, &theta, imgcrd, pixcrd, stat);

	if (!status) {
		double xx = pixcrd[0];
		double yy = pixcrd[1];
		// return values even if outside (required for celestial grid display)
		if (x) *x = xx;
		if (y) *y = yy;
		if (xx < 0.0 || yy < 0.0 || xx > (double)fit->rx || yy > (double)fit->ry) {
			//siril_debug_print("outside image but valid return\n");
			// wcss2p returns values between 0 and 9, picking a new one
			status = 10;
		}
	}
	return status;
#endif
}

/* get image center celestial coordinates */
void center2wcs(fits *fit, double *r, double *d) {
	*r = -1.0;
	*d = -1.0;
#ifdef HAVE_WCSLIB
	int status, stat[NWCSFIX];
	double imgcrd[NWCSFIX], phi, pixcrd[NWCSFIX], theta, world[NWCSFIX];

	pixcrd[0] = (double)(fit->rx) / 2.;
	pixcrd[1] = (double)(fit->ry) / 2.;

	status = wcsp2s(fit->wcslib, 1, 2, pixcrd, imgcrd, &phi, &theta, world, stat);
	if (status != 0)
		return;

	*r = world[0];
	*d = world[1];
#endif
}

/* get resolution in degree/pixel */
double get_wcs_image_resolution(fits *fit) {
	double resolution = -1.0;
#ifdef HAVE_WCSLIB
	if (fit->wcslib) {
		double cd[NAXIS][NAXIS], pc[NAXIS][NAXIS];
		double cdelt[NAXIS];

		double *pcij = fit->wcslib->pc;
		for (int i = 0; i < NAXIS; i++) {
			for (int j = 0; j < NAXIS; j++) {
				pc[i][j] = *(pcij++);
			}
		}

		for (int i = 0; i < NAXIS; i++) {
			cdelt[i] = fit->wcslib->cdelt[i];
		}

		wcs_pc_to_cd(pc, cdelt, cd);

		double res_x = sqrt(cd[0][0] * cd[0][0] + cd[1][0] * cd[1][0]);
		double res_y = sqrt(cd[0][1] * cd[0][1] + cd[1][1] * cd[1][1]);
		resolution = (res_x + res_y) * 0.5;
	}
#endif
	if (resolution <= 0.0) {
		if (fit->focal_length >= 0.0 && fit->pixel_size_x >= 0.0 && fit->pixel_size_y == fit->pixel_size_x)
			resolution = (RADCONV / fit->focal_length * fit->pixel_size_x) / 3600.0;
		// what about pix size x != y?
	}
	return resolution;
}
