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

#include <stdio.h>
#include <string.h>
#include <assert.h>

#include "pslib.h"

#include "pmFPA.h"
#include "pmConceptsRead.h"
#include "pmConceptsWrite.h"
#include "pmConceptsStandard.h"

// The functions in this file are intended to be called solely within the psModules concepts code.  For this
// reason, they use "assert" instead of the PS_ASSERT_WHATEVER functions --- if there's a problem, then
// there's a BIG problem that affects all of the code.

#define COMPARE_REGIONS(a,b) (((a)->x0 == (b)->x0 && \
                               (a)->x1 == (b)->x1 && \
                               (a)->y0 == (b)->y0 && \
                               (a)->y1 == (b)->y1) ? true : false)

#define TYPE_CASE(assign, item, TYPE) \
case PS_TYPE_##TYPE: \
assign = item->data.TYPE; \
break;



static double defaultCoordScaling(psMetadataItem *pattern)
{
    if (strcmp(pattern->name, "FPA.RA") == 0) {
        psLogMsg(__func__, PS_LOG_WARN, "Assuming format for %s is HOURS.\n", pattern->name);
        return M_PI / 12.0;
    }
    if (strcmp(pattern->name, "FPA.DEC") == 0) {
        psLogMsg(__func__, PS_LOG_WARN, "Assuming format for %s is DEGREES.\n", pattern->name);
        return M_PI / 180.0;
    }
    psAbort(__func__, "Should never ever get here.\n");
    return NAN;
}


// FPA.RA and FPA.DEC
psMetadataItem *p_pmConceptParse_FPA_Coords(psMetadataItem *concept,
        psMetadataItem *pattern,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);
    assert(pattern);
    assert(cameraFormat);

    double coords = NAN;                // The coordinates
    switch (concept->type) {
    case PS_TYPE_F32:
        coords = concept->data.F32;
        break;
    case PS_TYPE_F64:
        coords = concept->data.F64;
        break;
    case PS_DATA_STRING:
        // Sexagesimal format
        {
            int big, medium;
            float small;
            // XXX: Upgrade path is to allow dd:mm.mmm
            if (sscanf(concept->data.V, "%d:%d:%f", &big, &medium, &small) != 3 &&
                    sscanf(concept->data.V, "%d %d %f", &big, &medium, &small) != 3)
            {
                psError(PS_ERR_UNKNOWN, true, "Cannot interpret FPA.RA: %s\n", concept->data.str);
                break;
            }
            coords = abs(big) + (float)medium/60.0 + small/3600.0;
            if (big < 0)
            {
                coords *= -1.0;
            }
        }
        break;
    default:
        psError(PS_ERR_UNKNOWN, true, "%s concept is of an unexpected type: %x\n", pattern->name, concept->type);
        return NULL;
    }

    // How to interpret the coordinates
    bool mdok = true;           // Status of MD lookup
    psMetadata *formats = psMetadataLookupMD(&mdok, cameraFormat, "FORMATS");
    if (mdok && formats) {
        psString format = psMetadataLookupStr(&mdok, formats, pattern->name);
        if (mdok && strlen(format) > 0) {
            if (strcasecmp(format, "HOURS") == 0) {
                coords *= M_PI / 12.0;
            } else if (strcasecmp(format, "DEGREES") == 0) {
                coords *= M_PI / 180.0;
            } else if (strcasecmp(format, "RADIANS") == 0) {
                // No action required
            } else {
                coords *= defaultCoordScaling(pattern);
            }
        } else {
            coords *= defaultCoordScaling(pattern);
        }
    } else {
        coords *= defaultCoordScaling(pattern);
    }

    return psMetadataItemAllocF64(pattern->name, pattern->comment, coords);
}

