/** @file  pmConfig.h
 *
 *  @author PAP, IfA
 *
 *  @version $Revision: 1.7.4.11 $ $Name: not supported by cvs2svn $
 *  @date $Date: 2006-04-13 06:18:22 $
 *
 *  Copyright 2004 Maui High Performance Computing Center, University of Hawaii
 *
 */
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <glob.h>
#include "pslib.h"
#include "pmConfig.h"

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

static psArray *configPath = NULL;

static void configFree(pmConfig *config)
{
    psFree(config->site);
    psFree(config->files);
    psFree(config->camera);
    psFree(config->recipes);
    psFree(config->arguments);
    psFree(config->database);
}

pmConfig *pmConfigAlloc(void)
{
    pmConfig *config = psAlloc(sizeof(pmConfig));
    (void)psMemSetDeallocator(config, (psFreeFunc)configFree);

    // Initialise
    config->site = NULL;
    config->camera = NULL;
    config->recipes = NULL;
    config->arguments = NULL;
    config->database = NULL;

    // the file structure is used to carry pmFPAfiles
    config->files = psMetadataAlloc ();
    return config;
}

void pmConfigDone(void)
{
    psFree(configPath);
}


/** readConfig
 *
 * This function attempts to open the file specified in the parameter list
 * parse it into metadata, then return it as a psMetaData table.
 *
 */
bool readConfig(
    psMetadata **config,                // Config to output
    const char *name,                   // Name of file
    const char *description)            // Description of file
{
    char *realName = NULL;
    unsigned int numBadLines = 0;
    struct stat filestat;

    psLogMsg(__func__, PS_LOG_INFO, "Loading %s configuration from file %s\n",
             description, name);

    uid_t uid = getuid();
    gid_t gid = getgid();

    // we try: name, path[0]/name, path[1]/name, ...
    // find the first existing entry in the path (starting with the bare name)
    realName = psStringCopy (name);
    psTrace (__func__, 5, "trying %s\n", realName);

    int status = stat (realName, &filestat);
    if (status == 0) {
        if ((uid == filestat.st_uid) && (filestat.st_mode & S_IRUSR))
            goto found;
        if ((gid == filestat.st_gid) && (filestat.st_mode & S_IRGRP))
            goto found;
        if (filestat.st_mode & S_IROTH)
            goto found;
    }
    psFree (realName);

    if (configPath == NULL) {
        psError(PS_ERR_IO, false, "Cannot find %s configuration file in path\n", description);
        return false;
    }

    for (int i = 0; i < configPath->n; i++) {
        realName = psStringCopy (configPath->data[i]);
        psStringAppend (&realName, "/%s", name);
        psTrace (__func__, 5, "trying %s\n", realName);

        status = stat (realName, &filestat);
        if (status == 0) {
            if ((uid == filestat.st_uid) && (filestat.st_mode & S_IRUSR))
                goto found;
            if ((gid == filestat.st_gid) && (filestat.st_mode & S_IRGRP))
                goto found;
            if (filestat.st_mode & S_IROTH)
                goto found;
        }
        psFree (realName);
    }

    psError(PS_ERR_IO, false, "Cannot find %s configuration file in path\n", description);
    return false;

found:
    *config = psMetadataConfigParse(NULL, &numBadLines, realName, true);
    if (numBadLines > 0) {
        psLogMsg(__func__, PS_LOG_WARN, "%d bad lines in %s configuration file (%s)\n",
                 description, realName);
    }
    if (!*config) {
        psError(PS_ERR_IO, false, "Unable to read %s configuration from %s\n",
                description, realName);
        psFree (realName);
        return false;
    }

    psFree (realName);
    return true;
}


/******************************************************************************
pmConfigRead(**site, **camera, **recipe, *argc, **argv, char *recipeName)
 
XXX: The log/trace command line options (as processed by psArgumentVerbosity)
must take precedence override the values set here.  This must be, somehow,
coded.
 
XXX: Must load camera and recipe configuration if specified in the command line.
 *****************************************************************************/
