/*  cdrdao - write audio CD-Rs in disc-at-once mode
 *
 *  Copyright (C) 1998-2000 Andreas Mueller <mueller@daneb.ping.de>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <config.h>

#include <stdio.h>
#include <assert.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>
#include <netdb.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Toc.h"
#include "CdTextItem.h"
#include "util.h"

#include "Cddb.h"


#define CDDB_MAX_LINE_LEN 1024
#define CDDB_DEFAULT_PORT_CDDBP 888
#define CDDB_DEFAULT_PORT_HTTP 80


static int getCode(const char *line, int code[3]);
static unsigned int cddbSum(unsigned int n);
static void convertEscapeSequences(const char *in, char *out);
static int parseQueryResult(char *line, char *category, char *diskId,
			    char *title);


static RETSIGTYPE alarmHandler(int sig)
{
  message(0, "ALARM");
#if RETSIGTYPE != void
  return 0;
#endif
}

Cddb::Cddb(const Toc *t)
{
  toc_ = t;

  serverList_ = NULL;
  selectedServer_ = NULL;
  localCddbDirectory_ = NULL;

  fd_ = -1;
  connected_ = 0;
  queryResults_ = NULL;
  cddbEntry_ = NULL;
  httpCmd_ = NULL;
  httpData_ = NULL;
  httpMode_ = 0;

  timeout_ = 20;
}

Cddb::~Cddb()
{
  ServerList *snext;

  shutdown();

  clearQueryResults();
  clearCddbEntry();

  delete[] localCddbDirectory_;
  localCddbDirectory_ = NULL;

  delete[] httpCmd_;
  httpCmd_ = NULL;

  delete[] httpData_;
  httpData_ = NULL;

  while (serverList_ != NULL) {
    snext = serverList_->next;

    delete[] serverList_->server;
    serverList_->server = NULL;

    delete[] serverList_->httpCgiBin;
    serverList_->httpCgiBin = NULL;

    delete[] serverList_->httpProxyServer;
    serverList_->httpProxyServer = NULL;

    delete serverList_;

    serverList_ = snext;
  }
}

void Cddb::timeout(int t)
{
  if (t > 0)
    timeout_ = t;
}

void Cddb::localCddbDirectory(const char *dir)
{
  const char *homeDir;

  delete[] localCddbDirectory_;

  // replace ~/ by the path to the home directory as indicated by $HOME
  if (dir[0] == '~' && dir[1] == '/' && (homeDir = getenv("HOME")) != NULL) {
    localCddbDirectory_ = strdupvCC(homeDir, dir + 1, NULL);
  }
  else {
    localCddbDirectory_ = strdupCC(dir);
  }
}

void Cddb::appendQueryResult(const char *category, const char *diskId,
			     const char *title, int exactMatch)
{
  QueryResults *run, *ent;

  for (run = queryResults_; run != NULL && run->next != NULL;
       run = run->next) ;

  ent = new QueryResults;

  ent->category = strdupCC(category);
  ent->diskId = strdupCC(diskId);
  ent->title = strdupCC(title);
  ent->exactMatch = (exactMatch != 0) ? 1 : 0;

  ent->next = NULL;

  if (run == NULL)
    queryResults_ = ent;
  else
    run->next = ent;
}

void Cddb::clearQueryResults()
{
  QueryResults *next;

  while (queryResults_ != NULL) {
    next = queryResults_->next;

    delete[] queryResults_->category;
    queryResults_->category = NULL;

    delete[] queryResults_->diskId;
    queryResults_->diskId = NULL;
    
    delete[] queryResults_->title;
    queryResults_->title = NULL;

    delete queryResults_;
    queryResults_ = next;
  }
}

void Cddb::clearCddbEntry()
{
  int i;

  if (cddbEntry_ != NULL) {
    delete[] cddbEntry_->diskTitle;
    cddbEntry_->diskTitle = NULL;

    delete[] cddbEntry_->diskArtist;
    cddbEntry_->diskArtist = NULL;

    delete[] cddbEntry_->diskExt;
    cddbEntry_->diskExt = NULL;

    for (i = 0; i < cddbEntry_->ntracks; i++) {
      delete[] cddbEntry_->trackTitles[i];
      cddbEntry_->trackTitles[i] = NULL;

      delete[] cddbEntry_->trackExt[i];
      cddbEntry_->trackExt[i] = NULL;
    }

    delete[] cddbEntry_->trackTitles;
    cddbEntry_->trackTitles = NULL;

    delete[] cddbEntry_->trackExt;
    cddbEntry_->trackExt = NULL;

    delete cddbEntry_;
    cddbEntry_ = NULL;
  }
}

/* Appends a CDDB server name to the server list. Format of server strings:
 * <server>      
 *   connect to <server>, default cddbp port, use cddbp protocol 
 *
 * <server>:<port> 
 *   connect to <server>, port <port>, use cddbp protocol
 * 
 * <server>:<cgi-bin-path>
 *   connect to <server>, default http port, use http protocol,
 *   url: <cgi-bin-path>
 *
 * <server>:<port>:<cgi-bin-path>
 *   connect to <server>, port <port>, use http protocol, url: <cgi-bin-path>
 *
 * <server>:<port>:<cgi-bin-path>:<proxy-server>
 *   connect to <proxy-server>, default http port, use http protocol,
 *   url: http://<server>:<port>/<cgi-bin-path>
 *
 * <server>:<port>:<cgi-bin-path>:<proxy-server>:<proxy-port>
 *   connect to <proxy-server>, port <proxy-port>, use http protocol,
 *   url: http://<server>:<port>/<cgi-bin-path>
 */
