/*
 * Indexer for mp3, m4a, etc. files.
 * Not pretty, but it will do.
 */

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

#define dprint if(debug)print

typedef struct Tags Tags;
struct Tags
{
	char *ptr;
	char *title;
	int n;
	char *file;
	char *artist[50];
	int artistn[50];
	int nartist;
	int track[2];
	int disk[2];
	char *album;
	int albumn;
	char *composer[50];
	int composern[50];
	int ncomposer;
	char *genre[50];
	int genren[50];
	int ngenre;
	int year;
	char *copyright;
	char *albumartist;
};

typedef struct File File;
struct File
{
	char *buf;
	uint nbuf;
	int len;
	char *name;
	int used;
};
File cache[64];

typedef struct Index Index;
typedef struct IEntry IEntry;

struct Index
{
	IEntry *hash[4096];
	IEntry **allie;
	int maxn;
	int nallie;
	char *name;
	char *file;
};

struct IEntry
{
	IEntry *next;
	char *string;
	char *title;	/* for songs */
	int n;
};

Index *songs, *composers, *artists, *genres, *albums;
int now;
int debug;
int verbose;
char *jukedir;
void* xmalloc(uint n);
char* xstrdup(char*);
char* estrdup(char*);

void		addentry(Index *ix, int in, char *name, char *what, int n, char *label);
void*		emalloc(uint n);
void*		erealloc(void *v, uint n);
char*		evsmprint(char *s, va_list v);
File*		file(char *path);
void		fileflush(void);
int		fileopen(char *path, File *f);
void		filewrite(File *f, char *s);
void		flush1(File *f);
int		gettags(char *file, Tags *t);
uint		hash(char *s);
int		lookup(Index *ix, char *s);
int		lookup0(Index *ix, char *s, char *title, int alloc);
Index*		readindex(char *file, char *name);
char*		sysrun(char *fmt, ...);
void		threadmain(int argc, char **argv);
void		writesonginfo(Tags *t);
int		fromscratch;

void
xmkdir(char *s)
{
	int fd;
	
	if(access(s, AEXIST) < 0){
		if((fd = create(s, OREAD, DMDIR|0775)) < 0){
			if(s == jukedir)
				sysfatal("mkdir %s: %r", jukedir);
			else
				sysfatal("mkdir %s/%s: %r", jukedir, s);
		}
		close(fd);
	}
}

File*
file(char *path)
{
	int i, lo;

	lo = 0;
	for(i=0; i<nelem(cache); i++){
		if(cache[i].name==0)
			lo = i;
		else if(strcmp(cache[i].name, path) == 0)
			return &cache[i];
		else if(cache[i].used < cache[lo].used)
			lo = i;
	}
	flush1(&cache[lo]);
	fileopen(path, &cache[lo]);
	return &cache[lo];
}

int
fileopen(char *path, File *f)
{
	int fd, n, m, tot;
	char *buf;
	
	dprint("open/%d %s\n", f-cache, path);
	if((fd = open(path, OREAD)) < 0 && access(path, AEXIST) >= 0)
		sysfatal("open %s/%s: %r", jukedir, path);
	buf = emalloc(1024);
	m = 1024;
	tot = 0;
	n = 0;
	if(fd >= 0){
		while((n = read(fd, buf+tot, m-tot)) > 0){
			tot += n;
			if(tot == m){
				m *= 2;
				buf = erealloc(buf, m);
			}
		}
		close(fd);
	}
	buf[tot] = 0;
	if(n != 0)
		sysfatal("read %s/%s: %r", jukedir, path);
	f->name = estrdup(path);
	f->buf = buf;
	f->nbuf = m;
	f->used = ++now;
	f->len = tot;
	return 0;
}

