/*
 * Copyright (c) 2002, Peter Bentley
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 * 
 *   Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 * 
 *   Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 * 
 *   The name Peter Bentley may not be used to endorse or promote
 * products derived from this software without specific prior
 * written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * 
 */

#include "omd.h"
#include <stdio.h>
#include <string.h>

CVSID( "$Id: omd_unit.c,v 1.21 2003/11/30 23:46:35 pete Exp $" );

/*
 * To handle multiple units, we keep a static list of all devices here
 * (with a hardcoded top limit).  Whenever we see a new device we
 * add it to the list... It stays there even if the device is
 * later unplugged
 *
 * We could make the list size dynamic, but frankly I doubt it's
 * worth the 10 minutes to write the code :)
 */

#define MAXUNIT		8	/* More than enough for now, I would hope */

static omd_unit_t omd_units[MAXUNIT];
static int num_units = 0;


/*
 * Private data for matching USD IDs against known devices
 */
#define SPARE 0x01				// Spare vendor ID - XXX
static omd_unit_info_t unit_info[] = {
    { 0x54c,	0x75,	"Sony", "MZ-N1" 	},
    { 0x54c, 	0x80, 	"Sony", "(Unknown)"	},
    { 0x54c, 	0x81, 	"Sony", "MDS-JB980" 	},
    { 0x54c, 	0x84, 	"Sony", "MZ-N505" 	},
    { 0x54c, 	0x85, 	"Sony", "MZ-S1" 	},
    { 0x54c, 	0x86, 	"Sony", "MZ-N707" 	},
    { 0x54c, 	0xc6, 	"Sony", "MZ-N10" 	},
    { 0x54c, 	0x14c, 	"Aiwa", "AM-NX9" 	}, /* How come Aiwa
						    Use Sony product IDs ?*/
    { SPARE,	SPARE,	NULL,	NULL		},
    { SPARE,	SPARE,	NULL,	NULL		},
    { SPARE,	SPARE,	NULL,	NULL		},
    { 0x00,  	0x00, 	NULL,	NULL		},
};

/*
 * Private function for matching devices
 */
static omd_unit_info_t *omd_match_unit( unsigned int vendor,
					      unsigned int product )
{
    omd_unit_info_t *info;

    slog( LOG_TRACE, "Try match 0x%04x/0x%04x", vendor, product );
    for( info = unit_info; info->vendor_id != 0; info++ )
    {
	if( info->vendor_id == vendor && info->product_id == product )
	{
	    slog( LOG_TRACE, "Match unit 0x%04x/0x%04x = %s %s",
		  vendor, product, info->vendor, info->model );
	    return info;
	}
    }
    return NULL;
}

// Guess a vendor name from a vendor ID by comparing with
// ones we know
static const char *guess_vendor( unsigned int id )
{
    const omd_unit_info_t	*info;
    for( info = unit_info; info->vendor_id != 0; info++ )
    {
	if( info->vendor_id == id )
	{
	    return info->vendor;
	}
    }
    return "Unknown vendor";
}



int omd_num_devices( void )
{
    return num_units;
}



/*
 * Open a unit
 */

int omd_open_unit( omd_unit_t *unit )
{
    char buffer[256];
    
    if( unit->open )
    {
	slog( LOG_ERROR, "omd_open_unit: Unit already open" );
	return -1;
    }

    if( ! unit->present )
    {
	slog( LOG_ERROR, "omd_open_unit: Device has been detached" );
	return -1;
    }

    if( ( unit->handle = usb_open( unit->device )) == NULL )
    {
	slog( LOG_ERROR, "omd_open_unit: usb_open error" );
	return -1;
    }

    /* XXX error checking */
    usb_set_configuration( unit->handle, 1 );
    usb_claim_interface( unit->handle, 0 );
    unit->open = TRUE;
    
    if( usb_get_string_simple( unit->handle, 1, buffer, 255) < 0 )
    {
	slog( LOG_ERROR, "Failed to get device vendor string" );
    }
    unit->vendor = strdup( buffer );
    
    if( unit->vendor && unit->info && unit->info->vendor &&
	strcmp( unit->vendor, unit->info->vendor ) )
    {
	slog( LOG_WARNING, "Device reports an unexpected vendor name: '%s'",
	      unit->vendor );
    }
    
    if( usb_get_string_simple( unit->handle, 2, buffer, 255) < 0 )
    {
	slog( LOG_ERROR, "Failed to get device product string" );
    }
    unit->product = strdup( buffer );
    
    if( unit->product && strcmp( unit->product, "Net MD Walkman" ))
    {
	slog( LOG_WARNING,
	      "Device's product string is '%s' not 'Net MD Walkman'",
	      unit->product
    );
    }
	    
    return 0;
}


