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

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

#include "ppSub.h"

// Print usage information and die
static void usage(const char *program,  // Name of the program
                  psMetadata *arguments, // Command-line arguments
                  pmConfig *config      // Configuration
    )
{
    fprintf(stderr, "\nPan-STARRS PSF-matched image subtraction\n\n");
    fprintf(stderr, "Usage: %s INPUT.fits REFERENCE.fits OUTPUT_ROOT \n"
            "\t[-psf REFERENCE.psf.fits] [-sources REFERENCE.cmf]\n\n"
            "This subtracts the convolved REFERENCE from the INPUT, by default.\n",
            program);
    fprintf(stderr, "\n");
    psArgumentHelp(arguments);
    psFree(config);
    pmConfigDone();
    psLibFinalize();
    exit(PS_EXIT_CONFIG_ERROR);
}

// Get a float-point value from the command-line or recipe, and add it to the arguments
#define VALUE_ARG_RECIPE_FLOAT(ARGNAME, RECIPENAME, TYPE) { \
    ps##TYPE value = psMetadataLookup##TYPE(NULL, config->arguments, ARGNAME); \
    if (isnan(value)) { \
        bool mdok; \
        value = psMetadataLookup##TYPE(&mdok, recipe, RECIPENAME); \
        if (!mdok) { \
            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to find %s in recipe %s", \
                RECIPENAME, PPSUB_RECIPE); \
            goto ERROR; \
        } \
    } \
    psMetadataAdd##TYPE(recipe, PS_LIST_TAIL, RECIPENAME, PS_META_REPLACE, NULL, value); \
}

// Get an integer value from the command-line or recipe, and add it to the arguments
#define VALUE_ARG_RECIPE_INT(ARGNAME, RECIPENAME, TYPE, UNSET) { \
    ps##TYPE value = psMetadataLookup##TYPE(NULL, config->arguments, ARGNAME); \
    if (value == UNSET) { \
        bool mdok; \
        value = psMetadataLookup##TYPE(&mdok, recipe, RECIPENAME); \
        if (!mdok) { \
            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to find %s in recipe %s", \
                RECIPENAME, PPSUB_RECIPE); \
            goto ERROR; \
        } \
    } \
    psMetadataAdd##TYPE(recipe, PS_LIST_TAIL, RECIPENAME, PS_META_REPLACE, NULL, value); \
}

// Get a string value from the command-line and add it to the target
static bool valueArgStr(psMetadata *arguments, // Command-line arguments
                        const char *argName, // Argument name in the command-line arguments
                        const char *mdName, // Name for value in the metadata
                        psMetadata *target // Target metadata to which to add value
                        )
{
    psString value = psMetadataLookupStr(NULL, arguments, argName); // Value of interest
    if (value && strlen(value) > 0) {
        return psMetadataAddStr(target, PS_LIST_TAIL, mdName, PS_META_REPLACE, NULL, value);
    }
    return false;
}

// Get a string value from the command-line or recipe and add it to the target
static bool valueArgRecipeStr(psMetadata *arguments, // Command-line arguments
                              psMetadata *recipe, // Recipe
                              const char *argName, // Argument name in the command-line arguments
                              const char *mdName, // Name for value in the metadata and recipe
                              psMetadata *target // Target metadata to which to add value
                              )
{
    bool mdok;                          // Status of MD lookup
    psString value = psMetadataLookupStr(&mdok, arguments, argName); // Value of interest
    if (!mdok) {
        value = psMetadataLookupStr(&mdok, recipe, mdName);
        if (!mdok) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to find %s in recipe %s",
                    mdName, PPSUB_RECIPE);
            return false;
        }
    }
    return psMetadataAddStr(target, PS_LIST_TAIL, mdName, PS_META_REPLACE, NULL, value);
}

// Get a vector from the command-line or recipe, and add it to the target
static bool vectorArgRecipe(psMetadata *arguments, // Command-line arguments
                            const char *argName, // Argument name in the command-line arguments
                            const psMetadata *recipe, // Recipe
                            const char *recipeName, // Name for value in the recipe
                            psMetadata *target, // Target to which to add value
                            psElemType type // Type for vector
    )
{
    psVector *vector;                   // Vector
    psString string = psMetadataLookupStr(NULL, arguments, argName); // String from arguments
    if (string) {
        psArray *array = psStringSplitArray(string, ", ", false); // Array of strings
        vector = psVectorAlloc(array->n, type);
        for (int i = 0; i < array->n; i++) {
            const char *subString = array->data[i]; // String with a value
            char *end;                  // Ptr to end of string parsed

            switch (type) {
              case PS_TYPE_F32:
                vector->data.F32[i] = strtof(subString, &end);
                break;
              case PS_TYPE_S32: {
                  long value = strtol(subString, &end, 10);
                  if (value > PS_MAX_S32 || value < PS_MIN_S32) {
                      psError(PS_ERR_BAD_PARAMETER_VALUE, true,
                              "%s list includes value beyond S32 representation: %s",
                              argName, string);
                      psFree(vector);
                      return false;
                  }
                  vector->data.S32[i] = value;
                  break;
              }
              default:
                psAbort("Unsupported type: %x\n", type);
            }
            if (end == subString) {
                psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unable to decipher %s list: %s",
                        argName, string);
                psFree(vector);
                return false;
            }
        }
        psFree(array);
    } else {
        vector = psMetadataLookupPtr(NULL, recipe, recipeName);
        if (!psMemCheckVector(vector) || vector->type.type != type) {
            psError(PS_ERR_BAD_PARAMETER_TYPE, false, "%s in recipe %s is not a vector of type F32.",
                    recipeName, PPSUB_RECIPE);
            return false;
        }
        psMemIncrRefCounter(vector);
    }

    psMetadataAddVector(target, PS_LIST_TAIL, recipeName, PS_META_REPLACE, NULL, vector);
    psFree(vector);                     // Drop reference

    return true;
}

// Add a single filename to the arguments as an array, so that it can be used with pmFPAfileBindFromArgs, etc
static void fileList(const char *file, // The symbolic name for the file
                     const char *name, // The name of the file
                     const char *comment, // Description of the file
                     pmConfig *config // Configuration
    )
{
    psArray *files = psArrayAlloc(1); // Array with file names
    files->data[0] = psStringCopy(name);
    psMetadataAddArray(config->arguments, PS_LIST_TAIL, file, 0, comment, files);
    psFree(files);
    return;
}

bool ppSubArgumentsSetup(int argc, char *argv[], pmConfig *config)
{
    assert(config);

    pmConfigFileSetsMD(config->arguments, &argc, argv, "PPSUB.SOURCES", "-sources", NULL);

    psMetadata *arguments = config->arguments; // Command-line arguments
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-inmask", 0, "Input mask image", NULL);
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-inweight", 0, "Input weight image", NULL);
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-refmask", 0, "Referencemask image", NULL);
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-refweight", 0, "Referenceweight image", NULL);
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-stats", 0, "Statistics file", NULL);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-region", 0, "Size of iso-kernel region", NAN);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-size", 0, "Kernel half-size (pixels)", 0);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-order", 0, "Spatial polynomial order", -1);
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-type", 0,
                     "Kernel type (ISIS|POIS|SPAM|FRIES|GUNK|RINGS)", NULL);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-penalty", 0, "Penalty for wideness", NAN);
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-isis-widths", 0,
                     "ISIS Gaussian FWHMs (comma-separated)", NULL);
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-isis-orders", 0,
                     "ISIS polynomial orders (comma-separated)", NULL);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-rings-order", 0, "RINGS polynomial order", -1);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-inner", 0, "SPAM and FRIES inner radius", -1);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-spam-binning", 0, "SPAM kernel binning", 0);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-spacing", 0, "Typical stamp spacing (pixels)", NAN);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-footprint", 0, "Stamp footprint half-size (pixels)", -1);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-threshold", 0, "Minimum threshold for stamps (ADU)", NAN);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-iter", 0, "Number of rejection iterations", -1);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-rej", 0, "Rejection thresold (sigma)", NAN);
    psMetadataAddU8(arguments,  PS_LIST_TAIL, "-mask-bad", 0, "Mask value for bad pixels", 0);
    psMetadataAddU8(arguments,  PS_LIST_TAIL, "-mask-poor", 0, "Mask value for poor pixels", 0);
    psMetadataAddF32(arguments,  PS_LIST_TAIL, "-poor-frac", 0, "Fraction of weight for poor pixels", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-badfrac", 0, "Maximum fraction of bad pixels to accept", 1.0);
    psMetadataAddBool(arguments,  PS_LIST_TAIL, "-reverse", 0, "Reverse sense of subtraction?", false);
    psMetadataAddBool(arguments,  PS_LIST_TAIL, "-generate-mask", 0, "Generate mask if not supplied?", false);
    psMetadataAddStr(arguments,  PS_LIST_TAIL, "-stamps", 0,
                     "Stamps filename; file has x,y on each line", NULL);
    psMetadataAddBool(arguments, PS_LIST_TAIL, "-opt", 0,
                      "Derive optimum parameters for ISIS kernels?", false);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-opt-min", 0, "Minimum value for optimum kernel search", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-opt-max", 0, "Minimum value for optimum kernel search", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-opt-step", 0, "Step value for optimum kernel search", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-opt-tol", 0, "Tolerance for optimum kernel search", NAN);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-opt-order", 0, "Maximum order for optimum kernel search", -1);
    psMetadataAddBool(arguments, PS_LIST_TAIL, "-dual", 0, "Dual convolution", false);
    psMetadataAddBool(arguments, PS_LIST_TAIL, "-renorm", 0, "Renormalise weights?", false);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-renorm-width", 0, "Renormalisation width", 0);
    psMetadataAddBool(arguments, PS_LIST_TAIL, "-photometry", 0, "Perform photometry?", false);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-threads", 0, "Number of threads", 0);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-bin1", 0, "Binning factor for first level", 0);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-bin2", 0, "Binning factor for second level", 0);
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-dumpconfig", 0, "file to dump configuration to", NULL);

    if (argc == 1 || !psArgumentParse(arguments, &argc, argv) || argc != 4) {
        usage(argv[0], arguments, config);
    }

    fileList("INPUT", argv[1], "Name of the input image",     config);
    fileList("REF",   argv[2], "Name of the reference image", config);
    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "Name of the output image", argv[3]);

    const char *inMask = psMetadataLookupStr(NULL, arguments, "-inmask"); // Name of input mask
    if (inMask && strlen(inMask) > 0) {
        fileList("INPUT.MASK", inMask, "Name of the input mask image", config);
    }
    const char *inWeight = psMetadataLookupStr(NULL, arguments, "-inweight"); // Name of input weight
    if (inWeight && strlen(inWeight) > 0) {
        fileList("INPUT.WEIGHT", inWeight, "Name of the input weight image", config);
    }

    const char *refMask = psMetadataLookupStr(NULL, arguments, "-refmask"); // Name of reference mask
    if (refMask && strlen(refMask) > 0) {
        fileList("REF.MASK", refMask, "Name of the reference mask image", config);
    }
    const char *refWeight = psMetadataLookupStr(NULL, arguments, "-refweight"); // Name of ref weight
    if (refWeight && strlen(refWeight) > 0) {
        fileList("REF.WEIGHT", refWeight, "Name of the reference weight image", config);
    }

    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, PPSUB_RECIPE); // Recipe for ppSim
    if (!recipe) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find recipe %s", PPSUB_RECIPE);
        return false;
    }

    if (psMetadataLookupBool(NULL, arguments, "-photometry")) {
        psMetadataAddBool(recipe, PS_LIST_TAIL, "PHOTOMETRY", PS_META_REPLACE, "Perform photometry?", true);
    }

    return true;
}