pmConfig *pmConfigRead(
    int *argc,
    char **argv)
{
    PS_ASSERT_INT_POSITIVE(*argc, false);
    PS_ASSERT_PTR_NON_NULL(argv, false);

    pmConfig *config = pmConfigAlloc(); // The configuration, containing site, camera and recipes

    //
    // The following section of code attempts to determine which file is
    // the configuration file.  At the end of this code block, the siteName
    // variable will contain the name of the configuration file.
    //
    char *siteName = NULL;
    //
    // First, try command line
    //
    psS32 argNum = psArgumentGet(*argc, argv, "-site");
    if (argNum != 0) {
        //
        // We remove the "-site" argument from argv.  Then
        // we look for the next argument, which should be the filename, and
        // remove it as well.
        //
        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 = psStringCopy(argv[argNum]);
            psArgumentRemove(argNum, argc, argv);
        }
    }
    //
    // Next, try environment variable
    //
    if (!siteName) {
        siteName = getenv(PS_SITE);
        if (siteName) {
            siteName = psStringCopy (siteName);
        }
    }

    //
    // Last chance is ~/.ipprc
    //
    if (!siteName) {
        char *home = getenv("HOME");
        siteName = psStringCopy(home);
        psStringAppend(&siteName, "/%s", PS_DEFAULT_SITE);
    }


    // We have the configuration filename; now we read and parse the config
    // file and store in psMetadata struct site.
    //

    if (!readConfig(&config->site, siteName, "site")) {
        psFree(config);
        return NULL;
    }
    psFree(siteName);

    // define the config-file search path (configPath)
    if (configPath) {
        psFree(configPath);
        configPath = NULL;
    }
    char *path = psMetadataLookupStr(NULL, config->site, "PATH");
    if (path) {
        psList *list = psStringSplit(path, ":");
        configPath = psListToArray(list);
        psFree(list);
    }

    // Next, we do a similar thing for the camera configuration file.  The
    // file is read and parsed into psMetadata struct "camera".
    //
    argNum = psArgumentGet(*argc, argv, "-camera");
    if (argNum > 0) {
        psArgumentRemove(argNum, argc, argv);
        if (argNum >= *argc) {
            psLogMsg(__func__, PS_LOG_WARN,
                     "-camera command-line switch provided without the required filename --- ignored.\n");
        } else {
            psArgumentRemove(argNum, argc, argv);
            readConfig(&config->camera, argv[argNum], "camera");
        }
    }

    // Load the recipes from the camera file, if appropriate
    if (! config->recipes && config->camera) {
        pmConfigReadRecipes(config);
    }


    //
    // We now have the config, camera, and recipe files parsed and stored in
    // metadata.  Now, we can look into the site configuration and do
    // the required stuff.
    //
    bool mdok = true;   // Status of MD lookup result

    //
    // If TIME is specified in the configuration file, then we must initialize
    // with a call to psTimeInitialize.
    //
    psString timeName = psMetadataLookupStr(&mdok, config->site, "TIME");
    if (mdok && timeName) {
        psTrace(__func__, 7, "Initialising psTime with file %s\n", timeName);
        // XXX: PAP had a call to psLibInit is PRODUCTION not set.  Why?
        psTimeInitialize(timeName);
    }


    //
    // If LOGLEVEL is specified in the configuration file, then we must initialize
    // with a call to psLogSetLevel().
    //
    int logLevel = psMetadataLookupS32(&mdok, config->site, "LOGLEVEL");
    if (mdok && logLevel >= 0) {
        psTrace(__func__, 7, "Setting log level to %d\n", logLevel);
        psLogSetLevel(logLevel);
    }


    //
    // If LOGFORMAT is specified in the configuration file, then we must initialize
    // with a call to psLogSetFormat().
    //
    psString logFormat = psMetadataLookupStr(&mdok, config->site, "LOGFORMAT");
    if (mdok && logFormat) {
        psTrace(__func__, 7, "Setting log format to %s\n", logFormat);
        psLogSetFormat(logFormat);
    }


    //
    // If LOGDEST is specified in the configuration file, then we must initialize
    // with a call to psLogSetDestination().
    // XXX: This is not spec'ed in the SDRS.
    //
    psString logDest = psMetadataLookupStr(&mdok, config->site, "LOGDEST");
    if (mdok && logDest) {
        // XXX: Only stdout and stderr are provided for now; this section should be
        // expanded in the future to do files, and perhaps even sockets.
        int logFD = STDIN_FILENO; // a known invalid value
        if (!strcasecmp(logDest, "STDOUT")) {
            logFD = STDOUT_FILENO;
        }
        if (!strcasecmp(logDest, "STDERR")) {
            logFD = STDERR_FILENO;
        }
        if (logFD == STDIN_FILENO) {
            psLogMsg(__func__, PS_LOG_WARN, "Only STDERR and STDOUT currently supported as a log destination.\n");
            logFD = STDERR_FILENO;
        }
        psTrace(__func__, 7, "Setting log destination to STDOUT.\n");
        psLogSetDestination(logFD);
    }

    //
    // If TRACE is specified in the configuration file, then we must initialize
    // with a call to psTraceSetLevel().
    // XXX: This is not spec'ed in the SDRS.
    //
    psMetadata *trace = psMetadataLookupMD(&mdok, config->site, "TRACE");
    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_DATA_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);
            psTraceSetLevel(traceItem->name, traceItem->data.S32);
        }
        psFree(traceIter);
    }

    //
    // Allow command line options to override defaults for logging.
    // XXX: Is it appropriate to use the ArgVerbosity function for this?
    //   A: it removes the options from the command line.
    //   B: will the pmConfigRead function always be called on initialization.
    //
    psS32 saveLogLevel = psLogGetLevel();
    psArgumentVerbosity(argc, argv);
    // XXX: substitute the string for the default log level for "2".
    if (2 == psLogGetLevel()) {
        psLogSetLevel(saveLogLevel);
    }

    return config;
}


