/*
 * cdda_cddb.c: The cddb access implementation
 *
 * See the README file for copyright information and how to reach the author.
 *
 * $Id: cdda_cddb.c,v 1.20 2005/12/23 08:52:27 cm Exp $
 */

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

const int CDDB_MAX_ENTRIES = 8;

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

cCddaCddbCommon::cCddaCddbCommon() {
}

cCddaCddbCommon::~cCddaCddbCommon() {
}

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

void cCddaCddbCommon::Store(cCddaDisc *disc, cCddaTag *tag[], int num) {
  if (NULL != disc && NULL != tag && 0 != num) {
    tag[0]->SetDirty(true);
    if (NULL == disc->Tags()->Add(tag[0]))
      delete tag[0];
    tag[0] = NULL;

    for (int i = 1; i < num; i++) {
      tag[i]->SetDirty(true);
      if (NULL == disc->Tracks()->Item(i)->Tags()->Add(tag[i]))
        delete tag[i];
      tag[i] = NULL;
    }

    if (NULL == disc->Tags()->Current()->GetTitle())
      disc->Tags()->Next();
  }
}

void cCddaCddbCommon::Parse(cCddaTag *tag[], int num, const char *genre, const char *key, const char *value) {
  if (NULL != tag && 0 != num && NULL != key && NULL != value) {
    char *p_key = NULL;
    int track = 0;

    sscanf(key, "%a[^\n0-9]%d", &p_key, &track);

    char *p_value1 = NULL, *p_value2 = NULL, *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 (track <= num) {
      char *p_str = NULL;
      if (0 == strncmp("DISCID", p_key, 6)) {
        p_str = ConCatString(tag[0]->GetDiscId(), p_value1);
        free(p_value1);
        p_value1 = p_str;
        tag[0]->SetDiscId(p_value1);
        tag[0]->SetGenre(genre);
      } else if (0 == strncmp("DTITLE", p_key, 6)) {
        if (NULL != p_value1 && NULL != p_value2) {
          p_str = ConCatString(tag[0]->GetPerformer(), p_value1);
          free(p_value1);
          p_value1 = p_str;
          tag[0]->SetPerformer(p_value1);

          p_str = ConCatString(tag[0]->GetTitle(), p_value2);
          free(p_value2);
          p_value2 = p_str;
          tag[0]->SetTitle(p_value2);
        } else if (NULL != p_value1 && NULL == p_value2) {
          if (NULL == tag[0]->GetPerformer())
            tag[0]->SetPerformer(p_value1);
          else {
            p_str = ConCatString(tag[0]->GetTitle(), p_value1);
            free(p_value1);
            p_value1 = p_str;
            tag[0]->SetTitle(p_value1);
          }
        }
      } else if (0 == strncmp("DYEAR", p_key, 5)) {
        tag[0]->SetTocInfo2(p_value1);
      } else if (0 == strncmp("DGENRE", p_key, 6)) {
        // currently not implemented
      } else if (0 == strncmp("TTITLE", p_key, 6)) {
        if (NULL == p_value2) {
          tag[track+1]->SetPerformer(tag[0]->GetPerformer());

          p_str = ConCatString(tag[track+1]->GetTitle(), p_value1);
          free(p_value1);
          p_value1 = p_str;
          tag[track+1]->SetTitle(p_value1);
        } else {
          p_str = ConCatString(tag[track+1]->GetPerformer(), p_value1);
          free(p_value1);
          p_value1 = p_str;
          tag[track+1]->SetPerformer(p_value1);

          p_str = ConCatString(tag[track+1]->GetTitle(), p_value2);
          free(p_value2);
          p_value2 = p_str;
          tag[track+1]->SetTitle(p_value2);
        }
        tag[track+1]->SetGenre(genre);
      } else if (0 == strncmp("EXTD", p_key, 4)) {
        tag[0]->SetTocInfo(p_value1);
      } else if (0 == strncmp("EXTT", p_key, 4)) {
        tag[track+1]->SetMessage(p_value1);
      } else if (0 == strncmp("PLAYORDER", p_key, 9)) {
        // 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::Read(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) {
      cCondWait::SleepMs(250); // 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) {
  if (NULL != string) {
    char *p_pos = 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;
}

char *cCddaCddbCommon::ConCatString(const char *first, const char *second) {
  if (NULL != second) {
    int fLen = 0, sLen = 0;
    if (NULL != first)
      fLen = strlen(first);
    if (NULL != second)
      sLen = strlen(second);

    char *s = (char *)malloc(sizeof(char) * (fLen + sLen + 1));

    memcpy(s, first, fLen);
    memcpy(s + fLen, second, sLen);
    s[fLen + sLen] = 0x00;

    return s;
  }
  return NULL;
}

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

cCddaCddbCache::cCddaCddbCache(cCddaDisc *disc, const char *dir) :
  mp_directory(NULL), mpo_disc(disc) {
  if (NULL != dir)
    mp_directory = NewStrDup(dir);
}

cCddaCddbCache::~cCddaCddbCache() {
  delete [] mp_directory;
}

bool cCddaCddbCache::Write(unsigned long id, const char *genre, const char *data, int length) {
  bool result = false;

  if (NULL == mp_directory || NULL == mpo_disc)
    return result;

  char *p_filename = NULL;

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

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

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

      if (-1 != cachefd) {
        int 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 id) {
  bool hit = false;

  if (NULL == mp_directory)
    return hit;

  int genre = 0;
  while(genre < cgMAX) {
    char *p_filename = NULL;
    asprintf(&p_filename, "%s/%s/%08lx", mp_directory, cCddaCddbCommon::Genre[genre], id);
    if (NULL != p_filename) {
      int cachefd = open(p_filename, O_RDONLY);
#ifdef DEBUG
      printf("CQ testing %s:%d:%s\n", p_filename, cachefd, strerror(errno));
#endif

      if (-1 != cachefd) {
#ifdef DEBUG
        printf("QL cache hit: %s\n", p_filename);
#endif
        int pc = 0;
        char *p = Read(cachefd, pc);
        close(cachefd);

        int num = mpo_disc->Tracks()->Count() + 1;
        cCddaTag *p_tag[num];
        for (int i = 0; i < num; i++)
          p_tag[i] = new cCddaTag();

        char *p_copy = p;
        while(NULL != p_copy && '.' != p_copy[0] && 0x00 != p_copy[0]) {
          if ('#' != p_copy[0]) {
            char *p_key = NULL, *p_entry = NULL;
            int 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
            Parse(p_tag, num, cCddaCddbCommon::Genre[genre], p_key, 2 == i ? p_entry : NULL);
            free(p_key);
            free(p_entry);
          }
          p_copy = index(p_copy, '\n');
          if (NULL != p_copy)
            p_copy++; // increase pointer to skip \n
        }

        Store(mpo_disc, p_tag, num);

        free(p);
        hit = true;
      }
      free(p_filename);
    }
    genre++;
  }

  return hit;
}

bool cCddaCddbCache::Delete(unsigned long id, const char *genre) {
  bool result = false;

  if (0 != id && NULL != genre && NULL != mp_directory) {
    char *p_filename = NULL;

    asprintf(&p_filename, "%s/%s/%08lx", mp_directory, genre, id);
    if (NULL != p_filename) {
#ifdef DEBUG
      printf("CD file=%s\n", p_filename);
#endif
      result = (0 == unlink(p_filename));
      free(p_filename);
    }
  }

  return result;
}

bool cCddaCddbCache::TestDirectory(const char *dir) {
  bool result = false;

  if (NULL == dir)
    return false;

  struct stat dStat;
  if (-1 == stat(dir, &dStat) && ENOENT == errno)
    result = (0 == mkdir(dir, S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH));
  else
    result = S_ISDIR(dStat.st_mode) && dStat.st_mode & (S_IWUSR | S_IWGRP | S_IWOTH);

  return result;
}

// --- cCddaCddbServer ---------------------------------------------------------

cCddaCddbServer::cCddaCddbServer(cCddaDisc *disc, const char *fqdn, unsigned int port) :
  m_sockfd(-1), mp_fqdn(NULL), mpo_disc(disc), mpo_cache(NULL) {
  if (NULL != fqdn)
    mp_fqdn = NewStrDup(fqdn);

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

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

  delete [] mp_fqdn;
}

const char * const cCddaCddbServer::g_username = "anonymous";
const char * const cCddaCddbServer::g_hostname = "localhost";
const char * const cCddaCddbServer::g_clientname = "vdr-cdda";
const char * const cCddaCddbServer::g_version = "0.1.0";

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

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

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

  return true;
}

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

bool cCddaCddbServer::LookupHost(void) {
  struct hostent *hostEnt = gethostbyname2(mp_fqdn, AF_INET);

  if (NULL != hostEnt) {
    if (AF_INET == hostEnt->h_addrtype && 4 == hostEnt->h_length) {
      char **pp_addr = NULL;
      for (pp_addr = hostEnt->h_addr_list; NULL != (*pp_addr); (*pp_addr)++) {
        m_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 id, unsigned char numTracks, unsigned int length, int offsets[]) {
  if (! isConnected())
    return false;

  bool hit = false;
  int state = 0;

  int idx = 0;
  char *p_cdCategory[CDDB_MAX_ENTRIES] = { NULL, NULL, NULL, NULL,
                                           NULL, NULL, NULL, NULL };
  unsigned long cdId[CDDB_MAX_ENTRIES] = { 0, 0, 0, 0, 0, 0, 0, 0 };

  char *p_discTitle = NULL; // switch
  cCddaTag *p_tag[numTracks + 1]; // switch
  memset(p_tag, 0x00, sizeof(cCddaTag *) * numTracks + 1);

  while (-1 != state) {
    char *p = NULL, *p_h = NULL, *p_v = NULL, *p_d = NULL;
    int pc = 0;
    // send command handler
    switch (state) {
      case 1:  // say hello
        pc = asprintf(&p, "cddb hello %s %s %s %s\n", g_username, g_hostname, g_clientname, g_version);
        break;
      case 2:  // query disc
        asprintf(&p_d, "%d", offsets[0]);
        if (1 < numTracks) {
          for (int i = 1; i < numTracks; 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, numTracks, p_h, length);
        free(p_h);
        break;
      case 3: // read entry from database
        pc = asprintf(&p, "cddb read %s %08lx\n", p_cdCategory[idx], cdId[idx]);
        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(m_sockfd, p, pc);
      free(p);
      pc = 0;
    }

    p = Read(m_sockfd, pc);

    int code = 0;
    p_h = p_v = p_d = NULL;
    // parse answer handler
    switch (state) {
      case 0: // server greeting
        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
        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
        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 %lx %a[^\r\n]\r\n", &p_cdCategory[idx], &cdId[idx], &p_discTitle); // we _must_ overwrite our cddbId
#ifdef DEBUG
            printf("P Category: %s Disc Title: %s\n", p_cdCategory[idx], p_discTitle);
#endif
            free(p_discTitle);
            p_discTitle = NULL;

            state++;
            break;
          case 211:
            p_h = index(p, '\n'); // starts at p
            while(NULL != p_h && '.' != p_h[1] && 0x00 != p_h[1] && idx < CDDB_MAX_ENTRIES) {
              p_h++; // increase pointer to skip \n
              sscanf(p_h, "%as %lx %a[^\r\n]\r\n", &p_cdCategory[idx], &cdId[idx], &p_discTitle); // we _must_ overwrite our cddbId 
#ifdef DEBUG
              printf("P Category: %s Disc Title: %s\n", p_cdCategory[idx], p_discTitle);
#endif
              free(p_discTitle);
              p_discTitle = NULL;

              p_h = index(p_h, '\n');
              idx++;
            }
            idx = 0;
            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
        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: Read 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
              mpo_cache->Write(id, p_cdCategory[idx], p_h+1, len);
            }

            // numTracks + 1 (the disc itself)
            for (int i = 0; i < numTracks + 1; i++)
              p_tag[i] = new cCddaTag();

            while(NULL != p_h && '.' != p_h[1] && 0x00 != p_h[1]) {
              p_h++; // increase pointer to skip \n
              if ('#' != p_h[0]) {
                char *p_key = NULL, *p_entry = NULL;
                int 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
                Parse(p_tag, numTracks + 1, p_cdCategory[idx], p_key, 2 == i ? p_entry : NULL);
                free(p_key);
                free(p_entry);
              }
              p_h = index(p_h, '\n');
            }

            Store(mpo_disc, p_tag, numTracks + 1);
            memset(p_tag, 0x00, sizeof(cCddaTag *) * numTracks + 1);

            idx++;
            if (CDDB_MAX_ENTRIES <= idx || NULL == p_cdCategory[idx])
              state++;
            break;
          case 401:
          case 402:
          case 403:
            state = 4;
            break;
          case 409:
            state = 0;
            break;
        }
        hit = true;
        break;
      case 4: // exit query loop
        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:
            state = -1;
            break;
        }
        break;
    } // switch (state) - parse answer handler
    free(p);
  } // while (run)

  for (int i = 0; i < CDDB_MAX_ENTRIES && NULL != p_cdCategory[i]; i++)
    free(p_cdCategory[i]);

  return hit;
}

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

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

cCddaCddb::cCddaCddb() :
  mpo_device(NULL) {
}

cCddaCddb::~cCddaCddb() {
  Cancel(3);
}

cCddaCddb *cCddaCddb::gpo_cddb = NULL;

cCddaCddb& cCddaCddb::GetInstance(void) {
  if (NULL == gpo_cddb)
    gpo_cddb = new cCddaCddb();

  return *gpo_cddb;
}

void cCddaCddb::Action() {
#ifdef DEBUG
  printf("CDDB thread startup\n");
#endif

  cCddaConfiguration& o_config = cCddaConfiguration::GetInstance();
  eCddbPriority priority = o_config.GetCddbPriority();

  mpo_device->Lock();
  if (mpo_device->isOpen() && mpo_device->Disc()->isAudio() && 
     (cpNoCdText < priority || (cpNoCdText == priority && NULL == mpo_device->Disc()->Tags()->Default()->GetTitle()))) {
    cCddaCddbCache o_cache(mpo_device->Disc(), o_config.GetCddbDir());

    int attempt = 0;
    if (o_config.allowCddbCache() && NULL != o_config.GetCddbDir())
      if (o_cache.Query(mpo_device->Disc()->GetCddbId()))
        attempt = 3;

    if (cpCacheOnly != priority && 3 != attempt) {
      cCddaTracks *po_tracks = mpo_device->Disc()->Tracks();
      int *p_offsets = new int[po_tracks->Count()];

      if (NULL != p_offsets) {
        cCddaCddbServer o_server(mpo_device->Disc(), o_config.GetCddbServerFqdn(), o_config.GetCddbServerPort());
        o_server.SetCache(&o_cache);

        for (unsigned int i = po_tracks->GetFirst(); i <= po_tracks->Count(); i++)
          p_offsets[i - po_tracks->GetFirst()] = po_tracks->Item(i)->GetLba();

        while (Running() && 3 > attempt) {
          if (! o_server.isConnected()) {
            attempt++;
            o_server.Connect();
          }

          if (o_server.isConnected()) {
            o_server.Query(mpo_device->Disc()->GetCddbId(), po_tracks->Count(), DataSizeToSeconds(po_tracks->GetSize() + 2), p_offsets);
            o_server.Disconnect();
            attempt = 3;
          } else
            cCondWait::SleepMs(1000);
        }
        delete [] p_offsets;
      }
    }
  }

  mpo_device->Unlock();
  if (! mpo_device->isLocked())
    mpo_device->Close();
  mpo_device = NULL;

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

bool cCddaCddb::Query(cCddaDevice *device) {
  if (NULL != device && ! Running()) {
    mpo_device = device;
    Start();
  } else
    return false;

  return true;
}

bool cCddaCddb::Delete(unsigned long id, const char *genre) {
  if (cCddaConfiguration::GetInstance().allowCddbCache()) {
    printf("id=%08lx genre=%s\n", id, genre);
    cCddaCddbCache o_cache(NULL, cCddaConfiguration::GetInstance().GetCddbDir());
    return o_cache.Delete(id, genre);
  } else
    return false;
}