// FPA.RA and FPA.DEC
psMetadataItem *p_pmConceptFormat_FPA_Coords(psMetadataItem *concept,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);
    assert(cameraFormat);

    double coords = concept->data.F64;  // The coordinates

    // How to interpret the coordinates
    bool mdok = true;                   // Status of MD lookup
    psMetadata *formats = psMetadataLookupMD(&mdok,
                          cameraFormat,
                          "FORMATS");
    if (mdok && formats) {
        psString format = psMetadataLookupStr(&mdok,formats, concept->name);
        if (mdok && strlen(format) > 0) {
            if (strcasecmp(format, "HOURS") == 0) {
                coords /= M_PI / 12.0;
            } else if (strcasecmp(format, "DEGREES") == 0) {
                coords /= M_PI / 180.0;
            } else if (strcasecmp(format, "RADIANS") == 0) {
                // No action required
            } else {
                coords /= defaultCoordScaling(concept);
            }
        } else {
            coords /= defaultCoordScaling(concept);
        }
    } else {
        coords /= defaultCoordScaling(concept);
    }

    // We choose to write sexagesimal format
    int big, medium;
    float small;
    big = (int)coords;
    medium = (int)(60.0*(coords - (double)big));
    small = 3600.0*(coords - (double)big - 60.0 * (double)medium);
    psString coordString = NULL;        // String with the coordinates in sexagesimal format
    psStringAppend(&coordString, "%d:%d:%.2f", big, medium, small);
    psMetadataItem *coordItem = psMetadataItemAllocStr(concept->name, concept->comment, coordString);
    psFree(coordString);

    return coordItem;
}


psMetadataItem *p_pmConceptParse_CELL_TRIMSEC(psMetadataItem *concept,
        psMetadataItem *pattern,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);
    assert(cell);
    assert(pattern);

    psRegion *trimsec = psRegionAlloc(0, 0, 0, 0);

    if (concept->type != PS_DATA_STRING) {
        psError(PS_ERR_UNKNOWN, true, "CELL.TRIMSEC after read is not of type STR (%x)\n", concept->type);
    } else {
        *trimsec = psRegionFromString(concept->data.V);
    }

    psMetadataItem *item = psMetadataItemAllocPtr(pattern->name, PS_DATA_REGION, pattern->comment, trimsec);
    psFree(trimsec);
    return item;
}

psMetadataItem *p_pmConceptParse_CELL_BIASSEC(psMetadataItem *concept,
        psMetadataItem *pattern,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);
    assert(cell);
    assert(pattern);

    psList *biassecs = psListAlloc(NULL); // List of bias sections

    switch (concept->type) {
    case PS_DATA_STRING: {
            psList *regions = psStringSplit(concept->data.V, " ;", true); // List of regions
            psListIterator *regionsIter = psListIteratorAlloc(regions, PS_LIST_HEAD, false); // Iterator
            psString regionString = NULL; // Region string from iteration
            while ((regionString = psListGetAndIncrement(regionsIter))) {
                psRegion *region = psAlloc(sizeof(psRegion)); // The region
                *region = psRegionFromString(regionString);
                psListAdd(biassecs, PS_LIST_TAIL, region);
                psFree(region);           // Drop reference
            }
            psFree(regionsIter);
            psFree(regions);
            break;
        }
    case PS_DATA_LIST: {
            psList *regions = concept->data.V; // The list of regions
            psListIterator *regionsIter = psListIteratorAlloc(regions, PS_LIST_HEAD, false); // Iterator
            psMetadataItem *regionItem = NULL; // Item from list iteration
            while ((regionItem = psListGetAndIncrement(regionsIter))) {
                if (regionItem->type != PS_DATA_STRING) {
                    psLogMsg(__func__, PS_LOG_WARN, "CELL.BIASSEC member is not of type STR --- ignored.\n");
                    continue;
                }
                psRegion *region = psAlloc(sizeof(psRegion)); // The region
                *region = psRegionFromString(regionItem->data.V);
                psListAdd(biassecs, PS_LIST_TAIL, region);
                psFree(region);           // Drop reference
            }
            psFree(regionsIter);
            break;
        }
    default:
        psError(PS_ERR_UNKNOWN, true, "CELL.BIASSEC after read is not of type STRING or LIST --- assuming "
                "blank.\n");
    }

    psMetadataItem *item = psMetadataItemAllocPtr(pattern->name, PS_DATA_LIST, pattern->comment, biassecs);
    psFree(biassecs);               // Drop reference
    return item;
}

