/*
 * AdPlug - Replayer for many OPL2/OPL3 audio file formats.
 * Copyright (c) 1999 - 2006 Simon Peter <dn.tlp@gmx.net>, et al.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * database.cpp - AdPlug database class
 * Copyright (c) 2002 Riven the Mage <riven@ok.ru>
 * Copyright (c) 2002, 2003, 2006 Simon Peter <dn.tlp@gmx.net>
 */

#include <binio.h>
#include <binfile.h>
#include <string.h>

#include "database.h"

#define DB_FILEID_V10	"AdPlug Module Information Database 1.0\x10"

/***** CAdPlugDatabase *****/

const unsigned short CAdPlugDatabase::hash_radix = 0xfff1;	// should be prime

CAdPlugDatabase::CAdPlugDatabase()
  : linear_index(0), linear_logic_length(0), linear_length(0)
{
  db_linear = new DB_Bucket * [hash_radix];
  db_hashed = new DB_Bucket * [hash_radix];
  memset(db_linear, 0, sizeof(DB_Bucket *) * hash_radix);
  memset(db_hashed, 0, sizeof(DB_Bucket *) * hash_radix);
}

CAdPlugDatabase::~CAdPlugDatabase()
{
  unsigned long i;

  for(i = 0; i < linear_length; i++)
    delete db_linear[i];

  delete [] db_linear;
  delete [] db_hashed;
}

bool CAdPlugDatabase::load(std::string db_name)
{
  binifstream f(db_name);
  if(f.error()) return false;
  return load(f);
}

bool CAdPlugDatabase::load(binistream &f)
{
  unsigned int idlen = strlen(DB_FILEID_V10);
  char *id = new char [idlen];
  unsigned long length;

  // Open database as little endian with IEEE floats
  f.setFlag(binio::BigEndian, false); f.setFlag(binio::FloatIEEE);

  f.readString(id,idlen);
  if(memcmp(id,DB_FILEID_V10,idlen)) {
    delete [] id;
    return false;
  }
  delete [] id;
  length = f.readInt(4);

  // read records
  for(unsigned long i = 0; i < length; i++) {
    CRecord *rec = CRecord::factory(f);
    if (!insert(rec))
      delete rec;
  }

  return true;
}

bool CAdPlugDatabase::save(std::string db_name)
{
  binofstream f(db_name);
  if(f.error()) return false;
  return save(f);
}

bool CAdPlugDatabase::save(binostream &f)
{
  unsigned long i;

  // Save database as little endian with IEEE floats
  f.setFlag(binio::BigEndian, false); f.setFlag(binio::FloatIEEE);

  f.writeString(DB_FILEID_V10);
  f.writeInt(linear_logic_length, 4);

  // write records
  for(i = 0; i < linear_length; i++)
    if(!db_linear[i]->deleted)
      db_linear[i]->record->write(f);

  return true;
}

CAdPlugDatabase::CRecord *CAdPlugDatabase::search(CKey const &key)
{
  if(lookup(key)) return get_record(); else return 0;
}

bool CAdPlugDatabase::lookup(CKey const &key)
{
  unsigned long index = make_hash(key);
  if(!db_hashed[index]) return false;

  // immediate hit ?
  DB_Bucket *bucket = db_hashed[index];

  if(!bucket->deleted && bucket->record->key == key) {
    linear_index = bucket->index;
    return true;
  }

  // in-chain hit ?
  bucket = db_hashed[index]->chain;

  while(bucket) {
    if(!bucket->deleted && bucket->record->key == key) {
      linear_index = bucket->index;
      return true;
    }

    bucket = bucket->chain;
  }

  return false;
}

bool CAdPlugDatabase::insert(CRecord *record)
{
  long index;

  // sanity checks
  if(!record) return false;			// null-pointer given
  if(linear_length == hash_radix) return false;	// max. db size exceeded
  if(lookup(record->key)) return false;		// record already in db

  // make bucket
  DB_Bucket *bucket = new DB_Bucket(linear_length, record);
  if(!bucket) return false;

  // add to linear list
  db_linear[linear_length] = bucket;
  linear_logic_length++; linear_length++;

  // add to hashed list
  index = make_hash(record->key);

  if(!db_hashed[index])	// First entry in hashtable
    db_hashed[index] = bucket;
  else {		// Add entry in chained list
    DB_Bucket *chain = db_hashed[index];

    while(chain->chain) chain = chain->chain;
    chain->chain = bucket;
  }

  return true;
}

void CAdPlugDatabase::wipe(CRecord *record)
{
  if(!lookup(record->key)) return;
  wipe();
}

void CAdPlugDatabase::wipe()
{
  if(!linear_length) return;

  DB_Bucket *bucket = db_linear[linear_index];

  if(!bucket->deleted) {
    delete bucket->record;
    linear_logic_length--;
    bucket->deleted = true;
  }
}

CAdPlugDatabase::CRecord *CAdPlugDatabase::get_record()
{
  if(!linear_length) return 0;
  return db_linear[linear_index]->record;
}

bool CAdPlugDatabase::go_forward()
{
  if(linear_index + 1 < linear_length) {
    linear_index++;
    return true;
  } else
    return false;
}

bool CAdPlugDatabase::go_backward()
{
  if(!linear_index) return false;
  linear_index--;
  return true;
}

void CAdPlugDatabase::goto_begin()
{	
  if(linear_length) linear_index = 0;
}

void CAdPlugDatabase::goto_end()
{
  if(linear_length) linear_index = linear_length - 1;
}

