/*
 * cdda_lib.c: The cddb access implementation
 *
 * See the README file for copyright information and how to reach the author.
 *
 * $Id: cdda_cddb.c,v 1.1.1.3 2005/06/27 13:34:02 cm Exp $
 */

#include "cdda_cddb.h"
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <errno.h>

enum eCddbGenre { cgBlues,
                  cgClassical,
                  cgCountry,
                  cgData,
                  cgFolk,
                  cgJazz,
                  cgNewAge,
                  cgReggae,
                  cgRock,
                  cgSoundtrack,
                  cgMisc,
                  cgMAX };

const char *cddbGenre[] = { "blues", "classical", "country", "data",
                           "folk", "jazz", "newage", "reggae",
                           "rock", "soundtrack", "misc" };

// --- cCddaCddbCommon --------------------------------------------------------

cCddaCddbCommon::cCddaCddbCommon(cCddaDisc *disc) {
  po_disc = disc;
}

cCddaCddbCommon::~cCddaCddbCommon() {
}

void cCddaCddbCommon::UpdateCdtext(const char *genre, const char *key, const char *value) {
  char *p_key = NULL, *p_value1 = NULL, *p_value2 = NULL, *p_idx = NULL;
  int track = 0;

  if (NULL != key && NULL != value) {
    sscanf(key, "%a[^\n0-9]%d", &p_key, &track);
    
    p_idx = index(value, '/');
    if (NULL != p_idx && 0x20 == *(p_idx-1) && 0x20 == *(p_idx+1) && 0x00 != *(p_idx+2))
      sscanf(value, "%a[^/] / %a[^/]", &p_value1, &p_value2);
    else
      sscanf(value, "%a[^/]", &p_value1);
    
    FixString(p_value1);
    FixString(p_value2);
#ifdef DEBUG
    printf("S key=<%s> track=%d value1=<%s> value2=<%s>\n", p_key, track, p_value1, p_value2);
#endif

    if (0 == strcmp("DISCID", p_key)) {
      po_disc->Cdtext()->SetDiscId(p_value1);
    } else if (0 == strcmp("DTITLE", p_key)) {
      po_disc->Cdtext()->SetPerformer(p_value1);
      po_disc->Cdtext()->SetTitle(p_value2);
    } else if (0 == strcmp("DYEAR", p_key)) {
      po_disc->Cdtext()->SetTocInfo2(p_value1);
    } else if (0 == strcmp("DGENRE", p_key)) {
      // currently not implemented
    } else if (0 == strcmp("TTITLE", p_key)) {
      if (NULL == p_value2) {
        po_disc->Tracks()->Item(track+1)->Cdtext()->SetPerformer(po_disc->Cdtext()->GetPerformer());
        po_disc->Tracks()->Item(track+1)->Cdtext()->SetTitle(p_value1);
      } else {
        po_disc->Tracks()->Item(track+1)->Cdtext()->SetPerformer(p_value1);
        po_disc->Tracks()->Item(track+1)->Cdtext()->SetTitle(p_value2);
      }
      po_disc->Tracks()->Item(track+1)->Cdtext()->SetGenre(genre);
    } else if (0 == strcmp("EXTD", p_key)) {
      po_disc->Cdtext()->SetTocInfo(p_value1);
    } else if (0 == strcmp("EXTT", p_key)) {
      po_disc->Tracks()->Item(track+1)->Cdtext()->SetMessage(p_value1);
    } else if (0 == strcmp("PLAYORDER", p_key)) {
      // currently not implemented
    } else {
      // key not known
#ifdef DEBUG
      printf("S unknown key(%s)\n", p_key);
#endif
    }

    free(p_key);
    free(p_value1);
    free(p_value2);
  }
}

