#include <stdio.h>
#include <string.h>
#include <assert.h>
#include "pslib.h"
#include "psAdditionals.h"
#include "pmConfig.h"

#define PS_SITE "PS_SITE"		// Name of the environment variable containing the site config file
#define DEFAULT_SITE "ipprc.config"	// Default site config file

static bool readConfig(psMetadata **config, // Config to output
		       const char *name,// Name of file
		       const char *description // Description of file
    )
{
    int numBadLines = 0;		// Number of bad lines in config file
    psLogMsg(__func__, PS_LOG_INFO, "Loading %s configuration from file %s\n", description, name);
    *config = psMetadataConfigParse(NULL, &numBadLines, name, true);
    if (numBadLines > 0) {
	psLogMsg(__func__, PS_LOG_WARN, "%d bad lines in %s configuration file (%s)\n", description,
		 name);
    }
    if (! *config) {
	psError(PS_ERR_IO, false, "Unable to read %s configuration from %s\n", description, name);
	return false;
    }

    return true;
}


bool pmConfigRead(psMetadata **site, psMetadata **camera, psMetadata **recipe,
                  int *argc, char **argv, const char *recipeName)
{
    // Make sure we've been given the correct inputs, and that we won't leak memory
    assert(site && *site == NULL);
    assert(camera && *camera == NULL);
    assert(recipe && *recipe == NULL);
    assert(*argc > 0);
    assert(argv);

    char *siteName = NULL;		// Name of the site configuration file

    // First, try command line
    int argNum = 0;			// Number of the site argument
    if (argNum = psArgumentGet(*argc, argv, "-site")) {
	(void)psArgumentRemove(argNum, argc, argv);
	if (argNum >= *argc) {
	    psLogMsg(__func__, PS_LOG_WARN,
		     "-site command-line switch provided without the required filename --- ignored.\n");
	} else {
	    siteName = argv[argNum];
	    (void)psArgumentRemove(argNum, argc, argv);
	}
    }
    // Next, try environment variable
    if (! siteName) {
	siteName = getenv(PS_SITE);
    }
    // Last chance is ~/.ipprc
    bool cleanupSiteName = false;	// Do I have to psFree siteName?
    if (! siteName) {
	siteName = psStringCopy(DEFAULT_SITE);
	cleanupSiteName = true;
    }

    if (! readConfig(site, siteName, "site")) {
	if (cleanupSiteName) {
	    psFree(siteName);
	}
	return false;
    }

    // Next is the camera configuration
    if (argNum = psArgumentGet(*argc, argv, "-camera")) {
	(void)psArgumentRemove(argNum, argc, argv);
	if (argNum >= *argc) {
	    psLogMsg(__func__, PS_LOG_WARN,
		     "-camera command-line switch provided without the required filename --- ignored.\n");
	} else {
	    (void)psArgumentRemove(argNum, argc, argv);
	    (void)readConfig(camera, argv[argNum], "camera");
	}
    }

    // And then the recipe configuration
    if (argNum = psArgumentGet(*argc, argv, "-recipe")) {
	(void)psArgumentRemove(argNum, argc, argv);
	if (argNum >= *argc) {
	    psLogMsg(__func__, PS_LOG_WARN,
		     "-recipe command-line switch provided without the required filename --- ignored.\n");
	} else {
	    (void)psArgumentRemove(argNum, argc, argv);
	    (void)readConfig(recipe, argv[argNum], "recipe");
	}
    }
    // Or, load the recipe from the camera file, if appropriate
    if (! *recipe && *camera && recipeName) {
	*recipe = pmConfigRecipeFromCamera(*camera, recipeName);
    }


    // Now we can look into the site configuration and do the required stuff
    bool mdok = true;			// Status of MD lookup result
    psString timeName = psMetadataLookupString(&mdok, *site, "TIME"); // Name of time file
    if (mdok && timeName) {
	psTrace(__func__, 7, "Initialising psTime with file %s\n", timeName);
#ifdef PRODUCTION
	psTimeInitialize(timeName);
#else
	psLibInit(timeName);
#endif
    }

    int logLevel = psMetadataLookupS32(&mdok, *site, "LOGLEVEL"); // Logging level
    if (mdok && logLevel >= 0) {
	psTrace(__func__, 7, "Setting log level to %d\n", logLevel);
	psLogSetLevel(logLevel);
    }

    psString logFormat = psMetadataLookupString(&mdok, *site, "LOGLEVEL"); // Log format
    if (mdok && logFormat) {
	psTrace(__func__, 7, "Setting log format to %s\n", logFormat);
	psLogSetFormat(logFormat);
    }

    psString logDest = psMetadataLookupString(&mdok, *site, "LOGDEST"); // Log destination
    if (mdok && logDest) {
	// XXX: Only stdout is provided for now; this section should be expanded in the future to do files,
	// and perhaps even sockets.
	if (strcasecmp(logDest, "STDOUT") != 0) {
	    psLogMsg(__func__, PS_LOG_WARN, "Only STDOUT is currently supported as a log destination.\n");
	}
	psTrace(__func__, 7, "Setting log destination to STDOUT.\n");
	psLogSetDestination(0);
    }

    psMetadata *trace = psMetadataLookupMD(&mdok, *site, "TRACE"); // Trace levels
    if (mdok && trace) {
	psMetadataIterator *traceIter = psMetadataIteratorAlloc(trace, PS_LIST_HEAD, NULL); // Iterator
	psMetadataItem *traceItem = NULL; // Item from MD iteration
	while (traceItem = psMetadataGetAndIncrement(traceIter)) {
	    if (traceItem->type != PS_META_S32) {
		psLogMsg(__func__, PS_LOG_WARN, "The level for trace component %s is not of type S32 (%x)\n",
			 traceItem->name, traceItem->type);
		continue;
	    }
	    psTrace(__func__, 7, "Setting trace level for %s to %d\n", traceItem->name, traceItem->data.S32);
	    (void)psTraceSetLevel(traceItem->name, traceItem->data.S32);
	}
	psFree(traceIter);
    }

    if (cleanupSiteName) {
	psFree(siteName);
    }
    return true;
}

