/*
 * Purpose: Dedicated driver for Yamaha USB MIDI devices
 */
/*
 * This file is part of Open Sound System
 *
 * Copyright (C) 4Front Technologies 1996-2007.
 *
 * This software is released under CDDL 1.0 source license for operating systems
 * that have their full source code available under the BSD or CDDL licenses.
 *
 * See the COPYING file included in the main directory of this source
 * distribution. Please contact sales@opensound.com for further info.
 */

/*
 * NOTE! This is not the development version of this file. Please download the
 * sources of the latest Open Sound System version from 
 * http://developer.opensound.com/sources if you plan to make any changes
 * to the software.
 */

#include "oss_config.h"
#include "midi_core.h"
#include "ossusb.h"
#include "midiparser.h"

typedef struct ymhusb_devc ymhusb_devc;

#define MAX_INDEVS	1
#define MAX_OUTDEVS	1
#define TMPBUF_SIZE 	64
#define TMPBUF_NSLOTS	24

typedef unsigned char tmpbuf_slot_t[TMPBUF_SIZE];
#define OUTBUF_SIZE (TMPBUF_NSLOTS*TMPBUF_SIZE)

typedef struct
{
  ymhusb_devc *devc;
  oss_device_t *osdev;
  udi_usb_devc *usbdev;
  oss_mutex_t mutex;

  int portnum;
  int midi_dev;
  int open_mode;

  void *endpoint_desc;
  udi_endpoint_handle_t *endpoint_handle;
  udi_usb_request_t *datapipe;

  oss_midi_inputbyte_t midi_input_intr;
  unsigned char *tmpbuf;
  int tmp_len;
  midiparser_common_p parser;

  volatile int output_busy;

  /* Output buffer */
  tmpbuf_slot_t *outbuf;
  int buf_t, buf_h;
} ymhusb_midic;

struct ymhusb_devc
{
  special_unload_t unload_func;

  oss_device_t *osdev;
  udi_usb_devc *usbdev;

  oss_mutex_t mutex;

  int n_inputs, n_outputs;
  ymhusb_midic indevs[MAX_INDEVS], outdevs[MAX_OUTDEVS];
};

static void
ymhusb_unload (void *d)
{
  ymhusb_devc *devc = (ymhusb_devc *) d;
  int i;

  for (i = 0; i < devc->n_inputs; i++)
    {
      MUTEX_CLEANUP (devc->indevs[i].mutex);
    }
  for (i = 0; i < devc->n_outputs; i++)
    {
      MUTEX_CLEANUP (devc->outdevs[i].mutex);
    }
  MUTEX_CLEANUP (devc->mutex);
}

static int ymhmidi_start_input (ymhusb_devc * devc, ymhusb_midic * midic);

void
ymhmidi_record_callback (udi_usb_request_t * request, void *arg)
{
  ymhusb_midic *midic = arg;
  ymhusb_devc *devc = midic->devc;
  unsigned char *data;
  int l;

  l = udi_usb_request_actlen (request);
  data = udi_usb_request_actdata (request);

  if (l == 0)
    goto restart;

  if (midi_devs[midic->midi_dev]->event_input != NULL)
    midi_devs[midic->midi_dev]->event_input (midic->midi_dev, data, l);

restart:
  if (midic->open_mode & OPEN_READ)
    ymhmidi_start_input (devc, midic);
}

static int ymhmidi_submit_output (ymhusb_devc * devc, ymhusb_midic * midic);

void
ymhmidi_play_callback (udi_usb_request_t * request, void *arg)
{
  ymhusb_midic *midic = arg;
  ymhusb_devc *devc = midic->devc;

  midic->output_busy = 0;
  ymhmidi_submit_output (devc, midic);
}

static int
ymhmidi_start_input (ymhusb_devc * devc, ymhusb_midic * midic)
{
  int err;

  if ((err =
       udi_usb_submit_request (midic->datapipe, ymhmidi_record_callback,
			       midic, midic->endpoint_handle,
			       UDI_USBXFER_BULK_READ, midic->tmpbuf,
			       TMPBUF_SIZE)) < 0)
    {
      return err;
    }

  return 0;
}