void Cddb::appendServer(const char *s)
{
  ServerList *run, *ent;
  char *name;
  char *port;
  char *httpCgiBin = NULL;
  char *httpProxyServer = NULL;
  char *httpProxyPort = NULL;
  unsigned short portNr = CDDB_DEFAULT_PORT_CDDBP;
  unsigned short httpProxyPortNr = CDDB_DEFAULT_PORT_HTTP;

  if (s == NULL || *s == 0)
    return;

  name = strdupCC(s);

  if ((port = strchr(name, ':')) != NULL) {
    *port = 0;
    port++;

    if (!isdigit(*port)) {
      httpCgiBin = port;
      port = NULL;
      portNr = CDDB_DEFAULT_PORT_HTTP;
    }
    else {
      if ((httpCgiBin = strchr(port, ':')) != NULL) {
	*httpCgiBin = 0;
	httpCgiBin++;
      }
    }

    if (httpCgiBin != NULL && 
	(httpProxyServer = strchr(httpCgiBin, ':')) != NULL) {
      *httpProxyServer = 0;
      httpProxyServer++;
    }

    if (httpProxyServer != NULL &&
	(httpProxyPort = strchr(httpProxyServer, ':')) != NULL) {
      *httpProxyPort = 0;
      httpProxyPort++;
    }
  }

  for (run = serverList_; run != NULL && run->next != NULL; run = run->next) {
    if (strcmp(run->server, name) == 0) {
      delete[] name;
      return;
    }
  }

  if (run != NULL && strcmp(run->server, s) == 0) {
    delete[] name;
    return;
  }

  if (port != NULL)
    portNr = (unsigned short)strtoul(port, NULL, 0);

  if (httpProxyPort != NULL)
    httpProxyPortNr = (unsigned short)strtoul(httpProxyPort, NULL, 0);

  ent = new ServerList;
  ent->server = name;
  ent->port = portNr;
  ent->httpCgiBin = (httpCgiBin != NULL) ? strdupCC(httpCgiBin) : NULL;
  if (httpProxyServer != NULL) {
    ent->httpProxyServer = strdupCC(httpProxyServer);
    ent->httpProxyPort = httpProxyPortNr;
  }
  else {
    ent->httpProxyServer = NULL;
    ent->httpProxyPort = 0;
  }
  ent->next = NULL;

  if (run == NULL)
    serverList_ = ent;
  else
    run->next = ent;
}

/* Tries to connect to a CDDB server. If no server was previously connected
 * (selectedServer_ == NULL) all servers from the server list will be tested
 * and the first successful connected server will be taken.
 * Return: 0: OK
 *         1: could not connect to any server
 */

