/*
 *  Copyright (c) 2009 Tomasz Moń <desowin@gmail.com>
 *
 *  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; under version 3 of the License.
 *
 *  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 <stdio.h>
#include <gtk/gtk.h>
#include <gio/gio.h>
#include <getopt.h>
#include <alsa/asoundlib.h>
#include <alloca.h>
#include "gdigi.h"
#include "gdigi_xml.h"
#include "gui.h"

static unsigned char device_id = 0x7F;
static unsigned char family_id = 0x7F;
unsigned char product_id = 0x7F;

static snd_rawmidi_t *output = NULL;
static snd_rawmidi_t *input = NULL;
static char *device_port = NULL;

static GQueue *message_queue = NULL;
static GMutex *message_queue_mutex = NULL;
static GCond *message_queue_cond = NULL;

static guint DebugFlags;

gboolean
debug_flag_is_set (debug_flags_t flags)
{
    if (DebugFlags & flags) {
        return TRUE;
    }
    return FALSE;
}

gboolean set_debug_flags (const gchar *option_name, const gchar *value,
                          gpointer data, GError **error)
{
    if (strchr(value, 'd')) {
        DebugFlags |= DEBUG_MSG2DEV;
    }
    if (strchr(value, 'h')) {
        DebugFlags |= DEBUG_MSG2HOST;
    }
    if (strchr(value, 'm')) {
        DebugFlags |= DEBUG_MSG2DEV|DEBUG_MSG2HOST|DEBUG_GROUP;
    }
    if (strchr(value, 's')) {
        DebugFlags |= DEBUG_STARTUP;
    }
    if (strchr(value, 'H')) {
        DebugFlags |= DEBUG_HEX;
    }
    if (strchr(value, 'g')) {
        DebugFlags |= DEBUG_GROUP;
    }
    if (strchr(value, 'x')) {
        DebugFlags |= DEBUG_XML;
    }
    if (strchr(value, 'v')) {
        DebugFlags |= DEBUG_VERBOSE;
    }
    if (strchr(value, 'a')) {
        DebugFlags = -1;
    }

    return TRUE;
}

void
debug_msg (debug_flags_t flags, char *fmt, ...)
{
    char buf[1024];
    if (flags & DebugFlags) {
        va_list ap;

        va_start(ap, fmt);
        vsnprintf(buf, 1024, fmt, ap);
        va_end(ap);

        fprintf(stderr, "%s\n", buf);
    }
}

/*
 * Format a value according to the xml setting.
 * Returns an allocated buffer that must be freed by the caller.
 */
GString *
format_value (XmlSettings *xml, guint value)
{
    GString        *buf = g_string_sized_new(1);
    EffectValues   *values = NULL;
    ValueType       vtype;
    gchar          *suffix = "";
    gdouble         step = 1.0;
    gint            offset = 0;
    gboolean        decimal = FALSE;

    values = xml->values;
    vtype = values->type;
    while ((vtype & VALUE_TYPE_EXTRA) && value_is_extra(values, value)) {
        values = values->extra;
        vtype = values->type;
    }
    vtype &= ~VALUE_TYPE_EXTRA;

    if (vtype & VALUE_TYPE_OFFSET) {
        offset = values->offset;
        vtype &= ~VALUE_TYPE_OFFSET;
    }

    if (vtype & VALUE_TYPE_STEP) {
        step = values->step;
        vtype &= ~VALUE_TYPE_STEP;
    }

    if (vtype & VALUE_TYPE_SUFFIX) {
        suffix = values->suffix;
        vtype &= ~VALUE_TYPE_SUFFIX;
    }

    if (vtype & VALUE_TYPE_DECIMAL) {
        decimal = TRUE;
        vtype &= ~VALUE_TYPE_DECIMAL;
    }

    switch (vtype) {
    case VALUE_TYPE_LABEL:
    {
        char *textp = map_xml_value(xml, values, value);
        if (!textp) {
            g_warning("%s: Unable to map %s value %d for id %d position %d",
                      __FUNCTION__, xml->label, value, xml->id, xml->position);
            textp = "";
        }
        g_string_printf(buf, "%s", textp);
        break;
    }
    case VALUE_TYPE_PLAIN:
    {
        if (decimal) {
            double dvalue = ((gint)value + offset) * step;
                g_string_printf(buf, "%0.2f%s", dvalue, suffix);
        } else {
            gint ivalue = ((gint)value + offset) * step;
            g_string_printf(buf, "%d%s", ivalue, suffix);
        }
        break;
    }
    case VALUE_TYPE_NONE:
        g_string_printf(buf, "%s", "");
        break;

    case VALUE_TYPE_POSID:
        g_string_printf(buf, "%d", value);
        break;

    default:
        g_warning("Unhandled value type %d", vtype);
        break;
    }

    return buf;
}

GString *
format_ipv (guint id, guint pos, guint val)
{
    GString *buf = g_string_sized_new(1);
    GString *vec_buf = g_string_sized_new(1);
    XmlSettings *xml = get_xml_settings(id, pos);
    GString *val_buf;

    if (!xml) {
        g_warning("Failed to find xml settings for position %d id %d.",
                   pos, id);
        g_string_printf(buf, "%s", "error");
        return buf;
    }
    val_buf = format_value(xml, val);

    g_string_printf(vec_buf, "(%d, %d, %d)", pos, id, val);
    g_string_printf(buf, "%-16s %s: %s: %s",
                          vec_buf->str,
                          get_position(pos), xml->label, val_buf->str);
    g_string_free(vec_buf, TRUE);
    g_string_free(val_buf, TRUE);
    return buf;
}