// pointer must be free'd by caller
char *cCddaCddbCommon::ReadAllFromSocket(int fd, int &length) {
  int i = 0, pc = 0;
  char *p = NULL, *p_copy = NULL;

  do { // read all what we can get
    i++;
    if (1 < i) {
      usleep(250000); // give the server some time to transfer data
      p_copy = p;
      p = NULL;
    }

    p = (char *)malloc(256*i+1); // add one byte to let some space for the last terminating \0

    if (NULL != p) {
      memset(p, 0x00, 256*i+1);

      if (1 < i) {
        memcpy(p, p_copy, 256*(i-1));
        free(p_copy);
        p_copy = NULL;
      }

      pc = read(fd, p + (256*(i-1)), 256);
    }
  } while (256 == pc && NULL != p);
  pc += 256*(i-1);

  length = pc;
  return p;
}

char *cCddaCddbCommon::FixString(char *string) {
  char *p_pos = string;

  if (NULL != string) {
    // strip trailing space
    while (0x00 != *p_pos)
      p_pos++;

    if (0x20 == *(--p_pos))
      *p_pos = 0x00;

    // strip leading space
    p_pos = string;
    if (0x20 == *p_pos) {
      while (0x00 != *p_pos) {
        p_pos++;
        *(p_pos-1) = *p_pos;
      }
    }
  }

  return string;
}

// --- cCddaCddbCache ---------------------------------------------------------

cCddaCddbCache::cCddaCddbCache(cCddaDisc *disc, const char *dir) : cCddaCddbCommon(disc) {
  if (NULL != dir)
    p_directory = strdup(dir);
  else
    p_directory = NULL;
}

cCddaCddbCache::~cCddaCddbCache() {
  free(p_directory);
}

bool cCddaCddbCache::Write(unsigned long cddbId, const char *genre, const char *data, int length) {
  bool result = false;
  int cachefd = -1, i = 0;
  char *p_filename = NULL;

  if (NULL == p_directory)
    return result;

  asprintf(&p_filename, "%s/%s", p_directory, genre);
  if (NULL != p_filename) 
    result = TestDirectory(p_filename);
  free(p_filename);

  if (result) {
    asprintf(&p_filename, "%s/%s/%08lx", p_directory, genre, cddbId);

    if (NULL != p_filename) {
      cachefd = open(p_filename, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH);

      if (-1 != cachefd) {
        i = write(cachefd, data, length);
        close(cachefd);

        if (-1 == i)
          unlink(p_filename);
        else
          result = true;
#ifdef DEBUG
      } else
        printf("CQ open cache failed (%s)\n", p_filename);
#else
      }
#endif

      free(p_filename);
    }
  }

  return result;
}

bool cCddaCddbCache::Query(unsigned long cddbId) {
  bool hit = false;
  int cachefd = -1, i = 0, genre = 0, pc = 0;
  char *p_filename = NULL, *p = NULL, *p_copy = NULL, *p_key = NULL, *p_entry = NULL;

  if (NULL == p_directory)
    return hit;

  while(genre < cgMAX) {
    asprintf(&p_filename, "%s/%s/%08lx", p_directory, cddbGenre[genre], cddbId);
    if (NULL != p_filename) {
      cachefd = open(p_filename, O_RDONLY);
#ifdef DEBUG
      printf("CQ testing (%s:%d:%s)\n", p_filename,cachefd, strerror(errno));
#endif

      if (0 < cachefd || (-1 == cachefd && ENOENT != errno)) {
#ifdef DEBUG
        printf("QL cache hit: %s\n", p_filename);
#endif
        break;
      } else {
        free(p_filename);
        p_filename = NULL;
        genre++;
      }
    }
  }

  if (0 < cachefd) {
    p = ReadAllFromSocket(cachefd, pc);
    close(cachefd);

    p_copy = p;
    while(NULL != p_copy && '.' != p_copy[0] && 0x00 != p_copy[0]) {
      if ('#' != p_copy[0]) {
        i = sscanf(p_copy, "%a[^=\n]=%a[^\r\n]\r\n", &p_key, &p_entry);
#ifdef DEBUG
        printf("QL key=<%s> entry=<%s>\n", p_key, 2 == i ? p_entry : NULL);
#endif
        UpdateCdtext(cddbGenre[genre], p_key, 2 == i ? p_entry : NULL);
        free(p_key);
        free(p_entry);
        p_key = p_entry = NULL;
      }
      p_copy = index(p_copy, '\n');
      if (NULL != p_copy)
        p_copy++; // increase pointer to skip \n
    }
    free(p);
    hit = true;
  }

  free(p_filename);
  return hit;
}

