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

#include <stdio.h>
#include <string.h>
#include <strings.h>		/* for strn?casecmp */
#include <unistd.h>
#include <libgen.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <glob.h>
#include <pslib.h>
#include "pmConfig.h"
#include "pmConfigRecipes.h"

static bool loadRecipeSite(bool *status, pmConfig *config, psMetadata *source);
static bool loadRecipeCamera(bool *status, pmConfig *config, psMetadata *source);
static bool loadRecipeFromArguments(bool *status, pmConfig *config);
static bool loadRecipeSymbols(bool *status, pmConfig *config);
static bool loadRecipeOptions(bool *status, pmConfig *config);

// use this function to select the options structure for the specified recipe
// add additional command-line options to this metadata (before parsing the camera)
psMetadata *pmConfigRecipeOptions (pmConfig *config, char *recipeName)
{
    bool success;

    // select or create the OPTIONS folder
    psMetadata *options = psMetadataLookupMetadata(&success, config->arguments, "OPTIONS");
    if (!options) {
        options = psMetadataAlloc ();
        success = psMetadataAddPtr (config->arguments, PS_LIST_TAIL, "OPTIONS",  PS_DATA_METADATA, "", options);
	assert (success); // type mismatch : OPTIONS already defined but wrong type
	psFree (options); // drop extra reference
    }

    // look for the recipe defined in recipes
    // if the recipe is already defined in config->arguments:OPTIONS, supplement
    // save the recipe options onto config->arguments:RECIPES
    psMetadata *recipe = psMetadataLookupMetadata(&success, options, recipeName);
    if (!recipe) {
        recipe = psMetadataAlloc();
        success = psMetadataAddPtr(options, PS_LIST_TAIL, recipeName,  PS_DATA_METADATA, "", recipe);
	assert (success); // type mismatch : OPTIONS already defined but wrong type
	psFree (recipe);  // drop extra reference
    }
    return recipe;
}

// this function may be called several times.  it attempts to load the recipe data from one of
// three locations: config->site, config->camera, and argv.  We cannot read the recipes
// from config->camera until a camera has been read BUT, the argv recipes must override the
// camera and site recipes.
bool pmConfigReadRecipes(pmConfig *config, pmRecipeSource source)
{
    bool status;
    PS_ASSERT_PTR_NON_NULL(config, false);

    if (!config->recipes) {
        config->recipes = psMetadataAlloc();
    }

    // Read the recipe file names from the site configuration and camera configuration
    // It is an error for config->site:recipes not to exist.  all programs install their
    // master recipe files in the site:recipe location when they are built.
    if (config->site && (source & PM_RECIPE_SOURCE_SITE)) {
	if (!loadRecipeSite(&status, config, config->site)) {
            psError(PS_ERR_IO, false, "Failed to read recipes from site config");
	    return false;
        } 
	psTrace ("psModules.config", 3, "read recipes from site config");
    }

    // camera-specific recipes are not required : note the absence with a message
    // camera-specific recipes may be read for a specified camera (in pmConfigRead) or
    // for an identified camera (in pmConfigCameraFormatFromHeader).  the second
    // set should not override the first set
    if (config->camera && (source & PM_RECIPE_SOURCE_CAMERA) && !(config->recipesRead & PM_RECIPE_SOURCE_CAMERA)) {
        if (!loadRecipeCamera(&status, config, config->camera)) {
            psError(PS_ERR_IO, false, "Failed to read recipes from camera config");
	    return false;
        } 
	if (status) {
	    psTrace ("psModules.config", 3, "read recipes from camera config");
	} else {
	    psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied by camera config");
	}
    }

    // apply recipes loaded into config->arguments based on command-line arguments
    if (config->arguments && (source & PM_RECIPE_SOURCE_CL)) {
        if (!loadRecipeFromArguments(&status, config)) {
            psError(PS_ERR_IO, false, "Failed to read recipes from command-line arguments");
	    return false;
        } 
	if (status) {
	    psTrace ("psModules.config", 3, "read recipes from command-line arguments");
	} else {
	    psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied on command-line arguments");
	}
        if (!loadRecipeSymbols(&status, config)) {
            psError(PS_ERR_IO, false, "Failed to read recipes from symbolic references");
	    return false;
        } 
	if (status) {
	    psTrace ("psModules.config", 3, "read recipes from symbolic references");
	} else {
	    psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied by symbolic reference");
	}
        if (!loadRecipeOptions(&status, config)) {
            psError(PS_ERR_IO, false, "Failed to read recipes from symbolic references");
	    return false;
        } 
	if (status) {
            psTrace ("psModules.config", 3, "read recipes from command-line arguments");
        } else {
            psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied on command-line arguments");
        }
    }
    return true;
}

