contract LuckyStrike is usingOraclize {

    /* --- see: https://github.com/oraclize/ethereum-examples/blob/master/solidity/random-datasource/randomExample.sol */

    // see: https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC20/BasicToken.sol
    using SafeMath for uint256;
    using SafeMath for uint16;

    address public owner;
    address admin;
    //
    uint256 public ticketPriceInWei = 20000000000000000; // 0.02 ETH
    uint256 public tokenPriceInWei = 150000000000000; // 0.00015 ETH
    uint16 public maxTicketsToBuyInOneTransaction = 333; //
    //
    uint256 public eventsCounter;
    //
    mapping(uint256 => address) public theLotteryTicket;
    uint256 public ticketsTotal;
    //
    address public kingOfTheHill;
    uint256 public kingOfTheHillTicketsNumber;

    mapping(address => uint256) public reward;

    event rewardPaid(uint256 indexed eventsCounter, address indexed to, uint256 sum); //
    function getReward() public {
        require(reward[msg.sender] > 0);
        msg.sender.transfer(reward[msg.sender]);

        eventsCounter = eventsCounter + 1;
        rewardPaid(eventsCounter, msg.sender, reward[msg.sender]);
        sum[affiliateRewards] = sum[affiliateRewards].sub(reward[msg.sender]);
        reward[msg.sender] = 0;
    }

    // gas for oraclize_query:
    uint256 public oraclizeCallbackGas = 230000; // amount of gas we want Oraclize to set for the callback function

    // > to be able to read it from browser after updating
    uint256 public currentOraclizeGasPrice; //
    function oraclizeGetPrice() public returns (uint256){
        currentOraclizeGasPrice = oraclize_getPrice("random", oraclizeCallbackGas);
        return currentOraclizeGasPrice;
    }

    function getContractsWeiBalance() public view returns (uint256) {
        return this.balance;
    }

    // mapping to keep sums on accounts (including 'income'):
    mapping(uint8 => uint256) public sum;
    uint8 public instantGame = 0;
    uint8 public dailyJackpot = 1;
    uint8 public weeklyJackpot = 2;
    uint8 public monthlyJackpot = 3;
    uint8 public yearlyJackpot = 4;
    uint8 public income = 5;
    uint8 public marketingFund = 6;
    uint8 public affiliateRewards = 7; //
    uint8 public playersBets = 8;

    mapping(uint8 => uint256) public period; // in seconds

    event withdrawalFromMarketingFund(uint256 indexed eventsCounter, uint256 sum); //
    function withdrawFromMarketingFund() public {
        require(msg.sender == owner);
        owner.transfer(sum[marketingFund]);

        eventsCounter = eventsCounter + 1;
        withdrawalFromMarketingFund(eventsCounter, sum[marketingFund]);

        sum[marketingFund] = 0;
    }

    mapping(uint8 => bool) public jackpotPlayIsRunning; //

    // allocation:
    mapping(uint8 => uint16) public rate;

    // JackpotCounters (starts with 0):
    mapping(uint8 => uint256) jackpotCounter;
    mapping(uint8 => uint256) public lastJackpotTime;  // unix time

    // uint256 public lastDividendsPaymentTime; // unix time, for 'income' only

    address public luckyStrikeTokensContractAddress;

    LuckyStrikeTokens public luckyStrikeTokens;

    /* --- constructor */

    // (!) requires acces to Oraclize contract
    // will fail on JavaScript VM
    function LuckyStrike() public {

        admin = msg.sender;

        // sets the Ledger authenticity proof in the constructor
        oraclize_setProof(proofType_Ledger);

    }

    function init(address _luckyStrikeTokensContractAddress) public payable {

        require(ticketsTotal == 0);
        require(msg.sender == admin);

        owner = 0x0bBAb60c495413c870F8cABF09436BeE9fe3542F;

        require(msg.value / ticketPriceInWei >= 1);

        luckyStrikeTokensContractAddress = _luckyStrikeTokensContractAddress;

        // should be updated every time we use it
        // now we just get value to show in webapp
        oraclizeGetPrice();

        kingOfTheHill = msg.sender;
        ticketsTotal = kingOfTheHillTicketsNumber = 1;
        theLotteryTicket[1] = kingOfTheHill;

        // initialize jackpot periods
        // see: https://solidity.readthedocs.io/en/v0.4.20/units-and-global-variables.html#time-units
        period[dailyJackpot] = 1 days;
        period[weeklyJackpot] = 1 weeks;
        period[monthlyJackpot] = 30 days;
        period[yearlyJackpot] = 1 years;
        // for testing:
        //        period[dailyJackpot] = 60 * 1;
        //        period[weeklyJackpot] = 60 * 3;
        //        period[monthlyJackpot] = 60 * 5;
        //        period[yearlyJackpot] = 60 * 7;

        // set last block numbers and timestamps for jackpots:
        for (uint8 i = dailyJackpot; i <= yearlyJackpot; i++) {
            lastJackpotTime[i] = block.timestamp;
        }

        rate[instantGame] = 8500;
        rate[dailyJackpot] = 500;
        rate[weeklyJackpot] = 300;
        rate[monthlyJackpot] = 100;
        rate[yearlyJackpot] = 100;
        rate[income] = 500;

        luckyStrikeTokens = LuckyStrikeTokens(luckyStrikeTokensContractAddress);

    }

    /* --- Tokens contract information   */
    function tokensTotalSupply() public view returns (uint256) {
        return luckyStrikeTokens.totalSupply();
    }

    function tokensBalanceOf(address acc) public view returns (uint256){
        return luckyStrikeTokens.balanceOf(acc);
    }

    function weiInTokensContract() public view returns (uint256){
        return luckyStrikeTokens.balance;
    }

    function tokenSaleIsRunning() public view returns (bool) {
        return luckyStrikeTokens.tokenSaleIsRunning();
    }

    event AllocationAdjusted(
        uint256 indexed eventsCounter,
        address by,
        uint16 instantGame,
        uint16 dailyJackpot,
        uint16 weeklyJackpot,
        uint16 monthlyJackpot,
        uint16 yearlyJackpot,
        uint16 income);

    function adjustAllocation(
        uint16 _instantGame,
        uint16 _dailyJackpot,
        uint16 _weeklyJackpot,
        uint16 _monthlyJackpot,
        uint16 _yearlyJackpot,
        uint16 _income) public {

        // only owner !!!
        require(msg.sender == owner);

        rate[instantGame] = _instantGame;
        rate[dailyJackpot] = _dailyJackpot;
        rate[weeklyJackpot] = _weeklyJackpot;
        rate[monthlyJackpot] = _monthlyJackpot;
        rate[yearlyJackpot] = _yearlyJackpot;
        rate[income] = _income;

        // check if provided %% amount to 10,000
        uint16 _sum = 0;
        for (uint8 i = instantGame; i <= income; i++) {
            _sum = _sum + rate[i];
        }

        require(_sum == 10000);

        eventsCounter = eventsCounter + 1;
        AllocationAdjusted(
            eventsCounter,
            msg.sender,
            rate[instantGame],
            rate[dailyJackpot],
            rate[weeklyJackpot],
            rate[monthlyJackpot],
            rate[yearlyJackpot],
            rate[income]
        );

    } // end of adjustAllocation

    // this function calculates jackpots/income allocation and returns prize for the instant game
    uint256 sumAllocatedInWeiCounter;

    event SumAllocatedInWei(
        uint256 indexed eventsCounter,
        uint256 indexed sumAllocatedInWeiCounter,
        address betOf,
        uint256 bet, // 0
        uint256 dailyJackpot, // 1
        uint256 weeklyJackpot, // 2;
        uint256 monthlyJackpot, // 3;
        uint256 yearlyJackpot, // 4;
        uint256 income,
        uint256 affiliateRewards,
        uint256 payToWinner
    );

    function allocateSum(uint256 _sum, address loser) private returns (uint256) {

        // for event
        // https://solidity.readthedocs.io/en/v0.4.24/types.html#allocating-memory-arrays
        uint256[] memory jackpotsSumAllocation = new uint256[](5);

        // jackpots:
        for (uint8 i = dailyJackpot; i <= yearlyJackpot; i++) {
            uint256 sumToAdd = _sum * rate[i] / 10000;
            sum[i] = sum[i].add(sumToAdd);
            // for event:
            jackpotsSumAllocation[i] = sumToAdd;
        }

        // income before affiliate reward subtraction:
        uint256 incomeSum = (_sum * rate[income]) / 10000;
        // referrer reward:
        uint256 refSum = 0;
        if (referrer[loser] != address(0)) {

            address referrerAddress = referrer[loser];

            refSum = incomeSum / 2;
            incomeSum = incomeSum.sub(refSum);

            reward[referrerAddress] = reward[referrerAddress].add(refSum);

            sum[affiliateRewards] = sum[affiliateRewards].add(refSum);
        }

        sum[income] = sum[income].add(incomeSum);

        uint256 payToWinner = _sum * rate[instantGame] / 10000;

        eventsCounter = eventsCounter + 1;
        sumAllocatedInWeiCounter = sumAllocatedInWeiCounter + 1;
        SumAllocatedInWei(
            eventsCounter,
            sumAllocatedInWeiCounter,
            loser,
            _sum,
            jackpotsSumAllocation[1], //  dailyJackpot
            jackpotsSumAllocation[2], // weeklyJackpot
            jackpotsSumAllocation[3], // monthlyJackpot
            jackpotsSumAllocation[4], //  yearlyJackpot
            incomeSum,
            refSum,
            payToWinner
        );

        return payToWinner;
    }

    /* -------------- GAME: --------------*/

    /* --- Instant Game ------- */
    uint256 public instantGameCounter; // id's of the instant games
    //
    // to allow only one game for the given address simultaneously:
    mapping(address => bool) public instantGameIsRunning;
    //
    mapping(address => uint256) public lastInstantGameBlockNumber; // for address
    mapping(address => uint256) public lastInstantGameTicketsNumber; // for address

    // first step for player is to make a bet:
    uint256 public betCounter;

    event BetPlaced(
        uint256 indexed eventsCounter,
        uint256 indexed betCounter,
        address indexed player,
        uint256 betInWei,
        uint256 ticketsBefore,
        uint256 newTickets
    );

    function placeABetInternal(uint value) private {

        require(msg.sender != kingOfTheHill);

        // only one game allowed for the address at the given moment:
        require(!instantGameIsRunning[msg.sender]);

        // number of new tickets to create;
        uint256 newTickets = value / ticketPriceInWei;

        eventsCounter = eventsCounter + 1;
        betCounter = betCounter + 1;
        BetPlaced(eventsCounter, betCounter, msg.sender, value, ticketsTotal, newTickets);

        uint256 playerBetToPlace = newTickets.mul(ticketPriceInWei);

        sum[playersBets] = sum[playersBets].add(playerBetToPlace);

        require(newTickets > 0 && newTickets <= maxTicketsToBuyInOneTransaction);

        uint256 newTicketsTotal = ticketsTotal.add(newTickets);
        // new tickets included in jackpot games instantly:
        for (uint256 i = ticketsTotal + 1; i <= newTicketsTotal; i++) {
            theLotteryTicket[i] = msg.sender;
        }

        ticketsTotal = newTicketsTotal;

        lastInstantGameTicketsNumber[msg.sender] = newTickets;
        instantGameIsRunning[msg.sender] = true;
        lastInstantGameBlockNumber[msg.sender] = block.number;
    }

    function placeABet() public payable {
        placeABetInternal(msg.value);
    }

    mapping(address => address) public referrer;

    function placeABetWithReferrer(address _referrer) public payable {
        /* referrer: */
        if (referrer[msg.sender] == 0x0000000000000000000000000000000000000000) {
            referrer[msg.sender] = _referrer;
        }
        placeABetInternal(msg.value);
    }

    event Investment(
        uint256 indexed eventsCounter, //.1
        address indexed by, //............2
        uint256 sum, //...................3
        uint256 sumToMarketingFund, //....4
        uint256 bet, //...................5
        uint256 tokens //.................6
    ); //
    function investAndPlay() public payable {

        // require( luckyStrikeTokens.tokenSaleIsRunning());
        // < we will check this in luckyStrikeTokens.mint method

        uint256 sumToMarketingFund = msg.value / 5;

        sum[marketingFund] = sum[marketingFund].add(sumToMarketingFund);

        uint256 bet = msg.value.sub(sumToMarketingFund);

        placeABetInternal(bet);

        // uint256 tokensToMint = msg.value / ticketPriceInWei;
        // uint256 tokensToMint = bet / tokenPriceInWei;
        uint256 tokensToMint = sumToMarketingFund / tokenPriceInWei;

        // require(bet / ticketPriceInWei > 0); // > makes more complicated
        luckyStrikeTokens.mint(msg.sender, tokensToMint, sumToMarketingFund);

        eventsCounter = eventsCounter + 1;
        Investment(
            eventsCounter, //......1
            msg.sender, //.........2
            msg.value, //..........3
            sumToMarketingFund, //.4
            bet, //................5
            tokensToMint //........6
        );
    }

    function investAndPlayWithReferrer(address _referrer) public payable {
        if (referrer[msg.sender] == 0x0000000000000000000000000000000000000000) {
            referrer[msg.sender] = _referrer;
        }
        investAndPlay();
    }

    /*
        function invest() public payable {
            //  require(luckyStrikeTokens.tokenSaleIsRunning());
            // < we will check this in luckyStrikeTokens.mint method

            sum[marketingFund] = sum[marketingFund].add(msg.value);

            uint256 tokensToMint = msg.value / ticketPriceInWei;

            // uint256 bonus = (tokensToMint / 100).mul(75);
            uint256 bonus1part = tokensToMint / 2;
            uint256 bonus2part = tokensToMint / 4;
            uint256 bonus = bonus1part.add(bonus2part);

            tokensToMint = tokensToMint.add(bonus);

            // require(bet / ticketPriceInWei > 0); // > makes more complicated
            luckyStrikeTokens.mint(
                msg.sender,
                tokensToMint,
                msg.value // all sum > investment
            );

            eventsCounter = eventsCounter + 1;
            Investment(
                eventsCounter, //..1
                msg.sender, //.....2
                msg.value, //......3
                msg.value, //......4
                0, //..............5 (bet)
                tokensToMint //....6
            );
        }

        function investWithReferrer(address _referrer) public payable {
            if (referrer[msg.sender] == 0x0000000000000000000000000000000000000000) {
                referrer[msg.sender] = _referrer;
            }
            invest();
        }
    */

    // second step in instant game:
    event InstantGameResult (
        uint256 indexed eventsCounter, //...0
        uint256 gameId, // .................1
        bool theBetPlayed, //...............2
        address indexed challenger, //......3
        address indexed king, //............4
        uint256 kingsTicketsNumber, //......5
        address winner, //..................6
        uint256 prize, //...................7
        uint256 ticketsInTheInstantGame, //.8
        uint256 randomNumber, //............9
        address triggeredBy //..............10
    );

    function play(address player) public {

        require(instantGameIsRunning[player]);
        require(lastInstantGameBlockNumber[player] < block.number);
        // block number with the bet must be no more than 255 blocks before the current block
        // or we get 0 as blockhash

        instantGameCounter = instantGameCounter + 1;

        uint256 playerBet = lastInstantGameTicketsNumber[player].mul(ticketPriceInWei);
        // in any case playerBet should be subtracted sum[playerBets]
        sum[playersBets] = sum[playersBets].sub(playerBet);

        // TODO: recheck this >
        if (block.number - lastInstantGameBlockNumber[player] > 250) {
            eventsCounter = eventsCounter + 1;
            InstantGameResult(
                eventsCounter,
                instantGameCounter,
                false,
                player,
                address(0), // kingOfTheHill, // oldKingOfTheHill,
                0,
                address(0), // winner,
                0, // prize,
                0, // lastInstantGameTicketsNumber[player], // ticketsInTheInstantGame,
                0, // randomNumber,
                msg.sender // triggeredBy
            );

            // player.transfer(playerBet);
            sum[income] = sum[income].add(playerBet);

            lastInstantGameTicketsNumber[player] = 0;
            instantGameIsRunning[player] = false;
            return;
        }

        address oldKingOfTheHill = kingOfTheHill;
        uint256 oldKingOfTheHillTicketsNumber = kingOfTheHillTicketsNumber;
        uint256 ticketsInTheInstantGame = kingOfTheHillTicketsNumber.add(lastInstantGameTicketsNumber[player]);

        // TODO: recheck this
        bytes32 seed = keccak256(
            block.blockhash(lastInstantGameBlockNumber[player]) // bytes32
        );

        uint256 seedToNumber = uint256(seed);

        uint256 randomNumber = seedToNumber % ticketsInTheInstantGame;

        // 0 never plays, and ticketsInTheInstantGame can not be returned by the function above
        if (randomNumber == 0) {
            randomNumber = ticketsInTheInstantGame;
        }

        uint256 prize;
        address winner;
        address loser;

        if (randomNumber > kingOfTheHillTicketsNumber) {// challenger ('player') wins
            winner = player;
            loser = kingOfTheHill;

            // new kingOfTheHill:
            kingOfTheHill = player;
            kingOfTheHillTicketsNumber = lastInstantGameTicketsNumber[player];
        } else {// kingOfTheHill wins
            winner = kingOfTheHill;
            loser = player;
        }

        // prize = allocateSum(playerBet, loser, winner);
        prize = allocateSum(playerBet, loser);

        instantGameIsRunning[player] = false;

        // pay prize to the winner
        winner.transfer(prize);

        eventsCounter = eventsCounter + 1;
        InstantGameResult(
            eventsCounter,
            instantGameCounter,
            true,
            player,
            oldKingOfTheHill,
            oldKingOfTheHillTicketsNumber,
            winner,
        // playerBet,
            prize,
            ticketsInTheInstantGame,
            randomNumber,
            msg.sender
        );

    }

    // convenience function;
    function playMyInstantGame() public {
        play(msg.sender);
    }

    /* ----------- Jackpots: ------------ */

    function requestRandomFromOraclize() private returns (bytes32 oraclizeQueryId)  {

        require(msg.value >= oraclizeGetPrice());
        // < to pay to oraclize

        // call Oraclize
        // uint N :
        // number nRandomBytes between 1 and 32, which is the number of random bytes to be returned to the application.
        // see: http://www.oraclize.it/papers/random_datasource-rev1.pdf
        uint256 N = 32;
        // number of seconds to wait before the execution takes place
        uint delay = 0;
        // this function internally generates the correct oraclize_query and returns its queryId
        oraclizeQueryId = oraclize_newRandomDSQuery(delay, N, oraclizeCallbackGas);

        // playJackpotEvent(msg.sender, msg.value, tx.gasprice, oraclizeQueryId);
        return oraclizeQueryId;

    }

    // reminder (this defined above):
    // mapping(uint8 => uint256) public period; // in seconds
    // mapping(uint8 => uint256) public lastJackpotTime;  // unix time

    mapping(bytes32 => uint8) public jackpot;

    event JackpotPlayStarted(
        uint256 indexed eventsCounter,
        uint8 indexed jackpotType,
        address startedBy,
        bytes32 oraclizeQueryId
    );//
    // uint8 jackpotType
    function startJackpotPlay(uint8 jackpotType) public payable {

        require(msg.value >= oraclizeGetPrice());

        require(jackpotType >= 1 && jackpotType <= 4);
        require(!jackpotPlayIsRunning[jackpotType]);
        require(
        // block.timestamp (uint): current block timestamp as seconds since unix epoch
            block.timestamp >= lastJackpotTime[jackpotType].add(period[jackpotType])
        );

        bytes32 oraclizeQueryId = requestRandomFromOraclize();
        jackpot[oraclizeQueryId] = jackpotType;

        jackpotPlayIsRunning[jackpotType] = true;

        eventsCounter = eventsCounter + 1;
        JackpotPlayStarted(eventsCounter, jackpotType, msg.sender, oraclizeQueryId);
    }

    uint256 public allJackpotsCounter;

    event JackpotResult(
        uint256 indexed eventsCounter,
        uint256 allJackpotsCounter,
        uint8 indexed jackpotType,
        uint256 jackpotIdNumber,
        uint256 prize,
        address indexed winner,
        uint256 randomNumberSeed,
        uint256 randomNumber,
        uint256 ticketsTotal
    );

    // the callback function is called by Oraclize when the result is ready
    // the oraclize_randomDS_proofVerify modifier prevents an invalid proof to execute this function code:
    // the proof validity is fully verified on-chain
    function __callback(bytes32 _queryId, string _result, bytes _proof) public {

        require(msg.sender == oraclize_cbAddress());

        if (oraclize_randomDS_proofVerify__returnCode(_queryId, _result, _proof) != 0) {
            // the proof verification has failed, do we need to take any action here? (depends on the use case)
            revert();
        } else {

            // find jackpot for this _queryId:

            uint8 jackpotType = jackpot[_queryId];

            require(jackpotPlayIsRunning[jackpotType]);

            jackpotCounter[jackpotType] = jackpotCounter[jackpotType] + 1;

            // select jackpot winner:

            bytes32 hashOfTheRandomString = keccak256(_result);

            uint256 randomNumberSeed = uint256(hashOfTheRandomString);

            uint256 randomNumber = randomNumberSeed % ticketsTotal;

            // there is no ticket # 0, and above function can not return number equivalent to 'ticketsTotal'
            if (randomNumber == 0) {
                randomNumber = ticketsTotal;
            }

            address winner = theLotteryTicket[randomNumber];

            // transfer jackpot sum to the winner

            winner.transfer(sum[jackpotType]);

            // emit event:
            eventsCounter = eventsCounter + 1;
            allJackpotsCounter = allJackpotsCounter + 1;
            JackpotResult(
                eventsCounter,
                allJackpotsCounter,
                jackpotType,
                jackpotCounter[jackpotType],
                sum[jackpotType],
                winner,
                randomNumberSeed,
                randomNumber,
                ticketsTotal
            );

            // update information for this jackpot:

            sum[jackpotType] = 0;

            lastJackpotTime[jackpotType] = block.timestamp;

            jackpotPlayIsRunning[jackpotType] = false;

        }
    } // end of function __callback

    event DividendsSentToTokensContract(uint256 indexed eventsCounter, uint256 sum, address indexed triggeredBy);
    // can be called by any address:
    function payDividends() public returns (bool success){

        luckyStrikeTokensContractAddress.transfer(sum[income]);

        eventsCounter = eventsCounter + 1;
        DividendsSentToTokensContract(eventsCounter, sum[income], msg.sender);

        sum[income] = 0;

        return true;
    }

}
function investAndPlay() public payable {

        // require( luckyStrikeTokens.tokenSaleIsRunning());
        // < we will check this in luckyStrikeTokens.mint method

        uint256 sumToMarketingFund = msg.value / 5;

        sum[marketingFund] = sum[marketingFund].add(sumToMarketingFund);

        uint256 bet = msg.value.sub(sumToMarketingFund);

        placeABetInternal(bet);

        // uint256 tokensToMint = msg.value / ticketPriceInWei;
        // uint256 tokensToMint = bet / tokenPriceInWei;
        uint256 tokensToMint = sumToMarketingFund / tokenPriceInWei;

        // require(bet / ticketPriceInWei > 0); // > makes more complicated
        luckyStrikeTokens.mint(msg.sender, tokensToMint, sumToMarketingFund);

        eventsCounter = eventsCounter + 1;
        Investment(
            eventsCounter, //......1
            msg.sender, //.........2
            msg.value, //..........3
            sumToMarketingFund, //.4
            bet, //................5
            tokensToMint //........6
        );
    }
//