/*
  Copyright (C) 2011 David Robillard
  Copyright (C) 2013 Paul Davis
  Copyright (C) 2019 Filipe Coelho

  This program is free software; you can redistribute it and/or modify it
  under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation; either version 2.1 of the License, or (at
  your option) any later version.

  This program is distributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with this program; if not, write to the Free Software Foundation,
  Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

#include "JackMetadata.h"

#include "JackClient.h"

#include <string.h>
#include <sys/stat.h>
#include <limits.h>


#define JACK_METADATA_PREFIX "http://jackaudio.org/metadata/"
LIB_EXPORT const char* JACK_METADATA_CONNECTED   = JACK_METADATA_PREFIX "connected";
LIB_EXPORT const char* JACK_METADATA_EVENT_TYPES = JACK_METADATA_PREFIX "event-types";
LIB_EXPORT const char* JACK_METADATA_HARDWARE    = JACK_METADATA_PREFIX "hardware";
LIB_EXPORT const char* JACK_METADATA_ICON_LARGE  = JACK_METADATA_PREFIX "icon-large";
LIB_EXPORT const char* JACK_METADATA_ICON_NAME   = JACK_METADATA_PREFIX "icon-name";
LIB_EXPORT const char* JACK_METADATA_ICON_SMALL  = JACK_METADATA_PREFIX "icon-small";
LIB_EXPORT const char* JACK_METADATA_ORDER       = JACK_METADATA_PREFIX "order";
LIB_EXPORT const char* JACK_METADATA_PORT_GROUP  = JACK_METADATA_PREFIX "port-group";
LIB_EXPORT const char* JACK_METADATA_PRETTY_NAME = JACK_METADATA_PREFIX "pretty-name";
LIB_EXPORT const char* JACK_METADATA_SIGNAL_TYPE = JACK_METADATA_PREFIX "signal-type";
#undef JACK_METADATA_PREFIX

namespace Jack
{

JackMetadata::JackMetadata(bool isEngine)
#if HAVE_DB
    : fDB(NULL), fDBenv(NULL), fIsEngine(isEngine)
#endif
{
    PropertyInit();
}

JackMetadata::~JackMetadata()
{
#if HAVE_DB
    char dbpath[PATH_MAX + 1];

    if (fDB) {
        fDB->close (fDB, 0);
        fDB = NULL;
    }
    if (fDBenv) {
        fDBenv->close (fDBenv, 0);
        fDBenv = NULL;
    }

    if (fIsEngine)
    {
        // cleanup after libdb, nasty!
        snprintf (dbpath, sizeof(dbpath), "%s/jack_db/metadata.db", fDBFilesDir);
        remove (dbpath);

        snprintf (dbpath, sizeof(dbpath), "%s/jack_db/__db.001", fDBFilesDir);
        remove (dbpath);

        snprintf (dbpath, sizeof(dbpath), "%s/jack_db/__db.002", fDBFilesDir);
        remove (dbpath);

        snprintf (dbpath, sizeof(dbpath), "%s/jack_db/__db.003", fDBFilesDir);
        remove (dbpath);

        // remove our custom dir
        snprintf (dbpath, sizeof(dbpath), "%s/jack_db", fDBFilesDir);
        rmdir (dbpath);
    }
#endif
}

int JackMetadata::PropertyInit()
{
#if HAVE_DB

    int ret;
    char dbpath[PATH_MAX + 1];

#ifdef WIN32
    ret = GetTempPathA (PATH_MAX, fDBFilesDir);
    if ((ret > PATH_MAX) || (ret == 0)) {
        jack_error ("cannot get path for temp files");
        return -1;
    }
#else
    strncpy (fDBFilesDir, jack_server_dir, PATH_MAX);
#endif

    /* idempotent */

    if (fDBenv) {
        return 0;
    }

    if ((ret = db_env_create (&fDBenv, 0)) != 0) {
        jack_error ("cannot initialize DB environment: %s\n", db_strerror (ret));
        return -1;
    }

    snprintf (dbpath, sizeof(dbpath), "%s/jack_db", fDBFilesDir);