static int
ymhmidi_submit_output (ymhusb_devc * devc, ymhusb_midic * midic)
{
  int err;
  int n = midic->buf_t;

  if (midic->buf_h == midic->buf_t)
    return 0;

  if ((err =
       udi_usb_submit_request (midic->datapipe, ymhmidi_play_callback, midic,
			       midic->endpoint_handle, UDI_USBXFER_BULK_WRITE,
			       midic->outbuf[n], TMPBUF_SIZE)) < 0)
    {
      cmn_err (CE_WARN, "Submit USB MIDI request failed\n");
      return err;
    }

  midic->buf_t = (n + 1) % TMPBUF_NSLOTS;;
  midic->output_busy = 1;

  return 0;
}

static int
ymhmidi_put_output (ymhusb_devc * devc, ymhusb_midic * midic)
{
  int n = midic->buf_h;

  midic->buf_h = (n + 1) % TMPBUF_NSLOTS;

  memcpy (midic->outbuf[n], midic->tmpbuf, TMPBUF_SIZE);
  midic->tmp_len = 0;
  memset (midic->tmpbuf, 0, TMPBUF_SIZE);

  if (midic->output_busy)
    return 0;

  ymhmidi_submit_output (devc, midic);

  return 0;
}

static void
ymhusb_close_input (int dev, int mode)
{
  oss_native_word flags;
  ymhusb_midic *midic;
  ymhusb_devc *devc;

  midic = midi_devs[dev]->devc;
  devc = midic->devc;

  MUTEX_ENTER_IRQDISABLE (midic->mutex, flags);
  midic->open_mode = 0;
  midic->midi_input_intr = NULL;
  udi_usb_free_request (midic->datapipe);
  udi_close_endpoint (midic->endpoint_handle);
  if (midic->tmpbuf != NULL)
    CONTIG_FREE (midic->osdev, midic->tmpbuf, TMPBUF_SIZE);
  MUTEX_EXIT_IRQRESTORE (midic->mutex, flags);
}

static int
ymhusb_open_input (int dev, int mode, oss_midi_inputbyte_t inputbyte,
		   oss_midi_inputbuf_t inputbuf,
		   oss_midi_outputintr_t outputintr)
{
  oss_native_word flags, phaddr;
  ymhusb_midic *midic;
  ymhusb_devc *devc;
  int err;

  midic = midi_devs[dev]->devc;
  devc = midic->devc;

  MUTEX_ENTER_IRQDISABLE (midic->mutex, flags);
  if (midic->open_mode)
    {
      MUTEX_EXIT_IRQRESTORE (midic->mutex, flags);
      return -EBUSY;
    }

  midic->open_mode = mode;
  midic->midi_input_intr = inputbyte;
  MUTEX_EXIT_IRQRESTORE (midic->mutex, flags);

  midic->tmpbuf =
    CONTIG_MALLOC (midic->osdev, TMPBUF_SIZE, MEMLIMIT_32BITS, &phaddr);
  memset (midic->tmpbuf, 0, TMPBUF_SIZE);

  if ((midic->endpoint_handle =
       udi_open_endpoint (midic->usbdev, midic->endpoint_desc)) == NULL)
    {
      midic->open_mode = 0;
      midic->midi_input_intr = NULL;
      cmn_err (CE_WARN, "Cannot open audio pipe\n");
      return -ENOMEM;
    }
  if ((midic->datapipe =
       udi_usb_alloc_request (midic->usbdev, midic->endpoint_handle, 1,
			      UDI_USBXFER_BULK_READ)) == NULL)
    {
      return -EIO;
    }

  return ymhmidi_start_input (devc, midic);
}