/**
 *  Registers an error quark for gdigi if necessary.
 *
 *  \return error quark used for gdigi errors
 **/
static GQuark gdigi_error_quark()
{
    static GQuark quark = 0;

    if (quark == 0) {
        quark = g_quark_from_static_string("gdigi-error");
    }

    return quark;
}

/**
 *  \param array data to calculate checksum
 *  \param length data length
 *
 *  Calculates message checksum.
 *
 *  \return calculated checksum.
 **/
static char calculate_checksum(gchar *array, gint length)
{
    int x;
    int checksum = 0;

    for (x = 0; x<length; x++) {
        checksum ^= array[x];
    }

    return checksum;
}

/**
 *  Opens MIDI device. This function modifies global input and output variables.
 *
 *  \return FALSE on success, TRUE on error.
 **/
gboolean open_device()
{
    int err;

    err = snd_rawmidi_open(&input, &output, device_port, SND_RAWMIDI_SYNC);
    if (err) {
        fprintf(stderr, "snd_rawmidi_open %s failed: %d\n", device_port, err);
        return TRUE;
    }

    err = snd_rawmidi_nonblock(output, 0);
    if (err) {
        fprintf(stderr, "snd_rawmidi_nonblock failed: %d\n", err);
        return TRUE;
    }

    snd_rawmidi_read(input, NULL, 0); /* trigger reading */

    return FALSE;
}

/**
 *  \param data data to be sent
 *  \param length data length
 *
 *  Sends data to device. This function uses global output variable.
 **/
void send_data(char *data, int length)
{
    snd_rawmidi_write(output, data, length);
}

/**
 *  \param data data to be packed
 *  \param len data length
 *
 *  Packs data using method used on all newer DigiTech products.
 *
 *  \return GString containing packed data
 **/
GString *pack_data(gchar *data, gint len)
{
    GString *packed;
    gint i;
    gint new_len;
    unsigned char status;
    gint status_byte;

    new_len = len + (len/7);
    packed = g_string_sized_new(new_len);
    status = 0;
    status_byte = 0;

    for (i=0; i<len; i++) {
        if ((i % 7) == 0) {
            packed->str[status_byte] = status;
            status = 0;
            status_byte = packed->len;
            g_string_append_c(packed, '\0');
        }
        g_string_append_c(packed, (data[i] & 0x7F));
        status |= (data[i] & 0x80) >> ((i%7) + 1);
    }
    packed->str[status_byte] = status;

    return packed;
}

static void message_free_func(GString *msg, gpointer user_data)
{
    g_string_free(msg, TRUE);
}

/**
 *  \param msg message to unpack
 *
 *  Unpacks message data. This function modifies given GString.
 **/
static void unpack_message(GString *msg)
{
    int offset;
    int x;
    int i;
    unsigned char status;
    unsigned char *str;
    gboolean stop = FALSE;

    g_return_if_fail(msg != NULL);
    g_return_if_fail(msg->len > 9);

    offset = 1;
    x = 0;
    i = 8;

    str = (unsigned char*)msg->str;
    do {
        offset += 8;
        status = str[offset-1];
        for (x=0; x<7 && !stop; x++) {
            if (offset+x >= msg->len) {
                i++;
                stop = TRUE;
                break;
            }
            if (str[offset+x] == 0xF7) {
                if (x == 0) {
                    str[i] = status;
                    i++;
                }
                str[i] = 0xF7;
                i++;
                stop = TRUE;
                break;
            }

            str[i] = (((status << (x+1)) & 0x80) | str[x+offset]);
            i++;
        }
    } while (!stop);

    g_string_truncate(msg, i);
}

/**
 *  \param msg SysEx message
 *
 *  Checks message ID.
 *
 *  \return MessageID, or -1 on error.
 **/
MessageID get_message_id(GString *msg)
{
    /** \todo check if msg is valid SysEx message */
    g_return_val_if_fail(msg != NULL, -1);
    g_return_val_if_fail(msg->str != NULL, -1);

    if (msg->len > 7) {
        return (unsigned char)msg->str[7];
    }
    return -1;
}

#define HEX_WIDTH 26

static gboolean modifier_linkable_list_request_pending = FALSE;

