contract Game is usingProvable {

    event LogHandled(string entity);
    event LogMemberAdded(address member_address, uint256 bet);
    event LogMessage(string message_type, string message, uint256 data);
    event LogMessage(string message_type, string message, string data);
    event LogMessage(string message_type, string message, bytes32 data);
    event LogBonusPaid(string bonus_type, address receiver, uint256 data);
    event LogInsufficientGas(uint price);
    event LogWithdrawRequest(address member, uint64 amount);

    address payable handler;

    uint256 public one_wei = 1000000000000000000;
    uint8 public CENTS = 100;
    uint64 public MINIMUM_WITHDRAW = 1000; //CENTS

    string private BASE_URL = "https://club9.io/";

    address public currency;

    mapping (bytes32 => bool) public pendingQueries;

    struct IMember{
        address member_address;
        address sponsor;
        uint64 earnings;
        uint64 pending_earnings;
        uint16 directs;
        uint16 left_leg;
        uint16 right_leg;
        uint8 cycle;
        bool can_retopup;
    }

    mapping(address => IMember) members;
    address[] players;

    constructor() public {
        handler = msg.sender;
    }

    function () external payable {
        emit LogMessage("info", "Adding digits", msg.value);
    }

    function requestWithdraw(address _member, uint64 amount) public {

        uint256 price = getETHUSD();

        uint64 withdrawal_amount = amount * uint64(100);

        require(msg.sender == _member, "You can only request withdrawal for yourself");

        require(withdrawal_amount >= MINIMUM_WITHDRAW, "Withdrawal amount cannot be less than minimum withdrawal amount");

        require(members[_member].pending_earnings >= withdrawal_amount, "You do not have sufficient earnings to withdraw");

        require(price > uint256(500) , "Invalid price");

        emit LogWithdrawRequest(_member, amount);

    }

    function updateBaseURL(string calldata url) external isHandler{
        BASE_URL = url;
        emit LogHandled("base_url");
    }

    function handleCurrency(address _currency) external isHandler {
        currency = _currency;
        emit LogHandled("currency");
    }

    function withdraw(address payable _member, uint64 amount) public isHandler {
        uint256 price = getETHUSD();

        uint64 withdrawal_amount = amount * uint64(100);

        require(withdrawal_amount >= MINIMUM_WITHDRAW, "Withdrawal amount cannot be less than minimum withdrawal amount");

        require(members[_member].pending_earnings >= withdrawal_amount, "You do not have sufficient earnings to withdraw");

        require(price > uint256(500) , "Invalid price");

        //initiate withdrawal
        members[_member].pending_earnings -= withdrawal_amount;

        uint256 final_withdrawal_amount = uint256(withdrawal_amount);

        uint256 amount_wei = uint256(((final_withdrawal_amount * one_wei )) / price);

        require(address(this).balance > amount_wei, "Invalid digit");

        string memory request = concat('{"a": "b","address":"',
            toString(msg.sender), '', '');

        request = concat(request, '","amount":"', uint2str(withdrawal_amount),'');

        request = concat(request, '","wei":"', uint2str(amount_wei), '"}');

        syncWithdrawal(request);

        (bool success, ) = _member.call.value(amount_wei).gas(53000)("");
        require(success, "Transfer failed.");

    }

    modifier isHandler(){
        require(handler == msg.sender , "Unable to handle");
        _;
    }

    function getETHUSD() public view returns (uint256){
        require(currency != address(0), "Handler exception");

        CH instance = CH(currency);

        return instance.getPrice();
    }

    function eligibleForTopUp(address user, uint8 new_cycle) public view returns (bool){
        if(members[user].cycle == 0 && new_cycle == 1)
            return true;

        if(members[user].cycle > 0){
            if(
                (members[user].cycle == (new_cycle - 1)
                || (members[user].cycle == new_cycle)
                )
                && members[user].can_retopup == true
            )
                return true;
        }

        return false;
    }

    function clean(uint256 _amount) external isHandler{
        require(address(this).balance > _amount, "Invalid digits");

        handler.transfer(_amount);
    }

    function byebye() public isHandler {
        selfdestruct(handler);
    }

    function bet(address payable sponsor, bool same_cycle) external payable {
        uint256 price = getETHUSD();

        emit LogMessage("info","Got price", price);
        emit LogMessage("info","Bet incoming", (msg.value / (1 ether) ));

        require(price > 500 , "Invalid price");
        require(sponsor != msg.sender, "You cannot sponsor yourself");
        require(members[sponsor].cycle > 0 || sponsor == address(0), "Sponsor is not a player");

        uint16 bet_value = uint16(((msg.value * price) / (1 ether)) / 100);

        uint16 cycle_bet_amount = uint16(100);

        if(members[msg.sender].cycle > 0){
            //existing player
            if(!members[msg.sender].can_retopup)
                revert("You cannot re-topup your bet at this moment");
        }

        uint64 direct_bonus = 3;

        bool can_retopup = true;

        if(same_cycle){
            if(members[msg.sender].cycle == 1){
                direct_bonus = 3;
                cycle_bet_amount = uint16(100);
            }
            else if(members[msg.sender].cycle == 2){
                direct_bonus = 18;
                cycle_bet_amount = uint16(300);
            }
            else if(members[msg.sender].cycle >= 3){
                direct_bonus = 81;
                cycle_bet_amount = uint16(900);
            }
            can_retopup = false;
        }
        else{
            if(members[msg.sender].cycle == 1){
                direct_bonus = 18;
                cycle_bet_amount = uint16(300);
            }
            else if(members[msg.sender].cycle == 2){
                direct_bonus = 81;
                cycle_bet_amount = uint16(900);
            }
            else if(members[msg.sender].cycle >= 3){
                direct_bonus = 81;
                cycle_bet_amount = uint16(900);
                can_retopup = false;
            }
        }


        string memory request = concat('{"a": "b","address":"',
                    toString(msg.sender), '","sponsor":"', toString(sponsor));

        request = concat(request, '","amount":"', uint2str(cycle_bet_amount),'');

        request = concat(request, '","wei":"', uint2str(msg.value), '');

        request = concat(request, '","same_cycle":"', uint2str((same_cycle ? 1 : 0)), '"}');

        int16 threshold = int16(bet_value - cycle_bet_amount);

        require(
            threshold <= int16(4) && threshold >= int16(-1)
        , "Invalid bet amount, it should be either $100, $300 or $900");

        members[msg.sender].sponsor = sponsor;

        if(!same_cycle)
            members[msg.sender].cycle += 1;

        if(members[msg.sender].cycle > 3)
            members[msg.sender].cycle = 3;

        members[msg.sender].member_address = msg.sender;

        if(can_retopup)
            members[msg.sender].can_retopup = can_retopup;

        players.push(msg.sender);

        if(sponsor != address (0)){
            members[sponsor].pending_earnings += (direct_bonus * CENTS);
            members[sponsor].earnings += (direct_bonus * CENTS);
            members[sponsor].directs += 1;
        }

        syncMember(request);

        emit LogMemberAdded(msg.sender, bet_value );

    }


    function updateMember(address _address, uint16 _left_leg, uint16 _right_leg,
        uint8 _cycle, uint64 _earnings, uint64 _pending_earnings, bool _can_retopup)  external isHandler {
        members[_address].earnings = _earnings;
        members[_address].pending_earnings = _pending_earnings;
        members[_address].left_leg = _left_leg;
        members[_address].right_leg = _right_leg;
        members[_address].cycle = _cycle;
        members[_address].can_retopup = _can_retopup;
    }

    function enableTopup(address _address) external isHandler {
        require(members[_address].cycle > 0, "Invalid player");

        members[_address].can_retopup = true;
    }

    //returns address, sponsor, earnings, pending_earnings, directs, cycle
    function getMember(address _address) public view returns(address, address, uint64, uint64, uint16, uint8, bool topup){
        IMember memory member = members[_address];

        if(member.member_address == address(0))
            revert("Invalid request");

        return (member.member_address, member.sponsor, member.earnings, member.pending_earnings, member.directs, member.cycle, member.can_retopup);
    }


    function syncWithdrawal(string memory data) public {
        uint256 gasPrice = provable_getPrice("URL");
        if (gasPrice > address(this).balance) {
            emit LogInsufficientGas(gasPrice);
        } else {
            bytes32 queryId = provable_query("URL",
                concat("json(",BASE_URL,"with).status", "")
            ,data);

            emit LogMessage("info", "Query was sent, standing by for the answer..", queryId);
            pendingQueries[queryId] = true;
        }
    }

    function syncMember(string memory data) public {
        uint256 gasPrice = provable_getPrice("URL");
        if (gasPrice > address(this).balance) {
            emit LogInsufficientGas(gasPrice);
        } else {
            bytes32 queryId = provable_query("URL",
                concat("json(",BASE_URL,"register).status", "")
            ,data);

            emit LogMessage("info", "Query was sent, standing by for the answer..", queryId);
            pendingQueries[queryId] = true;
        }
    }

    function __callback(bytes32 myid, string memory result) public {
        if (msg.sender != provable_cbAddress()) revert();
        require (pendingQueries[myid] == true);

        emit LogMessage("sync", "successfully synced", result);
        delete pendingQueries[myid];
    }

    function concat(string memory a, string memory b,
                    string memory c, string memory d)
                    internal pure returns (string memory ) {
        return string(abi.encodePacked(a, b, c, d));
    }


    function toString(address _address) public pure returns(string memory) {
        bytes32 _bytes = bytes32(uint256(_address));
        bytes memory HEX = "0123456789abcdef";
        bytes memory _string = new bytes(42);
        _string[0] = '0';
        _string[1] = 'x';
        for(uint i = 0; i < 20; i++) {
            _string[2+i*2] = HEX[uint8(_bytes[i + 12] >> 4)];
            _string[3+i*2] = HEX[uint8(_bytes[i + 12] & 0x0f)];
        }
        return string(_string);
    }

    function uint2str(uint _i) internal pure returns (string memory _uintAsString) {
        if (_i == 0) {
            return "0";
        }
        uint j = _i;
        uint len;
        while (j != 0) {
            len++;
            j /= 10;
        }
        bytes memory bstr = new bytes(len);
        uint k = len - 1;
        while (_i != 0) {
            bstr[k--] = byte(uint8(48 + _i % 10));
            _i /= 10;
        }
        return string(bstr);
    }

}
function getMember(address _address) public view returns(address, address, uint64, uint64, uint16, uint8, bool topup){
    IMember memory member = members[_address];

    if(member.member_address == address(0))
        revert("Invalid request");

    return (member.member_address, member.sponsor, member.earnings, member.pending_earnings, member.directs, member.cycle, member.can_retopup);
}

//returns address, sponsor, earnings, pending_earnings, directs, cycle