static void
ymhusb_flush_output (int dev)
{
  oss_native_word flags;
  ymhusb_midic *midic;
  ymhusb_devc *devc;
  int mode;
  int next;

  midic = midi_devs[dev]->devc;
  devc = midic->devc;

  if (midic->tmp_len == 0)
    return;
  next = (midic->buf_t + 1) % TMPBUF_NSLOTS;
  if (next == midic->buf_h)	/* Buffer full */
    return;

  MUTEX_ENTER_IRQDISABLE (midic->mutex, flags);
  ymhmidi_put_output (devc, midic);
  MUTEX_EXIT_IRQRESTORE (midic->mutex, flags);
}

static int
ymhusb_wait_output (int dev)
{
  oss_native_word flags;
  ymhusb_midic *midic;
  ymhusb_devc *devc;
  int mode;
  int next;

  ymhusb_flush_output (dev);

  midic = midi_devs[dev]->devc;
  devc = midic->devc;

  if (midic->output_busy)
    return 1;

  if (midic->buf_t == midic->buf_h)
    return 0;			/* Not busy */

  return 1;
}

static void
ymhusb_close_output (int dev, int mode)
{
  oss_native_word flags;
  ymhusb_midic *midic;
  ymhusb_devc *devc;

  midic = midi_devs[dev]->devc;
  devc = midic->devc;

  ymhusb_flush_output (dev);

  MUTEX_ENTER_IRQDISABLE (midic->mutex, flags);
  midic->open_mode = 0;
  midic->midi_input_intr = NULL;
  udi_usb_free_request (midic->datapipe);
  udi_close_endpoint (midic->endpoint_handle);
  if (midic->outbuf != NULL)
    CONTIG_FREE (midic->osdev, midic->outbuf, OUTBUF_SIZE);
  if (midic->tmpbuf != NULL)
    KERNEL_FREE (midic->tmpbuf);
  midiparser_unalloc (midic->parser);
  midic->parser = NULL;
  MUTEX_EXIT_IRQRESTORE (midic->mutex, flags);
}

static void
out_event (ymhusb_midic * midic, unsigned char a, unsigned char b,
	   unsigned char c, unsigned char d)
{
  unsigned char *e;
  ymhusb_devc *devc = midic->devc;

  if (midic->tmp_len > TMPBUF_SIZE - 4)
    ymhmidi_put_output (devc, midic);

  e = midic->tmpbuf + midic->tmp_len;
  *e++ = a;
  *e++ = b;
  *e++ = c;
  *e++ = d;

  midic->tmp_len += 4;

  if (midic->tmp_len >= TMPBUF_SIZE)
    ymhmidi_put_output (devc, midic);
}

void
parser_cb (void *context, int category, unsigned char msg, unsigned char ch,
	   unsigned char *parms, int len)
{
  ymhusb_midic *midic = context;

  if (category == CAT_VOICE)
    {
      switch (msg)
	{
	case MIDI_NOTEON:
	  out_event (midic, 0x9, msg | ch, parms[0], parms[1]);
	  break;

	case MIDI_NOTEOFF:
	  out_event (midic, 0x8, msg | ch, parms[0], parms[1]);
	  break;

	case MIDI_KEY_PRESSURE:
	  out_event (midic, 0xa, msg | ch, parms[0], parms[1]);
	  break;
	}

      return;
    }

  if (category == CAT_CHN)
    {
      switch (msg)
	{
	case MIDI_CTL_CHANGE:
	  out_event (midic, 0xb, msg | ch, parms[0], parms[1]);
	  break;

	case MIDI_PGM_CHANGE:
	  out_event (midic, 0xc, msg | ch, parms[0], 0);
	  break;

	case MIDI_CHN_PRESSURE:
	  out_event (midic, 0xd, msg | ch, parms[0], 0);
	  break;

	case MIDI_PITCH_BEND:
	  out_event (midic, 0xe, msg | ch, parms[0], parms[1]);
	  break;
	}
      return;
    }

  if (category == CAT_REALTIME)
    {
      out_event (midic, 0xf, msg | ch, 0, 0);
      return;
    }

  if (category == CAT_MTC)
    {
      out_event (midic, 0x2, 0xf1, parms[0], 0);
      return;
    }

  if (category == CAT_SYSEX)
    {
      int l = len, n;
      unsigned char *d = parms;

      while (l > 0)
	{
	  n = l;
	  if (n > 3)
	    n = 3;

	  switch (n)
	    {
	    case 3:
	      out_event (midic, 0x4, d[0], d[1], d[2]);
	      break;
	    case 2:
	      out_event (midic, 0x6, d[0], d[1], 0);
	      break;
	    case 1:
	      out_event (midic, 0x5, d[0], 0, 0);
	      break;
	    }

	  l -= n;
	  d += n;
	}
      return;
    }
}