void push_message(GString *msg)
{
    MessageID msgid = get_message_id(msg);
    if (((unsigned char)msg->str[0] == 0xF0) &&
            ((unsigned char)msg->str[msg->len-1] == 0xF7)) {
        debug_msg(DEBUG_VERBOSE, "Pushing correct message!");
    } else {
        g_warning("Pushing incorrect message!");
    }

    int x;
    if (debug_flag_is_set(DEBUG_HEX)) {
        for (x = 0; x<msg->len; x++) {
            if (x && (x % HEX_WIDTH) == 0) {
                printf("\n");
            }
            printf("%02x ", (unsigned char)msg->str[x]);
        }
        if (x % HEX_WIDTH) {
            printf("\n");
        }
    }
    debug_msg(DEBUG_VERBOSE, "Received %s", get_message_name(msgid));

    SettingParam *param;
    switch (msgid) {
        case ACK:
            g_string_free(msg, TRUE);
            return;

        case NACK:
            g_warning("Received NACK!");
            g_string_free(msg, TRUE);
            return;

        case RECEIVE_PARAMETER_VALUE:
        {
            unpack_message(msg);
            param = setting_param_new_from_data(&msg->str[8], NULL);
            if (debug_flag_is_set(DEBUG_MSG2HOST)) {
                GString *ipv = format_ipv(param->id,
                                          param->position,
                                          param->value);
                debug_msg(DEBUG_MSG2HOST, "RECEIVE_PARAMETER_VALUE\n%s",
                                          ipv->str);
                g_string_free(ipv, TRUE);
            }

            GDK_THREADS_ENTER();
            apply_setting_param_to_gui(param);
            GDK_THREADS_LEAVE();

            setting_param_free(param);
            g_string_free(msg, TRUE);
            return;
        }

        case RECEIVE_DEVICE_NOTIFICATION:
            unpack_message(msg);
            unsigned char *str = (unsigned char*)msg->str;
            switch (str[8]) {
            case NOTIFY_PRESET_MOVED:
                if (str[11] == PRESETS_EDIT_BUFFER && str[12] == 0) {

                    GDK_THREADS_ENTER();
                    g_timeout_add(0, apply_current_preset_to_gui, NULL);
                    GDK_THREADS_LEAVE();
                    debug_msg(DEBUG_MSG2HOST,
                              "RECEIVE_DEVICE_NOTIFICATION: Loaded preset "
                              "%d from bank %d",
                              str[10], str[9]);
                } else {
                    debug_msg(DEBUG_MSG2HOST,
                              "RECEIVE_DEVICE_NOTIFICATION: %d %d moved to "
                              "%d %d",
                              str[9], str[10],
                              str[11], str[12]);
                }
                break;

            case NOTIFY_MODIFIER_GROUP_CHANGED:
            {
                int i;
                if (debug_flag_is_set(DEBUG_HEX)) {
                    printf("\n");
                    for (i = 0; i < msg->len; i++) {
                        printf(" %02x", (unsigned char) str[i]);
                    }
                    printf("\n");
                }

                debug_msg(DEBUG_MSG2HOST,
                          "NOTIFY_MODIFIER_GROUP_CHANGED: Modifier group "
                          "id %d changed",
                          (str[9] << 8) | (str[10]));

                if (!modifier_linkable_list_request_pending) {
                    send_message(REQUEST_MODIFIER_LINKABLE_LIST, "\x00\x01", 2);
                    modifier_linkable_list_request_pending = TRUE;
                }

                break;
            }
            default:
                g_warning("Received unhandled device notification 0x%x",
                          str[11]);
            }
            g_string_free(msg, TRUE);
            return;
        case RECEIVE_GLOBAL_PARAMETERS:
            unpack_message(msg);
            gint tot, n, x;
            tot = (unsigned char)msg->str[9];
            if (debug_flag_is_set(DEBUG_HEX)) {
                for (n = 0; n < msg->len; n++) {
                    printf("%02x ",(unsigned char) msg->str[n]);
                }
                printf("\n");
            }

            n = 0;
            x = 10;
            do {
                param = setting_param_new_from_data(&msg->str[x], &x);
                debug_msg(DEBUG_MSG2HOST,
                          "RECEIVE_GLOBAL_PARAMETERS ID: %5d "
                          "Position: %2.1d Value: %6.1d: %s",
                          param->id,
                          param->position, param->value, "XXX");

                GDK_THREADS_ENTER();
                apply_setting_param_to_gui(param);
                GDK_THREADS_LEAVE();

                setting_param_free(param);
            } while ( (x < msg->len) && n < tot);

            g_string_free(msg, TRUE);
            return;


        case RECEIVE_MODIFIER_LINKABLE_LIST:

            modifier_linkable_list_request_pending = FALSE;
            unpack_message(msg);
            tot = (unsigned char)msg->str[9];

            if (debug_flag_is_set(DEBUG_HEX)) {
                for (n = 0; n < msg->len; n++) {
                    printf("%02x ",(unsigned char) msg->str[n]);
                }
                printf("\n");
            }


            update_modifier_linkable_list(msg);

            g_string_free(msg, TRUE);

            GDK_THREADS_ENTER();

            create_modifier_group(EXP_POSITION, EXP_ASSIGN1);
            create_modifier_group(LFO1_POSITION, LFO_TYPE);
            create_modifier_group(LFO2_POSITION, LFO_TYPE);

            GDK_THREADS_LEAVE();

            return;


        default:
            g_mutex_lock(message_queue_mutex);
            g_queue_push_tail(message_queue, msg);
            g_cond_signal(message_queue_cond);
            g_mutex_unlock(message_queue_mutex);
            break;
    }
}