/*
 *  Function to close a unit
 */

void omd_close_unit( omd_unit_t *unit )
{
    slog( LOG_DEBUG, "close unit at 0x%p", unit );
    if( unit )
    {
	if( ! unit->open )
	{
	    slog( LOG_ERROR, "omd_close_unit: Device was not open" );
	    return;
	}
	/* Give the USB resources back */
	if( unit->handle )
	{
	    usb_release_interface( unit->handle, 0 );
	    usb_close( unit->handle );
	    unit->handle = NULL;
	}
        /* Mark as closed */
        unit->open = FALSE;
	
	if( unit->vendor )
	{
	    free( (char *) unit->vendor );
	}
	if( unit->product )
	{
	    free( (char *) unit->product );
	}
    }
}

/*****************************************************************************
 * Scan for new devices, mark detached ones
 */

static omd_unit_t *find_by_position( int bus, int dev )
{
    int i;
    for( i = 0; i < num_units; i++ )
    {
	if( ( omd_units[i].busid == bus ) && ( omd_units[i].devid == dev ))
	{
	    return omd_units + i;
	}
    }
    return NULL;
}

int omd_scan_usb()
{
    struct usb_bus		*bus;
    struct usb_device		*device;
    const omd_unit_info_t	*info;
    omd_unit_t			*unit;
    static int			initialised = FALSE;
    int				i, busid, devid;

    if( ! initialised )
    {
	usb_init();
	initialised = TRUE;
    }
    usb_find_busses();
    usb_find_devices();

    /* Mark all existing devices so we can see if they've disappeared */
    for( i = 0; i < num_units; i++ )
    {
	omd_units[i].marked = TRUE;
        omd_units[i].changed = FALSE;
    }

    /* Scan for devices */
    for( busid = 0, bus = usb_busses; bus != NULL; busid++, bus = bus->next)
    {
	for( devid = 0, device = bus->devices; device != NULL ;
	     devid++, device = device->next) 
	{
	    info = omd_match_unit( device->descriptor.idVendor,
				   device->descriptor.idProduct );

	    if( info )
	    {
		if( (( unit = find_by_position( busid, devid )) != NULL ) &&
		    ( unit->info == info ))
		{
		    unit->marked = FALSE;
		    if( unit->present )
		    {
			/* Existing device, still there */
			continue;
		    }
		    /* We've seen this one before (or one very like it :) */
		    if( unit->open )
		    {
			slog( LOG_WARNING,
			      "Re-attached device was still open!");
		    }
		}
		else
		{
		    /* Allocate a new one */
		    if( num_units >= MAXUNIT )
		    {
			slog( LOG_ERROR, "Too many NetMD units found!" );
			continue;
		    }
		    unit = omd_units + num_units;
		    num_units++;
		}
		memset( unit, '\0', sizeof( omd_unit_t ));
		unit->device = device;
		unit->info = info;
		unit->busid = busid;
		unit->devid = devid;
		unit->vendorid = device->descriptor.idVendor;
		unit->productid = device->descriptor.idProduct;
		unit->present = TRUE;
		unit->open = FALSE;
		unit->marked = FALSE;
                unit->changed = TRUE;
                unit->userdata = NULL;
                unit->busy_handler = NULL;
	    }
	}
    }

    /* Scan for detached devices still marked as present */
    for( i = 0; i < num_units; i++ )
    {
	if( omd_units[i].marked && omd_units[i].present )
	{
	    slog( LOG_INFO, "Device %d detached", i );
	    if( omd_units[i].open )
	    {
		slog( LOG_WARNING, "Device %d detached while open", i );
		/* XXX should we really try and close it? */
		omd_close_unit( omd_units + i );
	    }
	    omd_units[i].marked = FALSE;
	    omd_units[i].present = FALSE;
            omd_units[i].changed = TRUE;
	    /* XXX provide a callback function to notify app? */
	}
    }
    
    return num_units;
}

/*****************************************************************************
 * Scan harder for devices... Open every device we can and look at
 * the product string
 */


