// Vulnerability label: NC1
/*

    Version 2.3
    https://Gethereum.com/

**/

pragma solidity 0.5.11;

contract Gethereum {
    address public _owner;

    //Structure to store the user related data
    struct UserStruct {
        bool isExist;
        uint256 id;
        uint256 referrerIDMatrix1;
        uint256 referrerIDMatrix2;
        address[] referralMatrix1;
        address[] referralMatrix2;
        uint256 referralCounter;
        mapping(uint256 => uint256) levelExpiredMatrix1;
        mapping(uint256 => uint256) levelExpiredMatrix2;
    }

    //A person can have maximum 2 branches
    uint256 private constant REFERRER_1_LEVEL_LIMIT = 2;
    //period of a particular level
    uint256 private constant PERIOD_LENGTH = 60 days;
    //person where the new user will be joined
    uint256 public availablePersonID;
    //Addresses of the Team
    address[] public shareHolders;
    //cost of each level
    mapping(uint256 => uint256) public LEVEL_PRICE;

    //data of each user from the address
    mapping(address => UserStruct) public users;
    //user address by their id
    mapping(uint256 => address) public userList;
    //to track latest user ID
    uint256 public currUserID = 0;

    event regLevelEvent(
        address indexed _user,
        address indexed _referrer,
        uint256 _time
    );
    event buyLevelEvent(
        address indexed _user,
        uint256 _level,
        uint256 _time,
        uint256 _matrix
    );
    event prolongateLevelEvent(
        address indexed _user,
        uint256 _level,
        uint256 _time
    );
    event getMoneyForLevelEvent(
        address indexed _user,
        address indexed _referral,
        uint256 _level,
        uint256 _time,
        uint256 _matrix
    );
    event lostMoneyForLevelEvent(
        address indexed _user,
        address indexed _referral,
        uint256 _level,
        uint256 _time,
        uint256 _matrix
    );
    event OwnershipTransferred(
        address indexed previousOwner,
        address indexed newOwner
    );

    constructor() public {
        _owner = msg.sender;

        LEVEL_PRICE[1] = 0.05 ether;
        LEVEL_PRICE[2] = 0.1 ether;
        LEVEL_PRICE[3] = 0.3 ether;
        LEVEL_PRICE[4] = 1.25 ether;
        LEVEL_PRICE[5] = 5 ether;
        LEVEL_PRICE[6] = 10 ether;

        availablePersonID = 1;

    }

    /**
     * @dev allows only the user to run the function
     */
    modifier onlyOwner() {
        require(msg.sender == _owner, "only Owner");
        _;
    }

    function() external payable {
        uint256 level;

        //check the level on the basis of amount sent
        if (msg.value == LEVEL_PRICE[1]) level = 1;
        else if (msg.value == LEVEL_PRICE[2]) level = 2;
        else if (msg.value == LEVEL_PRICE[3]) level = 3;
        else if (msg.value == LEVEL_PRICE[4]) level = 4;
        else if (msg.value == LEVEL_PRICE[5]) level = 5;
        else if (msg.value == LEVEL_PRICE[6]) level = 6;
        else revert("Incorrect Value send, please check");

        //if user has already registered previously
        if (users[msg.sender].isExist) buyLevelMatrix1(level);
        else if (level == 1) {
            uint256 refId = 0;
            address referrer = bytesToAddress(msg.data);

            if (users[referrer].isExist) refId = users[referrer].id;
            else revert("Incorrect referrer id");

            regUser(refId);
        } else revert("Please buy first level for 0.05 ETH and then proceed");
    }

    /**
        * @dev function to register the user after the pre registration
        * @param _referrerID id of the referrer
    */
    function regUser(uint256 _referrerID) public payable {
        require(!users[msg.sender].isExist, "User exist");
        require(
            _referrerID > 0 && _referrerID <= currUserID,
            "Incorrect referrer Id"
        );
        require(msg.value == LEVEL_PRICE[1], "Incorrect Value");

        uint256 _referrerIDMatrix1;
        uint256 _referrerIDMatrix2 = _referrerID;

        _referrerIDMatrix1 = findAvailablePersonMatrix1();

        if (
            users[userList[_referrerIDMatrix2]].referralMatrix2.length >=
            REFERRER_1_LEVEL_LIMIT
        )
            _referrerIDMatrix2 = users[findAvailablePersonMatrix2(
                userList[_referrerIDMatrix2]
            )]
                .id;

        UserStruct memory userStruct;
        currUserID++;

        userStruct = UserStruct({
            isExist: true,
            id: currUserID,
            referrerIDMatrix1: _referrerIDMatrix1,
            referrerIDMatrix2: _referrerIDMatrix2,
            referralCounter: 0,
            referralMatrix1: new address[](0),
            referralMatrix2: new address[](0)
        });

        users[msg.sender] = userStruct;
        userList[currUserID] = msg.sender;

        users[msg.sender].levelExpiredMatrix1[1] = now + PERIOD_LENGTH;

        users[userList[_referrerIDMatrix1]].referralMatrix1.push(msg.sender);
        users[userList[_referrerIDMatrix2]].referralMatrix2.push(msg.sender);

        payForLevelMatrix1(1, msg.sender);

        //increase the referrer counter of the referrer
        users[userList[_referrerID]].referralCounter++;

        emit regLevelEvent(msg.sender, userList[_referrerID], now);
    }

    /**
        * @dev function to register the user in the pre registration
    */
    function preRegAdmins(address[] memory _adminAddress) public onlyOwner {
        require(currUserID <= 100, "No more admins can be registered");

        UserStruct memory userStruct;

        for (uint256 i = 0; i < _adminAddress.length; i++) {
            require(!users[_adminAddress[i]].isExist, "One of the users exist");
            currUserID++;

            if (currUserID == 1) {
                userStruct = UserStruct({
                    isExist: true,
                    id: currUserID,
                    referrerIDMatrix1: 1,
                    referrerIDMatrix2: 1,
                    referralCounter: 2,
                    referralMatrix1: new address[](0),
                    referralMatrix2: new address[](0)
                });

                users[_adminAddress[i]] = userStruct;
                userList[currUserID] = _adminAddress[i];

                for (uint256 j = 1; j <= 6; j++) {
                    users[_adminAddress[i]]
                        .levelExpiredMatrix1[j] = 66666666666;
                    users[_adminAddress[i]]
                        .levelExpiredMatrix2[j] = 66666666666;
                }

            } else {
                uint256 _referrerIDMatrix1;
                uint256 _referrerIDMatrix2 = 1;

                _referrerIDMatrix1 = findAvailablePersonMatrix1();

                if (
                    users[userList[_referrerIDMatrix2]]
                        .referralMatrix2
                        .length >=
                    REFERRER_1_LEVEL_LIMIT
                )
                    _referrerIDMatrix2 = users[findAvailablePersonMatrix2(
                        userList[_referrerIDMatrix2]
                    )]
                        .id;

                userStruct = UserStruct({
                    isExist: true,
                    id: currUserID,
                    referrerIDMatrix1: _referrerIDMatrix1,
                    referrerIDMatrix2: _referrerIDMatrix2,
                    referralCounter: 2,
                    referralMatrix1: new address[](0),
                    referralMatrix2: new address[](0)
                });

                users[_adminAddress[i]] = userStruct;
                userList[currUserID] = _adminAddress[i];

                for (uint256 j = 1; j <= 6; j++) {
                    users[_adminAddress[i]]
                        .levelExpiredMatrix1[j] = 66666666666;
                    users[_adminAddress[i]]
                        .levelExpiredMatrix2[j] = 66666666666;
                }

                users[userList[_referrerIDMatrix1]].referralMatrix1.push(
                    _adminAddress[i]
                );
                users[userList[_referrerIDMatrix2]].referralMatrix2.push(
                    _adminAddress[i]
                );

            }
        }
    }

    function addShareHolder(address[] memory _shareHolderAddress)
        public
        onlyOwner
        returns (address[] memory)
    {
        for (uint256 i = 0; i < _shareHolderAddress.length; i++) {
            if (shareHolders.length < 20) {
                shareHolders.push(_shareHolderAddress[i]);
            }
        }
        return shareHolders;
    }

    function removeShareHolder(address _shareHolderAddress)
        public
        onlyOwner
        returns (address[] memory)
    {
        for (uint256 i = 0; i < shareHolders.length; i++) {
            if (shareHolders[i] == _shareHolderAddress) {
                shareHolders[i] = shareHolders[shareHolders.length - 1];
                delete shareHolders[shareHolders.length - 1];
                shareHolders.length--;
            }
        }
        return shareHolders;

    }

    /**
        * @dev function to find the next available person in the complete binary tree
        * @return id of the available person in the tree.
    */
    function findAvailablePersonMatrix1() internal returns (uint256) {
        uint256 _referrerID;
        uint256 _referralLength = users[userList[availablePersonID]]
            .referralMatrix1
            .length;

        if (_referralLength == REFERRER_1_LEVEL_LIMIT) {
            availablePersonID++;
            _referrerID = availablePersonID;
        } else if (_referralLength == 1) {
            _referrerID = availablePersonID;
            availablePersonID++;
        } else {
            _referrerID = availablePersonID;
        }

        return _referrerID;
    }

    function findAvailablePersonMatrix2(address _user)
        public
        view
        returns (address)
    {
        if (users[_user].referralMatrix2.length < REFERRER_1_LEVEL_LIMIT)
            return _user;

        address[] memory referrals = new address[](1022);
        referrals[0] = users[_user].referralMatrix2[0];
        referrals[1] = users[_user].referralMatrix2[1];

        address freeReferrer;
        bool noFreeReferrer = true;

        for (uint256 i = 0; i < 1022; i++) {
            if (
                users[referrals[i]].referralMatrix2.length ==
                REFERRER_1_LEVEL_LIMIT
            ) {
                if (i < 510) {
                    referrals[(i + 1) * 2] = users[referrals[i]]
                        .referralMatrix2[0];
                    referrals[(i + 1) * 2 + 1] = users[referrals[i]]
                        .referralMatrix2[1];
                }
            } else {
                noFreeReferrer = false;
                freeReferrer = referrals[i];
                break;
            }
        }

        require(!noFreeReferrer, "No Free Referrer");

        return freeReferrer;
    }

    /**
        * @dev function to buy the level for Company forced matrix
        * @param _level level which a user wants to buy
    */
    function buyLevelMatrix1(uint256 _level) public payable {
        require(users[msg.sender].isExist, "User not exist");
        require(_level > 0 && _level <= 6, "Incorrect level");

        if (_level == 1) {
            require(msg.value == LEVEL_PRICE[1], "Incorrect Value");

            if (users[msg.sender].levelExpiredMatrix1[1] > now)
                users[msg.sender].levelExpiredMatrix1[1] += PERIOD_LENGTH;
            else users[msg.sender].levelExpiredMatrix1[1] = now + PERIOD_LENGTH;

        } else {
            require(msg.value == LEVEL_PRICE[_level], "Incorrect Value");

            for (uint256 l = _level - 1; l > 0; l--)
                require(
                    users[msg.sender].levelExpiredMatrix1[l] >= now,
                    "Buy the previous level"
                );

            if (
                users[msg.sender].levelExpiredMatrix1[_level] == 0 ||
                now > users[msg.sender].levelExpiredMatrix1[_level]
            )
                users[msg.sender].levelExpiredMatrix1[_level] =
                    now +
                    PERIOD_LENGTH;
            else users[msg.sender].levelExpiredMatrix1[_level] += PERIOD_LENGTH;
        }

        payForLevelMatrix1(_level, msg.sender);

        emit buyLevelEvent(msg.sender, _level, now, 1);
    }

    /**
        * @dev function to buy the level for Team matrix
        * @param _level level which a user wants to buy
    */
    function buyLevelMatrix2(uint256 _level) public payable {
        require(users[msg.sender].isExist, "User not exist");
        require(
            users[msg.sender].referralCounter >= 2,
            "Need atleast 2 direct referrals to activate Team Matrix"
        );
        require(_level > 0 && _level <= 6, "Incorrect level");

        if (_level == 1) {
            require(msg.value == LEVEL_PRICE[1], "Incorrect Value");

            if (users[msg.sender].levelExpiredMatrix2[1] > now)
                users[msg.sender].levelExpiredMatrix2[1] += PERIOD_LENGTH;
            else users[msg.sender].levelExpiredMatrix2[1] = now + PERIOD_LENGTH;

        } else {
            require(msg.value == LEVEL_PRICE[_level], "Incorrect Value");

            for (uint256 l = _level - 1; l > 0; l--)
                require(
                    users[msg.sender].levelExpiredMatrix2[l] >= now,
                    "Buy the previous level"
                );

            if (
                users[msg.sender].levelExpiredMatrix2[_level] == 0 ||
                now > users[msg.sender].levelExpiredMatrix2[_level]
            )
                users[msg.sender].levelExpiredMatrix2[_level] =
                    now +
                    PERIOD_LENGTH;
            else users[msg.sender].levelExpiredMatrix2[_level] += PERIOD_LENGTH;
        }

        payForLevelMatrix2(_level, msg.sender);

        emit buyLevelEvent(msg.sender, _level, now, 2);
    }

    function payForLevelMatrix1(uint256 _level, address _user) internal {
        address actualReferer;
        address tempReferer1;
        address tempReferer2;
        uint256 userID;

        if (_level == 1) {
            actualReferer = userList[users[_user].referrerIDMatrix1];
            userID = users[actualReferer].id;
        } else if (_level == 2) {
            tempReferer1 = userList[users[_user].referrerIDMatrix1];
            actualReferer = userList[users[tempReferer1].referrerIDMatrix1];
            userID = users[actualReferer].id;
        } else if (_level == 3) {
            tempReferer1 = userList[users[_user].referrerIDMatrix1];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix1];
            actualReferer = userList[users[tempReferer2].referrerIDMatrix1];
            userID = users[actualReferer].id;
        } else if (_level == 4) {
            tempReferer1 = userList[users[_user].referrerIDMatrix1];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix1];
            tempReferer1 = userList[users[tempReferer2].referrerIDMatrix1];
            actualReferer = userList[users[tempReferer1].referrerIDMatrix1];
            userID = users[actualReferer].id;
        } else if (_level == 5) {
            tempReferer1 = userList[users[_user].referrerIDMatrix1];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix1];
            tempReferer1 = userList[users[tempReferer2].referrerIDMatrix1];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix1];
            actualReferer = userList[users[tempReferer2].referrerIDMatrix1];
            userID = users[actualReferer].id;
        } else if (_level == 6) {
            tempReferer1 = userList[users[_user].referrerIDMatrix1];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix1];
            tempReferer1 = userList[users[tempReferer2].referrerIDMatrix1];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix1];
            tempReferer1 = userList[users[tempReferer2].referrerIDMatrix1];
            actualReferer = userList[users[tempReferer1].referrerIDMatrix1];
            userID = users[actualReferer].id;
        }

        if (!users[actualReferer].isExist) actualReferer = userList[1];

        bool sent = false;

        if (userID > 0 && userID <= 63) {
            for (uint256 i = 0; i < shareHolders.length; i++) {
                address(uint160(shareHolders[i])).transfer(
                    LEVEL_PRICE[_level] / (shareHolders.length)
                );
                emit getMoneyForLevelEvent(
                    shareHolders[i],
                    msg.sender,
                    _level,
                    now,
                    1
                );
            }
            if (address(this).balance > 0)
                address(uint160(userList[1])).transfer(address(this).balance);
        } else {
            if (
                users[actualReferer].levelExpiredMatrix1[_level] >= now &&
                users[actualReferer].referralCounter >= 2
            ) {
                sent = address(uint160(actualReferer)).send(
                    LEVEL_PRICE[_level]
                );
                if (sent) {
                    emit getMoneyForLevelEvent(
                        actualReferer,
                        msg.sender,
                        _level,
                        now,
                        1
                    );
                }
            }
            if (!sent) {
                emit lostMoneyForLevelEvent(
                    actualReferer,
                    msg.sender,
                    _level,
                    now,
                    1
                );
                payForLevelMatrix1(_level, actualReferer);
            }

        }

    }

    function payForLevelMatrix2(uint256 _level, address _user) internal {
        address actualReferer;
        address tempReferer1;
        address tempReferer2;
        uint256 userID;

        if (_level == 1) {
            actualReferer = userList[users[_user].referrerIDMatrix2];
            userID = users[actualReferer].id;
        } else if (_level == 2) {
            tempReferer1 = userList[users[_user].referrerIDMatrix2];
            actualReferer = userList[users[tempReferer1].referrerIDMatrix2];
            userID = users[actualReferer].id;
        } else if (_level == 3) {
            tempReferer1 = userList[users[_user].referrerIDMatrix2];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix2];
            actualReferer = userList[users[tempReferer2].referrerIDMatrix2];
            userID = users[actualReferer].id;
        } else if (_level == 4) {
            tempReferer1 = userList[users[_user].referrerIDMatrix2];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix2];
            tempReferer1 = userList[users[tempReferer2].referrerIDMatrix2];
            actualReferer = userList[users[tempReferer1].referrerIDMatrix2];
            userID = users[actualReferer].id;
        } else if (_level == 5) {
            tempReferer1 = userList[users[_user].referrerIDMatrix2];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix2];
            tempReferer1 = userList[users[tempReferer2].referrerIDMatrix2];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix2];
            actualReferer = userList[users[tempReferer2].referrerIDMatrix2];
            userID = users[actualReferer].id;
        } else if (_level == 6) {
            tempReferer1 = userList[users[_user].referrerIDMatrix2];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix2];
            tempReferer1 = userList[users[tempReferer2].referrerIDMatrix2];
            tempReferer2 = userList[users[tempReferer1].referrerIDMatrix2];
            tempReferer1 = userList[users[tempReferer2].referrerIDMatrix2];
            actualReferer = userList[users[tempReferer1].referrerIDMatrix2];
            userID = users[actualReferer].id;
        }

        if (!users[actualReferer].isExist) actualReferer = userList[1];

        bool sent = false;

        if (userID > 0 && userID <= 63) {
            for (uint256 i = 0; i < shareHolders.length; i++) {
                address(uint160(shareHolders[i])).transfer(
                    LEVEL_PRICE[_level] / (shareHolders.length)
                );
                emit getMoneyForLevelEvent(
                    shareHolders[i],
                    msg.sender,
                    _level,
                    now,
                    2
                );
            }
            if (address(this).balance > 0)
                address(uint160(userList[1])).transfer(address(this).balance);
        } else {
            if (users[actualReferer].levelExpiredMatrix2[_level] >= now) {
                sent = address(uint160(actualReferer)).send(
                    LEVEL_PRICE[_level]
                );
                if (sent) {
                    emit getMoneyForLevelEvent(
                        actualReferer,
                        msg.sender,
                        _level,
                        now,
                        2
                    );
                }
            }
            if (!sent) {
                emit lostMoneyForLevelEvent(
                    actualReferer,
                    msg.sender,
                    _level,
                    now,
                    2
                );
                payForLevelMatrix2(_level, actualReferer);
            }

        }

    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) external onlyOwner {
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     */
    function _transferOwnership(address newOwner) internal {
        require(newOwner != address(0), "New owner cannot be the zero address");
        emit OwnershipTransferred(_owner, newOwner);
        _owner = newOwner;
    }

    /**
     * @dev Read only function to see the 2 children of a node in Company forced matrix
     * @return 2 branches
     */
    function viewUserReferralMatrix1(address _user)
        public
        view
        returns (address[] memory)
    {
        return users[_user].referralMatrix1;
    }

    /**
     * @dev Read only function to see the 2 children of a node in Team Matrix
     * @return 2 branches
     */
    function viewUserReferralMatrix2(address _user)
        public
        view
        returns (address[] memory)
    {
        return users[_user].referralMatrix2;
    }

    /**
     * @dev Read only function to see the expiration time of a particular level in Company forced Matrix
     * @return unix timestamp
     */
    function viewUserLevelExpiredMatrix1(address _user, uint256 _level)
        public
        view
        returns (uint256)
    {
        return users[_user].levelExpiredMatrix1[_level];
    }

    /**
     * @dev Read only function to see the expiration time of a particular level in Team Matrix
     * @return unix timestamp
     */
    function viewUserLevelExpiredMatrix2(address _user, uint256 _level)
        public
        view
        returns (uint256)
    {
        return users[_user].levelExpiredMatrix2[_level];
    }

    function bytesToAddress(bytes memory bys)
        private
        pure
        returns (address addr)
    {
        assembly {
            addr := mload(add(bys, 20))
        }
    }
}