contract Manager is Pausable {
using SafeMath for uint;

mapping(address => bool) public ownerAccountsMap;
mapping(address => bool) public exchangeAccountsMap;

//SETTLEMENT PREPARATION####

enum SettlementPhase {
PREPARING, ONGOING, FINISHED
}

enum Cryptocurrency {
ETH, BBD
}

//Initially ready for a settlement
SettlementPhase public currentSettlementPhase = SettlementPhase.FINISHED;

uint public startingFeeBalance = 0;
uint public totalFeeFlows = 0;
uint public startingInsuranceBalance = 0;
uint public totalInsuranceFlows = 0;

uint public lastSettlementStartedTimestamp = 0;
uint public earliestNextSettlementTimestamp = 0;

mapping(uint => mapping(address => bool)) public custodiesServedETH;
mapping(uint => mapping(address => bool)) public custodiesServedBBD;

address public feeAccount;
address public insuranceAccount;
ERC20 public bbdToken;
CustodyStorage public custodyStorage;

address public custodyFactory;
uint public gweiBBDPriceInWei;
uint public lastTimePriceSet;
uint constant public gwei = 1000000000;

uint public maxTimeIntervalHB = 1 weeks;
uint public heartBeat = now;

constructor(address _feeAccount, address _insuranceAccount, address _bbdTokenAddress, address _custodyStorage,
address _serviceRegistryAddress) public {
//Contract creator is the first owner
ownerAccountsMap[msg.sender] = true;
feeAccount = _feeAccount;
insuranceAccount = _insuranceAccount;
bbdToken = ERC20(_bbdTokenAddress);
custodyStorage = CustodyStorage(_custodyStorage);
}

function() public payable {}

function setCustodyFactory(address _custodyFactory) external onlyOwner {
custodyFactory = _custodyFactory;
}

function pause() public onlyExchangeOrOwner {
paused = true;
}

function unpause() public onlyExchangeOrOwner {
paused = false;
}

modifier onlyAllowedInPhase(SettlementPhase _phase) {
require(currentSettlementPhase == _phase, "Not allowed in this phase");
_;
}

modifier onlyOwner() {
require(ownerAccountsMap[msg.sender] == true, "Only an owner can perform this action");
_;
}

modifier onlyExchange() {
require(exchangeAccountsMap[msg.sender] == true, "Only an exchange can perform this action");
_;
}

modifier onlyExchangeOrOwner() {
require(exchangeAccountsMap[msg.sender] == true ||
ownerAccountsMap[msg.sender] == true);
_;
}

function isDailySettlementOnGoing() external view returns (bool) {
return currentSettlementPhase != SettlementPhase.FINISHED;
}

function updateHeartBeat() external whenNotPaused onlyOwner {
heartBeat = now;
}

function isExchangeAlive() external view returns (bool) {
return now - heartBeat < maxTimeIntervalHB;
}

function addOwnerAccount(address _exchangeAccount) external onlyOwner {
ownerAccountsMap[_exchangeAccount] = true;
}

function addExchangeAccount(address _exchangeAccount) external onlyOwner whenNotPaused {
exchangeAccountsMap[_exchangeAccount] = true;
}

function rmExchangeAccount(address _exchangeAccount) external onlyOwner whenNotPaused {
exchangeAccountsMap[_exchangeAccount] = false;
}

function setBBDPrice(uint _priceInWei) external onlyExchangeOrOwner whenNotPaused
onlyAllowedInPhase(SettlementPhase.FINISHED) {
if(gweiBBDPriceInWei == 0) {
gweiBBDPriceInWei = _priceInWei;
} else {
//Max 100% daily increase in price
if(_priceInWei > gweiBBDPriceInWei) {
require(_priceInWei - gweiBBDPriceInWei <= (gweiBBDPriceInWei / 2));
//Max 50% daily decrease in price
} else if(_priceInWei < gweiBBDPriceInWei) {
require(gweiBBDPriceInWei - _priceInWei <= (gweiBBDPriceInWei / 2));
}
gweiBBDPriceInWei = _priceInWei;
}
//Price can only be set once per day
require(now - lastTimePriceSet > 23 hours);

lastTimePriceSet = now;
}

function createCustody(address _custody) external whenNotPaused onlyAllowedInPhase(SettlementPhase.FINISHED) {
require(msg.sender == custodyFactory);
custodyStorage.addCustody(_custody);
}

function removeCustody(address _custodyAddress, uint _arrayIndex) external whenNotPaused onlyExchangeOrOwner
onlyAllowedInPhase(SettlementPhase.FINISHED) {
custodyStorage.removeCustody(_custodyAddress, _arrayIndex);
}

/// @dev Exchange uses this function to withdraw ether from the contract
/// @param _amount to withdraw
/// @param _recipient to send withdrawn ether to
function withdrawFromManager(uint _amount, address _recipient) external onlyExchangeOrOwner
whenNotPaused onlyAllowedInPhase(SettlementPhase.FINISHED) {
_recipient.transfer(_amount);
}

/// @dev Users use this function to withdraw ether from their custody
/// @param _amount to withdraw
/// @param _custodyAddress to withdraw from
function withdrawFromCustody(uint _amount, address _custodyAddress,address _recipient) external onlyExchangeOrOwner
whenNotPaused onlyAllowedInPhase(SettlementPhase.FINISHED) {
Custody custody = Custody(_custodyAddress);
custody.withdraw(_amount, _recipient);
}

/// @dev Users use this function to withdraw ether from their custody
/// @param _tokenAddress of the ERC20 to withdraw from
/// @param _amount to withdraw
/// @param _custodyAddress to withdraw from
function withdrawTokensFromCustody(address _tokenAddress, uint _amount, address _custodyAddress, address _recipient)
external whenNotPaused onlyAllowedInPhase(SettlementPhase.FINISHED) onlyExchangeOrOwner {
Custody custody = Custody(_custodyAddress);
custody.transferToken(_tokenAddress, _recipient,_amount);
}

//DAILY SETTLEMENT

/// @dev This function prepares the daily settlement - resets all settlement
/// @dev scope storage variables to 0.
function startSettlementPreparation() external whenNotPaused onlyExchangeOrOwner
onlyAllowedInPhase(SettlementPhase.FINISHED) {
require(now > earliestNextSettlementTimestamp, "A settlement can happen once per day");
require(gweiBBDPriceInWei > 0, "BBD Price cannot be 0 during settlement");

lastSettlementStartedTimestamp = now;
totalFeeFlows = 0;
totalInsuranceFlows = 0;

currentSettlementPhase = SettlementPhase.ONGOING;


startingFeeBalance = feeAccount.balance +
((bbdToken.balanceOf(feeAccount) * gweiBBDPriceInWei) / gwei);

startingInsuranceBalance = insuranceAccount.balance;
}

/// @dev This function is used to process a batch of net eth flows, two arrays
/// @dev are pairs of custody addresses and the balance changes that should
/// @dev be executed. Transaction will revert if exchange rules are violated.
/// @param _custodies flow addresses
/// @param _flows flow balance changes (can be negative or positive)
/// @param _fee calculated and deducted from all batch flows
/// @param _insurance to be used
function settleETHBatch(address[] _custodies, int[] _flows, uint _fee, uint _insurance) external whenNotPaused onlyExchangeOrOwner
onlyAllowedInPhase(SettlementPhase.ONGOING) {

require(_custodies.length == _flows.length);

uint preBatchBalance = address(this).balance;

if(_insurance > 0) {
Insurance(insuranceAccount).useInsurance(_insurance);
}

for (uint flowIndex = 0; flowIndex < _flows.length; flowIndex++) {

//Every custody can be served ETH once during settlement
require(custodiesServedETH[lastSettlementStartedTimestamp][_custodies[flowIndex]] == false);

//All addresses must be custodies
require(custodyStorage.custodiesMap(_custodies[flowIndex]));

if (_flows[flowIndex] > 0) {
//10% rule
var outboundFlow = uint(_flows[flowIndex]);

//100% rule exception threshold
if(outboundFlow > 10 ether) {
//100% rule
require(getTotalBalanceFor(_custodies[flowIndex]) >= outboundFlow);
}

_custodies[flowIndex].transfer(uint(_flows[flowIndex]));

} else if (_flows[flowIndex] < 0) {
Custody custody = Custody(_custodies[flowIndex]);

custody.withdraw(uint(-_flows[flowIndex]), address(this));
}

custodiesServedETH[lastSettlementStartedTimestamp][_custodies[flowIndex]] = true;
}

if(_fee > 0) {
feeAccount.transfer(_fee);
totalFeeFlows = totalFeeFlows + _fee;
//100% rule for fee account
require(totalFeeFlows <= startingFeeBalance);
}

uint postBatchBalance = address(this).balance;

//Zero-sum guaranteed for ever batch
if(address(this).balance > preBatchBalance) {
uint leftovers = address(this).balance - preBatchBalance;
insuranceAccount.transfer(leftovers);
totalInsuranceFlows += leftovers;
//100% rule for insurance account
require(totalInsuranceFlows <= startingInsuranceBalance);
}
}

/// @dev This function is used to process a batch of net bbd flows, two arrays
/// @dev are pairs of custody addresses and the balance changes that should
/// @dev be executed. Transaction will revert if exchange rules are violated.
/// @param _custodies flow addresses
/// @param _flows flow balance changes (can be negative or positive)
/// @param _fee calculated and deducted from all batch flows
function settleBBDBatch(address[] _custodies, int[] _flows, uint _fee) external whenNotPaused onlyExchangeOrOwner
onlyAllowedInPhase(SettlementPhase.ONGOING) {
//TODO optimize for gas usage

require(_custodies.length == _flows.length);

uint preBatchBalance = bbdToken.balanceOf(address(this));

for (uint flowIndex = 0; flowIndex < _flows.length; flowIndex++) {

//Every custody can be served BBD once during settlement
require(custodiesServedBBD[lastSettlementStartedTimestamp][_custodies[flowIndex]] == false);
//All addresses must be custodies
require(custodyStorage.custodiesMap(_custodies[flowIndex]));

if (_flows[flowIndex] > 0) {
var flowValue = ((uint(_flows[flowIndex]) * gweiBBDPriceInWei)/gwei);

//Minimal BBD transfer is 1gWeiBBD
require(flowValue >= 1);

//50% rule threshold
if(flowValue > 10 ether) {
//50% rule for bbd
require((getTotalBalanceFor(_custodies[flowIndex]) / 2) >= flowValue);
}

bbdToken.transfer(_custodies[flowIndex], uint(_flows[flowIndex]));

} else if (_flows[flowIndex] < 0) {
Custody custody = Custody(_custodies[flowIndex]);

custody.transferToken(address(bbdToken),address(this), uint(-(_flows[flowIndex])));
}

custodiesServedBBD[lastSettlementStartedTimestamp][_custodies[flowIndex]] = true;
}

if(_fee > 0) {
bbdToken.transfer(feeAccount, _fee);
//No need for safe math, as transfer will trow if _fee could cause overflow
totalFeeFlows += ((_fee * gweiBBDPriceInWei) / gwei);
require (totalFeeFlows <= startingFeeBalance);
}

uint postBatchBalance = bbdToken.balanceOf(address(this));

//Zero-or-less-sum guaranteed for every batch, no insurance for spots
require(postBatchBalance <= preBatchBalance);
}

/// @dev This function is used to finish the settlement process
function finishSettlement() external whenNotPaused onlyExchangeOrOwner
onlyAllowedInPhase(SettlementPhase.ONGOING) {
//TODO phase change event?
earliestNextSettlementTimestamp = lastSettlementStartedTimestamp + 23 hours;

currentSettlementPhase = SettlementPhase.FINISHED;
}

function getTotalBalanceFor(address _custody) internal view returns (uint) {

var bbdHoldingsInWei = ((bbdToken.balanceOf(_custody) * gweiBBDPriceInWei) / gwei);

return _custody.balance + bbdHoldingsInWei;
}

function checkIfCustodiesServedETH(address[] _custodies) external view returns (bool) {
for (uint custodyIndex = 0; custodyIndex < _custodies.length; custodyIndex++) {
if(custodiesServedETH[lastSettlementStartedTimestamp][_custodies[custodyIndex]]) {
return true;
}
}
return false;
}

function checkIfCustodiesServedBBD(address[] _custodies) external view returns (bool) {
for (uint custodyIndex = 0; custodyIndex < _custodies.length; custodyIndex++) {
if(custodiesServedBBD[lastSettlementStartedTimestamp][_custodies[custodyIndex]]) {
return true;
}
}
return false;
}
}
function withdrawFromManager(uint _amount, address _recipient) external onlyExchangeOrOwner
whenNotPaused onlyAllowedInPhase(SettlementPhase.FINISHED) {
_recipient.transfer(_amount);
}

/// @dev Exchange uses this function to withdraw ether from the contract
/// @param _amount to withdraw
/// @param _recipient to send withdrawn ether to