#ifdef WIN32
    mkdir (dbpath);
#else
    mkdir (dbpath, S_IRWXU | S_IRWXG);
#endif

    if ((ret = fDBenv->open (fDBenv, dbpath, DB_CREATE | DB_INIT_LOCK | DB_INIT_MPOOL | DB_THREAD, 0)) != 0) {
#ifdef WIN32
        // new versions of jack2 are built with HAVE_MIXED_SIZE_ADDRESSING, which induces this error, this is expected
        if (ret == DB_VERSION_MISMATCH) {
            // cleanup old stuff
            snprintf (dbpath, sizeof(dbpath), "%s/jack_db/metadata.db", fDBFilesDir);
            remove (dbpath);

            snprintf (dbpath, sizeof(dbpath), "%s/jack_db/__db.001", fDBFilesDir);
            remove (dbpath);

            snprintf (dbpath, sizeof(dbpath), "%s/jack_db/__db.002", fDBFilesDir);
            remove (dbpath);

            snprintf (dbpath, sizeof(dbpath), "%s/jack_db/__db.003", fDBFilesDir);
            remove (dbpath);

            // try again fresh
            ret = fDBenv->open (fDBenv, dbpath, DB_CREATE | DB_INIT_LOCK | DB_INIT_MPOOL | DB_THREAD, 0);
        }
        if (ret != 0)
#endif
        {
            jack_error ("cannot open DB environment: %s", db_strerror (ret));
            fDBenv = NULL;
            return -1;
        }
    }

    if ((ret = db_create (&fDB, fDBenv, 0)) != 0) {
        jack_error ("Cannot initialize metadata DB (%s)", db_strerror (ret));
        fDBenv->close (fDBenv, 0);
        fDBenv = NULL;
        return -1;
    }

    snprintf (dbpath, sizeof(dbpath), "%s/jack_db/metadata.db", fDBFilesDir);
    if ((ret = fDB->open (fDB, NULL, dbpath, NULL, DB_HASH, DB_CREATE | DB_THREAD, 0666)) != 0) {
        jack_error ("Cannot open metadata DB at %s: %s", dbpath, db_strerror (ret));
        fDB->close (fDB, 0);
        fDB = NULL;
        fDBenv->close (fDBenv, 0);
        fDBenv = NULL;
        return -1;
    }

    return 0;

#else // !HAVE_DB
    return -1;
#endif
}

int JackMetadata::PropertyChangeNotify(JackClient* client, jack_uuid_t subject, const char* key, jack_property_change_t change)
{
    /* the engine passes in a NULL client when it removes metadata during port or client removal
     */

    if (client == NULL) {
        return 0;
    }

    return client->PropertyChangeNotify(subject, key, change);
}

#if HAVE_DB
void JackMetadata::MakeKeyDbt(DBT* dbt, jack_uuid_t subject, const char* key)
{
    char ustr[JACK_UUID_STRING_SIZE];
    size_t len1, len2;

    memset (dbt, 0, sizeof(DBT));
    memset (ustr, 0, JACK_UUID_STRING_SIZE);
    jack_uuid_unparse (subject, ustr);
    len1 = JACK_UUID_STRING_SIZE;
    len2 = strlen (key) + 1;
    dbt->size = len1 + len2;
    dbt->data = malloc (dbt->size);
    memcpy (dbt->data, ustr, len1);         // copy subject+null
    memcpy ((char *)dbt->data + len1, key, len2);   // copy key+null
}
#endif

