/*
  Copyright (C) 2013 Paul Davis
    
  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 <string.h>
#include <db.h>
#include <limits.h>

#include <jack/metadata.h>
#include <jack/uuid.h>

#include "internal.h"
#include "local.h"

const char* JACK_METADATA_PRETTY_NAME = "http://jackaudio.org/metadata/pretty-name";
const char* JACK_METADATA_HARDWARE    = "http://jackaudio.org/metadata/hardware";
const char* JACK_METADATA_CONNECTED   = "http://jackaudio.org/metadata/connected";
const char* JACK_METADATA_PORT_GROUP  = "http://jackaudio.org/metadata/port-group";
const char* JACK_METADATA_ICON_SMALL  = "http://jackaudio.org/metadata/icon-small";
const char* JACK_METADATA_ICON_LARGE  = "http://jackaudio.org/metadata/icon-large";

static DB* db = NULL;
static DB_ENV* db_env = NULL;

static int
jack_property_init (const char* server_name)
{
        int ret;
        char dbpath[PATH_MAX+1];
        char server_dir[PATH_MAX+1];

        /* idempotent */

        if (db_env) {
                return 0;
        }

        if ((ret = db_env_create(&db_env, 0)) != 0) { 
                jack_error ("cannot initialize DB environment: %s\n", db_strerror(ret));
                return -1;
        } 
        
        if ((ret = db_env->open(db_env, jack_server_dir (server_name, server_dir), DB_CREATE | DB_INIT_LOCK | DB_INIT_MPOOL | DB_THREAD, 0)) != 0) { 
                jack_error ("cannot open DB environment: %s", db_strerror (ret));
                return -1;
        }

        if ((ret = db_create (&db, db_env, 0)) != 0) {
                jack_error ("Cannot initialize metadata DB (%s)", db_strerror (ret));
                return -1;
        }

        snprintf (dbpath, sizeof (dbpath), "%s/%s", jack_server_dir (server_name, server_dir), "metadata.db");

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

        return 0;
}

static void jack_properties_uninit () __attribute__ ((destructor));

void
jack_properties_uninit ()
{
        if (db) {
                db->close (db, 0);
                db = NULL;
        }
        if (db_env) {
                db_env->close (db_env, 0);
                db_env = 0;
        }
}

void
jack_free_description(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);
        }
}

static int
jack_property_change_notify (jack_client_t* client, jack_uuid_t uuid, const char* key, jack_property_change_t change)
{
        jack_request_t req;

        /* the engine passes in a NULL client when it removes metadata during port or client removal
         */

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

        req.type = PropertyChangeNotify;
        req.x.property.change = change;
        jack_uuid_copy (&req.x.property.uuid, uuid);
        req.x.property.keylen = key ? strlen (key) + 1 : 0;
        req.x.property.key = key;
        return jack_client_deliver_request (client, &req);
}

static void
make_key_dbt (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 (dbt->data + len1, key, len2); // copy key+null
}


int
jack_set_property (jack_client_t* client,
                   jack_uuid_t subject,
                   const char* key,
                   const char* value,
                   const char* type)
{
        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 (jack_property_init (NULL)) {
                return -1;
        }

        /* build a key */

        make_key_dbt (&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 (data.data + len1, type, len2);
        }

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

        if ((ret = db->put (db, 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));
                return -1;
        }

        jack_property_change_notify (client, subject, key, change);

        return 0;
}

int
jack_get_property (jack_uuid_t subject,
                   const char* key,
                   char**      value,
                   char**      type)
{
        DBT d_key;
        DBT data;
        int ret;
        size_t len1, len2;

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

        if (jack_property_init (NULL)) {
                return -1;
        }

        /* build a key */

        make_key_dbt (&d_key, subject, key);
        
        /* setup data DBT */

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

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

        /* result must have at least 2 chars plus 2 nulls to be valid 
         */

        if (data.size < 4) {
                if (data.size > 0) {
                        free (data.data);
                }
                return -1;
        }
        
        len1 = strlen (data.data) + 1;
        (*value) = (char *) malloc (len1);
        memcpy (*value, data.data, len1);

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

        if (data.size) {
                free (data.data);
        }

        return 0;
}

