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

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

#include "pmConfigMask.h"

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Private functions
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

// maskSetValues examine named mask values and set the bits for maskValue and markValue.
// Ensures that the below-named bad mask values are set, and calculates the mask value to catch them all
// Ensure that the below-named other mask values are set (to 0x00 if necessary)

// List of mask names for "bad" (i.e., mask me please) pixels
static const char *badMaskNames[] = { "DETECTOR", // Something is wrong with the detector
                                      "DARK", // Pixel doesn't dark-subtract properly
                                      "FLAT", // Pixel doesn't flat-field properly
                                      "BLANK", // Pixel doesn't contain valid data
                                      "RANGE",// Pixel is out-of-range of linearity
                                      "SAT",  // Pixel is saturated
                                      // "LOW",  // Pixel is low
                                      // "CONV", // Pixel is bad after convolution with a bad pixel
                                      "BAD", // Pixel is low
                                      "BAD.WARP", // Pixel is bad after convolution with a bad pixel
                                      "CR",   // Pixel contains a cosmic ray
                                      "GHOST",// Pixel contains an optical ghost
                                      NULL // End marker
};
// Fallback names in case a bad mask name is not defined
static const char *fallbackMaskNames[] = { NULL, // DETECTOR
                                           "DETECTOR", // DARK
                                           "DETECTOR", // FLAT
                                           "DETECTOR", // BLANK
                                           NULL, // RANGE
                                           "RANGE", // SAT
                                           "RANGE", // LOW
                                           NULL, // CONV
                                           NULL, // CR
                                           NULL, // GHOST
};
// Default values in case a bad mask name and its fallback is not defined
static const psMaskType defaultMask[] = { 0x00, // DETECTOR
                                          0x00, // DARK
                                          0x01, // FLAT
                                          0x01, // BLANK
                                          0x00, // RANGE
                                          0x01, // SAT
                                          0x01, // LOW
                                          0x01, // CONV
                                          0x00, // CR
                                          0x00  // GHOST
};
// Other mask names to ensure exist; these shouldn't be combined in the MASK.VALUE
static const char *otherMaskNames[] = { // "POOR", // Pixel is poor after convolution with a bad pixel
                                        "POOR.WARP", // Pixel is poor after convolution with a bad pixel
                                        NULL // End marker
};

static bool maskSetValues(psMaskType *outMaskValue, // Value of MASK.VALUE, returned
                          psMaskType *outMarkValue, // Value of MARK.VALUE, returned
                          psMetadata *source  // Source of mask bits
                          )
{
    PS_ASSERT_METADATA_NON_NULL(source, false);

    // Ensure all the bad mask names exist, and set the value to catch all bad pixels
    psMaskType maskValue = 0;           // Value to mask to catch all the bad pixels
    for (int i = 0; badMaskNames[i]; i++) {
        const char *name = badMaskNames[i]; // Name for mask
        const char *fallback = fallbackMaskNames[i]; // Fallback for mask

        bool mdok;                      // Status of MD lookup
        psMaskType value = psMetadataLookupU8(&mdok, source, name); // Value of mask
        if (!value) {
            if (fallback) {
                value = psMetadataLookupU8(&mdok, source, fallback);
            }
            if (!value) {
                value = defaultMask[i];
            }
            psMetadataAddU8(source, PS_LIST_TAIL, name, PS_META_REPLACE, NULL, value);
        }
        maskValue |= value;
    }

    // Ensure all the other mask names exist
    for (int i = 0; otherMaskNames[i]; i++) {
        const char *name = otherMaskNames[i]; // Name for mask
        bool mdok;                      // Status of MD lookup
        psMaskType value = psMetadataLookupU8(&mdok, source, name); // Value of mask
        if (!value) {
            psMetadataAddU8(source, PS_LIST_TAIL, name, PS_META_REPLACE, NULL, 0x00);
        }
    }

    // search for an unset bit to use for MARK:
    psMaskType markValue = 0x80;

    int nBits = sizeof(psMaskType) * 8;
    for (int i = 0; !markValue && (i < nBits); i++) {
        if (maskValue & markValue) {
            markValue >>= 1;
        } else {
            markValue = markValue;
        }
    }
    if (!markValue) {
        psError (PS_ERR_UNKNOWN, true, "Unable to define the MARK bit mask: all bits taken!");
        return false;
    }

    // update the list with the results
    psMetadataAddU8(source, PS_LIST_TAIL, "MASK.VALUE", PS_META_REPLACE, NULL, maskValue);
    psMetadataAddU8(source, PS_LIST_TAIL, "MARK.VALUE", PS_META_REPLACE, NULL, markValue);

    if (outMaskValue) {
        *outMaskValue = maskValue;
    }
    if (outMarkValue) {
        *outMarkValue = markValue;
    }

    return true;
}