gpointer read_data_thread(gboolean *stop)
{
    /* This is mostly taken straight from alsa-utils-1.0.19 amidi/amidi.c
       by Clemens Ladisch <clemens@ladisch.de> */
    int err;
    int npfds;
    struct pollfd *pfds;
    GString *string = NULL;

    npfds = snd_rawmidi_poll_descriptors_count(input);
    pfds = alloca(npfds * sizeof(struct pollfd));
    snd_rawmidi_poll_descriptors(input, pfds, npfds);

    do {
        unsigned char buf[256];
        int i, length;
        unsigned short revents;

        /* SysEx messages can't contain bytes with 8th bit set.
           memset our buffer to 0xFF, so if for some reason we'll
           get out of reply bounds, we'll catch it */
        memset(buf, '\0', sizeof(buf));

        err = poll(pfds, npfds, 200);
        if (err < 0 && errno == EINTR)
            break;
        if (err < 0) {
            g_error("poll failed: %s", strerror(errno));
            break;
        }
        if ((err = snd_rawmidi_poll_descriptors_revents(input, pfds, npfds, &revents)) < 0) {
            g_error("cannot get poll events: %s", snd_strerror(errno));
            break;
        }
        if (revents & (POLLERR | POLLHUP))
            break;
        if (!(revents & POLLIN))
            continue;

        err = snd_rawmidi_read(input, buf, sizeof(buf));
        if (err == -EAGAIN)
            continue;
        if (err < 0) {
            g_error("cannot read: %s", snd_strerror(err));
            break;
        }

        length = 0;
        for (i = 0; i < err; ++i)
            if ((unsigned char)buf[i] != 0xFE) /* ignore active sensing */
                buf[length++] = buf[i];

        i = 0;

        while (i < length) {
            int pos;
            int bytes;

            if (string == NULL) {
                while (buf[i] != 0xF0 && i < length)
                    i++;
            }

            pos = i;

            for (bytes = 0; (bytes<length-i) && (buf[i+bytes] != 0xF7); bytes++);

            if (buf[i+bytes] == 0xF7) bytes++;

            i += bytes;

            if (string == NULL)
                string = g_string_new_len((gchar*)&buf[pos], bytes);
            else
                g_string_append_len(string, (gchar*)&buf[pos], bytes);

            if ((unsigned char)string->str[string->len-1] == 0xF7) {
                /* push message on stack */
                push_message(string);
                string = NULL;
            }
        }
    } while (*stop == FALSE);

    if (string) {
        g_string_free(string, TRUE);
        string = NULL;
    }

    return NULL;
}

/**
 *  \param procedure procedure ID
 *  \param data unpacked message data
 *  \param len data length
 *
 *  Creates SysEx message then sends it. This function uses folowing global variables: device_id, family_id and product_id.
 **/
void send_message(gint procedure, gchar *data, gint len)
{
    GString *msg = g_string_new_len("\xF0"          /* SysEx status byte */
                                    "\x00\x00\x10", /* Manufacturer ID   */
                                    4);
    g_string_append_printf(msg,
                           "%c%c%c"   /* device, family, product ID */
                           "%c",      /* procedure */
                           device_id, family_id, product_id,
                           procedure);

    if (len > 0) {
        GString *tmp = pack_data(data, len);
        g_string_append_len(msg, tmp->str, tmp->len);
        g_string_free(tmp, TRUE);
    }

    g_string_append_printf(msg, "%c\xF7",
                           calculate_checksum(&msg->str[1], msg->len - 1));

    debug_msg(DEBUG_VERBOSE, "Sending %s len %d",
                              get_message_name(procedure), len);

    send_data(msg->str, msg->len);

    g_string_free(msg, TRUE);
}

/**
 *  \param id MessageID of requested message
 *
 *  Reads data from message queue until message with matching id is found.
 *
 *  \return GString containing unpacked message.
 **/
GString *get_message_by_id(MessageID id)
{
    GString *data = NULL;
    guint x, len;
    gboolean found = FALSE;

    g_mutex_lock(message_queue_mutex);
    do {
        len = g_queue_get_length(message_queue);
        for (x = 0; x<len; x++) {
            data = g_queue_peek_nth(message_queue, x);
            if (get_message_id(data) == id) {
                found = TRUE;
                g_queue_pop_nth(message_queue, x);
                break;
            }
        }

        if (found == FALSE)
            g_cond_wait(message_queue_cond, message_queue_mutex);

    } while (found == FALSE);
    g_mutex_unlock(message_queue_mutex);

    unpack_message(data);

    return data;
}

/**
 *  \param msg message to append value
 *  \param value value to append
 *
 *  Packs value using scheme used on all newer DigiTech products.
 **/
void append_value(GString *msg, guint value)
{
    /* check how many bytes long the value is */
    guint temp = value;
    gint n = 0;
    do {
        n++;
        temp = temp >> 8;
    } while (temp);

    if (n == 1) {
        if (value & 0x80)
            n = 2;
        else
            g_string_append_printf(msg, "%c", value);
    }

    if (n > 1) {
        gint x;
        g_string_append_printf(msg, "%c", (n | 0x80));
        for (x=0; x<n; x++) {
            g_string_append_printf(msg, "%c",
                                   ((value >> (8*(n-x-1))) & 0xFF));
        }
    }
}

/**
 *  \param str pointer to value to unpack
 *  \param len return location for how many bytes value is encoded on (length is added to current value)
 *
 *  Unpacks value using scheme used on all newer DigiTech products.
 *
 *  \return unpacked value
 **/
guint unpack_value(gchar *str, int *len)
{
    guint value;
    gint tmp;

    value = (unsigned char)str[0];
    if (len != NULL)
       *len += 1;

    if (value > 0x80) {
        tmp = value & 0x7F;
        value = 0;
        gint i;
        for (i = 0; i<tmp; i++) {
            value |= ((unsigned char)str[1+i] << (8*(tmp-i-1)));
        }

        if (len != NULL)
            *len += tmp;
    }

    return value;
}

/**
 *  Allocates memory for SettingParam.
 *
 *  \return SettingParam which must be freed using setting_param_free.
 **/
SettingParam *setting_param_new()
{
    SettingParam *param = g_slice_new(SettingParam);
    param->id = -1;
    param->position = -1;
    param->value = -1;

    return param;
}

/**
 *  \param str pointer to setting param in message
 *  \param len return location for how many bytes value is encoded on (length is added to current value)
 *
 *  Creates SettingParam basing on data.
 *  This function expects str to point on:
 *    -Parameter ID - 2 bytes
 *    -Parameter position - 1 byte
 *    -Parameter value - var
 *
 *  \return newly created SettingParam which must be freed using setting_param_free.
 **/