int JackMetadata::SetProperty(JackClient* client, jack_uuid_t subject, const char* key, const char* value, const char* type)
{
#if HAVE_DB

    DBT d_key;
    DBT data;
    int ret;
    size_t len1, len2;
    jack_property_change_t change;

    if (!key || key[0] == '\0') {
        jack_error ("empty key string for metadata not allowed");
        return -1;
    }

    if (!value || value[0] == '\0') {
        jack_error ("empty value string for metadata not allowed");
        return -1;
    }

    if (PropertyInit()) {
        return -1;
    }

    /* build a key */

    MakeKeyDbt(&d_key, subject, key);

    /* build data */

    memset (&data, 0, sizeof(data));

    len1 = strlen (value) + 1;
    if (type && type[0] != '\0') {
        len2 = strlen (type) + 1;
    } else {
        len2 = 0;
    }

    data.size = len1 + len2;
    data.data = malloc (data.size);
    memcpy (data.data, value, len1);

    if (len2) {
        memcpy ((char *)data.data + len1, type, len2);
    }

    if (fDB->exists (fDB, NULL, &d_key, 0) == DB_NOTFOUND) {
        change = PropertyCreated;
    } else {
        change = PropertyChanged;
    }

    if ((ret = fDB->put (fDB, NULL, &d_key, &data, 0)) != 0) {
        char ustr[JACK_UUID_STRING_SIZE];
        jack_uuid_unparse (subject, ustr);
        jack_error ("Cannot store metadata for %s/%s (%s)", ustr, key, db_strerror (ret));
        if (d_key.size > 0) {
            free (d_key.data);
        }
        if (data.size  > 0) {
            free (data.data);
        }
        return -1;
    }

    PropertyChangeNotify(client, subject, key, change);

    if (d_key.size > 0) {
        free (d_key.data);
    }
    if (data.size  > 0) {
        free (data.data);
    }

    return 0;

#else // !HAVE_DB
    return -1;
#endif
}

int JackMetadata::GetProperty(jack_uuid_t subject, const char* key, char** value, char** type)
{
#if HAVE_DB

    DBT d_key;
    DBT data;
    int ret;
    size_t len1, len2;

    if (key == NULL || key[0] == '\0') {
        return -1;
    }

    if (PropertyInit()) {
        return -1;
    }

    /* build a key */

    MakeKeyDbt(&d_key, subject, key);

    /* setup data DBT */

    memset (&data, 0, sizeof(data));
    data.flags = DB_DBT_MALLOC;

    if ((ret = fDB->get (fDB, NULL, &d_key, &data, 0)) != 0) {
        if (ret != DB_NOTFOUND) {
            char ustr[JACK_UUID_STRING_SIZE];
            jack_uuid_unparse (subject, ustr);
            jack_error ("Cannot retrieve metadata for %s/%s (%s)", ustr, key, db_strerror (ret));
        }
        if (d_key.size > 0) {
            free (d_key.data);
        }
        if (data.size  > 0) {
            free (data.data);
        }
        return -1;
    }

    /* result must have at least 1 char plus 1 null to be valid
       (old rule was:
         result must have at least 2 chars plus 2 nulls to be valid
       )
     */

    if (data.size < 2) {
        if (d_key.size > 0) {
            free (d_key.data);
        }
        if (data.size  > 0) {
            free (data.data);
        }
        return -1;
    }

    len1 = strlen ((const char*)data.data) + 1;
    (*value) = (char*)malloc (len1);
    memcpy (*value, data.data, len1);

    if (len1 < data.size) {
        len2 = strlen ((const char*)data.data + len1) + 1;

        (*type) = (char*)malloc (len2);
        memcpy (*type, (const char *)data.data + len1, len2);
    } else {
        /* no type specified, assume default */
        *type = NULL;
    }

    if (d_key.size > 0) {
        free (d_key.data);
    }
    if (data.size  > 0) {
        free (data.data);
    }

    return 0;

#else // !HAVE_DB
    return -1;
#endif
}