void
flush1(File *f)
{
	int fd, n;
	
	if(f->used == 0)
		return;
	dprint("flush/%d %s\n", f-cache, f->name);
	if((fd = create(f->name, OWRITE, 0666)) < 0)
		sysfatal("cannot write %s/%s: %r", jukedir, f->name);
	if((n = write(fd, f->buf, f->len)) != f->len)
		sysfatal("write %s/%s: %r", jukedir, f->name);
	close(fd);
	free(f->buf);
	free(f->name);
	f->buf = nil;
	f->name = nil;
	f->nbuf = 0;
	f->len = 0;
	f->used = 0;
}

void
filewrite(File *f, char *s)
{
	int l;
	
	l = strlen(s);
	if(f->len+l+1 > f->nbuf){
		while(f->len+l+1 > f->nbuf)
			f->nbuf *= 2;
		f->buf = erealloc(f->buf, f->nbuf);
	}
	strcpy(f->buf+f->len, s);
	f->len += l;
}

void
fileflush(void)
{
	int i;
	
	for(i=0; i<nelem(cache); i++)
		flush1(&cache[i]);
}

uint
hash(char *s)
{
	uint h;
	uchar *p;

	h = 0;
	for(p=(uchar*)s; *p; p++)
		h = h*37 + *p;
	return h%nelem(((Index*)0)->hash);
}

Index*
readindex(char *file, char *name)
{
	char *p, *q;
	int n, h, nr;
	Biobuf *b;
	Index *ix;
	IEntry *ie;

	if(fromscratch)
		b = nil;
	else if((b = Bopen(file, OREAD)) == nil)
		fprint(2, "creating index %s/%s\n", jukedir, file);

	dprint("read %s\n", file);
	ix = emalloc(sizeof *ix);
	ix->maxn = 0;
	ix->name = name;
	ix->file = xstrdup(file);
	ix->nallie = 0;
	ix->allie = nil;
	if(b){
		nr = 0;
		while((p = Brdline(b, '\n')) != nil){
			nr++;
			p[Blinelen(b)-1] = 0;
			q = strchr(p, ' ');
			if(q == nil)
				continue;
			*q++ = 0;
			p = strchr(p, '/');
			if(p == nil)
				continue;
			n = atoi(p+1);
			if(n == 0){
				fprint(2, "warning: %s/%s:%d: bad line\n", jukedir, file, nr);
				continue;
			}
			ie = xmalloc(sizeof *ie);
			ie->n = n;
			if(n >= ix->nallie){
				ix->allie = erealloc(ix->allie, (n+100)*sizeof(ix->allie[0]));
				for(; ix->nallie<n+100; ix->nallie++)
					ix->allie[ix->nallie] = nil;
			}
			if(n > ix->maxn)
				ix->maxn = n;
			ie->string = xstrdup(q);
			ix->allie[n] = ie;
			h = hash(q);
			if(debug>1) print("add %s hash %d\n", q, h);
			ie->next = ix->hash[h];
			ix->hash[h] = ie;
		}
		Bterm(b);
	}
	return ix;
}

Index*
readindex0(Index *ix, char *file)
{
	char *p, *q;
	int n, nr;
	Biobuf *b;
	IEntry *ie;
	
	if(fromscratch)
		b = nil;
	else if((b = Bopen(file, OREAD)) == nil)
		fprint(2, "creating index %s/%s\n", jukedir, file);

	if(b){
		nr = 0;
		while((p = Brdline(b, '\n')) != nil){
			nr++;
			p[Blinelen(b)-1] = 0;
			q = strchr(p, ' ');
			if(q == nil)
				continue;
			*q++ = 0;
			p = strchr(p, '/');
			if(p == nil)
				continue;
			n = atoi(p+1);
			if(n == 0){
				fprint(2, "warning: %s/%s:%d: bad line\n", jukedir, file, nr);
				continue;
			}
			if(n < 0 || n >= ix->nallie || (ie=ix->allie[n]) == nil){
				fprint(2, "warning: unexpected song %d in song index\n", n);
				continue;
			}
			ie->title = xstrdup(q);
		}
		Bterm(b);
	}
	return ix;
}