SettingParam *setting_param_new_from_data(gchar *str, gint *len)
{
    gint id;
    gint position;
    guint value;

    id = ((unsigned char)str[0] << 8) | (unsigned char)str[1];
    position = (unsigned char)str[2];
    if (len != NULL)
       *len += 3;

    value = unpack_value(&str[3], len);

    SettingParam *param = g_slice_new(SettingParam);
    param->id = id;
    param->position = position;
    param->value = value;

    return param;
}

/**
 *  \param param SettingParam to be freed
 *
 *  Frees all memory used by SettingParam.
 **/
void setting_param_free(SettingParam *param)
{
    g_slice_free(SettingParam, param);
}

/**
 *  Allocates memory for SettingGenetx.
 *
 *  \return SettingGenetx which must be freed using setting_genetx_free.
 **/
SettingGenetx *setting_genetx_new()
{
    SettingGenetx *genetx = g_slice_new(SettingGenetx);
    /* Older patches don't specify GeNetX version */
    genetx->version = GENETX_VERSION_1;
    genetx->type = GENETX_TYPE_NOT_SET;
    genetx->channel = -1;
    genetx->name = NULL;
    genetx->data = NULL;

    return genetx;
}

/**
 *  \param genetx SettingGenetx to be freed
 *
 *  Frees all memory used by SettingGenetx.
 **/
void setting_genetx_free(SettingGenetx *genetx)
{
    g_free(genetx->name);
    if (genetx->data != NULL) {
        g_string_free(genetx->data, TRUE);
    }
    g_slice_free(SettingGenetx, genetx);
}

/**
 *  \param version GeNetX version
 *  \param type GeNetX type
 *
 *  Retrieves SectionID for specified GeNetX version and type.
 *
 *  \return SectionID specified by version and type, or -1 on error.
 **/
SectionID get_genetx_section_id(gint version, gint type)
{
    if (version == GENETX_VERSION_1) {
        if (type == GENETX_TYPE_AMP) {
            return SECTION_GENETX_AMP;
        } else if (type == GENETX_TYPE_CABINET) {
            return SECTION_GENETX_CABINET;
        }
    } else if (version == GENETX_VERSION_2) {
        if (type == GENETX_TYPE_AMP) {
            return SECTION_GENETX2_AMP;
        } else if (type == GENETX_TYPE_CABINET) {
            return SECTION_GENETX2_CABINET;
        }
    }

    g_warning("This version of gdigi don't know what to do with this "
              "GeNetX version (%d) and type (%d)", version, type);

    return -1;
}

/**
 *  \param id Parameter ID
 *  \param position Parameter position
 *  \param value Parameter value
 *
 *  Forms SysEx message to request parameter then sends it to device.
 **/
void get_option(guint id, guint position)
{
    GString *msg = g_string_sized_new(9);
    debug_msg(DEBUG_MSG2DEV, "REQUEST_PARAMETER_VALUE: id %d position %d",
                              id, position);
    g_string_append_printf(msg, "%c%c%c",
                           ((id & 0xFF00) >> 8), (id & 0xFF),
                           position);
    send_message(REQUEST_PARAMETER_VALUE, msg->str, msg->len);
    g_string_free(msg, TRUE);
}
/**
 *  \param id Parameter ID
 *  \param position Parameter position
 *  \param value Parameter value
 *
 *  Forms SysEx message to set parameter then sends it to device.
 **/
void set_option(guint id, guint position, guint value)
{
    GString *msg = g_string_sized_new(9);
    g_string_append_printf(msg, "%c%c%c",
                           ((id & 0xFF00) >> 8), (id & 0xFF),
                           position);
    append_value(msg, value);
    if (debug_flag_is_set(DEBUG_MSG2DEV)) {
        GString *ipv = format_ipv(id, position, value);
        debug_msg(DEBUG_MSG2DEV, "RECEIVE_PARAMETER_VALUE\n%s", ipv->str);
        g_string_free(ipv, TRUE);
    }
    send_message(RECEIVE_PARAMETER_VALUE, msg->str, msg->len);
    g_string_free(msg, TRUE);
}

/**
 *  \param section data section ID
 *  \param bank section-specific bank number
 *  \param index index of the desired object within the bank
 *  \param name object name
 *  \param data GString containing object data
 *
 *  Forms RECEIVE_OBJECT SysEx message then sends it to device.
 **/
void send_object(SectionID section, guint bank, guint index,
                 gchar *name, GString *data)
{
    GString *msg = g_string_new(NULL);

    gint len = data->len;

    g_string_append_printf(msg,
                           "%c%c%c%c%s%c%c%c",
                           section, bank,
                           ((index & 0xFF00) >> 8), (index & 0xFF),
                           name, 0 /* NULL terminated string */,
                           ((len & 0xFF00) >> 8), (len & 0xFF));

    g_string_append_len(msg, data->str, data->len);

    send_message(RECEIVE_OBJECT, msg->str, msg->len);

    g_string_free(msg, TRUE);
}

/**
 *  \param params GList containing SettingParam
 *
 *  Forms RECEIVE_PRESET_PARAMETERS SysEx message then sends it to device.
 **/
