/*
 * BUG - Doesn't extract genre, probably others.
 * I don't have enough mp3s to care.
 */

#include <u.h>
#include <libc.h>
#include <bio.h>

typedef struct Header Header;
typedef struct ExtHeader ExtHeader;
typedef struct FrameHeader FrameHeader;
typedef struct Frame Frame;
typedef struct Id3 Id3;

struct Header {
	char magic[3];	/* "ID3" for header, "3DI" for footer */
	uchar major;
	uchar minor;
	uchar flags;
	uchar size[4];	/* synchsafe (7-bits per byte), excludes header and footer (if present) */
};
enum {
	HeaderSize = 3+1+1+1+4
};

enum {	/* Header.flags */
	FUnsync = 0x80,
	FExtendedHeader = 0x40,
	FExperimental = 0x20,
	FFooter = 0x10,
};

struct ExtHeader {
	uchar size[4];	/* synchsafe */
	uchar nbytes;
	uchar flags;
	uchar data[1];
};

enum {	/* ExtHeader.flags */
	EFUpdate = 0x40,		/* Tag is an update */
	EFCrc = 0x20,			/* CRC-32 is present */
	EFTagRestrict = 0x10,	/* Tag restrictions */
};

struct FrameHeader {
	char magic[4];	/* identifies type of frame */
	uchar size[4];	/* excludes frame header */
	uchar flags[2];
};
enum {
	FrameHeaderSize = 4+4+2,
};

struct Frame {
	char type[5];
	ushort flags;
	char **s;
	int ns;
	int sz;
};

struct Id3 {
	Frame *f;
	int nf;
};

enum {	/* frame text encoding bytes */
	EncLatin1 = 0x00,
	EncUTF16Little = 0x01,
	EncUTF16Big = 0x02,
	EncUTF8 = 0x03,
};

enum {	/* FrameHeader.flags */
	FFDiscardOnTag = 0x4000,	/* discard if altering tag and this frame is unrecognized */
	FFDiscardOnFile = 0x2000,	/* discard if altering file and this frame is unrecognized */
	FFReadOnly = 0x1000,		/* contents intended to be read only */
	FFGroupInfo = 0x0040,		/* frame contains group information */
	FFCompressed = 0x0008,		/* frame is compressed with deflate */
	FFEncrypted = 0x0004,		/* frame is encrypted */
	FFUnsynched = 0x0002,		/* unsynchronization was applied */
	FFDatalength = 0x0001,		/* frame includes data length indicator */
};

static ulong
gsync(uchar *p)
{
	return (p[0]<<21)|(p[1]<<14)|(p[2]<<7)|p[3];
}

char*
decode(uchar **pstr, uchar *end)
{
	int len;
	char *s;
	char *t;
	uchar *p, *str;
	Rune r;

	str = *pstr;
	p = nil;
	s = nil;
	switch(*str++){
	case EncLatin1:
		s = malloc(UTFmax*strlen((char*)str+1)+1);
		if(s == nil)
			sysfatal("out of memory");
		for(p=str, t=s; *p && p<end; p++){
			r = *p;
			t += runetochar(t, &r);
		}
		*t = '\0';
		if(p<end)
			p++;
		break;
	case EncUTF16Little:
		s = malloc(UTFmax*runestrlen((Rune*)(str+1))+1);
		if(s == nil)
			sysfatal("out of memory");
		for(p=str, t=s; p[0]||p[1]; p+=2){
			r = p[0] | (p[1]<<8);
			t += runetochar(t, &r);
		}
		*t = '\0';
		if(p<end)
			p += 2;
		break;
	case EncUTF16Big:
		s = malloc(UTFmax*runestrlen((Rune*)(str+1))+1);
		if(s == nil)
			sysfatal("out of memory");
		for(p=str, t=s; p[0]||p[1]; p+=2){
			r = (p[0]<<8) | p[1];
			t += runetochar(t, &r);
		}
		*t = '\0';
		if(p < end)
			p += 2;
		break;
	case EncUTF8:
		p = memchr(str, 0, end-str);
		if(p){
			p++;
			len = p-str;
		}else{
			p = end;
			len = end-str;
		}
		s = malloc(len+1);
		if(s == nil)
			sysfatal("out of memory");
		memmove(s, str, len);
		s[len] = 0;
		break;
	}
	*pstr = p;
	return s;
}

