/*
 * daap_client.c: The daap client (wrapper to the opendaap library)
 *
 * See the README file for copyright information and how to reach the author.
 *
 * $Id: daap_client.c,v 1.56 2005/10/31 14:29:13 cm Exp $
 */

#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <ctype.h>
#include <limits.h>
#include "daap_client.h"
#include "daap_config.h"

extern cDaapConfiguration oConfig;

// --- fStatusCallback ---------------------------------------------------------

static void fStatusCallback(DAAP_SClient *daap, DAAP_Status state, int num, void *ptr) {
#ifdef DEBUG
  char *p_state = NULL;
#endif
  cDaapClient *po_client = (cDaapClient *)ptr;

  switch (state) {
    case DAAP_STATUS_error:
#ifdef DEBUG
      p_state = "error";
#endif
      break;
    case DAAP_STATUS_dying:
#ifdef DEBUG
      p_state = "dying";
#endif
      break;
    case DAAP_STATUS_idle:
#ifdef DEBUG
      p_state = "idle";
#endif
      break;
    case DAAP_STATUS_connecting:
#ifdef DEBUG
      p_state = "connecting";
#endif
      break;
    case DAAP_STATUS_negotiating:
#ifdef DEBUG
      p_state = "negotiating";
#endif
      break;
    case DAAP_STATUS_updating:
#ifdef DEBUG
      p_state = "updating";
#endif
      break;
    case DAAP_STATUS_downloading:
#ifdef DEBUG
      p_state = "downloading";
#endif
      break;
    case DAAP_STATUS_hostschanged:
#ifdef DEBUG
      p_state = "hostschanged";
#endif
      po_client->Hosts()->Enumerate(daap);
      break;
    default:
#ifdef DEBUG
      p_state = "none";
#endif
      break;
  }

#ifdef DEBUG
  printf("%.16s (%d) callback state=%s num=%i ptr=%p\n", __FILE__, __LINE__, p_state, num, ptr);
#endif
}

// --- cDaapClient -------------------------------------------------------------

cDaapClient::cDaapClient() : mo_hosts(this) {
  mp_daap = NULL;
}

cDaapClient::~cDaapClient() {
  if (NULL != mp_daap)
    Destroy();
}

// returns 0 if successful
int cDaapClient::Create(void) {
#ifdef DEBUG
  printf("%.16s (%d) create daap\n", __FILE__, __LINE__);
#endif

  if (NULL == mp_daap)
    mp_daap = DAAP_Client_Create(fStatusCallback, this);

  return NULL != mp_daap ? 0 : -1;
}

// returns 0 if successful
int cDaapClient::Destroy(void) {
  int rval = -1;

#ifdef DEBUG
  printf("%.16s (%d) destroy daap\n", __FILE__, __LINE__);
#endif

  if (NULL != mp_daap) {
    rval = DAAP_Client_Release(mp_daap);
    mp_daap = NULL;
  }

  return rval;
}

// --- fEnumerateHostsCallback -------------------------------------------------

static int fEnumerateHostsCallback(DAAP_SClient *daap, DAAP_SClientHost *host, void *ptr) {
  bool found = false;
  char share[129];

  if (NULL != host && NULL != ptr) {
    cDaapHosts *po_hosts = (cDaapHosts *)ptr;

    DAAP_Client_AddRef(daap);

    memset(share, 0x00, 129);
    if (0 != po_hosts->Count() &&
        0 == DAAP_ClientHost_GetSharename(host, share, 128)) {

      for (int i = 0; i < po_hosts->Count() && ! found; i++)
        if (0 == strcasecmp(po_hosts->Item(i)->GetShare(), share)) {
          po_hosts->Item(i)->SetRunning(true);
          found = true;
        }
    }

    DAAP_Client_Release(daap);

    if (! found) {
      po_hosts->Add(new cDaapHost(po_hosts->Client(), host));
#ifdef DEBUG
      printf("%.16s (%d) enumerate share=%s\n", __FILE__, __LINE__, 0 != strnlen(share, 129) ? share : "(na)");
    } else
      printf("%.16s (%d) enumerate share=%s (already present)\n", __FILE__, __LINE__, share);
#else
    }
#endif
  }

  // return 0 to interrupt, != 0 to continue
  return 1;
}