bool pmConfigValidateCamera(const psMetadata *camera, const psMetadata *header)
{
    // Read the rule for that camera
    bool mdStatus = true;		// Status of MD lookup
    psMetadata *rule = psMetadataLookupMD(&mdStatus, camera, "RULE");
    if (! mdStatus || ! rule) {
	psLogMsg(__func__, PS_LOG_WARN, "Unable to read rule for camera.\n");
	return false;
    }

    // Apply the rules
    psMetadataIterator *ruleIter = psMetadataIteratorAlloc(rule, PS_LIST_HEAD, NULL); // Rule iterator
    psMetadataItem *ruleItem = NULL;	// Item from the metadata
    bool match = true;			// Does it match?
    while ((ruleItem = psMetadataGetAndIncrement(ruleIter)) && match) {
	// Check for the existence of the rule
	psMetadataItem *headerItem = psMetadataLookup((psMetadata*)header, ruleItem->name);
	if (! headerItem || headerItem->type != ruleItem->type) {
	    match = false;
	}

	// Check to see if the rule works
	switch (ruleItem->type) {
	  case PS_META_STR:
	    psTrace(__func__, 8, "Matching %s: '%s' vs '%s'\n", ruleItem->name,
		    ruleItem->data.V, headerItem->data.V);
	    if (strncmp(ruleItem->data.V, headerItem->data.V,
			    strlen(ruleItem->data.V)) != 0) {
		match = false;
	    }
	    break;
	  case PS_TYPE_S32:
	  case PS_TYPE_BOOL:
	    psTrace(__func__, 8, "Matching %s: %d vs %d\n", ruleItem->name,
		    ruleItem->data.S32, headerItem->data.S32);
	    if (ruleItem->data.S32 != headerItem->data.S32) {
		match = false;
	    }
	    break;
	  case PS_TYPE_F32:
	    psTrace(__func__, 8, "Matching %s: %f vs %f\n", ruleItem->name,
		    ruleItem->data.F32, headerItem->data.F32);
	    if (ruleItem->data.F32 != headerItem->data.F32) {
		match = false;
	    }
	    break;
	  case PS_TYPE_F64:
	    psTrace(__func__, 8, "Matching %s: %g vs %g\n", ruleItem->name,
		    ruleItem->data.F64, headerItem->data.F64);
	    if (ruleItem->data.F64 != headerItem->data.F64) {
		match = false;
	    }
	    break;
	  default:
	    psLogMsg(__func__, PS_LOG_WARN, "Ignoring invalid type in metadata: %x\n",
		     ruleItem->type);
	}
    } // Iterating through the RULEs

    psFree(ruleIter);

    return match;
}
    


