contract GuessGame is Ownable, usingOraclize {

    struct UserPlayed {
        address payable user;
        address payable referrer;
        uint value;
    }

    event CaseOpened(
        uint amount,
        uint prize,
        address indexed user
    );

    struct ReferralStat {
        uint profit;
        uint count;
    }

    struct Probability {
        uint from;
        uint to;
    }

    uint public usersCount = 0;
    uint public openedCases = 0;
    uint public totalWins = 0;
    Probability[11] public probabilities;
    mapping(uint => uint32[11]) public betsPrizes;
    mapping(uint => bool) public cases;
    uint[3] public casesArr = [
    5 * 10 ** 16,
    10 ** 17,
//    2 * 10 ** 17,
    /*
    3 * 10 ** 17,
    5 * 10 ** 17,
    7 * 10 ** 17,
    10 ** 18,*/
//    15 * 10 ** 17,
    2 * 10 ** 18
    ];
    mapping(uint => uint) public caseWins;
    mapping(uint => uint) public caseOpenings;
    mapping(address => bool) private users;
    mapping(address => uint) private userCasesCount;
    mapping(address => address payable) private referrals;
    mapping(address => mapping(address => bool)) private usedReferrals;
    mapping(address => ReferralStat) public referralStats;
    uint private constant multiplier = 1 ether / 10000;

    mapping(bytes32 => UserPlayed) private querySavedInfo;

    uint constant GAS_LIMIT_ORACLIZE_QUERY = 500000; // gas limit for Oraclize callback

    event LogOraclizeQuery(string description);
    uint private valueReserved = 0;
    event RandomNumberReceived(uint number);

    /**
    * @dev The GuessGame constructor sets the default cases that are available for opening.
    */
    constructor() public Ownable() {
        oraclize_setProof(proofType_Ledger);
        oraclize_setCustomGasPrice(15000000000 wei); // 15 Gwei
        for (uint i = 0; i < 3; i++) cases[casesArr[i]] = true;
        probabilities[0] = Probability(1, 6);
        probabilities[1] = Probability(7, 18);
        probabilities[2] = Probability(19, 30);
        probabilities[3] = Probability(31, 44);
        probabilities[4] = Probability(45, 58);
        probabilities[5] = Probability(59, 72);
        probabilities[6] = Probability(73, 83);
        probabilities[7] = Probability(84, 92);
        probabilities[8] = Probability(93, 97);
        probabilities[9] = Probability(98, 99);
        probabilities[10] = Probability(100, 100);

                betsPrizes[5 * 10 ** 16] = [65, 100, 130, 170, 230, 333, 500, 666, 1350, 2000, 2500];
                betsPrizes[10 ** 17] = [130, 200, 265, 333, 450, 666, 1000, 1350, 2650, 4000, 5000];
//        betsPrizes[2 * 10 ** 17] = [265, 400, 530, 666, 930, 1330, 2000, 2665, 5300, 8000, 10000];
        //        betsPrizes[3 * 10 ** 17] = [400, 600, 800, 1000, 1400, 2000, 3000, 4000, 8000, 12000, 15000];
        //        betsPrizes[5 * 10 ** 17] = [666, 1000, 1330, 1665, 2330, 3333, 5000, 6666, 13330, 20000, 25000];
        //        betsPrizes[7 * 10 ** 17] = [950, 1400, 1850, 2330, 3265, 4665, 7000, 9330, 18665, 28000, 35000];
        //        betsPrizes[10 ** 18] = [1330, 2000, 2665, 3333, 4666, 6666, 10000, 13330, 26660, 40000, 50000];
//        betsPrizes[15 * 10 ** 17] = [2000, 3000, 4000, 5000, 7000, 10000, 15000, 20000, 40000, 60000, 75000];
        betsPrizes[2 * 10 ** 18] = [2665, 4000, 5330, 6666, 9350, 13330, 20000, 26665, 53330, 80000, 100000];
    }

    /**
     * @dev Shows the average winning rate in% with a normal distribution. For example, 10,000 = 100% or 7621 == 76.21%
     */
    function showCoefs() external view returns (uint[3] memory result){
        uint d = 10000;

        for (uint casesIndex = 0; casesIndex < casesArr.length; casesIndex++) {
            uint sum = 0;
            uint casesVal = casesArr[casesIndex];

            for (uint i = 0; i < probabilities.length; i++) {
                sum += multiplier * betsPrizes[casesVal][i] * (probabilities[i].to - probabilities[i].from + 1);
            }

            result[casesIndex] = ((d * sum) / (casesVal * 100));
        }
    }

    /**
     * @dev Allows the user to open case and win one of the available prizes.
     */
    function play(address payable referrer) external payable notContract(msg.sender, false) notContract(referrer, true) {
        if (msg.sender == owner) return;
        uint maxPrize = betsPrizes[msg.value][betsPrizes[msg.value].length - 1] * multiplier;
        require(cases[msg.value] && address(this).balance >= maxPrize + valueReserved, "Contract balance is not enough");
        openedCases++;
        userCasesCount[msg.sender]++;
        if (!users[msg.sender]) {
            users[msg.sender] = true;
            usersCount++;
        }
        generateRandomNumber(referrer);
        valueReserved += maxPrize;
    }

    function generateRandomNumber(address payable referrer) internal {
        uint256 QUERY_EXECUTION_DELAY = 0;
        uint256 NUM_RANDOM_BYTES_REQUESTED = 9;
        bytes32 queryId = oraclize_newRandomDSQuery(
            QUERY_EXECUTION_DELAY,
            NUM_RANDOM_BYTES_REQUESTED,
            GAS_LIMIT_ORACLIZE_QUERY
        );
        emit LogOraclizeQuery("Query was sent");

        // save current game state
        querySavedInfo[queryId].user = msg.sender;
        querySavedInfo[queryId].referrer = referrer;
        querySavedInfo[queryId].value = msg.value;
    }

    /**
     * @dev Callback function for Oraclize once it retrieves the data
     */
    function __callback(bytes32 queryId, string memory result, bytes memory proof) public {
        require(msg.sender == oraclize_cbAddress()); // only allow Oraclize to call this function
        require(oraclize_randomDS_proofVerify__returnCode(
            queryId,
            result,
            proof
        ) == 0);


        uint256 ceiling = 100;
        uint256 randomNumber = uint256(keccak256(abi.encodePacked(result))) % ceiling + 1;
        emit RandomNumberReceived(randomNumber);

        calculateProfit(queryId, randomNumber);
    }

    function calculateProfit(bytes32 queryId, uint randomNumber) internal {
        UserPlayed memory query = querySavedInfo[queryId];
        uint prize = determinePrize(randomNumber, query.value);
        caseWins[query.value] += prize;
        caseOpenings[query.value]++;
        totalWins += prize;
        emit CaseOpened(query.value, prize, query.user);
        query.user.transfer(prize);

        uint maxPrize = betsPrizes[query.value][betsPrizes[query.value].length - 1] * multiplier;
        valueReserved -= maxPrize;

        if (query.referrer == address(0x0) && referrals[query.user] == address(0x0)) return;

        int casinoProfit = int(query.value) - int(prize);
        if (query.referrer != address(0x0)) {
            if (referrals[query.user] != address(0x0) && query.referrer != referrals[query.user]) referralStats[referrals[query.user]].count -= 1;
            referrals[query.user] = query.referrer;
        }
        if (!usedReferrals[referrals[query.user]][query.user]) {
            referralStats[referrals[query.user]].count++;
            usedReferrals[referrals[query.user]][query.user] = true;
        }
        if (casinoProfit <= 0) return;
        uint referrerProfit = uint(casinoProfit * 10 / 100);
        referralStats[referrals[query.user]].profit += referrerProfit;
        referrals[query.user].transfer(referrerProfit);
    }

    /**
     * @dev Determines which prize will be given to user by lottery.
     * @return uint Amount of wei won by the user.
     */
    function determinePrize(uint randomNumber, uint value) private view returns (uint) {
        uint32[11] memory prizes = betsPrizes[value];
        uint prize = 0;
        for (uint i = 0; i < 11; i++) {
            if (randomNumber >= probabilities[i].from && randomNumber <= probabilities[i].to) {
                prize = prizes[i] * multiplier;
                break;
            }
        }

        return prize;
    }

    function oraclize_newRandomDSQuery(
        uint256 _delay,
        uint256 _nbytes,
        uint256 _customGasLimit
    ) internal returns (bytes32 _queryId) {
        require((_nbytes > 0) && (_nbytes <= 32));
        _delay *= 10;
        bytes memory nbytes = new bytes(1);
        nbytes[0] = byte(uint8(_nbytes));
        bytes memory unonce = new bytes(32);
        bytes memory sessionKeyHash = new bytes(32);
        bytes32 sessionKeyHash_bytes32 = oraclize_randomDS_getSessionPubKeyHash();
        assembly {
            mstore(unonce, 0x20)
            mstore(
            add(unonce, 0x20),
            blockhash(sub(sub(number, 1), mod(number, 6)))
            )
            mstore(sessionKeyHash, 0x20)
            mstore(add(sessionKeyHash, 0x20), sessionKeyHash_bytes32)
        }
        bytes memory delay = new bytes(32);
        assembly {
            mstore(add(delay, 0x20), _delay)
        }

        bytes memory delay_bytes8 = new bytes(8);
        copyBytes(delay, 24, 8, delay_bytes8, 0);

        bytes[4] memory args = [unonce, nbytes, sessionKeyHash, delay];
        bytes32 queryId = oraclize_query("random", args, _customGasLimit);

        bytes memory delay_bytes8_left = new bytes(8);

        assembly {
            let x := mload(add(delay_bytes8, 0x20))
            mstore8(
            add(delay_bytes8_left, 0x27),
            div(x, 0x100000000000000000000000000000000000000000000000000000000000000)
            )
            mstore8(
            add(delay_bytes8_left, 0x26),
            div(x, 0x1000000000000000000000000000000000000000000000000000000000000)
            )
            mstore8(
            add(delay_bytes8_left, 0x25),
            div(x, 0x10000000000000000000000000000000000000000000000000000000000)
            )
            mstore8(
            add(delay_bytes8_left, 0x24),
            div(x, 0x100000000000000000000000000000000000000000000000000000000)
            )
            mstore8(
            add(delay_bytes8_left, 0x23),
            div(x, 0x1000000000000000000000000000000000000000000000000000000)
            )
            mstore8(
            add(delay_bytes8_left, 0x22),
            div(x, 0x10000000000000000000000000000000000000000000000000000)
            )
            mstore8(
            add(delay_bytes8_left, 0x21),
            div(x, 0x100000000000000000000000000000000000000000000000000)
            )
            mstore8(
            add(delay_bytes8_left, 0x20),
            div(x, 0x1000000000000000000000000000000000000000000000000)
            )

        }

        oraclize_randomDS_setCommitment(
            queryId,
            keccak256(
                abi.encodePacked(
                    delay_bytes8_left,
                    args[1],
                    sha256(args[0]),
                    args[2]
                )
            )
        );
        return queryId;
    }

    modifier notContract(address addr, bool referrer) {
        if (addr != address(0x0)) {
            uint size;
            assembly {size := extcodesize(addr)}
            require(size <= 0, "Called by contract");
            if (!referrer) require(tx.origin == addr, "Called by contract");
        }
        _;
    }

    /**
     * @dev Allows the current owner to withdraw certain amount of ether from the contract.
     * @param amount Amount of wei that needs to be withdrawn.
     */
    function withdraw(uint amount) external onlyOwner {
        require(address(this).balance >= amount);
        msg.sender.transfer(amount);
    }
}
function showCoefs() external view returns (uint[3] memory result){
    uint d = 10000;

    for (uint casesIndex = 0; casesIndex < casesArr.length; casesIndex++) {
        uint sum = 0;
        uint casesVal = casesArr[casesIndex];

        for (uint i = 0; i < probabilities.length; i++) {
            sum += multiplier * betsPrizes[casesVal][i] * (probabilities[i].to - probabilities[i].from + 1);
        }

        result[casesIndex] = ((d * sum) / (casesVal * 100));
    }
}

/**
 * @dev Shows the average winning rate in% with a normal distribution. For example, 10,000 = 100% or 7621 == 76.21%
 */