int JackMetadata::GetProperties(jack_uuid_t subject, jack_description_t* desc)
{
#if HAVE_DB

    DBT key;
    DBT data;
    DBC* cursor;
    int ret;
    size_t len1, len2;
    size_t cnt = 0;
    char ustr[JACK_UUID_STRING_SIZE];
    size_t props_size = 0;
    jack_property_t* prop;

    desc->properties = NULL;
    desc->property_cnt = 0;

    memset (ustr, 0, JACK_UUID_STRING_SIZE);
    jack_uuid_unparse (subject, ustr);

    if (PropertyInit()) {
        return -1;
    }


    if ((ret = fDB->cursor (fDB, NULL, &cursor, 0)) != 0) {
        jack_error ("Cannot create cursor for metadata search (%s)", db_strerror (ret));
        return -1;
    }

    memset (&key, 0, sizeof(key));
    memset (&data, 0, sizeof(data));
    data.flags = DB_DBT_MALLOC;

    while ((ret = cursor->get (cursor, &key, &data, DB_NEXT)) == 0) {

        /* require 2 extra chars (data+null) for key,
           which is composed of UUID str plus a key name
         */

        if (key.size < JACK_UUID_STRING_SIZE + 2) {
            /* if (key.size  > 0) free(key.data); */
            if (data.size > 0) {
                free (data.data);
            }
            continue;
        }

        if (memcmp (ustr, key.data, JACK_UUID_STRING_SIZE) != 0) {
            /* not relevant */
            /* if (key.size  > 0) free(key.data); */
            if (data.size > 0) {
                free (data.data);
            }
            continue;
        }

        /* result must have at least 1 char plus 1 null to be valid
           (old rule was:
             result must have at least 2 chars plus 2 nulls to be valid
           )
        */

        if (data.size < 2) {
            /* if (key.size  > 0) free(key.data); */
            if (data.size > 0) {
                free (data.data);
            }
            continue;
        }

        /* realloc array if necessary */

        if (cnt == props_size) {
            if (props_size == 0) {
                props_size = 8; /* a rough guess at a likely upper bound for the number of properties */
            } else {
                props_size *= 2;
            }

            desc->properties = (jack_property_t*)realloc (desc->properties, sizeof(jack_property_t) * props_size);
        }

        prop = &desc->properties[cnt];

        /* store UUID/subject */

        jack_uuid_copy (&desc->subject, subject);

        /* copy key (without leading UUID as subject */

        len1 = key.size - JACK_UUID_STRING_SIZE;
        prop->key = (char*)malloc (len1);
        memcpy ((char*)prop->key, (const char *)key.data + JACK_UUID_STRING_SIZE, len1);

        /* copy data (which contains 1 or 2 null terminated strings, the value
           and optionally a MIME type.
         */

        len1 = strlen ((const char*)data.data) + 1;
        prop->data = (char*)malloc (len1);
        memcpy ((char*)prop->data, data.data, len1);

        if (len1 < data.size) {
            len2 = strlen ((const char *)data.data + len1) + 1;

            prop->type = (char*)malloc (len2);
            memcpy ((char*)prop->type, (const char *)data.data + len1, len2);
        } else {
            /* no type specified, assume default */
            prop->type = NULL;
        }

        /* if (key.size  > 0) free(key.data);  */
        if (data.size > 0) {
            free (data.data);
        }

        ++cnt;
    }

    cursor->close (cursor);
    desc->property_cnt = cnt;

    return cnt;

#else // !HAVE_DB
    return -1;
#endif
}