// Get a mask value by name(s)
static psMaskType maskGet(psMetadata *source, // Source of masks
                          const char *masks // Mask values to get
                          )
{
    psMaskType mask = 0;                // Mask value, to return

    psArray *names = psStringSplitArray(masks, " ,;", false); // Array of symbolic names
    for (int i = 0; i < names->n; i++) {
        const char *name = names->data[i]; // Symbolic name of interest
        bool mdok;                      // Status of MD lookup
        psMaskType value = psMetadataLookupU8(&mdok, source, name);
        if (!mdok) {
            // Try and generate the value if we can
            if (strcmp(name, "MASK.VALUE") == 0 || strcmp(name, "MARK.VALUE") == 0) {
                if (!maskSetValues(NULL, NULL, source)) {
                    psError(PS_ERR_UNKNOWN, false, "Unable to set mask bits.");
                    return 0;
                }
                value = psMetadataLookupU8(&mdok, source, name);
                psAssert(mdok, "Should have generated mask value");
            } else {
                psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Unable to find mask value for %s", name);
                psFree(names);
                return 0;
            }
        }
        mask |= value;
    }
    psFree(names);

    return mask;
}

// Remove from the header keywords starting with the provided string
static int maskRemoveHeader(psMetadata *header, // Header from which to remove keywords
                            const char *start // Remove keywords that start with this string
                            )
{
    psString regex = NULL;              // Regular expression for keywords
    psStringAppend(&regex, "^%s[0-9][0-9]", start);
    psMetadataIterator *iter = psMetadataIteratorAlloc(header, PS_LIST_HEAD, regex); // Iterator
    psFree(regex);
    psMetadataItem *item;               // Item from iteration
    int num = 0;                        // Number of items removed
    while ((item = psMetadataGetAndIncrement(iter))) {
        psMetadataRemoveKey(header, item->name);
        num++;
    }
    psFree(iter);
    return num;
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
// FPA version of mask functions.  These are not ready to go yet.
//////////////////////////////////////////////////////////////////////////////////////////////////////////////

#if 0
bool pmFPAMaskSetValues(psMaskType *outMaskValue, psMaskType *outMarkValue, pmFPA *fpa)
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);

    return maskSetValues(outMaskValue, outMarkValue, fpa->masks);
}

psMaskType pmFPAMaskGet(const pmFPA *fpa, const char *masks, const pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(fpa, 0);
    PS_ASSERT_STRING_NON_EMPTY(masks, 0);
    PS_ASSERT_PTR_NON_NULL(config, 0);

    if (fpa->masks) {
        return maskGet(fpa->masks, masks);
    }
    return pmConfigMaskGet(masks, config);
}

bool pmFPAMaskSet(pmFPA *fpa, const char *maskName, psMaskType maskValue)
{
    PS_ASSERT_PTR_NON_NULL(fpa, 0);
    PS_ASSERT_STRING_NON_EMPTY(maskName, false);

    if (!fpa->masks) {
        fpa->masks = psMetadataAlloc();
    }
    return psMetadataAddU8(fpa->masks, PS_LIST_TAIL, maskName, PS_META_REPLACE, NULL, maskValue);
}