static int
ymhusb_open_output (int dev, int mode, oss_midi_inputbyte_t inputbyte,
		    oss_midi_inputbuf_t inputbuf,
		    oss_midi_outputintr_t outputintr)
{
  oss_native_word flags, phaddr;
  ymhusb_midic *midic;
  ymhusb_devc *devc;
  int err;

  midic = midi_devs[dev]->devc;
  devc = midic->devc;

  MUTEX_ENTER_IRQDISABLE (midic->mutex, flags);
  if (midic->open_mode)
    {
      MUTEX_EXIT_IRQRESTORE (midic->mutex, flags);
      return -EBUSY;
    }

  midic->open_mode = mode;
  midic->output_busy = 0;
  MUTEX_EXIT_IRQRESTORE (midic->mutex, flags);

  midic->tmpbuf = KERNEL_MALLOC (TMPBUF_SIZE);
  memset (midic->tmpbuf, 0, TMPBUF_SIZE);
  midic->outbuf =
    CONTIG_MALLOC (midic->osdev, OUTBUF_SIZE, MEMLIMIT_32BITS, &phaddr);
  midic->tmp_len = 0;
  midic->buf_h = midic->buf_t = 0;	/* Empty buffer */
  memset (midic->outbuf, 0, OUTBUF_SIZE);

  if ((midic->parser = midiparser_create (parser_cb, midic)) == NULL)
    {
      midic->open_mode = 0;
      midic->midi_input_intr = NULL;
      cmn_err (CE_WARN, "Cannot create MIDI parser\n");
      return -ENOMEM;
    }

  if ((midic->endpoint_handle =
       udi_open_endpoint (midic->usbdev, midic->endpoint_desc)) == NULL)
    {
      midic->open_mode = 0;
      midic->midi_input_intr = NULL;
      cmn_err (CE_WARN, "Cannot open audio pipe\n");
      return -ENOMEM;
    }
  if ((midic->datapipe =
       udi_usb_alloc_request (midic->usbdev, midic->endpoint_handle, 1,
			      UDI_USBXFER_BULK_WRITE)) == NULL)
    {
      return -EIO;
    }

  return 0;
}

static int
ymhusb_out (int dev, unsigned char midi_byte)
{
  ymhusb_midic *midic = midi_devs[dev]->devc;
  oss_native_word flags;
  int next;

  next = (midic->buf_t + 1) % TMPBUF_NSLOTS;
  if (next == midic->buf_h)	/* Buffer full */
    return 0;			/* Try again later */

  MUTEX_ENTER_IRQDISABLE (midic->mutex, flags);
  midiparser_input (midic->parser, midi_byte);
  MUTEX_EXIT_IRQRESTORE (midic->mutex, flags);

  return 1;
}

static int
ymhusb_ioctl (int dev, unsigned cmd, ioctl_arg arg)
{
  return -EINVAL;
}

static midi_driver_t ymhusb_input_driver = {
  ymhusb_open_input,
  ymhusb_close_input,
  ymhusb_ioctl,
  ymhusb_out,
};