// CELL.XBIN and CELL.YBIN
psMetadataItem *p_pmConceptParse_CELL_Binning(psMetadataItem *concept,
        psMetadataItem *pattern,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);
    assert(pattern);

    int binning = 1;                    // Binning factor in x
    switch (concept->type) {
    case PS_DATA_STRING: {
            psString binString = concept->data.V; // The string containing the binning
            if ((strcmp(pattern->name, "CELL.XBIN") == 0 && sscanf(binString, "%d %*d", &binning) != 1 &&
                    sscanf(binString, "%d,%*d", &binning) != 1) ||
                    (strcmp(pattern->name, "CELL.YBIN") == 0 && sscanf(binString, "%*d %d", &binning) != 1 &&
                     sscanf(binString, "%*d,%d", &binning) != 1)) {
                psError(PS_ERR_UNKNOWN, true, "Unable to parse string to get %s: %s\n", pattern->name, binString);
            }
            break;
        }
        TYPE_CASE(binning, concept, U8);
        TYPE_CASE(binning, concept, U16);
        TYPE_CASE(binning, concept, U32);
        TYPE_CASE(binning, concept, S8);
        TYPE_CASE(binning, concept, S16);
        TYPE_CASE(binning, concept, S32);
    default:
        psError(PS_ERR_UNKNOWN, true, "Note sure how to parse %s of type %x --- assuming 1.\n", pattern->name,
                concept->type);
    }

    return psMetadataItemAllocS32(pattern->name, pattern->comment, binning);
}


psMetadataItem *p_pmConceptParse_CELL_TIMESYS(psMetadataItem *concept,
        psMetadataItem *pattern,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);
    assert(pattern);

    psTimeType timeSys = PS_TIME_UTC;   // The time system
    psString sys = concept->data.V;     // The time system string
    if (concept->type != PS_DATA_STRING || strlen(sys) <= 0) {
        psError(PS_ERR_UNKNOWN, true, "Can't interpret CELL.TIMESYS --- assuming UTC.\n");
    } else if (strcasecmp(sys, "TAI") == 0) {
        timeSys = PS_TIME_TAI;
    } else if (strcasecmp(sys, "UTC") == 0) {
        timeSys = PS_TIME_UTC;
    } else if (strcasecmp(sys, "UT1") == 0) {
        timeSys = PS_TIME_UT1;
    } else if (strcasecmp(sys, "TT") == 0) {
        timeSys = PS_TIME_TT;
    } else {
        psError(PS_ERR_UNKNOWN, true, "Can't interpret CELL.TIMESYS --- assuming UTC.\n");
    }

    return psMetadataItemAllocS32(pattern->name, pattern->comment, timeSys);
}