int JackMetadata::GetAllProperties(jack_description_t** descriptions)
{
#if HAVE_DB

    DBT key;
    DBT data;
    DBC* cursor;
    int ret;
    size_t dcnt = 0;
    size_t dsize = 0;
    size_t n = 0;
    jack_description_t* desc = NULL;
    jack_uuid_t uuid = JACK_UUID_EMPTY_INITIALIZER;
    jack_description_t* current_desc = NULL;
    jack_property_t* current_prop = NULL;
    size_t len1, len2;

    if (PropertyInit()) {
        return -1;
    }

    if ((ret = fDB->cursor (fDB, NULL, &cursor, 0)) != 0) {
        jack_error ("Cannot create cursor for metadata search (%s)", db_strerror (ret));
        return -1;
    }

    memset (&key, 0, sizeof(key));
    memset (&data, 0, sizeof(data));
    data.flags = DB_DBT_MALLOC;

    dsize = 8; /* initial guess at number of descriptions we need */
    dcnt = 0;
    desc = (jack_description_t*)malloc (dsize * sizeof(jack_description_t));

    while ((ret = cursor->get (cursor, &key, &data, DB_NEXT)) == 0) {

        /* require 2 extra chars (data+null) for key,
           which is composed of UUID str plus a key name
         */

        if (key.size < JACK_UUID_STRING_SIZE + 2) {
            /* if (key.size  > 0) free(key.data);  */
            if (data.size > 0) {
                free (data.data);
            }
            continue;
        }

        if (jack_uuid_parse ((const char *)key.data, &uuid) != 0) {
            continue;
        }

        /* do we have an existing description for this UUID */

        for (n = 0; n < dcnt; ++n) {
            if (jack_uuid_compare (uuid, desc[n].subject) == 0) {
                break;
            }
        }

        if (n == dcnt) {
            /* we do not have an existing description, so grow the array */

            if (dcnt == dsize) {
                dsize *= 2;
                desc = (jack_description_t*)realloc (desc, sizeof(jack_description_t) * dsize);
            }

            /* initialize */

            desc[n].property_size = 0;
            desc[n].property_cnt = 0;
            desc[n].properties = NULL;

            /* set up UUID */

            jack_uuid_copy (&desc[n].subject, uuid);
            dcnt++;
        }

        current_desc = &desc[n];

        /* see if there is room for the new property or if we need to realloc
         */

        if (current_desc->property_cnt == current_desc->property_size) {
            if (current_desc->property_size == 0) {
                current_desc->property_size = 8;
            } else {
                current_desc->property_size *= 2;
            }

            current_desc->properties = (jack_property_t*)realloc (current_desc->properties, sizeof(jack_property_t) * current_desc->property_size);
        }

        current_prop = &current_desc->properties[current_desc->property_cnt++];

        /* copy key (without leading UUID) */

        len1 = key.size - JACK_UUID_STRING_SIZE;
        current_prop->key = (char*)malloc (len1);
        memcpy ((char*)current_prop->key, (const char *)key.data + JACK_UUID_STRING_SIZE, len1);

        /* copy data (which contains 1 or 2 null terminated strings, the value
           and optionally a MIME type.
         */

        len1 = strlen ((const char *)data.data) + 1;
        current_prop->data = (char*)malloc (len1);
        memcpy ((char*)current_prop->data, data.data, len1);

        if (len1 < data.size) {
            len2 = strlen ((const char *)data.data + len1) + 1;

            current_prop->type = (char*)malloc (len2);
            memcpy ((char*)current_prop->type, (const char *)data.data + len1, len2);
        } else {
            /* no type specified, assume default */
            current_prop->type = NULL;
        }

        /* if (key.size  > 0) free(key.data);  */
        if (data.size > 0) {
            free (data.data);
        }
    }

    cursor->close (cursor);

    (*descriptions) = desc;

    return dcnt;

#else // !HAVE_DB
    return -1;
#endif
}

int JackMetadata::GetDescription(jack_uuid_t subject, jack_description_t* desc)
{
    return 0;
}

int JackMetadata::GetAllDescriptions(jack_description_t** descs)
{
    return 0;
}