bool cCddaCddbCache::TestDirectory(const char *dir) {
  bool result = false;
  int i = 0;
  struct stat dirStat;

  if (NULL == dir)
    return false;

  i = stat(dir, &dirStat);

  if (-1 == i && ENOENT == errno) {
    i = mkdir(dir, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH);
    if (0 == i)
      result = true;
    else
      result = false;
  } else {
    if (S_ISDIR(dirStat.st_mode) && dirStat.st_mode & (S_IWUSR | S_IWGRP | S_IWOTH))
      result = true;
    else
      result = false;
  }

  return result;
}

// --- cCddaCddbServer --------------------------------------------------------
cCddaCddbServer::cCddaCddbServer(cCddaDisc *disc, const char *fqdn, unsigned int port) : cCddaCddbCommon(disc) {
  sockfd = -1;

  p_fqdn = strdup(fqdn);

  po_cache = NULL;

  serverAddr.sin_family = AF_INET;
  serverAddr.sin_addr.s_addr = 0;
  serverAddr.sin_port = htons(port);

  p_username = strdup("anonymous");
  p_hostname = strdup("localhost");
  p_clientname = strdup("vdr-cdda");
  p_version = strdup("0.0.4");
}

cCddaCddbServer::~cCddaCddbServer() {
  if (isConnected())
    Disconnect();

  free(p_fqdn);
  free(p_username);
  free(p_hostname);
  free(p_clientname);
  free(p_version);
}

bool cCddaCddbServer::Connect() {
  if (0 == serverAddr.sin_addr.s_addr)
    if (! LookupHost())
      return false;

  if (-1 == sockfd)
    sockfd = socket(AF_INET, SOCK_STREAM, 0);

  if (-1 != sockfd) {
    if (0 != connect(sockfd, (struct sockaddr *)&serverAddr, sizeof(struct sockaddr))) {
      close(sockfd);
      sockfd = -1;
      return false;
    }
  } else
    return false;

  return true;
}

void cCddaCddbServer::Disconnect() {
  close(sockfd);
  sockfd = -1;
}

bool cCddaCddbServer::LookupHost(void) {
  struct hostent *hostEnt = NULL;
  char **pp_addr = NULL;

  hostEnt = gethostbyname(p_fqdn);

  if (NULL != hostEnt) {
    if (AF_INET == hostEnt->h_addrtype && 4 == hostEnt->h_length) {
      for (pp_addr = hostEnt->h_addr_list; NULL != (*pp_addr); (*pp_addr)++) {
        serverAddr.sin_addr.s_addr = ((struct in_addr *)(*pp_addr))->s_addr;
        break; // we are only using the first ip
      }
    } else
      return false;
  } else
    return false;

  return true;
}