psMetadataItem *p_pmConceptParse_CELL_TIME(psMetadataItem *concept,
        psMetadataItem *pattern,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);
    assert(cameraFormat);
    assert(cell);

    // Need CELL.TIMESYS first
    bool mdok = true;                  // Result of MD lookup
    psTimeType timeSys = psMetadataLookupS32(&mdok, cell->concepts, "CELL.TIMESYS"); // The time system
    if (!mdok) {
        psLogMsg(__func__, PS_LOG_WARN, "Unable to find CELL.TIMESYS in concepts --- assuming UTC.\n");
        timeSys = PS_TIME_UTC;
    }

    // Work out how the time is represented
    bool usaTime = false;               // Is the time specified in USA (MM-DD-YYYY) order?
    bool backwardsTime = false;         // Is the time specified in backwards (DD-MM-YYYY) order?
    bool pre2000Time = false;           // Is the time specified pre-2000 (where the year only has two digits)
    bool mjdTime = false;               // Is the time specified a MJD?
    bool jdTime = false;                // Is the time specified a JD?

    // Get format
    psMetadata *formats = psMetadataLookupMD(&mdok, cameraFormat, "FORMATS");
    if (!mdok || !formats) {
        psError(PS_ERR_UNKNOWN, true, "Unable to find FORMATS in camera configuration.\n");
        return NULL;
    }

    psString timeFormat = psMetadataLookupStr(&mdok, formats, "CELL.TIME");
    if (!mdok || strlen(timeFormat) == 0) {
        psError(PS_ERR_UNKNOWN, true, "Unable to find CELL.TIME in FORMATS.\n");
        return NULL;
    }

    // Parse the time format
    psList *timeFormats = psStringSplit(timeFormat, " ,;", false); // List of the format options
    psListIterator *timeFormatsIter = psListIteratorAlloc(timeFormats, PS_LIST_HEAD, false); // Iter
    while ((timeFormat = psListGetAndIncrement(timeFormatsIter))) {
        if (strcasecmp(timeFormat, "USA") == 0) {
            usaTime = true;
            backwardsTime = false;
            jdTime = false;
            mjdTime = false;
        } else if (strcasecmp(timeFormat, "BACKWARDS") == 0) {
            backwardsTime = true;
            usaTime = false;
            jdTime = false;
            mjdTime = false;
        } else if (strcasecmp(timeFormat, "PRE2000") == 0) {
            pre2000Time = true;
        } else if (strcasecmp(timeFormat, "MJD") == 0) {
            mjdTime = true;
            jdTime = false;
            backwardsTime = false;
            usaTime = false;
        } else if (strcasecmp(timeFormat, "JD") == 0) {
            jdTime = true;
            mjdTime = false;
            backwardsTime = false;
            usaTime = false;
        } else {
            psError(PS_ERR_UNKNOWN, true, "Unrecognised FORMATS option for CELL.TIME: %s --- "
                    "ignored.\n", timeFormat);
            psFree(timeFormatsIter);
            psFree(timeFormats);
            return NULL;
        }
    }
    psFree(timeFormatsIter);
    psFree(timeFormats);

    psTime *time = NULL;                // The time
    switch (concept->type) {
    case PS_DATA_LIST: {
            // The date and time are stored separately
            // Assume the date is first and the time second
            psList *dateTime = concept->data.V; // The list containing items for date and time
            psMetadataItem *dateItem = psListGet(dateTime, PS_LIST_HEAD); // Item containing the date
            psMetadataItem *timeItem = psListGet(dateTime, PS_LIST_HEAD + 1); // Item containing the time
            if (dateItem->type != PS_DATA_STRING) {
                psError(PS_ERR_UNKNOWN, true, "Date is not of type STR.\n");
                return NULL;
            }
            psString dateString = dateItem->data.V; // The string with the date
            int day = 0, month = 0, year = 0;
            if (sscanf(dateString, "%d-%d-%d", &year, &month, &day) != 3 &&
                    sscanf(dateString, "%d/%d/%d", &year, &month, &day) != 3) {
                psError(PS_ERR_UNKNOWN, true, "Unable to read date: %s\n", dateString);
                return NULL;
            }
            if (backwardsTime) {
                // Need to switch days and years
                int temp = day;
                day = year;
                year = temp;
            }
            if (usaTime) {
                // Need to switch everything around.... Yanks!
                int temp = day;
                day = month;
                month = year;
                year = temp;
            }
            if (year < 100) {
                if (pre2000Time) {
                    year += 1900;
                } else {
                    year += 2000;
                }
            }
            sprintf(dateString,"%04d-%02d-%02d", year, month, day);

            psString timeString = NULL; // The string with the time
            if (timeItem->type == PS_DATA_STRING) {
                timeString = timeItem->data.V;
            } else {
                // Assume that time is specified in Second of Day (!)
                double seconds = NAN;
                switch (timeItem->type) {
                    TYPE_CASE(seconds, timeItem, U8);
                    TYPE_CASE(seconds, timeItem, U16);
                    TYPE_CASE(seconds, timeItem, U32);
                    TYPE_CASE(seconds, timeItem, S8);
                    TYPE_CASE(seconds, timeItem, S16);
                    TYPE_CASE(seconds, timeItem, S32);
                    TYPE_CASE(seconds, timeItem, F32);
                    TYPE_CASE(seconds, timeItem, F64);
                default:
                    psError(PS_ERR_UNKNOWN, true, "Time is not of an expected type: %x\n", timeItem->type);
                    return NULL;
                }
                // Now print to timeString as "hh:mm:ss.ss"
                int hours = seconds / 3600;
                seconds -= (double)hours * 3600.0;
                int minutes = seconds / 60;
                seconds -= (double)minutes * 60.0;
                psStringAppend(&timeString, "%02d:%02d:%02f", hours, minutes, seconds);
            }
            psString dateTimeString = NULL;
            psStringAppend(&dateTimeString, "%sT%s", dateString, timeString);
            time = psTimeFromISO(dateTimeString, timeSys);
            psFree(dateTimeString);
            break;
        }
    case PS_DATA_STRING: {
            psString timeString = concept->data.V;   // String with the time
            if (jdTime) {
                double timeValue = strtod (timeString, NULL);
                time = psTimeFromJD(timeValue);
            } else if (mjdTime) {
                double timeValue = strtod (timeString, NULL);
                time = psTimeFromMJD(timeValue);
            } else {
                // It's ISO
                time = psTimeFromISO(timeString, timeSys);
            } // Interpreting the time string
            break;
        }
    case PS_TYPE_F32: {
            double timeValue = (double)concept->data.F32;
            if (jdTime) {
                time = psTimeFromJD(timeValue);
            } else if (mjdTime) {
                time = psTimeFromMJD(timeValue);
            } else {
                psError(PS_ERR_UNKNOWN, true, "Not sure how to parse CELL.TIME (%f) --- trying JD\n",
                        timeValue);
                time = psTimeFromJD(timeValue);
            }
            break;
        }
    case PS_TYPE_F64: {
            double timeValue = (double)concept->data.F64;
            if (jdTime) {
                time = psTimeFromJD(timeValue);
            } else if (mjdTime) {
                time = psTimeFromMJD(timeValue);
            } else {
                psError(PS_ERR_UNKNOWN, true, "Not sure how to parse CELL.TIME (%f) --- trying JD\n",
                        timeValue);
                time = psTimeFromJD(timeValue);
            }
            break;
        }
    default:
        psError(PS_ERR_UNKNOWN, true, "Unable to parse CELL.TIME.\n");
        return NULL;
    }

    psMetadataItem *item = psMetadataItemAllocPtr(pattern->name, PS_DATA_TIME, pattern->comment, time);
    psFree(time);                       // Drop reference
    return item;
}

