/***********************************************
 *  id3.c
 *                                            
 *  Copyright 2001 Eric Kratzer.              
 *  Includes Xaudio software Copyright (C)
 *           1996-2000 Xaudio Corporation
 *           All Rights Reserved.               
 *
 *  This file is part of imp3sh.
 *                                            
 *  imp3sh is free software; you can distribute
 *  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.
 *
 *  imp3sh 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 recieved a copy of the GNU
 *  General Public License along with imp3sh; if
 *  not, write to the Free Software Foundation,
 *  Inc., 59 Temple Place, Suite 330, Boston MA
 *  02111-1307 USA
 ************************************************/


/* 
 * Some functions taken from Christopher Collins id3tool:
 *
 *
 *
 *  id3tool:  An editor for ID3 tags.
 *  Copyright (C) 1999-2000  Christopher Collins
 *
 *  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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*/


/************************************
 *          Include Files           *
 ************************************/
#include "imp3sh.h"


/************************************
 *       External References        *
 ************************************/

extern DCharBuf *args;

extern CommandDef com[];

extern ListDef *edit_list;

/************************************
 *        Local variables           *
 ************************************/

static char tmpstr_id[COMM_SIZE];

/* File id string location definitions */
static FileInfoDef file_info[] = {
    /* {"Extension", <byte_offset>, <id_length>} */
    {".XM", 17, 21},
    {".ULT", 16, 32},
    {".STX", 0, 20},
    {".S3M", 0, 28},
    {".MTM", 4, 20},
    {".MOD", 0, 21},
    {".M15", 0, 21},
    {".IT", 4, 26},
    {".IMF", 0, 32},
    {".GDM", 4, 32},
    {".FAR", 4, 40},
    {".DSM", 0, 28},
    {".AMF", 4, 32},
    {".669", 2, 108},
    {NULL, 0, 0}
};