bool pmConfigValidateCameraFormat(
    const psMetadata *cameraFormat,
    const psMetadata *header)
{
    // Read the rule for that camera format
    bool mdStatus = true;
    psMetadata *rule = psMetadataLookupMD(&mdStatus, cameraFormat, "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
    while ((ruleItem = psMetadataGetAndIncrement(ruleIter))) {
        // Check for the existence of the rule
        psMetadataItem *headerItem = psMetadataLookup((psMetadata*)header, ruleItem->name);
        if (! headerItem || headerItem->type != ruleItem->type) {
            psFree(ruleIter);
            return false;
        }

        // Check to see if the rule works
        switch (ruleItem->type) {
        case PS_DATA_STRING:
            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) {
                psFree(ruleIter);
                return false;
            }
            break;
        case PS_DATA_S32:
        case PS_DATA_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) {
                psFree(ruleIter);
                return false;
            }
            break;
        case PS_DATA_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) {
                psFree(ruleIter);
                return false;
            }
            break;
        case PS_DATA_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) {
                psFree(ruleIter);
                return false;
            }
            break;
        default:
            psLogMsg(__func__, PS_LOG_WARN, "Ignoring invalid type in metadata: %x\n",
                     ruleItem->type);
        }
    } // Iterating through the RULEs

    psFree(ruleIter);
    return true;
}


static bool formatFromHeader(psMetadata **format, // Format to return
                             psMetadata *camera, // Camera configuration
                             const psMetadata *header, // FITS header
                             const char *cameraName // Name of camera
                            )
{
    psMetadata *testFormat;

    assert(format);
    assert(camera);
    assert(header);

    bool result = false;                // Did we find the first match?

    // Read the list of formats
    bool mdok = true;                   // Status of MD lookup
    psMetadata *formats = psMetadataLookupMD(&mdok, camera, "FORMATS"); // List of formats
    if (!mdok || !formats) {
        psLogMsg(__func__, PS_LOG_WARN, "Unable to find list of FORMATS in camera %s --- ignored\n",
                 cameraName);
        return false;
    }
    // Iterate over the formats
    psMetadataIterator *formatsIter = psMetadataIteratorAlloc(formats, PS_LIST_HEAD, NULL);
    psMetadataItem *formatsItem = NULL; // Item from formats
    while ((formatsItem = psMetadataGetAndIncrement(formatsIter))) {
        if (formatsItem->type != PS_DATA_STRING) {
            psLogMsg(__func__, PS_LOG_WARN, "In camera %s, camera format %s is not of type STR --- "
                     "ignored.\n", cameraName, formatsItem->name);
            continue;
        }
        psTrace(__func__, 5, "Reading camera format for %s...\n", formatsItem->name);
        // unsigned int badLines = 0;  // Number of bad lines in reading camera configuration
        // Format to test against what we've got

        if (!readConfig(&testFormat, formatsItem->data.V, formatsItem->name)) {
            psLogMsg(__func__, PS_LOG_WARN, "trouble reading reading camera format %s\n", formatsItem->name);
            psFree(testFormat);
            continue;
        }

        # if (0)
            psMetadata *testFormat = psMetadataConfigParse(NULL, &badLines, formatsItem->data.V, true);
        if (badLines > 0) {
            psLogMsg(__func__, PS_LOG_WARN, "%d bad lines encountered while reading camera"
                     "format %s\n", badLines, formatsItem->name);
        }
        # endif

        if (pmConfigValidateCameraFormat(testFormat, header)) {
            if (!*format) {
                psLogMsg(__func__, PS_LOG_INFO, "Camera %s, format %s matches header.\n", cameraName,
                         formatsItem->name);
                *format = psMemIncrRefCounter(testFormat);
                result = true;
            } else {
                psLogMsg(__func__, PS_LOG_WARN, "Camera %s, format %s also matches header --- ignored.\n",
                         cameraName, formatsItem->name);
            }
        }
        psFree(testFormat);
    }
    psFree(formatsIter);

    return result;
}