// Correct a position --- in case the user wants FORTRAN indexing (like the FITS standard...)
static int fortranCorr(psMetadata *cameraFormat, // The camera format description
                       const char *name // Name of concept to check for FORTRAN indexing
                      )
{
    bool mdok = false;                  // Result of MD lookup
    psMetadata *formats = psMetadataLookupMD(&mdok, cameraFormat, "FORMATS");
    if (mdok && formats) {
        psString format = psMetadataLookupStr(&mdok, formats, name);
        if (mdok && strlen(format) > 0 && strcasecmp(format, "FORTRAN") == 0) {
            return 1;
        }
    }
    return 0;
}

psMetadataItem *p_pmConceptParse_Positions(psMetadataItem *concept,
        psMetadataItem *pattern,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);
    assert(cameraFormat);

    int offset = 0;                     // Offset of component (0,0) corner from the parent (0,0) corner

    switch (concept->type) {
        TYPE_CASE(offset, concept, U8);
        TYPE_CASE(offset, concept, U16);
        TYPE_CASE(offset, concept, U32);
        TYPE_CASE(offset, concept, S8);
        TYPE_CASE(offset, concept, S16);
        TYPE_CASE(offset, concept, S32);
    default:
        if (concept->type == PS_TYPE_F32 && concept->data.F32 - (int)concept->data.F32 == 0) {
            offset = concept->data.F32;
        } else if (concept->type == PS_TYPE_F64 && concept->data.F64 - (int)concept->data.F64 == 0) {
            offset = concept->data.F64;
        } else {
            psError(PS_ERR_UNKNOWN, true, "Concept %s is not of integer type, as expected.\n", pattern->name);
            return NULL;
        }
    }
    offset -= fortranCorr(cameraFormat, pattern->name);
    return psMetadataItemAllocS32(pattern->name, pattern->comment, offset);
}


psMetadataItem *p_pmConceptFormat_CELL_TRIMSEC(psMetadataItem *concept,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);

    psRegion *trimsec = concept->data.V; // The trimsec region
    psString trimsecString = psRegionToString(*trimsec);
    psMetadataItem *formatted = psMetadataItemAllocStr(concept->name, concept->comment,
                                trimsecString);
    psFree(trimsecString);
    return formatted;
}

psMetadataItem *p_pmConceptFormat_CELL_BIASSEC(psMetadataItem *concept,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    // Return a metadata item containing a list of metadata items of region strings
    psList *biassecs = concept->data.V; // The biassecs region list
    psListIterator *biassecsIter = psListIteratorAlloc(biassecs, PS_LIST_HEAD, false); // Iterator
    psRegion *region = NULL;            // Region from iteration
    psList *new = psListAlloc(NULL);    // New list containing metadatas
    while ((region = psListGetAndIncrement(biassecsIter))) {
        psString regionString = psRegionToString(*region); // The string region "[x0:x1,y0:y1]"
        psMetadataItem *item = psMetadataItemAllocStr(concept->name, concept->comment, regionString);
        psFree(regionString);           // Drop reference
        psListAdd(new, PS_LIST_TAIL, item);
        psFree(item);                   // Drop reference
    }
    psFree(biassecsIter);
    psMetadataItem *formatted = psMetadataItemAllocPtr(concept->name, PS_DATA_LIST, concept->comment, new);
    psFree(new);                        // Drop reference
    return formatted;
}

