/*
 * 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.
 *
 *
 *
 * ** omd_toc.c - Table of contents stuff
 */
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "omd.h"

CVSID( "$Id: omd_toc.c,v 1.27 2003/08/25 17:13:06 pete Exp $" );

/*****************************************************************************
 * Bitrate type names and lookup function
 */
struct bitrate_info {
    uint8	bitrate;
    uint8	mono;
    const char	*name;
};

static struct bitrate_info bitrates[] = {
    { OMD_BITRATE_SP,	OMD_FLAG_STEREO,	"Stereo" },
    { OMD_BITRATE_LP2,	OMD_FLAG_STEREO,	"LP2" },
    { OMD_BITRATE_LP4,	OMD_FLAG_STEREO,	"LP4" },
    { OMD_BITRATE_SP,	OMD_FLAG_MONO,		"Mono" },
    { OMD_BITRATE_LP2,	OMD_FLAG_MONO,		"LP2 Mono?" },
    { OMD_BITRATE_LP4,	OMD_FLAG_MONO,		"LP4 Mono?" },
    {		    0,		    0,		NULL},
};

static const char *find_bitrate_name( uint8 rate, uint8 mono )
{
    struct bitrate_info *info;

    for( info = bitrates; info->name; info++ )
    {
	if( ( info->bitrate == rate ) && ( info->mono == mono ))
	{
	    return info->name;
	}
    }
    return "Unknown!";
}


/*****************************************************************************
 * Allocate an omd_toc_t with space for an initial number of tracks
 * (-1 means use default on-the-fly allocation policy
 */

/* Tuning params ... maybe these should be runtime tweakable */
#define TRACK_ALLOCSIZE		16
#define GROUP_ALLOCSIZE		8

omd_toc_t *omd_toc_allocate()
{
    omd_toc_t	*toc = malloc( sizeof( omd_toc_t ));

    if( toc )
    {
	memset( toc, '\0', sizeof( omd_toc_t ));
    }
    return toc;
}




/*****************************************************************************
 * Free a TOC and related resources
 */

void omd_toc_free( omd_toc_t *toc )
{
    int i;
    
    if( toc )
    {
	/* Free the title if there is one */
	if( toc->title )
	{
	    free( toc->title );
	}

	/* Free track info */
	if( toc->track )
	{
	    /* Free the title strings of any tracks */
	    for( i = 0; i < toc->track_count; i++ )
	    {
		if( toc->track[i].title )
		{
		    free( toc->track[i].title );
		}
	    }
	    free( toc->track );
	}
	
	/* Free group info */
	if( toc->group )
	{
	    /* Free the title strings of any tracks */
	    for( i = 0; i < toc->group_count; i++ )
	    {
		if( toc->group[i].name )
		{
		    free( toc->group[i].name );
		}
	    }
	    free( toc->group );
	}

	/* And finally free the toc structure */
	free( toc );
    }
}


/*****************************************************************************
 * Return a track structure - allocate if needed
 */

omd_track_t	*omd_toc_get_track( omd_toc_t *toc, int trackid )
{
    int		newsize, newbytes, oldbytes;
    omd_track_t	*tmp, *newtrk;
    
    if( toc )
    {
	/* Easy case, we already have enough space */
	if( trackid < toc->_maxtrack )
	{
	    return( toc->track + trackid );
	}

	/* Need to allocate some more space and remember old allocation */
	oldbytes = toc->_maxtrack * sizeof( omd_track_t );
	for( newsize = toc->_maxtrack; newsize <= trackid; )
	{
	    newsize += TRACK_ALLOCSIZE;
	}
	newbytes = newsize * sizeof( omd_track_t );
	slog( LOG_DEBUG, "Re-allocating space to allow %d tracks", newsize );
	/* Do this a bit long-windedly to make sure any new space is zeroed*/
	tmp = malloc( newbytes );
	if( tmp )
	{
	    memset( tmp, 0, newbytes );
	    if( toc->track )
	    {
		memcpy( tmp, toc->track, oldbytes );
		free( toc->track );
	    }
	    toc->track = tmp;
	    toc->_maxtrack = newsize;

	    newtrk = toc->track + trackid;
	    return( newtrk );
	}
	slog( LOG_ERROR, "Alloc failed" );
    }
    return NULL;
}