void
writeindex(Index *ix)
{
	Biobuf *b;
	int i;
	IEntry *ie;
	
	if((b = Bopen(ix->file, OWRITE)) == nil)
		sysfatal("create %s/%s: %r", jukedir, ix->file);
	for(i=0; i<ix->nallie; i++)
		if((ie = ix->allie[i]) != nil)
			Bprint(b, "%s/%d %s\n", ix->name, ie->n, ie->string);
	Bterm(b);
}

void
writeindex0(Index *ix)
{
	Biobuf *b;
	int i;
	IEntry *ie;
	
	if((b = Bopen("song/index", OWRITE)) == nil)
		sysfatal("create %s/%s: %r", jukedir, ix->file);
	for(i=0; i<ix->nallie; i++)
		if((ie = ix->allie[i]) != nil)
			Bprint(b, "%s/%d %s\n", ix->name, ie->n, ie->title);
	Bterm(b);
}

void
loaddb(void)
{
	dprint("loaddb\n");
	songs = readindex("song/filelist", "song");
	readindex0(songs, "song/index");
	composers = readindex("composer/index", "composer");
	artists = readindex("artist/index", "artist");
	genres = readindex("genre/index", "genre");
	albums = readindex("album/index", "album");
}

void
closedb(void)
{
	dprint("closedb\n");
	fileflush();
	writeindex(songs);
	writeindex0(songs);
	writeindex(composers);
	writeindex(artists);
	writeindex(genres);
	writeindex(albums);
}

int
lookup0(Index *ix, char *s, char *title, int alloc)
{
	int h;
	IEntry *ie;
	
	h = hash(s);
	dprint("lookup0 %s hash %d\n", s, h);
	for(ie=ix->hash[h]; ie; ie=ie->next)
		if(strcmp(ie->string, s) == 0)
			return ie->n;
	dprint("lookup0 not found\n");
	if(!alloc)
		return -1;
	ie = xmalloc(sizeof *ie);
	ie->n = ++ix->maxn;
	if(ie->n >= ix->nallie){
		ix->allie = erealloc(ix->allie, (ie->n+500)*sizeof(ix->allie[0]));
		for(; ix->nallie<ie->n+500; ix->nallie++)
			ix->allie[ix->nallie] = nil;
	}
	dprint("%s/%d %s\n", ix->name, ie->n, s);
	ix->allie[ie->n] = ie;
	ie->string = xstrdup(s);
	if(title)
		ie->title = xstrdup(title);
	ie->next = ix->hash[h];
	ix->hash[h] = ie;
	return ie->n;
}

int
lookup(Index *ix, char *s)
{
	return lookup0(ix, s, 0, 1);
}

int
gettags(char *file, Tags *t)
{
	char *p;
	char *info, *lines[100], *f[10];
	int nl, i, nf;
	
	info = sysrun("jukeinfo %q", file);
	if(info == nil){
		werrstr("no information");
		return -1;
	}
	dprint("info %s\n", info);
	
	memset(t, 0, sizeof *t);
	t->file = file;
	nl = getfields(info, lines, nelem(lines), 1, "\n");
	for(i=0; i<nl; i++){
		p = lines[i];
		if(p[0] == 0 || p[0] == '#')
			continue;
		nf = tokenize(p, f, nelem(f));
		if(nf < 2)
			continue;
		if(strcmp(f[0], "title") == 0)
			t->title = f[1];
		else if(strcmp(f[0], "artist") == 0){
			if(t->nartist < nelem(t->artist))
				t->artist[t->nartist++] = f[1];
		}else if(strcmp(f[0], "track") == 0){
			t->track[0] = atoi(f[1]);
			if(nf > 2)
				t->track[1] = atoi(f[2]);
		}else if(strcmp(f[0], "disk") == 0){
			t->disk[0] = atoi(f[1]);
			if(nf > 2)
				t->disk[1] = atoi(f[2]);
		}else if(strcmp(f[0], "album") == 0){
			t->album = f[1];
		}else if(strcmp(f[0], "genre") == 0){
			if(t->ngenre < nelem(t->genre))
				t->genre[t->ngenre++] = f[1];
		}else if(strcmp(f[0], "year") == 0)
			t->year = atoi(f[1]);
		else if(strcmp(f[0], "copyright") == 0)
			t->copyright = f[1];
		else if(strcmp(f[0], "albumartist") == 0)
			t->albumartist = f[1];
	}
	return 0;
}

