/*
 *  Copyright (c) by Jaroslav Kysela <perex@jcu.cz>
 *  Routines for control of MPU-401 in UART mode
 *
 *  MPU-401 supports UART mode which isn't capable generate transmit
 *  interrupts thus output is done via polling.
 *
 *   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 2 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, write to the Free Software
 *   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 */

#define SND_MAIN_OBJECT_FILE
#include "driver.h"
#include "midi.h"
#include "mpu401.h"

/*
 *
 */

void snd_mpu401_uart_interrupt( snd_rawmidi_t *rmidi )
{
  unsigned long flags;
  mpu401_t *mpu;
  int max = 128;
  unsigned char byte, status;

  mpu = (mpu401_t *)rmidi -> private_data;
  if ( !mpu ) {
    inb( MPU401D( mpu ) );		/* ack interrupt only */
    return;
  }
  while ( max-- > 0 ) {
    snd_spin_lock( mpu, input, &flags );
    status = inb( MPU401C( mpu ) );
    if ( status == 0xff ) break;	/* wrong value, MPU isn't connected */
    if ( !(status & 0x80) ) {
      byte = inb( MPU401D( mpu ) );
#if 0
      snd_printk( "interrupt!! - rx byte = 0x%x\n", byte );
#endif
      snd_spin_unlock( mpu, input, &flags );
      if ( mpu -> mode & MPU401_MODE_INPUT_TRIGGER )
        rmidi -> input.data( rmidi, &byte, 1 );
    } else {
      snd_spin_unlock( mpu, input, &flags );
      break;
    }
  }
}

/*
 *
 */

static void snd_mpu401_uart_cmd( mpu401_t *mpu, unsigned char cmd, int ack )
{
  unsigned long flags;
  int timeout, ok;
  
  snd_spin_lock( mpu, input, &flags );
  outb( 0x00, MPU401D( mpu ) );
  for ( timeout = 100000; timeout > 0 && !(inb( MPU401C( mpu ) ) & 0x80); timeout-- )
    inb( MPU401D( mpu ) );
#ifdef SNDCFG_DEBUG
  if ( timeout <= 0 )
    snd_printk( "snd_mpu401_uart_cmd: clear rx timeout (status = 0x%x)\n", inb( MPU401C( mpu ) ) );
#endif
  /* ok. standard MPU-401 initialization */
  if ( mpu -> hardware != MPU401_HW_SB ) {
    for ( timeout = 1000; timeout > 0 && inb( MPU401C( mpu ) ) & 0x40; timeout-- )
      snd_delay( 1 );
#ifdef SNDCFG_DEBUG
    if ( !timeout )
      snd_printk( "snd_mpu401_uart_cmd: tx timeout (status = 0x%x)\n", inb( MPU401C( mpu ) ) );
#endif
  }
  outb( cmd, MPU401C( mpu ) );
  if ( ack ) {
    ok = 0; timeout = 10000;
    while ( !ok && timeout-- > 0 ) {
      if ( !(inb( MPU401C( mpu ) ) & 0x80) ) {
        if ( inb( MPU401D( mpu ) ) == 0xfe ) ok = 1;
      }
    }
  } else {
    ok = 1;
  }
  snd_spin_unlock( mpu, input, &flags );
  if ( !ok ) {
    snd_printk( "snd_mpu401_uart_cmd: 0x%x failed at 0x%x (status = 0x%x, data = 0x%x)!!!\n", cmd, mpu -> port, inb( MPU401C( mpu ) ), inb( MPU401D( mpu ) ) );
  }
#if 0
  snd_printk( "snd_mpu401_uart_cmd: 0x%x at 0x%x (status = 0x%x, data = 0x%x)!!!\n", cmd, mpu -> port, inb( MPU401C( mpu ) ), inb( MPU401D( mpu ) ) );
#endif
}