/*****************************************************************************
 * Return  a group structure - allocate if necessary
 */

omd_group_t	*omd_toc_get_group( omd_toc_t *toc, int groupid )
{
    int		newsize, newbytes, oldbytes;
    omd_group_t	*tmp, *newgrp;
    
    if( toc )
    {
	/* Easy case, we already have enough space */
	if( groupid < toc->_maxgroup )
	{
	    return( toc->group + groupid );
	}

	/* Need to allocate some more space and remember old allocation */
	oldbytes = toc->_maxgroup * sizeof( omd_group_t );
	for( newsize = toc->_maxgroup; newsize <= groupid; )
	{
	    newsize += GROUP_ALLOCSIZE;
	}
	newbytes = newsize * sizeof( omd_group_t );
	slog( LOG_DEBUG, "Re-allocating space to allow %d groups", newsize );
	/* Do this a bit long-windedly to make sure any new space is zeroed*/
	tmp = malloc( newbytes );
	if( tmp )
	{
	    memset( tmp, 0, newbytes );
	    if( toc->group )
	    {
		memcpy( tmp, toc->group, oldbytes );
		free( toc->group );
	    }
	    toc->group = tmp;
	    toc->_maxgroup = newsize;

	    newgrp = toc->group + groupid;
	    return( newgrp );
	}
	slog( LOG_ERROR, "Alloc failed" );
    }
    return NULL;
}


/*****************************************************************************
 * parse_groups - Parse the group information (and title, if any ) out of
 * the raw title string and use it generate group entries
 */

static int parse_groups( omd_toc_t *toc )
{
    char tmp[ OMD_RAW_DISC_TITLE_MAX + 1 ];
    char *thistok, *nexttok, *semi, *hyphen;
    int	 start, finish, grpid;
    omd_group_t *group;

    /* Take a copy of the raw title...less efficient, but this way we
     * can chop it up but leave *toc intact for debugging
     */
    strcpy( tmp, toc->raw_title );
    thistok = tmp;
    for( thistok = tmp; thistok; thistok = nexttok )
    {
	/* Split on next // token --- strtok is no good here because
	 * it will split on / so we just search by hand
	 */
	for( nexttok = thistok; *nexttok; nexttok++ )
	{
	    if( nexttok[0] == '/' && nexttok[1] == '/' )
	    {
		break;
	    }
	}
	/* Now we're either on a token or the end of the string */
	if( *nexttok == '/' )
	{
	    /* Hit one... drop a terminator here and point to following string */
	    *nexttok = '\0';
	    nexttok += 2;
	    /* Handle case where there's a spare sep. on the end of the title */
	    if( *nexttok == '\0' )
	    {
		nexttok = NULL;
	    }
	}
	else
	{
	    nexttok = NULL;
	}

	/* Finally we have a token.  Parse it */
	slog( LOG_TRACE, "Title token: '%s'", thistok );

	/* Ignore any leading 0; so various title cases work out the same */
	if( (thistok[0] == '0') && thistok[1] == ';' )
	{
	    thistok += 2;
	}

	/* Look for a valid group specifier, ie "1-7;"
	 * We should scan this by hand too so we can cope
	 * with titles with semicolons in them.
	 */
	for( semi = thistok; *semi; semi++ )
	{
	    if( *semi == ';' )	/* Found a semicolon */
		break;
	    if( ! strchr( "01234567890-", *semi ))
		break;		/* Found a char which couldn't be a grp spec */
	}
	if( *semi != ';' )
	{
	    /* No Semicolon so it must be a title string */
	    if( toc->title )
	    {
		slog( LOG_ERROR, "Parse error in title string - 2nd title??");
		return -1;
	    }
	    toc->title = strdup( thistok );
	    slog( LOG_DEBUG, "Disc Title: '%s'", thistok );
	    continue;
	}

	/* So now parse it as a group */
	*semi = '\0';
	hyphen = strchr( thistok, '-' );
	if( hyphen )
	{
	    /* We have (eg) "1-5;" */
	    *hyphen = '\0';
	    start = atoi( thistok );
	    finish = atoi( hyphen + 1 );
	}
	else
	{
	    /* We have (eg) "7;" */
	    start = finish = atoi( thistok );
	}
	if( (start <= 0) || (finish <= 0) )
	{
	    slog( LOG_ERROR, "Parse error in title string - invalid groups");
	    return -1;
	}
	if( finish < start )
	{
	    slog( LOG_WARNING, "Invalid group specification: start < finish");
	    finish = start;
	    return -1;
	}
	if( finish > toc->track_count )
	{
	    slog( LOG_WARNING, "Invalid group specification: finish is too large");
	    finish = toc->track_count;
	    return -1;
	}
	
	grpid = toc->group_count;
	if( ( group = omd_toc_get_group( toc, grpid )) == NULL )
	{
	    slog( LOG_ERROR, "Group allocation error");
	    return -1;
	}
	toc->group_count++;
	group->start = start - 1; /* -1 to get same base as track numbers */
	group->finish = finish -1;
	group->name = strdup( semi + 1 ); /* Name starts after semicolon */

	slog( LOG_DEBUG, "Disc Group %d: From track %d to %d: '%s'",
		 toc->group_count, group->start, group->finish, group->name );

    }
    
    return 0;
}