/* id3 tag styles */
static struct style_s id3_styles[] = {
    {0x00, "Blues"},
    {0x01, "Classic Rock"},
    {0x02, "Country"},
    {0x03, "Dance"},
    {0x04, "Disco"},
    {0x05, "Funk"},
    {0x06, "Grunge"},
    {0x07, "Hip-Hop"},
    {0x08, "Jazz"},
    {0x09, "Metal"},
    {0x0A, "New Age"},
    {0x0B, "Oldies"},
    {0x0C, "Other"},
    {0x0D, "Pop"},
    {0x0E, "R&B"},
    {0x0F, "Rap"},
    {0x10, "Reggae"},
    {0x11, "Rock"},
    {0x12, "Techno"},
    {0x13, "Industrial"},
    {0x14, "Alternative"},
    {0x15, "Ska"},
    {0x16, "Death Metal"},
    {0x17, "Pranks"},
    {0x18, "Soundtrack"},
    {0x19, "Euro-Techno"},
    {0x1A, "Ambient"},
    {0x1B, "Trip-Hop"},
    {0x1C, "Vocal"},
    {0x1D, "Jazz+Funk"},
    {0x1E, "Fusion"},
    {0x1F, "Trance"},
    {0x20, "Classical"},
    {0x21, "Instrumental"},
    {0x22, "Acid"},
    {0x23, "House"},
    {0x24, "Game"},
    {0x25, "Sound Clip"},
    {0x26, "Gospel"},
    {0x27, "Noise"},
    {0x28, "Alt. Rock"},
    {0x29, "Bass"},
    {0x2A, "Soul"},
    {0x2B, "Punk"},
    {0x2C, "Space"},
    {0x2D, "Meditative"},
    {0x2E, "Instrumental Pop"},
    {0x2F, "Instrumental Rock"},
    {0x30, "Ethnic"},
    {0x31, "Gothic"},
    {0x32, "Darkwave"},
    {0x33, "Techno-Industrial"},
    {0x34, "Electronic"},
    {0x35, "Pop-Folk"},
    {0x36, "Eurodance"},
    {0x37, "Dream"},
    {0x38, "Southern Rock"},
    {0x39, "Comedy"},
    {0x3A, "Cult"},
    {0x3B, "Gangsta"},
    {0x3C, "Top 40"},
    {0x3D, "Christian Rap"},
    {0x3E, "Pop/Funk"},
    {0x3F, "Jungle"},
    {0x40, "Native US"},
    {0x41, "Cabaret"},
    {0x42, "New Wave"},
    {0x43, "Psychadelic"},
    {0x44, "Rave"},
    {0x45, "Showtunes"},
    {0x46, "Trailer"},
    {0x47, "Lo-Fi"},
    {0x48, "Tribal"},
    {0x49, "Acid Punk"},
    {0x4A, "Acid Jazz"},
    {0x4B, "Polka"},
    {0x4C, "Retro"},
    {0x4D, "Musical"},
    {0x4E, "Rock & Roll"},
    {0x4F, "Hard Rock"},
    {0x50, "Folk"},
    {0x51, "Folk-Rock"},
    {0x52, "National Folk"},
    {0x53, "Swing"},
    {0x54, "Fast Fusion"},
    {0x55, "Bebob"},
    {0x56, "Latin"},
    {0x57, "Revival"},
    {0x58, "Celtic"},
    {0x59, "Bluegrass"},
    {0x5A, "Avantgarde"},
    {0x5B, "Gothic Rock"},
    {0x5C, "Progressive Rock"},
    {0x5D, "Psychedelic Rock"},
    {0x5E, "Symphonic Rock"},
    {0x5F, "Slow Rock"},
    {0x60, "Big Band"},
    {0x61, "Chorus"},
    {0x62, "Easy Listening"},
    {0x63, "Acoustic"},
    {0x64, "Humour"},
    {0x65, "Speech"},
    {0x66, "Chanson"},
    {0x67, "Opera"},
    {0x68, "Chamber Music"},
    {0x69, "Sonata"},
    {0x6A, "Symphony"},
    {0x6B, "Booty Bass"},
    {0x6C, "Primus"},
    {0x6D, "Porn Groove"},
    {0x6E, "Satire"},
    {0x6F, "Slow Jam"},
    {0x70, "Club"},
    {0x71, "Tango"},
    {0x72, "Samba"},
    {0x73, "Folklore"},
    {0x74, "Ballad"},
    {0x75, "Power Ballad"},
    {0x76, "Rhytmic Soul"},
    {0x77, "Freestyle"},
    {0x78, "Duet"},
    {0x79, "Punk Rock"},
    {0x7A, "Drum Solo"},
    {0x7B, "Acapella"},
    {0x7C, "Euro-House"},
    {0x7D, "Dance Hall"},
    {0x7E, "Goa"},
    {0x7F, "Drum & Bass"},
    {0x80, "Club-House"},
    {0x81, "Hardcore"},
    {0x82, "Terror"},
    {0x83, "Indie"},
    {0x84, "BritPop"},
    {0x85, "Negerpunk"},
    {0x86, "Polsk Punk"},
    {0x87, "Beat"},
    {0x88, "Christian Gangsta Rap"},
    {0x89, "Heavy Metal"},
    {0x8A, "Black Metal"},
    {0x8B, "Crossover"},
    {0x8C, "Contemporary Christian"},
    {0x8D, "Christian Rock"},
    {0x8E, "Merengue"},
    {0x8F, "Salsa"},
    {0x90, "Trash Metal"},
    {0xFF, NULL}
};

/************************************
 *        ID3 Util functions        *
 ************************************/

/* from Christopher Collins' id3tool */
int id3_readtag(FILE * fin, id3tag_t * id3tag)
{
    int n;
    if (NULL == fin || NULL == id3tag) {
	return (-1);
    }
    fseek(fin, 0, SEEK_END);
    fseek(fin, ftell(fin) - 128, SEEK_SET);
    n = fread(id3tag, 128, 1, fin);
    if (1 != n || strncmp(id3tag->magic, "TAG", 3)) {
	return (1);
    }
    return (0);
}

/* from Christopher Collins' id3tool */
int id3_appendtag(FILE * fout, id3tag_t * id3tag)
{
    if (NULL == fout || NULL == id3tag) {
	return (-1);
    }
    fseek(fout, 0, SEEK_END);

    fwrite(id3tag, 128, 1, fout);

    return (0);
}

/* from Christopher Collins' id3tool */
int id3_replacetag(FILE * fout, id3tag_t * id3tag)
{
    if (NULL == fout || NULL == id3tag) {
	return (-1);
    }
    fseek(fout, 0, SEEK_END);
    fseek(fout, ftell(fout) - 128, SEEK_SET);

    fwrite(id3tag, 128, 1, fout);

    return (0);
}