bool ppSubArgumentsParse(pmConfig *config)
{
    assert(config);

    psMetadata *arguments = config->arguments; // Command-line arguments

    valueArgStr(arguments, "-stats",  "STATS",  arguments);
    valueArgStr(arguments, "-stamps", "STAMPS", arguments);

    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, PPSUB_RECIPE); // Recipe for ppSim
    if (!recipe) {
        psError(PS_ERR_UNEXPECTED_NULL, false, "Unable to find recipe %s", PPSUB_RECIPE);
        goto ERROR;
    }

    VALUE_ARG_RECIPE_FLOAT("-region",     "REGION.SIZE",     F32);
    VALUE_ARG_RECIPE_INT("-size",         "KERNEL.SIZE",     S32, 0);
    VALUE_ARG_RECIPE_INT("-order",        "SPATIAL.ORDER",   S32, -1);
    VALUE_ARG_RECIPE_FLOAT("-spacing",    "STAMP.SPACING",   F32);
    VALUE_ARG_RECIPE_INT("-rings-order",  "RINGS.ORDER",     S32, -1);
    VALUE_ARG_RECIPE_INT("-inner",        "INNER",           S32, -1);
    VALUE_ARG_RECIPE_INT("-spam-binning", "SPAM.BINNING",    S32, 0);
    VALUE_ARG_RECIPE_INT("-footprint",    "STAMP.FOOTPRINT", S32, -1);
    VALUE_ARG_RECIPE_FLOAT("-threshold",  "STAMP.THRESHOLD", F32);
    VALUE_ARG_RECIPE_INT("-iter",         "ITER",            S32, -1);
    VALUE_ARG_RECIPE_FLOAT("-rej",        "REJ",             F32);
    VALUE_ARG_RECIPE_FLOAT("-badfrac",    "BADFRAC",         F32);
    VALUE_ARG_RECIPE_FLOAT("-penalty",    "PENALTY",         F32);
    VALUE_ARG_RECIPE_FLOAT("-poor-frac",  "POOR.FRACTION",   F32);
    VALUE_ARG_RECIPE_INT("-bin1",         "BIN1",            S32, 0);
    VALUE_ARG_RECIPE_INT("-bin2",         "BIN2",            S32, 0);

    valueArgRecipeStr(arguments, recipe, "-mask-in",   "MASK.IN",  recipe);
    valueArgRecipeStr(arguments, recipe, "-mask-bad",  "MASK.BAD",  recipe);
    valueArgRecipeStr(arguments, recipe, "-mask-poor", "MASK.POOR", recipe);

    vectorArgRecipe(arguments, "-isis-widths", recipe, "ISIS.WIDTHS", recipe, PS_TYPE_F32);
    vectorArgRecipe(arguments, "-isis-orders", recipe, "ISIS.ORDERS", recipe, PS_TYPE_S32);

    psVector *widths = psMetadataLookupPtr(NULL, recipe, "ISIS.WIDTHS"); // ISIS Gaussian widths
    psVector *orders = psMetadataLookupPtr(NULL, recipe, "ISIS.ORDERS"); // ISIS Polynomial orders
    if (widths->n != orders->n) {
        psError(PS_ERR_BAD_PARAMETER_SIZE, true, "Size of vectors for ISIS widths and orders do not match.");
        goto ERROR;
    }

    if (psMetadataLookupBool(NULL, arguments, "-opt") || psMetadataLookupBool(NULL, recipe, "OPTIMUM")) {
        psMetadataAddBool(recipe, PS_LIST_TAIL, "OPTIMUM", PS_META_REPLACE,
                          "Derive optimum parameters for ISIS kernels?", true);
        VALUE_ARG_RECIPE_FLOAT("-opt-min", "OPTIMUM.MIN",   F32);
        VALUE_ARG_RECIPE_FLOAT("-opt-max", "OPTIMUM.MAX",   F32);
        VALUE_ARG_RECIPE_FLOAT("-opt-step","OPTIMUM.STEP",  F32);
        VALUE_ARG_RECIPE_FLOAT("-opt-tol", "OPTIMUM.TOL",   F32);
        VALUE_ARG_RECIPE_INT("-opt-order", "OPTIMUM.ORDER", S32, -1);
    }

    psMetadataAddBool(arguments, PS_LIST_TAIL, "REVERSE", 0, "Reverse sense of subtraction",
                      psMetadataLookupBool(NULL, arguments, "-reverse"));
    psMetadataAddBool(recipe, PS_LIST_TAIL, "MASK.GENERATE", PS_META_REPLACE, "Generate mask if not supplied",
                      psMetadataLookupBool(NULL, arguments, "-generate-mask"));
    psMetadataAddBool(recipe, PS_LIST_TAIL, "DUAL", PS_META_REPLACE, "Dual convolution?",
                      psMetadataLookupBool(NULL, arguments, "-dual"));

    if (psMetadataLookupBool(NULL, arguments, "-renorm") ||
        psMetadataLookupBool(NULL, recipe, "RENORM")) {
        psMetadataAddBool(recipe, PS_LIST_TAIL, "RENORM", PS_META_REPLACE, "Renormalise weights?", true);
        VALUE_ARG_RECIPE_INT("-renorm-width", "RENORM.WIDTH", S32, 0);
    }

    // Need to update this because it could have been overwritten by the camera's own recipe
    if (psMetadataLookupBool(NULL, arguments, "-photometry")) {
        psMetadataAddBool(recipe, PS_LIST_TAIL, "PHOTOMETRY", PS_META_REPLACE, "Perform photometry?", true);
    }

    // Translate the kernel type
    psString type = psMetadataLookupStr(NULL, arguments, "-type"); // Name of kernel type
    if (!type || strlen(type) == 0) {
        type = psMetadataLookupStr(NULL, recipe, "KERNEL.TYPE");
        if (!type || strlen(type) == 0) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, false, "Unable to find KERNEL.TYPE specified.");
            goto ERROR;
        }
    }
    psMetadataAddStr(recipe, PS_LIST_TAIL, "KERNEL.TYPE", PS_META_REPLACE, "Type of kernel", type);

    psTrace("ppSub", 1, "Done reading command-line arguments\n");

    // Dump configuration, now that's it's settled
    psBool status;
    psString dump_file =  psMetadataLookupStr(&status, config->arguments, "-dumpconfig");
    if (dump_file) {
        pmConfigCamerasCull(config, NULL);
        pmConfigRecipesCull(config, "PPSUB,PPSTATS,PSPHOT,MASKS");

        pmFPAfile *input = psMetadataLookupPtr(NULL, config->files, "PPSUB.INPUT"); // Input file
        pmConfigDump(config, input->fpa, dump_file);
    }

    return true;

ERROR:
    return false;
}