/*****************************************************************************
 * Read and parse the group info into an already allocated TOC structure
 */
int omd_toc_read_groups( omd_unit_t *unit, omd_toc_t *toc )
{
    if( ( unit == NULL ) || ( toc == NULL ))
    {
	slog( LOG_ERROR, "omd_toc_read_groups: NULL unit or toc passed in" );
	return -1;
    }

    /* Read raw disc title */
    if( netmd_disc_get_title( unit, toc->raw_title,
			      OMD_RAW_DISC_TITLE_MAX ) < 0 )
    {
	slog( LOG_ERROR, "omd_toc_read_groups: Unable to retrieve disc title");
	return -1;
    }
    slog( LOG_INFO, "Read raw disc title: '%s'", toc->raw_title );
    
    /* Free any existing title and zero group list */
    if( toc->title )
    {
	free( toc->title );
	toc->title = NULL;
    }
    toc->group_count = 0;
    
    /* Parse any group info in the title */
    if( parse_groups( toc ) < 0 )
    {
	slog( LOG_ERROR, "omd_toc_read_groups: Title parse error" );
	return -1;
    }

    return 0;
}

/*****************************************************************************
 * Read information for some or all tracks into a pre-allocated
 * toc... NB unless the group info has already been read, the tracks
 * will end up with incorrect group entries
 */
int omd_toc_read_tracks( omd_unit_t *unit, omd_toc_t *toc, int first, int last)
{
    int i, g;
    omd_track_t *trk;
    
    /* Read the track information for each track */
    for( i = first; i <= last; i++ )
    {
	trk = omd_toc_get_track( toc, i );
	/* By default, not in any group (calculate groups later) */
	trk->group = -1;
	if( omd_read_track_info( unit, i, trk) < 0 )
	{
	    slog( LOG_ERROR, "Error reading info for track %d", i + 1 );
	    return -1;
	}
	if( i >= toc->track_count )
	{
	    slog( LOG_INFO, "New track count %d", i + 1 );
	    toc->track_count = i + 1;	/* XXX rethink this */
	}
    }

    /* Fill in leftover group information - XXX optimise so we
     * don't keep doing this
     */
    for( g = 0; g < toc->group_count; g++ )
    {
	for( i = toc->group[g].start; i <= toc->group[g].finish; i++ )
	{
	    if( ( i >= first ) && ( i <= last ) )
	    {
		toc->track[i].group = g;
	    }
	}
    }
    return 0;
}

/*****************************************************************************
 * Refresh an in-memory TOC's track info. This should only need to
 * be called ifwe think the number of tracks on the disc has changed.
 * Otherwise omd_toc_invalidate_track() for individual tracks that
 * we bthink have changed is sufficient
 *
 * XXX rationalise this with omd_toc_read!
 */