Id3*
readtags(Biobuf *b)
{
	char m[] = "ID3";
	uchar *string, *estring;
	int c, i, ntag, nstring;
	uchar *tag;
	Frame *f;
	FrameHeader *fhdr;
	Header hdr;
	Id3 *id3;

	for(i=0; i<3; i++){
		if((c=Bgetc(b)) != m[i]){
			if(c == -1)
				i--;
			for(; i>=0; i--)
				Bungetc(b);
			return nil;
		}
	}
	memmove(hdr.magic, m, 3);
	if(Bread(b, (char*)&hdr+3, HeaderSize-3) != HeaderSize-3)
		sysfatal("short read in id3 header");

	ntag = gsync(hdr.size);
	tag = mallocz(ntag, 1);
	if(tag == nil)
		sysfatal("out of memory");
	if(Bread(b, tag, ntag) != ntag)
		sysfatal("short read reading tags");
	if(hdr.flags)
		sysfatal("unsupported flags: %x\n", hdr.flags);
	id3 = mallocz(sizeof *id3, 1);
	if(id3 == nil)
		sysfatal("out of memory");
	for(i=0; i<ntag; ){
		fhdr = (FrameHeader*)(tag+i);
		if(fhdr->magic[0]!='T' && fhdr->magic[0]!='W'){
		//	fprint(2, "warning: skipping %.4s frame\n", fhdr->magic);
			i += FrameHeaderSize;
			i += gsync(fhdr->size);
			continue;
		}
		if(id3->nf%16==0){
			id3->f = realloc(id3->f, (id3->nf+16)*sizeof(Frame));
			if(id3->f == nil)
				sysfatal("out of memory");
		}
		f = &id3->f[id3->nf];
		id3->nf++;
		memset(f, 0, sizeof *f);
		memmove(f->type, fhdr->magic, 4);
		f->type[4] = '\0';
		f->flags = (fhdr->flags[0]<<8) | fhdr->flags[1];
		i += FrameHeaderSize;
		nstring = gsync(fhdr->size);
		string = (uchar*)tag+i;
		estring = string+nstring;
		i += nstring;
		while(string && string < estring){
			if(f->ns%16 == 0){
				f->s = realloc(f->s, (f->ns+16)*sizeof(f->s[0]));
				if(f->s == nil)
					sysfatal("out of memory");
			}
			f->s[f->ns++] = decode(&string, estring);
		}
	}
	return id3;
}

Id3*
readtexttags(char *file)
{
	int n;
	char *fld[2];
	char *p;
	Biobuf *b;
	Id3 *id;
	Frame *f;

	if((b = Bopen(file, OREAD)) == nil)
		sysfatal("open %s: %r", file);
	id = mallocz(sizeof(Id3), 1);
	if(id == nil)
		sysfatal("out of memory");
	for(; (p = Brdstr(b, '\n', 1)) != nil; free(p)){
		if(p[0]=='#')
			continue;
		n = getfields(p, fld, nelem(fld), 1, " \t");
		if(n == 0)
			continue;
		if(fld[0][0]=='\0'){
			if(n==1 || fld[1][0]=='\0')
				continue;
			if(id->nf == 0)
				sysfatal("continuation of nonexistant frame: %s", p);
			f = &id->f[id->nf-1];
		}else{
			if(n==1)
				fld[1] = "";
			if(strlen(fld[0])!=4 || (fld[0][0]!='T' && fld[0][0]!='W'))
				sysfatal("bad tag '%.4s'", fld[0]);
			if(id->nf%16==0){
				id->f = realloc(id->f, (id->nf+16)*sizeof(id->f[0]));
				if(id->f == nil)
					sysfatal("out of memory");
			}
			f = &id->f[id->nf++];
		}
		if(f->ns%16 == 0){
			f->s = realloc(f->s, (f->ns+16)*sizeof(f->s[0]));
			if(f->s == nil)
				sysfatal("out of memory");
		}
		fld[1] = strdup(fld[1]);
		if(fld[1]==nil)
			sysfatal("out of memory");
		f->s[f->ns++] = fld[1];
	}
	Bterm(b);
	return id;
}

void
usage(void)
{
	fprint(2, "usage: mp3info file.mp3...\n");
	exits("usage");
}

Id3*
gettags(Biobuf *b)
{
	Id3 *id;
	Header h;

	id = readtags(b);
	if(id == nil){
		Bseek(b, -HeaderSize, 2);
		if(Bread(b, &h, HeaderSize) == HeaderSize
		&& memcmp(h.magic, "3DI", 3) == 0){
			Bseek(b, -HeaderSize-gsync(h.size)-HeaderSize, 2);
			id = readtags(b);
		}
	}
	return id;
}

