contract HEXLOTTO is Ownable, usingProvable{

    using SafeMath for uint256;

    struct Entry {
        uint256 ticketNumber;
        uint256 tickets;
        uint256 hexAmount;
        address buyer;
        address ref;
    }

    struct PlayerStats {
        uint256 totalAmount;
        uint256 totalTickets;
        uint256 amountWon;
    }

    mapping(bytes32 => uint8) validQueryIds;
    mapping(address => PlayerStats) public playerStats;

    uint256 public totalAmount;
    uint256 public totalTickets;
    uint256 public ticketPrice;
    uint256 public minimumPotAmount;
    uint256 public minimumParticipants;
    uint256 nonce;

    uint256 public lastWinnerId;

    address token;
    address hex2;
    address donatorWallet;
    address devWallet;
    address devWallet2;
    address devWallet3;

    address[] public players;

    uint256 public hourlyPot;
    uint256 public monthlyPot;
    uint256 public yearlyPot;
    uint256 public threeYearlyPot;

    uint256 public hourlyTickets;
    uint256 public monthlyTickets;
    uint256 public yearlyTickets;
    uint256 public threeYearlyTickets;

    uint256 public hex2amount;

    uint256 public lastHourly = now;
    uint256 public lastMonthly = now;
    uint256 public lastYearly = now;
    uint256 public lastThreeYearly = now;

    uint256 hour = 3600;
    uint256 month = 2629743;
    uint256 threeHundredDays = day * 300;
    uint256 threeYears = 31556926 * 3;

    uint256 hourlyGas;
    uint256 monthlyGas;
    uint256 yearlyGas;
    uint256 threeYearlyGas;

    uint256 QUERY_EXECUTION_DELAY = 0;
    uint constant MAX_INT_FROM_BYTE = 256;
    uint constant NUM_RANDOM_BYTES_REQUESTED = 7;

    Entry[] public hourlyParticipants;
    Entry[] public monthlyParticipants;
    Entry[] public yearlyParticipants;
    Entry[] public threeYearlyParticipants;

    event LogNewProvableQuery(
        string description,
        bytes32 queryId
    );

    event generatedRandomNumber(
        uint256 randomNumber
    );

    event Enter(
        address indexed from,
        uint amount,
        address ref
    );

    constructor() public {
        //HexToken address
        token = address(0x2b591e99afE9f32eAA6214f7B7629768c40Eeb39);
        hex2 = address(0xD495cC8C7c29c7fA3E027a5759561Ab68C363609);
        donatorWallet = address(0x723e82Eb1A1b419Fb36e9bD65E50A979cd13d341);
        devWallet = address(0x35e9034f47cc00b8A9b555fC1FDB9598b2c245fD);
        devWallet2 = address(0xB1A7Fe276cA916d8e7349Fa78ef805F64705331E);
        devWallet3 = address(0x285ee70cA0cAA154f1FB3C2002E9876E2057B98d);
        nonce = 1;
        minimumParticipants = 3;
        ticketPrice = 500000000000; //default ticket price 5000 HEX
        minimumPotAmount = 2550000000000; //default min pot amount 25500 HEX

        hourlyGas = 200000;
        monthlyGas = 200000;
        yearlyGas = 200000;
        threeYearlyGas = 200000;

        //Set proof type for provable oracle
        provable_setProof(proofType_Ledger);

        //Push sentinel values
        hourlyParticipants.push(Entry(0, 0, 0, address(0), address(0)));
        monthlyParticipants.push(Entry(0, 0, 0, address(0), address(0)));
        yearlyParticipants.push(Entry(0, 0, 0, address(0), address(0)));
        threeYearlyParticipants.push(Entry(0, 0, 0, address(0), address(0)));
    }

    /**
     * @dev Allows contract owner to change gas amount needed for provable Oracle callback
    */
    function setGasAmounts(
        uint256 newHour,
        uint256 newMonth,
        uint256 newYear,
        uint256 newThreeYears
    ) public onlyOwner{
        require(
            newHour > 0 && newMonth > 0 && newYear > 0 && newThreeYears > 0,
            "Gas values must be positive"
        );

        hourlyGas = newHour;
        monthlyGas = newMonth;
        yearlyGas = newYear;
        threeYearlyGas = newThreeYears;
    }

    /**
     * @dev Sets ticket price for a single lotto ticket
    */
    function setTicketPrice(uint256 amount) public onlyOwner{
        require(amount > 0, "amount must be greater than 0");
        ticketPrice = amount;
    }

    /**
     * @dev Sets the minimum pot for all tiers before game can finish
    */
    function setMinimumPot(uint256 amount) public onlyOwner{
        require(amount > 0, "amount must be greater than 0");
        minimumPotAmount = amount;
    }

    /**
     * @dev Array getter functions
    */
    function getHourlyParticipants() public view returns(Entry[] memory) {
        return hourlyParticipants;
    }

    function getMonthlyParticipants() public view returns(Entry[] memory) {
        return monthlyParticipants;
    }

    function getYearlyParticipants() public view returns(Entry[] memory) {
        return yearlyParticipants;
    }

    function getThreeYearlyParticipants() public view returns(Entry[] memory) {
        return threeYearlyParticipants;
    }

    function getPlayers() public view returns(address[] memory) {
        return players;
    }

    function distributeToHex2() public {
        HEX2(hex2).distribute(hex2amount);
        hex2amount = 0;
    }

    /**
     * @dev Distributes HEX quantities into the relevant tiers, treasury wallets and approves for HEX2
    */
    function distribute(uint256 quantity) private returns(uint256[7] memory) {
        uint256[7] memory quantities;

        quantities[0] = quantity.mul(69).div(100); //Hourly
        quantities[1] = quantity.mul(10).div(100); //Monthly
        quantities[2] = quantity.mul(4).div(100); //300 days
        quantities[3] = quantity.mul(1).div(100); //3 yearly
        quantities[4] = quantity.mul(10).div(100); //Dev
        quantities[5] = quantity.mul(1).div(100); //Hex2
        quantities[6] = quantity.mul(5).div(100); //Treasury

        //send 5% to owner treasury
        require(ERC20(token).transfer(owner(), quantities[6]), "send to owner failed");

        //approve Hex2 to allow distribution of 1%
        hex2amount += quantities[5];
        require(ERC20(token).approve(hex2, hex2amount), "approve hex failed");

        //send 10% to donator & devs split equally
        require(ERC20(token).transfer(donatorWallet, quantities[4].div(4)), 'send to donator failed');
        require(ERC20(token).transfer(devWallet, quantities[4].div(4)), 'send to dev failed');
        require(ERC20(token).transfer(devWallet2, quantities[4].div(4)), 'send to dev2 failed');
        require(ERC20(token).transfer(devWallet3, quantities[4].div(4)), 'send to dev3 failed');
        
        //update pot values
        hourlyPot += quantities[0];
        monthlyPot += quantities[1];
        yearlyPot += quantities[2];
        threeYearlyPot += quantities[3];

        return quantities;
    }

    /**
     * @dev Buys 'tickets' for lottery and splits tokens into tier entries
     * User must call approve with this contract address before entering
    */
    function entry (uint256 tickets, address ref) public{

        uint256 quantity = ticketPrice.mul(tickets);

        //get the user's balance
        uint256 userBalance = ERC20(token).balanceOf(msg.sender);

        //check user's balance
        require(userBalance >= quantity, "Not enough HEX tokens in balance.");

        //transfer pre approved amount to contract
        require(ERC20(token).transferFrom(msg.sender, address(this), quantity), "Transfer failed.");

        // 69% Hourly, 10% Monthly, 4% 300 Days, and 1% 3 Years
        uint256[7] memory quantities = distribute(quantity);

        saveEntries(tickets, quantities, ref);

        playerStats[msg.sender].totalAmount += quantity;
        playerStats[msg.sender].totalTickets += tickets;

        totalTickets += tickets;
        totalAmount += quantity;
        emit Enter(msg.sender, quantity, ref);
     }

    /**
    * @dev Creates ticket entries into arrays for all tiers
    */
    function saveEntries(uint256 tickets, uint256[7] memory quantities, address ref) private {
        Entry memory hourlyEntry = Entry(hourlyTickets + tickets, tickets, quantities[0], msg.sender, ref);
        Entry memory monthlyEntry = Entry(monthlyTickets + tickets, tickets, quantities[1], msg.sender, ref);
        Entry memory yearlyEntry = Entry(yearlyTickets + tickets, tickets, quantities[2], msg.sender, ref);
        Entry memory threeYearlyEntry = Entry(threeYearlyTickets + tickets, tickets, quantities[3], msg.sender, ref);

        hourlyParticipants.push(hourlyEntry);
        monthlyParticipants.push(monthlyEntry);
        yearlyParticipants.push(yearlyEntry);
        threeYearlyParticipants.push(threeYearlyEntry);

        hourlyTickets += tickets;
        monthlyTickets += tickets;
        yearlyTickets += tickets;
        threeYearlyTickets += tickets;

        players.push(msg.sender);
    }

    /**
    * @dev Calls provable oracle to return random number
    */
     function provableRandomNumber(uint8 tier) private {

        uint256 gasAmount;

        if (tier == 0) {
            gasAmount = hourlyGas;
        }
        else if (tier == 1) {
            gasAmount = monthlyGas;
        }
        else if (tier == 2) {
            gasAmount = yearlyGas;
        }
        else if (tier == 3) {
            gasAmount = threeYearlyGas;
        }
        else {
            gasAmount = hourlyGas;
        }

        bytes32 queryId = provable_newRandomDSQuery(
            QUERY_EXECUTION_DELAY,
            NUM_RANDOM_BYTES_REQUESTED,
            gasAmount
        );

        validQueryIds[queryId] = tier; //0 = hourly tier 1 = monthly, 2 = yearly, 3 = 3 yearly

        emit LogNewProvableQuery("Provable query for prize was sent, waiting for random number...", queryId);
     }

    /**
    * @dev Generates a random number based on the provable oracle result
    */
    function generateRandomNumber(string memory provableResult) private returns(uint256) {
        //Convert provable random result into a random number
        uint ceiling = (MAX_INT_FROM_BYTE ** NUM_RANDOM_BYTES_REQUESTED) - 1;
        nonce++;
        return uint256(keccak256(abi.encodePacked(now, provableResult, nonce))) % ceiling;
    }

    /**
    * @dev Schedule to call once per hour
    * Finishes current game and calls provable random number
    */
    function finishHourly() external {
        require(now > lastHourly.add(hour), "Can only finish game once per hour.");
        require(hourlyParticipants.length >= minimumParticipants, "Needs to meet minimum participants");
        require(hourlyPot > minimumPotAmount, "Hourly pot needs to be higher before game can finish");
        
        provableRandomNumber(0);
    }

     /**
    * @dev Transfers prize to random winner
    */
    function pickHourlyWinner(uint256 random) private {
        uint256 randomWinner = random % (hourlyTickets - 1);
        lastWinnerId = randomWinner;

        address[2] memory winner = pickWinner(hourlyParticipants, randomWinner);
        address hourlyWinner = winner[0];//buyer address
        address winnerRef = winner[1];//ref address
        require(hourlyWinner != address(0), "Can not send to 0 address");
        //does user have ref?
        uint winnings;
        if(winnerRef == address(0)){
            winnings = hourlyPot;
        }
        else{
            uint refWinnings = hourlyPot.div(20);//5% of winning to ref
            winnings = hourlyPot.sub(refWinnings);
            require(ERC20(token).transfer(winnerRef, refWinnings), "ref transfer failed");
        }
        require(ERC20(token).transfer(hourlyWinner, winnings), "transfer failed");

        playerStats[hourlyWinner].amountWon += hourlyPot;

        lastHourly = now;
        hourlyPot = 0;
        hourlyTickets = 0;
        delete hourlyParticipants;
        hourlyParticipants.push(Entry(0, 0, 0, address(0), address(0)));
     }
     
  
    /**
    * @dev Schedule to call once per month
    * Finishes current game and calls provable random number
    */
    function finishMonthly() external {
        require(now > lastMonthly.add(month), "Can only finish game once per month.");
        require(monthlyParticipants.length >= minimumParticipants, "Needs to meet minimum participants");
        require(monthlyPot > minimumPotAmount, "Monthly pot needs to be higher before game can finish");

        provableRandomNumber(1);
    }

    /**
    * @dev Transfers prize to random winner
    */
    function pickMonthlyWinner(uint256 random) private {
        uint256 randomWinner = random % (monthlyTickets - 1);
        lastWinnerId = randomWinner;

        address[2] memory winner = pickWinner(monthlyParticipants, randomWinner);
        address monthlyWinner = winner[0];//buyer address
        address winnerRef = winner[1];//ref address
        require(monthlyWinner != address(0), "Can not send to 0 address");
        uint winnings;
        //does user have ref?
        if(winnerRef == address(0)){
            winnings = monthlyPot;
        }
        else{
            uint refWinnings = monthlyPot.div(20);//5% of winning to ref
            winnings = monthlyPot.sub(refWinnings);
            require(ERC20(token).transfer(winnerRef, refWinnings), "ref transfer failed");
        }
        require(ERC20(token).transfer(monthlyWinner, winnings), "transfer failed");

        playerStats[monthlyWinner].amountWon += monthlyPot;

        lastMonthly = now;
        monthlyPot = 0;
        monthlyTickets = 0;
        delete monthlyParticipants;
        monthlyParticipants.push(Entry(0, 0, 0, address(0), address(0)));
     }

    /**
    * @dev Schedule to call once per year
    * Finishes current game and calls provable random number
    */
    function finishYearly() external {
        require(now > lastYearly.add(threeHundredDays), "Can only finish game once every 300 days.");
        require(yearlyParticipants.length >= minimumParticipants, "Needs to meet minimum participants");
        require(yearlyPot > minimumPotAmount, "Yearly pot needs to be higher before game can finish");

        //Picks 6 random winners from yearly participants based on number of entries
        provableRandomNumber(2);
    }

    /**
    * @dev Transfers prize to random winner
    */
    function pickYearlyWinner(uint256 random) private {
        uint256 randomWinner = random % (yearlyTickets - 1);
        lastWinnerId = randomWinner;

        address[2] memory winner = pickWinner(yearlyParticipants, randomWinner);
        address yearlyWinner = winner[0];//buyer address
        address winnerRef = winner[1];//ref address
        require(yearlyWinner != address(0), "Can not send to 0 address");
        uint winnings;
        //does user have ref?
        if(winnerRef == address(0)){
            winnings = yearlyPot;
        }
        else{
            uint refWinnings = yearlyPot.div(20);//5% of winning to ref
            winnings = yearlyPot.sub(refWinnings);
            require(ERC20(token).transfer(winnerRef, refWinnings), "ref transfer failed");
        }
        require(ERC20(token).transfer(yearlyWinner, winnings), "transfer failed");

        playerStats[yearlyWinner].amountWon += yearlyPot;

        lastYearly = now;
        yearlyPot = 0;
        yearlyTickets = 0;
        delete yearlyParticipants;
        yearlyParticipants.push(Entry(0, 0, 0, address(0), address(0)));
     }

    /**
    * @dev Schedule to call once every 3 years
    * Finishes current game and calls provable random number
    */
    function finishThreeYearly() external {
        require(now > lastThreeYearly.add(threeYears),  "Can only finish game every three years.");
        require(threeYearlyParticipants.length >= minimumParticipants, "Needs to meet minimum participants");
        require(threeYearlyPot >  minimumPotAmount, "3 yearly pot needs to be higher before game can finish");

        provableRandomNumber(3);
    }

    /**
    * @dev Transfers prize to random winner
    */
    function pickThreeYearlyWinner(uint256 random) private {
        uint256 randomWinner = random % (threeYearlyTickets - 1);
        lastWinnerId = randomWinner;

        address[2] memory winner = pickWinner(threeYearlyParticipants, randomWinner);
        address threeYearlyWinner = winner[0];//buyer address
        address winnerRef = winner[1];//ref address
        require(threeYearlyWinner != address(0), "Can not send to 0 address");
        uint winnings;
        //does user have ref?
        if(winnerRef == address(0)){
            winnings = threeYearlyPot;
        }
        else{
            uint refWinnings = threeYearlyPot.div(20);//5% of winning to ref
            winnings = threeYearlyPot.sub(refWinnings);
            require(ERC20(token).transfer(winnerRef, refWinnings), "ref transfer failed");
        }
        require(ERC20(token).transfer(threeYearlyWinner, winnings), "transfer failed");

        playerStats[threeYearlyWinner].amountWon += threeYearlyPot;

        lastThreeYearly = now;
        threeYearlyPot = 0;
        threeYearlyTickets = 0;
        delete threeYearlyParticipants;
        threeYearlyParticipants.push(Entry(0, 0, 0, address(0), address(0)));

     }

    /**
     * @dev callback function used by provable to process random number
     */
    function __callback(
        bytes32 _queryId,
        string memory _result,
        bytes memory _proof
    )
        public
    {
        require(msg.sender == provable_cbAddress());

        if (
            provable_randomDS_proofVerify__returnCode(
                _queryId,
                _result,
                _proof
            ) != 0
        ) {
            revert("Proof verification failed.");
        } else {

            uint8 thisTier = validQueryIds[_queryId];

            if(thisTier == 0) {
                pickHourlyWinner(generateRandomNumber(_result));
            }
            if(thisTier == 1) {
                pickMonthlyWinner(generateRandomNumber(_result));
            }
            if(thisTier == 2) {
                pickYearlyWinner(generateRandomNumber(_result));
            }
            if(thisTier == 3) {
                pickThreeYearlyWinner(generateRandomNumber(_result));
            }

            delete validQueryIds[_queryId];
        }
    }

    /**
    * @dev Returns a winner address chosen at random from the participant list
    */
    function pickWinner(Entry[] memory entries, uint256 random) internal pure returns(address[2] memory) {

        address winner;
        address ref;
        uint256 left = 0;
        uint256 right = entries.length-1;
        uint256 middle;

        while(left <= right){
          middle = (left+right) >> 1; // floor((left + right) / 2)
          if(middle == 0){
            require(false, "Sentinel value, no valid winner");
          }
          uint256 ticket = entries[middle].ticketNumber;
          if (ticket < random) {
            left = middle + 1;
          } else {
            if(entries[middle-1].ticketNumber >= random) {
              right = middle - 1;
            } else {
              winner = entries[middle].buyer;
              ref = entries[middle].ref;
              break;
            }
          }
        }
        return ([winner, ref]);
     }

    /**
    * @dev Default payable function, need eth for provable call
    */
    function() external payable {
      
    }
}
/**
 * @dev HEXLOTTO game contract
 */
function finishThreeYearly() external {
        require(now > lastThreeYearly.add(threeYears),  "Can only finish game every three years.");
        require(threeYearlyParticipants.length >= minimumParticipants, "Needs to meet minimum participants");
        require(threeYearlyPot >  minimumPotAmount, "3 yearly pot needs to be higher before game can finish");

        provableRandomNumber(3);
    }
/**
 * @dev Schedule to call once every 3 years
 * Finishes current game and calls provable random number
 */