// Work out what camera we have, based on the FITS header and a set of rules specified in the IPP
// configuration; return the camera configuration
psMetadata *pmConfigCameraFromHeader(const psMetadata *ipprc, // The IPP configuration
				     const psMetadata *header // The FITS header
    )
{
    bool mdStatus = false;		// Metadata lookup status
    psMetadata *cameras = psMetadataLookupMD(&mdStatus, ipprc, "CAMERAS");
    if (! mdStatus) {
	psError(PS_ERR_IO, false, "Unable to find CAMERAS in the configuration.\n");
	return NULL;
    }

    psMetadata *winner = NULL;	      // The camera configuration whose rule first matches the supplied header

    // Iterate over the cameras
    psMetadataIterator *iterator = psMetadataIteratorAlloc(cameras, PS_LIST_HEAD, NULL); // MD Iterator
    psMetadataItem *cameraItem = NULL; // Item from the metadata
    while (cameraItem = psMetadataGetAndIncrement(iterator)) {
	// Open the camera information
	psTrace(__func__, 3, "Inspecting camera %s (%s)\n", cameraItem->name,
		cameraItem->comment);
	psMetadata *camera = NULL;	// The camera metadata
	if (cameraItem->type == PS_META_META) {
	    camera = psMemIncrRefCounter(cameraItem->data.md);
	} else if (cameraItem->type == PS_META_STR) {
	    psTrace(__func__, 5, "Reading camera configuration for %s...\n", cameraItem->name);
	    int badLines = 0;		// Number of bad lines in reading camera configuration
	    camera = psMetadataConfigParse(NULL, &badLines, cameraItem->data.V, true);
	    if (badLines > 0) {
		psLogMsg(__func__, PS_LOG_WARN, "%d bad lines encountered while reading camera"
			 "configuration %s\n", badLines, cameraItem->name);
	    }
	}

	if (! camera) {
	    psLogMsg(__func__, PS_LOG_WARN, "Unable to interpret camera configuration for %s (%s)\n",
		     cameraItem->name, cameraItem->comment);
	    continue;
	}

	if (pmConfigValidateCamera(camera, header)) {
	    if (! winner) {
		// This is the first match
		winner = psMemIncrRefCounter(camera);
		psLogMsg(__func__, PS_LOG_INFO, "FITS header matches camera %s\n",
			 cameraItem->name);
	    } else {
		// We have a duplicate match
		psLogMsg(__func__, PS_LOG_WARN, "Additional camera found that matches the rules: %s\n",
			 cameraItem->name);
	    }
	} // Done inspecting the camera

	psFree(camera);
	
    } // Done looking at all cameras
    if (! winner) {
	psError(PS_ERR_IO, true, "Unable to find an camera that matches input FITS header!\n");
    }

    psFree(iterator);
    return winner;
}

psMetadata *pmConfigRecipeFromCamera(const psMetadata *camera, const char *recipeName)
{
    assert(camera);
    assert(recipeName);

    psMetadata *recipe = NULL;	// Recipe to read
    bool mdok = true;			// Status of MD lookup
    psMetadata *recipes = psMetadataLookupMD(&mdok, camera, "RECIPES"); // The list of recipes
    if (! mdok || ! recipes) {
	psLogMsg(__func__, PS_LOG_WARN, "RECIPES in the camera configuration file is not of type METADATA\n");
    } else {
	psString recipeFileName = psMetadataLookupString(&mdok, recipes, recipeName);
	(void)readConfig(&recipe, recipeFileName, "recipe");
    }

    return recipe;
}