static void
add_input_device (ymhusb_devc * devc, char *name, void *desc, int caps)
{
  int n;
  ymhusb_midic *midic;
  char tmp[128];

  if (devc->n_inputs >= MAX_INDEVS)
    {
      cmn_err (CE_WARN, "Yamaha MIDI: Too many inputs\n");
      return;
    }

  n = devc->n_inputs++;

  midic = &devc->indevs[n];

  midic->devc = devc;
  midic->osdev = devc->osdev;
  MUTEX_INIT (midic->osdev, midic->mutex, MH_DRV + 1);
  midic->endpoint_desc = desc;
  midic->portnum = n;
  midic->usbdev = devc->usbdev;

  sprintf (tmp, "%s input", name, n);
  if ((midic->midi_dev =
       oss_install_mididev (OSS_MIDI_DRIVER_VERSION, "YMHMIDI", tmp,
			    &ymhusb_input_driver,
			    sizeof (ymhusb_input_driver),
			    NULL /* &std_midi_synth */ ,
			    MFLAG_INPUT, midic, midic->osdev)) < 0)
    {
      cmn_err (CE_CONT, "Failed to install MIDI device\n");
      return;
    }
  midi_devs[midic->midi_dev]->caps |= caps;
}

static midi_driver_t ymhusb_output_driver = {
  ymhusb_open_output,
  ymhusb_close_output,
  ymhusb_ioctl,
  ymhusb_out,
  NULL,
  0,
  ymhusb_flush_output,
  ymhusb_wait_output
};

static void
add_output_device (ymhusb_devc * devc, char *name, void *desc, int caps)
{
  int n;
  ymhusb_midic *midic;
  char tmp[128];

  if (devc->n_outputs >= MAX_OUTDEVS)
    {
      cmn_err (CE_WARN, "Yamaha MIDI: Too many outputs\n");
      return;
    }

  n = devc->n_outputs++;

  midic = &devc->outdevs[n];

  midic->devc = devc;
  midic->osdev = devc->osdev;
  MUTEX_INIT (midic->osdev, midic->mutex, MH_DRV + 1);
  midic->endpoint_desc = desc;
  midic->portnum = n;
  midic->usbdev = devc->usbdev;

  sprintf (tmp, "%s output", name, n);
  if ((midic->midi_dev =
       oss_install_mididev (OSS_MIDI_DRIVER_VERSION, "YMHMIDI", tmp,
			    &ymhusb_output_driver,
			    sizeof (ymhusb_output_driver),
			    NULL /* &std_midi_synth */ ,
			    MFLAG_OUTPUT, midic, midic->osdev)) < 0)
    {
      cmn_err (CE_CONT, "Failed to install MIDI device\n");
      return;
    }

  midi_devs[midic->midi_dev]->caps |= caps;
}

void *
yamaha_usb_midi_driver (ossusb_devc * usb_devc)
{
  ymhusb_devc *devc;
  int i;
  unsigned char *desc, *d;
  int desc_len;
  int caps = MIDI_CAP_EXTERNAL;
  unsigned int devid;

  char *name = "Yamaha USB MIDI";

  if (usb_devc->dev_name != NULL)
    name = usb_devc->dev_name;

  if ((devc = PMALLOC (usb_devc->osdev, sizeof (*devc))) == NULL)
    {
      cmn_err (CE_CONT, "usb_midisport: Out of memory\n");
      return NULL;
    }

  memset (devc, 0, sizeof (*devc));

  devc->unload_func = ymhusb_unload;
  devc->osdev = usb_devc->osdev;
  devc->usbdev = usb_devc->last_usbdev;
  MUTEX_INIT (devc->osdev, devc->mutex, MH_DRV);
  devid = (usb_devc->vendor << 16) | usb_devc->product;

  DDB (cmn_err
       (CE_CONT, "Attaching Yamaha MIDI device %08x (%s)\n", devid, name));

  switch (devid)
    {
    case 0x0499101e:		/* PSR-K1 keyboard */
      caps |= MIDI_CAP_PTOP;
      break;
    }

  for (i = 0; i < 32; i++)
    if ((desc =
	 udi_usbdev_get_endpoint (devc->usbdev, 0, i, &desc_len)) != NULL)
      {
	if (desc[2] & 0x80)
	  {
	    add_input_device (devc, name, desc, caps);
	  }
	else
	  {
	    add_output_device (devc, name, desc, caps);
	  }
      }

  return devc;
}
