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

#include <stdio.h>
#include <string.h>
#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 loadRecipeFiles(pmConfig *config, psMetadata *source, pmRecipeSource type, const char *name);
static bool loadRecipeFromArguments(pmConfig *config);
static bool loadRecipeSymbols(pmConfig *config);
static bool loadRecipeOptions(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)
{

    // if the recipe is already defined in config->arguments, supplement
    // save the recipe options onto config->arguments:RECIPES
    psMetadata *options;
    psMetadataItem *optionsItem = psMetadataLookup(config->arguments, "OPTIONS");
    if (optionsItem) {
        assert(optionsItem->type == PS_DATA_METADATA);
        options = psMemIncrRefCounter(optionsItem->data.V);
    } else {
        options = psMetadataAlloc ();
        psMetadataAddPtr (config->arguments, PS_LIST_TAIL, "OPTIONS",  PS_DATA_METADATA, "", options);
    }

    // look for the recipe defined in recipes
    psMetadata *recipe;
    psMetadataItem *recipeItem = psMetadataLookup(options, recipeName);
    psFree(options);                   // Drop reference
    if (recipeItem) {
        assert(recipeItem->type == PS_DATA_METADATA);
        recipe = psMemIncrRefCounter(recipeItem->data.V);
    } else {
        recipe = psMetadataAlloc();
        psMetadataAddPtr(options, PS_LIST_TAIL, recipeName,  PS_DATA_METADATA, "", recipe);
    }

    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 config->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)
{
    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
    // XXX EAM : I think it should be an error for config->site:recipes not to exist
    // for now, keep this as a warning
    if (config->site && (source & PM_RECIPE_SOURCE_SITE)) {
        if (!loadRecipeFiles(config, config->site, PM_RECIPE_SOURCE_SITE, "site configuration")) {
            psLogMsg ("psModules.config", PS_LOG_WARN, "Failed to read recipes from site config");
        } else {
            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 (!loadRecipeFiles(config, config->camera, PM_RECIPE_SOURCE_CAMERA, "camera configuration")) {
            psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied by camera config");
        } else {
            psTrace ("psModules.config", 3, "read recipes from camera config");
        }
    }

    // apply recipes loaded into config->arguments based on command-line arguments
    if (config->arguments && (source & PM_RECIPE_SOURCE_CL)) {
        if (!loadRecipeFromArguments(config)) {
            psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied on command-line arguments");
        } else {
            psTrace ("psModules.config", 3, "read recipes from command-line arguments");
        }
        if (!loadRecipeSymbols(config)) {
            psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied on command-line arguments");
        } else {
            psTrace ("psModules.config", 3, "read recipes from command-line arguments");
        }
        if (!loadRecipeOptions(config)) {
            psLogMsg ("psModules.config", PS_LOG_DETAIL, "no recipe supplied on command-line arguments");
        } else {
            psTrace ("psModules.config", 3, "read recipes from command-line arguments");
        }
    }
    /*
     * We may have seen real errors, but we also get false status returned by valid conditions (e.g. asking a
     * file for a recipe when none was provided).  For now we'll never signal an error, but this should
     * be reconsidered ASAP.
     */
    psErrorClear();
    return true;
}

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

    int argNum;

    // save the recipes onto config->arguments:RECIPES
    // increment so we can free below (is a NOP is options is NULL)
    psMetadata *options;                // OPTIONS on the arguments
    psMetadataItem *optionsItem = psMetadataLookup(config->arguments, "OPTIONS");
    if (optionsItem) {
        if (optionsItem->type != PS_DATA_METADATA) {
            psAbort(__func__, "OPTIONS is not of type METADATA.");
        }
        options = psMemIncrRefCounter(optionsItem->data.V);
    } else {
        options = psMetadataAlloc();
        psMetadataAddPtr(config->arguments, PS_LIST_TAIL, "OPTIONS",  PS_DATA_METADATA,
                         "Command-line options specified with -D", options);
    }

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

        // do we have enough arguments?
        if (argNum + 1 >= *config->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(config->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;
        psMetadataItem *recipeItem = psMetadataLookup(options, recipeName);
        if (recipeItem) {
            assert(recipeItem->type == PS_DATA_METADATA);
            recipe = psMemIncrRefCounter(recipeItem->data.V);
        } else {
            recipe = psMetadataAlloc();
            psMetadataAddPtr(options, PS_LIST_TAIL, recipeName,  PS_DATA_METADATA, "", recipe);
        }

        bool valid = false;
        if (!strcmp (flag, "-D")) {
            psMetadataAddStr (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", config->argv[argNum+1]);
            valid = true;
        }
        if (!strcmp (flag, "-Di")) {
            psMetadataAddS32 (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", atoi(config->argv[argNum+1]));
            valid = true;
        }
        if (!strcmp (flag, "-Df")) {
            psMetadataAddF32 (recipe, PS_LIST_TAIL, key, PS_META_REPLACE, "", atof(config->argv[argNum+1]));
            valid = true;
        }
        if (!strcmp (flag, "-Db")) {
            if (!strcasecmp (config->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);
        psFree (recipe);
        assert (valid);  // flag may be: -D, -Df, -Di, -Db

        psArgumentRemove (argNum, config->argc, config->argv);
        psArgumentRemove (argNum, config->argc, config->argv);
    }
    psFree (options);
    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 (pmConfig *config)
{
    psMetadata *recipes;                // Recipes in the arguments list
    psMetadataItem *recipesItem = psMetadataLookup(config->arguments, "RECIPES");
    if (recipesItem) {
        if (recipesItem->type != PS_DATA_METADATA) {
            psAbort(__func__, "RECIPES is not of type METADATA.");
        }
        recipes = psMemIncrRefCounter(recipesItem->data.V);
    } else {
        recipes = psMetadataAlloc();
        psMetadataAddPtr (config->arguments, PS_LIST_TAIL, "RECIPES",  PS_DATA_METADATA, "", recipes);
    }


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

        const char *recipeName = psStringCopy(config->argv[argNum]); // Name of the recipe
        psArgumentRemove(argNum, config->argc, config->argv);
        const char *recipeSource = psStringCopy(config->argv[argNum]); // Source of the recipe
        psArgumentRemove(argNum, config->argc, config->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 ("pmConfig.c", "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

    psFree(recipes);                    // Drop reference
    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 loadRecipeFiles(pmConfig *config, // The configuration into which to read the recipes
                            psMetadata *source, // The source configuration, from which to read the filenames
                            pmRecipeSource sourceType, // The source type
                            const char *sourceName // The name of the source, for error messages
                           )
{
    assert(config);
    if ((sourceType != PM_RECIPE_SOURCE_SITE) && (sourceType != PM_RECIPE_SOURCE_CAMERA)) {
        psAbort ("pmConfig", "invalid source for loadRecipes");
    }
    if (!source) {
        psTrace("psModules.config", 4, "The %s has not been read --- cannot read recipes from this "
                "location.\n", sourceName);
        config->recipesRead &= ~sourceType;
        return false;
    }

    psMetadata *recipes = psMetadataLookupMetadata(NULL, source, "RECIPES"); // The list of recipes
    if (!recipes) {
        psLogMsg("psModules.config", PS_LOG_WARN, "RECIPES not found in the %s\n", sourceName);
        config->recipesRead &= ~sourceType;
        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) {
            psAbort ("pmConfig", "%s in %s RECIPES is not of type STR", fileItem->name, sourceName);
        }

        // 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")) {
            psWarning("Failed to read recipe file %s listed in %s\n", (char *)fileItem->data.V, sourceName);
            continue;
        }

        // if this named recipe exists, supplement it
        psMetadata *current = NULL;     // Current, extant recipe of the same name, to be supplemented
        psMetadataItem *currentItem = psMetadataLookup(config->recipes, fileItem->name);
        if (currentItem) {
            if (currentItem->type != PS_DATA_METADATA) {
                psAbort(__func__, "Item from recipes is not of type METADATA.");
            }
            current = psMemIncrRefCounter(currentItem->data.V);
            psTrace("psModules.config", 3, "Supplementing %s from %s within %s.\n", currentItem->name,
                    fileItem->name, sourceName);
        }
        current = psMetadataCopy(current, recipe);
        psFree(recipe);  // Drop reference
        psMetadataAdd(config->recipes, PS_LIST_TAIL, fileItem->name, PS_DATA_METADATA | PS_META_REPLACE,
                      fileItem->comment, current);
        psFree(current);  // Drop reference
    }
    psFree(recipesIter);
    config->recipesRead |= sourceType;
    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(pmConfig *config // The configuration into which to read the recipes
                                   )
{
    assert(config);
    if (!config->arguments) {
        psTrace("psModules.config", 4, "no config->arguments metadata, nothing to read here");
        return false;
    }

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

    // 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 ("pmConfig", "%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 = NULL;
        psMetadataItem *currentItem = psMetadataLookup(config->recipes, item->name);
        if (currentItem) {
            assert(currentItem->type == PS_DATA_METADATA);
            current = psMemIncrRefCounter(currentItem->data.V);
        }
        current = psMetadataCopy (current, recipe);
        psMetadataAdd(config->recipes, PS_LIST_TAIL, item->name, PS_DATA_METADATA | PS_META_REPLACE,
                      "added from command-line recipe", current);
        psFree(current);  // Drop reference
    }
    psFree(recipesIter);
    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(pmConfig *config // The configuration into which to read the recipes
                             )
{
    assert(config);

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

        // search for linkName in config->recipes
        psMetadataItem *recipeItem = psMetadataLookup(config->recipes, symbolRef);
        if (recipeItem) {
            // if this named recipe exists, supplement it
            assert(recipeItem->type == PS_DATA_METADATA);
            psMetadata *recipe = recipeItem->data.V;
            psTrace("psModules.config", 3, "Supplementing %s from %s.\n", symbolName, symbolRef);

            psMetadata *current = NULL;
            psMetadataItem *currentItem = psMetadataLookup(config->recipes, symbolName);
            if (currentItem) {
                assert(currentItem->type == PS_DATA_METADATA);
                current = psMemIncrRefCounter(currentItem->data.V);
            }

            current = psMetadataCopy(current, recipe);
            psMetadataAdd(config->recipes, PS_LIST_TAIL, symbolName, PS_DATA_METADATA | PS_META_REPLACE,
                          symbolRef, current);
            psFree(current);  // Drop reference
            continue;
        }

        // search for linkName in config->recipes:NAME
        psMetadata *current = psMetadataLookupMetadata(NULL, config->recipes, symbolName); // The source
        if (current) {
            psMetadataItem *recipeItem = psMetadataLookup(current, symbolRef);
            if (recipeItem) {
                assert(recipeItem->type == PS_DATA_METADATA);
                psMetadata *recipe = recipeItem->data.V;
                psTrace("psModules.config", 3, "Supplementing %s from %s metadata within %s.\n",
                        symbolName, symbolRef, symbolRef);
                psMetadataCopy (current, recipe);
                psMetadataAdd(config->recipes, PS_LIST_TAIL, symbolName, PS_DATA_METADATA | PS_META_REPLACE,
                              symbolRef, current);
                continue;
            }
        }
        // symbol is not found: ERROR
    }
    psFree(recipesIter);
    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(pmConfig *config // The configuration into which to read the recipes
                             )
{
    assert(config);
    if (!config->arguments) {
        psTrace("psModules.config", 4, "no config->arguments metadata, nothing to read here");
        return false;
    }

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

    // 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))) {
        // type mismatch is a serious error
        if (item->type != PS_DATA_METADATA) {
            psAbort ("pmConfig", "%s in config arguments OPTIONS is not of type METADATA", item->name);
        }
        // increment the ref counter to protect the data
        psMetadata *recipe = psMemIncrRefCounter (item->data.V);

        // if this named recipe exists, supplement it
        psMetadata *current = NULL;
        psMetadataItem *currentItem = psMetadataLookup(config->recipes, item->name);
        if (currentItem) {
            assert(currentItem->type == PS_DATA_METADATA);
            current = psMemIncrRefCounter(currentItem->data.V);
        }
        current = psMetadataCopy (current, recipe);
        psMetadataAdd(config->recipes, PS_LIST_TAIL, item->name, PS_DATA_METADATA | PS_META_REPLACE,
                      "supplement command-line arguments", current);
        psFree(recipe);  // Drop reference
        psFree(current);  // Drop reference
    }
    psFree(recipesIter);
    return true;
}