void send_preset_parameters(GList *params)
{
    GString *msg = g_string_sized_new(500);
    GList *iter = params;
    gint len = g_list_length(iter);

    g_string_append_printf(msg, "%c%c",
                           ((len & 0xFF00) >> 8),
                           (len & 0xFF));

    while (iter) {
        SettingParam *param = (SettingParam *) iter->data;
        iter = iter->next;

        g_string_append_printf(msg, "%c%c%c",
                               ((param->id & 0xFF00) >> 8),
                               (param->id & 0xFF),
                               param->position);

        append_value(msg, param->value);
    };

    send_message(RECEIVE_PRESET_PARAMETERS, msg->str, msg->len);

    g_string_free(msg, TRUE);
}

/**
 *  \param bank preset bank
 *  \param x preset index
 *
 *  Switches to selected preset.
 **/
void switch_preset(guint bank, guint x)
{
    GString *msg = g_string_sized_new(6);
    g_string_append_printf(msg, "%c%c%c%c%c%c",
                           bank, x,                /* source */
                           PRESETS_EDIT_BUFFER, 0, /* destination */
                           0,                      /* keep existing name */
                           1);                     /* load */
    send_message(MOVE_PRESET, msg->str, msg->len);
    g_string_free(msg, TRUE);
}

/**
 *  \param x preset index
 *  \param name preset name
 *
 *  Stores current edit buffer in user presets bank.
 **/
void store_preset_name(int x, const gchar *name)
{
    GString *msg = g_string_sized_new(6);
    g_string_append_printf(msg, "%c%c%c%c%s%c%c",
                           PRESETS_EDIT_BUFFER, 0, /* source */
                           PRESETS_USER, x,        /* destination */
                           name, 0,                /* name */
                           1);                     /* load */
    send_message(MOVE_PRESET, msg->str, msg->len);
    g_string_free(msg, TRUE);
}

/**
 *  \param x preset index
 *  \param name preset name
 *
 *  Sets preset name.
 **/
void set_preset_name(int x, gchar *name)
{
    GString *msg = g_string_sized_new(12);
    g_string_append_printf(msg, "%c%c%s%c",
                           PRESETS_USER,    /* preset bank */
                           x,               /* preset index */
                           name, 0);        /* name */
    send_message(RECEIVE_PRESET_NAME, msg->str, msg->len);
    g_string_free(msg, TRUE);
}

/**
 *  \param bank preset bank
 *
 *  Queries preset names.
 *
 *  \return GStrv which must be freed with g_strfreev, or NULL on error.
 **/
GStrv query_preset_names(gchar bank)
{
    GString *data = NULL;
    int x;                    /* used to iterate over whole reply */
    int n = 0;                /* current preset number */
    int n_total;              /* total number of presets */
    gchar **str_array = NULL;

    /* query user preset names */
    send_message(REQUEST_PRESET_NAMES, &bank, 1);

    /* read reply */
    data = get_message_by_id(RECEIVE_PRESET_NAMES);

    if (data != NULL) {
        if (data->len >= 10) {
            n_total = data->str[9];
            str_array = g_new(gchar*, n_total + 1);
            str_array[n_total] = NULL;
        }

        for (x=10; ((x<data->len) && (n<n_total)); x++) {
            if ((unsigned char)data->str[x] == 0xF7) /* every message ends with 0xF7 */
                break;

            str_array[n] = g_strdup(&data->str[x]);
            x += strlen(str_array[n]);
            n++;
        }
        g_string_free(data, TRUE);
    }
    return str_array;
}

/**
 *  Reads multiple messages and puts them into GList.
 *
 *  \param id MessageID starting message sequence
 *
 *  \return GList with SysEx messages, which must be freed using message_list_free.
 **/
GList *get_message_list(MessageID id)
{
    GString *data = NULL;
    GList *list = NULL;
    guint x, len;
    gboolean found = FALSE;
    gboolean done = FALSE;

    g_mutex_lock(message_queue_mutex);
    do {
        len = g_queue_get_length(message_queue);

        for (x = 0; x<len && (found == FALSE); x++) {
            data = g_queue_peek_nth(message_queue, x);
            if (get_message_id(data) == id) {
                found = TRUE;
                g_queue_pop_nth(message_queue, x);
                unpack_message(data);
                list = g_list_append(list, data);
                break;
            }
        }

        if (found == TRUE) {
            int i;
            int amt;

            switch (id) {
                case RECEIVE_PRESET_START:
                    for (i = 10; (i < data->len) && data->str[i]; i++);
                    amt = (unsigned char)data->str[i+2];
                    break;
                case RECEIVE_BULK_DUMP_START:
                    amt = ((unsigned char)data->str[8] << 8) | (unsigned char)data->str[9];
                    break;
                default:
                    g_error("get_message_list() doesn't support followning id: %d", id);
                    g_string_free(data, TRUE);
                    g_list_free(list);
                    g_assert(!"BUG");
                    return NULL;
            }

            while (amt) {
                debug_msg(DEBUG_VERBOSE, "%d messages left", amt);
                data = g_queue_pop_nth(message_queue, x);
                if (data == NULL) {
                    g_cond_wait(message_queue_cond, message_queue_mutex);
                } else {
                    unpack_message(data);
                    list = g_list_append(list, data);
                    amt--;
                }
            }

            done = TRUE;
        } else {
            /* id not found in message queue */
            g_cond_wait(message_queue_cond, message_queue_mutex);
        }
    } while (done == FALSE);
    g_mutex_unlock(message_queue_mutex);

    return list;
}

/**
 *  \param list list to be freed
 *
 *  Frees all memory used by message list.
 **/
void message_list_free(GList *list)
{
    g_return_if_fail(list != NULL);

    g_list_foreach(list, (GFunc) message_free_func, NULL);
    g_list_free(list);
}

