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

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

#include "ppSim.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 data simulator\n\n");
    fprintf(stderr, "Usage: %s -camera CAMERA_NAME\n", program);
    fprintf(stderr, "\n");
    psArgumentHelp(arguments);
    psFree(arguments);
    psFree(config);
    pmConfigDone();
    psLibFinalize();
    exit(PS_EXIT_CONFIG_ERROR);
}

// Get a value from the command-line arguments or the recipe, and add it to the target
bool valueArgRecipe(pmConfig *config,   // Configuration
                    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
    )
{
    float value = psMetadataLookupF32(NULL, arguments, argName); // Value of interest
    if (isnan(value)) {
        bool mdok;                      // Status of MD lookup
        value = psMetadataLookupF32(&mdok, recipe, recipeName);
        if (!mdok) {
            psErrorStackPrint(stderr, "Unable to find %s in recipe %s", recipeName, PPSIM_RECIPE);
            psFree((psPtr)arguments);
            psFree(config);
            exit(PS_EXIT_CONFIG_ERROR);
        }
    }
    return psMetadataAddF32(target, PS_LIST_TAIL, recipeName, 0, NULL, value);
}

void ppSimArguments(int argc, char *argv[], pmConfig *config)
{
    assert(config);

    psMetadata *arguments = psMetadataAlloc(); // Command-line arguments
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-format", 0, "Camera format name", NULL);
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-type", 0, "Exposure type (BIAS|DARK|FLAT|OBJECT)", NULL);
    psMetadataAddStr(arguments, PS_LIST_TAIL, "-filter", 0, "Filter name", NULL);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-exptime", 0, "Exposure time (s)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-biaslevel", 0, "Bias level (e)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-biasrange", 0, "Bias range (e)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-darkrate", 0, "Dark rate (e/s)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-flatsigma", 0, "Flat sigma", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-flatrate", 0, "Flat rate (e/s)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-shuttertime", 0, "Shutter time (s)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-skyrate", 0, "Sky rate (e/s)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-ra", 0, "RA (degrees)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-dec", 0, "Dec (degrees)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-pa", 0, "Position angle (degrees)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-scale", 0, "Plate scale (arcsec/pixel)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-zp", 0, "Photometric zero point", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-seeing", 0, "Seeing FWHM (arcsec)", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-starslum", 0, "Fake star luminosity function slope", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-starsmag", 0, "Brightest magnitude for fake stars", NAN);
    psMetadataAddF32(arguments, PS_LIST_TAIL, "-starsdensity", 0, "Density of fake stars at magnitude", NAN);
    psMetadataAddS32(arguments, PS_LIST_TAIL, "-bin", 0, "Binning in x and y", 1);

    if (!config->camera) {
        psErrorStackPrint(stderr, "A camera name must be specified using the -camera option.");
        usage(argv[0], arguments, config);
    }

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

    psString formatName = psMetadataLookupStr(NULL, arguments, "-format"); // Name of format
    if (formatName) {
        config->formatName = psMemIncrRefCounter(formatName);

        psMetadata *formats = psMetadataLookupMetadata(NULL, config->camera, "FORMATS"); // The camera formats
        if (!formats) {
            psErrorStackPrint(stderr, "Unable to find FORMATS in camera configuration.");
            psFree(arguments);
            psFree(config);
            exit(PS_EXIT_CONFIG_ERROR);
        }
        psMetadata *format = psMetadataLookupMetadata(NULL, formats, formatName); // Format of interest
        if (!format) {
            psErrorStackPrint(stderr, "Unable to find format %s in camera FORMATS.", formatName);
            psFree(arguments);
            psFree(config);
            exit(PS_EXIT_CONFIG_ERROR);
        }
        config->format = psMemIncrRefCounter(format);
    }

    const char *typeStr = psMetadataLookupStr(NULL, arguments, "-type"); // Exposure type
    if (!typeStr) {
        psErrorStackPrint(stderr, "An exposure type must be specified using -type");
        usage(argv[0], arguments, config);
    }
    ppSimType type;                     // Type to simulate
    if (strcasecmp(typeStr, "BIAS") == 0) {
        type = PPSIM_TYPE_BIAS;
    } else if (strcasecmp(typeStr, "DARK") == 0) {
        type = PPSIM_TYPE_DARK;
    } else if (strcasecmp(typeStr, "FLAT") == 0) {
        type = PPSIM_TYPE_FLAT;
    } else if (strcasecmp(typeStr, "OBJECT") == 0) {
        type = PPSIM_TYPE_OBJECT;
    } else {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Unrecognised exposure type: %s", typeStr);
        usage(argv[0], arguments, config);
    }
    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "TYPE", 0, "Exposure type", type);

    const char *filter = psMetadataLookupStr(NULL, arguments, "-filter"); // Filter name
    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "FILTER", 0, "Filter name", filter);

    float expTime;
    if (type == PPSIM_TYPE_BIAS) {
        expTime = 0.0;
    } else {
        expTime = psMetadataLookupF32(NULL, arguments, "-exptime"); // Exposure time
        if (isnan(expTime)) {
            psErrorStackPrint(stderr, "The exposure time must be specified using -exptime");
            usage(argv[0], arguments, config);
        }
    }
    psMetadataAddF32(config->arguments, PS_LIST_TAIL, "EXPTIME", 0, "Exposure time (s)", expTime);

    psMetadata *recipe = psMetadataLookupMetadata(NULL, config->recipes, PPSIM_RECIPE); // Recipe for ppSim
    if (!recipe) {
        psErrorStackPrint(stderr, "Unable to find recipe %s", PPSIM_RECIPE);
        psFree(arguments);
        psFree(config);
        exit(PS_EXIT_CONFIG_ERROR);
    }

    valueArgRecipe(config, arguments, "-biaslevel",    recipe, "BIAS.LEVEL",    config->arguments);
    valueArgRecipe(config, arguments, "-biasrange",    recipe, "BIAS.RANGE",    config->arguments);
    valueArgRecipe(config, arguments, "-darkrate",     recipe, "DARK.RATE",     config->arguments);
    valueArgRecipe(config, arguments, "-flatsigma",    recipe, "FLAT.SIGMA",    config->arguments);
    valueArgRecipe(config, arguments, "-flatrate",     recipe, "FLAT.RATE",     config->arguments);
    valueArgRecipe(config, arguments, "-shuttertime",  recipe, "SHUTTER.TIME",  config->arguments);
    valueArgRecipe(config, arguments, "-skyrate",      recipe, "SKY.RATE",      config->arguments);
    valueArgRecipe(config, arguments, "-starslum",     recipe, "STARS.LUM",     config->arguments);
    valueArgRecipe(config, arguments, "-starsmag",     recipe, "STARS.MAG",     config->arguments);
    valueArgRecipe(config, arguments, "-starsdensity", recipe, "STARS.DENSITY", config->arguments);

    bool mdok;                          // Status of MD lookup
    int biasOrder = psMetadataLookupS32(&mdok, recipe, "BIAS.ORDER"); // Overscan polynomial order
    if (!mdok) {
        psWarning("BIAS.ORDER(S32) is not set in the recipe %s --- assuming %d", PPSIM_RECIPE, biasOrder);
    }
    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "BIAS.ORDER", 0,
                     "Overscan polynomial order", biasOrder);

    int binning = psMetadataLookupS32(NULL, arguments, "-bin"); // Binning in x and y
    if (binning <= 0) {
        psError(PS_ERR_BAD_PARAMETER_VALUE, true, "Binning (%d) is non-positive.", binning);
        usage(argv[0], arguments, config);
    }
    psMetadataAddS32(config->arguments, PS_LIST_TAIL, "BINNING", 0, "Binning in x and y", binning);

    psMetadataAddStr(config->arguments, PS_LIST_TAIL, "OUTPUT", 0, "Name of the output image", argv[1]);

    if (type == PPSIM_TYPE_OBJECT) {
        // Load values required for adding stars
        float scale = psMetadataLookupF32(NULL, arguments, "-scale"); // Plate scale
        float zp = psMetadataLookupF32(NULL, arguments, "-zp"); // Zero point
        float seeing = psMetadataLookupF32(NULL, arguments, "-seeing"); // Zero point
        float ra0 = psMetadataLookupF32(NULL, arguments, "-ra"); // Right Ascension of boresight
        float dec0 = psMetadataLookupF32(NULL, arguments, "-dec"); // Declination of boresight
        float pa = psMetadataLookupF32(NULL, arguments, "-pa"); // Position angle

        if (isnan(ra0) || isnan(dec0) || isnan(pa) || isnan(scale) || isnan(zp) || isnan(seeing)) {
            psError(PS_ERR_BAD_PARAMETER_VALUE, false,
                    "-ra, -dec, -pa, -scale, -zp, -seeing must be specified for OBJECT type");
            usage(argv[0], arguments, config);
        }
        psMetadataAddF32(config->arguments, PS_LIST_TAIL, "SCALE", 0, "Plate scale (arcsec/pix)", scale);
        psMetadataAddF32(config->arguments, PS_LIST_TAIL, "ZEROPOINT", 0, "Photometric zeropoint", zp);
        psMetadataAddF32(config->arguments, PS_LIST_TAIL, "SEEING", 0, "Seeing simga (pix)",
                         seeing / 2.0 / sqrt(2.0 * log(2.0)) / scale);
        psMetadataAddF32(config->arguments, PS_LIST_TAIL, "RA", 0, "Boresight RA (radians)",
                         ra0 * M_PI / 180.0);
        psMetadataAddF32(config->arguments, PS_LIST_TAIL, "DEC", 0, "Boresight Declination (radians)",
                         dec0 * M_PI / 180.0);
        psMetadataAddF32(config->arguments, PS_LIST_TAIL, "PA", 0, "Boresight position angle (radians)",
                         pa * M_PI / 180.0);
    }

    psFree(arguments);
    return;
}