/* from Christopher Collins' id3tool */
void id3_cleartag(id3tag_t * id3tag)
{
    if (NULL == id3tag) {
	return;
    }
    memset(id3tag, 0, sizeof(*id3tag));
    strncpy(id3tag->magic, "TAG", 3);
    id3tag->style = 0xFF;
}

/* from Christopher Collins' id3tool */
char *id3_findstyle(int styleid)
{
    int ctr;
    for (ctr = 0; id3_styles[ctr].name != NULL; ctr++) {
	if (id3_styles[ctr].styleid == styleid) {
	    return (id3_styles[ctr].name);
	}
    }
    return "Unknown Style";
}


/************************************
 *        id3 manip functions       *
 ************************************/

void build_id3_tag_line(char *out, id3tag_t * id3)
{
    char tmp[31], str[INFO_SIZE];

    *out = tmp[30] = ZCHAR;

    strncpy(tmp, id3->artist, 30);
    if (!is_all_space(tmp)) {
	remove_end_space(tmp);
	sprintf(str, "%s ", tmp);
	strcat(out, str);
    } else
	strcat(out, "<unknown> ");

    strncpy(tmp, id3->songname, 30);
    if (!is_all_space(tmp)) {
	remove_end_space(tmp);
	sprintf(str, "%c %s ", ID3_SEPERATOR, tmp);
	strcat(out, str);
    }

    strncpy(tmp, id3->album, 30);
    if (!is_all_space(tmp)) {
	remove_end_space(tmp);
	sprintf(str, "%c %s ", ID3_SEPERATOR, tmp);
	strcat(out, str);
    }

    tmp[4] = ZCHAR;
    strncpy(tmp, id3->year, 4);
    if (!is_all_space(tmp)) {
	remove_end_space(tmp);
	sprintf(str, "%c %s ", ID3_SEPERATOR, tmp);
	strcat(out, str);
    }

    strncpy(tmp, id3->note, 30);
    if (!is_all_space(tmp)) {
	remove_end_space(tmp);
	sprintf(str, "%c %s ", ID3_SEPERATOR, tmp);
	strcat(out, str);
    }

    if (id3->style <= 0x90) {
	sprintf(str, "%c %s", ID3_SEPERATOR, id3_findstyle(id3->style));
	strcat(out, str);
    }
}

int read_id3tag(char *file, char *buf)
{
    FILE *fd;
    int ret;
    id3tag_t id3;

    fd = myfopen(file, "r");
    ret = FALSE;

    if (fd) {
	if (id3_readtag(fd, &id3) == 0) {
	    build_id3_tag_line(buf, &id3);
	    ret = TRUE;
	}
	fclose(fd);
    }

    return ret;
}

#ifdef OGG_MODULE

static char ogg_artist[256];
static char tmpstr2_id[COMM_SIZE];

int read_oggtag(char *file, char *buf)
{
    vorbis_comment *vc;
    FILE *fd;
    char *tmp;
    int i, ret;

    fd = myfopen(file, "r");
    ret = FALSE;
    *buf = *ogg_artist = *tmpstr2_id = ZCHAR;

    if (fd) {
	vcedit_state *st = vcedit_new_state();

	if (vcedit_open(st, (void *) fd) >= 0) {
	    vc = vcedit_comments(st);

	    for (i = 0; i < vc->comments; i++) {
		if (strncasecmp(vc->user_comments[i], "artist=", 7) == 0)
		    strcpy(ogg_artist, &vc->user_comments[i][7]);
		else {
		    tmp = strchr(vc->user_comments[i], '=');

		    if (tmp) {
			strcat(tmpstr2_id, " - ");
			strcat(tmpstr2_id, (tmp + 1));
		    }
		}
	    }

	    /* Make sure artist name is first in tag string! */
	    if (*ogg_artist == ZCHAR)
		strcat(buf, "<unknown>");
	    else
		strcat(buf, ogg_artist);

	    if (*tmpstr2_id != ZCHAR)
		strcat(buf, tmpstr2_id);

	    ret = TRUE;
	}

	vcedit_clear(st);
	fclose(fd);
    }

    return ret;
}
#endif