// --- cDaapHosts --------------------------------------------------------------

cDaapHosts::cDaapHosts(cDaapClient *parent) {
  mpo_parent = parent;
}

cDaapHosts::~cDaapHosts() {
}

void cDaapHosts::Enumerate(DAAP_SClient *daap) {
  DAAP_Client_AddRef(daap);

  for (int i = 0; i < mo_hosts.Count(); i++)
    mo_hosts.Item(i)->SetRunning(false);

  DAAP_Client_EnumerateHosts(daap, fEnumerateHostsCallback, this);

  DAAP_Client_Release(daap);
}

cDaapHost *cDaapHosts::Add(cDaapHost *host) {
  return mo_hosts.Add(host);
}

void cDaapHosts::Delete(cDaapHost *host) {
  int index = mo_hosts.Contains(host);

  if (-1 != index)
    mo_hosts.Delete(index);
}

// --- cDaapHost ---------------------------------------------------------------

cDaapHost::cDaapHost(cDaapClient *parent, DAAP_SClientHost *host) {
  mpo_parent = parent;
  mp_scHost = host;
  mpo_dbs = NULL;
  m_running = true;

  DAAP_ClientHost_AddRef(host);

  Initialize();
}

cDaapHost::~cDaapHost() {
  if (NULL != mpo_dbs) {
    delete mpo_dbs;
    DAAP_ClientHost_Disconnect(mp_scHost);
  }

  DAAP_ClientHost_Release(mp_scHost);
}

void cDaapHost::Initialize(void) {
  memset(m_share, 0x00, 129);
  DAAP_ClientHost_GetSharename(mp_scHost, m_share, 128);

  memset(m_password, 0x00, 129);
}

const char *cDaapHost::SetPassword(char *pwd) {
#ifdef DEBUG
  printf("%.16s (%d) host password=%s\n", __FILE__, __LINE__, pwd);
#endif

  DAAP_ClientHost_SetPassword(mp_scHost, pwd);
  return strncpy(m_password, pwd, 128);
}

bool cDaapHost::SetRunning(bool set) {
  return m_running = set;
}

cDaapDatabases *cDaapHost::Databases() {
  unsigned int rval = 0;

  if (! m_running)
    return NULL;

  if (NULL == mpo_dbs) {
    if (0 < strnlen(m_password, 129) && 129 != strnlen(m_password, 129))
      DAAP_ClientHost_SetPassword(mp_scHost, m_password);

    rval = DAAP_ClientHost_Connect(mp_scHost);

    if (0 == rval)
      mpo_dbs = new cDaapDatabases(this, mp_scHost);
    else if (-401 == (int)rval) {
      delete mpo_dbs;
      mpo_dbs = NULL;
    }

#ifdef DEBUG
    printf("%.16s (%u) host rval=%u\n", __FILE__, __LINE__, rval);
#endif
  }

  return mpo_dbs;
}

// --- cDaapDatabases ----------------------------------------------------------

cDaapDatabases::cDaapDatabases(cDaapHost *parent, DAAP_SClientHost *host) {
  mpo_parent = parent;
  mp_scHost = host;
  DAAP_ClientHost_AddRef(host);

  Load();
}

cDaapDatabases::~cDaapDatabases() {
  DAAP_ClientHost_Release(mp_scHost);
}

void cDaapDatabases::Load(void) {
  int numOfItems = 0;
  unsigned int rval = 0, size = 8 * sizeof(DAAP_ClientHost_Database);
  DAAP_ClientHost_Database *p_chDB = NULL;

  do {
    if (NULL != p_chDB) {
      free(p_chDB);
      size = rval;
      rval = 0;
    }

    p_chDB = (DAAP_ClientHost_Database *)malloc(size);
    if (NULL != p_chDB)
      rval = DAAP_ClientHost_GetDatabases(mp_scHost, p_chDB, &numOfItems, size);
  } while (rval != 0);
  
#ifdef DEBUG
  printf("%.16s (%d) databases numOfItems=%d\n", __FILE__, __LINE__, numOfItems);
#endif

  for (int i = 0; i < numOfItems; i++) {
    mo_dbs.Add(new cDaapDatabase(mpo_parent, mp_scHost, p_chDB[i].name, p_chDB[i].id));
#ifdef DEBUG
    printf("%.16s (%d) databases id=%d name=%s\n", __FILE__, __LINE__, p_chDB[i].id, p_chDB[i].name);
#endif
  }

  free(p_chDB);
}