int omd_toc_refresh_tracks( omd_unit_t *unit, omd_toc_t *toc,
			    omd_disc_info_t *discinfo )
{
    if( ( unit == NULL ) || ( toc == NULL ))
    {
	slog( LOG_ERROR, "omd_toc_refresh_tracks: NULL unit passed in" );
	return -1;
    }

    /* Invalidate any loaded track info */
    if( omd_toc_invalidate_track( unit, toc, -1 ) < 0 )
    {
	slog( LOG_ERROR,"omd_toc_refresh_tracks: Cannot invalidate track inf");
	return -1;
    }
    /* Determine the new track count */
    if( (toc->track_count = omd_disc_get_track_count( unit, discinfo )) < 0 )
    {
	slog( LOG_ERROR, "omd_toc_refresh_tracks: Unable to determine count" );
	return -1;
    }
    
    
    return 0;
}                    


/*****************************************************************************
 * Allocate and read a disc's table of contents.  Can either be passed
 * existing disc_info or will read it's own from the unit
 */
omd_toc_t	*omd_toc_read( omd_unit_t *unit, omd_disc_info_t *discinfo,
			       int read_groups, int read_tracks )
{
    int		ntrack;
    omd_toc_t	*toc;

    if( unit == NULL )
    {
	slog( LOG_ERROR, "omd_toc_read: NULL unit passed in" );
	return NULL;
    }

    /* Determine track count */
    if( (ntrack = omd_disc_get_track_count( unit, discinfo )) < 0 )
    {
	slog( LOG_ERROR, "omd_toc_read: Unable to determine track count" );
	return NULL;
    }

    /* Allocate TOC structure */
    if( (( toc = omd_toc_allocate()) == NULL ) ||
	( omd_toc_get_track( toc, ntrack ) == NULL ))
    {
	slog( LOG_ERROR, "TOC allocation failed" );
	return NULL;
    }
    toc->track_count = ntrack;

    /* Have to read groups in order to read tracks */
    if( read_tracks )
    {
	read_groups = TRUE;
    }
    
    /* Read groups */
    if( read_groups )
    {
	if( omd_toc_read_groups( unit, toc ) < 0 )
	{
	    goto errexit;
	}
    }

    /* Read all tracks */
    if( read_tracks )
    {
	if( omd_toc_read_tracks( unit, toc, 0, toc->track_count - 1 ) < 0 )
	{
	    goto errexit;
	}
    }
    
    return toc;

 errexit:
    if( toc != NULL )
    {
	omd_toc_free( toc );
    }
    return NULL;
}

/*****************************************************************************
 * Get the info structure for a track from a toc.  This is the preferred
 * method as it will handle "lazy" loadinf of track data from the disk.
 *
 */
omd_track_t *omd_toc_track_info( omd_unit_t *unit, omd_toc_t *toc, int trackid)
{
    omd_track_t *info;
    
    if(( trackid < 0 ) || ( trackid >= toc->track_count ))
    {
	slog( LOG_ERROR, "omd_toc_track_info: Invalid track" );
	return NULL;
    }
    // slog( LOG_INFO, "omd_toc_track_info: Get track %d", trackid );
    info = omd_toc_get_track( toc, trackid );
    
    /* Already loaded? */
    if( info->loaded )
    {
	return info;
    }

    slog( LOG_DEBUG, "omd_toc_track_info: Read track %d from disk", trackid );
    omd_unit_busy( unit );
    if( omd_toc_read_tracks( unit, toc, trackid, trackid ) < 0 )
    {
        omd_unit_idle( unit );
	slog( LOG_ERROR, "omd_toc_track_info: Track read failed" );
	return NULL;
    }
    omd_unit_idle( unit );
    
    if( ! info->loaded )
    {
        slog( LOG_WARNING, "omd_toc_track_info: Track %d still not loaded??", trackid );
    }
    
    return info;
}


/*****************************************************************************
 * Invalidate a track entry so it will get re-read from the disk
 */

int omd_toc_invalidate_track( omd_unit_t *unit, omd_toc_t *toc, int trackid )
{
    omd_track_t *info;
    int i, first = trackid, last = trackid;

    if(( trackid < 0 ) || ( trackid >= toc->track_count ))
    {
	/* Invalidate all tracks if given 1- or anything else illegal */
	first = 0;
	last = toc->track_count - 1;
    }
    for( i = first; i <= last; i++ )
    {
	info = omd_toc_get_track( toc, i );
	info->loaded = FALSE;
    }
    return 0;
}



