#ifdef _HAVE_ALSA_
/*
 * alsabackend.c
 * ALSA sequencer MIDI backend.
 *
 * for Denemo, a gtk+ frontend to GNU Lilypond
 * Copyright (C) 2011  Dominic Sacré
 *
 * 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.
 */

#include "alsabackend.h"
#include "midi.h"

#include <alsa/asoundlib.h>
#include <glib.h>


static char const *ALSA_SEQ_CLIENT_NAME = "denemo";

static int const PLAYBACK_INTERVAL = 5000;


static snd_seq_t *seq;

static int in_port_id;
static int out_port_id;

static snd_midi_event_t *parser;

static GThread *process_thread;
static GCond *process_cond;
static gboolean quit_thread = FALSE;

static gboolean reset = FALSE;

static double playback_start_time;


static gpointer
process_thread_func (gpointer data)
{
  GMutex *mutex = g_mutex_new ();

  for (;;)
    {
      // FIXME: GTimeVals are not monotonic
      GTimeVal timeval;
      g_get_current_time (&timeval);
      g_time_val_add (&timeval, PLAYBACK_INTERVAL);

      g_cond_timed_wait (process_cond, mutex, &timeval);

      if (g_atomic_int_get (&quit_thread))
        {
          break;
        }

      snd_seq_event_t *pev;
      while (snd_seq_event_input (seq, &pev) >= 0)
        {
          unsigned char buffer[3];

          snd_midi_event_reset_decode (parser);
          if (snd_midi_event_decode (parser, buffer, sizeof (buffer), pev) > 0)
            {
              input_midi_event (MIDI_BACKEND, 0, buffer);
            }
        }


      GTimeVal tv;
      g_get_current_time (&tv);
      double now = (double) tv.tv_sec + tv.tv_usec / 1000000.0;
      double playback_time = now - playback_start_time;

      unsigned char event_data[3];
      size_t event_length;
      double event_time;

      double until_time = playback_time + PLAYBACK_INTERVAL / 1000000.0;

      if (reset)
        {
          int n;
          for (n = 0; n < 16; ++n)
            {
              snd_seq_event_t ev;
              snd_seq_ev_set_controller (&ev, n, 123, 0);
              snd_seq_ev_set_subs (&ev);
              snd_seq_ev_set_direct (&ev);
              snd_seq_ev_set_source (&ev, out_port_id);
              snd_seq_event_output_direct (seq, &ev);
            }
          reset = FALSE;
        }

      while (read_event_from_queue (MIDI_BACKEND, event_data, &event_length, &event_time, until_time))
        {
          snd_seq_event_t ev;

          snd_midi_event_reset_encode (parser);
          snd_midi_event_encode (parser, event_data, event_length, &ev);

          snd_seq_ev_set_subs (&ev);
          snd_seq_ev_set_direct (&ev);
          snd_seq_ev_set_source (&ev, out_port_id);

          snd_seq_event_output_direct (seq, &ev);
        }

      if (is_playing ())
        {
          update_playback_time (TIMEBASE_PRIO_MIDI, playback_time);
        }
    }

  g_mutex_free (mutex);
  return NULL;
}



static int
alsa_seq_initialize (DenemoPrefs * config)
{
  g_print ("initializing ALSA sequencer MIDI backend\n");

  // create sequencer client
  if (snd_seq_open (&seq, "hw", SND_SEQ_OPEN_DUPLEX, SND_SEQ_NONBLOCK) < 0)
    {
      g_warning ("error opening alsa sequencer");
      return -1;
    }

  snd_seq_set_client_name (seq, ALSA_SEQ_CLIENT_NAME);

  // create input port
  in_port_id = snd_seq_create_simple_port (seq, "midi_in", SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE, SND_SEQ_PORT_TYPE_APPLICATION);
  if (in_port_id < 0)
    {
      g_warning ("error creating sequencer output port");
      return -1;
    }

  // create output port
  out_port_id = snd_seq_create_simple_port (seq, "midi_out", SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ, SND_SEQ_PORT_TYPE_APPLICATION);

  if (in_port_id < 0)
    {
      g_warning ("error creating sequencer output port");
      return -1;
    }

  // initialize MIDI event parser
  if (snd_midi_event_new (12, &parser))
    {
      g_warning ("error initializing MIDI event parser");
      return -1;
    }
  snd_midi_event_init (parser);
  snd_midi_event_no_status (parser, 1);


  process_cond = g_cond_new ();

  process_thread = g_thread_create_full (process_thread_func, NULL, 262144, TRUE, FALSE, G_THREAD_PRIORITY_NORMAL, NULL);


  return 0;
}


static int
alsa_seq_destroy ()
{
  g_print ("destroying ALSA sequencer MIDI backend\n");

  g_atomic_int_set (&quit_thread, TRUE);
  g_cond_signal (process_cond);
  g_thread_join (process_thread);

  g_cond_free (process_cond);


  snd_midi_event_free (parser);

  snd_seq_delete_port (seq, in_port_id);
  snd_seq_delete_port (seq, out_port_id);

  snd_seq_close (seq);

  return 0;
}


static int
alsa_seq_reconfigure (DenemoPrefs * config)
{
  alsa_seq_destroy ();
  return alsa_seq_initialize (config);
}


static int
alsa_seq_start_playing ()
{
  GTimeVal tv;
  g_get_current_time (&tv);
  playback_start_time = (double) tv.tv_sec + tv.tv_usec / 1000000.0;
  playback_start_time -= get_playback_time ();
  return 0;
}


static int
alsa_seq_stop_playing ()
{
  reset = TRUE;
  return 0;
}


static int
alsa_seq_panic ()
{
  reset = TRUE;
  return 0;
}


backend_t alsa_seq_midi_backend = {
  alsa_seq_initialize,
  alsa_seq_destroy,
  alsa_seq_reconfigure,
  alsa_seq_start_playing,
  alsa_seq_stop_playing,
  alsa_seq_panic,
};
#endif //_HAVE_ALSA_