void
addentry(Index *ix, int in, char *name, char *what, int n, char *label)
{
	char buf[100];
	char *p, *q;
	File *f;
	
	snprint(buf, sizeof buf, "%s/%d", ix->name, in);
	f = file(buf);
	if(f->len == 0){
		filewrite(f, name);
		filewrite(f, "\n\n");
	}
	p = smprint("%s/%d %s\n", what, n, label);
	q = strstr(f->buf, p);
	if(q == nil || (q > f->buf && *(q-1) != '\n'))
		filewrite(f, p);
	free(p);
}

void
writesonginfo(Tags *t)
{
	int i;
	Biobuf *b;
	char buf[100];
	
	snprint(buf, sizeof buf, "song/%d", t->n/1000);
	xmkdir(buf);
	snprint(buf, sizeof buf, "song/%d/%d", t->n/1000, t->n%1000);
	if((b = Bopen(buf, OWRITE)) == nil){
		fprint(2, "create %s/%s: %r\n", jukedir, buf);
		return;
	}
	Bprint(b, "%s\n\n", t->title);
	if(t->album)
		Bprint(b, "album/%d %s\n", t->albumn, t->album);
	for(i=0; t->artist[i]; i++)
		Bprint(b, "artist/%d %s\n", t->artistn[i], t->artist[i]);
	for(i=0; t->composer[i]; i++)
		Bprint(b, "composer/%d %s\n", t->composern[i], t->composer[i]);
	for(i=0; t->genre[i]; i++)
		Bprint(b, "genre/%d %s\n", t->genren[i], t->genre[i]);
	Bterm(b);
}

char*
sysrun(char *fmt, ...)
{
	static char buf[16*1024];
	char *cmd;
	va_list arg;
	int n, fd[3], p[2], tot;

#undef pipe
	if(pipe(p) < 0)
		sysfatal("pipe: %r");
	fd[0] = open("/dev/null", OREAD);
	fd[1] = p[1];
	fd[2] = dup(2, -1);

	va_start(arg, fmt);
	cmd = evsmprint(fmt, arg);
	va_end(arg);
	threadspawnl(fd, "rc", "rc", "-Ic", cmd, 0);

	tot = 0;
	while((n = read(p[0], buf+tot, sizeof buf-tot)) > 0)
		tot += n;
	close(p[0]);
	if(n < 0)
		return nil;
	free(cmd);
	if(tot == sizeof buf)
		tot--;
	buf[tot] = 0;
	while(tot > 0 && isspace(buf[tot-1]))
		tot--;
	buf[tot] = 0;
	if(tot == 0){
		werrstr("no output");
		return nil;
	}
	return buf;
}

void*
xmalloc(uint n)
{
	void *v;
	static void *p;
	static int np;
	
	n = (n+7)&~7;
	if(np < n){
		p = mallocz(4*1024*1024, 1);
		np = 4*1024*1024;
		if(p == nil)
			sysfatal("out of memory");
	}
	v = p;
	p += n;
	np -= n;
	return v;
}

void*
emalloc(uint n)
{
	void *v;

	v = mallocz(n, 1);
	if(v == nil)
		sysfatal("out of memory");
	return v;
}

void*
erealloc(void *v, uint n)
{
	v = realloc(v, n);
	if(v == nil)
		sysfatal("out of memory");
	return v;
}