bool pmFPAMaskReadHeader(pmFPA *fpa, const psMetadata *header, pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(fpa, false);
    PS_ASSERT_METADATA_NON_NULL(header, false);
    PS_ASSERT_PTR_NON_NULL(config, false);

    if (!fpa->masks) {
        fpa->masks = psMetadataAlloc();
    }

    bool mdok;                          // Status of MD lookup
    int numMask = psMetadataLookupS32(&mdok, header, "MSKNUM"); // Number of mask values in header
    if (!mdok) {
        if (psMetadataLookupBool(&mdok, config->camera, "MASK.FORCE")) {
            psWarning("No mask values in header.  Assuming MASKS recipe is accurate because of MASK.FORCE");
            numMask = 0;
        } else {
            psError(PS_ERR_UNKNOWN, true, "Unable to find MSKNUM in header.");
            return false;
        }
    }

    char namekey[80];                   // Keyword name for symbolic name of mask entry
    char valuekey[80];                  // Keyword name for value of mask entry
    for (int i = 0; i < numMask; i++) {
        snprintf(namekey,  64, "MSKNAM%02d", i);
        snprintf(valuekey, 64, "MSKVAL%02d", i);

        char *name = psMetadataLookupStr(&mdok, header, namekey);
        if (!mdok || !name) {
            psWarning("Unable to find header keyword %s when parsing mask", namekey);
            continue;
        }
        psU8 bit = psMetadataLookupU8(&mdok, header, valuekey);
        if (!mdok) {
            psWarning("Unable to find header keyword %s when parsing mask", namekey);
            continue;
        }

        // XXX validate that bit is a 2^n value?

        psMetadataItem *item = psMetadataLookup(fpa->masks, name); // Item in recipe with current value
        if (item) {
            psAssert(item->type == PS_TYPE_MASK, "Mask entry %s is not of a mask type (%x)",
                     name, item->type);
            if (item->data.PS_TYPE_MASK_DATA != bit) {
                psWarning("New mask entry %s doesn't match previously loaded entry: %x vs %x",
                          name, bit, item->data.PS_TYPE_MASK_DATA);
            }
        } else {
            psMetadataAddU8(fpa->masks, PS_LIST_TAIL, name, 0, NULL, bit);
        }
    }

    // Now copy everything else from the recipe
    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, "MASKS"); // The recipe
    if (!recipe) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
        return false;
    }

    psMetadataIterator *iter = psMetadataIteratorAlloc(recipe, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *item;               // Item from iteration
    while ((item = psMetadataGetAndIncrement(iter))) {
        if (item->type != PS_TYPE_MASK) {
            psWarning("Recipe mask entry %s is not of a mask type (%x)", item->name, item->type);
            continue;
        }
        if (!psMetadataLookup(fpa->masks, item->name)) {
            psMetadataAddU8(fpa->masks, PS_LIST_TAIL, item->name, 0, item->comment,
                            item->data.PS_TYPE_MASK_DATA);
        }
    }
    psFree(iter);

    return true;
}


bool pmFPAMaskWriteHeader(psMetadata *header, const pmFPA *fpa)
{
    PS_ASSERT_METADATA_NON_NULL(header, false);
    PS_ASSERT_PTR_NON_NULL(fpa, false);

    maskRemoveHeader(header, "MSKNAM");
    maskRemoveHeader(header, "MSKVAL");
    if (psMetadataLookup(header, "MSKNUM")) {
        psMetadataRemoveKey(header, "MSKNUM");
    }

    char namekey[80], valuekey[80];     // Mask name and mask value header keywords
    int numMask = 0;                    // Number of mask entries

    psMetadataIterator *iter = psMetadataIteratorAlloc(fpa->masks, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *item;               // Item from iteration
    while ((item = psMetadataGetAndIncrement(iter))) {
        if (item->type != PS_TYPE_MASK) {
            psWarning("mask recipe entry %s is not of a mask type (%x)", item->name, item->type);
            continue;
        }

        snprintf(namekey,  64, "MSKNAM%02d", numMask);
        snprintf(valuekey, 64, "MSKVAL%02d", numMask);

        psMetadataAddStr(header, PS_LIST_TAIL, namekey, 0, "Bitmask bit name", item->name);
        psMetadataAddU8(header, PS_LIST_TAIL, valuekey, 0, "Bitmask bit value", item->data.PS_TYPE_MASK_DATA);
        numMask++;
    }
    psFree(iter);

    return psMetadataAddS32(header, PS_LIST_TAIL, "MSKNUM", 0, "Number of named mask entries", numMask);
}

#endif


psMaskType pmConfigMaskGet(const char *masks, const pmConfig *config)
{
    psAssert(config, "Require configuration");
    PS_ASSERT_STRING_NON_EMPTY(masks, 0);

    bool mdok;                          // Status of MD lookup
    psMetadata *recipe = psMetadataLookupMetadata(&mdok, config->recipes, "MASKS"); // The recipe
    if (!recipe) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
        return 0;
    }
    return maskGet(recipe, masks);
}


bool pmConfigMaskSet(const pmConfig *config, const char *maskName, psMaskType maskValue)
{
    psAssert(config, "Require configuration");
    PS_ASSERT_STRING_NON_EMPTY(maskName, false);

    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, "MASKS"); // The recipe
    if (!recipe) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
        return false;
    }

    return psMetadataAddU8(recipe, PS_LIST_TAIL, maskName, PS_META_REPLACE, NULL, maskValue);
}