inline unsigned long CAdPlugDatabase::make_hash(CKey const &key)
{
  return (key.crc32 + key.crc16) % hash_radix;
}

/***** CAdPlugDatabase::DB_Bucket *****/

CAdPlugDatabase::DB_Bucket::DB_Bucket(unsigned long nindex, CRecord *newrecord, DB_Bucket *newchain)
  : index(nindex), deleted(false), chain(newchain), record(newrecord)
{
}

CAdPlugDatabase::DB_Bucket::~DB_Bucket()
{
  if(!deleted) delete record;
}

/***** CAdPlugDatabase::CRecord *****/

CAdPlugDatabase::CRecord *CAdPlugDatabase::CRecord::factory(RecordType type)
{
  switch(type) {
  case Plain: return new CPlainRecord;
  case SongInfo: return new CInfoRecord;
  case ClockSpeed: return new CClockRecord;
  default: return 0;
  }
}

CAdPlugDatabase::CRecord *CAdPlugDatabase::CRecord::factory(binistream &in)
{
  RecordType	type;
  unsigned long	size;
  CRecord	*rec;

  type = (RecordType)in.readInt(1); size = in.readInt(4);
  rec = factory(type);

  if(rec) {
    rec->key.crc16 = in.readInt(2); rec->key.crc32 = in.readInt(4);
    rec->filetype = in.readString('\0'); rec->comment = in.readString('\0');
    rec->read_own(in);
    return rec;
  } else {
    // skip this record, cause we don't know about it
    in.seek(size, binio::Add);
    return 0;
  }
}

void CAdPlugDatabase::CRecord::write(binostream &out)
{
  out.writeInt(type, 1);
  out.writeInt(get_size() + filetype.length() + comment.length() + 8, 4);
  out.writeInt(key.crc16, 2); out.writeInt(key.crc32, 4);
  out.writeString(filetype); out.writeInt('\0', 1);
  out.writeString(comment); out.writeInt('\0', 1);

  write_own(out);
}

bool CAdPlugDatabase::CRecord::user_read(std::istream &in, std::ostream &out)
{
  return user_read_own(in, out);
}

bool CAdPlugDatabase::CRecord::user_write(std::ostream &out)
{
  out << "Record type: ";
  switch(type) {
  case Plain: out << "Plain"; break;
  case SongInfo: out << "SongInfo"; break;
  case ClockSpeed: out << "ClockSpeed"; break;
  default: out << "*** Unknown ***"; break;
  }
  out << std::endl;
  out << "Key: " << std::hex << key.crc16 << ":" << key.crc32 << std::dec << std::dec << std::endl;
  out << "File type: " << filetype << std::endl;
  out << "Comment: " << comment << std::endl;

  return user_write_own(out);
}

/***** CAdPlugDatabase::CRecord::CKey *****/

CAdPlugDatabase::CKey::CKey(binistream &buf)
{
  make(buf);
}

bool CAdPlugDatabase::CKey::operator==(const CKey &key)
{
  return ((crc16 == key.crc16) && (crc32 == key.crc32));
}

void CAdPlugDatabase::CKey::make(binistream &buf)
// Key is CRC16:CRC32 pair. CRC16 and CRC32 calculation routines (c) Zhengxi
{
  static const unsigned short magic16 = 0xa001;
  static const unsigned long  magic32 = 0xedb88320;

  crc16 = 0; crc32 = 0xffffffffL;

  while(!buf.eof())
    {
      unsigned char byte = buf.readInt(1);

      for (int j=0;j<8;j++)
	{
	  if ((crc16 ^ byte) & 1)
	    crc16 = (crc16 >> 1) ^ magic16;
	  else
	    crc16 >>= 1;

	  if ((crc32 ^ byte) & 1)
	    crc32 = (crc32 >> 1) ^ magic32;
	  else
	    crc32 >>= 1;

	  byte >>= 1;
	}
    }

  crc16 &= 0xffff;
  crc32  = ~crc32;
  crc32 &= 0xffffffffL;
}

/***** CInfoRecord *****/

CInfoRecord::CInfoRecord()
{
  type = SongInfo;
}

void CInfoRecord::read_own(binistream &in)
{
  title = in.readString('\0');
  author = in.readString('\0');
}

void CInfoRecord::write_own(binostream &out)
{
  out.writeString(title); out.writeInt('\0', 1);
  out.writeString(author); out.writeInt('\0', 1);
}

unsigned long CInfoRecord::get_size()
{
  return title.length() + author.length() + 2;
}

bool CInfoRecord::user_read_own(std::istream &in, std::ostream &out)
{
  out << "Title: "; in >> title;
  out << "Author: "; in >> author;
  return true;
}

bool CInfoRecord::user_write_own(std::ostream &out)
{
  out << "Title: " << title << std::endl;
  out << "Author: " << author << std::endl;
  return true;
}

/***** CClockRecord *****/

CClockRecord::CClockRecord()
  : clock(0.0f)
{
  type = ClockSpeed;
}

void CClockRecord::read_own(binistream &in)
{
  clock = in.readFloat(binio::Single);
}

void CClockRecord::write_own(binostream &out)
{
  out.writeFloat(clock, binio::Single);
}

unsigned long CClockRecord::get_size()
{
  return 4;
}

bool CClockRecord::user_read_own(std::istream &in, std::ostream &out)
{
  out << "Clockspeed: "; in >> clock;
  return true;
}

bool CClockRecord::user_write_own(std::ostream &out)
{
  out << "Clock speed: " << clock << " Hz" << std::endl;
  return true;
}