// --- cDaapDatabase -----------------------------------------------------------

// the "master" database will initialize all structures by itself: mpo_items
// (dbe) and mpo_playlists (pls).
// filtered databases (method Filter()) will get a new cMediaDatabaseEnge
// by the method Filter(). but all the items/playlists always point to the same
// items in the master db.
cDaapDatabase::cDaapDatabase(cDaapHost *parent, DAAP_SClientHost *host, const char *name, int id, cMediaDatabaseEngine *dbe, cDaapPlaylists *pls) {
  mpo_parent = parent;
  mp_scHost = host;
  m_copy = false;
  m_initialized = false;

  DAAP_ClientHost_AddRef(mp_scHost);

  m_id = id;  
  if (NULL != name)
    mp_name = strdup(name);
  else
    mp_name = NULL;

  if (NULL == dbe && NULL == pls) {
    mpo_playlists = new cDaapPlaylists(this, host);
    mpo_items = new cMediaDatabaseEngine();
  } else {
    mpo_items = dbe;
    mpo_playlists = pls;
    m_copy = true;
    m_initialized = true;
  }
}

cDaapDatabase::~cDaapDatabase() {
  if (! m_copy)    
    delete mpo_playlists;

  // the items can always be delete'd, because the underlaying engine will take
  // care of the referencing object's and deletes them when necessary
  delete mpo_items;

  free(mp_name);

  DAAP_ClientHost_Release(mp_scHost);
}

void cDaapDatabase::Load(void) {
  int rval = 0, size = 128 * sizeof(DAAP_ClientHost_DatabaseItem), numOfItems = 0;
  DAAP_ClientHost_DatabaseItem *p_chItems = NULL;

  if (! m_initialized && ! m_copy) { 
    do {
      if (NULL != p_chItems) {
        free(p_chItems);
        size = rval;
        rval = 0;
      }

      p_chItems = (DAAP_ClientHost_DatabaseItem *)malloc(size);

      if (NULL != p_chItems)
        rval = DAAP_ClientHost_GetDatabaseItems(mp_scHost, m_id, p_chItems, &numOfItems, size);
    } while (rval != 0 && rval != -1);

    for (int i = 0; i < numOfItems; i++)
      mpo_items->Add(new cDaapDatabaseItem(this, mp_scHost, &p_chItems[i]));

    m_initialized = true;
    free(p_chItems);
  }

#ifdef DEBUG
  printf("%.16s (%d) database numOfItems=%d\n", __FILE__, __LINE__, numOfItems);
#endif
}

cDaapDatabaseItem *cDaapDatabase::Item(unsigned int index) {
  if (! m_initialized)
    Load();

  return dynamic_cast<cDaapDatabaseItem *>(mpo_items->Item(index));
}

cDaapDatabase *cDaapDatabase::Sort(eDatabaseField id) {
  if (! m_initialized)
    Load();

  mpo_items->Sort(id);

  return this;
}

// returns a new allocated cDaapDatabase; only the caller will hold the
// pointer to it 
cDaapDatabase *cDaapDatabase::Filter(eDatabaseField field, const char *key) {
  int a = 0, z = 0, c = 0;
  cMediaDatabaseEngine *po_dbe = NULL;

  if (! m_initialized)
    Load();

  if (-1 != (c = mpo_items->BinSearch(field, key))) {
    a = z = c;
    // look at the items < c
    while (0 < a &&
           NULL != mpo_items->Item(a-1)->GetValue(field) &&
           0 == strcasecmp(key, mpo_items->Item(a-1)->GetValue(field)))
      a--;

    // also in the other direction
    while ((mpo_items->Count()-1) > z &&
           NULL != mpo_items->Item(z+1)->GetValue(field) &&
           0 == strcasecmp(key, mpo_items->Item(z+1)->GetValue(field)))
      z++;

#ifdef DEBUG
    printf("%.16s (%d) [%d-%d][%d:%d]\n", __FILE__, __LINE__, a, z, mpo_items->Item(a)->GetId(), mpo_items->Item(z)->GetId());
#endif
    // the new cDaapDatabase object will get ownership
    po_dbe = new cMediaDatabaseEngine(false);

    if (NULL != po_dbe) {
      for (c = a; c <= z; c++)
        po_dbe->Add(mpo_items->Item(c));
    }
  }

  return new cDaapDatabase(mpo_parent, mp_scHost, mp_name, m_id, po_dbe, mpo_playlists); 
}