// 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 and format
psMetadata *pmConfigCameraFormatFromHeader(
    pmConfig *config,                   // The configuration
    const psMetadata *header           // The FITS header
)
{
    psMetadata *format = NULL;          // The winning format
    bool mdok = false;                  // Metadata lookup status
    psMetadata *testCamera = NULL;

    // If we don't know what sort of camera we have, we try all that we know
    if (! config->camera) {
        psMetadata *cameras = psMetadataLookupMD(&mdok, config->site, "CAMERAS");
        if (! mdok) {
            psError(PS_ERR_IO, false, "Unable to find CAMERAS in the configuration.\n");
            return false;
        }

        // Iterate over the cameras
        psMetadataIterator *camerasIter = psMetadataIteratorAlloc(cameras, PS_LIST_HEAD, NULL);
        psMetadataItem *camerasItem = NULL; // Item from the metadata
        while ((camerasItem = psMetadataGetAndIncrement(camerasIter))) {
            // Open the camera information
            psTrace(__func__, 3, "Inspecting camera %s (%s)\n", camerasItem->name, camerasItem->comment);
            if (camerasItem->type != PS_DATA_STRING) {
                psLogMsg(__func__, PS_LOG_WARN, "Camera configuration for %s in CAMERAS is not of type STR "
                         "--- ignored.\n", camerasItem->name);
                continue;
            }

            psTrace(__func__, 5, "Reading camera configuration for %s...\n", camerasItem->name);
            // unsigned int badLines = 0;  // Number of bad lines in reading camera configuration
            // Camera to test against what we've got:

            if (!readConfig(&testCamera, camerasItem->data.V, camerasItem->name)) {
                psLogMsg(__func__, PS_LOG_WARN, "trouble reading reading camera configuration %s\n", camerasItem->name);
                psFree(testCamera);
                continue;
            }

            # if (0)
                psMetadata *testCamera = psMetadataConfigParse(NULL, &badLines, camerasItem->data.V, true);
            if (badLines > 0) {
                psLogMsg(__func__, PS_LOG_WARN, "%d bad lines encountered while reading camera"
                         "configuration %s\n", badLines, camerasItem->name);
            }
            # endif

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

            if (formatFromHeader(&format, testCamera, header, camerasItem->name)) {
                config->camera = psMemIncrRefCounter(testCamera);
            }
            psFree(testCamera);
        } // Done looking at all cameras
        psFree(camerasIter);

        if (! config->camera) {
            psError(PS_ERR_IO, true, "Unable to find a camera that matches input FITS header!\n");
            return NULL;
        }

        // Now we have the camera, we can read the recipes
        if (!config->recipes) {
            pmConfigReadRecipes(config);
        }

        return format;
    }

    // Otherwise, try the specific camera
    if (! formatFromHeader(&format, config->camera, header, "specified camera")) {
        psError(PS_ERR_IO, true, "Unable to find a format with the specified camera that matches the "
                "given header.\n");
        return NULL;
    }
    return format;
}

bool pmConfigReadRecipes(
    pmConfig *config
)
{
    PS_ASSERT_PTR_NON_NULL(config, false);
    PS_ASSERT_PTR_NON_NULL(config->camera, false);

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

    bool mdok = true;                   // Status of MD lookup
    psMetadata *recipes = psMetadataLookupMD(&mdok, config->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");
        return false;
    }
    // Go through the recipes and load each one
    psMetadataIterator *recipesIter = psMetadataIteratorAlloc(recipes, PS_LIST_HEAD, NULL); // Iterator
    psMetadataItem *recipesItem = NULL; // Item from iteration
    while ((recipesItem = psMetadataGetAndIncrement(recipesIter))) {
        if (recipesItem->type != PS_DATA_STRING) {
            psLogMsg(__func__, PS_LOG_WARN, "Filename for recipe %s isn't of type STR --- ignored.\n",
                     recipesItem->name);
            continue;
        }

        psMetadata *recipe = NULL;      // Recipe from file
        if (readConfig(&recipe, recipesItem->data.V, "recipe")) {
            psMetadataAdd(config->recipes, PS_LIST_TAIL, recipesItem->name,
                          PS_DATA_METADATA | PS_META_REPLACE, recipesItem->comment, recipe);
        }
        psFree(recipe);                 // Drop reference
    }
    psFree(recipesIter);

    return true;
}