/*****************************************************************************
 * Read all the info for a track into a pre-allocated omd_track_t
 */
int omd_read_track_info(omd_unit_t *unit, int num, omd_track_t *track)
{
    uint8 buffer[OMD_RAW_TRACK_TITLE_MAX + 1];

    
    if(( unit == NULL) || (track == NULL ))
    {
	slog( LOG_ERROR, "omd_read_track_info(%d): Invalid arguments", num );
	return -1;
    }

    /* First read the track title */
    if( netmd_track_get_title( unit, num, buffer,OMD_RAW_TRACK_TITLE_MAX ) < 0 )
    {
	slog( LOG_ERROR, "omd_read_track_info: Cannot read title" );
	return -1;
    }
    if( track->title )
    {
	free( track->title );
    }
    if( strncmp( buffer, "LP:", 3 ))
    {
	track->title = strdup( buffer );
    }
    else
    {
	/* Lose the LP: ... (remember to put it back later when writing)... */
	track->title = strdup( buffer + 3 );
    }


    /* Next get the track length */
    if( netmd_track_get_length( unit, num,buffer,OMD_TRACK_LENGTH_RAW_SIZE ) < 0)
    {
	slog( LOG_ERROR, "omd_read_track_info: Cannot read length" );
	return -1;
    }
    /* Sanity check and decode */
    if( (buffer[0] != 0x00) || ( buffer[3] != 0x06 ) ||
	( omd_time_from_bytes( &track->length, buffer + 6 ) < 0 ))
    {
	slog( LOG_ERROR, "omd_read_track_info: invalid length data" );
	return -1;
    }


    /* Next get bitrate info */
    if( netmd_track_get_bitrate( unit, num, buffer,
				 OMD_TRACK_BITRATE_RAW_SIZE ) < 0)
    {
	slog( LOG_ERROR, "omd_read_track_info: Cannot read bitrate" );
	return -1;
    }
    if( (buffer[0] != 0x80) || ( buffer[3] != 0x04 ))
    {
	slog( LOG_ERROR, "omd_read_track_info: invalid bitrate data" );
	return -1;
    }
    track->bitrate = buffer[6];
    track->mono = buffer[7];
    track->bitrate_name = find_bitrate_name( track->bitrate, track->mono );
    /* Work out the codec. XXX we may just be plain wrong here! */
    if( track->bitrate == OMD_BITRATE_SP )
    {
	track->codec = OMD_CODEC_ATRAC;
	track->codec_name = "ATRAC";
    }
    else
    {
	track->codec = OMD_CODEC_ATRAC3;
	track->codec_name = "ATRAC3";
    }
    
    /* Finally get the track flags */
    if( netmd_track_get_flags( unit, num, &track->flags ) < 0)
    {
	slog( LOG_ERROR, "omd_read_track_info: Cannot read flags" );
	return -1;
    }

    track->loaded = TRUE;
    
    return 0;
}

/*****************************************************************************
 * Rename a track
 */
int omd_rename_track( omd_unit_t *unit, int track, omd_track_t *oldinfo,
		      const char *name)
{
    omd_track_t trackbuf;
    int allocated = FALSE, oldlen;
    char *newtitle = NULL;
    int rc = 0;

    if( oldinfo == NULL )
    {
	memset( &trackbuf, '\0', sizeof( trackbuf ));
	if( omd_read_track_info( unit, track, &trackbuf ) < 0 )
	{
	    slog( LOG_ERROR, "omd_read_track_info: Unable to read old info" );
	    return -1;
	}
	allocated = TRUE;
	oldinfo = &trackbuf;
    }
    oldlen = strlen( oldinfo->title );
    if( oldinfo->codec == OMD_CODEC_ATRAC3 )
    {
	/* account for LP: prefix on MDLP tracks */
	oldlen += 3;
	if( (newtitle = malloc( strlen(name) + 4 )) == NULL )
	{
	    slog( LOG_ERROR, "omd_read_track_info: malloc error" );
	    rc = -1;
	    goto errexit;
	}
	strcpy( newtitle, "LP:" );
	strcat( newtitle, name );
    }

    if( netmd_track_set_title( unit, track, oldlen,
			       newtitle ? newtitle : name ) < 0 )
    {
	slog( LOG_ERROR, "omd_read_track_info: NetMD error" );
	rc = -1;
    }
    
 errexit:
    if( allocated && oldinfo->title )
    {
	free( oldinfo->title );
    }
    if( newtitle )
    {
	free( newtitle );
    }
    return rc;
}

