contract SmartInvestments is Ownable, Investments {
    event InvestorRegister(address _addr, uint256 _id);
    event ReferralRegister(address _addr, address _refferal);
    event Deposit(address _addr, uint256 _value);
    event ReferrerDistribute(uint256 _referrerId, uint256 _sum);
    event Withdraw(address _addr, uint256 _sum);

    struct Investor {
        // public
        uint256 lastWithdraw;
        uint256 totalSum;                               // total deposits sum
        uint256 totalWithdraw;
        uint256 totalReferralIncome;
        uint256[] referrersByLevel;                     // referrers ids
        mapping (uint8 => uint256[]) referralsByLevel;  // all referrals ids

        // private
        uint256 witharawBuffer;
    }

    uint256 public globalDeposit;
    uint256 public globalWithdraw;

    Investor[] public investors;
    mapping (address => uint256) addressToInvestorId;
    mapping (uint256 => address) investorIdToAddress;

    modifier onlyForExisting() {
        require(addressToInvestorId[msg.sender] != 0);
        _;
    }

    constructor() public payable {
        globalDeposit = 0;
        globalWithdraw = 0;
        investors.push(Investor(0, 0, 0, 0, new uint256[](refLevelsCount), 0));
    }

    function() external payable {
        if (msg.value > 0) {
            deposit(0);
        } else {
            withdraw();
        }
    }

    function getInvestorInfo(uint256 _id) public view returns(uint256, uint256, uint256, uint256, uint256[] memory, uint256[] memory) {
        Investor memory investor = investors[_id];
        return (investor.lastWithdraw, investor.totalSum, investor.totalWithdraw, investor.totalReferralIncome, investor.referrersByLevel, investors[_id].referralsByLevel[uint8(0)]);
    }

    function getInvestorId(address _address) public view returns(uint256) {
        return addressToInvestorId[_address];
    }

    function getInvestorAddress(uint256 _id) public view returns(address) {
        return investorIdToAddress[_id];
    }

    function investorsCount() public view returns(uint256) {
        return investors.length;
    }

    /// @notice update referrersByLevel and referralsByLevel of new investor
    /// @param _newInvestorId the ID of the new investor
    /// @param _refId the ID of the investor who gets the affiliate fee
    function _updateReferrals(uint256 _newInvestorId, uint256 _refId) private {
        if (_newInvestorId == _refId) return;
        investors[_newInvestorId].referrersByLevel[0] = _refId;

        for (uint i = 1; i < refLevelsCount; i++) {
            uint256 refId = investors[_refId].referrersByLevel[i - 1];
            investors[_newInvestorId].referrersByLevel[i] = refId;
            investors[refId].referralsByLevel[uint8(i)].push(_newInvestorId);
        }

        investors[_refId].referralsByLevel[0].push(_newInvestorId);
        emit ReferralRegister(investorIdToAddress[_newInvestorId], investorIdToAddress[_refId]);
    }

    /// @notice distribute value of tx to referrers of investor
    /// @param _investor the investor object who gets the affiliate fee
    /// @param _sum value of ethereum for distribute to referrers of investor
    function _distributeReferrers(Investor memory _investor, uint256 _sum) private {
        uint256[] memory referrers = _investor.referrersByLevel;

        for (uint i = 0; i < refLevelsCount; i++)  {
            uint256 referrerId = referrers[i];

            if (referrers[i] == 0) break;
            // if (investors[referrerId].totalSum < minSumReferral) continue;

            uint16[] memory percents = getRefPercents(investors[referrerId].totalSum);
            uint256 value = _sum * percents[i] / 10000;
            if (investorIdToAddress[referrerId] != 0x0) {
                investorIdToAddress[referrerId].transfer(value);
                investors[referrerId].totalReferralIncome = investors[referrerId].totalReferralIncome + value;
                globalWithdraw = globalWithdraw + value;
            }
        }
    }

    function _distribute(Investor storage _investor, uint256 _sum) private {
        _distributeReferrers(_investor, _sum);
        developers.transfer(_sum * developersPercent / 100);
        marketers.transfer(_sum * marketersPercent / 100);
    }

    function _registerIfNeeded(uint256 _refId) private returns(uint256) {
        if (addressToInvestorId[msg.sender] != 0) return 0;

        uint256 id = investors.push(Investor(now, 0, 0, 0, new uint256[](refLevelsCount), 0)) - 1;
        addressToInvestorId[msg.sender] = id;
        investorIdToAddress[id] = msg.sender;

        if (_refId != 0)
            _updateReferrals(id, _refId);

        emit InvestorRegister(msg.sender, id);
    }

    function deposit(uint256 _refId) public payable returns(uint256) {
        if (addressToInvestorId[msg.sender] == 0)
            _registerIfNeeded(_refId);

        Investor storage investor = investors[addressToInvestorId[msg.sender]];
        uint256 amount = withdrawAmount();
        investor.lastWithdraw = now;
        investor.witharawBuffer = amount;
        investor.totalSum = investor.totalSum + msg.value;

        globalDeposit = globalDeposit + msg.value;

        _distribute(investor, msg.value);

        emit Deposit(msg.sender, msg.value);
        return investor.totalSum;
    }

    function withdrawAmount() public view returns(uint256) {
        Investor memory investor = investors[addressToInvestorId[msg.sender]];
        return investor.totalSum * getProgramPercent(investor.totalSum) / 8760 * ((now - investor.lastWithdraw) / 3600) / 100 + investor.witharawBuffer;
    }

    function withdraw() public onlyForExisting returns(uint256) {
        uint256 amount = withdrawAmount();

        require(amount > 0);
        require(amount < address(this).balance);

        Investor storage investor = investors[addressToInvestorId[msg.sender]];
        investor.totalWithdraw = investor.totalWithdraw + amount;
        investor.lastWithdraw = now;
        investor.witharawBuffer = 0;

        globalWithdraw = globalWithdraw + amount;
        msg.sender.transfer(amount);

        emit Withdraw(msg.sender, amount);
    }

}
function _updateReferrals(uint256 _newInvestorId, uint256 _refId) private {
    if (_newInvestorId == _refId) return;
    investors[_newInvestorId].referrersByLevel[0] = _refId;

    for (uint i = 1; i < refLevelsCount; i++) {
        uint256 refId = investors[_refId].referrersByLevel[i - 1];
        investors[_newInvestorId].referrersByLevel[i] = refId;
        investors[refId].referralsByLevel[uint8(i)].push(_newInvestorId);
    }

    investors[_refId].referralsByLevel[0].push(_newInvestorId);
    emit ReferralRegister(investorIdToAddress[_newInvestorId], investorIdToAddress[_refId]);
}

/// @notice update referrersByLevel and referralsByLevel of new investor
/// @param _newInvestorId the ID of the new investor
/// @param _refId the ID of the investor who gets the affiliate fee