bool cCddaCddbServer::Query(unsigned long cddbId, unsigned char num, unsigned int length, unsigned int offsets[]) {
  bool run = true, hit = false;
  char *p = NULL;
  char *p_h = NULL, *p_v = NULL, *p_d = NULL;
  char *p_category = NULL, *p_discTitle = NULL, *p_key = NULL, *p_entry = NULL;
  int pc = 0, code = 0, state = 0, i = 0;
  unsigned long id = cddbId;

  if (! isConnected())
    return false;

  while (run) {
    // send command handler
    switch (state) {
      case 1:  // say hello
        pc = asprintf(&p, "cddb hello %s %s %s %s\n", p_username, p_hostname, p_clientname, p_version);
        break;
      case 2:  // query disc
        p_d = p_h = NULL;
        asprintf(&p_d, "%d", offsets[0]);
        if (1 < num) {
          for (i = 1; i < num; i++) {
            asprintf(&p_h, "%s %d", p_d, offsets[i]);
            free(p_d);
            p_d = p_h;
          }
        }
        pc = asprintf(&p, "cddb query %08lx %d %s %d\n", id, num, p_h, length);
        free(p_h);
        break;
      case 3: // read entry from database
        pc = asprintf(&p, "cddb read %s %08lx\n", p_category, id);
        break;
      case 4: // say bye bye to the server
        pc = asprintf(&p, "quit\n");
        break;
    } // switch (state) - send command handler

    if (0 < state) {
#ifdef DEBUG
      printf("W %s", p);
#endif
      write(sockfd, p, pc);
      free(p);
      pc = 0;
    }

    p = ReadAllFromSocket(sockfd, pc);

    // parse answer handler
    switch (state) {
      case 0: // server greeting
        p_h = p_v = p_d = NULL;
        sscanf(p, "%3d %as CDDBP server %as ready at %a[^\r\n]\r\n", &code, &p_h, &p_v, &p_d);
#ifdef DEBUG
        printf("R %d %s %s %s\n", code, p_h, p_v, p_d);
#endif

        switch (code) {
          case 200:
          case 201:
            state++;
            break;
          case 432:
          case 433:
          case 434:
            state = 4;
            break;
        }
        free(p_h);
        free(p_v);
        free(p_d);
        break;
      case 1: // server echoed my client info
        p_d = NULL;
        sscanf(p, "%3d %a[^\r\n]\r\n", &code, &p_d);
#ifdef DEBUG
        printf("R %d %s\n", code, p_d);
#endif

        switch (code) {
          case 200:
          case 402:
            state++;
            break;
          case 431:
            state = 4;
            break;
        }
        free(p_d);
        break;
      case 2: // server give's me list of entries/matches
        p_d = NULL;
        sscanf(p, "%3d %a[^\r\n.].\r\n", &code, &p_d);
#ifdef DEBUG
        printf("R %d %s\n", code, p_d);
#endif
        switch (code) {
          case 200:
            sscanf(p_d, "%as %*s %a[^\n]", &p_category, &p_discTitle);
#ifdef DEBUG
            printf("P Category: %s Disc Title: %s\n", p_category, p_discTitle);
#endif
            state++;
            break;
          case 211:
            p_h = index(p, '\n'); // starts at p
            while(NULL != p_h && '.' != p_h[1] && 0x00 != p_h[1]) {
              p_h++; // increase pointer to skip \n
              sscanf(p_h, "%as %lx %a[^\r\n]\r\n", &p_category, &id , &p_discTitle); // we _must_ overwrite our cddbId 
#ifdef DEBUG
              printf("P Category: %s Disc Title: %s\n", p_category, p_discTitle);
#endif
              p_h = index(p_h, '\n');
              break; // read only the first entry
            }
            p_h = NULL;
            state++;
            break;
          case 202:
          case 403:
#ifdef DEBUG
            printf("CDDB no cddb entry found\n");
#endif
            state = 4;
            break;
          case 409:
            state = 0;
            break;
        }
        free(p_d);
        break;
      case 3: // server lists cddb entry
        p_d = p_h = NULL;
        sscanf(p, "%3d %a[^\r\n]\r\n", &code, &p_d);
#ifdef DEBUG
        printf("R %d %s\n", code, p_d);
#endif
        free(p_d);
        switch (code) {
          case 210:
            p_h = index(p, '\n'); // starts at p

            if (0 < pc) {
              // FIX: ReadAllFromSocket should be "optimized"
              int len = pc - ((p_h+1) - p);
              if ('.' == p_h[len-2]) {
                len-=3;
              }
              // FIX: sometimes the server given id differs from my own
              // FIX: but also other programs generate the same as i do
              // FIX: so i'll cache the disc under my own id
              po_cache->Write(cddbId, p_category, p_h+1, len);
            }

            while(NULL != p_h && '.' != p_h[1] && 0x00 != p_h[1]) {
              p_h++; // increase pointer to skip \n
              if ('#' != p_h[0]) {
                i = sscanf(p_h, "%a[^=\n]=%a[^\r\n]\r\n", &p_key, &p_entry);
#ifdef DEBUG
                printf("P key=<%s> entry=<%s>\n", p_key, 2 == i ? p_entry : NULL);
#endif
                UpdateCdtext(p_category, p_key, 2 == i ? p_entry : NULL);
                free(p_key);
                free(p_entry);
                p_key = p_entry = NULL;
              }
              p_h = index(p_h, '\n');
            }
            p_h = NULL;
            state++;
            break;
          case 401:
          case 402:
          case 403:
            state = 4;
            break;
          case 409:
            state = 0;
            break;
        }
        hit = true;
        break;
      case 4: // exit query loop
        p_d = NULL;
        sscanf(p, "%3d %a[^\r\n]\r\n", &code, &p_d);
#ifdef DEBUG
        printf("R %d %s\n", code, p_d);
#endif
        free(p_d);
        switch (code) {
          case 230:
          case 530:
            run = false;
            break;
        }
        break;
    } // switch (state) - parse answer handler
    free(p);
  } // while (run)

  free(p_category);
  free(p_discTitle);

  return hit;
}