/**
 *  Queries current edit buffer.
 *
 *  \return GList with preset SysEx messages, which must be freed using message_list_free.
 **/
GList *get_current_preset()
{
    send_message(REQUEST_PRESET, "\x04\x00", 2);
    return get_message_list(RECEIVE_PRESET_START);
}

/**
 *  Creates backup file.
 *
 *  \param file backup file handle
 *  \param error a GError
 *
 *  \return FALSE on success, TRUE on error.
 **/
static gboolean create_backup_file(GFile *file, GError **error)
{
    GFileOutputStream *output = NULL;
    GList *list = NULL, *iter = NULL;
    const gchar header[] = {'\x01', '\x00'};
    gsize written;
    gboolean val;

    if (error)
        *error = NULL;
    output = g_file_create(file, G_FILE_CREATE_NONE, NULL, error);
    if (output == NULL)
        return TRUE;

    if (error)
        *error = NULL;
    val = g_output_stream_write_all(G_OUTPUT_STREAM(output), header,
                                    sizeof(header), &written, NULL, error);
    if (val == FALSE) {
        g_object_unref(output);
        return TRUE;
    }

    send_message(REQUEST_BULK_DUMP, "\x00", 1);
    list = get_message_list(RECEIVE_BULK_DUMP_START);

    for (iter = list; iter; iter = g_list_next(iter)) {
        GString *str;
        guchar id;    /* message id */
        guint32 len;  /* message length */

        str = (GString*) iter->data;

        id = get_message_id(str);
        if (error)
            *error = NULL;
        val = g_output_stream_write_all(G_OUTPUT_STREAM(output), &id,
                                        sizeof(id), &written, NULL, error);
        if (val == FALSE) {
            message_list_free(list);
            g_object_unref(output);
            return TRUE;
        }

        len = GUINT32_TO_LE(str->len - 10);
        if (error)
            *error = NULL;
        val = g_output_stream_write_all(G_OUTPUT_STREAM(output), &len,
                                        sizeof(len), &written, NULL, error);
        if (val == FALSE) {
            message_list_free(list);
            g_object_unref(output);
            return TRUE;
        }

        if (error)
            *error = NULL;
        val = g_output_stream_write_all(G_OUTPUT_STREAM(output), &str->str[8],
                                        str->len - 10, &written, NULL, error);
        if (val == FALSE) {
            message_list_free(list);
            g_object_unref(output);
            return TRUE;
        }
    }

    message_list_free(list);

    if (error)
        *error = NULL;
    val = g_output_stream_close(G_OUTPUT_STREAM(output), NULL, error);
    g_object_unref(output);
    return !val;
}

/**
 *  Restores backup file.
 *
 *  \param filename backup filename
 *  \param error a GError
 *
 *  \return FALSE on success, TRUE on error.
 **/
static gboolean restore_backup_file(const gchar *filename, GError **error)
{
    gchar *data;
    gsize length;
    gsize x;

    if (g_file_get_contents(filename, &data, &length, error) == FALSE)
        return TRUE;

    if (error)
        *error = NULL;

    if (!(data[0] == 0x01 && data[1] == 0x00)) {
        g_free(data);
        g_set_error_literal(error, gdigi_error_quark(), 0,
                            "Magic byte doesn't match");
        return TRUE;
    }

    x = 0x02;
    while (x < length) {
        gchar id;
        guint32 len;

        id = data[x];
        x++;

        if (x+4 <= length) {
            len = GUINT32_FROM_LE(*((guint32*) &data[x]));
            x += 4;
        } else {
            g_free(data);
            g_set_error_literal(error, gdigi_error_quark(), 0,
                                "Unexpected end of data");
            return TRUE;
        }

        if (x+len <= length) {
            send_message(id, &data[x], len);
            x += len;
        } else {
            g_free(data);
            g_set_error_literal(error, gdigi_error_quark(), 0,
                                "Unexpected end of data");
            return TRUE;
        }
    }

    g_free(data);
    return FALSE;
}

/**
 *  \param device_id Variable to hold device ID
 *  \param family_id Variable to hold family ID
 *  \param product_id Variable to hold product ID
 *
 *  Requests device information.
 *
 *  \return TRUE on success, FALSE on error.
 **/
static gboolean request_who_am_i(unsigned char *device_id, unsigned char *family_id,
                                 unsigned char *product_id)
{
    send_message(REQUEST_WHO_AM_I, "\x7F\x7F\x7F", 3);

    GString *data = get_message_by_id(RECEIVE_WHO_AM_I);
    if ((data != NULL) && (data->len > 11)) {
        *device_id = data->str[8];
        *family_id = data->str[9];
        *product_id = data->str[10];
        g_string_free(data, TRUE);
        debug_msg(DEBUG_STARTUP, "Found device id %d family %d product id %d.",
                                *device_id,
                                *family_id,
                                *product_id);
        return TRUE;
    }
    return FALSE;
}

static void request_device_configuration()
{
    gint os_major, os_minor;
    gint cpu_major, cpu_minor;
    gint protocol_version;
    gint current_bank, current_preset;
    gint media_card;

    send_message(REQUEST_DEVICE_CONFIGURATION, NULL, 0);

    GString *data = get_message_by_id(RECEIVE_DEVICE_CONFIGURATION);

    if (data->len > 14) {
        os_major = data->str[8];
        os_minor = (((data->str[9] & 0xF0) >> 4) * 10) |
                   (data->str[9] & 0x0F);
        g_message("OS version: %d.%d", os_major, os_minor);

        cpu_major = data->str[10];
        cpu_minor = data->str[11];
        g_message("CPU version: %d.%d", cpu_major, cpu_minor);

        protocol_version = data->str[12];
        g_message("Protocol version: %d", protocol_version);

        current_bank = data->str[13];
        current_preset = data->str[14];
        g_message("Active bank: %d", current_bank);
        g_message("Active preset: %d", current_preset);

        if (os_major >= 1) {
            media_card = data->str[15];
            g_message("Media card present: %d", media_card);
        }
    }
}