enum
{
	V1Title = 3,
	V1Artist = 33,
	V1Album = 63,
	V1Year = 93,
	V1Comment = 97,
	V1Track = 126,
	V1Genre = 127,
	V1Size = 128
};

void
procv1tag(char *p, int n, char *type, Frame *f)
{
      char *q;

      strcpy(f->type, type);
      f->flags = 0;
      for(q = p + n - 1; q >= p && (*q == ' ' || *q == '\0'); --q);
      f->s = mallocz(sizeof(char *), 1);
      f->s[0] = mallocz(q - p + 2, 1);
      strncpy(f->s[0], p, q - p + 1);
      f->ns = 1;
}

Id3*
readv1tags(Biobuf *b)
{
      char tagbuf[V1Size];
      int ntag;
      Frame *f;
      Id3 *id3;

      Bseek(b, -V1Size, 2);
      if(Bread(b, tagbuf, V1Size) != V1Size)
              sysfatal("Short read for v1 tag");
      if(strncmp(tagbuf, "TAG", 3)){
              Bseek(b, 0, 0);
              return nil;
      }
      ntag = 0;
      if(tagbuf[V1Title] && tagbuf[V1Title] != ' ')
      	++ntag;
      if(tagbuf[V1Artist] && tagbuf[V1Artist] != ' ')
      	++ntag;
      if(tagbuf[V1Album] && tagbuf[V1Album] != ' ')
      	++ntag;
      if(tagbuf[V1Year] && tagbuf[V1Year] != ' ')
      	++ntag;
      id3 = mallocz(sizeof *id3, 1);
      if(id3 == nil)
              sysfatal("out of memory");
      id3->nf = ntag;
      id3->f = mallocz(ntag * sizeof(id3->f[0]), 1);
      if(id3->f == nil)
              sysfatal("out of memory");
      f = id3->f;
      if(tagbuf[V1Title] && tagbuf[V1Title] != ' '){
              procv1tag(tagbuf + V1Title, 30, "TIT2", f);
              ++f;
      }
      if(tagbuf[V1Artist] && tagbuf[V1Artist] != ' '){
              procv1tag(tagbuf + V1Artist, 30, "TPE1", f);
              ++f;
      }
      if(tagbuf[V1Album] && tagbuf[V1Album] != ' '){
              procv1tag(tagbuf + V1Album, 30, "TALB", f);
              ++f;
      }
      if(tagbuf[V1Year] && tagbuf[V1Year] != ' '){
              procv1tag(tagbuf + V1Year, 4, "TYER", f);
              ++f;
      }
      return id3;
}

void
freetags(Id3 *id)
{
	int i, j;
	Frame *f;

	if(id == nil)
		return;
	for(i=0; i<id->nf; i++){
		f = &id->f[i];
		for(j=0; j<f->ns; j++)
			free(f->s[j]);
		free(f->s);
	}
	free(id->f);
	free(id);
}

static struct {
	char *tag;
	char *name;
} tags[] = {
	"TALB",	"album",
	"TCOM",	"composer",
	"TEXT",	"lyricist",
	"TIT2",	"title",
	"TYER",	"year",
	"TPE1",	"artist",
};

void
printtags(Id3 *id)
{
	int i, j;
	Frame *f;

	for(i=0; i<id->nf; i++){
		f = &id->f[i];
		if(f->ns == 0 || f->type == nil)
			continue;
		for(j=0; j<nelem(tags); j++)
			if(strcmp(tags[j].tag, f->type) == 0)
				print("%s %q\n", tags[j].name, f->s[0]);
	}
}

void
main(int argc, char **argv)
{
	int i;
	Id3 *id;
	Biobuf *b;

	ARGBEGIN{
	default:
		usage();
	}ARGEND

	doquote = needsrcquote;
	quotefmtinstall();
	for(i=0; i<argc; i++){
		print("file %q\n", argv[i]);
		if((b = Bopen(argv[i], OREAD)) == nil){
			fprint(2, "open %s: %r\n", argv[i]);
			continue;
		}
		id = gettags(b);
		if(id == nil)
			id = readv1tags(b);
		if(id == nil){
			fprint(2, "no id3 tags found in %s\n", argv[i]);
			Bterm(b);
			continue;
		}
		printtags(id);
		freetags(id);
		Bterm(b);
	}
	exits(nil);
}