static int snd_mpu401_uart_input_open( snd_rawmidi_t *rmidi )
{
  unsigned long flags;
  mpu401_t *mpu;

  mpu = (mpu401_t *)rmidi -> private_data;
#if 0
  snd_printk( "[0x%x] MPU-401 command port - 0x%x\n", MPU401C( mpu ), inb( MPU401C( mpu ) ) );
  snd_printk( "[0x%x] MPU-401 data port - 0x%x\n", MPU401D( mpu ), inb( MPU401D( mpu ) ) );
#endif
  snd_spin_lock( mpu, open, &flags );
  mpu -> mode |= MPU401_MODE_INPUT;
  if ( !(mpu -> mode & MPU401_MODE_OUTPUT) ) {
    snd_spin_unlock( mpu, open, &flags );
    snd_mpu401_uart_cmd( mpu, 0xff, 1 );	/* reset */
    snd_mpu401_uart_cmd( mpu, 0x3f, 1 );	/* enter UART mode */
  } else {
    snd_spin_unlock( mpu, open, &flags );
  }
  return 0;
}

static int snd_mpu401_uart_output_open( snd_rawmidi_t *rmidi )
{
  unsigned long flags;
  mpu401_t *mpu;
  
  mpu = (mpu401_t *)rmidi -> private_data;
  snd_spin_lock( mpu, open, &flags );
  mpu -> mode |= MPU401_MODE_OUTPUT;
  if ( !(mpu -> mode & MPU401_MODE_INPUT) ) {
    snd_spin_unlock( mpu, open, &flags );
    snd_mpu401_uart_cmd( mpu, 0xff, 1 );	/* reset */
    snd_mpu401_uart_cmd( mpu, 0x3f, 1 );	/* enter UART mode */
  } else {
    snd_spin_unlock( mpu, open, &flags );
  }
  return 0;
}

static int snd_mpu401_uart_input_close( snd_rawmidi_t *rmidi )
{
  unsigned long flags;
  mpu401_t *mpu;
  
  mpu = (mpu401_t *)rmidi -> private_data;
  snd_spin_lock( mpu, open, &flags );
  mpu -> mode &= ~MPU401_MODE_INPUT;
  if ( !(mpu -> mode & MPU401_MODE_OUTPUT) ) {
    snd_spin_unlock( mpu, open, &flags );
    snd_mpu401_uart_cmd( mpu, 0xff, 0 );	/* reset */
  } else {
    snd_spin_unlock( mpu, open, &flags );
  }
  return 0;
}

static int snd_mpu401_uart_output_close( snd_rawmidi_t *rmidi )
{
  unsigned long flags;
  mpu401_t *mpu;
  
  mpu = (mpu401_t *)rmidi -> private_data;
  snd_spin_lock( mpu, open, &flags );
  mpu -> mode &= ~MPU401_MODE_OUTPUT;
  if ( !(mpu -> mode & MPU401_MODE_INPUT) ) {
    snd_spin_unlock( mpu, open, &flags );
    snd_mpu401_uart_cmd( mpu, 0xff, 0 );	/* reset */
  } else {
    snd_spin_unlock( mpu, open, &flags );
  }
  return 0;
}

static void snd_mpu401_uart_input_trigger( snd_rawmidi_t *rmidi, int up )
{
  unsigned long flags;
  mpu401_t *mpu;
  
  mpu = (mpu401_t *)rmidi -> private_data;
  snd_spin_lock( mpu, input, &flags );
  if ( up )
    mpu -> mode |= MPU401_MODE_INPUT_TRIGGER;
   else
    mpu -> mode &= ~MPU401_MODE_INPUT_TRIGGER;
  snd_spin_unlock( mpu, input, &flags );  
}