// This function actually does both CELL.XBIN and CELL.YBIN if CELL.XBIN and CELL.YBIN are specified by the
// same header.
psMetadataItem *p_pmConceptFormat_CELL_XBIN(psMetadataItem *concept,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);

    psMetadata *translation = psMetadataLookupMD(NULL, cameraFormat, "TRANSLATION");
    bool xBinOK = true, yBinOK = true;  // Status of MD lookups
    psString xKeyword = psMetadataLookupStr(&xBinOK, translation, "CELL.XBIN");
    psString yKeyword = psMetadataLookupStr(&yBinOK, translation, "CELL.YBIN");
    if (xBinOK && yBinOK && strlen(xKeyword) > 0 && strlen(yKeyword) > 0 &&
            strcasecmp(xKeyword, yKeyword) == 0) {
        psMetadataItem *yBinItem = psMetadataLookup(cell->concepts, "CELL.YBIN"); // Binning factor in y
        psString binString = psStringCopy("");
        psStringAppend(&binString, "%d %d", concept->data.S32, yBinItem->data.S32);
        psMetadataItem *binItem = psMetadataItemAllocStr(concept->name, concept->comment, binString);
        psFree(binString);
        return binItem;
    }

    // Otherwise, there's no formatting required
    return psMemIncrRefCounter(concept);
}

// Only need to format if both if CELL.XBIN and CELL.YBIN are not specified by the same header.
psMetadataItem *p_pmConceptFormat_CELL_YBIN(psMetadataItem *concept,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);

    psMetadata *translation = psMetadataLookupMD(NULL, cameraFormat, "TRANSLATION");
    bool xBinOK = true, yBinOK = true;  // Status of MD lookups
    psString xKeyword = psMetadataLookupStr(&xBinOK, translation, "CELL.XBIN");
    psString yKeyword = psMetadataLookupStr(&yBinOK, translation, "CELL.YBIN");
    if (xBinOK && yBinOK && strlen(xKeyword) > 0 && strlen(yKeyword) > 0 &&
            strcasecmp(xKeyword, yKeyword) == 0) {
        // Censor this --- it's already done (though no harm if it's done twice
        return NULL;
    }

    // No formatting required
    return psMemIncrRefCounter(concept);
}


psMetadataItem *p_pmConceptFormat_CELL_TIMESYS(psMetadataItem *concept,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    psString sys = NULL;            // String to store
    switch (concept->data.S32) {
    case PS_TIME_TAI:
        sys = psStringCopy("TAI");
        break;
    case PS_TIME_UTC:
        sys = psStringCopy("UTC");
        break;
    case PS_TIME_UT1:
        sys = psStringCopy("UT1");
        break;
    case PS_TIME_TT:
        sys = psStringCopy("TT");
        break;
    default:
        sys = psStringCopy("Unknown");
    }
    psMetadataItem *newItem = psMetadataItemAllocStr(concept->name, concept->comment, sys);
    psFree(sys);

    return newItem;
}

