/*
Calf Box, an open source musical instrument.
Copyright (C) 2012 Krzysztof Foltman

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 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 General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "cmd.h"
#include "errors.h"
#include "dom.h"

#include <assert.h>
#include <glib.h>
#include <malloc.h>
#include <string.h>

static GHashTable *class_name_hash = NULL;

struct cbox_class_per_document
{
    GList *instances;
};

struct cbox_document
{
    GHashTable *classes_per_document;
    GHashTable *services_per_document;
    GHashTable *uuids_per_document;
    struct cbox_command_target cmd_target;
    int item_ctr;
    uint64_t generation_ctr;
};

////////////////////////////////////////////////////////////////////////////////////////

void cbox_dom_init()
{
    class_name_hash = g_hash_table_new(g_str_hash, g_str_equal);
}

void cbox_dom_close()
{
    g_hash_table_destroy(class_name_hash);    
}

////////////////////////////////////////////////////////////////////////////////////////

struct cbox_class *cbox_class_find_by_name(const char *name)
{
    assert(class_name_hash != NULL);
    return g_hash_table_lookup(class_name_hash, name);
}

void cbox_class_register(struct cbox_class *class_ptr)
{
    assert(class_name_hash != NULL);
    g_hash_table_insert(class_name_hash, (gpointer)class_ptr->name, class_ptr);
}

static struct cbox_class_per_document *get_cpd_for_class(struct cbox_document *doc, struct cbox_class *class_ptr)
{
    struct cbox_class_per_document *p = g_hash_table_lookup(doc->classes_per_document, class_ptr);
    if (p != NULL)
        return p;
    p = malloc(sizeof(struct cbox_class_per_document));
    p->instances = NULL;
    g_hash_table_insert(doc->classes_per_document, class_ptr, p);
    return p;
}

////////////////////////////////////////////////////////////////////////////////////////

void cbox_uuid_clear(struct cbox_uuid *uuid)
{
    uuid_clear(uuid->uuid);
}

guint cbox_uuid_hash(gconstpointer v)
{
    char buf[40];
    uuid_unparse_lower(((struct cbox_uuid *)v)->uuid, buf);
    return g_str_hash(buf);
}

gboolean cbox_uuid_equal(gconstpointer v1, gconstpointer v2)
{
    const struct cbox_uuid *p1 = v1;
    const struct cbox_uuid *p2 = v2;
    
    return !uuid_compare(p1->uuid, p2->uuid);
}

void cbox_uuid_copy(struct cbox_uuid *vto, const struct cbox_uuid *vfrom)
{
    uuid_copy(vto->uuid, vfrom->uuid);
}

gboolean cbox_uuid_report_as(struct cbox_uuid *uuid, const char *cmd, struct cbox_command_target *fb, GError **error)
{
    if (!fb)
        return TRUE;
    return cbox_execute_on(fb, NULL, cmd, "u", error, uuid->uuid);    
}

gboolean cbox_uuid_report(struct cbox_uuid *uuid, struct cbox_command_target *fb, GError **error)
{
    return cbox_uuid_report_as(uuid, "/uuid", fb, error);
}

gboolean cbox_uuid_fromstring(struct cbox_uuid *uuid, const char *str, GError **error)
{
    if (uuid_parse(str, uuid->uuid))
    {
        g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Malformed UUID: '%s'", str);
        return FALSE;
    }
    return TRUE;
}

void cbox_uuid_tostring(struct cbox_uuid *uuid, char str[40])
{
    uuid_unparse(uuid->uuid, str);
}

void cbox_uuid_generate(struct cbox_uuid *uuid)
{
    uuid_generate(uuid->uuid);   
}

////////////////////////////////////////////////////////////////////////////////////////

void cbox_object_register_instance(struct cbox_document *doc, struct cbox_objhdr *obj)
{
    assert(obj != NULL);

    struct cbox_class_per_document *cpd = get_cpd_for_class(doc, obj->class_ptr);
    cpd->instances = g_list_prepend(cpd->instances, obj);
    obj->owner = doc;
    obj->link_in_document = cpd->instances;
    g_hash_table_insert(obj->owner->uuids_per_document, &obj->instance_uuid, obj);
}

struct cbox_command_target *cbox_object_get_cmd_target(struct cbox_objhdr *hdr_ptr)
{
    if (!hdr_ptr->class_ptr->getcmdtargetfunc)
        return NULL;
    return hdr_ptr->class_ptr->getcmdtargetfunc(hdr_ptr);
}

gboolean cbox_object_try_default_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, const char *subcmd, gboolean *result, GError **error)
{
    // XXXKF this assumes objhdr ptr == object ptr - needs to add the header offset in cmd target?
    struct cbox_objhdr *obj = ct->user_data;
    if (!strcmp(subcmd, "/status") && !strcmp(cmd->arg_types, ""))
    {
        if (!cbox_object_default_status(obj, fb, error))
        {
            *result = FALSE;
            return TRUE;
        }
        return FALSE;
    }
    if (!strcmp(subcmd, "/delete") && !strcmp(cmd->arg_types, ""))
    {
        cbox_object_destroy(obj);
        *result = TRUE;
        return TRUE;
    }
    if (!strcmp(subcmd, "/get_uuid") && !strcmp(cmd->arg_types, ""))
    {
        if (!cbox_check_fb_channel(fb, cmd->command, error))
        {
            *result = FALSE;
            return TRUE;
        }
        
        *result = cbox_uuid_report(&obj->instance_uuid, fb, error);
        return TRUE;
    }
    if (!strcmp(subcmd, "/get_class_name") && !strcmp(cmd->arg_types, ""))
    {
        if (!cbox_check_fb_channel(fb, cmd->command, error))
        {
            *result = FALSE;
            return TRUE;
        }        
        *result = cbox_execute_on(fb, NULL, "/class_name", "s", error, obj->class_ptr->name);
        return TRUE;
    }
    return FALSE;
}

gboolean cbox_object_default_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error)
{
    gboolean result = FALSE;
    if (cbox_object_try_default_process_cmd(ct, fb, cmd, cmd->command, &result, error))
        return result;
    struct cbox_objhdr *obj = ct->user_data;
    g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s' for object class '%s'", cmd->command, cmd->arg_types, obj->class_ptr->name);
    return FALSE;
}

gboolean cbox_object_default_status(struct cbox_objhdr *objhdr, struct cbox_command_target *fb, GError **error)
{
    char buf[40];
    uuid_unparse(objhdr->instance_uuid.uuid, buf);
    return cbox_execute_on(fb, NULL, "/uuid", "s", error, buf);    
}

void cbox_object_destroy(struct cbox_objhdr *hdr_ptr)
{
    struct cbox_class_per_document *cpd = get_cpd_for_class(hdr_ptr->owner, hdr_ptr->class_ptr);
    cpd->instances = g_list_delete_link(cpd->instances, hdr_ptr->link_in_document);
    hdr_ptr->link_in_document = NULL;
    g_hash_table_remove(hdr_ptr->owner->uuids_per_document, &hdr_ptr->instance_uuid);
    
    hdr_ptr->class_ptr->destroyfunc(hdr_ptr);
}

////////////////////////////////////////////////////////////////////////////////////////

static gboolean document_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error)
{
    char *uuid;
    const char *subcommand;
    if (!strcmp(cmd->command, "/dump") && !strcmp(cmd->arg_types, ""))
    {
        struct cbox_document *doc = ct->user_data;
        cbox_document_dump(doc);
        return TRUE;
    }
    if (cbox_parse_path_part_str(cmd, "/uuid/", &subcommand, &uuid, error))
    {
        struct cbox_document *doc = ct->user_data;
        if (!subcommand)
            return FALSE;
        struct cbox_objhdr *obj = cbox_document_get_object_by_text_uuid(doc, uuid, NULL, error);
        g_free(uuid);
        if (!obj)
            return FALSE;
        struct cbox_command_target *ct2 = cbox_object_get_cmd_target(obj);
        return cbox_execute_sub(ct2, fb, cmd, subcommand, error);
    }
    g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unknown combination of target path and argument: '%s', '%s'", cmd->command, cmd->arg_types);
    return FALSE;    
}

struct cbox_document *cbox_document_new()
{
    struct cbox_document *res = malloc(sizeof(struct cbox_document));
    res->classes_per_document = g_hash_table_new_full(NULL, NULL, NULL, g_free);
    res->services_per_document = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
    res->uuids_per_document = g_hash_table_new(cbox_uuid_hash, cbox_uuid_equal);
    res->cmd_target.process_cmd = document_process_cmd;
    res->cmd_target.user_data = res;
    res->item_ctr = 0;
    res->generation_ctr = 1000; // start with non-zero value just to spot invalid values more easily

    return res;
}

struct cbox_command_target *cbox_document_get_cmd_target(struct cbox_document *doc)
{
    return &doc->cmd_target;
}

struct cbox_objhdr *cbox_document_get_service(struct cbox_document *document, const char *name)
{
    return g_hash_table_lookup(document->services_per_document, name);
}

void cbox_document_set_service(struct cbox_document *document, const char *name, struct cbox_objhdr *obj)
{
    g_hash_table_insert(document->services_per_document, g_strdup(name), obj);
}

struct cbox_objhdr *cbox_document_get_object_by_uuid(struct cbox_document *doc, const struct cbox_uuid *uuid)
{
    return g_hash_table_lookup(doc->uuids_per_document, uuid);
}

struct cbox_objhdr *cbox_document_get_object_by_text_uuid(struct cbox_document *doc, const char *uuid, const struct cbox_class *class_ptr, GError **error)
{
    struct cbox_uuid uuidv;
    if (!cbox_uuid_fromstring(&uuidv, uuid, error))
        return NULL;
    struct cbox_objhdr *obj = cbox_document_get_object_by_uuid(doc, &uuidv);
    if (!obj)
    {
        g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "UUID not found: '%s'", uuid);
        return NULL;
    }
    if (class_ptr && !cbox_class_is_a(obj->class_ptr, class_ptr))
    {
        g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Unexpected object type '%s' for UUID '%s' (expected '%s')", obj->class_ptr->name, uuid, class_ptr->name);
        return NULL;
    }
    return obj;
}

static void iter_func(gpointer key, gpointer value, gpointer doc_)
{
#ifndef NDEBUG
    struct cbox_document *doc = (struct cbox_document *)doc_;
#endif
    struct cbox_class *class_ptr = key;
    struct cbox_class_per_document *cpd = value;
    int first = 1;
    printf("Class %s: ", class_ptr->name);
    GList *l = cpd->instances;
    while(l) {
        if (!first)
            printf(", ");
        printf("%p", l->data);
        fflush(stdout);
        struct cbox_objhdr *hdr = (struct cbox_objhdr *)l->data;
        char buf[40];
        uuid_unparse(hdr->instance_uuid.uuid, buf);
        printf("[%s]", buf);
        fflush(stdout);
        assert(cbox_document_get_object_by_uuid(doc, &hdr->instance_uuid));
        l = l->next;
        first = 0;
    }
    if (first)
        printf("<no instances>");
    printf("\n");
}

static void iter_func2(gpointer key, gpointer value, gpointer document)
{
    struct cbox_objhdr *oh = value;
    char buf[40];
    uuid_unparse(oh->instance_uuid.uuid, buf);
    printf("Service %s: %p", (const char *)key, value);
    fflush(stdout);
    printf("[%s]", buf);
    fflush(stdout);
    printf(" (%s)\n", oh->class_ptr->name);
}

void cbox_document_dump(struct cbox_document *document)
{
     g_hash_table_foreach(document->classes_per_document, iter_func, document);
     g_hash_table_foreach(document->services_per_document, iter_func2, document);
}

uint64_t cbox_document_get_next_stamp(struct cbox_document *document)
{
    return document->generation_ctr;
}

void cbox_document_destroy(struct cbox_document *document)
{
    g_hash_table_destroy(document->classes_per_document);
    g_hash_table_destroy(document->services_per_document);
    g_hash_table_destroy(document->uuids_per_document);
    free(document);
}


