/*
Calf Box, an open source musical instrument.
Copyright (C) 2010-2011 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 "errors.h"
#include "master.h"
#include "pattern.h"
#include "rt.h"
#include "seq.h"
#include "track.h"
#include "song.h"
#include <assert.h>
#include <malloc.h>

CBOX_CLASS_DEFINITION_ROOT(cbox_track)
CBOX_CLASS_DEFINITION_ROOT(cbox_track_item)

static gboolean cbox_track_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error);
static gboolean cbox_track_item_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error);

void cbox_track_item_destroyfunc(struct cbox_objhdr *hdr)
{
    struct cbox_track_item *item = CBOX_H2O(hdr);
    item->owner->items = g_list_remove(item->owner->items, item);
    free(item);
}

struct cbox_track *cbox_track_new(struct cbox_document *document)
{
    struct cbox_track *p = malloc(sizeof(struct cbox_track));
    CBOX_OBJECT_HEADER_INIT(p, cbox_track, document);
    
    p->name = g_strdup("Unnamed");
    p->items = NULL;
    p->pb = NULL;
    p->owner = NULL;
    p->external_output_set = FALSE;
    p->generation = 0;
    p->mute = FALSE;

    cbox_command_target_init(&p->cmd_target, cbox_track_process_cmd, p);
    CBOX_OBJECT_REGISTER(p);
    return p;
}

#define CBTI(it) ((struct cbox_track_item *)(it)->data)

void cbox_track_add_item_to_list(struct cbox_track *track, struct cbox_track_item *item)
{
    GList *it = track->items;
    while(it != NULL && CBTI(it)->time < item->time)
        it = g_list_next(it);
    // all items earlier than the new one -> append
    if (it == NULL)
    {
        track->items = g_list_append(track->items, item);
        cbox_track_set_dirty(track);
        return;
    }
    // Here, I don't really care about overlaps - it's more important to preserve
    // all clips as sent by the caller.
    track->items = g_list_insert_before(track->items, it, item);
    cbox_track_set_dirty(track);
}

struct cbox_track_item *cbox_track_add_item(struct cbox_track *track, uint32_t time, struct cbox_midi_pattern *pattern, uint32_t offset, uint32_t length)
{
    struct cbox_track_item *item = malloc(sizeof(struct cbox_track_item));
    CBOX_OBJECT_HEADER_INIT(item, cbox_track_item, CBOX_GET_DOCUMENT(track));
    item->owner = track;
    item->time = time;
    item->pattern = pattern;
    item->offset = offset;
    item->length = length;
    cbox_command_target_init(&item->cmd_target, cbox_track_item_process_cmd, item);
    
    cbox_track_add_item_to_list(track, item);
    CBOX_OBJECT_REGISTER(item);
    return item;
}

void cbox_track_clear_clips(struct cbox_track *track)
{
    while(track->items) {
        cbox_object_destroy(track->items->data);
    }
    cbox_track_set_dirty(track);
}

void cbox_track_set_dirty(struct cbox_track *track)
{
    ++track->generation;
}

void cbox_track_destroyfunc(struct cbox_objhdr *objhdr)
{
    struct cbox_track *track = CBOX_H2O(objhdr);
    if (track->owner)
        cbox_song_remove_track(track->owner, track);
    // XXXKF I'm not sure if I want the lifecycle of track playback objects to be managed by the track itself
    if (track->pb && track->pb->ref_count == 1)
        cbox_track_playback_destroy(track->pb);
    // The items will unlink themselves from the list in destructor
    while(track->items)
        cbox_object_destroy(track->items->data);
    g_free((gchar *)track->name);
    free(track);
}

gboolean cbox_track_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error)
{
    struct cbox_track *track = ct->user_data;
    if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, ""))
    {
        if (!cbox_check_fb_channel(fb, cmd->command, error))
            return FALSE;
        
        GList *it = track->items;
        while(it != NULL)
        {
            struct cbox_track_item *trki = it->data;
            if (!cbox_execute_on(fb, NULL, "/clip", "iiioo", error, trki->time, trki->offset, trki->length, trki->pattern, trki))
                return FALSE;
            it = g_list_next(it);
        }

        return cbox_execute_on(fb, NULL, "/mute", "i", error, track->mute) &&
            cbox_execute_on(fb, NULL, "/name", "s", error, track->name) &&
            (track->external_output_set ? cbox_uuid_report_as(&track->external_output, "/external_output", fb, error) : TRUE) &&
            CBOX_OBJECT_DEFAULT_STATUS(track, fb, error);
    }
    else if (!strcmp(cmd->command, "/clear_clips") && !strcmp(cmd->arg_types, ""))
    {
        cbox_track_clear_clips(track);
        return TRUE;
    }
    else if (!strcmp(cmd->command, "/add_clip") && !strcmp(cmd->arg_types, "iiis"))
    {
        int pos = CBOX_ARG_I(cmd, 0);
        int offset = CBOX_ARG_I(cmd, 1);
        int length = CBOX_ARG_I(cmd, 2);
        if (pos < 0)
        {
            g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid pattern position %d (cannot be negative)", pos);
            return FALSE;
        }
        if (offset < 0)
        {
            g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid pattern offset %d (cannot be negative)", offset);
            return FALSE;
        }
        if (length <= 0)
        {
            g_set_error(error, CBOX_MODULE_ERROR, CBOX_MODULE_ERROR_FAILED, "Invalid pattern length %d (must be positive)", length);
            return FALSE;
        }
        struct cbox_objhdr *pattern = CBOX_ARG_O(cmd, 3, track, cbox_midi_pattern, error);
        if (!pattern)
            return FALSE;
        struct cbox_midi_pattern *mp = CBOX_H2O(pattern);
        struct cbox_track_item *trki = cbox_track_add_item(track, pos, mp, offset, length);
        if (fb)
            return cbox_execute_on(fb, NULL, "/uuid", "o", error, trki);
        cbox_track_set_dirty(track);
        return TRUE;
    }
    else if (!strcmp(cmd->command, "/name") && !strcmp(cmd->arg_types, "s"))
    {
        char *old_name = track->name;
        track->name = g_strdup(CBOX_ARG_S(cmd, 0));
        g_free(old_name);
        cbox_track_set_dirty(track);
        return TRUE;
    }
    else if (!strcmp(cmd->command, "/external_output") && !strcmp(cmd->arg_types, "s"))
    {
        if (*CBOX_ARG_S(cmd, 0))
        {
            if (cbox_uuid_fromstring(&track->external_output, CBOX_ARG_S(cmd, 0), error)) {
                track->external_output_set = TRUE;
                cbox_track_set_dirty(track);
            }
        }
        else {
            track->external_output_set = FALSE;
            cbox_track_set_dirty(track);
        }
        return TRUE;
    }
    else if (!strcmp(cmd->command, "/mute") && !strcmp(cmd->arg_types, "i"))
    {
        if (CBOX_ARG_I(cmd, 0) == (int)track->mute) // no-op
            return TRUE;
        track->mute = CBOX_ARG_I(cmd, 0) != 0;
        cbox_track_set_dirty(track);
        return TRUE;
    }
    else
        return cbox_object_default_process_cmd(ct, fb, cmd, error);
}

gboolean cbox_track_item_process_cmd(struct cbox_command_target *ct, struct cbox_command_target *fb, struct cbox_osc_command *cmd, GError **error)
{
    struct cbox_track_item *trki = ct->user_data;
    if (!strcmp(cmd->command, "/status") && !strcmp(cmd->arg_types, ""))
    {
        if (!cbox_check_fb_channel(fb, cmd->command, error))
            return FALSE;
        
        return cbox_execute_on(fb, NULL, "/pos", "i", error, trki->time) &&
            cbox_execute_on(fb, NULL, "/offset", "i", error, trki->offset) &&
            cbox_execute_on(fb, NULL, "/length", "i", error, trki->length) &&
            cbox_execute_on(fb, NULL, "/pattern", "o", error, trki->pattern) &&
            CBOX_OBJECT_DEFAULT_STATUS(trki, fb, error);
    }
    if (!strcmp(cmd->command, "/delete") && !strcmp(cmd->arg_types, ""))
    {
        cbox_track_set_dirty(trki->owner);
        cbox_object_destroy(CBOX_O2H(trki));
        return TRUE;
    }
    if (!strcmp(cmd->command, "/pattern") && !strcmp(cmd->arg_types, "s"))
    {
        struct cbox_objhdr *pattern = CBOX_ARG_O(cmd, 0, trki->owner, cbox_midi_pattern, error);
        if (!pattern)
            return FALSE;
        if (trki->pattern == CBOX_H2O(pattern)) // no-op
            return TRUE;
        trki->pattern = CBOX_H2O(pattern);
        cbox_track_item_set_dirty(trki);
        return TRUE;
    }
    if (!strcmp(cmd->command, "/length") && !strcmp(cmd->arg_types, "i"))
    {
        if (CBOX_ARG_I(cmd, 0) == (int)trki->length) // no-op
            return TRUE;
        trki->length = CBOX_ARG_I(cmd, 0);
        cbox_track_item_set_dirty(trki);
        return TRUE;
    }
    if (!strcmp(cmd->command, "/pos") && !strcmp(cmd->arg_types, "i"))
    {
        if (CBOX_ARG_I(cmd, 0) == (int)trki->time) // no-op
            return TRUE;
        trki->owner->items = g_list_remove(trki->owner->items, trki);
        trki->time = CBOX_ARG_I(cmd, 0);
        cbox_track_add_item_to_list(trki->owner, trki);
        cbox_track_item_set_dirty(trki);
        return TRUE;
    }
    if (!strcmp(cmd->command, "/offset") && !strcmp(cmd->arg_types, "i"))
    {
        if (CBOX_ARG_I(cmd, 0) == (int)trki->offset) // no-op
            return TRUE;
        cbox_track_item_set_dirty(trki);
        trki->offset = CBOX_ARG_I(cmd, 0);
        return TRUE;
    }
    return cbox_object_default_process_cmd(ct, fb, cmd, error);
}

extern void cbox_track_item_set_dirty(struct cbox_track_item *track_item)
{
    cbox_track_set_dirty(track_item->owner);
}