int cDaapDatabase::FindIndexByChar(eDatabaseField field, char key, bool caseSensitive) {
  int c = 0;

  if (! m_initialized)
    Load();

  if (-1 != (c = mpo_items->BinSearch(field, key, caseSensitive))) {
    if (! caseSensitive)
      key = tolower(key);

    // lets look if the items before us also match our key
    while (0 < c &&
          NULL != mpo_items->Item(c - 1)->GetValue(field) &&
          key == (caseSensitive ? mpo_items->Item(c - 1)->GetValue(field)[0] : tolower(mpo_items->Item(c - 1)->GetValue(field)[0])))
      c--;
  }

  return c;
}

int cDaapDatabase::FindIndexById(int key) {
  if (! m_initialized)
    Load();

  return mpo_items->BinSearch(dfId, key);
}

int cDaapDatabase::Count(void) {
  if (! m_initialized)
    Load();

  return mpo_items->Count();
}

// --- cDaapDatabaseItem -------------------------------------------------------

cDaapDatabaseItem::cDaapDatabaseItem(cDaapDatabase *parent, DAAP_SClientHost *host, const DAAP_ClientHost_DatabaseItem *item) :
  m_id(0), mp_itemName(NULL), mp_songAlbum(NULL), mp_songArtist(NULL),
  m_songBeatsPerMinute(0), m_songBitRate(0), m_songDiscCount(0), m_songDiscNumber(0),
  mp_songGenre(NULL), m_songSampleRate(0), m_songSize(0), m_songTime(0),
  m_songTrackCount(0), m_songTrackNumber(0), m_songUserRating(0), m_songYear(0),
  mp_songFormat(NULL), mp_scHost(host), mpo_parent(parent) {

  if (NULL != item) {
    m_id = item->id;
    m_songBeatsPerMinute = item->songbeatsperminute;
    m_songBitRate = item->songbitrate;
    m_songDiscCount = item->songdisccount;
    m_songDiscNumber = item->songdiscnumber;
    m_songSampleRate = item->songsamplerate;
    m_songSize = item->songsize;
    m_songTime = item->songtime;
    m_songTrackCount = item->songtrackcount;
    m_songTrackNumber = item->songtracknumber;
    m_songUserRating = item->songuserrating;
    m_songYear = item->songyear;

    if (NULL != item->itemname)
      mp_itemName = strdup(item->itemname);
    if (NULL != item->songalbum)
      mp_songAlbum = strdup(item->songalbum);
    if (NULL != item->songartist)
      mp_songArtist = strdup(item->songartist);
    if (NULL != item->songgenre)
      mp_songGenre = strdup(item->songgenre);
    if (NULL != item->songformat)
      mp_songFormat = strdup(item->songformat);
  }

  DAAP_ClientHost_AddRef(host);
}

cDaapDatabaseItem::~cDaapDatabaseItem() {
  free(mp_itemName);
  free(mp_songAlbum);
  free(mp_songArtist);
  free(mp_songGenre);
  free(mp_songFormat);

  DAAP_ClientHost_Release(mp_scHost);
}

const char *cDaapDatabaseItem::GetValue(eDatabaseField field) const {
  switch (field) {
    case dfArtist:
      return mp_songArtist;
      break;
    case dfAlbum:
      return mp_songAlbum;
      break;
    case dfGenre:
      return mp_songGenre;
      break;
    case dfName:
      return mp_itemName;
    default:
      break;
  }

  return NULL;
}