int omd_probe_all() {
    struct usb_bus	*bus;
    struct usb_device	*device;
    usb_dev_handle	*handle;
    omd_unit_info_t	*info;
    static int		initialised = FALSE;
    int			busid, devid;
    char		buffer[256];
    int			num_found = 0;

    if( ! initialised )
    {
	usb_init();
	initialised = TRUE;
    }
    usb_find_busses();
    usb_find_devices();

    /* Scan for devices */
    for( busid = 0, bus = usb_busses; bus != NULL; busid++, bus = bus->next)
    {
	for( devid = 0, device = bus->devices; device != NULL ;
	     devid++, device = device->next) 
	{
	    if( ( handle = usb_open( device )) == NULL )
	    {
		slog( LOG_TRACE, "Cannot open bus %d dev %d", busid, devid );
		continue;
	    }
	    
	    if( usb_get_string_simple( handle, 2, buffer, 255 ) <= 0 )
	    {
		slog( LOG_TRACE, "Cannot get product name for %d/%d",
		      busid, devid );
		usb_close( handle );
		continue;
	    }
	    
	    usb_close( handle );
            // XXX - do all devs have the "Walkman" part of the string
	    slog( LOG_DEBUG, "Device %d claims to be '%s'", devid, buffer );
	    if( strcmp( buffer, "Net MD Walkman" ) &&
		strcmp( buffer, "Net MD" ))
	    {
		slog( LOG_TRACE, "Device %d/%d is not a Net MD Walkman",
		      busid, devid );
		continue;
	    }
	    
	    
	    if ( ( info = omd_match_unit( SPARE, SPARE )) == NULL )
	    {
		slog( LOG_ERROR, "No more spare info structure!" );
		break;
	    }
	    
	
            info->vendor_id = device->descriptor.idVendor;
            info->product_id = device->descriptor.idProduct;
            info->vendor = guess_vendor( device->descriptor.idVendor );
            info->model = "(Unknown)";
            num_found++;
	    slog( LOG_INFO, "Add Device %d/%d, Vendor %x/%x, %s - %s",
		  busid, devid,
		  info->vendor_id, info->product_id,
		  info->vendor, info->model );
	    
	}
    }

    return num_found;
}


/*****************************************************************************
 * Release all allocated NetMD devices
 */

int omd_close_all_devices()
{
    int		i;

    for( i = 0; i < num_units; i++ )
    {
	omd_close_unit( omd_units + i );
    }
    num_units = 0;
    return 0;
}


/*****************************************************************************
 * Get a handle on a specific unit
 */

omd_unit_t *omd_get_unit( int number )
{
    if( number >= 0 && number < num_units ) {
	return omd_units + number;
    }
    return NULL;
}

/*****************************************************************************
 * Get and open the default unit (first one we see and can open)
 */
omd_unit_t *omd_open_default_unit(  )
{
    omd_unit_t	*unit = NULL;
    int		num;
    
    if( ( num = omd_scan_usb()) < 0 )
    {
	slog( LOG_ERROR, "omd_open_default_unit: Cannot scan USB bus" );
	return NULL;
    }
    if( num == 0 )
    {
	slog( LOG_INFO, "omd_open_default_unit: No NetMD devices" );
	return NULL;
    }
    if( num > 1 )
    {
	slog( LOG_WARNING,
	      "omd_open_default_unit: Multiple devices - Using first" );
    }

    unit = omd_get_unit( 0 );

    if( omd_open_unit( unit ) < 0 )
    {
	slog( LOG_ERROR, "omd_open_default_unit: Cannot open unit" );
	return NULL;
    }

    return unit;
}

/*****************************************************************************
 * Get unit status
 */

omd_unit_status_t *omd_get_unit_status( omd_unit_t *unit, omd_unit_status_t *status)
{
    int		allocated = FALSE;
    
    if( unit == NULL )
    {
	slog( LOG_ERROR, "omd_get_unit_status: NULL unit" );
	return NULL;
    }
    if( status == NULL )
	{
	status = malloc( sizeof( omd_unit_status_t ));
	if( status == NULL )
	{
	    slog( LOG_ERROR, "omd_get_unit_status: Malloc error" );
	    return NULL;
	}
	allocated = TRUE;
    }

    if( netmd_unit_status( unit, status->raw, OMD_UNIT_STATUS_RAW_SIZE ) < 0 )
    {
	slog( LOG_ERROR, "Unit status command failed" );
	if( allocated )
	{
	    free( status );
	}
	return NULL;
    }

    /* Interpret "disc present" byte as best we know how */
    if( status->raw[4] == 0x40 )
    {
	status->disc_inserted = TRUE;
    }
    else if( status->raw[4] == 0x80 )
    {
	status->disc_inserted = FALSE;
    }
    else
    {
	slog( LOG_ERROR, "omd_get_unit_status(): Unexpected disc present byte 0x%02x", status->raw[4] );
	status->disc_inserted = FALSE; /* Err on the safe side */
    }


    return status;
}

