/*
 * SwamiguiControlMidiKey.c - MIDI keyboard control
 *
 * Swami
 * Copyright (C) 1999-2014 Element Green <element@elementsofsound.org>
 *
 * 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; version 2
 * of the License only.
 *
 * 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, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA or point your web browser to http://www.gnu.org.
 */

/*
 * NOTE: Setting key arrays is NOT multi-thread safe!
 */

#include <stdio.h>
#include <string.h>
#include <glib.h>
#include <glib-object.h>

#include <gdk/gdkkeysyms.h>

#include "SwamiguiControlMidiKey.h"
#include "SwamiguiRoot.h"

/* Some default values */
#define DEFAULT_LOWER_OCTAVE    2
#define DEFAULT_UPPER_OCTAVE    DEFAULT_LOWER_OCTAVE + 1

enum
{
    PROP_0,
    PROP_LOWER_OCTAVE,
    PROP_UPPER_OCTAVE,
    PROP_JOIN_OCTAVES,
    PROP_LOWER_VELOCITY,
    PROP_UPPER_VELOCITY,
    PROP_SAME_VELOCITY,
    PROP_LOWER_CHANNEL,
    PROP_UPPER_CHANNEL
};

typedef struct
{
    guint key;		    /* GDK key (see gdk/gdkkeysym.h header) */
    gint8 active_note;	    /* active MIDI note or -1 if not active */
    gint8 active_chan;	 /* MIDI channel of active note (if not -1) */
} MidiKey;

static void swamigui_control_midi_key_get_property(GObject *object,
        guint property_id,
        GValue *value,
        GParamSpec *pspec);
static void swamigui_control_midi_key_set_property(GObject *object,
        guint property_id,
        const GValue *value,
        GParamSpec *pspec);
static gint swamigui_control_midi_key_snooper(GtkWidget *grab_widget,
        GdkEventKey *event,
        gpointer func_data);
static void swamigui_control_midi_key_finalize(GObject *object);


/* FIXME - Dynamically determine key map */

/* default lower keys */
static guint default_lower_keys[] =
{
    GDK_z, GDK_s, GDK_x, GDK_d, GDK_c, GDK_v, GDK_g, GDK_b, GDK_h, GDK_n, GDK_j,
    GDK_m, GDK_comma, GDK_l, GDK_period, GDK_semicolon, GDK_slash
};

/* default upper keys */
static guint default_upper_keys[] =
{
    GDK_q, GDK_2, GDK_w, GDK_3, GDK_e, GDK_r, GDK_5, GDK_t, GDK_6, GDK_y, GDK_7,
    GDK_u, GDK_i, GDK_9, GDK_o, GDK_0, GDK_p, GDK_bracketleft, GDK_equal,
    GDK_bracketright
};

/* Widget types to ignore key presses from (text entries, etc) */
static char *ignore_widget_type_names[] =
{
    "GtkEntry",
    "GtkTextView"
};

/* Allocated and resolved type array, G_TYPE_INVALID terminated */
static GType *ignore_widget_types;


G_DEFINE_TYPE(SwamiguiControlMidiKey, swamigui_control_midi_key,
              SWAMI_TYPE_CONTROL_MIDI);