void cCddaCddbServer::SetUser(const char *name) {
  if (NULL != p_username)
    free(p_username);

  p_username = strdup(name);
}

void cCddaCddbServer::SetHost(const char *name) {
  if (NULL != p_hostname)
    free(p_hostname);

  p_hostname = strdup(name);
}

void cCddaCddbServer::SetClient(const char *name) {
  if (NULL != p_clientname)
    free(p_clientname);

  p_clientname = strdup(name);
}

void cCddaCddbServer::SetVersion(const char *version) {
  if (NULL != p_version)
    free(p_version);

  p_version = strdup(version);
}

// FIX: should be handled in another way
void cCddaCddbServer::SetCache(cCddaCddbCache *cache) {
 po_cache = cache;
}

// --- cCddaCddb --------------------------------------------------------------

cCddaCddb::cCddaCddb(cCddaDisc *disc, const char *fqdn, unsigned int port, const char *cacheDir) : oCache(disc, cacheDir) , oServer(disc, fqdn, port) {
  active = false;
  id = numTracks = discLength = 0;
  p_trackOffsets = NULL;

  oServer.SetCache(&oCache);
  po_disc = disc;
}

cCddaCddb::~cCddaCddb() {
  Activate(false);

  po_disc = NULL;

  free(p_trackOffsets);
}

void cCddaCddb::Action () {
  int attempt = 0;

  active = true;
  while (active && 3 > attempt) {
      if (oCache.Query(id)) {
        po_disc->SetCdtextDirty(true);
        active = false;
      } else {
        if (! oServer.isConnected()) {
          attempt++;
          oServer.Connect();
        }

        if (oServer.isConnected()) {
          po_disc->SetCdtextDirty(oServer.Query(id, numTracks, discLength, p_trackOffsets));
          oServer.Disconnect();
          active = false;
        } else
          sleep(1);
      }
  }
  active = false;

  Clear();
#ifdef DEBUG
  printf("CDDB thread shutdown\n");
#endif
}

void cCddaCddb::Activate (bool On) {
  if (On) {
#ifdef DEBUG
    printf("CDDB thread activated\n");
#endif
    Start();
  } else {
    active = false;
    Cancel(1);
#ifdef DEBUG
    printf("CDDB thread deactivated\n");
#endif
  }
}

// taking ownership of offsets[]
bool cCddaCddb::Query(unsigned long cddbId, unsigned char num, unsigned int length, unsigned int offsets[]) {
  if (0 != cddbId && 0 != num && 0 != length && NULL != offsets && NULL != po_disc && ! active) {
    id = cddbId;
    numTracks = num;
    discLength = length;

    if (NULL != p_trackOffsets)
      free(p_trackOffsets);

    p_trackOffsets = offsets;

    Activate(true);
  } else
    return false;

  return true;
}

void cCddaCddb::SetUser(const char *name) {
  oServer.SetUser(name);
}

void cCddaCddb::SetHost(const char *name) {
  oServer.SetHost(name);
}

void cCddaCddb::SetClient(const char *name) {
  oServer.SetClient(name);
}

void cCddaCddb::SetVersion(const char *version) {
  oServer.SetVersion(version);
}

void cCddaCddb::Clear(void) {
  id = numTracks = discLength = 0;

  if (NULL != p_trackOffsets) {
    free(p_trackOffsets);
    p_trackOffsets = NULL;
  }
}