int
jack_get_properties (jack_uuid_t subject,
                     jack_description_t* desc)
{
        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;

        jack_uuid_unparse (subject, ustr);

        if (jack_property_init (NULL)) {
                return -1;
        }


        if ((ret = db->cursor (db, 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 (data.size > 0) {
                                free (data.data);
                        }
                        continue;
                }

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

                /* result must have at least 2 chars plus 2 nulls to be valid 
                 */
                
                if (data.size < 4) {
                        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 = malloc (len1);
                memcpy ((char*) prop->key, 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 (data.data) + 1;
                prop->data = (char *) malloc (len1);
                memcpy ((char*) prop->data, data.data, len1);
                
                if (len1 < data.size) {
                        len2 = strlen (data.data+len1) + 1;
                        
                        prop->type= (char *) malloc (len2);
                        memcpy ((char*) prop->type, data.data+len1, len2);
                } else {
                        /* no type specified, assume default */
                        prop->type = NULL;
                }
                
                if (data.size) {
                        free (data.data);
                }

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

        return cnt;
}

int
jack_get_all_properties (jack_description_t** descriptions)
{
        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 (jack_property_init (NULL)) {
                return -1;
        }

        if ((ret = db->cursor (db, 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 (data.size > 0) {
                                free (data.data);
                        }
                        continue;
                }

                if (jack_uuid_parse (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 = malloc (len1);
                memcpy ((char*) current_prop->key, 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 (data.data) + 1;
                current_prop->data = (char *) malloc (len1);
                memcpy ((char*) current_prop->data, data.data, len1);
                
                if (len1 < data.size) {
                        len2 = strlen (data.data+len1) + 1;
                        
                        current_prop->type= (char *) malloc (len2);
                        memcpy ((char*) current_prop->type, data.data+len1, len2);
                } else {
                        /* no type specified, assume default */
                        current_prop->type = NULL;
                }
                
                if (data.size) {
                        free (data.data);
                }
        }
        
        cursor->close (cursor);

        (*descriptions) = desc;

        return dcnt;
}


int
jack_get_description (jack_uuid_t         subject,
                       jack_description_t* desc)
{
        return 0;
}

int
jack_get_all_descriptions (jack_description_t** descs)
{
        return 0;
}

int 
jack_set_property_change_callback (jack_client_t *client,
                                   JackPropertyChangeCallback callback, void *arg)
{
	if (client->control->active) {
		jack_error ("You cannot set callbacks on an active client.");
		return -1;
	}
	client->property_cb = callback;
	client->property_cb_arg = arg;
	client->control->property_cbset = (callback != NULL);
	return 0;
}

int        
jack_remove_property (jack_client_t* client, jack_uuid_t subject, const char* key)
{
        DBT d_key;
        int ret;

        if (jack_property_init (NULL)) {
                return -1;
        }

        make_key_dbt (&d_key, subject, key);
        if ((ret = db->del (db, NULL, &d_key, 0)) != 0) {
                jack_error ("Cannot delete key %s (%s)", key, db_strerror (ret));
                return -1;
        }

        jack_property_change_notify (client, subject, key, PropertyDeleted);

        return 0;
}

int        
jack_remove_properties (jack_client_t* client, jack_uuid_t subject)
{
        DBT key;
        DBT data;
        DBC* cursor;
        int ret;
        char ustr[JACK_UUID_STRING_SIZE];
        int retval = 0;
        uint32_t cnt = 0;

        jack_uuid_unparse (subject, ustr);

        if (jack_property_init (NULL)) {
                return -1;
        }

        if ((ret = db->cursor (db, 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 (data.size > 0) {
                                free (data.data);
                        }
                        continue;
                }

                if (memcmp (ustr, key.data, JACK_UUID_STRING_SIZE) != 0) {
                        /* not relevant */
                        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++;
        }

        cursor->close (cursor);

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

        if (retval) {
                return -1;
        } 

        return cnt;
}

int        
jack_remove_all_properties (jack_client_t* client)
{
        int ret;
        jack_uuid_t empty_uuid = JACK_UUID_EMPTY_INITIALIZER;

        if (jack_property_init (NULL)) {
                return -1;
        }

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

        jack_property_change_notify (client, empty_uuid, NULL, PropertyDeleted);

        return 0;
}