static void
swamigui_control_midi_key_class_init(SwamiguiControlMidiKeyClass *klass)
{
    GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    int i;

    obj_class->set_property = swamigui_control_midi_key_set_property;
    obj_class->get_property = swamigui_control_midi_key_get_property;
    obj_class->finalize = swamigui_control_midi_key_finalize;

    g_object_class_install_property(obj_class, PROP_LOWER_OCTAVE,
                                    g_param_spec_int("lower-octave", "Lower octave", "Lower keyboard MIDI octave",
                                            -2, 8, DEFAULT_LOWER_OCTAVE, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_UPPER_OCTAVE,
                                    g_param_spec_int("upper-octave", "Upper octave", "Upper keyboard MIDI octave",
                                            -2, 8, DEFAULT_UPPER_OCTAVE, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_JOIN_OCTAVES,
                                    g_param_spec_boolean("join-octaves", "Join octaves", "Join upper and lower octaves",
                                            TRUE, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_LOWER_VELOCITY,
                                    g_param_spec_int("lower-velocity", "Lower velocity", "Lower keyboard MIDI velocity",
                                            1, 127, 127, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_UPPER_VELOCITY,
                                    g_param_spec_int("upper-velocity", "Upper velocity", "Upper keyboard MIDI velocity",
                                            1, 127, 127, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_SAME_VELOCITY,
                                    g_param_spec_boolean("same-velocity", "Same velocity", "Same velocity for upper and lower",
                                            TRUE, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_LOWER_CHANNEL,
                                    g_param_spec_int("lower-channel", "Lower channel", "Lower keyboard MIDI channel",
                                            0, 15, 0, G_PARAM_READWRITE));
    g_object_class_install_property(obj_class, PROP_UPPER_CHANNEL,
                                    g_param_spec_int("upper-channel", "Upper channel", "Upper keyboard MIDI channel",
                                            0, 15, 0, G_PARAM_READWRITE));

    ignore_widget_types = g_new(GType, G_N_ELEMENTS(ignore_widget_type_names) + 1);

    for(i = 0; i < G_N_ELEMENTS(ignore_widget_type_names); i++)
    {
        ignore_widget_types[i] = g_type_from_name(ignore_widget_type_names[i]);
    }

    ignore_widget_types[i] = G_TYPE_INVALID;
}

static void
swamigui_control_midi_key_get_property(GObject *object, guint property_id,
                                       GValue *value, GParamSpec *pspec)
{
    SwamiguiControlMidiKey *keyctrl = SWAMIGUI_CONTROL_MIDI_KEY(object);

    switch(property_id)
    {
    case PROP_LOWER_OCTAVE:
        g_value_set_int(value, keyctrl->lower_octave);
        break;

    case PROP_UPPER_OCTAVE:
        g_value_set_int(value, keyctrl->upper_octave);
        break;

    case PROP_JOIN_OCTAVES:
        g_value_set_boolean(value, keyctrl->join_octaves);
        break;

    case PROP_LOWER_VELOCITY:
        g_value_set_int(value, keyctrl->lower_velocity);
        break;

    case PROP_UPPER_VELOCITY:
        g_value_set_int(value, keyctrl->upper_velocity);
        break;

    case PROP_SAME_VELOCITY:
        g_value_set_boolean(value, keyctrl->same_velocity);
        break;

    case PROP_LOWER_CHANNEL:
        g_value_set_int(value, keyctrl->lower_channel);
        break;

    case PROP_UPPER_CHANNEL:
        g_value_set_int(value, keyctrl->upper_channel);
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

static void
swamigui_control_midi_key_set_property(GObject *object, guint property_id,
                                       const GValue *value, GParamSpec *pspec)
{
    SwamiguiControlMidiKey *keyctrl = SWAMIGUI_CONTROL_MIDI_KEY(object);

    switch(property_id)
    {
    case PROP_LOWER_OCTAVE:
        keyctrl->lower_octave = g_value_get_int(value);

        if(keyctrl->join_octaves)
        {
            keyctrl->upper_octave = keyctrl->lower_octave < 8 ? keyctrl->lower_octave + 1 : 8;
            g_object_notify(object, "upper-octave");
        }

        break;

    case PROP_UPPER_OCTAVE:
        keyctrl->upper_octave = g_value_get_int(value);

        if(keyctrl->join_octaves)
        {
            keyctrl->lower_octave = keyctrl->upper_octave > -2 ? keyctrl->upper_octave - 1 : -2;
            g_object_notify(object, "lower-octave");
        }

        break;

    case PROP_JOIN_OCTAVES:
        keyctrl->join_octaves = g_value_get_boolean(value);

        if(keyctrl->join_octaves
                && keyctrl->lower_octave + 1 != keyctrl->upper_octave)
        {
            keyctrl->upper_octave = keyctrl->lower_octave < 8 ? keyctrl->lower_octave + 1 : 8;
            g_object_notify(object, "upper-octave");
        }

        break;

    case PROP_LOWER_VELOCITY:
        keyctrl->lower_velocity = g_value_get_int(value);

        if(keyctrl->same_velocity)
        {
            keyctrl->upper_velocity = keyctrl->lower_velocity;
            g_object_notify(object, "upper-velocity");
        }

        break;

    case PROP_UPPER_VELOCITY:
        keyctrl->upper_velocity = g_value_get_int(value);

        if(keyctrl->same_velocity)
        {
            keyctrl->lower_velocity = keyctrl->upper_velocity;
            g_object_notify(object, "lower-velocity");
        }

        break;

    case PROP_SAME_VELOCITY:
        keyctrl->same_velocity = g_value_get_boolean(value);

        if(keyctrl->same_velocity
                && keyctrl->lower_velocity != keyctrl->upper_velocity)
        {
            keyctrl->upper_velocity = keyctrl->lower_velocity;
            g_object_notify(object, "upper-velocity");
        }

        break;

    case PROP_LOWER_CHANNEL:
        keyctrl->lower_channel = g_value_get_int(value);
        break;

    case PROP_UPPER_CHANNEL:
        keyctrl->upper_channel = g_value_get_int(value);
        break;

    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
        break;
    }
}

static void
swamigui_control_midi_key_init(SwamiguiControlMidiKey *keyctrl)
{
    MidiKey midikey;
    int i;

    swami_control_set_flags(SWAMI_CONTROL(keyctrl), SWAMI_CONTROL_SENDS);

    keyctrl->lower_keys = g_array_new(FALSE, FALSE, sizeof(MidiKey));
    keyctrl->upper_keys = g_array_new(FALSE, FALSE, sizeof(MidiKey));
    keyctrl->lower_octave = DEFAULT_LOWER_OCTAVE;
    keyctrl->upper_octave = DEFAULT_UPPER_OCTAVE;
    keyctrl->join_octaves = TRUE;
    keyctrl->lower_velocity = 127;
    keyctrl->upper_velocity = 127;
    keyctrl->same_velocity = TRUE;
    keyctrl->lower_channel = 0;
    keyctrl->upper_channel = 0;

    /* initialize lower and upper key array */

    for(i = 0; i < G_N_ELEMENTS(default_lower_keys); i++)
    {
        midikey.key = default_lower_keys[i];
        midikey.active_note = -1;
        g_array_append_val(keyctrl->lower_keys, midikey);
    }

    for(i = 0; i < G_N_ELEMENTS(default_upper_keys); i++)
    {
        midikey.key = default_upper_keys[i];
        midikey.active_note = -1;
        g_array_append_val(keyctrl->upper_keys, midikey);
    }

    /* install our key snooper */
    keyctrl->snooper_id =
        gtk_key_snooper_install(swamigui_control_midi_key_snooper, keyctrl);
}

static gint
swamigui_control_midi_key_snooper(GtkWidget *grab_widget, GdkEventKey *event,
                                  gpointer func_data)
{
    SwamiguiControlMidiKey *keyctrl = SWAMIGUI_CONTROL_MIDI_KEY(func_data);
    GtkWidget *toplevel, *focus;
    GType *ignore;
    guint key;

    if(event->state & (GDK_CONTROL_MASK | GDK_MOD1_MASK))
    {
        return (FALSE);
    }

    key = gdk_keyval_to_lower(event->keyval);

    if(event->type == GDK_KEY_PRESS)
    {
        toplevel = gtk_widget_get_toplevel(grab_widget);

        /* Only listen to key presses on main window, not dialogs (allow releases though) */
        if(toplevel != swamigui_root->main_window)
        {
            return (FALSE);
        }

        /* Check if focus widget type should be ignored (text entries, etc) */
        focus = gtk_window_get_focus(GTK_WINDOW(toplevel));

        if(focus)
        {
            for(ignore = ignore_widget_types; *ignore != G_TYPE_INVALID; ignore++)
            {
                if(g_type_is_a(G_OBJECT_TYPE(focus), *ignore))
                {
                    return (FALSE);
                }
            }
        }

        swamigui_control_midi_key_press(keyctrl, key);
    }
    else if(event->type == GDK_KEY_RELEASE)
    {
        swamigui_control_midi_key_release(keyctrl, key);
    }

    return (FALSE);	/* don't stop event propagation */
}

static void
swamigui_control_midi_key_finalize(GObject *object)
{
    SwamiguiControlMidiKey *keyctrl = SWAMIGUI_CONTROL_MIDI_KEY(object);

    gtk_key_snooper_remove(keyctrl->snooper_id);

    g_array_free(keyctrl->lower_keys, TRUE);
    g_array_free(keyctrl->upper_keys, TRUE);

    if(G_OBJECT_CLASS(swamigui_control_midi_key_parent_class)->finalize)
    {
        G_OBJECT_CLASS(swamigui_control_midi_key_parent_class)->finalize(object);
    }
}

/**
 * swamigui_control_midi_key_new:
 *
 * Create a new MIDI keyboard control.
 *
 * Returns: New MIDI keyboard control with a refcount of 1 which the
 * caller owns.
 */
SwamiguiControlMidiKey *
swamigui_control_midi_key_new(void)
{
    return (SWAMIGUI_CONTROL_MIDI_KEY
            (g_object_new(SWAMIGUI_TYPE_CONTROL_MIDI_KEY, NULL)));
}

/**
 * swamigui_control_midi_key_press:
 * @keyctrl: MIDI keyboard control
 * @key: GDK keyboard key (see gdk/gdkkeysyms.h header)
 *
 * Send a key press to a MIDI keyboard control.
 */
void
swamigui_control_midi_key_press(SwamiguiControlMidiKey *keyctrl, guint key)
{
    MidiKey *midikey, *found = NULL;
    int newnote, newvel, newchan;
    int i, count;

    g_return_if_fail(SWAMIGUI_IS_CONTROL_MIDI_KEY(keyctrl));

    /* look for key in lower octave */
    count = keyctrl->lower_keys->len;

    for(i = 0; i < count; i++)
    {
        midikey = &g_array_index(keyctrl->lower_keys, MidiKey, i);

        if(midikey->key == key)	/* found key? */
        {
            newnote = (keyctrl->lower_octave + 2) * 12 + i; /* calculate MIDI note */
            newvel = keyctrl->lower_velocity;
            newchan = keyctrl->lower_channel;
            found = midikey;
            break;
        }
    }

    if(!found)			/* not found in lower octave? */
    {
        /* look for key in upper octave */
        count = keyctrl->upper_keys->len;

        for(i = 0; i < count; i++)
        {
            midikey = &g_array_index(keyctrl->upper_keys, MidiKey, i);

            if(midikey->key == key)	/* found key? */
            {
                newnote = (keyctrl->upper_octave + 2) * 12 + i; /* calc MIDI note */
                newvel = keyctrl->upper_velocity;
                newchan = keyctrl->upper_channel;
                found = midikey;
                break;
            }
        }
    }

    /* return if key not found or already active and the same as new note */
    if(!found || found->active_note == newnote)
    {
        return;
    }

    if(found->active_note != -1)	/* previous active note? */
    {
        /* send note off event */
        swami_control_midi_transmit(SWAMI_CONTROL_MIDI(keyctrl),
                                    SWAMI_MIDI_NOTE_OFF, found->active_chan,
                                    found->active_note, 0x7F);
        found->active_note = -1;
    }

    if(newnote <= 127)
    {
        /* send note on event */
        swami_control_midi_transmit(SWAMI_CONTROL_MIDI(keyctrl),
                                    SWAMI_MIDI_NOTE_ON, newchan,
                                    newnote, newvel);
        found->active_note = newnote;
        found->active_chan = newchan;
    }
}

/**
 * swamigui_control_midi_key_release:
 * @keyctrl: MIDI keyboard control
 * @key: GDK keyboard key (see gdk/gdkkeysyms.h header)
 *
 * Send a key release to a MIDI keyboard control.
 */
void
swamigui_control_midi_key_release(SwamiguiControlMidiKey *keyctrl, guint key)
{
    MidiKey *midikey, *found = NULL;
    int i, count;

    g_return_if_fail(SWAMIGUI_IS_CONTROL_MIDI_KEY(keyctrl));

    /* look for key in lower octave */
    count = keyctrl->lower_keys->len;

    for(i = 0; i < count; i++)
    {
        midikey = &g_array_index(keyctrl->lower_keys, MidiKey, i);

        if(midikey->key == key)	/* found key? */
        {
            found = midikey;
            break;
        }
    }

    if(!found)			/* not found in lower octave? */
    {
        /* look for key in upper octave */
        count = keyctrl->upper_keys->len;

        for(i = 0; i < count; i++)
        {
            midikey = &g_array_index(keyctrl->upper_keys, MidiKey, i);

            if(midikey->key == key)	/* found key? */
            {
                found = midikey;
                break;
            }
        }
    }

    if(!found || found->active_note == -1)
    {
        return;
    }

    /* send note off event */
    swami_control_midi_transmit(SWAMI_CONTROL_MIDI(keyctrl),
                                SWAMI_MIDI_NOTE_OFF, found->active_chan,
                                found->active_note, 0x7F);
    found->active_note = -1;
}

/**
 * swamigui_control_midi_key_set_lower:
 * @keyctrl: MIDI keyboard control
 * @keys: Array of GDK key values
 * @count: Number of values in @keys
 *
 * Set the lower keyboard key array.
 *
 * Note: Not multi-thread safe
 */
void
swamigui_control_midi_key_set_lower(SwamiguiControlMidiKey *keyctrl,
                                    const guint *keys, guint count)
{
    MidiKey midikey;
    guint i;

    g_return_if_fail(SWAMIGUI_IS_CONTROL_MIDI_KEY(keyctrl));
    g_return_if_fail(keys != NULL || count == 0);

    g_array_set_size(keyctrl->lower_keys, 0);

    for(i = 0; i < count; i++)
    {
        midikey.key = keys[i];
        midikey.active_note = -1;
        g_array_append_val(keyctrl->lower_keys, midikey);
    }
}

/**
 * swamigui_control_midi_key_set_upper:
 * @keyctrl: MIDI keyboard control
 * @keys: Array of GDK key values
 * @count: Number of values in @keys
 *
 * Set the upper keyboard key array.
 *
 * Note: Not multi-thread safe
 */
void
swamigui_control_midi_key_set_upper(SwamiguiControlMidiKey *keyctrl,
                                    const guint *keys, guint count)
{
    MidiKey midikey;
    guint i;

    g_return_if_fail(SWAMIGUI_IS_CONTROL_MIDI_KEY(keyctrl));
    g_return_if_fail(keys != NULL || count == 0);

    g_array_set_size(keyctrl->upper_keys, 0);

    for(i = 0; i < count; i++)
    {
        midikey.key = keys[i];
        midikey.active_note = -1;
        g_array_append_val(keyctrl->upper_keys, midikey);
    }
}
