/* build_idx.c - builds PhatBox playlist indexes
 * by Vince Busam <vince@phatnoise.com>
 * $Revision: 1.6 $
 * This is not copyrighted.  You are free to use this as you please.
 */
 
/*
-- Index file spec --

There are several new files added, all of which contain a series of signed,
little-endian 32-bit integers.  These files are:
tracks.idx
tracks.dat
Profiles/<active profile>/pX.idx
Profiles/<active profile>/(Artists|Albums|Genres)/X.idx
                                                 /num_tracks.db
                                                 /alpha.db

Each track is given a "track id", starting with 1.

tracks.idx - index into tracks.db
  first int - number of tracks
  subsequent ints - position (in bytes) into tracks.db of each "track id"
                    (the beginning of the line).
  Thus, for track id 7, the 8th int (or 7th int if you're a cool 0-based C
  programmer!) in tracks.idx will say how far to seek into tracks.db.

tracks.dat - position of track in various modes

  Sequence of 8 int structs (see below), 1 for each track id.  Indicates
  where each file is in each mode (e.g., for mode switching).  All 1 based. 
  For playlists, I put the first place it appears in a playlist, but you can
  put the last place, or another.  You could even list a different file if
  you wanted to land on a different file when switching modes.

pX.idx - index for each playlist

  Sequence of track ids, corresponding to each track in the associated .m3u
  file.  Note here, the playlist files are 0 based.  Why are the 0 based?  I
  don't know.
  
X.idx - index for each Artist/Album/Genre

  Sequence of track ids, for each track in each "disc" in Artist/Album/Genre
  mode.  Here the X is 1-based.
  
num_tracks.db - number of tracks in each A/A/G

  The first int is the number of "discs", followed by the number of tracks
  in each "disc".  Note that this is different from num_tracks.db in the
  Profiles directory, which does not start off with the number of discs.
  
alpha.db - for alphabet seeking

  26 ints - indicating first "disc" for each letter of the alphabet.  These
  should be 0 based, so disc 3 would be 2 here.  Again, I don't know how
  this got to be 0 based.  If there is no disc for that letter, it should be
  set to -1.


Note how it is nicely inconsistent in where to be 0 based and where to be 1
based.  Also, (artist|album|genre).db are not used by the PhatBox anymore,
but are used by this program to generate those indexes.  Finally, your
product_version in Profiles.ini must be greater than or equal to "1, 8, 4, 0".

*/

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <string.h>
#include <ctype.h>

/* This is my default PHTDTA mount point, it can be changed from the command line */
#define DEFAULT_BASE_PATH "/mnt/phatdata/"

#define MAXFN 4096

/* Maximum number of "discs" and "tracks" for the artist/album/genre modes */
#define MAX_LISTS 2000
#define MAX_TRACKS 2000

#define TRACKS_DB "tracks.db"
#define ARTISTS_DB "artist.db"
#define GENRES_DB "genre.db"
#define ALBUMS_DB "album.db"

/* This works with OS X! */
#ifdef __BIG_ENDIAN__
#define htolel(x) \
     ((((x) & 0xff000000) >> 24) | (((x) & 0x00ff0000) >>  8) | \
      (((x) & 0x0000ff00) <<  8) | (((x) & 0x000000ff) << 24))
#else
#define htolel(x) (x)
#endif

/* This is the struct for tracks.dat */
struct track_data {
  int playlist;
  int playlist_track;
  int genre;
  int genre_track;
  int artist;
  int artist_track;
  int album;
  int album_track;
};

/* Profile to use.  Can be changed on command line. */
/* This _should_ be parsed from Profiles.ini or just run for each Profile. */
char *profile = "Default";

static int count_lines(char *fn)
     /*
      * Count lines not starting with '#'.
      */
{
  int cnt=0;
  FILE *fp;
  char str[MAXFN];

  if ((fp=fopen(fn, "r"))!=NULL) {
    while (fgets(str, MAXFN, fp) != NULL) {
      if (str[0] != '#')
	cnt++;
    }
    fclose(fp);
  }
  return cnt;
}

