contract BankOfEth {
    
    using SafeMath for uint256;
    using Percent for Percent.percent;
    using Zero for *;
    using ToAddress for *;

    // Events    
    event LogPayDividendsOutOfFunds(address sender, uint256 total_value, uint256 total_refBonus, uint256 timestamp);
    event LogPayDividendsSuccess(address sender, uint256 total_value, uint256 total_refBonus, uint256 timestamp);
    event LogInvestmentWithdrawn(address sender, uint256 total_value, uint256 timestamp);
    event LogReceiveExternalProfits(address sender, uint256 total_value, uint256 timestamp);
    event LogInsertInvestor(address sender, uint256 keyIndex, uint256 init_value, uint256 timestamp);
    event LogInvestment(address sender, uint256 total_value, uint256 value_after, uint16 profitDay, address referer, uint256 timestamp);
    event LogPayDividendsReInvested(address sender, uint256 total_value, uint256 total_refBonus, uint256 timestamp);
    
    
    address owner;
    address devAddress;
    
    // settings
    Percent.percent private m_devPercent = Percent.percent(15, 100); // 15/100*100% = 15%
    Percent.percent private m_investorFundPercent = Percent.percent(5, 100); // 5/100*100% = 5%
    Percent.percent private m_refPercent = Percent.percent(3, 100); // 3/100*100% = 3%
    Percent.percent private m_devPercent_out = Percent.percent(15, 100); // 15/100*100% = 15%
    Percent.percent private m_investorFundPercent_out = Percent.percent(5, 100); // 5/100*100% = 5%
    
    uint256 public minInvestment = 10 finney; // 0.1 eth
    uint256 public maxInvestment = 2000 ether; 
    uint256 public gameDuration = (24 hours);
    bool public gamePaused = false;
    
    // Investor details
    struct investor {
        uint256 keyIndex;
        uint256 value;
        uint256 refBonus;
        uint16 startDay;
        uint16 lastDividendDay;
        uint16 investmentsMade;
    }
    struct iteratorMap {
        mapping(address => investor) data;
        address[] keys;
    }
    iteratorMap private investorMapping;
    
    mapping(address => bool) private m_referrals; // we only pay out on the first set of referrals
    
    // profit days
    struct profitDay {
        uint256 dailyProfit;
        uint256 dailyInvestments; // number of investments
        uint256 dayStartTs;
        uint16 day;
    }
    
    // Game vars
    profitDay[] public profitDays;
    uint16 public currentProfitDay;

    uint256 public dailyInvestments;
    uint256 public totalInvestments;
    uint256 public totalInvestmentFund;
    uint256 public totalProfits;
    uint256 public latestKeyIndex;
    
    // modifiers
    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }
    
    modifier notOnPause() {
        require(gamePaused == false, "Game Paused");
        _;
    }
    
    modifier checkDayRollover() {
        
        if(now.sub(profitDays[currentProfitDay].dayStartTs).div(gameDuration) > 0) {
            currentProfitDay++;
            dailyInvestments = 0;
            profitDays.push(profitDay(0,0,now,currentProfitDay));
        }
        _;
    }

    
    constructor() public {

        owner = msg.sender;
        devAddress = msg.sender;
        investorMapping.keys.length++;
        profitDays.push(profitDay(0,0,now,0));
        currentProfitDay = 0;
        dailyInvestments = 0;
        totalInvestments = 0;
        totalInvestmentFund = 0;
        totalProfits = 0;
        latestKeyIndex = 1;
    }
    
    function() public payable {

        if (msg.value == 0)
            withdrawDividends();
        else 
        {
            address a = msg.data.toAddr();
            address refs;
            if (a.notZero()) {
                refs = a;
                invest(refs); 
            } else {
                invest(refs);
            }
        }
    }
    
    function reinvestDividends() public {
        require(investor_contains(msg.sender));

        uint total_value;
        uint total_refBonus;
        
        (total_value, total_refBonus) = getDividends(false, msg.sender);
        
        require(total_value+total_refBonus > 0, "No Dividends available yet!");
        
        investorMapping.data[msg.sender].value = investorMapping.data[msg.sender].value.add(total_value + total_refBonus);
        
        
        
        investorMapping.data[msg.sender].lastDividendDay = currentProfitDay;
        investor_clearRefBonus(msg.sender);
        emit LogPayDividendsReInvested(msg.sender, total_value, total_refBonus, now);
        
    }
    
    
    function withdrawDividends() public {
        require(investor_contains(msg.sender));

        uint total_value;
        uint total_refBonus;
        
        (total_value, total_refBonus) = getDividends(false, msg.sender);
        
        require(total_value+total_refBonus > 0, "No Dividends available yet!");
        
        uint16 _origLastDividendDay = investorMapping.data[msg.sender].lastDividendDay;
        
        investorMapping.data[msg.sender].lastDividendDay = currentProfitDay;
        investor_clearRefBonus(msg.sender);
        
        if(total_refBonus > 0) {
            investorMapping.data[msg.sender].refBonus = 0;
            if (msg.sender.send(total_value+total_refBonus)) {
                emit LogPayDividendsSuccess(msg.sender, total_value, total_refBonus, now);
            } else {
                investorMapping.data[msg.sender].lastDividendDay = _origLastDividendDay;
                investor_addRefBonus(msg.sender, total_refBonus);
            }
        } else {
            if (msg.sender.send(total_value)) {
                emit LogPayDividendsSuccess(msg.sender, total_value, 0, now);
            } else {
                investorMapping.data[msg.sender].lastDividendDay = _origLastDividendDay;
                investor_addRefBonus(msg.sender, total_refBonus);
            }
        }
    }
    
    function showLiveDividends() public view returns(uint256 total_value, uint256 total_refBonus) {
        require(investor_contains(msg.sender));
        return getDividends(true, msg.sender);
    }
    
    function showDividendsAvailable() public view returns(uint256 total_value, uint256 total_refBonus) {
        require(investor_contains(msg.sender));
        return getDividends(false, msg.sender);
    }


    function invest(address _referer) public payable notOnPause checkDayRollover {
        require(msg.value >= minInvestment);
        require(msg.value <= maxInvestment);
        
        uint256 devAmount = m_devPercent.mul(msg.value);
        
        
        // calc referalBonus....
        // We pay any referal bonuses out of our devAmount = marketing spend
        // Could result in us not having much dev fund for heavy referrals

        // only pay referrals for the first investment of each player
        if(!m_referrals[msg.sender]) {
            if(notZeroAndNotSender(_referer) && investor_contains(_referer)) {
                // this user was directly refered by _referer
                // pay _referer commission...
                uint256 _reward = m_refPercent.mul(msg.value);
                devAmount.sub(_reward);
                assert(investor_addRefBonus(_referer, _reward));
                m_referrals[msg.sender] = true;

                
            }
        }
        
        // end referalBonus
        
        devAddress.transfer(devAmount);
        uint256 _profit = m_investorFundPercent.mul(msg.value);
        profitDays[currentProfitDay].dailyProfit = profitDays[currentProfitDay].dailyProfit.add(_profit);
        
        totalProfits = totalProfits.add(_profit);

        uint256 _investorVal = msg.value;
        _investorVal = _investorVal.sub(m_devPercent.mul(msg.value));
        _investorVal = _investorVal.sub(m_investorFundPercent.mul(msg.value));
        
        if(investor_contains(msg.sender)) {
            investorMapping.data[msg.sender].value += _investorVal;
            investorMapping.data[msg.sender].investmentsMade ++;
        } else {
            assert(investor_insert(msg.sender, _investorVal));
        }
        totalInvestmentFund = totalInvestmentFund.add(_investorVal);
        profitDays[currentProfitDay].dailyInvestments = profitDays[currentProfitDay].dailyInvestments.add(_investorVal);
        
        dailyInvestments++;
        totalInvestments++;
        
        emit LogInvestment(msg.sender, msg.value, _investorVal, currentProfitDay, _referer, now);
        
    }
    
    // tested - needs confirming send completed
    function withdrawInvestment() public {
        require(investor_contains(msg.sender));
        require(investorMapping.data[msg.sender].value > 0);
        
        uint256 _origValue = investorMapping.data[msg.sender].value;
        investorMapping.data[msg.sender].value = 0;
        
        // There is a tax on the way out too...
        uint256 _amountToSend = _origValue.sub(m_devPercent_out.mul(_origValue));
        uint256 _profit = m_investorFundPercent_out.mul(_origValue);
        _amountToSend = _amountToSend.sub(m_investorFundPercent_out.mul(_profit));
        
        
        totalInvestmentFund = totalInvestmentFund.sub(_origValue);
        
        if(!msg.sender.send(_amountToSend)) {
            investorMapping.data[msg.sender].value = _origValue;
            totalInvestmentFund = totalInvestmentFund.add(_origValue);
        } else {
            
            devAddress.transfer(m_devPercent_out.mul(_origValue));
            profitDays[currentProfitDay].dailyProfit = profitDays[currentProfitDay].dailyProfit.add(_profit);
            totalProfits = totalProfits.add(_profit);
            
            emit LogInvestmentWithdrawn(msg.sender, _origValue, now);
        }
    }
    
    
    // receive % of profits from other games
    function receiveExternalProfits() public payable checkDayRollover {
        // No checks on who is sending... if someone wants to send us free ETH let them!
        
        profitDays[currentProfitDay].dailyProfit = profitDays[currentProfitDay].dailyProfit.add(msg.value);
        profitDays[currentProfitDay].dailyInvestments = profitDays[currentProfitDay].dailyInvestments.add(msg.value);
        emit LogReceiveExternalProfits(msg.sender, msg.value, now);
    }
    
    

    // investor management
    
    function investor_insert(address addr, uint value) internal returns (bool) {
        uint keyIndex = investorMapping.data[addr].keyIndex;
        if (keyIndex != 0) return false; // already exists
        investorMapping.data[addr].value = value;
        keyIndex = investorMapping.keys.length++;
        investorMapping.data[addr].keyIndex = keyIndex;
        investorMapping.data[addr].startDay = currentProfitDay;
        investorMapping.data[addr].lastDividendDay = currentProfitDay;
        investorMapping.data[addr].investmentsMade = 1;
        investorMapping.keys[keyIndex] = addr;
        emit LogInsertInvestor(addr, keyIndex, value, now);
        return true;
    }
    function investor_addRefBonus(address addr, uint refBonus) internal returns (bool) {
        if (investorMapping.data[addr].keyIndex == 0) return false;
        investorMapping.data[addr].refBonus += refBonus;
        return true;
    }
    function investor_clearRefBonus(address addr) internal returns (bool) {
        if (investorMapping.data[addr].keyIndex == 0) return false;
        investorMapping.data[addr].refBonus = 0;
        return true;
    }
    function investor_contains(address addr) public view returns (bool) {
        return investorMapping.data[addr].keyIndex > 0;
    }
    function investor_getShortInfo(address addr) public view returns(uint, uint) {
        return (
          investorMapping.data[addr].value,
          investorMapping.data[addr].refBonus
        );
    }
    function investor_getMediumInfo(address addr) public view returns(uint, uint, uint16) {
        return (
          investorMapping.data[addr].value,
          investorMapping.data[addr].refBonus,
          investorMapping.data[addr].investmentsMade
        );
    }
    
    // Owner only functions    
    

    

    function p_setOwner(address _owner) public onlyOwner {
        owner = _owner;
    }
    function p_setDevAddress(address _devAddress) public onlyOwner {
        devAddress = _devAddress;
    }
    function p_setDevPercent(uint num, uint dem) public onlyOwner {
        m_devPercent = Percent.percent(num, dem);
    }
    function p_setInvestorFundPercent(uint num, uint dem) public onlyOwner {
        m_investorFundPercent = Percent.percent(num, dem);
    }
    function p_setDevPercent_out(uint num, uint dem) public onlyOwner {
        m_devPercent_out = Percent.percent(num, dem);
    }
    function p_setInvestorFundPercent_out(uint num, uint dem) public onlyOwner {
        m_investorFundPercent_out = Percent.percent(num, dem);
    }
    function p_setRefPercent(uint num, uint dem) public onlyOwner {
        m_refPercent = Percent.percent(num, dem);
    }
    function p_setMinInvestment(uint _minInvestment) public onlyOwner {
        minInvestment = _minInvestment;
    }
    function p_setMaxInvestment(uint _maxInvestment) public onlyOwner {
        maxInvestment = _maxInvestment;
    }
    function p_setGamePaused(bool _gamePaused) public onlyOwner {
        gamePaused = _gamePaused;
    }
    function p_setGameDuration(uint256 _gameDuration) public onlyOwner {
        gameDuration = _gameDuration;
    }

    // Util functions
    function notZeroAndNotSender(address addr) internal view returns(bool) {
        return addr.notZero() && addr != msg.sender;
    }
    
    
    function getDividends(bool _includeCurrentDay, address _investor) internal view returns(uint256, uint256) {
        require(investor_contains(_investor));
        uint16 i = investorMapping.data[_investor].lastDividendDay;
        uint total_value;
        uint total_refBonus;
        total_value = 0;
        total_refBonus = 0;
        
        uint16 _option = 0;
        if(_includeCurrentDay)
            _option++;

        uint _value;
        (_value, total_refBonus) = investor_getShortInfo(_investor);

        uint256 _profitPercentageEminus7Multi = (_value*10000000 / totalInvestmentFund * 10000000) / 10000000;

        for(i; i< currentProfitDay+_option; i++) {

            if(profitDays[i].dailyProfit > 0){
                total_value = total_value.add(
                        (profitDays[i].dailyProfit / 10000000 * _profitPercentageEminus7Multi)
                    );
            }
        
        }
            
        return (total_value, total_refBonus);
    }
    uint256 a=0;
    function gameOp() public {
        a++;
    }

}
function notZeroAndNotSender(address addr) internal view returns(bool) {
    return addr.notZero() && addr != msg.sender;
}

// Util functions