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

#include "pslib.h"

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


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


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 *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_IO, true, "Cannot interpret FPA.RA: %s\n", concept->data.V);
                break;
            }
            coords = abs(big) + (float)medium/60.0 + small/3600.0;
            if (big < 0)
            {
                coords *= -1.0;
            }
        }
        break;
    default:
        psError(PS_ERR_IO, 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 *pmConceptFormat_FPA_Coords(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat)
{
    assert(concept);
    assert(pattern);
    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, 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);
    }

    // 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(pattern->name, pattern->comment, coordString);
    psFree(coordString);

    return coordItem;
}


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

    psRegion *trimsec = psAlloc(sizeof(psRegion)); // Make space for a psRegion (usually passed by value)

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

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

psMetadataItem *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, " ;"); // 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_IO, 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 *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
    if (concept->type == 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_IO, true, "Unable to parse string to get %s: %s\n", pattern->name, binString);
        }
    } else if (concept->type == PS_TYPE_S32) {
        binning = concept->data.S32;
    } else {
        psError(PS_ERR_IO, 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 *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_IO, 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_IO, true, "Can't interpret CELL.TIMESYS --- assuming UTC.\n");
    }

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


psMetadataItem *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;
    }

    // Get format
    psMetadata *formats = psMetadataLookupMD(&mdok, cameraFormat, "FORMATS");
    if (!mdok || !formats) {
        psError(PS_ERR_IO, false, "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_IO, false, "Unable to find CELL.TIME in FORMATS.\n");
        return NULL;
    }

    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_IO, 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", &day, &month, &year) != 3 &&
                    sscanf(dateString, "%d/%d/%d", &day, &month, &year) != 3) {
                psError(PS_ERR_IO, true, "Unable to read date: %s\n", dateString);
                return NULL;
            }
            if (strstr(timeFormat, "BACKWARDS")) {
                int temp = day;
                day = year;
                year = temp;
            }
            if (strstr(timeFormat, "PRE2000") || year < 2000) {
                year += 2000;
            }

            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) {
                case PS_TYPE_S32:
                    seconds = timeItem->data.S32;
                    break;
                case PS_TYPE_F32:
                    seconds = timeItem->data.F32;
                    break;
                case PS_TYPE_F64:
                    seconds = timeItem->data.F64;
                    break;
                default:
                    psError(PS_ERR_IO, 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);
            break;
        }
    case PS_DATA_STRING: {
            psString timeString = concept->data.V;   // String with the time
            if (strcasecmp(timeFormat, "ISO") == 0) {
                // timeString contains an ISO time
                time = psTimeFromISO(timeString, timeSys);
            } else if (strcasecmp(timeFormat, "JD") == 0) {
                double timeValue = strtod (timeString, NULL);
                time = psTimeFromJD(timeValue);
            } else if (strcasecmp(timeFormat, "MJD") == 0) {
                double timeValue = strtod (timeString, NULL);
                time = psTimeFromMJD(timeValue);
            } else {
                psError(PS_ERR_IO, true, "Not sure how to parse CELL.TIME (%s) --- trying "
                        "ISO\n", timeString);
                time = psTimeFromISO(timeString, timeSys);
            } // Interpreting the time string
            break;
        }
    case PS_TYPE_F32: {
            double timeValue = (double)concept->data.F32;
            if (strcasecmp(timeFormat, "JD") == 0) {
                time = psTimeFromJD(timeValue);
            } else if (strcasecmp(timeFormat, "MJD") == 0) {
                time = psTimeFromMJD(timeValue);
            } else {
                psError(PS_ERR_IO, 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 (strcasecmp(timeFormat, "JD") == 0) {
                time = psTimeFromJD(timeValue);
            } else if (strcasecmp(timeFormat, "MJD") == 0) {
                time = psTimeFromMJD(timeValue);
            } else {
                psError(PS_ERR_IO, true, "Not sure how to parse CELL.TIME (%f) --- trying "
                        "JD\n", timeValue);
                time = psTimeFromJD(timeValue);
            }
            break;
        }
    default:
        psError(PS_ERR_IO, true, "Unable to parse CELL.TIME.\n");
        return NULL;
    }

    psMetadataItem *item = psMetadataItemAllocPtr(pattern->name, PS_DATA_TIME, pattern->comment, time);
    psFree(time);
    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 *pmConceptParse_CELL_Positions(psMetadataItem *concept, psMetadataItem *pattern, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
{
    assert(concept);
    assert(cameraFormat);

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


psMetadataItem *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 *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 *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 *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 *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 *pmConceptFormat_CELL_TIME(psMetadataItem *concept, psMetadata *cameraFormat, pmFPA *fpa, pmChip *chip, pmCell *cell)
{
    psTime *time = concept->data.V;     // The time
    psMetadata *formats = psMetadataLookupMD(NULL, cameraFormat, "FORMATS");
    psString format = psMetadataLookupStr(NULL, formats, "CELL.TIME");

    if (strlen(format) == 0 || strcasecmp(format, "ISO") == 0) {
        psString dateTimeString = psTimeToISO(time); // String representation
        psMetadataItem *item = psMetadataItemAllocStr(concept->name, concept->comment, dateTimeString);
        psFree(dateTimeString);
        return item;
    }
    if (strstr(format, "SEPARATE")) {
        // 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");
        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 (strstr(format, "PRE2000")) {
            psError(PS_ERR_IO, true, "Don't you realise it's the twenty-first century?\n");
            return NULL;
        }
        if (strstr(format, "BACKWARDS")) {
            psError(PS_ERR_IO, true, "You want it BACKWARDS?  Not right now, thanks.\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 (strcasecmp(format, "MJD") == 0) {
        double mjd = psTimeToMJD(time);
        return psMetadataItemAllocF64(concept->name, concept->comment, mjd);
    }
    if (strcasecmp(format, "JD") == 0) {
        double jd = psTimeToMJD(time);
        return psMetadataItemAllocF64("CELL.TIME", "JD of observation", jd);
    }

    psError(PS_ERR_IO, true, "Not sure how to format concept CELL.TIME\n");
    return NULL;
}

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

    if (concept->type != PS_TYPE_S32) {
        psError(PS_ERR_IO, 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);
}