int read_othertag(char *file, char *buf, int len)
{
    FILE *fd;
    int i, slen;

    i = slen = 0;
    *buf = ZCHAR;

    while (file_info[i].type) {
	slen = get_strlen(file_info[i].type);

	if (len >= slen
	    && strcasecmp(&file[len - slen], file_info[i].type) == 0) {
	    if (!(fd = myfopen(file, "r")))
		return FALSE;

	    if (file_info[i].offset > 0)
		if (fseek(fd, file_info[i].offset, SEEK_SET) == -1) {
		    fclose(fd);
		    return FALSE;
		}

	    if (fread(buf, file_info[i].len, 1, fd) == -1) {
		fclose(fd);
		*buf = ZCHAR;
		return FALSE;
	    } else {
		buf[file_info[i].len] = ZCHAR;
		remove_end_space(buf);
		make_printable(buf);

		if (is_all_space(buf)) {
		    fclose(fd);
		    *buf = ZCHAR;
		    return FALSE;
		} else {
		    fclose(fd);
		    return TRUE;
		}
	    }
	}
	i++;
    }				/* end while() */

    return FALSE;
}

int get_stream_info(char *file, char *buf)
{
    int len;

    len = get_strlen(file);

    if (len) {
	if (len >= 4 && strcasecmp(&file[len - 4], ".MP3") == 0)
	    return read_id3tag(file, buf);
#ifdef OGG_MODULE
	else if (len >= 4 && strcasecmp(&file[len - 4], ".OGG") == 0)
	    return read_oggtag(file, buf);
#endif
	else
	    return read_othertag(file, buf, len);
    }

    return FALSE;
}

char *determine_fname(char *tok)
{
    if (is_all_digits(tok)) {
	if (edit_list)
	    return get_stream(edit_list->dcb, atoi(tok), STR_FILE);
	else {
	    show_user(DEBUG_MSG " Edit list undefined");
	    return NULL;
	}
    }

    return tok;
}

void /* user */ print_id3_styles()
{
    char *name;
    int i, len;

    name = g_dbuf(args, 1);
    len = get_strlen(name);

    for (i = 0; i <= 0x90; i++) {
	if (len && grep_against_args(1, id3_styles[i].name) == FALSE)
	    continue;

	add_outbuffer("%s", id3_styles[i].name);
    }

    flush_outbuffer();
}

unsigned char find_style_match(char *str)
{
    int i;

    if (str)
	for (i = 0; i <= 0x90; i++)
	    if (strcasecmp(id3_styles[i].name, str) == 0)
		return id3_styles[i].styleid;
    
    return 0xFF;
}

void print_addtag_help()
{
    show_user(STATUS_MSG " ID3 options are:");
    show_user("\t-n <name>       : Add/Modify song name");
    show_user("\t-a <artist>     : Add/Modify artist name");
    show_user("\t-l <album>      : Add/Modify album name");
    show_user("\t-y <year>       : Add/Modify year");
    show_user("\t-o <note>       : Add/Modify note");
    show_user("\t-s <style>      : Add/Modify style");
    show_user("\tYear = MAX 4 chars, all other = MAX 30 chars");
    show_user("\tALWAYS double-quote multiple-word entries");
}

void add_id3tag(char *file)
{
    char *tok;
    FILE *fd;
    int i, err, new;
    unsigned char id;
    id3tag_t id3;

    i = 2;
    err = FALSE;
    new = TRUE;
    tok = g_dbuf(args, i);
    id3_cleartag(&id3);

    if ((fd = myfopen(file, "r+"))) {
	if (id3_readtag(fd, &id3) == 0)
	    new = FALSE;
	else
	    id3_cleartag(&id3);
	fclose(fd);
    } else {
	show_user(ERROR_MSG " Failed file open of: \"%s\" : %s",
		  file, strerror(errno));
	return;
    }

    while (tok) {
	if (strcmp(tok, "-n") == 0) {	/* set songname */
	    tok = g_dbuf(args, ++i);
	    if (tok)
		strncpy(id3.songname, tok, 30);
	    else
		err = TRUE;
	} else if (strcmp(tok, "-a") == 0) {	/* set artist */
	    tok = g_dbuf(args, ++i);
	    if (tok)
		strncpy(id3.artist, tok, 30);
	    else
		err = TRUE;
	} else if (strcmp(tok, "-l") == 0) {	/* set album */
	    tok = g_dbuf(args, ++i);
	    if (tok)
		strncpy(id3.album, tok, 30);
	    else
		err = TRUE;
	} else if (strcmp(tok, "-y") == 0) {	/* set year */
	    tok = g_dbuf(args, ++i);
	    if (tok)
		strncpy(id3.year, tok, 4);
	    else
		err = TRUE;
	} else if (strcmp(tok, "-o") == 0) {	/* set note */
	    tok = g_dbuf(args, ++i);
	    if (tok)
		strncpy(id3.note, tok, 30);
	    else
		err = TRUE;
	} else if (strcmp(tok, "-s") == 0) {	/* set style */
	    tok = g_dbuf(args, ++i);
	    if (tok) {
		id = find_style_match(tok);

		if (id != 0xFF)
		    id3.style = id;
		else {
		    show_user(ERROR_MSG " Bad style name: \"%s\"", tok);
		    show_user(ERROR_MSG
			      " Always double-quote multiple-word styles");
		    show_user(ERROR_MSG
			      " Type \"%s\" for a list of styles",
			      com[PRINT_STYLES].name);
		    return;
		}
	    } else
		err = TRUE;
	} else {
	    show_user(ERROR_MSG " Unkown option: \"%s\"", tok);
	    print_addtag_help();
	    return;
	}

	if (err) {
	    show_user(ERROR_MSG " Missing token?");
	    print_addtag_help();
	    return;
	} else
	    tok = g_dbuf(args, ++i);
    }

    /* if we got this far, then go ahead !!! */
    fd = myfopen(file, "r+");

    if (new)
	id3_appendtag(fd, &id3);
    else
	id3_replacetag(fd, &id3);

    fclose(fd);
}

