#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(const 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.FILTER
psMetadataItem *p_pmConceptParse_FPA_FILTER(const psMetadataItem *concept,
        const psMetadataItem *pattern,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const pmCell *cell)
{
    assert(concept);
    assert(pattern);
    assert(fpa);
    assert(fpa->camera);

    if (concept->type != PS_DATA_STRING) {
        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Type for %s (%x) is not STR\n",
                pattern->name, concept->type);
        return NULL;
    }

    bool mdok;                          // Status of MD lookup
    psMetadata *filters = psMetadataLookupMetadata(&mdok, fpa->camera, "FILTER.ID");
    if (!mdok || !filters) {
        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find FILTER.ID in camera configuration.\n");
        return NULL;
    }

    const char *key = concept->data.str;        // The name to look up
    psString value = psMetadataLookupStr(&mdok, filters, key); // Value to use
    if (!mdok || !value || strlen(value) == 0) {
        psError(PS_ERR_UNEXPECTED_NULL, true,
                "Unable to find %s in FILTER.ID in camera configuration.\n", key);
        return NULL;
    }

    return psMetadataItemAllocStr(pattern->name, pattern->comment, value);
}

psMetadataItem *p_pmConceptFormat_FPA_FILTER(const psMetadataItem *concept,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const pmCell *cell)
{
    assert(concept);
    assert(fpa);
    assert(fpa->camera);

    if (concept->type != PS_DATA_STRING) {
        psError(PS_ERR_BAD_PARAMETER_TYPE, true, "Type for %s (%x) is not STR\n",
                concept->name, concept->type);
        return NULL;
    }

    bool mdok;                          // Status of MD lookup
    psMetadata *filters = psMetadataLookupMetadata(&mdok, fpa->camera, "FILTER.ID");
    if (!mdok || !filters) {
        psError(PS_ERR_UNEXPECTED_NULL, true, "Unable to find FILTER.ID in camera configuration.\n");
        return NULL;
    }

    psMetadataIterator *iter = psMetadataIteratorAlloc(filters, PS_LIST_HEAD, NULL); // Iterator for filters
    psMetadataItem *item;               // Item from iteration
    char *name = NULL;                  // The winning name
    while ((item = psMetadataGetAndIncrement(iter))) {
        if (item->type != PS_DATA_STRING) {
            psWarning("Type for %s (%x) in FILTER.ID in camera configuration is not STR\n",
                      item->name, item->type);
            continue;
        }
        if (strcmp(item->data.str, concept->data.str) == 0) {
            name = item->name;
            break;
        }
    }
    psFree(iter);
    if (!name) {
        psError(PS_ERR_UNEXPECTED_NULL, false,
                "Unable to find any filter matching %s in FILTER.ID in camera configuration.\n",
                concept->data.str);
        return NULL;
    }

    return psMetadataItemAllocStr(concept->name, concept->comment, name);
}


