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

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

#include "pmConfigMask.h"


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

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

    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, recipe, name);
        if (!mdok) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, false,
                    "Unable to find mask value for %s in MASK recipe", name);
            psFree(names);
            return 0;
        }
        mask |= value;
    }
    psFree(names);

    return mask;
}

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

    psMetadataAddU8 (recipe, PS_LIST_TAIL, maskName, PS_META_REPLACE, "user-defined mask", maskValue);

    return true;
}

// 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;
}

// 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;
}


// 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;
}

// examine named mask values in mask recipe and set the bits for maskValue and markValue
// this function sets an appropriate value for the following required named mask concepts:
// FLAT (used to mark out-of-range corrections in the flat-fielding)
// BLANK (used to mark non-existent pixels)
// SAT (used to mark pixels with values out-of-range on the high end)
// BAD (used to mark pixels with values out-of-range on the low end)
// If there is no explicit value for the above, the 'DETECTOR' and 'RANGE' bits are used
// If these latter do not exist, the value 0x01 is used.
// The values actually used for these names are written back to the config file
bool pmConfigMaskSetBits(psMaskType *outMaskValue, psMaskType *outMarkValue, const pmConfig *config)
{
    PS_ASSERT_PTR_NON_NULL(config, false);

    psMaskType maskValue = 0;

    // mask for generic detector defect
    psMaskType detectorMask = pmConfigMaskGet("DETECTOR", config);
    maskValue |= detectorMask;

    // mask for dark structures
    psMaskType darkMask = pmConfigMaskGet("DARK", config);
    maskValue |= darkMask;

    // mask for non-linear flat regions (default to DETECTOR if not defined)
    psMaskType flatMask = pmConfigMaskGet("FLAT", config);
    if (!flatMask) {
        flatMask = detectorMask;
        pmConfigMaskSet (config, "FLAT", flatMask);
    }
    if (!flatMask) {
        flatMask = 0x01;
        pmConfigMaskSet (config, "FLAT", flatMask);
    }
    maskValue |= flatMask;

    // mask for non-existent data  (default to DETECTOR if not defined)
    psMaskType blankMask = pmConfigMaskGet("BLANK", config);
    if (!blankMask) {
        blankMask = detectorMask;
        pmConfigMaskSet (config, "BLANK", blankMask);
    }
    if (!blankMask) {
        blankMask = 0x01;
        pmConfigMaskSet (config, "BLANK", blankMask);
    }
    maskValue |= blankMask;

    // mask for generic data range errors
    psMaskType rangeMask = pmConfigMaskGet("RANGE", config);
    maskValue |= rangeMask;

    // mask for saturated data  (default to RANGE if not defined)
    psMaskType satMask = pmConfigMaskGet("SAT", config);
    if (!satMask) {
        satMask = rangeMask;
        pmConfigMaskSet (config, "SAT", satMask);
    }
    if (!satMask) {
        satMask = 0x01;
        pmConfigMaskSet (config, "SAT", satMask);
    }
    maskValue |= satMask;

    // mask for below-range data  (default to RANGE if not defined)
    psMaskType badMask = pmConfigMaskGet("BAD", config);
    if (!badMask) {
        badMask = rangeMask;
        pmConfigMaskSet (config, "BAD", badMask);
    }
    if (!badMask) {
        badMask = 0x01;
        pmConfigMaskSet (config, "BAD", badMask);
    }
    maskValue |= badMask;

    // XXX not sure what to do with these
    psMaskType crMask = pmConfigMaskGet("CR", config);
    maskValue |= crMask;

    psMaskType ghostMask = pmConfigMaskGet("GHOST", config);
    maskValue |= ghostMask;

    // 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 config table
    pmConfigMaskSet(config, "MASK.VALUE", maskValue);
    pmConfigMaskSet(config, "MARK.VALUE", markValue);

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

    return true;
}