int Cddb::openConnection()
{
  ServerList *run;
  struct hostent *hostEnt;
  struct sockaddr_in sockAddr;
  const char *server;
  unsigned short port;
  struct sigaction newAlarmHandler;
  struct sigaction oldAlarmHandler;
#ifndef HAVE_INET_ATON
  long inetAddr;
#endif

  if (fd_ >= 0) // already connected
    return 0;

  memset(&newAlarmHandler, 0, sizeof(newAlarmHandler));
  sigemptyset(&(newAlarmHandler.sa_mask));

#ifdef UNIXWARE
  newAlarmHandler.sa_handler = (void (*)()) alarmHandler;
#else
  newAlarmHandler.sa_handler = alarmHandler;
#endif
  
  if (sigaction(SIGALRM, &newAlarmHandler, &oldAlarmHandler) != 0) {
    message(-2, "CDDB: Cannot install alarm signal handler: %s",
	    strerror(errno));
    return 1;
  }
  alarm(0);

  for (run = (selectedServer_ != NULL) ? selectedServer_ : serverList_;
       run != NULL; 
       run = (selectedServer_ != NULL) ? (ServerList*)0 : run->next) {

    server = run->server;
    port = run->port;

    if (run->httpCgiBin != NULL) {
      if (run->httpProxyServer != NULL) {
	server = run->httpProxyServer;
	port = run->httpProxyPort;

	message(1,
		"CDDB: Connecting to http://%s:%u%s via proxy %s:%u ...",
		run->server, run->port, run->httpCgiBin, server, port);
      }
      else {
	message(1,
		"CDDB: Connecting to http://%s:%u%s ...", server, port,
		run->httpCgiBin);
      }
    }
    else {
      message(1, "CDDB: Connecting to cddbp://%s:%u ...", server, port);
    }

#ifdef HAVE_INET_ATON
    if (!inet_aton(server, &sockAddr.sin_addr)) {
#else
    if ((inetAddr = (long)inet_addr(server)) == -1) {
#endif
      if ((hostEnt = gethostbyname(server)) == NULL ||
	  hostEnt->h_addrtype != AF_INET) {
	alarm(0);
	message(-1, "CDDB: Cannot resolve hostname '%s' - skipping.", server);
	continue;
      }
      else {
	memcpy((char*) &sockAddr.sin_addr, hostEnt->h_addr, hostEnt->h_length);
      }
    }
#ifndef HAVE_INET_ATON
    else {
      memcpy((char*)&sockAddr.sin_addr, (char*)&inetAddr, sizeof(inetAddr));
    }
#endif

    message(4, "CDDB: Hostname: %s -> IP: %s", server,
	    inet_ntoa(sockAddr.sin_addr));

    if ((fd_ = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
      message(-2, "CDDB: Cannot create socket: %s", strerror(errno));
      goto fail;
    }

    sockAddr.sin_family = AF_INET;
    sockAddr.sin_port = htons(port);

    alarm(timeout_);

    if (connect(fd_, (struct sockaddr*)&sockAddr, sizeof(sockAddr)) == 0) {
      alarm(0);
      message(1, "CDDB: Ok.");
      selectedServer_ = run;
      break;
    }
    else {
      alarm(0);
      message(-1, "CDDB: Failed to connect to '%s:%u: %s", server, port,
	      strerror(errno));
      closeConnection();
    }
  }

 fail:
  connected_ = 0;

  alarm(0);

  if (sigaction(SIGALRM, &oldAlarmHandler, NULL) != 0) {
    message(-1, "CDDB: Cannot restore alarm signal handler: %s",
	    strerror(errno));
  }
  
  if (fd_ < 0)
    return 1;

  return 0;
}

/* Closes connection.
 */

void Cddb::closeConnection()
{
  if (fd_ >= 0) {
    close(fd_);
    fd_ = -1;
    connected_ = 0;
  }
}

/* Create some strings that are used for all communications via the http
 * protocol.
 */
void Cddb::setupHttpData(const char *userName, const char *hostName,
			 const char *clientName, const char *version)
{
  delete[] httpCmd_;

  httpCmd_ = strdupvCC("&hello=", userName, "+",  hostName, "+",
		       clientName, "+", version, 
		       "&proto=1", NULL);

  delete[] httpData_;

  httpData_ = strdupvCC("User-Agent: ", clientName, "/", version, "\r\n",
			"Accept: text/plain\r\n", NULL);

}

/* Tries to connect to a server of the internal server list and performs the
 * the client-server handshake.
 * Return: 0: OK
 *         1: could not connect to any server
 *         2: handshake failed
 */

int Cddb::connectDb(const char *userName, const char *hostName,
		    const char *clientName, const char *version)
{
  int code[3];
  const char *response;
  const char *cmdArgs[6];

  if (connected_)
    return 0;

  if (openConnection() != 0)
    return 1;

  if (selectedServer_->httpCgiBin != NULL) {
    httpMode_ = 1;
    setupHttpData(userName, hostName, clientName, version);
    return 0;
  }

  response = getServerResponse(code);

  if (response == NULL) {
    message(-2, "CDDB: EOF while waiting for server greeting.");
    closeConnection();
    return 2;
  }

  if (code[0] != 2) {
    message(-2, "CDDB: Connection to server denied: %s", response);
    return 2;
  }

  message(4, "CDDB: Server greeting: %s", response);

  connected_ = 1;

  cmdArgs[0] = "cddb";
  cmdArgs[1] = "hello";
  cmdArgs[2] = userName;
  cmdArgs[3] = hostName;
  cmdArgs[4] = clientName;
  cmdArgs[5] = version;

  if (sendCommand(6, cmdArgs) != 0) {
    message(-2, "CDDB: Failed to send handshake command.");
    return 2;
  }

  response = getServerResponse(code);

  if (response == NULL) {
    message(-2, "CDDB: EOF while waiting for server handshake response.");
    return 2;
  }

  if (code[0] != 2) {
    message(-2, "CDDB: Server handshake failed: %s", response);
    return 2;
  }

  message(4, "CDDB: Handshake response: %s", response);

  return 0;
}

/* Queries for entries that match the current 'toc_'.
 * 'results' will be filled with a list of matching diskIds/category/title
 * triples. 'results' will be NULL if no matching entry is found.
 * Return: 0: OK
 *         1: communication error occured
 */
int Cddb::queryDb(QueryResults **results)
{
  const char *cddbId;
  const char **args;
  const char *resp;
  char qtitle[CDDB_MAX_LINE_LEN];
  char qcategory[CDDB_MAX_LINE_LEN];
  char qdiskId[CDDB_MAX_LINE_LEN];
  char respBuf[CDDB_MAX_LINE_LEN];
  char *buf;
  int code[3];
  int err = 0;
  int nargs;
  int arg, i;
  int ntracks;
  const Track *t;
  Msf start, end;
  long diskLength;

  // clear previous results
  clearQueryResults();

  if (httpMode_) {
    if (openConnection() != 0)
      return 1;
  }

  ntracks = toc_->nofTracks();

  nargs = ntracks + 5;

  args = new (const char*)[nargs];
  arg = 0;

  args[arg++] = "cddb";
  args[arg++] = "query";

  cddbId = calcCddbId();

  args[arg++] = cddbId;

  buf = new char[20];
  sprintf(buf, "%d", ntracks);
  args[arg++] = buf;

  TrackIterator itr(toc_);

  for (t = itr.first(start, end); t != NULL; t = itr.next(start, end)) {
    long trackStart = start.lba() + 150;
    buf = new char[20];

    sprintf(buf, "%ld", trackStart);
    args[arg++] = buf;
  }

  buf = new char[20];
  diskLength = toc_->length().min() * 60 + toc_->length().sec() + 2;
  sprintf(buf, "%ld", diskLength);
  args[arg++] = buf;
  
  if (sendCommand(nargs, args) != 0) {
    message(-2, "CDDB: Failed to send QUERY command.");
    err = 1; goto fail;
  }

  if ((resp = getServerResponse(code)) == NULL) {
    message(-2, "CDDB: EOF while waiting for QUERY response.");
    err = 1; goto fail;
  }    

  message(4, "CDDB: QUERY response: %s", resp);

  if (code[0] != 2) {
    message(-2, "CDDB: QUERY failed: %s", resp);
    err = 1; goto fail;
  }
  else {
    if (code[2] == 0) {
      // found exact match
      strcpy(respBuf, resp + 3);
      if (parseQueryResult(respBuf, qcategory, qdiskId, qtitle)) {
	appendQueryResult(qcategory, qdiskId, qtitle, 1);
      }
      else {
	message(-2, "CDDB: Received invalid QUERY response: %s", resp);
	err = 1; goto fail;
      }
    }
    else if(code[2] == 1) {
      // found inexact matches
      while ((resp = readLine()) != NULL &&
	     strcmp(resp, ".") != 0) {
	strcpy(respBuf, resp);

	message(4, "CDDB: Query data: %s", resp);

	if (parseQueryResult(respBuf, qcategory, qdiskId, qtitle)) {
	  appendQueryResult(qcategory, qdiskId, qtitle, 0);
	}
	else {
	  message(-2, "CDDB: Received invalid QUERY data: %s", resp);
	  err = 1; goto fail;
	}
      }

      if (resp == NULL) {
	message(-2, "CDDB: EOF while reading QUERY data.");
	err = 1; goto fail;
      }	
    }
    else {
      // found no match
    }
  }
  
  
 fail:

  if (httpMode_)
    closeConnection();

  for (i = 3; i < arg; i++)
    delete[] args[i];

  delete[] args;
  
  *results = queryResults_;
  return err;
  
}

/* Reads CDDB entry for specified category and disk id. 'entry' will be
 * set to the stucture containing the record data.
 * Return: 0: OK
 *         1: communication error or could not retrieve CDDB entry
 */
int Cddb::readDb(const char *category, const char *diskId, CddbEntry **entry)
{
  int code[3];
  const char *args[4];
  const char *resp;
  int localRecordFd = -1;

  clearCddbEntry();

  if (httpMode_) {
    if (openConnection() != 0)
      return 1;
  }

  args[0] = "cddb";
  args[1] = "read";
  args[2] = category;
  args[3] = diskId;

  if (sendCommand(4, args) != 0) {
    message(-2, "CDDB: Failed to send READ command.");
    goto fail;
  }
  
  if ((resp = getServerResponse(code)) == NULL) {
    message(-2, "CDDB: EOF while waiting for READ response.");
    goto fail;
  }

  message(4, "CDDB: READ response: %s", resp);

  if (code[0] == 2) {
    if ((localRecordFd = createLocalCddbFile(category, diskId)) == -2) {
      message(-1, "Existing local CDDB record for %s/%s will not be overwritten.", category, diskId);
    }
    if (readDbEntry(localRecordFd) != 0) {
      message(-2, "CDDB: Received invalid database entry.");
      goto fail;
    }
  }
  else {
    message(-2, "CDDB: READ failed: %s", resp);
    goto fail;
  }
  
  *entry = cddbEntry_;

  if (httpMode_)
    closeConnection();

  if (localRecordFd >= 0)
    close(localRecordFd);

  return 0;

 fail:

  if (httpMode_)
    closeConnection();

  if (localRecordFd >= 0)
    close(localRecordFd);

  *entry = NULL;
  return 1;
}

/* Shuts down the connection to the CDDB server.
 */
void Cddb::shutdown()
{
  const char *resp;
  const char *args[1];
  int code[3];

  if (fd_ < 0) 
    return;

  if (!connected_) {
    closeConnection();
    return;
  }

  args[0] = "quit";

  if (sendCommand(1, args) == 0) {
    if ((resp = getServerResponse(code)) == NULL) {
      message(-1, "CDDB: EOF while waiting for QUIT response.");
    }
    else {
      message(4, "CDDB: QUIT response: %s", resp);
    }
  }
  else {
    message(-1, "CDDB: Failed to send QUIT command.");
  }

  closeConnection();
}


/* Filter characters of given string so that it is suitable as CD-TEXT data.
 */
static char *cdTextFilter(char *s)
{
  char *p = s;

  while (*p != 0) {
    if (*p == '\n' || *p == '\t')
      *p = ' ';

    p++;
  }

  return s;
}

/* Adds the data of the retrieved CDDB record stored in 'cddbEntry_' to
 * the given toc as CD-TEXT data.
 * Return: 1: CD-TEXT data was added
 *         0: toc was not modified
 */
int Cddb::addAsCdText(Toc *toc)
{
  int havePerformer = 0;
  int haveTitle = 0;
  int haveMessage = 0;
  int trun;
  CdTextItem *item;

  if (cddbEntry_ == NULL)
    return 0;

  if (cddbEntry_->diskTitle != NULL)
    haveTitle = 1;

  if (cddbEntry_->diskArtist != NULL)
    havePerformer = 1;

  if (cddbEntry_->diskExt != NULL)
    haveMessage = 1;

  for (trun = 0; trun < cddbEntry_->ntracks; trun++) {
    if (cddbEntry_->trackTitles[trun] != NULL)
      haveTitle = 1;

    if (cddbEntry_->trackExt[trun] != NULL) 
      haveMessage = 1;
  }

  if (!haveTitle && !havePerformer && !haveMessage)
    return 0;

  toc->cdTextLanguage(0, 9);

  if (haveTitle) {
    item = new CdTextItem(CdTextItem::CDTEXT_TITLE, 0,
      (cddbEntry_->diskTitle != NULL) ? cdTextFilter(cddbEntry_->diskTitle) : ""); 

    toc->addCdTextItem(0, item);
  }

  if (havePerformer) {
    item = new CdTextItem(CdTextItem::CDTEXT_PERFORMER, 0,
      (cddbEntry_->diskArtist != NULL) ? cdTextFilter(cddbEntry_->diskArtist) : "");

    toc->addCdTextItem(0, item);
  }

  if (haveMessage) {
    item = new CdTextItem(CdTextItem::CDTEXT_MESSAGE, 0,
      (cddbEntry_->diskExt != NULL) ? cdTextFilter(cddbEntry_->diskExt) : "");

    toc->addCdTextItem(0, item);
  }
    
  for (trun = 0; trun < toc->nofTracks(); trun++) {
    if (haveTitle) {
      
      item = new CdTextItem(CdTextItem::CDTEXT_TITLE, 0,
        (trun < cddbEntry_->ntracks && cddbEntry_->trackTitles[trun] != NULL) ? cdTextFilter(cddbEntry_->trackTitles[trun]) : ""); 

      toc->addCdTextItem(trun + 1, item);
    }

    if (havePerformer) {
      item = new CdTextItem(CdTextItem::CDTEXT_PERFORMER, 0,
        (cddbEntry_->diskArtist != NULL) ? cdTextFilter(cddbEntry_->diskArtist) : "");

      toc->addCdTextItem(trun + 1, item);
    }

    if (haveMessage) {
      item = new CdTextItem(CdTextItem::CDTEXT_MESSAGE, 0,
        (trun < cddbEntry_->ntracks && cddbEntry_->trackExt[trun] != NULL) ? cdTextFilter(cddbEntry_->trackExt[trun]) : "");

      toc->addCdTextItem(trun + 1, item);
    }
  }

  return 1;
}


/* Reads a line (until '\n') from 'fd_'. Checks for timeouts.
 */
const char *Cddb::readLine()
{
  static char buf[CDDB_MAX_LINE_LEN];
  int pos = 0;
  struct timeval tv;
  fd_set readFds;
  int ret;
  char *s;
  int characterRead = 0;

  while (pos < CDDB_MAX_LINE_LEN) {
    FD_ZERO(&readFds);
    FD_SET(fd_, &readFds);

    tv.tv_sec = timeout_;
    tv.tv_usec = 0;

    ret = select(fd_ + 1, &readFds, NULL, NULL, &tv);

    if (ret == 0) {
      message(-2, "CDDB: Timeout while reading data.");
      return NULL;
    }
    
    if (ret < 0) {
      message(-2, "CDDB: Error while waiting for data: %s", strerror(errno));
      return NULL;
    }

    ret = read(fd_, &(buf[pos]), 1);

    if (ret == 0) {
      // end of file
      break;
    }
    
    if (ret < 0) {
      message(-2, "CDDB: Error while reading data: %s", strerror(errno));
      return NULL;
    }
    
    characterRead = 1;

    if (buf[pos] == '\n')
      break;

    pos++;
  }

  if (pos >= CDDB_MAX_LINE_LEN)
    buf[CDDB_MAX_LINE_LEN - 1] = 0;
  else
    buf[pos] = 0;

  if (buf[0] == 0 && !characterRead) {
    // end of file
    return NULL;
  }

  // skip leading blanks
  for (s = buf; *s != 0 && isspace(*s); s++) ;

  // skip trailing blanks
  for (pos = strlen(s) - 1; pos >= 0 && isspace(s[pos]); pos--) 
    s[pos] = 0;

  message(5, "CDDB: Data read: %s", s);

  return s;
}

/* Checks if 'line' contains a cddb server status and sets 'code' to the
 * server code.
 * Return: 0: 'line' is not a cddb server status line
 *         1: 'line' is a cddb server status line, 'code' contains valid data
 */
static int getCode(const char *line, int code[3])
{
  if (isdigit(line[0]) && isdigit(line[1]) && isdigit(line[2]) &&
      isspace(line[3])) {
    code[0] = line[0] - '0';
    code[1] = line[1] - '0';
    code[2] = line[2] - '0';

    return 1;
  }

  return 0;
}


/* Reads lines from 'fd_' until a valid server status line is encountered.
 * 'code' is set to the server status code on success.
 * Return: server status line or 'NULL' on timeout or communication error
 */
const char *Cddb::getServerResponse(int code[3])
{
  const char *line;

  while ((line = readLine()) != NULL &&
	 !getCode(line, code)) ;

  return line;
}

/* Sends command in 'args' to 'fd_'. cdbbp and http protocols are handled.
 * Return: 0: OK
 *         1: communication error occured.
 */
int Cddb::sendCommand(int nargs, const char *args[])
{
  char portBuf[20];
  int len = 0;
  int err = 0;
  char *cmd, *p;
  char *httpCmd = NULL;
  int run, ret;
  struct timeval tv;
  fd_set writeFds;

  // build command line
  for (run = 0; run < nargs; run++)
    len += strlen(args[run]) + 1;

  cmd = new char[len + 1];
  *cmd = 0;

  for (run = 0; run < nargs; run++) {
    strcat(cmd, args[run]);
    if (run != nargs - 1) {
      if (httpMode_)
	strcat(cmd, "+");
      else
	strcat(cmd, " ");
    }
  }

  if (httpMode_) {
    if (selectedServer_->httpProxyServer != NULL) {
      sprintf(portBuf, ":%u", selectedServer_->port);
      httpCmd = strdupvCC("GET http://", selectedServer_->server,
			portBuf, selectedServer_->httpCgiBin, 
			"?cmd=", cmd, httpCmd_, " HTTP/1.0\r\n", 
			"Host: ", selectedServer_->server, "\r\n",
			httpData_, "\r\n", NULL);
    }
    else {
      httpCmd = strdupvCC("GET ", selectedServer_->httpCgiBin, 
			  "?cmd=", cmd, httpCmd_, " HTTP/1.0\r\n", 
			  "Host: ", selectedServer_->server, "\r\n",
			  httpData_, "\r\n", NULL);
    }

    delete[] cmd;
    cmd = httpCmd;
    httpCmd = NULL;

    message(4, "CDDB: Sending command '%s'...", cmd);
  }
  else {
    message(4, "CDDB: Sending command '%s'...", cmd);
    
    strcat(cmd, "\n");
  }

  len = strlen(cmd);
  p = cmd;

  while (len > 0) {
    FD_ZERO(&writeFds);
    FD_SET(fd_, &writeFds);

    tv.tv_sec = timeout_;
    tv.tv_usec = 0;

    ret = select(fd_ + 1, NULL, &writeFds, NULL, &tv);

    if (ret == 0) {
      message(-2, "CDDB: Timeout while sending data.");
      err = 1; goto fail;
    }
    
    if (ret < 0) {
      message(-2, "CDDB: Error while waiting for send: %s", strerror(errno));
      err = 1; goto fail;
    }
 
    ret = write(fd_, p, 1);

    if (ret < 0) {
      message(-2, "CDDB: Failed to send command '%s': %s", cmd,
	      strerror(errno));
      err = 1; goto fail;
    }

    if (ret != 1) {
      message(-2, "CDDB: Failed to send command '%s'.", cmd);
      err = 1; goto fail;
    }

    len--;
    p++;
  }

  message(4, "CDDB: Ok.");

  fail:
  delete[] cmd;

  return err;
}

static unsigned int cddbSum(unsigned int n)
{
  unsigned int ret;

  ret = 0;
  while (n > 0) {
    ret += (n % 10);
    n /= 10;
  }

  return ret;
}

const char *Cddb::calcCddbId()
{
  const Track *t;
  Msf start, end;
  unsigned int n = 0;
  unsigned int o = 0;
  int tcount = 0;
  static char buf[20];
  unsigned long id;

  TrackIterator itr(toc_);

  for (t = itr.first(start, end); t != NULL; t = itr.next(start, end)) {
    if (t->type() == TrackData::AUDIO) {
      n += cddbSum(start.min() * 60 + start.sec() + 2/* gap offset */);
      o  = end.min() * 60 + end.sec();
      tcount++;
    }
  }

  id = (n % 0xff) << 24 | o << 8 | tcount;
  sprintf(buf, "%08lx", id);

  return buf;
} 

static void convertEscapeSequences(const char *in, char *out)
{
  while (*in != 0) {
    if (*in == '\\') {
      switch (*(in + 1)) {
      case 'n':
	*out++ = '\n';
	in++;
	break;

      case 't':
	*out++ = '\t';
	in++;
	break;

      case '\\':
	*out++ = '\\';
	in++;
	break;

      default:
	*out++ = '\\';
	break;
      }
    }
    else {
      *out++ = *in;
    }
	
    in++;
  }

  *out = 0;
}

/* Retrieves the category, disk ID and title from a query response string.
 * The provided strings 'category', 'diskId' and 'title' must point to
 * existing buffers with at least the same length as 'line'.
 * Return: 1 if 'line' was successfully parsed, else 0
 */
static int parseQueryResult(char *line, char *category, char *diskId,
			    char *title)
{
  char *sep = " \t";
  char *p;
  
  if ((p = strtok(line, sep)) != NULL) {
    strcpy(category, p);

    if ((p = strtok(NULL, sep)) != NULL) {
      strcpy(diskId, p);

      if ((p = strtok(NULL, "")) != NULL) {
	// remove leading white space
	while (*p != 0 && isspace(*p))
	  p++;

	convertEscapeSequences(p, title);

	// remove newline from title string
	if ((p = strchr(title, '\n')) != NULL)
	  *p = 0;

	return 1;
      }
    }
  }

  return 0;
}

/* Reads a CDDB record from 'fd_' and fills 'cddbEntry_' with the required
 * data.
 * Return: 0: OK
 *         1: communication error occured
 */
int Cddb::readDbEntry(int localRecordFd)
{
  const char *resp;
  char buf[CDDB_MAX_LINE_LEN];
  char *line;
  char *p, *s;
  char *val;
  int ntracks = toc_->nofTracks();
  int i, trackNr;
  
  cddbEntry_ = new CddbEntry;

  cddbEntry_->diskTitle = NULL;
  cddbEntry_->diskArtist = NULL;
  cddbEntry_->diskExt = NULL;
  cddbEntry_->ntracks = ntracks;
  cddbEntry_->trackTitles = new (char*)[ntracks];
  cddbEntry_->trackExt = new (char*)[ntracks];

  for (i = 0; i < ntracks; i++) {
    cddbEntry_->trackTitles[i] = NULL;
    cddbEntry_->trackExt[i] = NULL;
  }
  

  
  while ((resp = readLine()) != NULL && strcmp(resp, ".") != 0) {
    message(4, "CDDB: READ data: %s", resp);

    if (localRecordFd >= 0) {
      // save to local CDDB record file
      fullWrite(localRecordFd, resp, strlen(resp));
      fullWrite(localRecordFd, "\n", 1);
    }

    convertEscapeSequences(resp, buf);

    // remove comments
    if ((p = strchr(buf, '#')) != NULL)
      *p = 0;

    // remove leading blanks
    for (line = buf; *line != 0 && isspace(*line); line++) ;

    if ((val = strchr(line, '=')) != NULL) {
      *val = 0;
      val++;

      if (strcmp(line, "DTITLE") == 0) {
	if (*val != 0) {
	  if (cddbEntry_->diskArtist == NULL) {
	    cddbEntry_->diskArtist = strdupCC(val);
	  }
	  else {
	    s = strdup3CC(cddbEntry_->diskArtist, val, NULL);
	    delete[] cddbEntry_->diskArtist;
	    cddbEntry_->diskArtist = s;
	  }
	}
      }
      else if (strcmp(line, "EXTD") == 0) {
	if (*val != 0) {
	  if (cddbEntry_->diskExt == NULL) {
	    cddbEntry_->diskExt = strdupCC(val);
	  }
	  else {
	    s = strdup3CC(cddbEntry_->diskExt, val, NULL);
	    delete[] cddbEntry_->diskExt;
	    cddbEntry_->diskExt = s;
	  }
	}
      }
      else if (strncmp(line, "TTITLE", 6) == 0) {
	if (*val != 0) {
	  trackNr = atoi(line + 6);
	  if (trackNr >= 0 && trackNr < ntracks) {
	    if (cddbEntry_->trackTitles[trackNr] == NULL) {
	      cddbEntry_->trackTitles[trackNr] = strdupCC(val);
	    }
	    else {
	      s = strdup3CC(cddbEntry_->trackTitles[trackNr], val, NULL);
	      delete[] cddbEntry_->trackTitles[trackNr];
	      cddbEntry_->trackTitles[trackNr] = s;
	    }
	  }
	}
      }
      else if (strncmp(line, "EXTT", 4) == 0) {
	if (*val != 0) {
	  trackNr = atoi(line + 4);
	  if (trackNr >= 0 && trackNr < ntracks) {
	    if (cddbEntry_->trackExt[trackNr] == NULL) {
	      cddbEntry_->trackExt[trackNr] = strdupCC(val);
	    }
	    else {
	      s = strdup3CC(cddbEntry_->trackExt[trackNr], val, NULL);
	      delete[] cddbEntry_->trackExt[trackNr];
	      cddbEntry_->trackExt[trackNr] = s;
	    }
	  }
	}
      }
    }
  }

  if (resp == NULL) {
    message(-2, "CDDB: EOF while reading database entry.");
    goto fail;
  }


  if (cddbEntry_->diskArtist != NULL) {
    if ((p = strchr(cddbEntry_->diskArtist, '/')) != NULL) {
      *p = 0;

      // remove leading white space of disk title
      for (s = p + 1; *s != 0 && isspace(*s); s++) ;
      
      cddbEntry_->diskTitle = strdupCC(s);

      // remove trailing white space of disk artist
      for (p = p - 1; p >= cddbEntry_->diskArtist && isspace(*p); p--)
	*p = 0;
    }
    else {
      cddbEntry_->diskTitle = strdupCC(cddbEntry_->diskArtist);
    }
  }

  return 0;

 fail:
  clearCddbEntry();

  return 1;
}


int Cddb::createLocalCddbFile(const char *category, const char *diskId)
{
  char *categoryDir = NULL;
  char *recordFile = NULL;
  struct stat sbuf;
  int ret;
  int fd = -1;

  if (localCddbDirectory_ == NULL)
    return -1;

  ret = stat(localCddbDirectory_, &sbuf);

  if (ret != 0 && errno == ENOENT) {
    message(-1, "CDDB: Local CDDB directory \"%s\" does not exist.",
	    localCddbDirectory_);
    return -1;
  }
  else if (ret == 0) {
    if (!S_ISDIR(sbuf.st_mode)) {
      message(-2, "CDDB: \"%s\" is not a directory.", localCddbDirectory_);
      return -1;
    }
  }
  else {
    message(-2, "CDDB: stat of \"%s\" failed: %s", localCddbDirectory_,
	    strerror(errno));
    return -1;
  }

  categoryDir = strdup3CC(localCddbDirectory_, "/", category);

  ret = stat(categoryDir, &sbuf);

  if (ret != 0 && errno == ENOENT) {
    if (mkdir(categoryDir, 0777) != 0) {
      message(-2, "CDDB: Cannot create directory \"%s\": %s", categoryDir,
	      strerror(errno));
      goto fail;
    }
  }
  else if (ret == 0) {
    if (!S_ISDIR(sbuf.st_mode)) {
      message(-2, "CDDB: \"%s\" is not a directory.", categoryDir);
    }
  }
  else {
    message(-2, "CDDB: stat of \"%s\" failed: %s", categoryDir,
	    strerror(errno));
    goto fail;
  }

  recordFile = strdup3CC(categoryDir, "/", diskId);
  
  ret = stat(recordFile, &sbuf);

  if (ret != 0 && errno == ENOENT) {
    if ((fd = open(recordFile, O_WRONLY|O_CREAT, 0666)) < 0) {
      message(-2, "CDDB: Cannot create CDDB record file \"%s\": %s",
	      recordFile, strerror(errno));
      fd = -1;
      goto fail;
    }
  }
  else if (ret == 0) {
    fd = -2;
    goto fail;
  }
  else {
    message(-2, "CDDB: stat of \"%s\" failed: %s", categoryDir,
	    strerror(errno));
    goto fail;
  }
  
 fail:

  delete[] categoryDir;
  delete[] recordFile;

  return fd;
}