// FPA.RA and FPA.DEC
psMetadataItem *p_pmConceptParse_FPA_Coords(const psMetadataItem *concept,
        const psMetadataItem *pattern,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const 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 %s: %s\n", pattern->name, 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 = psMetadataLookupMetadata(&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(const psMetadataItem *concept,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const pmCell *cell)
{
    assert(concept);
    assert(cameraFormat);

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

    if (!isfinite(coords)) {
        return psMetadataItemAllocF32(concept->name, concept->comment, NAN);
    }

    // How to interpret the coordinates
    bool mdok = true;                   // Status of MD lookup
    psMetadata *formats = psMetadataLookupMetadata(&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;                    // Degrees and minutes
    float small;                        // Seconds
    bool negative = (coords < 0);       // Are we working below zero?
    coords = fabs(coords);
    big = (int)abs(coords);
    coords -= big;
    medium = 60.0 * coords;
    coords -= medium / 60.0;
    small = 3600.0 * coords;
    if (negative) {
        big *= -1;
    }
    psString coordString = NULL;        // String with the coordinates in sexagesimal format
    psStringAppend(&coordString, "%d:%02d:%05.2f", big, medium, small);
    psMetadataItem *coordItem = psMetadataItemAllocStr(concept->name, concept->comment, coordString);
    psFree(coordString);

    return coordItem;
}


psMetadataItem *p_pmConceptParse_CELL_TRIMSEC(const psMetadataItem *concept,
        const psMetadataItem *pattern,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const 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(const psMetadataItem *concept,
        const psMetadataItem *pattern,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const 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(const psMetadataItem *concept,
        const psMetadataItem *pattern,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const 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_TIMESYS(const psMetadataItem *concept,
        const psMetadataItem *pattern,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const 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 %s --- assuming UTC.\n", pattern->name);
    } 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 %s --- assuming UTC.\n", pattern->name);
    }

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

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

    // Need TIMESYS first
    psString timesysName = psStringCopy(pattern->name); // e.g., "CELL.TIME" --> "CELL.TIMESYS"
    psStringSubstitute(&timesysName, "TIMESYS", "TIME");
    bool mdok = false;                  // Result of MD lookup
    psTimeType timeSys = PS_TIME_UTC; // The time system
    if (cell) {
        timeSys = psMetadataLookupS32(&mdok, cell->concepts, timesysName);
    }
    if (!mdok && chip) {
        timeSys = psMetadataLookupS32(&mdok, chip->concepts, timesysName);
    }
    if (!mdok && fpa) {
        timeSys = psMetadataLookupS32(&mdok, fpa->concepts, timesysName);
    }
    if (!mdok || (timeSys == 0xffffffff)) {
        psLogMsg(__func__, PS_LOG_WARN, "Unable to find %s in concepts when parsing %s --- assuming UTC.\n",
                 timesysName, pattern->name);
        timeSys = PS_TIME_UTC;
    }
    psFree(timesysName);

    // Work out how the time is represented
    bool separateTime = false;
    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 yearFirst = false;            // Is the time specified in yearFirst (YYYY-MM-DD) 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 = psMetadataLookupMetadata(&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, pattern->name);
    if (!mdok || strlen(timeFormat) == 0) {
        psError(PS_ERR_UNKNOWN, true, "Unable to find %s in FORMATS.\n", pattern->name);
        return NULL;
    }

    // Parse the time format
    // why should more than one be allowed??
    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, "SEPARATE") == 0) {
            separateTime = true;
        } else if (strcasecmp(timeFormat, "USA") == 0) {
            usaTime = true;
            backwardsTime = false;
            yearFirst = false;
            jdTime = false;
            mjdTime = false;
        } else if (strcasecmp(timeFormat, "BACKWARDS") == 0) {
            backwardsTime = true;
            usaTime = false;
            yearFirst = false;
            jdTime = false;
            mjdTime = false;
        } else if (strcasecmp(timeFormat, "YEAR.FIRST") == 0) {
            yearFirst = true;
            usaTime = false;
            backwardsTime = false;
            jdTime = false;
            mjdTime = false;
        } else if (strcasecmp(timeFormat, "PRE2000") == 0) {
            pre2000Time = true;
        } else if (strcasecmp(timeFormat, "MJD") == 0) {
            mjdTime = true;
            jdTime = false;
            yearFirst = false;
            backwardsTime = false;
            usaTime = false;
        } else if (strcasecmp(timeFormat, "JD") == 0) {
            jdTime = true;
            mjdTime = false;
            yearFirst = false;
            backwardsTime = false;
            usaTime = false;
        } else {
            psError(PS_ERR_UNKNOWN, true, "Unrecognised FORMATS option for %s: %s --- "
                    "ignored.\n", pattern->name, timeFormat);
            psFree(timeFormatsIter);
            psFree(timeFormats);
            return NULL;
        }
    }
    psFree(timeFormatsIter);
    psFree(timeFormats);

    psTime *time = NULL;                // The time
    switch (concept->type) {
    case PS_DATA_LIST: {
            if (!separateTime) {
                psWarning ("DATE and TIME stored separately, but not specified in format\n");
            }
            // 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
            if (!dateItem) {
                psError(PS_ERR_UNKNOWN, true, "Date is not found.\n");
                return NULL;
            }
            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);

            psMetadataItem *timeItem = psListGet(dateTime, PS_LIST_HEAD + 1); // Item containing the time
            if (!timeItem) {
                psError(PS_ERR_UNKNOWN, true, "Time is not found.\n");
                return NULL;
            }
            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 %s (%f) --- trying JD\n",
                        pattern->name, 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 %s (%f) --- trying JD\n",
                        pattern->name, timeValue);
                time = psTimeFromJD(timeValue);
            }
            break;
        }
    default:
        psError(PS_ERR_UNKNOWN, true, "Unable to parse %s.\n", pattern->name);
        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(const 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 = psMetadataLookupMetadata(&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(const psMetadataItem *concept,
        const psMetadataItem *pattern,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const 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);
        #if 0

    case PS_DATA_STRING: {
            // Interpret as a region specifier [x0:x1,y0:y1]
            psRegion region = psRegionFromString(concept->data.V);
            if (strstr(pattern->name, ".X0")) {
                offset = region.x0;
            } else if (strstr(pattern->name, ".Y0")) {
                offset = region.y0;
            } else if (strstr(pattern->name, ".X1")) {
                offset = region.x1;
            } else if (strstr(pattern->name, ".Y1")) {
                offset = region.y1;
            } else {
                psError(PS_ERR_UNKNOWN, true,
                        "Unable to interpret %s because unable to determine if concept is X or Y.\n",
                        pattern->name);
                return NULL;
            }
            break;
        }
        #endif
    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(const psMetadataItem *concept,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const 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(const psMetadataItem *concept,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const 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(const psMetadataItem *concept,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const pmCell *cell)
{
    assert(concept);

    psMetadata *translation = psMetadataLookupMetadata(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 = NULL;
        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 psMetadataItemCopy(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(const psMetadataItem *concept,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const pmCell *cell)
{
    assert(concept);

    psMetadata *translation = psMetadataLookupMetadata(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 psMetadataItemCopy(concept);
}


psMetadataItem *p_pmConceptFormat_TIMESYS(const psMetadataItem *concept,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const 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_TIME(const psMetadataItem *concept,
                                       const psMetadata *cameraFormat,
                                       const pmFPA *fpa,
                                       const pmChip *chip,
                                       const 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 yearFirst = false;

    bool mdok = true;                   // Status of MD lookup
    psMetadata *formats = psMetadataLookupMetadata(&mdok, cameraFormat, "FORMATS"); // The formats
    if (mdok && formats) {
        psString format = psMetadataLookupStr(&mdok, formats, concept->name); // The formats for eg, 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, "USA") == 0) {
                    usaTime = true;
                    backwardsTime = false;
                    jdTime = false;
                    mjdTime = false;
                } else if (strcasecmp(format, "BACKWARDS") == 0) {
                    backwardsTime = true;
                    usaTime = false;
                    mjdTime = false;
                    jdTime = false;
                } else if (strcasecmp(format, "YEAR.FIRST") == 0) {
                    yearFirst = true;
                    usaTime = false;
                    backwardsTime = false;
                    jdTime = false;
                    mjdTime = false;
                } else if (strcasecmp(format, "PRE2000") == 0) {
                    pre2000Time = true;
                } else if (strcasecmp(format, "MJD") == 0) {
                    mjdTime = true;
                    usaTime = false;
                    backwardsTime = false;
                    jdTime = false;
                    separateTime = false;
                } else if (strcasecmp(format, "JD") == 0) {
                    jdTime = true;
                    usaTime = false;
                    backwardsTime = false;
                    mjdTime = false;
                    separateTime = false;
                } else {
                    psLogMsg(__func__, PS_LOG_WARN, "Unrecognised FORMATS option for %s: %s --- "
                             "ignored.\n", concept->name, 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) {
            int day, month, year;
            psTrace ("psModules.concepts", 5, "ISO time has year first, convert to DD-MM-YYYY");
            sscanf (dateString, "%d-%d-%d", &year, &month, &day);
            sprintf (dateString, "%02d-%02d-%04d", day, month, year);
            // XXX fix this for str length
        }
        if (usaTime) {
            int day, month, year;
            psTrace ("psModules.concepts", 5, "ISO time has year first, convert to MM-DD-YYYY");
            sscanf (dateString, "%d-%d-%d", &year, &month, &day);
            sprintf (dateString, "%02d-%02d-%04d", month, day, year);
            // XXX fix this for str length
        }
        if (yearFirst) {
            psTrace ("psModules.concepts", 5, "ISO time has year first, no adjustment needed");
        }

        psMetadataItem *dateItem = psMetadataItemAllocStr(concept->name, "The date of observation",
                                   dateString);
        psMetadataItem *timeItem = psMetadataItemAllocStr(concept->name, "The time of observation",
                                   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);
        psFree (dateItem);
        psFree (timeItem);
        psFree (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(const psMetadataItem *concept,
        const psMetadata *cameraFormat,
        const pmFPA *fpa,
        const pmChip *chip,
        const 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;
    }

    #if 0
    // If both the X0 and Y0 positions are specified by the same header keyword, write both together, as part
    // of the call for the X0 position.
    // This is a bit of a kludge --- we're going to write it out as [x0:0,y0:0].  This means that we will be
    // able to read it back in, but we've destroyed the x1 and y1 if it was present.
    // We *could* attempt to read the header, parse the region, and only update the ones that we're trying
    // to update.  Consider this an upgrade option later.
    // Alternatively, we could add X1 and Y1 concepts, and write the whole lot out together.
    // But until we care about X1 and Y1, it doesn't really matter --- if you want X1 and Y1, look at X0 and
    // Y0 and add NXAIS1 and NAXIS2, respectively....
    if (strstr(concept->name, ".X0")) {
        psString companion = psStringCopy(concept->name); // Companion entry: ".Y" where this one has ".X"
        psStringSubstitute(&companion, ".Y0", ".X0");

        // Look both up in the camera format config
        psMetadata *translation = psMetadataLookupMetadata(NULL, cameraFormat, "TRANSLATION");
        bool xFound = true, yFound = true;  // Status of MD lookups
        psString xKeyword = psMetadataLookupStr(&xFound, translation, concept->name);
        psString yKeyword = psMetadataLookupStr(&yFound, translation, companion);
        if (xFound && yFound && strlen(xKeyword) > 0 && strlen(yKeyword) > 0 &&
                strcasecmp(xKeyword, yKeyword) == 0) {
            psMetadataItem *yItem = psMetadataLookup(cell->concepts, companion); // Corresponding y value

            int x = concept->data.S32 + fortranCorr(cameraFormat, concept->name); // x value
            int y = yItem->data.S32 + fortranCorr(cameraFormat, companion); // y value

            psRegion region = psRegionSet(x, x, y, y);
            psString string = psRegionToString(region);
            psMetadataItem *binItem = psMetadataItemAllocStr(concept->name, concept->comment, string);
            psFree(string);
            psFree(companion);
            return binItem;
        }
        psFree(companion);
    } else if (strstr(concept->name, ".Y0")) {
        psString companion = psStringCopy(concept->name); // Companion entry: ".Y" where this one has ".X"
        psStringSubstitute(&companion, ".X0", ".Y0");

        // Look both up in the camera format config
        psMetadata *translation = psMetadataLookupMetadata(NULL, cameraFormat, "TRANSLATION");
        bool xFound = true, yFound = true;  // Status of MD lookups
        psString xKeyword = psMetadataLookupStr(&xFound, translation, concept->name);
        psString yKeyword = psMetadataLookupStr(&yFound, translation, companion);
        psFree(companion);
        if (xFound && yFound && strlen(xKeyword) > 0 && strlen(yKeyword) > 0 &&
                strcasecmp(xKeyword, yKeyword) == 0) {
            return NULL;                // We did it with the X; don't do anything.
        }
    }
    #endif

    int offset = concept->data.S32;
    offset += fortranCorr(cameraFormat, concept->name);
    return psMetadataItemAllocS32(concept->name, concept->comment, offset);
}

