#include "common.h"
#include "db.h"
#include "log.h"
#include "sha1.h"
#include <cassert>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#include <ctime>

PosDb::PosDb(const char * fn)
	: log(fn)
{
	// read in the log
	for(std::list<LogEntry>::const_iterator p = log.begin(); p != log.end(); ++p)
		acceptLogEntry(*p);
}

void PosDb::acceptLogEntry(const LogEntry & l) {
	serial_to_object[l.serial] = l;
	switch(l.type) {
	case ET_HASH: {
		SHA1Hash h;
		h.set((unsigned char *)l.HashChange.hash);

		if(l.HashChange.add) {
			// verify hash not associated with anything
			for(ath_it p = account_to_hash.begin(); p != account_to_hash.end(); ++p) {
				if(p->second == h) {
					fprintf(stderr, "PosDb: Log associates a hash"
						" with more than one account! Aborting.");
					abort();
				}
			}
			account_to_hash.insert(std::pair<uint32_t, SHA1Hash>(l.HashChange.uid, h));
		} else {
			// verify hash associated with something
			ath_it p;
			for(p = account_to_hash.begin(); p != account_to_hash.end(); ++p) {
				if(p->second == h)
					break;
			}

			if(p == account_to_hash.end()) {
				fprintf(stderr, "PosDb: Log deassociates an"
					"unassociated hash! Aborting.");
				abort();
			} else {
				account_to_hash.erase(p);
			}
		}
		break;
	}

	case ET_TRANS: {
		atb_it q = account_to_balance.find(l.Transaction.uid);
		if(q == account_to_balance.end())
			account_to_balance[l.Transaction.uid] = l.Transaction.delta;
		else
			q->second += l.Transaction.delta;
		break;
	}

	case ET_SALE: {
		uts_it q = upc_to_stock.find(l.StockChange.upc);
		if(q == upc_to_stock.end())
			upc_to_stock[l.StockChange.upc] = l.StockChange.delta;
		else
			q->second += l.StockChange.delta;
		break;
	}

  case ET_PRICE: {
    upc_to_price[l.PriceChange.upc] = l.PriceChange.price;
    break;
  }

  case ET_ADDUPC: {
    upc_to_name[l.AddUPC.upc] = l.AddUPC.name;
    break;
  }

	case ET_REVERT: {
		sto_it q = serial_to_object.find(l.Revert.revert_serial);
		if((q == serial_to_object.end()) || (q->second.type != ET_TRANS)) {
			fprintf(stderr, "PosDb: Log reverts an invalid object! Aborting.");
			abort();
		} else {
			account_to_balance[q->second.Transaction.uid] -= q->second.Transaction.delta;
		}
		break;
	}

	}
}

uint32_t PosDb::getAccountFromHash(const SHA1Hash & hash) {
	ath_it p;
	for(p = account_to_hash.begin(); p != account_to_hash.end(); ++p) {
		if(p->second == hash)
			break;
	}
	if(p == account_to_hash.end())
		return 0;
	else
		return p->first;
}

std::list<std::string> PosDb::getHashesFromAccount(uint32_t account) {
	std::list<std::string> result;

	ath_it_r range = account_to_hash.equal_range(account);
	for(ath_it p = range.first; p != range.second; ++p)
		result.push_back(p->second.toHex());

	return result;
}

std::list<std::string> PosDb::getHashesFromAccount(std::string account) {
	return getHashesFromAccount(ns.get_id(account));
}

int32_t PosDb::getAccountBalance(const SHA1Hash & hash) {
	uint32_t acct = getAccountFromHash(hash);

	if(!acct)
		return 0;
	else {
		atb_it p = account_to_balance.find(acct);
		if(p == account_to_balance.end())
			return 0;
		else
			return p->second;
	}
}

int32_t PosDb::getAccountBalance(uint32_t account) {
	atb_it p = account_to_balance.find(account);
	if(p == account_to_balance.end())
		return 0;
	else
		return p->second;
}

int32_t PosDb::getAccountBalance(std::string account) {
	return getAccountBalance(ns.get_id(account));
}