// caller must free returned blob
const cMediaDatabaseBlob *cDaapDatabaseItem::GetData(void) const {
  int rval = 0;
  cMediaDatabaseBlob *p_blob = NULL;
  DAAP_ClientHost_Song *po_song = new DAAP_ClientHost_Song;

  rval = DAAP_ClientHost_GetAudioFile(mp_scHost,
                                      mpo_parent->GetId(),
                                      m_id,
                                      mp_songFormat,
                                      po_song);

  if (0 == rval) {
    uint8_t *p = new uint8_t[sizeof(uint8_t) * po_song->size];

    if (NULL != p) {
      memcpy(p, po_song->data, po_song->size);

      p_blob = new cMediaDatabaseBlob(p, po_song->size);
    }

    rval = DAAP_ClientHost_FreeAudioFile(mp_scHost, po_song);
  }

  delete po_song;

  return p_blob;
}

// --- cDaapPlaylists ----------------------------------------------------------

cDaapPlaylists::cDaapPlaylists(cDaapDatabase *parent, DAAP_SClientHost *host) {
  mpo_parent = parent;;
  mp_scHost = host;

  DAAP_ClientHost_AddRef(host);
  Load();
}

cDaapPlaylists::~cDaapPlaylists() {
  DAAP_ClientHost_Release(mp_scHost);
}

void cDaapPlaylists::Load(void) {
  int numOfItems = 0;
  unsigned int rval = 0, size = 8 * sizeof(DAAP_ClientHost_DatabasePlaylist);
  DAAP_ClientHost_DatabasePlaylist *p_chPL = NULL;

  do {
    if (NULL != p_chPL) {
      free(p_chPL);
      size = rval;
      rval = 0;
    }

    p_chPL = (DAAP_ClientHost_DatabasePlaylist *)malloc(size);
    if (NULL != p_chPL)
      rval = DAAP_ClientHost_GetPlaylists(mp_scHost, mpo_parent->GetId(), p_chPL, &numOfItems, size);
  } while (rval != 0);

  // mp_pl.Item(0) is always the instant playlist. the following lists are
  // recieved by the previous call to libopendaap
  mo_pls.Add(new cDaapPlaylist(mpo_parent, NULL, oConfig.GetInstantPlaylistName(), INT_MAX, true));
  for (int i = 0; i < numOfItems; i++)
    mo_pls.Add(new cDaapPlaylist(mpo_parent, mp_scHost, p_chPL[i].itemname, p_chPL[i].id, false));

  free(p_chPL);
}

// --- cDaapPlaylist -----------------------------------------------------------

// the value of the parameter host can be NULL, eg for instant playlist/user
// defined (not stored on the server)
cDaapPlaylist::cDaapPlaylist(cDaapDatabase *parent, DAAP_SClientHost *host, const char *name, int id, bool writeable) {
  mpo_parent = parent;
  m_writeable = writeable;
  mp_scHost = host;
  m_id = id;

  mp_name = NULL;

  if (NULL != name)
    mp_name = strdup(name);

  if (NULL != host) {
    DAAP_ClientHost_AddRef(mp_scHost);
    m_initialized = false;
  } else
    m_initialized = true;
}

cDaapPlaylist::~cDaapPlaylist() {
  free(mp_name);

  if (NULL != mp_scHost)
    DAAP_ClientHost_Release(mp_scHost);
}

void cDaapPlaylist::Load(void) {
  int rval = 0, size = 128 * sizeof(DAAP_ClientHost_DatabasePlaylistItem), numOfItems = 0;
  DAAP_ClientHost_DatabasePlaylistItem *p_plItems = NULL;

  if (NULL != mp_scHost) {
    do {
      if (NULL != p_plItems) {
        free(p_plItems);
        size = rval;
        rval = 0;
      }

      p_plItems = (DAAP_ClientHost_DatabasePlaylistItem *)malloc(size);

      if (NULL != p_plItems)
        rval = DAAP_ClientHost_GetPlaylistItems(mp_scHost,
                                                mpo_parent->GetId(),
                                                m_id,
                                                p_plItems,
                                                &numOfItems,
                                                size);
    } while (rval != 0 && rval != -1);

    for (int i = 0; i < numOfItems; i++)
      mo_items.Add(new cDaapPlaylistItem(this, &p_plItems[i]));

    m_initialized = true;

    free(p_plItems);
  }

#ifdef DEBUG
  if (NULL == mp_scHost)
    printf("%.16s (%d) playlist user defined\n", __FILE__, __LINE__);
#endif
}

cDaapPlaylistItem *cDaapPlaylist::Item(unsigned int index) {
  if (! m_initialized)
    Load();

  return mo_items.Item(index);
}

