// File: EthexJackpot.sol



pragma solidity ^0.5.0;

/**
 * (E)t)h)e)x) Jackpot Contract 
 *  This smart-contract is the part of Ethex Lottery fair game.
 *  See latest version at https://github.com/ethex-bet/ethex-lottery 
 *  http://ethex.bet
 */

contract EthexJackpot {
    mapping(uint256 => address payable) tickets;
    uint256 public numberEnd;
    uint256 public dailyAmount;
    uint256 public weeklyAmount;
    uint256 public monthlyAmount;
    uint256 public seasonalAmount;
    bool private dailyProcessed;
    bool private weeklyProcessed;
    bool private monthlyProcessed;
    bool private seasonalProcessed;
    uint256 private dailyNumberStartPrev;
    uint256 private weeklyNumberStartPrev;
    uint256 private monthlyNumberStartPrev;
    uint256 private seasonalNumberStartPrev;
    uint256 private dailyStart;
    uint256 private weeklyStart;
    uint256 private monthlyStart;
    uint256 private seasonalStart;
    uint256 private dailyEnd;
    uint256 private weeklyEnd;
    uint256 private monthlyEnd;
    uint256 private seasonalEnd;
    uint256 private dailyNumberStart;
    uint256 private weeklyNumberStart;
    uint256 private monthlyNumberStart;
    uint256 private seasonalNumberStart;
    uint256 private dailyNumberEndPrev;
    uint256 private weeklyNumberEndPrev;
    uint256 private monthlyNumberEndPrev;
    uint256 private seasonalNumberEndPrev;
    address public lotoAddress;
    address payable private owner;
    
    event Jackpot (
        uint256 number,
        uint256 count,
        uint256 amount,
        byte jackpotType
    );
    
    event Ticket (
        bytes16 indexed id,
        uint256 number
    );
    
    uint256 constant DAILY = 5000;
    uint256 constant WEEKLY = 35000;
    uint256 constant MONTHLY = 140000;
    uint256 constant SEASONAL = 420000;
    
    constructor() public payable {
        owner = msg.sender;
        dailyStart = block.number / DAILY * DAILY;
        dailyEnd = dailyStart + DAILY;
        dailyProcessed = true;
        weeklyStart = block.number / WEEKLY * WEEKLY;
        weeklyEnd = weeklyStart + WEEKLY;
        weeklyProcessed = true;
        monthlyStart = block.number / MONTHLY * MONTHLY;
        monthlyEnd = monthlyStart + MONTHLY;
        monthlyProcessed = true;
        seasonalStart = block.number / SEASONAL * SEASONAL;
        seasonalEnd = seasonalStart + SEASONAL;
        seasonalProcessed = true;
    }

    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }
    
    modifier onlyLoto {
        require(msg.sender == lotoAddress, "Loto only");
        _;
    }
    
    function migrate(address payable newContract) external onlyOwner {
        newContract.transfer(address(this).balance);
    }

    function registerTicket(bytes16 id, address payable gamer) external onlyLoto {
        uint256 number = numberEnd + 1;
        if (block.number >= dailyEnd) {
            setDaily();
            dailyNumberStart = number;
        }
        if (block.number >= weeklyEnd) {
            setWeekly();
            weeklyNumberStart = number;
        }
        if (block.number >= monthlyEnd) {
            setMonthly();
            monthlyNumberStart = number;
        }
        if (block.number >= seasonalEnd) {
            setSeasonal();
            seasonalNumberStart = number;
        }
        numberEnd = number;
        tickets[number] = gamer;
        emit Ticket(id, number);
    }
    
    function setLoto(address loto) external onlyOwner {
        lotoAddress = loto;
    }
    
    function payIn() external payable {
        uint256 amount = msg.value / 4;
        dailyAmount += amount;
        weeklyAmount += amount;
        monthlyAmount += amount;
        seasonalAmount += amount;
    }
    
    function settleJackpot() external {
        if (block.number >= dailyEnd) {
            setDaily();
        }
        if (block.number >= weeklyEnd) {
            setWeekly();
        }
        if (block.number >= monthlyEnd) {
            setMonthly();
        }
        if (block.number >= seasonalEnd) {
            setSeasonal();
        }
        
        if (block.number == dailyStart)
            return;
        
        uint48 modulo = uint48(bytes6(blockhash(dailyStart) << 29));
        
        uint256 dailyPayAmount;
        uint256 weeklyPayAmount;
        uint256 monthlyPayAmount;
        uint256 seasonalPayAmount;
        uint256 dailyWin;
        uint256 weeklyWin;
        uint256 monthlyWin;
        uint256 seasonalWin;
        if (dailyProcessed == false) {
            dailyPayAmount = dailyAmount; 
            dailyAmount = 0;
            dailyProcessed = true;
            dailyWin = getNumber(dailyNumberStartPrev, dailyNumberEndPrev, modulo);
            emit Jackpot(dailyWin, dailyNumberEndPrev - dailyNumberStartPrev + 1, dailyPayAmount, 0x01);
        }
        if (weeklyProcessed == false) {
            weeklyPayAmount = weeklyAmount;
            weeklyAmount = 0;
            weeklyProcessed = true;
            weeklyWin = getNumber(weeklyNumberStartPrev, weeklyNumberEndPrev, modulo);
            emit Jackpot(weeklyWin, weeklyNumberEndPrev - weeklyNumberStartPrev + 1, weeklyPayAmount, 0x02);
        }
        if (monthlyProcessed == false) {
            monthlyPayAmount = monthlyAmount;
            monthlyAmount = 0;
            monthlyProcessed = true;
            monthlyWin = getNumber(monthlyNumberStartPrev, monthlyNumberEndPrev, modulo);
            emit Jackpot(monthlyWin, monthlyNumberEndPrev - monthlyNumberStartPrev + 1, monthlyPayAmount, 0x04);
        }
        if (seasonalProcessed == false) {
            seasonalPayAmount = seasonalAmount;
            seasonalAmount = 0;
            seasonalProcessed = true;
            seasonalWin = getNumber(seasonalNumberStartPrev, seasonalNumberEndPrev, modulo);
            emit Jackpot(seasonalWin, seasonalNumberEndPrev - seasonalNumberStartPrev + 1, seasonalPayAmount, 0x08);
        }
        if (dailyPayAmount > 0)
            tickets[dailyWin].transfer(dailyPayAmount);
        if (weeklyPayAmount > 0)
            tickets[weeklyWin].transfer(weeklyPayAmount);
        if (monthlyPayAmount > 0)
            tickets[monthlyWin].transfer(monthlyPayAmount);
        if (seasonalPayAmount > 0)
            tickets[seasonalWin].transfer(seasonalPayAmount);
    }
    
    function setDaily() private {
        dailyProcessed = dailyNumberEndPrev == numberEnd;
        dailyStart = dailyEnd;
        dailyEnd = dailyStart + DAILY;
        dailyNumberStartPrev = dailyNumberStart;
        dailyNumberEndPrev = numberEnd;
    }
    
    function setWeekly() private {
        weeklyProcessed = weeklyNumberEndPrev == numberEnd;
        weeklyStart = weeklyEnd;
        weeklyEnd = weeklyStart + WEEKLY;
        weeklyNumberStartPrev = weeklyNumberStart;
        weeklyNumberEndPrev = numberEnd;
    }
    
    function setMonthly() private {
        monthlyProcessed = monthlyNumberEndPrev == numberEnd;
        monthlyStart = monthlyEnd;
        monthlyEnd = monthlyStart + MONTHLY;
        monthlyNumberStartPrev = monthlyNumberStart;
        monthlyNumberEndPrev = numberEnd;
    }
    
    function setSeasonal() private {
        seasonalProcessed = seasonalNumberEndPrev == numberEnd;
        seasonalStart = seasonalEnd;
        seasonalEnd = seasonalStart + SEASONAL;
        seasonalNumberStartPrev = seasonalNumberStart;
        seasonalNumberEndPrev = numberEnd;
    }
    
    function getNumber(uint256 startNumber, uint256 endNumber, uint48 modulo) pure private returns (uint256) {
        return startNumber + modulo % (endNumber - startNumber + 1);
    }
}