// search for options of the form -D KEY VALUE or -D RECIPE:KEY VALUE
bool pmConfigLoadRecipeOptions (int *argc, char **argv, pmConfig *config, char *flag)
{
    bool success;
    int argNum;

    // save the recipes onto config->arguments:RECIPES
    // increment so we can free below (is a NOP if 'options' is NULL)
    psMetadata *options = psMetadataLookupMetadata(&success, config->arguments, "OPTIONS");
    if (!options) {
        options = psMetadataAlloc();
        success = psMetadataAddPtr(config->arguments, PS_LIST_TAIL, "OPTIONS",  PS_DATA_METADATA, "Command-line options specified with -D", options);
	assert (success); // type mismatch : OPTIONS already defined but wrong type
	psFree (options); // drop extra reference
    }

    // -D key value (all added as string)
    while ((argNum = psArgumentGet (*argc, argv, flag))) {
        psArgumentRemove (argNum, argc, argv);

        // do we have enough arguments?
        if (argNum + 1 >= *argc) {
            psError(PS_ERR_IO, true, "insufficient parameters for command-line argument -D");
            return false;
        }

        // is a target recipe specified?
        const char *recipeName = NULL;
        char *key;
        psArray *words = psStringSplitArray(argv[argNum], ":", false);
        switch (words->n) {
        case 1:
            recipeName = config->defaultRecipe;
            if (!config->defaultRecipe) {
                psError(PS_ERR_IO, true, "syntax error in parameter: no default recipe available; must specify recipe");
                return false;
            }
            key = words->data[0];
            break;
        case 2:
            recipeName = words->data[0];
            key = words->data[1];
            break;
        default:
            psError(PS_ERR_IO, true, "syntax error in parameter");
            return false;
        }

        // if this recipe is already defined in options, supplement
        psMetadata *recipe = psMetadataLookupMetadata(&success, options, recipeName);
        if (!recipe) {
            recipe = psMetadataAlloc();
            success = psMetadataAddPtr(options, PS_LIST_TAIL, recipeName,  PS_DATA_METADATA, "", recipe);
	    assert (success); // type mismatch : recipe already defined but wrong type
	    psFree (recipe); // drop extra reference
        }

        bool valid = false;
        if (!strcmp (flag, "-D")) {
            psMetadataAddStr (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", argv[argNum+1]);
            valid = true;
        }
        if (!strcmp (flag, "-Di")) {
            psMetadataAddS32 (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", atoi(argv[argNum+1]));
            valid = true;
        }
        if (!strcmp (flag, "-Df")) {
            psMetadataAddF32 (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", atof(argv[argNum+1]));
            valid = true;
        }
        if (!strcmp (flag, "-Db")) {
            if (!strcasecmp (argv[argNum+1], "true")) {
                psMetadataAddBool (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", true);
            } else {
                psMetadataAddBool (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", false);
            }
            valid = true;
        }
        psFree (words);
        assert (valid);  // flag may be: -D, -Df, -Di, -Db

        psArgumentRemove (argNum, argc, argv);
        psArgumentRemove (argNum, argc, argv);
    }
    return true;
}

// examine command-line arguments for -recipe KEY VALUE
// if VALUE is an existing file, read as metadata and save on config->arguments with name = KEY
// if VALUE is not an exiting file, it is a symbolic lookup: it is saved on config->recipeSymbols
// for later interpolation (pmConfigReadRecipes with option CL)
bool pmConfigLoadRecipeArguments (int *argc, char **argv, pmConfig *config)
{
    bool success; 

    psMetadata *recipes = psMetadataLookupMetadata(&success, config->arguments, "RECIPES");
    if (!recipes) {
        recipes = psMetadataAlloc();
        success = psMetadataAddPtr (config->arguments, PS_LIST_TAIL, "RECIPES",  PS_DATA_METADATA, "", recipes);
	assert (success);
	psFree (recipes);
    }

    // Go through the command-line arguments
    int argNum; // Argument number for "-recipe"
    while ((argNum = psArgumentGet(*argc, argv, "-recipe")) > 0) {
        psArgumentRemove(argNum, argc, argv);
        if (argNum + 1 >= *argc) {
            psError(PS_ERR_IO, false, "-recipe command-line switch provided without the required recipe and source\n");
            return false;
        }

        char *recipeName = psStringCopy(argv[argNum]); // Name of the recipe
        psArgumentRemove(argNum, argc, argv);
        char *recipeSource = psStringCopy(argv[argNum]); // Source of the recipe
        psArgumentRemove(argNum, argc, argv);

        if (access(recipeSource, R_OK) == 0) {
            // The source is a file: load onto config->arguments
            // save the recipes onto config->arguments:RECIPES

            psMetadata *recipe = NULL;      // Recipe from file
            if (pmConfigFileRead(&recipe, recipeSource, "recipe")) {
                psString comment = psStringCopy("Recipe added at command line from ");
                psStringAppend(&comment, "%s", recipeSource);
                psMetadataAdd(recipes, PS_LIST_TAIL, recipeName, PS_DATA_METADATA | PS_META_REPLACE,
                              comment, recipe);
                psFree(comment);
                psFree(recipe);                 // Drop reference
            } else {
                psAbort("error reading config file %s\n", recipeSource);
            }
        } else {
            // Assume it's a symbolic reference to something that's not yet read in.
            // it will be loaded later by pmConfigReadRecipes with option CL
            psMetadataAddStr(config->recipeSymbols, PS_LIST_TAIL, recipeName, PS_META_REPLACE, "",
                             recipeSource);
        }
        psTrace ("psModules.config", 3, "read recipe %s from %s", recipeName, recipeSource);
        psFree(recipeName);
        psFree(recipeSource);
    } // Iterating through the command-line arguments

    return true;
}

// Load the recipe files for SITE : REQUIRED
static bool loadRecipeSite(bool *status,
			   pmConfig *config, // The configuration into which to read the recipes
			   psMetadata *source // The source configuration, from which to read the filenames
    )
{
    assert(status);
    assert(config);
    *status = false;

    if (!source) {
        psError(PS_ERR_IO, true, "The site configuration has not been read --- cannot read recipes from this location.\n");
        config->recipesRead &= ~PM_RECIPE_SOURCE_SITE;
        return false;
    }

    psMetadata *recipes = psMetadataLookupMetadata(NULL, source, "RECIPES"); // The list of recipes
    if (!recipes) {
        psError(PS_ERR_IO, false, "RECIPES not found in the site configuration\n");
        return false;
    }

    // Copy contents of the filenames to config->recipes from the "RECIPES" metadata in the source.
    // We could use psMetadataCopy for this, but it's better to check that everything's of the correct type.
    // If it's not of the correct type, we can tell the user which file it's in, so they can find it easier.
    psMetadataIterator *recipesIter = psMetadataIteratorAlloc(recipes, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *fileItem = NULL;    // MD item containing the filename, from recipe iteration
    while ((fileItem = psMetadataGetAndIncrement(recipesIter))) {
        // type mismatch is a serious error
        if (fileItem->type != PS_DATA_STRING) {
            psError(PS_ERR_IO, true, "%s in site configuration RECIPES is not of type STR", fileItem->name);
	    return false;
        }

        // Read the recipe file. if we fail on a file, give a warning, but continue
        psMetadata *recipe = NULL;
        if (!pmConfigFileRead(&recipe, fileItem->data.V, "recipe")) {
            psError(PS_ERR_IO, false, "Failed to read recipe file %s listed in site configuration\n", (char *)fileItem->data.V);
	    return false;
        }

        // if this named recipe exists, supplement it
	psMetadataAdd(config->recipes, PS_LIST_TAIL, fileItem->name, PS_DATA_METADATA | PS_META_REPLACE,
		      fileItem->comment, recipe);
        psFree(recipe);  // Drop reference
    }
    psFree(recipesIter);
    config->recipesRead |= PM_RECIPE_SOURCE_SITE;

    *status = true;
    return true;
}

// Load the recipe files (valid for SITE | CAMERA)
// each time we load a specific recipe, it overrides the existing metadata for that recipe
// for sourceType == SITE | CAMERA, RECIPES contains a list of files to be read (pmConfigFileRead)
static bool loadRecipeCamera(bool *status, // status variable
			     pmConfig *config, // The configuration into which to read the recipes
			     psMetadata *source // The source configuration, from which to read the filenames
    )
{
    bool success;

    assert(status);
    assert(config);
    *status = false;

    if (!source) {
        psError(PS_ERR_IO, true, "The camera configuration has not been read --- cannot read recipes from this location.\n");
        config->recipesRead &= ~PM_RECIPE_SOURCE_CAMERA;
        return false;
    }

    // it is not necessary to define any local recipes in the camera config
    psMetadata *recipes = psMetadataLookupMetadata(&success, source, "RECIPES"); // The list of recipes
    if (!recipes) {
	psTrace ("psModules.config", 3, "RECIPES not found in the camera configuration\n");
        return true;
    }

    // Copy contents of the filenames to config->recipes from the "RECIPES" metadata in the source.
    // We could use psMetadataCopy for this, but it's better to check that everything's of the correct type.
    // If it's not of the correct type, we can tell the user which file it's in, so they can find it easier.
    psMetadataIterator *recipesIter = psMetadataIteratorAlloc(recipes, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *fileItem = NULL;    // MD item containing the filename, from recipe iteration
    while ((fileItem = psMetadataGetAndIncrement(recipesIter))) {
	char *recipeName = fileItem->name;
	char *recipeFile = fileItem->data.V;

	psTrace("psModules.config", 3, "Supplementing %s from %s within camera configuration.\n", recipeName, recipeFile);

        // type mismatch is a serious error
        if (fileItem->type != PS_DATA_STRING) {
            psAbort("%s in camera configuration RECIPES is not of type STR", recipeName);
        }

        // Read the recipe file. if we fail on a file, give a warning, but continue
        psMetadata *recipe = NULL;
        if (!pmConfigFileRead(&recipe, recipeFile, "recipe")) {
            psError(PS_ERR_IO, false, "Failed to read recipe file %s listed in camera configuration\n", recipeFile);
	    return false;
        }

        // the named recipe must exist; supplement it
	psMetadata *current = psMetadataLookupMetadata(NULL, config->recipes, recipeName);
	if (!current) {
	    psError(PS_ERR_IO, false, "Failed to find recipe for %s in master recipe list", recipeName);
	    psFree(recipe);  // Drop reference
	    return false;
	}

	if (!psMetadataUpdate(current, recipe)) {
	    psError(PS_ERR_IO, false, "Failed to update recipe for %s from camera recipe", recipeName);
	    psFree(recipe);  // Drop reference
	    return false;
	}
        psFree(recipe);  // Drop reference
    }
    psFree(recipesIter);
    config->recipesRead |= PM_RECIPE_SOURCE_CAMERA;
    *status = true;
    return true;
}

// Load the recipes from config->arguments (CL)
// Load the recipes: each time we load a specific recipe, it overrides the metadata
// entries for an existing recipe metadata
static bool loadRecipeFromArguments(bool *status,
				    pmConfig *config // The configuration into which to read the recipes
    )
{
    assert(status);
    assert(config);
    *status = false;

    if (!config->arguments) {
        psTrace("psModules.config", 4, "no config->arguments metadata, nothing to read here");
        return true;
    }

    psMetadata *recipes = psMetadataLookupMetadata(NULL, config->arguments, "RECIPES"); // The list of recipes
    if (!recipes) {
        psTrace("psModules.config", 4, "no RECIPES in config->arguments, nothing to read here");
        return true;
    }

    // Copy the recipes from config->arguments:RECIPES to config->recipes.  supplement existing recipes.
    psMetadataIterator *recipesIter = psMetadataIteratorAlloc(recipes, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *item = NULL;    // MD item containing the filename, from recipe iteration
    while ((item = psMetadataGetAndIncrement(recipesIter))) {
        // type mismatch is a serious error
        if (item->type != PS_DATA_METADATA) {
            psAbort("%s in config arguments RECIPES is not of type METADATA", item->name);
        }
        // increment the ref counter to protect the data

        psMetadata *recipe = item->data.V; // Recipe of interest

        // if this named recipe exists, supplement it
        psMetadata *current = psMetadataLookupMetadata(NULL, config->recipes, item->name);
        if (!current) {
	    psError(PS_ERR_IO, false, "Failed to find recipe for %s in master recipe list", item->name);
	    psFree(recipe);  // Drop reference
	    return false;
	}
	psTrace("psModules.config", 3, "Supplementing %s from arguments.\n", item->name);

        if (!psMetadataUpdate (current, recipe)) {
	    psError(PS_ERR_IO, false, "Failed to update recipe for %s from camera recipe", item->name);
	    return false;
	}
    }
    psFree(recipesIter);
    *status = true;
    return true;
}

// Load the recipes: each time we load a specific recipe, it overrides the metadata
// entries for an existing recipe metadata
static bool loadRecipeSymbols(bool *status,
			      pmConfig *config // The configuration into which to read the recipes
    )
{
    bool found = false;

    assert(status);
    assert(config);
    *status = false;

    // check to see if any symbolic names need to be resolved
    // each entry in recipeSymbols are of the form TARGET=SOURCE where TARGET is an existing
    // recipe MD and REF is a MD to load over that recipe
    psMetadataIterator *iter = psMetadataIteratorAlloc(config->recipeSymbols, PS_LIST_HEAD, NULL);
    psMetadataItem *item = NULL;  // Item containing source, from iteration
    while ((item = psMetadataGetAndIncrement(iter))) {
        assert(item->type == PS_DATA_STRING); // It should be this type: we put it in ourselves
        const char *sourceName = item->data.V; // The name of the symbolic reference
        const char *targetName = item->name;
	psTrace("psModules.config", 3, "Supplementing %s from %s.\n", targetName, sourceName);

	// the target recipe must exist; select it
	psMetadata *targetMD = psMetadataLookupMetadata(&found, config->recipes, targetName);
	if (!targetMD) {
	    psError(PS_ERR_IO, true, "Failed to find recipe for %s in master recipe list", targetName);
	    return false;
	}

        // search for sourceName : it may be in config->recipes or target MD
        psMetadata *sourceMD = NULL;
	sourceMD = psMetadataLookupMetadata(&found, config->recipes, sourceName);
        if (!sourceMD) {
	    sourceMD = psMetadataLookupMetadata(&found, targetMD, sourceName);
	    if (!sourceMD) {
		psError(PS_ERR_IO, false, "Selected symbolic name %s does not exist in recipes", sourceName);
		return false;
	    }
	}

	if (!psMetadataUpdate(targetMD, sourceMD)) {
	    psError(PS_ERR_IO, false, "Failed to update recipe for %s from camera recipe", targetName);
	    return false;
	}
    }
    psFree(iter);
    *status = true;
    return true;
}

// Load the recipe options
// Load the recipes: each time we load a specific recipe, it overrides the metadata
// entries for an existing recipe metadata
static bool loadRecipeOptions(bool *status,
			      pmConfig *config // The configuration into which to read the recipes
    )
{
    bool found;
    assert(status);
    assert(config);
    *status = false;

    if (!config->arguments) {
        psTrace("psModules.config", 4, "no config->arguments metadata, nothing to read here");
        return true;
    }

    psMetadata *recipes = psMetadataLookupMetadata(&found, config->arguments, "OPTIONS"); // The list of recipes
    if (!recipes) {
        psTrace("psModules.config", 4, "no OPTIONS in config->arguments, nothing to read here");
        return true;
    }

    // Copy the recipes from config->arguments:OPTIONS to config->recipes.  supplement existing recipes.
    psMetadataIterator *recipesIter = psMetadataIteratorAlloc(recipes, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *item = NULL;    // MD item containing the filename, from recipe iteration
    while ((item = psMetadataGetAndIncrement(recipesIter))) {
	char *recipeName = item->name;
        psMetadata *recipe = item->data.V;

        // type mismatch is a serious error
        if (item->type != PS_DATA_METADATA) {
            psAbort("%s in config arguments OPTIONS is not of type METADATA", recipeName);
        }

        // if this named recipe exists, supplement it
        psMetadata *current = psMetadataLookupMetadata(NULL, config->recipes, recipeName);
        if (!current) {
	    psError(PS_ERR_IO, false, "Selected recipe %s is not found in camera recipe", recipeName);
	    return false;
	}
        if (!psMetadataUpdate (current, recipe)) {
	    psError(PS_ERR_IO, false, "Failed to update recipe for %s from camera recipe", recipeName);
	    return false;
	}
    }
    psFree(recipesIter);
    *status = true;
    return true;
}