uint64_t PosDb::associateHash(const SHA1Hash & hash, uint32_t account) {
	// verify hash not associated with anything
	if(getAccountFromHash(hash)) {
		return 0;
	} else {
    LogEntry l = log.newLogEntry(ET_HASH);
		l.HashChange.uid = account;
		l.HashChange.add = true;
		hash.get((unsigned char *)l.HashChange.hash);

		acceptLogEntry(l);
		return log.writeEntry(l);
	}
}

uint64_t PosDb::associateHash(const SHA1Hash & hash, std::string account) {
	return associateHash(hash, ns.get_id(account));
}

uint64_t PosDb::deassociateHash(const SHA1Hash & hash) {
	// verify hash associated with something
	uint32_t acct = getAccountFromHash(hash);

	if(!acct) {
		return 0;
	} else {
    LogEntry l = log.newLogEntry(ET_HASH);
    l.HashChange.uid = acct;
    l.HashChange.add = false;
    hash.get((unsigned char *)l.HashChange.hash);

    acceptLogEntry(l);
    return log.writeEntry(l);
	}
}

int32_t PosDb::getUPCPrice(UPC upc) {
  uts_it it = upc_to_price.find(upc);
  if (it == upc_to_price.end()) {
    return 0;
  }
  return it->second;
}

uint64_t PosDb::setUPCPrice(UPC upc, int32_t price) {
  LogEntry l = log.newLogEntry(ET_PRICE);
  l.PriceChange.upc = upc;
  l.PriceChange.price = price;

  acceptLogEntry(l);
  return log.writeEntry(l);
}

// NOTE: this in theory could be deprecated. The client should
//       probably get the uid from the hash before doing a transaction.
uint64_t PosDb::doTransaction(const SHA1Hash & hash, int32_t delta) {
	// verify hash associated with something
	uint32_t acct = getAccountFromHash(hash);

	if(!acct) {
		return 0;
	} else {
    LogEntry l = log.newLogEntry(ET_TRANS);
		l.Transaction.delta = delta;
		l.Transaction.uid = acct;

		acceptLogEntry(l);
		return log.writeEntry(l);
	}
}

uint64_t PosDb::doTransaction(const uint32_t account, int32_t delta) {
  LogEntry l = log.newLogEntry(ET_TRANS);
	l.Transaction.delta = delta;
	l.Transaction.uid = account;

	acceptLogEntry(l);
	return log.writeEntry(l);
}

uint64_t PosDb::doTransaction(std::string account, int32_t delta) {
	return doTransaction(ns.get_id(account), delta);
}

uint64_t PosDb::revertTransaction(uint64_t serial) {
	if(serial_to_object.find(serial) == serial_to_object.end()) {
		return 0;
	} else {
		// do not double-revert transactions
		sto_it p;
		for(p = serial_to_object.begin(); p != serial_to_object.end(); ++p) {
			if((p->second.type == ET_REVERT) && (p->second.Revert.revert_serial == serial))
				break;
		}
		if(p != serial_to_object.end())
			return 0;

    LogEntry l = log.newLogEntry(ET_REVERT);
		l.Revert.revert_serial = serial;

		acceptLogEntry(l);
		return log.writeEntry(l);
	}
}

int32_t PosDb::getStock(UPC upc) {
	uts_it p = upc_to_stock.find(upc);
	if(p == upc_to_stock.end())
		return 0;
	else
		return p->second;
}

uint64_t PosDb::doStockChange(UPC upc, int32_t delta) {
  LogEntry l = log.newLogEntry(ET_SALE);
	l.StockChange.delta = delta;
	l.StockChange.upc = upc;

	acceptLogEntry(l);
	return log.writeEntry(l);
}

std::string PosDb::getUPCName(UPC upc) {
  std::map<UPC, std::string>::iterator it = upc_to_name.find(upc);
  if(it == upc_to_name.end()) return "UNKNOWN UPC";
  return upc_to_name[upc];
}


uint64_t PosDb::setUPCName (UPC upc, std::string name) {
  LogEntry l = log.newLogEntry(ET_ADDUPC);
  l.AddUPC.upc = upc;
  strncpy(l.AddUPC.name, name.c_str(), 32);
  l.AddUPC.name[32] = 0;

  acceptLogEntry(l);
  return log.writeEntry(l);
}
std::vector<std::string> PosDb::toString() {
  return log.toString();
}