/*****************************************************************************
 * Sanitise a group/title string so we can parse it later (as per OpenMG)
 * (private)
 */
static void title_sanitise( uint8 *str )
{
    /* Get rid of any illegal sequences like // */
    for( ; *str ; str++ )
    {
	if( (str[0] == '/') && (str[1] == '/' ))
	{
	    str[1] = ' ';
	}
	/* Also slashes at the end of a string cause confusion */
	if( (str[0] == '/') && (str[1] == '\0' ))
	{
	    str[0] = ' ';
	}
	/*
	 * XXX Allow a bigger charset, including kana
	 * Limit to 7 bit ASCII for now
	 */
	if( (*str < ' ') || (*str > '~' ))
	{
	    *str = '.';
	}
    }
}



/*****************************************************************************
 * Add a new group to a TOC structure
 */
int omd_toc_add_group( omd_toc_t *toc, int first, int last, const char *name )
{
    int i;
    omd_group_t *group;

    /* Sanity check - make sure values are legal */
    if( ( first < 0 ) || ( first >= toc->track_count ) ||
	( last < 0 ) || ( last >= toc->track_count ) ||
	( first > last ))
    {
	slog( LOG_ERROR, "omd_toc_add_group: Invalid group information" );
	return -1;
    }
    if( strlen( name ) > (OMD_RAW_TRACK_TITLE_MAX / 2))	/* XXX Fudge */
    {
	slog( LOG_ERROR, "omd_toc_add_group: Group name too long" );
	return -1;
    }
    
    /* Make sure we don't overlap any other groups (XXX is that legal?) */
    for( i = 0, group = toc->group; i < toc->group_count; i++, group++ )
    {
	if( !(((first < group->start) && (last < group->finish)) ||
	      ((first > group->start) && (last > group->finish))) )
	{
	    slog( LOG_ERROR,"omd_toc_add_group: Group overlaps existing grps");
	    return -1;
	}
    }

    /* Allocate a new group and fill in the info */
    if( ( group = omd_toc_get_group( toc, toc->group_count )) == NULL )
    {
	slog( LOG_ERROR,"omd_toc_add_group: alloc failed");
	return -1;
    }
    toc->group_count++;
    group->start = first;
    group->finish = last;
    group->name = strdup( name );
    title_sanitise( group->name );

    /* Sort the groups (not sure if this is necessary */
    omd_toc_sort_groups( toc );

    return 0;
}



/*****************************************************************************
 * Rename a group
 */

int omd_toc_rename_group( omd_toc_t *toc, int groupidx, const char *name )
{
    if( ( groupidx < 0 ) || ( groupidx >= toc->group_count ))
    {
	slog( LOG_ERROR, "omd_toc_rename_group: Invalid group" );
	return -1;
    }

    /* Free the old title if there is one */
    if( toc->group[groupidx].name )
    {
	free( toc->group[groupidx].name );
    }
    toc->group[groupidx].name = strdup( name );
    title_sanitise( toc->group[groupidx].name );
    return 0;
}

/*****************************************************************************
 * Retitle the disc
 */
int omd_toc_set_title( omd_toc_t *toc, const char *name )
{

    if( ! toc )
    {
	return -1;
    }

    if( toc->title )
    {
	free( toc->title );
    }

    if( name )
    {
	toc->title = strdup( name );
	title_sanitise( toc->title );
    }
    else
    {
	/* NULL title is legit and means no title */
	toc->title = NULL;
    }
    return 0;
}


/*****************************************************************************
 * Delete a group
 */