// File: EthexLoto.sol



pragma solidity ^0.5.0;

import "./EthexJackpot.sol";

contract EthexLoto {
    struct Bet {
        uint256 blockNumber;
        uint256 amount;
        bytes16 id;
        bytes6 bet;
        address payable gamer;
    }
    
    struct Payout {
        uint256 amount;
        bytes32 blockHash;
        bytes16 id;
        address payable gamer;
    }
    
    Bet[] betArray;
    
    address payable public jackpotAddress;
    address payable private owner;

    event Result (
        uint256 amount,
        bytes32 blockHash,
        bytes16 indexed id,
        address indexed gamer
    );
    
    uint8 constant N = 16;
    uint256 constant MIN_BET = 0.01 ether;
    uint256 constant MAX_BET = 100 ether;
    uint256 constant PRECISION = 1 ether;
    uint256 constant JACKPOT_PERCENT = 10;
    
    constructor(address payable jackpot) public payable {
        owner = msg.sender;
        jackpotAddress = jackpot;
    }
    
    function() external payable { }
    
    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }
    
    function placeBet(bytes22 params) external payable {
        require(msg.value >= MIN_BET, "Bet amount should be greater or equal than minimal amount");
        require(msg.value <= MAX_BET, "Bet amount should be lesser or equal than maximal amount");
        require(bytes16(params) != 0, "Id should not be 0");
        
        bytes16 id = bytes16(params);
        bytes6 bet = bytes6(params << 128);
        
        uint256 jackpotFee = msg.value * JACKPOT_PERCENT * PRECISION / 100 / PRECISION;
        betArray.push(Bet(block.number, msg.value - jackpotFee, id, bet, msg.sender));
        
        uint8 markedCount;
        for (uint i = 0; i < bet.length; i++) {
            if (bet[i] > 0x13)
                continue;
            markedCount++;
        }
        if (markedCount > 1)
            EthexJackpot(jackpotAddress).registerTicket(id, msg.sender);
        
        EthexJackpot(jackpotAddress).payIn.value(jackpotFee)();
    }
    
    function settleBets() external {
        if (betArray.length == 0)
            return;

        Payout[] memory payouts = new Payout[](betArray.length);
        Bet[] memory missedBets = new Bet[](betArray.length);
        uint256 totalPayout;
        uint i = betArray.length;
        do {
            i--;
            if(betArray[i].blockNumber >= block.number)
                missedBets[i] = betArray[i];
            else {
                bytes32 blockHash = blockhash(betArray[i].blockNumber);
                uint256 coefficient = PRECISION;
                uint8 markedCount;
                uint8 matchesCount;
                uint256 divider = 1;
                for (uint8 j = 0; j < betArray[i].bet.length; j++) {
                    if (betArray[i].bet[j] > 0x13)
                        continue;
                    markedCount++;
                    byte field;
                    if (j % 2 == 0)
                        field = blockHash[29 + j / 2] >> 4;
                    else
                        field = blockHash[29 + j / 2] & 0x0F;
                    if (betArray[i].bet[j] < 0x10) {
                        if (field == betArray[i].bet[j])
                            matchesCount++;
                        else
                            divider *= 15 + N;
                        continue;
                    }
                    if (betArray[i].bet[j] == 0x10) {
                        if (field > 0x09 && field < 0x10) {
                            matchesCount++;
                            divider *= 6;
                        } else
                            divider *= 10 + N;
                        continue;
                    }
                    if (betArray[i].bet[j] == 0x11) {
                        if (field < 0x0A) {
                            matchesCount++;
                            divider *= 10;
                        } else
                            divider *= 6 + N;
                        continue;
                    }
                    if (betArray[i].bet[j] == 0x12) {
                        if (field < 0x0A && field & 0x01 == 0x01) {
                            matchesCount++;
                            divider *= 5;
                        } else
                            divider *= 11 + N;
                        continue;
                    }
                    if (betArray[i].bet[j] == 0x13) {
                        if (field < 0x0A && field & 0x01 == 0x0) {
                            matchesCount++;
                            divider *= 5;
                        } else
                            divider *= 11 + N;
                        continue;
                    }
                }
            
                if (matchesCount == 0)
                    coefficient = 0;
                else {
                    uint256 missedCount = markedCount - matchesCount;
                    divider *= missedCount ** missedCount;
                    coefficient = coefficient * 16**uint256(markedCount) / divider;
                }
                
                uint payoutAmount = betArray[i].amount * coefficient / PRECISION;
                if (payoutAmount == 0 && matchesCount > 0)
                    payoutAmount = matchesCount;
                payouts[i] = Payout(payoutAmount, blockHash, betArray[i].id, betArray[i].gamer);
                totalPayout += payoutAmount;
            }
            betArray.pop();
        } while (i > 0);
        
        i = missedBets.length;
        do {
            i--;
            if (missedBets[i].id != 0)
                betArray.push(missedBets[i]);
        } while (i > 0);
        
        uint balance = address(this).balance;
        for (i = 0; i < payouts.length; i++) {
            if (payouts[i].id > 0) {
                if (totalPayout > balance)
                    emit Result(balance * payouts[i].amount * PRECISION / totalPayout / PRECISION, payouts[i].blockHash, payouts[i].id, payouts[i].gamer);
                else
                    emit Result(payouts[i].amount, payouts[i].blockHash, payouts[i].id, payouts[i].gamer);
            }
        }
        for (i = 0; i < payouts.length; i++) {
            if (payouts[i].amount > 0) {
                if (totalPayout > balance)
                    payouts[i].gamer.transfer(balance * payouts[i].amount * PRECISION / totalPayout / PRECISION);
                else
                    payouts[i].gamer.transfer(payouts[i].amount);
            }
        }
    }
    
    function migrate(address payable newContract) external onlyOwner {
        newContract.transfer(address(this).balance);
    }
}