static void snd_mpu401_uart_output_write( snd_rawmidi_t *rmidi )
{
  unsigned long flags;
  mpu401_t *mpu;
  unsigned char byte;
  int max = 256, timeout;

  /* looks like AudioDrive ES1688 have Tx FIFO size = 12 bytes */
  /* looks like SoundBlaster AWE 64 have Tx FIFO size = 2 bytes (ugly hardware) */
  mpu = (mpu401_t *)rmidi -> private_data;
  while ( max > 0 ) {
    for ( timeout = 100; timeout > 0; timeout-- ) {
      if ( !(inb( MPU401C( mpu ) ) & 0x40) ) break;
    }
    snd_spin_lock( mpu, output, &flags );
    if ( !(inb( MPU401C( mpu ) ) & 0x40) ) {
      if ( rmidi -> output.data( rmidi, &byte, 1 ) != 1 ) {
        snd_spin_unlock( mpu, output, &flags );
        return;
      }
      outb( byte, MPU401D( mpu ) );
      snd_spin_unlock( mpu, output, &flags );
#if 0
      snd_printk( "mpu401_uart: tx = 0x%x\n", byte );
#endif
      max--;
    } else {
      snd_spin_unlock( mpu, output, &flags );
#if 0
      snd_printd( "snd_mpu401_uart_output_write: timeout - max = %i (%i)\n", max, 256 - max );
#endif
      return;
    }
  }
}

/*
 *
 */
 
static struct snd_stru_rawmidi_direction_hw snd_mpu401_uart_output = {
  SND_RAWMIDI_HW_POLL,          /* flags */
  NULL,                         /* private_data */
  NULL,                         /* private_free */
  snd_mpu401_uart_output_open,  /* open */
  snd_mpu401_uart_output_close, /* close */
  NULL,				/* trigger */
  { snd_mpu401_uart_output_write }, /* io.write */
  NULL,                         /* abort */
};

static struct snd_stru_rawmidi_direction_hw snd_mpu401_uart_input = {
  0,                            /* flags */
  NULL,                         /* private_data */
  NULL,                         /* private_free */
  snd_mpu401_uart_input_open,   /* open */
  snd_mpu401_uart_input_close,  /* close */
  snd_mpu401_uart_input_trigger, /* trigger */
  { NULL },                     /* io.read */
  NULL,                         /* abort */
};

static void snd_mpu401_uart_free( void *private_data )
{
  snd_free( private_data, sizeof( mpu401_t ) );
}

snd_rawmidi_t *snd_mpu401_uart_new_device( snd_card_t *card, unsigned short hardware, unsigned short port, unsigned short irq )
{
  mpu401_t *mpu;
  snd_rawmidi_t *rmidi;

  if ( ( rmidi = snd_rawmidi_new_device( card, "MPU-401U" ) ) == NULL ) return NULL;
  mpu = snd_malloc( sizeof( mpu401_t ) );
  if ( !mpu ) {
    snd_rawmidi_free( rmidi );
    return NULL;
  }
  memset( mpu, 0, sizeof( mpu401_t ) );
  snd_spin_prepare( mpu, open );
  snd_spin_prepare( mpu, input );
  snd_spin_prepare( mpu, output );
  mpu -> hardware = hardware;
  mpu -> port = port;
  mpu -> irq = irq;
  strcpy( rmidi -> name, "MPU-401 (UART)" );
  memcpy( &rmidi -> output.hw, &snd_mpu401_uart_output, sizeof( snd_mpu401_uart_output ) );
  memcpy( &rmidi -> input.hw, &snd_mpu401_uart_input, sizeof( snd_mpu401_uart_input ) );
  rmidi -> info_flags |= SND_RAWMIDI_INFO_OUTPUT | SND_RAWMIDI_INFO_INPUT | SND_RAWMIDI_INFO_DUPLEX;
  rmidi -> private_data = mpu;
  rmidi -> private_free = snd_mpu401_uart_free;
  return rmidi;
}

/*
 *  INIT part
 */

#ifndef LINUX_2_1
extern struct symbol_table snd_symbol_table_mpu401_uart_export;
#endif

int init_module( void )
{
#ifndef LINUX_2_1
  if ( register_symtab( &snd_symbol_table_mpu401_uart_export ) < 0 )
    return -ENOMEM;
#endif
  return 0;
}

void cleanup_module( void )
{
}