int omd_toc_delete_group( omd_toc_t *toc, int groupidx )
{
    if( ( groupidx < 0 ) || ( groupidx >= toc->group_count ))
    {
	slog( LOG_ERROR, "omd_toc_rename_group: Invalid group" );
	return -1;
    }

    /* Free the old title if there is one */
    if( toc->group[groupidx].name )
    {
	free( toc->group[groupidx].name );
    }

    toc->group_count--;
    if( groupidx < toc->group_count )
    {
	memmove( toc->group + groupidx, toc->group + groupidx + 1,
		 (toc->group_count - groupidx) * sizeof( omd_group_t ));
    }

    return 0;
}



/*****************************************************************************
 * Sort the groups in a toc
 */

static int groupcmp( const void *a, const void *b )
{
    const omd_group_t *ga = (const omd_group_t *) a;
    const omd_group_t *gb = (const omd_group_t *) b;

    /* Assumes non-overlapping groups */
    return ga->start - gb->start;
}

void omd_toc_sort_groups( omd_toc_t *toc )
{
    if( toc && ( toc->group_count > 1 ))
    {
	qsort( toc->group, toc->group_count, sizeof( omd_group_t ), groupcmp );
    }
}


/*****************************************************************************
 * Generate new disc title and write it out
 */

int omd_toc_write_title( omd_unit_t *unit, omd_toc_t *toc )
{
    uint8	buffer[OMD_RAW_TRACK_TITLE_MAX + 1];
    uint8	countbuf[32];
    uint8	*s = buffer;
    int		i, len, space, countlen;
    omd_group_t	*group;

    if( toc->title && (strlen( toc->title ) > 0) )
    {
	sprintf( buffer, "0;%s//", toc->title );
	len = strlen( buffer );
	s = buffer + len;
	space = OMD_RAW_TRACK_TITLE_MAX - len;
    }
    else
    {
	buffer[0] = '\0';
	len = 0;
	s = buffer;
	space = OMD_RAW_TRACK_TITLE_MAX;
    }

    for( i = 0, group = toc->group; i < toc->group_count; i++, group++ )
    {
	len = strlen( buffer );
	s = buffer + len;
	space = OMD_RAW_TRACK_TITLE_MAX - len;
	if( space <= 0 )
	{
	    slog( LOG_ERROR, "omd_toc_write_title: Title overflow" );
	    return -1;
	}

	if( group->start == group->finish )
	{
	    sprintf( countbuf, "%d;", group->start + 1 );
	}
	else
	{
	    sprintf( countbuf, "%d-%d;", group->start + 1,group->finish + 1);
	}
	countlen = strlen( countbuf ) + 2; /* Two for trailing /'s */

	if( strlen( group->name ) > ( space - countlen ))
	{
	    slog( LOG_ERROR, "omd_toc_write_title: Title overflow" );
	    return -1;
	}

	sprintf( s, "%s%s//", countbuf, group->name );
    }


    slog( LOG_INFO, "Old raw title: %s", toc->raw_title );
    slog( LOG_INFO, "New raw title: %s", buffer );

    if( ! strcmp( toc->raw_title, buffer ))
    {
	slog( LOG_DEBUG, "Title string unchanged, not writing" );
	return 0;
    }
    
    if( netmd_disc_set_title( unit, strlen( toc->raw_title ), buffer ) < 0 )
    {
	slog( LOG_ERROR, "Netmd set title failed" );
	return -1;
    }
    else
    {
	strcpy( toc->raw_title, buffer ); /* XXX re-read from disc? */
    }
    
    return 0;
}

/*****************************************************************************
 * Find the group for a track
 */
int omd_toc_group_for_track( omd_toc_t *toc, int trk )
{
    int grp;
    omd_track_t *track;

    if( ( trk < 0 ) || ( trk >= toc->track_count ))
    {
	slog( LOG_ERROR, "omd_toc_group_for_track: Invalid track ID %d" );
	return -1;
    }

    /* If the track info is loaded, use it directly */
    track = omd_toc_get_track( toc, trk );
    if( track->loaded )
    {
	slog( LOG_DEBUG, "Cached group for track %d is %d", trk, track->group );
	return track->group;
    }

    /* Do it the hard way by working through the groups */
    for( grp = 0; grp < toc->group_count; grp++ )
    {
	if( ( toc->group[grp].start <= trk ) &&
	    ( toc->group[grp].finish >= trk ))
	{
	    return grp;
	}
    }

    /* -1 here is not an error, but "no group" :) */
    return -1;
}