/*****************************************************************************
 * Get playback status, type A (no idea what this one means)
 */
omd_playback_status_a_t *omd_get_playback_status_a( omd_unit_t *unit,
						    omd_playback_status_a_t
						    *status)
{
    int		allocated = FALSE;
    
    if( unit == NULL )
    {
	slog( LOG_ERROR, "omd_playback_status_a_t: NULL unit" );
	return NULL;
    }

    if( status == NULL )
	{
	status = malloc( sizeof( omd_playback_status_a_t ));
	if( status == NULL )
	{
	    slog( LOG_ERROR, "omd_get_playback_status_a: Malloc error" );
	    return NULL;
	}
	allocated = TRUE;
    }

    if( netmd_playback_status_a( unit, status->raw,
				 OMD_PLAYBACK_STATUS_A_RAW_SIZE ) < 0 )
    {
	slog( LOG_ERROR, "Unit status command failed" );
	if( allocated )
	{
	    free( status );
	}
	return NULL;
    }

    return status;
}

/*****************************************************************************
 * Get playback status, type B (contains current playback mode)
 */
omd_playback_status_b_t *omd_get_playback_status_b( omd_unit_t *unit,
						    omd_playback_status_b_t
						    *status)
{
    int		allocated = FALSE;
    
    if( unit == NULL )
    {
	slog( LOG_ERROR, "omd_get_playback_status_b: NULL unit" );
	return NULL;
    }

    if( status == NULL )
	{
	status = malloc( sizeof( omd_playback_status_b_t ));
	if( status == NULL )
	{
	    slog( LOG_ERROR, "omd_get_playback_status_b: Malloc error" );
	    return NULL;
	}
	allocated = TRUE;
    }

    if( netmd_playback_status_b( unit, status->raw,
				 OMD_PLAYBACK_STATUS_B_RAW_SIZE ) < 0 )
    {
	slog( LOG_ERROR, "Unit status command failed" );
	if( allocated )
	{
	    free( status );
	}
	return NULL;
    }

    /* Sanity check */
    if( (status->raw[4] != 0xc3) && (status->raw[4] != 0xc5) && (status->raw[4] != 0xff))
    {
	slog( LOG_WARNING, "Unexpected playback status value: 0x%02x 0x%02x",
		 status->raw[4], status->raw[5] );
    }
    status->playback_mode = (status->raw[4] << 8) | status->raw[5];
    
    return status;
}

/*****************************************************************************
 * Get playback position
 */
omd_playback_position_t *omd_get_playback_position( omd_unit_t *unit,
						    omd_playback_position_t
						    *status)
{
    int		allocated = FALSE;
    
    if( unit == NULL )
    {
	slog( LOG_ERROR, "omd_get_playback_position: NULL unit" );
	return NULL;
    }

    if( status == NULL )
	{
	status = malloc( sizeof( omd_playback_position_t ));
	if( status == NULL )
	{
	    slog( LOG_ERROR, "omd_get_playback_position: Malloc error" );
	    return NULL;
	}
	allocated = TRUE;
    }

    if( netmd_get_playback_position( unit, status->raw,
				     OMD_PLAYBACK_POSITION_RAW_SIZE ) < 0 )
    {
	slog( LOG_ERROR, "Unit status command failed" );
	if( allocated )
	{
	    free( status );
	}
	return NULL;
    }

    /* Parse out track and position from raw data */
    status->track = status->raw[6];
    if( omd_time_from_bytes( &status->position, status->raw + 7 ) < 0 )
    {
	slog( LOG_ERROR, "omd_playback_position_t: Time parse error" );
	omd_time_from_double( &status->position, 0.0 );
    }
    
    return status;
}



int omd_set_playback_track( omd_unit_t *unit, int track )
{
    return netmd_set_playback_track( unit, track );
}

int omd_set_playback_position( omd_unit_t *unit, omd_playback_position_t *pos)
{
    return netmd_set_playback_position( unit, pos->track,
					omd_time_to_bcd( &pos->position ) );
}


/* Busy handler */
void omd_unit_set_busy_handler( omd_unit_t *unit, omd_busy_handler handler, void *userdata)
{
    unit->busy_handler = handler;
    unit->busy_data = userdata;
    
}


void omd_unit_busy( omd_unit_t *unit )
{
    if( unit && unit->busy_handler )
    {
        unit->busy_handler( unit->busy_data, TRUE );
    }
}

void omd_unit_idle( omd_unit_t *unit )
{
    if( unit && unit->busy_handler )
    {
        unit->busy_handler( unit->busy_data, FALSE );
    }
}