void print_othertag_help()
{
    int i;

    i = 0;
    *tmpstr_id = ZCHAR;

    while (file_info[i].type) {
	strcat(tmpstr_id, file_info[i++].type);
	strcat(tmpstr_id, " ");
    }

    show_user(STATUS_MSG " Non ID3 options:");
    show_user("\tFor <options...>, give new <tag>");
    show_user("\tSupported filetypes: %s", tmpstr_id);
}

void add_othertag(char *file, int ndx)
{
    char *info;
    FILE *fd;
    int iln, len, offs;

    info = g_dbuf(args, 2);
    len = get_strlen(info);
    offs = file_info[ndx].offset;
    iln = file_info[ndx].len;

    if (!len) {
	print_othertag_help();
	return;
    }

    if (len > iln) {
	show_user(DEBUG_MSG
		  " Max tag length for type \"%s\" is %d characters",
		  file_info[ndx].type, iln);
	return;
    }

    if ((fd = myfopen(file, "r+"))) {
	if (offs > 0 && -1 == fseek(fd, offs, SEEK_SET)) {
	    show_user(ERROR_MSG " Unable to read contents of \"%s\" : %s",
		      file, strerror(errno));
	    fclose(fd);
	    return;
	}

	/* make sure the string is padded */
	safe_strcpy(tmpstr_id, info, COMM_SIZE);

	if (-1 == fwrite(tmpstr_id, 1, iln, fd))
	    show_user(ERROR_MSG
		      " Unable to modify contents of \"%s\" : %s", file,
		      strerror(errno));

	fclose(fd);
    } else
	show_user(DEBUG_MSG " Failed file open of: \"%s\" : %s",
		  file, strerror(errno));
}


void /* user */ add_tag()
{
    char *file;
    int i, flen, slen;

    file = g_dbuf(args, 1);

    if (!file) {
	print_cmnd_help(ADD_TAG);
	print_addtag_help();
	print_othertag_help();
	return;
    }

    if ((file = determine_fname(file))) {
	flen = get_strlen(file);

	if (flen >= 4 && strcasecmp(&file[flen - 4], ".MP3") == 0) {
	    add_id3tag(file);
	    return;
	} else {
	    i = 0;
	    while (file_info[i].type) {
		slen = get_strlen(file_info[i].type);

		if (flen >= slen &&
		    strcasecmp(&file[flen - slen], file_info[i].type) == 0) {
		    add_othertag(file, i);
		    return;
		}
		i++;
	    }
	}
	show_user(DEBUG_MSG " No tag location information for: \"%s\"",
		  file);
    } else {
	show_user(DEBUG_MSG " List index out of range");
	return;
    }
}

void /* user */ show_id3tag()
{
    char *file;

    file = g_dbuf(args, 1);

    if (!file) {
	print_cmnd_help(SHOW_TAG);
	return;
    }

    if ((file = determine_fname(file))) {
	if (get_stream_info(file, tmpstr_id))
	    show_user(STATUS_MSG " Tag found : \"%s\"", tmpstr_id);
	else
	    show_user(DEBUG_MSG " Could not find tag for: \"%s\"", file);
    } else
	show_user(ERROR_MSG " List index out of range");
}