/* Parse the .db file into the given structures */
static int parse_db_file(char *fn, int *t_num, int **t_num_tracks, char ****t_filenames, int **t_alpha, int keynum) {
  FILE *fp;
  int num=MAX_LISTS, *num_tracks, i, tr=0, *alpha;
  char ***filenames, buf[MAXFN], *f, key[MAXFN], *t;

  if ((fp=fopen(fn,"r"))!=NULL) {
    num_tracks = malloc(sizeof(int)*num);
    filenames = malloc(sizeof(char *)*num);
    alpha = malloc(sizeof(int)*26);
    for (i=0; i<26; i++) alpha[i] = -1;
    num = -1;
    strcpy(key,"NULL");
    while (fgets(buf,MAXFN,fp)) {
      f=strtok(buf,"\t");
      t = f;
      for (i=0; i<keynum; i++) t=strtok(NULL,"\t");
      if (strcasecmp(key,t)) {
        num++;
        tr=0;
        num_tracks[num] = MAX_TRACKS;
        filenames[num] = malloc(sizeof(char *)*num_tracks[num]);
        num_tracks[num] = 0;
        strcpy(key,t);
        if ((tolower(key[0]) >= 'a') && (tolower(key[0]) <= 'z')) {
          if (alpha[tolower(key[0])-'a'] == -1)
            alpha[tolower(key[0])-'a'] = num;
        }
      }
      num_tracks[num]++;
      filenames[num][tr++] = strdup(f);
    }
    num++;
    fclose(fp);
    *t_num = num;
    *t_num_tracks = num_tracks;
    *t_filenames = filenames;
    *t_alpha = alpha;
    return 1;
  }
  printf("Can't open %s\n",fn);
  exit(1);
  return 0;
}

/* Append the given int to the given file */
int append_int(char *fn, int i) {
  FILE *fp;
  i=htolel(i);
  if ((fp=fopen(fn,"ab"))!=NULL) {
    fwrite(&i,sizeof(int),1,fp);
    fclose(fp);
  } else {
    printf("Couldn't open %s\n",fn);
    exit(1);
  }
  return 1;
}