void JackMetadata::FreeDescription(jack_description_t* desc, int free_actual_description_too)
{
    uint32_t n;

    for (n = 0; n < desc->property_cnt; ++n) {
        free ((char*)desc->properties[n].key);
        free ((char*)desc->properties[n].data);
        if (desc->properties[n].type) {
            free ((char*)desc->properties[n].type);
        }
    }

    free (desc->properties);

    if (free_actual_description_too) {
        free (desc);
    }
}

int JackMetadata::RemoveProperty(JackClient* client, jack_uuid_t subject, const char* key)
{
#if HAVE_DB

    DBT d_key;
    int ret;

    if (PropertyInit()) {
        return -1;
    }

    MakeKeyDbt(&d_key, subject, key);
    if ((ret = fDB->del (fDB, NULL, &d_key, 0)) != 0) {
        jack_error ("Cannot delete key %s (%s)", key, db_strerror (ret));
        if (d_key.size > 0) {
            free (d_key.data);
        }
        return -1;
    }

    PropertyChangeNotify(client, subject, key, PropertyDeleted);

    if (d_key.size > 0) {
        free (d_key.data);
    }

    return 0;

#else // !HAVE_DB
    return -1;
#endif
}

int JackMetadata::RemoveProperties(JackClient* client, jack_uuid_t subject)
{
#if HAVE_DB

    DBT key;
    DBT data;
    DBC* cursor;
    int ret;
    char ustr[JACK_UUID_STRING_SIZE];
    int retval = 0;
    uint32_t cnt = 0;

    memset (ustr, 0, JACK_UUID_STRING_SIZE);
    jack_uuid_unparse (subject, ustr);

    if (PropertyInit() || fDB == NULL) {
        return -1;
    }

    if ((ret = fDB->cursor (fDB, NULL, &cursor, 0)) != 0) {
        jack_error ("Cannot create cursor for metadata search (%s)", db_strerror (ret));
        return -1;
    }

    memset (&key, 0, sizeof(key));
    memset (&data, 0, sizeof(data));
    data.flags = DB_DBT_MALLOC;

    while ((ret = cursor->get (cursor, &key, &data, DB_NEXT)) == 0) {

        /* require 2 extra chars (data+null) for key,
           which is composed of UUID str plus a key name
         */

        if (key.size < JACK_UUID_STRING_SIZE + 2) {
            /* if (key.size  > 0) free(key.data); */
            if (data.size > 0) {
                free (data.data);
            }
            continue;
        }

        if (memcmp (ustr, key.data, JACK_UUID_STRING_SIZE) != 0) {
            /* not relevant */
            /* if (key.size  > 0) free(key.data); */
            if (data.size > 0) {
                free (data.data);
            }
            continue;
        }

        if ((ret = cursor->del (cursor, 0)) != 0) {
            jack_error ("cannot delete property (%s)", db_strerror (ret));
            /* don't return -1 here since this would leave things
               even more inconsistent. wait till the cursor is finished
             */
            retval = -1;
        }
        cnt++;

        /* if (key.size  > 0) free(key.data);  */
        if (data.size > 0) {
            free (data.data);
        }
    }

    cursor->close (cursor);

    if (cnt) {
        PropertyChangeNotify(client, subject, NULL, PropertyDeleted);
    }

    if (retval) {
        return -1;
    }

    return cnt;

#else // !HAVE_DB
    return -1;
#endif
}

int JackMetadata::RemoveAllProperties(JackClient* client)
{
#if HAVE_DB

    int ret;
    jack_uuid_t empty_uuid = JACK_UUID_EMPTY_INITIALIZER;

    if (PropertyInit()) {
        return -1;
    }

    if ((ret = fDB->truncate (fDB, NULL, NULL, 0)) != 0) {
        jack_error ("Cannot clear properties (%s)", db_strerror (ret));
        return -1;
    }

    PropertyChangeNotify(client, empty_uuid, NULL, PropertyDeleted);

    return 0;

#else // !HAVE_DB
    return -1;
#endif
}

} // end of namespace