/******************************************************************************
pmConfigDB(*site)
 
XXX: this should allow the option of having NO database server, if chosen by config
XXX: What should we use for the Database namespace in the call to psDBInit()?
This is currently NULL.
 *****************************************************************************/

psDB *pmConfigDB(
    psMetadata *site)
{
    PS_ASSERT_PTR_NON_NULL(site, NULL);
    psBool mdStatus01 = false;
    psBool mdStatus02 = false;
    psBool mdStatus03 = false;

    // XXX leaky strings
    psString dbServer = psMetadataLookupStr(&mdStatus01, site, "DBSERVER");
    psString dbUsername = psMetadataLookupStr(&mdStatus02, site, "DBUSER");
    psString dbPassword = psMetadataLookupStr(&mdStatus03, site, "DBPASSWORD");
    psString dbName = psMetadataLookupStr(&mdStatus01, site, "DBNAME");
    if (!(mdStatus01 & mdStatus02 & mdStatus03)) {
        psLogMsg(__func__, PS_LOG_WARN, "Could not determine database server name, userID, and password from site metadata.\n");
        return(NULL);
    }

    return(psDBInit(dbServer, dbUsername, dbPassword, dbName));
}


bool pmConfigConformHeader(psMetadata *header, // Header to conform
                           const psMetadata *format // Camera format
                          )
{
    bool mdok = true;                   // Status of MD lookup
    psMetadata *rules = psMetadataLookupMD(&mdok, format, "RULE"); // How to identify this format
    if (!mdok || !rules) {
        psError(PS_ERR_IO, false, "Unable to find RULE in camer format.\n");
        return false;
    }

    psMetadataIterator *rulesIter = psMetadataIteratorAlloc(rules, PS_LIST_HEAD, NULL); // Iterator for rules
    psMetadataItem *rulesItem = NULL;   // Item from iteration
    while ((rulesItem = psMetadataGetAndIncrement(rulesIter))) {
        psMetadataItem *newItem = psMetadataItemCopy(rulesItem); // Copy of item
        psMetadataAddItem(header, newItem, PS_LIST_TAIL, PS_META_REPLACE);
        psFree(newItem);                // Drop reference
    }
    psFree(rulesIter);

    return true;
}

// given the 'file' and 'list' words, find the arguments associated with these words
// and interpret them as lists of files.  return an array of the resulting filenames.
psArray *pmConfigFileSets (int *argc, char **argv, char *file, char *list)
{

    int Narg;

    // we load all input files onto a psArray, to be parsed later
    psArray *input = psArrayAlloc (16);
    input->n = 0;

    // load the list of filenames the supplied file (may be a glob: "file*.fits")
    if ((Narg = psArgumentGet (*argc, argv, file))) {
        glob_t globList;
        psArgumentRemove (Narg, argc, argv);
        globList.gl_offs = 0;
        glob (argv[Narg], 0, NULL, &globList);
        for (int i = 0; i < globList.gl_pathc; i++) {
            char *filename = psStringCopy (globList.gl_pathv[i]);
            psArrayAdd (input, 16, filename);
            psFree (filename);
        }
        psArgumentRemove (Narg, argc, argv);
    }

    // load the list from the supplied text file
    if ((Narg = psArgumentGet (*argc, argv, list))) {
        int nItems;
        char line[1024]; // XXX limits the list lines to 1024 chars
        char word[1024];
        char *filename;

        psArgumentRemove (Narg, argc, argv);
        FILE *f = fopen (argv[Narg], "r");
        if (f == NULL) {
            psAbort ("psphot", "unable to open specified list file");
        }
        while (fgets (line, 1024, f) != NULL) {
            nItems = sscanf (line, "%s", word);
            switch (nItems) {
            case 0:
                break;
            case 1:
                filename = psStringCopy (word);
                psArrayAdd (input, 16, filename);
                psFree (filename);
                break;
            default:
                // rigid format, no comments allowed?
                psAbort ("pmConfig", "error parsing input list file");
                break;
            }
        }
        psArgumentRemove (Narg, argc, argv);
    }

    return input;
}

bool pmConfigFileSetsMD (psMetadata *metadata, int *argc, char **argv, char *name, char *file, char *list)
{

    psArray *files = pmConfigFileSets (argc, argv, file, list);
    if (files->n == 0) {
        psFree (files);
        return false;
    }

    psMetadataAddPtr (metadata, PS_LIST_TAIL, name,  PS_DATA_ARRAY, "", files);
    psFree (files);
    return true;
}
