/** @file  psFitsHeader.c
 *
 *  @brief Contains Fits header I/O routines
 *
 *  @ingroup FileIO
 *
 *  @author Robert DeSonia, MHPCC
 *
 *  @version $Revision: 1.32 $ $Name: not supported by cvs2svn $
 *  @date $Date: 2007-02-21 21:39:26 $
 *
 *  Copyright 2004-2005 Maui High Performance Computing Center, University of Hawaii
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include <unistd.h>

#include "psFits.h"
#include "string.h"
#include "strings.h"
#include "psError.h"

#include "psImageStructManip.h"
#include "psMemory.h"
#include "psString.h"
#include "psLogMsg.h"
#include "psTrace.h"
#include "psVector.h"

#define MAX_STRING_LENGTH 256  // maximum length string for FITS routines

// list of FITS header keys to ignore; NULL-terminated
static char* ignoreFitsKeys[] = {"", NULL};

// List of FITS header keys that may be duplicated; NULL-terminated
static char *duplicateFitsKeys[] = {"COMMENT", "HIERARCH", "HISTORY", NULL};

// List of FITS header keys not to write (handled by cfitsio); NULL-terminated
static char *noWriteFitsKeys[] = {"SIMPLE", "XTENSION", "BITPIX", "NAXIS", "EXTNAME", "BSCALE", "BZERO",
                                  "TFIELDS", NULL};
// List of the start of FITS header keys not to write (handled by cfitsio); NULL-terminated
static char *noWriteFitsKeyStarts[] = {"NAXIS", "TTYPE", "TFORM", NULL};

// List of FITS header keys to be written with fits_write_comment

psMetadata* psFitsReadHeader(psMetadata* out,
                             const psFits* fits)
{
    if (fits == NULL) {
        psError(PS_ERR_BAD_PARAMETER_NULL, true,
                _("The input psFits object can not NULL."));
        return NULL;
    }

    if (out == NULL) {
        out = psMetadataAlloc();
    }

    // Get number of key names
    int numKeys = 0;                    // Number of keywords
    int keyNum = 0;                     // Current key number
    int status = 0;                     // Status of cfitsio calls
    fits_get_hdrpos(fits->fd, &numKeys, &keyNum, &status);

    // Get each key name. Keywords start at one.
    for (int i = 1; i <= numKeys; i++) {
        char keyName[MAX_STRING_LENGTH];// Keyword name
        char keyValue[MAX_STRING_LENGTH]; // Corresponding value
        char keyComment[MAX_STRING_LENGTH]; // Corresponding comment
        fits_read_keyn(fits->fd, i, keyName, keyValue, keyComment, &status);

        // Check to see if the keyword should be ignored
        bool ignoreKey = false;         // Ignore this keyword?
        for (int i = 0; ignoreFitsKeys[i] && !ignoreKey; i++) {
            if (strcmp(keyName, ignoreFitsKeys[i]) == 0) {
                ignoreKey = true;
            }
        }
        if (ignoreKey) {
            // We're done here; skip to the next key
            continue;
        }

        // Check to see if the keyword should be duplicated
        int dupFlag = 0;                // Duplicate flag
        for (int i = 0; duplicateFitsKeys[i] && !dupFlag; i++) {
            if (strcmp(keyName, duplicateFitsKeys[i]) == 0) {
                dupFlag = PS_META_DUPLICATE_OK;
            }
        }

        char keyType;                   // Type of key; from cfitsio
        if (keyValue[0] != 0) { // blank values are not handled by fits_get_keytype
            fits_get_keytype(keyValue, &keyType, &status);
        } else {
            keyType = 'C';
        }
        if (status != 0) {
            break;
        }

        bool success;                 // Was the add to the metadata successful?
        switch (keyType) {
        case 'X': // bit
        case 'B': // byte
            success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_S8 | dupFlag, keyComment,
                                    atoi(keyValue));
            break;
        case 'I': // short int.
            // This is the default type that cfitsio reports whenever it doesn't know what it is.
            // Trap NAN, INF and -INF, which cfitsio doesn't handle.
            if (strncasecmp(keyValue, "NAN", 3) == 0) {
                success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_F32 | dupFlag, keyComment,
                                        (psF32)NAN);
            } else if (strncasecmp(keyValue, "INF", 3) == 0) {
                success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_F32 | dupFlag, keyComment,
                                        (psF32)INFINITY);
            } else if (strncasecmp(keyValue, "-INF", 4) == 0) {
                success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_F32 | dupFlag, keyComment,
                                        (psF32)-INFINITY);
            } else {
                success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_S32 | dupFlag, keyComment,
                                        atoi(keyValue));
            }
            break;
        case 'J': // int.
            success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_S32 | dupFlag, keyComment,
                                    atoi(keyValue));
            break;
        case 'U': // unsigned int.
            success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_U32 | dupFlag, keyComment,
                                    atol(keyValue));
            break;

        case 'K': // long int. can't all fit in a psS32, put in psF64
        case 'F':
            success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_F64 | dupFlag, keyComment,
                                    atof(keyValue));
            break;
        case 'C': {
                char *keyValueFixed = keyValue; // Fixed version of the string
                // remove the single-quotes at front/end
                if (keyValueFixed[0] == '\'' && keyValueFixed[strlen(keyValue)-1] == '\'') {
                    keyValue[strlen(keyValue)-1] = '\0'; // Remove the trailing quote
                    keyValueFixed += 1; // Advance past the leading quote
                }
                // Remove trailing spaces, which are not significant, according to the FITS standard
                // http://archive.stsci.edu/fits/fits_standard/node31.html
                char *lastSpace = NULL; // The last space in the string
                while (strlen(keyValueFixed) > 1 && (lastSpace = strrchr(keyValueFixed, ' ')) &&
                        lastSpace[1] == '\0') {
                    // This is a trailing space, not a leading space.
                    lastSpace[0] = '\0'; // Truncate the string here
                }

                // Need to trap NAN, INF and -INF written by psFitsWriteHeader.
                // cfitsio won't write these, so we write them as strings, and then have to trap them on read.
                if (strcasecmp(keyValueFixed, "NAN") == 0) {
                    success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_F32 | dupFlag, keyComment,
                                            (psF32)NAN);
                } else if (strcasecmp(keyValueFixed, "INF") == 0) {
                    success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_F32 | dupFlag, keyComment,
                                            (psF32)INFINITY);
                } else if (strcasecmp(keyValueFixed, "-INF") == 0) {
                    success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_F32 | dupFlag, keyComment,
                                            (psF32)-INFINITY);
                } else {
                    success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_STRING | dupFlag, keyComment,
                                            keyValueFixed);
                }
                break;
            }
        case 'L': {
                bool temp = (keyValue[0] == 'T') ? 1 : 0;
                success = psMetadataAdd(out, PS_LIST_TAIL, keyName, PS_DATA_BOOL | dupFlag, keyComment, temp);
                break;
            }
        default:
            psError(PS_ERR_IO, true, _("Specified FITS metadata type, %c, is not supported."), keyType);
            return out;
        }

        if (!success) {
            psError(PS_ERR_UNKNOWN, false, _("Failed to add metadata item, %s."), keyName);
            return out;
        }

    }

    if (status != 0) {
        char fitsErr[MAX_STRING_LENGTH];
        (void)fits_get_errstatus(status, fitsErr);
        psError(PS_ERR_IO, true, _("Failed to add metadata item, %s."), fitsErr);
        return false;
    }

    return out;
}

psMetadata* psFitsReadHeaderSet(psMetadata* out, const psFits* fits)
{
    if (fits == NULL) {
        psError(PS_ERR_BAD_PARAMETER_NULL, true, _("The input psFits object can not NULL."));
        psFree(out);
        return NULL;
    }

    if (out == NULL) {
        out = psMetadataAlloc();
    }

    int size = psFitsGetSize(fits);

    int origPosition = psFitsGetExtNum(fits);

    for (int lcv=0; lcv < size; lcv++) {
        psFitsMoveExtNum(fits, lcv, false);

        char* name = NULL;
        if (lcv == 0) {
            name = psStringCopy("PHU");
        } else {
            name = psFitsGetExtName(fits);
        }

        psMetadata* header = psFitsReadHeader(NULL, fits);
        if (name != NULL && header != NULL) {
            psMetadataAddMetadata(out, PS_LIST_TAIL, name, 0, "FITS Header", header);
        } else {
            psWarning("Failed to read HDU#%d header data.",
                      lcv);
        }

        psFree(name);
        psFree(header);
    }

    // reposition to the original position
    psFitsMoveExtNum(fits, origPosition, false);

    return out;
}

static bool fitsWriteHeader(psFits *fits, // The FITS file handle
                            const psMetadata *output, // Metadata that is to be output into the FITS file
                            bool keyStarts // Write out the key starts?
                           )
{
    int status = 0;                     // Status of cfitsio calls
    bool simple = true;                 // If SIMPLE is T, then the file should conform to the FITS standard
    psMetadataItem *simpleItem = psMetadataLookup(output, "SIMPLE"); // SIMPLE in the header
    if (simpleItem) {
        // We allow the user to write SIMPLE, but it must be boolean
        if (simpleItem->type != PS_DATA_BOOL) {
            psError(PS_ERR_BAD_PARAMETER_TYPE, true, "SIMPLE in a FITS header must be of boolean type: "
                    "not %x --- assuming TRUE.\n", simpleItem->type);
            int value = false;          // Temporary holder for boolean
            fits_update_key(fits->fd, TLOGICAL, "SIMPLE", &value,
                            "File does not conform to FITS standard", &status);
            simple = false;
        } else if (!simpleItem->data.B) {
            simple = false;
            int value = false;          // Temporary holder for boolean
            fits_update_key(fits->fd, TLOGICAL, "SIMPLE", &value,
                            "File does not conform to FITS standard", &status);
        }
        // SIMPLE = T is taken care of by cfitsio.
    }

    // Traverse the metadata list and add each key.
    psListIterator* iter = psListIteratorAlloc(output->list, PS_LIST_HEAD, true); // Iterator
    psMetadataItem* item;               // Item from iteration
    while ((item = psListGetAndIncrement(iter))) {
        // Check to see if the item should be ignored
        if (simple) {
            // We ignore particular (required) keywords, because these are written by CFITSIO
            // Furthermore, users tend to supply FITS headers that are wrong (e.g., after binning down the
            // image, the NAXISn haven't been changed; or after converting to F32, the BITPIX hasn't been
            // changed) so we'll take care of that for them.
            bool writeKey = true;           // Should we write this keyword?
            for (int i = 0; noWriteFitsKeys[i] && writeKey; i++) {
                if (strcmp(item->name, noWriteFitsKeys[i]) == 0) {
                    writeKey = false;
                }
            }
            if (!writeKey) {
                // Don't write it; skip to the next key
                continue;
            }
            if (keyStarts) {
                // Also block out TTYPEn, NAXISn, etc --- keywords that start with a certain sequence.
                // We want to do this when writing an image or table, since it guarantees that the NAXISn etc
                // that go in are correct.  However, when we're writing a "blank" HDU (header only), we want
                // to preserve NAXISn etc for reference, so we don't do this.
                for (int i = 0; noWriteFitsKeyStarts[i] && writeKey; i++) {
                    if (strncmp(item->name, noWriteFitsKeyStarts[i], strlen(noWriteFitsKeyStarts[i])) == 0) {
                        writeKey = false;
                    }
                }
                if (!writeKey) {
                    // Don't write it; skip to the next key
                    continue;
                }
            }
        }

        if (strcmp(item->name, "COMMENT") == 0) {
            fits_write_comment(fits->fd, item->comment, &status);
        } else if (strcmp(item->name,  "HISTORY") == 0) {
            fits_write_history(fits->fd, item->comment, &status);
        } else {
            // A regular FITS header
            switch (item->type) {
            case PS_DATA_BOOL: {
                    int value = item->data.B;
                    fits_update_key(fits->fd, TLOGICAL, item->name, &value, item->comment, &status);
                    break;
                }
            case PS_DATA_S8:
                fits_update_key(fits->fd, TBYTE, item->name, &item->data.S8, item->comment, &status);
                break;
            case PS_DATA_S16:
                fits_update_key(fits->fd, TSHORT, item->name, &item->data.S16, item->comment, &status);
                break;
            case PS_DATA_S32:
                fits_update_key(fits->fd, TINT, item->name, &item->data.S32, item->comment, &status);
                break;
            case PS_DATA_U8: {
                    unsigned short int temp = item->data.U8;
                    fits_update_key(fits->fd, TUSHORT, item->name, &temp, item->comment, &status);
                }
                break;
            case PS_DATA_U16:
                fits_update_key(fits->fd, TUSHORT, item->name, &item->data.U16, item->comment, &status);
                break;
            case PS_DATA_U32:
                fits_update_key(fits->fd, TUINT, item->name, &item->data.U32, item->comment, &status);
                break;
            case PS_DATA_F32: {
                    int infCheck = 0;         // Result of isinf()
                    if (isnan(item->data.F32)) {
                        fits_update_key(fits->fd, TSTRING, item->name, "NaN", item->comment, &status);
                    } else if ((infCheck = isinf(item->data.F32)) != 0) {
                        if (infCheck == 1) {
                            fits_update_key(fits->fd, TSTRING, item->name, "Inf", item->comment, &status);
                        } else {
                            fits_update_key(fits->fd, TSTRING, item->name, "-Inf", item->comment, &status);
                        }
                    } else {
                        fits_update_key(fits->fd, TFLOAT, item->name, &item->data.F32, item->comment, &status);
                    }
                    break;
                }
            case PS_DATA_F64: {
                    int infCheck = 0;         // Result of isinf()
                    if (isnan(item->data.F64)) {
                        fits_update_key(fits->fd, TSTRING, item->name, "NaN", item->comment, &status);
                    } else if ((infCheck = isinf(item->data.F64)) != 0) {
                        if (infCheck == 1) {
                            fits_update_key(fits->fd, TSTRING, item->name, "Inf", item->comment, &status);
                        } else {
                            fits_update_key(fits->fd, TSTRING, item->name, "-Inf", item->comment, &status);
                        }
                    } else {
                        fits_update_key(fits->fd, TDOUBLE, item->name, &item->data.F64, item->comment, &status);
                    }
                    break;
                }
            case PS_DATA_STRING:
                fits_update_key(fits->fd, TSTRING, item->name, item->data.V, item->comment, &status);
                break;
            default:  // all other META types are ignored
                psLogMsg(__func__, PS_LOG_WARN, "Attempt to write metadata type %x to FITS header ignored.\n",
                         item->type);
                break;
            }
        }

        if (status != 0) {
            char fitsErr[MAX_STRING_LENGTH];
            (void)fits_get_errstatus(status, fitsErr);
            psError(PS_ERR_IO, true, _("Could not write data to file. CFITSIO Error: %s"), fitsErr);
            return false;
        }
    }

    psFree(iter);

    return true;
}

bool psFitsWriteHeader(psFits *fits,
                       const psMetadata *output
                      )
{
    if (!fits) {
        psError(PS_ERR_BAD_PARAMETER_NULL, true, _("The input psFits object can not NULL."));
        return false;
    }

    if (!output) {
        psError(PS_ERR_BAD_PARAMETER_NULL, true, _("The input psMetadata was NULL.  Need a non-NULL psMetadata for operation to be performed."));
        return false;
    }

    return fitsWriteHeader(fits, output, true);
}

bool psFitsWriteBlank(psFits* fits,
                      const psMetadata* output,
                      const char *extname
                     )
{
    if (!fits) {
        psError(PS_ERR_BAD_PARAMETER_NULL, true, _("The input psFits object can not NULL."));
        return false;
    }

    // We allow output == NULL in order to write a minimal header.

    // Create a dummy image HDU for the primary HDU
    int status = 0;                 // Status of cfitsio

    psFitsMoveLast(fits);

    int hdus = psFitsGetSize(fits);     // Number of HDUs in file
    if (hdus == 0) {
        // We're creating the first image
        fits_create_img(fits->fd, 16, 0, NULL, &status);
    } else {
        // Insert after the current position
        fits_insert_img(fits->fd, 16, 0, NULL, &status);
    }

    if (status) {
        char fitsErr[MAX_STRING_LENGTH];
        (void)fits_get_errstatus(status, fitsErr);
        psError(PS_ERR_IO, true, "Unable to create blank header.\n%s\n", fitsErr);
        return false;
    }

    if (output && !fitsWriteHeader(fits, output, false)) {
        psError(PS_ERR_IO, false, "Unable to write FITS header.\n");
        return false;
    }

    if (extname && strlen(extname)) {
        psFitsSetExtName(fits, extname);
    }

    char buffer[10];
    fits_write_img(fits->fd, TSHORT, 1, 0, buffer, &status);

    return true;
}

bool psFitsHeaderValidate(psMetadata *header)
{
    if (header == NULL) {
        psError(PS_ERR_BAD_PARAMETER_NULL, true, _("The input psMetadata was NULL.  Need a non-NULL psMetadata for operation to be performed."));
        return false;
    }

    // Traverse the metadata list and inspect at each key
    psListIterator* iter = psListIteratorAlloc(header->list, PS_LIST_HEAD, true); // Iterator
    psMetadataItem* item;               // Item from iteration
    bool valid = true;                  // Are all items valid?
    while ((item = psListGetAndIncrement(iter))) {
        if (item->type > PS_DATA_STRING) { // i.e., a non-primitive type
            valid = false;
        }

        if (strlen(item->name) > 8) {
            item->name[8] = '\0'; // truncate to 8 characters
        }

        fits_uppercase(item->name); // make uppercase

        // now, let's see if CFITSIO thinks this is a good keyword...
        int status = 0;                 // Status from cfitsio calls
        if (fits_test_keyword(item->name,&status) != 0) {
            valid = false;
        }
    }
    psFree(iter);

    return valid;
}