int main (int argc, char *argv[]) {
  char basepath[512], tempstr[4096], str[4];
  char **filenames, ***db_filenames;
  int *offsets, num_tracks, i, tempindex, j, k, db_num, *db_num_tracks, *db_alpha;
  int exists;
  struct track_data *track_indexes;
  FILE *fp;

  /* First arg changes PHTDTA path, second changes default profile */
  if (argc > 1) {
    strcpy(basepath,argv[1]);
    if (basepath[strlen(basepath)-1] != '/')
      strcat(basepath,"/");
    if (argc > 2) {
      profile = argv[2];
    }
  } else {
    strcpy(basepath,DEFAULT_BASE_PATH);
  }

  printf("Writing database in %s\n",basepath);

  sprintf(tempstr,"%s%s",basepath,TRACKS_DB);

  /* Get number of tracks and build tracks.idx information */
  num_tracks = count_lines(tempstr);
  printf("%i files in %s\n",num_tracks,tempstr);

  offsets = malloc((num_tracks+1)*sizeof(int));
  filenames = malloc((num_tracks+1)*sizeof(char *));
  track_indexes = malloc((num_tracks+1)*sizeof(struct track_data));
  memset(track_indexes,0,(num_tracks+1)*sizeof(struct track_data));
  offsets[0] = 0;
  i=0;
  if ((fp=fopen(tempstr,"r"))==NULL) {
    printf("%s not found\n",tempstr);
    exit(1);
  }

  while (fgets(tempstr,4096,fp)) {
    filenames[i] = strdup(strtok(tempstr,"\t"));
    printf("%i:%i:%s\n",i+1,offsets[i],filenames[i]);
    i++;
    offsets[i] = (int)ftell(fp);
  }

  fclose(fp);

  sprintf(tempstr,"%stracks.idx",basepath);
  unlink(tempstr);

  /* Make sure directories exist */

  sprintf(tempstr,"%sProfiles/%s/Artists",basepath,profile);
  mkdir(tempstr,0777);
  sprintf(tempstr,"%sProfiles/%s/Albums",basepath,profile);
  mkdir(tempstr,0777);
  sprintf(tempstr,"%sProfiles/%s/Genres",basepath,profile);
  mkdir(tempstr,0777);

  /* For each artist/album/genre, parse the .dbfile, file, write out .idx and .db files */

  /* Begin Artist */

  printf("Artist lists\n");
  sprintf(tempstr,"%s"ARTISTS_DB,basepath);
  parse_db_file(tempstr,&db_num,&db_num_tracks,&db_filenames,&db_alpha,2);
  sprintf(tempstr,"%sProfiles/%s/Artists/num_tracks.db",basepath,profile);
  unlink(tempstr);
  append_int(tempstr,db_num);
  for (j=1; j<=db_num; j++) {
    sprintf(tempstr,"%sProfiles/%s/Artists/num_tracks.db",basepath,profile);
    append_int(tempstr,db_num_tracks[j-1]);
    sprintf(tempstr,"%sProfiles/%s/Artists/%i.idx",basepath,profile,j);
    unlink(tempstr);
    for (k=1; k<=db_num_tracks[j-1]; k++) {
      tempindex = 0;
      for (i=0; i<num_tracks; i++) {
	if (!strcasecmp(db_filenames[j-1][k-1],filenames[i])) {
	  tempindex = i+1;
	}
      }
      printf("%i:%i:%i:%s\n",j,k,tempindex,db_filenames[j-1][k-1]);
      if (tempindex) {
	track_indexes[tempindex-1].artist = j;
	track_indexes[tempindex-1].artist_track = k;
      }
      append_int(tempstr,tempindex);
    }
  }
  sprintf(tempstr,"%sProfiles/%s/Artists/alpha.db",basepath,profile);
  unlink(tempstr);
  str[1] = '\0';
  for (j=0; j<26; j++) append_int(tempstr,db_alpha[j]);
  
  /* End Artist */

  /* Begin Album */

  printf("Album lists\n");
  sprintf(tempstr,"%s"ALBUMS_DB,basepath);
  parse_db_file(tempstr,&db_num,&db_num_tracks,&db_filenames,&db_alpha,3);
  sprintf(tempstr,"%sProfiles/%s/Albums/num_tracks.db",basepath,profile);
  unlink(tempstr);
  append_int(tempstr,db_num);
  for (j=1; j<=db_num; j++) {
    sprintf(tempstr,"%sProfiles/%s/Albums/num_tracks.db",basepath,profile);
    append_int(tempstr,db_num_tracks[j-1]);
    sprintf(tempstr,"%sProfiles/%s/Albums/%i.idx",basepath,profile,j);
    unlink(tempstr);
    for (k=1; k<=db_num_tracks[j-1]; k++) {
      tempindex = 0;
      for (i=0; i<num_tracks; i++) {
	if (!strcasecmp(db_filenames[j-1][k-1],filenames[i])) {
	  tempindex = i+1;
	}
      }
      printf("%i:%i:%i:%s\n",j,k,tempindex,db_filenames[j-1][k-1]);
      if (tempindex) {
	track_indexes[tempindex-1].album = j;
	track_indexes[tempindex-1].album_track = k;
      }
      append_int(tempstr,tempindex);
    }
  }
  sprintf(tempstr,"%sProfiles/%s/Albums/alpha.db",basepath,profile);
  unlink(tempstr);
  str[1] = '\0';
  for (j=0; j<26; j++) append_int(tempstr,db_alpha[j]);
  
  /* End Album */

  /* Begin Genre */

  printf("Genre lists\n");
  sprintf(tempstr,"%s"GENRES_DB,basepath);
  parse_db_file(tempstr,&db_num,&db_num_tracks,&db_filenames,&db_alpha,4);
  sprintf(tempstr,"%sProfiles/%s/Genres/num_tracks.db",basepath,profile);
  unlink(tempstr);
  append_int(tempstr,db_num);
  for (j=1; j<=db_num; j++) {
    sprintf(tempstr,"%sProfiles/%s/Genres/num_tracks.db",basepath,profile);
    append_int(tempstr,db_num_tracks[j-1]);
    sprintf(tempstr,"%sProfiles/%s/Genres/%i.idx",basepath,profile,j);
    unlink(tempstr);
    for (k=1; k<=db_num_tracks[j-1]; k++) {
      tempindex = 0;
      for (i=0; i<num_tracks; i++) {
	if (!strcasecmp(db_filenames[j-1][k-1],filenames[i])) {
	  tempindex = i+1;
	}
      }
      printf("%i:%i:%i:%s\n",j,k,tempindex,db_filenames[j-1][k-1]);
      if (tempindex) {
	track_indexes[tempindex-1].genre = j;
	track_indexes[tempindex-1].genre_track = k;
      }
      append_int(tempstr,tempindex);
    }
  }
  sprintf(tempstr,"%sProfiles/%s/Genres/alpha.db",basepath,profile);
  unlink(tempstr);
  str[1] = '\0';
  for (j=0; j<26; j++) append_int(tempstr,db_alpha[j]);
  
  /* End Artist */

  /* Go through playlists and write out playlist idx files */

  printf("Playlists\n");
  j = 1;
  do {
    char buffer[4096];
    k=1;
    sprintf(tempstr,"%sProfiles/%s/p%i.m3u",basepath,profile,j-1);
    if ((fp=fopen(tempstr,"r"))!=NULL) {
      sprintf(tempstr,"%sProfiles/%s/p%i.idx",basepath,profile,j-1);
      unlink(tempstr);
      while (fgets(buffer,4096,fp)) {
        if (buffer[0]=='#') continue;
        strtok(buffer,"\t\r\n");
        tempindex = 0;
        for (i=0; i<num_tracks; i++) {
  	  if (!strcasecmp(buffer,filenames[i])) {
	    tempindex = i+1;
	  }
        }
        printf("%i:%i:%i:%s\n",j,k,tempindex,buffer);
        if (tempindex && (track_indexes[tempindex-1].playlist == 0)) {
	  track_indexes[tempindex-1].playlist = j;
	  track_indexes[tempindex-1].playlist_track = k;
        }
        append_int(tempstr,tempindex);
        k++;
      }
      fclose(fp);
      exists=1;
    } else {
      exists=0;
    }
    j++;
  } while (exists);

  sprintf(tempstr,"%stracks.idx",basepath);
  unlink(tempstr);
  append_int(tempstr,num_tracks);
  for (i=0; i<num_tracks; i++) {
    append_int(tempstr,offsets[i]);
  }

  /* We've got all the tracks.dat info, write it out */

#ifdef __BIG_ENDIAN__
  for (i=0; i<num_tracks; i++) {
    track_indexes[i].playlist = htolel(track_indexes[i].playlist);
    track_indexes[i].playlist_track = htolel(track_indexes[i].playlist_track);
    track_indexes[i].genre = htolel(track_indexes[i].genre);
    track_indexes[i].genre_track = htolel(track_indexes[i].genre_track);
    track_indexes[i].artist = htolel(track_indexes[i].artist);
    track_indexes[i].artist_track = htolel(track_indexes[i].artist_track);
    track_indexes[i].album = htolel(track_indexes[i].album);
    track_indexes[i].album_track = htolel(track_indexes[i].album_track);
  }
#endif
  sprintf(tempstr,"%stracks.dat",basepath);
  if ((fp=fopen(tempstr,"wb"))!=NULL) {
    fwrite(track_indexes,sizeof(struct track_data),num_tracks,fp);
    fclose(fp);
  }

  return 0;
}