#ifndef DOXYGEN_SHOULD_SKIP_THIS

static GOptionEntry options[] = {
    {"device", 'd', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_STRING, &device_port, "MIDI device port to use", NULL},
    {"debug-flags <flags>", 'D', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_CALLBACK, set_debug_flags,
        "<flags> any of a, d, g, h, m, s, x, v:\n"
        "                                "
        "a: Everything.\n"
        "                                "
        "d: Messages to the device.\n"
        "                                "
        "g: Group messages.\n"
        "                                "
        "h: Dump message contents in hex.\n"
        "                                "
        "m: All messages.\n"
        "                                "
        "s: Startup.\n"
        "                                "
        "x: Debug xml parsing/writing.\n"
        "                                "
        "v: Additional verbosity.\n" ,
        NULL},
    {NULL}
};

#endif /* DOXYGEN_SHOULD_SKIP_THIS */

/**
 *  \param[out] devices GList containing numbers (packed into pointers)
 *              of connected DigiTech devices
 *
 *  Checks available soundcards for DigiTech devices.
 *
 *  \return the number of connected DigiTech devices.
 **/
static gint get_digitech_devices(GList **devices)
{
    gint card_num = -1;
    gint number = 0;

    while (!snd_card_next(&card_num) && (card_num > -1)) {
        char* name;
        snd_card_get_longname(card_num, &name);
        if (strspn(name,"DigiTech") > 0) {
            number++;
            *devices = g_list_append(*devices, GINT_TO_POINTER(card_num));
        }
        free(name);
    }

    return number;
}

int main(int argc, char *argv[]) {
    GError *error = NULL;
    GOptionContext *context;
    static gboolean stop_read_thread = FALSE;
    GThread *read_thread = NULL;

    g_thread_init(NULL);
    gdk_threads_init();

    context = g_option_context_new(NULL);
    g_option_context_add_main_entries(context, options, NULL);
    g_option_context_add_group(context, gtk_get_option_group(TRUE));

    if (!g_option_context_parse(context, &argc, &argv, &error)) {
        g_warning("option parsing failed: %s\n", error->message);
        g_error_free(error);
        g_option_context_free(context);
        exit(EXIT_FAILURE);
    }

    if (device_port == NULL) {
        /* port not given explicitly in commandline - search for devices */
        GList *devices = NULL;
        GList *device = NULL;
        int    num_devices = 0;
        int    chosen_device = 0;
        if ((num_devices = get_digitech_devices(&devices)) <= 0) {
            g_warning("Couldn't find DigiTech devices!");
            exit(EXIT_FAILURE);
        }
        if (num_devices > 1) {
            chosen_device = select_device_dialog(devices);
            if (chosen_device < 0) {
                show_error_message(NULL, "No device chosen");
                exit(EXIT_FAILURE);
            }
        }
        device = g_list_nth(devices, chosen_device);
        device_port = g_strdup_printf("hw:%d,0,0",
                                      GPOINTER_TO_INT(device->data));
        g_list_free(devices);
        debug_msg(DEBUG_STARTUP, "Found device %s.", device_port);
    } else {
        debug_msg(DEBUG_STARTUP, "Using device %s.", device_port);
    }

    g_option_context_free(context);

    if (open_device() == TRUE) {
        show_error_message(NULL, "Failed to open MIDI device");
    } else {
        message_queue = g_queue_new();
        message_queue_mutex = g_mutex_new();
        message_queue_cond = g_cond_new();
        read_thread = g_thread_create((GThreadFunc)read_data_thread,
                                      &stop_read_thread,
                                      TRUE, NULL);

        if (request_who_am_i(&device_id, &family_id, &product_id) == FALSE) {
            show_error_message(NULL, "No suitable reply from device");
        } else {
            Device *device = NULL;

            if (get_device_info(device_id, family_id, product_id, &device) == FALSE) {
                if (unsupported_device_dialog(&device) == FALSE) {
                    g_message("Shutting down");
                }
            }

            if (device != NULL) {
                /* enable GUI mode */
                set_option(GUI_MODE_ON_OFF, GLOBAL_POSITION, 1);

                gui_create(device);
                gtk_main();
                gui_free();

                /* disable GUI mode */
                set_option(GUI_MODE_ON_OFF, GLOBAL_POSITION, 0);
            }
        }
    }

    if (read_thread != NULL) {
        stop_read_thread = TRUE;
        g_thread_join(read_thread);
    }

    if (message_queue_mutex != NULL) {
        g_mutex_free(message_queue_mutex);
    }

    if (message_queue != NULL && g_queue_get_length(message_queue)) {
        g_warning("%d unread messages in queue",
                  g_queue_get_length(message_queue));
        g_queue_foreach(message_queue, (GFunc) message_free_func, NULL);
        g_queue_free(message_queue);
    }

    if (output != NULL) {
        snd_rawmidi_drain(output);
        snd_rawmidi_close(output);
    }

    if (input != NULL) {
        snd_rawmidi_drain(input);
        snd_rawmidi_close(input);
    }

    return EXIT_SUCCESS;
}