// returns pointer to added item
// will take overship of item
cDaapPlaylistItem *cDaapPlaylist::Add(cDaapPlaylistItem *item) {
  cDaapPlaylistItem *p_pli = NULL;

  if (m_writeable && NULL != item) {
    if (! m_initialized)
      Load();

    p_pli = mo_items.Add(item);
  }

  return p_pli;
}

// returns -1 when cannot delete item, otherwise new numOfItems
int cDaapPlaylist::Delete(int index) {
  int rval = 0;

  if (! m_initialized)
    Load();

  if (m_writeable && (! m_lock || (m_lock && index > m_currentItem))) {
    rval = mo_items.Delete(index);

    if (-1 != rval && m_currentItem == mo_items.Count()) // numOfItems is already decr by 1
      m_currentItem--;
  }

  return rval;
}

// returns -1 when cannot move item, otherwise new position
int cDaapPlaylist::Move(int dest, int src) {
  int rval = -1;

  if (! m_initialized)
    Load();

  if (m_writeable && (! m_lock || (m_lock && dest > m_currentItem && src > m_currentItem))) {
    rval = mo_items.Move(dest, src);

    if (-1 != rval && m_currentItem == src)
      m_currentItem = dest;
  }

  return rval;
}

bool cDaapPlaylist::Clear(void) {
  bool rval = false;

  if (m_writeable && ! m_lock) {
    rval = mo_items.Clear();
    m_currentItem = -1;
  }

  return rval;
}

cDaapPlaylistItem *cDaapPlaylist::Current(void) {
  if (! m_initialized)
    Load();

  if (-1 != m_currentItem)
    return mo_items.Item(m_currentItem);

  return NULL;
}

// stores the current track; -1 is used to signal not initialized/
// reached the end of the playlist
int cDaapPlaylist::SetCurrent(int current) {
  if (! m_initialized)
    Load();

  if ((0 <= current && current < mo_items.Count()) || -1 == current)
    return m_currentItem = current;
  else
    return m_currentItem;
}

int cDaapPlaylist::Count(void) {
  if (! m_initialized)
    Load();

  return mo_items.Count();
}

bool cDaapPlaylist::Lock(void) {
  return m_lock = true;
}

bool cDaapPlaylist::Unlock(void) {
  return m_lock = false;
}

// --- cDaapPlaylistItem -------------------------------------------------------

cDaapPlaylistItem::cDaapPlaylistItem(cDaapPlaylist *parent, const DAAP_ClientHost_DatabasePlaylistItem *item) {
  Initialize(parent, item->songid);
}

cDaapPlaylistItem::cDaapPlaylistItem(cDaapPlaylist *parent, int songid) {
  Initialize(parent, songid);
}

void cDaapPlaylistItem::Initialize(cDaapPlaylist *parent, int songid) {
  mpo_parent = parent;
  m_songId = songid;
  m_songIndex = mpo_parent->Database()->FindIndexById(songid);
}

cDaapPlaylistItem::~cDaapPlaylistItem() {
}

int cDaapPlaylistItem::SetSongId(int songid) {
  return m_songId = songid;
}

const cMediaDatabaseBlob *cDaapPlaylistItem::GetData(void) const {
  if (-1 != m_songIndex)
    return mpo_parent->Database()->Item(m_songIndex)->GetData();

  return NULL;
}

int cDaapPlaylistItem::GetChannels(void) const {
  // FIXME: how can i get the number of channels ?
  return 2;
}

int cDaapPlaylistItem::GetSampleRate(void) const {
  if (-1 != m_songIndex)
    return mpo_parent->Database()->Item(m_songIndex)->GetSongSampleRate();

  return 0;
}

short cDaapPlaylistItem::GetBitRate(void) const {
  if (-1 != m_songIndex)
    return mpo_parent->Database()->Item(m_songIndex)->GetSongBitRate();

  return 0;
}

int cDaapPlaylistItem::GetTime(void) const {
  if (-1 != m_songIndex)
    return mpo_parent->Database()->Item(m_songIndex)->GetSongTime();

  return 0;
}

const char *cDaapPlaylistItem::GetFormat(void) const {
  if (-1 != m_songIndex)
    return mpo_parent->Database()->Item(m_songIndex)->GetSongFormat();

  return NULL;
}