psMetadataItem *p_pmConceptFormat_CELL_TIME(psMetadataItem *concept,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    psTime *time = concept->data.V;     // The time

    // Work out the format
    bool separateTime = false;          // Are the date and time stored separately?
    bool pre2000Time = false;           // Is the year in pre-2000 format (two digits only)?
    bool backwardsTime = false;         // Is the date stored backwards (DD-MM-YYYY)?
    bool usaTime = false;               // Is the date stored in USA order (MM-DD-YYYY)?
    bool jdTime = false;                // Is the date stored as a JD?
    bool mjdTime = false;               // Is the date stored as a MJD?

    bool mdok = true;                   // Status of MD lookup
    psMetadata *formats = psMetadataLookupMD(&mdok, cameraFormat, "FORMATS"); // The formats
    if (mdok && formats) {
        psString format = psMetadataLookupStr(&mdok, formats, "CELL.TIME"); // The formats for CELL.TIME
        if (mdok && format && strlen(format) > 0) {
            psList *formatList = psStringSplit(format, " ,;", false); // List of formats specified
            psListIterator *formatListIter = psListIteratorAlloc(formatList, PS_LIST_HEAD, false); // Iterator
            while ((format = psListGetAndIncrement(formatListIter))) {
                if (strcasecmp(format, "SEPARATE") == 0) {
                    separateTime = true;
                } else if (strcasecmp(format, "PRE2000") == 0) {
                    pre2000Time = true;
                } else if (strcasecmp(format, "BACKWARDS") == 0) {
                    backwardsTime = true;
                    usaTime = false;
                    mjdTime = false;
                    jdTime = false;
                } else if (strcasecmp(format, "USA") == 0) {
                    usaTime = true;
                    backwardsTime = false;
                    jdTime = false;
                    mjdTime = false;
                } else if (strcasecmp(format, "JD") == 0) {
                    jdTime = true;
                    usaTime = false;
                    backwardsTime = false;
                    mjdTime = false;
                    separateTime = false;
                } else if (strcasecmp(format, "MJD") == 0) {
                    mjdTime = true;
                    usaTime = false;
                    backwardsTime = false;
                    jdTime = false;
                    separateTime = false;
                } else {
                    psLogMsg(__func__, PS_LOG_WARN, "Unrecognised FORMATS option for CELL.TIME: %s --- "
                             "ignored.\n", format);
                }
            }
            psFree(formatListIter);
            psFree(formatList);
        }
    }

    if (separateTime) {
        // We're working with two separate headers --- construct a list with the date and time separately
        psString dateTimeString = psTimeToISO(time); // String representation
        psList *dateTime = psStringSplit(dateTimeString, "T", true);
        psFree(dateTimeString);
        psString dateString = psListGet(dateTime, PS_LIST_HEAD); // The date string
        psString timeString = psListGet(dateTime, PS_LIST_TAIL); // The time string

        // Need to format the strings....
        // XXX: Couldn't be bothered doing these right now
        if (pre2000Time) {
            psError(PS_ERR_UNKNOWN, true, "Don't you realise it's the twenty-first century?\n");
            return NULL;
        }
        if (backwardsTime) {
            psError(PS_ERR_UNKNOWN, true, "You want it BACKWARDS?  Not right now, thanks.\n");
            return NULL;
        }
        if (usaTime) {
            psError(PS_ERR_UNKNOWN, true, "USA?  No OK.\n");
            return NULL;
        }

        psMetadataItem *dateItem = psMetadataItemAllocStr(concept->name, "The date of observation",
                                   dateString);
        psMetadataItem *timeItem = psMetadataItemAllocStr(concept->name, "The time of observation",
                                   timeString);
        psFree(dateString);
        psFree(timeString);

        psListRemove(dateTime, PS_LIST_HEAD);
        psListRemove(dateTime, PS_LIST_HEAD);
        psListAdd(dateTime, PS_LIST_HEAD, dateItem);
        psListAdd(dateTime, PS_LIST_TAIL, timeItem);

        psMetadataItem *item = psMetadataItemAllocPtr(concept->name, PS_DATA_LIST, concept->comment,
                               dateTime);
        return item;
    }
    if (jdTime) {
        double jd = psTimeToMJD(time);
        return psMetadataItemAllocF64(concept->name, concept->comment, jd);
    }
    if (mjdTime) {
        double mjd = psTimeToMJD(time);
        return psMetadataItemAllocF64(concept->name, concept->comment, mjd);
    }

    // If we've gotten this far, so it's straight ISO.
    psString dateTimeString = psTimeToISO(time); // String representation
    psMetadataItem *item = psMetadataItemAllocStr(concept->name, concept->comment, dateTimeString);
    psFree(dateTimeString);
    return item;
}

psMetadataItem *p_pmConceptFormat_Positions(psMetadataItem *concept,
        psMetadata *cameraFormat,
        pmFPA *fpa,
        pmChip *chip,
        pmCell *cell)
{
    assert(concept);
    assert(cameraFormat);

    if (concept->type != PS_TYPE_S32) {
        psError(PS_ERR_UNKNOWN, true, "Concept %s is not of type S32, as expected.\n", concept->name);
        return NULL;
    }
    int offset = concept->data.S32;
    offset += fortranCorr(cameraFormat, concept->name);
    return psMetadataItemAllocS32(concept->name, concept->comment, offset);
}