/*****************************************************************************
 * Move a track
 */

int omd_toc_move_track( omd_unit_t *unit, omd_toc_t *toc, int src, int dst )
{
    int		srcgrp, dstgrp, i;
    omd_group_t	*group;

    if( ( src < 0 ) || ( src >= toc->track_count ) ||
	( dst < 0 ) || ( dst >= toc->track_count ))
    {
	slog( LOG_ERROR, "omd_toc_move_track: Invalid tracks\n" );
	return -1;
    }
    
    if( src == dst )
    {
	return 0;
    }

    /* Do the low level move */
    if( netmd_track_move( unit, src, dst ) < 0 )
    {
	slog( LOG_ERROR, "omd_toc_move_track: NetMD move failed\n" );
	return -1;
    }

    srcgrp = omd_toc_group_for_track( toc, src );
    dstgrp = omd_toc_group_for_track( toc, dst );

    if( ( srcgrp > 0 ) && ( dstgrp > 0 ) && ( srcgrp == dstgrp ))
    {
	slog( LOG_INFO, "No group twiddling required. Yay!" );
	return 0;
    }

    /* Shrink src group */
    if( srcgrp >= 0 )
    {
	slog( LOG_INFO, "Group %d: shrink", srcgrp );
	toc->group[srcgrp].finish--;
    }
    /* Grow destination group */
    if( dstgrp >= 0 )
    {
	slog( LOG_INFO, "Group %d: grow", dstgrp );
	toc->group[dstgrp].finish++;
    }

    /* Shuffle groups to account for moved tracks */
    for( i = 0, group = toc->group; i < toc->group_count ; i++, group++ )
    {
	/* Track moved to after this group so shuffle it down */
	if( (src < group->start ) && ( dst >= group->start ))
	{
	    slog( LOG_INFO, "Shuffle down group %d", i );
	    group->start--;
	    group->finish--;
	}
	/* Track moved to before this group so shuffle it up */
	if( (src >= group->start ) && ( dst < group->start ))
	{
	    slog( LOG_INFO, "Shuffle up group %d", i );
	    group->start++;
	    group->finish++;
	}
    }

    /* Done! */
    
    return 0;
}


/*****************************************************************************
 * Delete a track
 */

int omd_toc_delete_track( omd_unit_t *unit, omd_toc_t *toc, int track )
{
    int		i, grpchange = 0;
    omd_group_t	*group;

    if( ( track < 0 ) || ( track >= toc->track_count ))
    {
	slog( LOG_ERROR, "omd_toc_delete_track: Invalid track\n" );
	return -1;
    }

    /* Do the low-level delete */
    if( netmd_track_delete( unit, track ) < 0 )
    {
	slog( LOG_ERROR, "omd_toc_delete_track: NetMD delete failed.\n" );
	return -1;
    }
    
    /* Invalidate affected tracks */
    for( i = track; i < toc->track_count; i++ )
    {
        omd_toc_invalidate_track( unit, toc, i );
    }
    toc->track_count--;

    /* Shuffle groups to account for deleted track */
    for( i = 0; i < toc->group_count ; i++ )
    {
	group = omd_toc_get_group( toc, i );
	if( group->start > track )
	{
	    group->start--;
	    slog( LOG_DEBUG, "Group %d now starts at %d", i, group->start );
            grpchange++;
	}
	if( group->finish >= track )
	{
	    if( group->finish > group->start )
	    {
		group->finish--;
		slog( LOG_DEBUG, "Group %d now ends at %d", i, group->finish );
		grpchange++;
	    }
	    else
	    {
		slog( LOG_WARNING, "Group %d now invalid... Deleting" );
		if( omd_toc_delete_group( toc, i ) < 0 )
		{
		    slog( LOG_WARNING, "Failed to delete group %d", i ); 
		}
                grpchange++;
	    }
	}
    }

    /* Done! */
    return grpchange;
}