char*
xstrdup(char *s)
{
	char *t;
	t = xmalloc(strlen(s)+1);
	strcpy(t, s);
	return t;
}
char*
estrdup(char *s)
{
	char *t;
	t = emalloc(strlen(s)+1);
	strcpy(t, s);
	return t;
}

char*
evsmprint(char *s, va_list v)
{
	s = vsmprint(s, v);
	if(s == nil)
		sysfatal("out of memory");
	return s;
}

void
usage(void)
{
	fprint(2, "usage: jukeindex [-vz] [-d jukedir] file...\n");
	threadexitsall("usage");
}

void
threadmain(int argc, char **argv)
{
	int i, j, n;
	char *s, *home;
	static char dir[2048], buf[2048];
	Tags t;
	
	ARGBEGIN{
	case 'D':
		debug++;
		break;
	case 'd':
		jukedir = EARGF(usage());
		break;
	case 'v':
		verbose++;
		break;
	case 'z':
		fromscratch = 1;
		break;
	default:
		usage();
	}ARGEND
	
	if(jukedir== nil)
		jukedir = getenv("jukedb");
	if(jukedir == nil){
		if((home = getenv("HOME")) != nil){
			snprint(dir, sizeof dir, "%s/lib/jukedb", home);
			jukedir = xstrdup(dir);
		}
	}
	if(jukedir == nil)
		sysfatal("cannot determine juke db directory");

	doquote = needsrcquote;
	quotefmtinstall();
	xmkdir(jukedir);
	if(getwd(dir, sizeof dir) == nil)
		sysfatal("getwd: %r");
	if(chdir(jukedir) < 0)
		sysfatal("chdir %s: %r", dir);
	if(fromscratch)
		sysrun("rm -rf song album artist composer genre song list");
	xmkdir("song");
	xmkdir("album");
	xmkdir("artist");
	xmkdir("composer");
	xmkdir("genre");
	xmkdir("song");
	xmkdir("list");

	loaddb();
	for(j=0; j<argc; j++){
		if(argv[j][0] != '/')
			s = smprint("%s/%s", dir, argv[j]);
		else
			s = argv[j];
		cleanname(s);
		dprint("%s -> %s\n", argv[j], s);
		memset(&t, 0, sizeof t);
		if(lookup0(songs, s, nil, 0) >= 0){
			dprint("already have %s\n", s);
			continue;
		}
		if(verbose)
			print("%s\n", argv[j]);
		if(gettags(s, &t) < 0){
			fprint(2, "%s: %r\n", argv[j]);
			continue;
		}
		t.n = lookup0(songs, s, t.title, 1);
		dprint("new song %d title %s\n", t.n, t.title);
		if(t.album){
			t.albumn = lookup(albums, t.album);
			if(t.track[0] > 0){
				if(t.disk[1] > 1)
					snprint(buf, sizeof buf, "%d %2d %s", t.disk[0], t.track[0], t.title);
				else
					snprint(buf, sizeof buf, "%2d %s", t.track[0], t.title);
			}else
				snprint(buf, sizeof buf, "%s", t.title);
			addentry(albums, t.albumn, t.album, "song", t.n, buf);
		}
		for(i=0; t.artist[i]; i++){
			n = t.artistn[i] = lookup(artists, t.artist[i]);
			addentry(artists, n, t.artist[i], "song", t.n, t.title);
			addentry(artists, n, t.artist[i], "album", t.albumn, t.album);
		}
		for(i=0; t.composer[i]; i++){
			n = t.composern[i] = lookup(composers, t.composer[i]);
			addentry(composers, n, t.composer[i], "song", t.n, t.title);
			addentry(composers, n, t.composer[i], "album", t.albumn, t.album);
		}
		for(i=0; t.genre[i]; i++){
			n = t.genren[i] = lookup(genres, t.genre[i]);
			addentry(genres, n, t.genre[i], "song", t.n, t.title);
		}
		writesonginfo(&t);
	}
	closedb();	
}