// replace the named masks in the recipe with values in the header:
// replace only the names in the header in the recipe
bool pmConfigMaskReadHeader(pmConfig *config, const psMetadata *header)
{
    PS_ASSERT_PTR_NON_NULL(config, false);
    PS_ASSERT_METADATA_NON_NULL(header, false);

    bool status = false;

    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, "MASKS"); // The recipe
    if (!recipe) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
        return false;
    }

    // MASK.VALUE and MARK.VALUE aren't usually set in the recipe, but may be set in the header: create fake
    // versions so that it won't complain later
    if (!psMetadataLookup(recipe, "MASK.VALUE")) {
        psMetadataAddU8(recipe, PS_LIST_TAIL, "MASK.VALUE", 0, "Bits to mask", 0);
    }
    if (!psMetadataLookup(recipe, "MARK.VALUE")) {
        psMetadataAddU8(recipe, PS_LIST_TAIL, "MARK.VALUE", 0, "Bits for marking", 0);
    }

    int nMask = psMetadataLookupS32(&status, header, "MSKNUM");
    if (!status) {
        if (psMetadataLookupBool(&status, config->camera, "MASK.FORCE")) {
            psWarning("No mask values in header.  Assuming MASKS recipe is accurate because of MASK.FORCE");
            return true;
        }
        psError(PS_ERR_UNKNOWN, true, "Unable to find MSKNUM in header.");
        return false;
    }

    char namekey[80];                   // Keyword name for symbolic name of mask entry
    char valuekey[80];                  // Keyword name for value of mask entry
    for (int i = 0; i < nMask; i++) {
        snprintf(namekey,  64, "MSKNAM%02d", i);
        snprintf(valuekey, 64, "MSKVAL%02d", i);

        char *name = psMetadataLookupStr(&status, header, namekey);
        if (!status || !name) {
            psWarning("Unable to find header keyword %s when parsing mask", namekey);
            continue;
        }
        psU8 bit = psMetadataLookupU8(&status, header, valuekey);
        if (!status) {
            psWarning("Unable to find header keyword %s when parsing mask", namekey);
            continue;
        }

        // XXX validate that bit is a 2^n value?

        psString nameAlready = NULL;    // Name of key with ".ALREADY" added
        psStringAppend(&nameAlready, "%s.ALREADY", name);
        bool already = psMetadataLookupBool(&status, recipe, nameAlready); // Already read this one?

        psMetadataItem *item = psMetadataLookup(recipe, name); // Item in recipe with current value
        if (item && item->type != PS_TYPE_MASK) {
            psWarning("Mask recipe entry is not of a mask type (%x)", item->type);
            item->type = PS_TYPE_MASK;
        }

        if (already) {
            if (item && item->data.U8 != bit) {
                psWarning("New mask recipe entry doesn't match previously loaded entry: %x vs %x",
                          bit, item->data.U8);
            }
        } else {
            psMetadataAddBool(recipe, PS_LIST_TAIL, nameAlready, 0, "Already read this mask value", true);
        }

        if (!item) {
            psWarning("Mask recipe entry %s not in recipe\n", name);
            psMetadataAddU8(recipe, PS_LIST_TAIL, name, 0, "Bitmask bit value", bit);
        } else {
            item->data.U8 = bit;
        }

        psFree(nameAlready);
    }


    return true;
}



// write the named mask bits to the header
bool pmConfigMaskWriteHeader(const pmConfig *config, psMetadata *header)
{
    PS_ASSERT_PTR_NON_NULL(config, false);
    PS_ASSERT_METADATA_NON_NULL(header, false);

    maskRemoveHeader(header, "MSKNAM");
    maskRemoveHeader(header, "MSKVAL");
    if (psMetadataLookup(header, "MSKNUM")) {
        psMetadataRemoveKey(header, "MSKNUM");
    }

    char namekey[80];
    char valuekey[80];

    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, "MASKS"); // The recipe
    if (!recipe) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
        return false;
    }

    int nMask = 0;

    psMetadataIterator *iter = psMetadataIteratorAlloc(recipe, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *item;               // Item from iteration
    while ((item = psMetadataGetAndIncrement(iter))) {
        if (strcmp(item->name + strlen(item->name) - strlen(".ALREADY"), ".ALREADY") == 0) {
            continue;
        }

        if (item->type != PS_DATA_U8) {
            psWarning("mask recipe entry %s is not a bit value\n", item->name);
            continue;
        }

        snprintf(namekey,  64, "MSKNAM%02d", nMask);
        snprintf(valuekey, 64, "MSKVAL%02d", nMask);

        psMetadataAddStr(header, PS_LIST_TAIL, namekey, 0, "Bitmask bit name", item->name);
        psMetadataAddU8(header, PS_LIST_TAIL, valuekey, 0, "Bitmask bit value", item->data.U8);
        nMask++;
    }
    psFree(iter);

    psMetadataAddS32(header, PS_LIST_TAIL, "MSKNUM", 0, "Bitmask bit count", nMask);
    return true;
}


bool pmConfigMaskSetBits(psMaskType *outMaskValue, psMaskType *outMarkValue, const pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, "MASKS"); // The recipe
    if (!recipe) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find MASKS recipe.");
        return false;
    }

    return maskSetValues(outMaskValue, outMarkValue, recipe);
}
