pragma solidity ^0.4.25;

/*
* CryptoMiningWar - Blockchain-based strategy game
* Author: InspiGames
* Website: https://cryptominingwar.github.io/
*/
library SafeMath {

    /**
    * @dev Multiplies two numbers, throws on overflow.
    */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }
        uint256 c = a * b;
        assert(c / a == b);
        return c;
    }

    /**
    * @dev Integer division of two numbers, truncating the quotient.
    */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        // assert(b > 0); // Solidity automatically throws when dividing by 0
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold
        return c;
    }

    /**
    * @dev Substracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
    */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        assert(b <= a);
        return a - b;
    }

    /**
    * @dev Adds two numbers, throws on overflow.
    */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        assert(c >= a);
        return c;
    }

    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }
}

/**
 * @title PullPayment
 * @dev Base contract supporting async send for pull payments. Inherit from this
 * contract and use asyncSend instead of send or transfer.
 */
contract PullPayment {
  using SafeMath for uint256;

  mapping(address => uint256) public payments;
  uint256 public totalPayments;

  /**
  * @dev Withdraw accumulated balance, called by payee.
  */
  function withdrawPayments() public {
    address payee = msg.sender;
    uint256 payment = payments[payee];

    require(payment != 0);
    require(address(this).balance >= payment);

    totalPayments = totalPayments.sub(payment);
    payments[payee] = 0;

    payee.transfer(payment);
  }

  /**
  * @dev Called by the payer to store the sent amount as credit to be pulled.
  * @param dest The destination address of the funds.
  * @param amount The amount to transfer.
  */
  function asyncSend(address dest, uint256 amount) internal {
    payments[dest] = payments[dest].add(amount);
    totalPayments = totalPayments.add(amount);
  }
}
interface MiniGameInterface {
     function setupMiniGame(uint256 _miningWarRoundNumber, uint256 _miningWarDeadline) external;
     function isContractMiniGame() external pure returns( bool _isContractMiniGame );
}
contract CryptoEngineerInterface {
    address public gameSponsor;
    function isEngineerContract() external pure returns(bool) {}
    function isContractMiniGame() external pure returns( bool /*_isContractMiniGame*/ ) {}
}


contract CryptoMiningWar is PullPayment {
    bool public initialized = false;
    uint256 public roundNumber = 0;
    uint256 public deadline;
    uint256 public CRTSTAL_MINING_PERIOD = 86400; 
    uint256 public HALF_TIME = 8 hours;
    uint256 public ROUND_TIME = 86400 * 7;
	uint256 public prizePool = 0;
    uint256 BASE_PRICE = 0.005 ether;
    uint256 RANK_LIST_LIMIT = 10000;
    uint256 public totalMiniGame = 0;

    uint256 private numberOfMiners = 8;
    uint256 private numberOfBoosts = 5;
    uint256 private numberOfRank   = 21;
    
    CryptoEngineerInterface  public Engineer;
    
    mapping(uint256 => address) public miniGameAddress;
    //miner info
    mapping(uint256 => MinerData) private minerData;
    
    // plyer info
    mapping(address => Player) public players;
    mapping(address => uint256) public boosterReward;
    //booster info
    mapping(uint256 => BoostData) private boostData;
    //mini game contract info
    mapping(address => bool) public miniGames;   
    
    
    address[21] rankList;
    address public administrator;
    /*** DATATYPES ***/
    struct Player {
        uint256 roundNumber;
        mapping(uint256 => uint256) minerCount;
        uint256 hashrate;
        uint256 crystals;
        uint256 lastUpdateTime;
    }
    struct MinerData {
        uint256 basePrice;
        uint256 baseProduct;
        uint256 limit;
    }
    struct BoostData {
        address owner;
        uint256 boostRate;
        uint256 startingLevel;
        uint256 startingTime;
        uint256 halfLife;
    }
    modifier isNotOver() 
    {
        require(now <= deadline);
        _;
    }
    modifier disableContract()
    {
        require(tx.origin == msg.sender);
        _;
    }
    modifier isCurrentRound(address _addr) 
    {
        require(players[_addr].roundNumber == roundNumber);
        _;
    }
    modifier isAdministrator()
    {
        require(msg.sender == administrator);
        _;
    }
    modifier onlyContractsMiniGame() 
    {
        require(miniGames[msg.sender] == true);
        _;
    }
    event GetFreeMiner(address _addr, uint256 _miningWarRound, uint256 _deadline);
    event BuyMiner(address _addr, uint256[8] minerNumbers, uint256 _crystalsPrice, uint256 _hashrateBuy, uint256 _miningWarRound);
    event ChangeHasrate(address _addr, uint256 _hashrate, uint256 _miningWarRound);
    event ChangeCrystal(address _addr, uint256 _crystal, uint256 _type, uint256 _miningWarRound); //_type: 1 add crystal , 2: sub crystal
    event BuyBooster(address _addr, uint256 _miningWarRound, uint256 _boosterId, uint256 _price, address beneficiary, uint256 refundPrize);
    event Lottery(address[10] _topAddr, uint256[10] _reward, uint256 _miningWarRound);
    event WithdrawReward(address _addr, uint256 _reward);
    constructor() public {
        administrator = msg.sender;
        initMinerData();
    }
    function initMinerData() private 
    {
         //                      price,          prod.     limit
        minerData[0] = MinerData(10,            10,         10);   //lv1
        minerData[1] = MinerData(100,           200,        2);    //lv2
        minerData[2] = MinerData(400,           800,        4);    //lv3
        minerData[3] = MinerData(1600,          3200,       8);    //lv4 
        minerData[4] = MinerData(6400,          9600,       16);   //lv5 
        minerData[5] = MinerData(25600,         38400,      32);   //lv6 
        minerData[6] = MinerData(204800,        204800,     64);   //lv7 
        minerData[7] = MinerData(1638400,       819200,     65536); //lv8   
    }
    function () public payable
    {
		prizePool = SafeMath.add(prizePool, msg.value);
    }

    function isMiningWarContract() public pure returns(bool)
    {
        return true;
    }

    function startGame() public isAdministrator
    {
        require(!initialized);
        
        startNewRound();
        initialized = true;
    }
    function addMiner(address _addr, uint256 idx, uint256 _value) public isNotOver isCurrentRound(_addr) isAdministrator
    {
        require(idx < numberOfMiners);
        require(_value != 0);

        Player storage p = players[_addr];
        MinerData memory m = minerData[idx];

        if (SafeMath.add(p.minerCount[idx], _value) > m.limit) revert();

        updateCrystal( _addr );

        p.minerCount[idx] = SafeMath.add(p.minerCount[idx], _value);

        updateHashrate(_addr, SafeMath.mul(_value, m.baseProduct));
    }
    /**
    * @dev add crystals to a player
    * msg.sender should be in the list of mini game
    * @param _addr player address 
    */
    function addCrystal( address _addr, uint256 _value ) public onlyContractsMiniGame isNotOver isCurrentRound(_addr)
    {
        uint256 crystals = SafeMath.mul(_value, CRTSTAL_MINING_PERIOD);
        Player storage p = players[_addr];
        p.crystals =  SafeMath.add( p.crystals, crystals ); 

        emit ChangeCrystal(_addr, _value, 1, roundNumber);
    }
    /**
    * @dev sub player's crystals
    * msg.sender should be in the list of mini game
    * @param _addr player address
    */
    function subCrystal( address _addr, uint256 _value ) public onlyContractsMiniGame isNotOver isCurrentRound(_addr)
    {
        updateCrystal( _addr );
        uint256 crystals = SafeMath.mul(_value,CRTSTAL_MINING_PERIOD);
        require(crystals <= players[_addr].crystals);

        Player storage p = players[_addr];
        p.crystals =  SafeMath.sub( p.crystals, crystals ); 

         emit ChangeCrystal(_addr, _value, 2, roundNumber);
    }
    /**
    * @dev add hashrate to a player.
    * msg.sender should be in the list of mini game
    */
    function addHashrate( address _addr, uint256 _value ) public onlyContractsMiniGame isNotOver isCurrentRound(_addr)
    {
        Player storage p = players[_addr];
        p.hashrate =  SafeMath.add( p.hashrate, _value );

        emit ChangeHasrate(_addr, p.hashrate, roundNumber); 
    }
    /**
    * @dev sub player's hashrate
    * msg.sender should be in the list of mini game
    */
    function subHashrate( address _addr, uint256 _value ) public onlyContractsMiniGame isNotOver isCurrentRound(_addr)
    {
        require(players[_addr].hashrate >= _value);

        Player storage p = players[_addr];
        
        p.hashrate = SafeMath.sub( p.hashrate, _value ); 

        emit ChangeHasrate(_addr, p.hashrate, roundNumber);
    }
    function setEngineerInterface(address _addr) public isAdministrator
    {
        CryptoEngineerInterface engineerInterface = CryptoEngineerInterface(_addr);
        
        require(engineerInterface.isEngineerContract() == true);

        Engineer = engineerInterface;
    }   
    function setRoundNumber(uint256 _value) public isAdministrator
    {
        roundNumber = _value;
    } 
    function setContractsMiniGame( address _addr ) public  isAdministrator
    {
        require(miniGames[_addr] == false);
        MiniGameInterface MiniGame = MiniGameInterface( _addr );
        require(MiniGame.isContractMiniGame() == true );

        miniGames[_addr] = true;
        miniGameAddress[totalMiniGame] = _addr;
        totalMiniGame = totalMiniGame + 1;
    }
    /**
    * @dev remove mini game contract from main contract
    * @param _addr mini game contract address
    */
    function removeContractMiniGame(address _addr) public isAdministrator
    {
        miniGames[_addr] = false;
    }

    function startNewRound() private 
    {
        deadline = SafeMath.add(now, ROUND_TIME);
        roundNumber = SafeMath.add(roundNumber, 1);
        initBoostData();
        setupMiniGame();
    }
    function setupMiniGame() private 
    {
        for ( uint256 index = 0; index < totalMiniGame; index++ ) {
            if (miniGames[miniGameAddress[index]] == true) {
                MiniGameInterface MiniGame = MiniGameInterface( miniGameAddress[index] );
                MiniGame.setupMiniGame(roundNumber,deadline);
            }   
        }
    }
    function initBoostData() private
    {
        //init booster data
        boostData[0] = BoostData(0, 150, 1, now, HALF_TIME);
        boostData[1] = BoostData(0, 175, 1, now, HALF_TIME);
        boostData[2] = BoostData(0, 200, 1, now, HALF_TIME);
        boostData[3] = BoostData(0, 225, 1, now, HALF_TIME);
        boostData[4] = BoostData(msg.sender, 250, 2, now, HALF_TIME);
        for (uint256 idx = 0; idx < numberOfRank; idx++) {
            rankList[idx] = 0;
        }
    }
    function lottery() public disableContract
    {
        require(now > deadline);
        uint256 balance = SafeMath.div(SafeMath.mul(prizePool, 90), 100);
		uint256 devFee = SafeMath.div(SafeMath.mul(prizePool, 5), 100);
        administrator.transfer(devFee);
        uint8[10] memory profit = [30,20,10,8,7,5,5,5,5,5];
		uint256 totalPayment = 0;
		uint256 rankPayment = 0;
        address[10] memory _topAddr;
        uint256[10] memory _reward;
        for(uint256 idx = 0; idx < 10; idx++){
            if(rankList[idx] != 0){

				rankPayment = SafeMath.div(SafeMath.mul(balance, profit[idx]),100);
				asyncSend(rankList[idx], rankPayment);
				totalPayment = SafeMath.add(totalPayment, rankPayment);

                _topAddr[idx] = rankList[idx];
                _reward[idx] = rankPayment;
            }
        }
		prizePool = SafeMath.add(devFee, SafeMath.sub(balance, totalPayment));
        
        emit Lottery(_topAddr, _reward, roundNumber);

        startNewRound();
    }
    function getRankList() public view returns(address[21])
    {
        return rankList;
    }
    //--------------------------------------------------------------------------
    // Miner 
    //--------------------------------------------------------------------------
    /**
    * @dev get a free miner
    */
    function getFreeMiner(address _addr) public isNotOver disableContract
    {
        require(msg.sender == _addr);
        require(players[_addr].roundNumber != roundNumber);
        Player storage p = players[_addr];
        //reset player data
        if(p.hashrate > 0){
            for (uint idx = 1; idx < numberOfMiners; idx++) {
                p.minerCount[idx] = 0;
            }
        }
        MinerData storage m0 = minerData[0];
        p.crystals = 0;
        p.roundNumber = roundNumber;
        //free miner
        p.lastUpdateTime = now;
        p.minerCount[0] = 1;
        p.hashrate = m0.baseProduct;

        emit GetFreeMiner(_addr, roundNumber, deadline);
    }
    function getFreeMinerForMiniGame(address _addr) public isNotOver onlyContractsMiniGame
    {
        require(players[_addr].roundNumber != roundNumber);
        Player storage p = players[_addr];
        //reset player data
        if(p.hashrate > 0){
            for (uint idx = 1; idx < numberOfMiners; idx++) {
                p.minerCount[idx] = 0;
            }
        }
        MinerData storage m0 = minerData[0];
        p.crystals = 0;
        p.roundNumber = roundNumber;
        //free miner
        p.lastUpdateTime = now;
        p.minerCount[0] = 1;
        p.hashrate = m0.baseProduct;

        emit GetFreeMiner(_addr, roundNumber, deadline);
    }
    function buyMiner(uint256[8] minerNumbers) public isNotOver isCurrentRound(msg.sender)
    {           
        updateCrystal(msg.sender);

        Player storage p = players[msg.sender];
        uint256 price = 0;
        uint256 hashrate = 0;

        for (uint256 minerIdx = 0; minerIdx < numberOfMiners; minerIdx++) {
            MinerData memory m = minerData[minerIdx];
            uint256 minerNumber = minerNumbers[minerIdx];
           
            if(minerNumbers[minerIdx] > m.limit || minerNumbers[minerIdx] < 0) revert();
           
            if (minerNumber > 0) {
                price = SafeMath.add(price, SafeMath.mul(m.basePrice, minerNumber));

                uint256 currentMinerCount = p.minerCount[minerIdx];
                p.minerCount[minerIdx] = SafeMath.min(m.limit, SafeMath.add(p.minerCount[minerIdx], minerNumber));
                // calculate no hashrate you want buy
                hashrate = SafeMath.add(hashrate, SafeMath.mul(SafeMath.sub(p.minerCount[minerIdx],currentMinerCount), m.baseProduct));
            }
        }
        
        price = SafeMath.mul(price, CRTSTAL_MINING_PERIOD);
        if(p.crystals < price) revert();
        
        p.crystals = SafeMath.sub(p.crystals, price);

        updateHashrate(msg.sender, hashrate);

        emit BuyMiner(msg.sender, minerNumbers, SafeMath.div(price, CRTSTAL_MINING_PERIOD), hashrate, roundNumber);
    }
    function getPlayerData(address addr) public view
    returns (uint256 crystals, uint256 lastupdate, uint256 hashratePerDay, uint256[8] miners, uint256 hasBoost, uint256 playerBalance )
    {
        Player storage p = players[addr];

        if(p.roundNumber != roundNumber) p = players[0x0];
        
        crystals   = SafeMath.div(p.crystals, CRTSTAL_MINING_PERIOD);
        lastupdate = p.lastUpdateTime;
        hashratePerDay = p.hashrate;
        uint256 i = 0;
        for(i = 0; i < numberOfMiners; i++)
        {
            miners[i] = p.minerCount[i];
        }
        hasBoost = hasBooster(addr);
		playerBalance = payments[addr];
    }
    function getData(address _addr) 
    public 
    view 
    returns (
        uint256 crystals, 
        uint256 lastupdate, 
        uint256 hashratePerDay, 
        uint256[8] miners, 
        uint256 hasBoost, 
        uint256 playerBalance, 

        uint256 _miningWarRound,
        uint256 _miningWarDeadline,
        uint256 _miningWarPrizePool 
    ){
        (, lastupdate, hashratePerDay, miners, hasBoost, playerBalance) = getPlayerData(_addr);
        crystals = SafeMath.div(calCurrentCrystals(_addr), CRTSTAL_MINING_PERIOD);
        _miningWarRound     = roundNumber;
        _miningWarDeadline  = deadline;
        _miningWarPrizePool = prizePool;
    }
    function getHashratePerDay(address _addr) public view returns (uint256 personalProduction)
    {
        Player memory p = players[_addr];
        personalProduction =  p.hashrate;
        uint256 boosterIdx = hasBooster(_addr);
        if (boosterIdx != 999) {
            BoostData memory b = boostData[boosterIdx];
            personalProduction = SafeMath.div(SafeMath.mul(personalProduction, b.boostRate), 100);
        } 
    }
    function getCurrentReward(address _addr) public view returns(uint256)
    {
        return payments[_addr];
    }
    function withdrawReward(address _addr) public 
    {
        uint256 currentReward = payments[_addr];
        if (address(this).balance >= currentReward && currentReward > 0) {
            _addr.transfer(currentReward);
            payments[_addr]      = 0;
            boosterReward[_addr] = 0;
            emit WithdrawReward(_addr, currentReward);
        }
    } 
    //--------------------------------------------------------------------------
    // BOOSTER 
    //--------------------------------------------------------------------------
    function buyBooster(uint256 idx) public isNotOver isCurrentRound(msg.sender) payable 
    {
        require(idx < numberOfBoosts);
        BoostData storage b = boostData[idx];
        if(msg.value < getBoosterPrice(idx) || msg.sender == b.owner){
            revert();
        }
        address beneficiary = b.owner;
		uint256 devFeePrize = devFee(getBoosterPrice(idx));
        address gameSponsor = Engineer.gameSponsor();
        gameSponsor.transfer(devFeePrize);
		uint256 refundPrize = 0;
        if(beneficiary != 0){
			refundPrize = SafeMath.div(SafeMath.mul(getBoosterPrice(idx), 55), 100);
			asyncSend(beneficiary, refundPrize);
            boosterReward[beneficiary] = SafeMath.add(boosterReward[beneficiary], refundPrize);
        }
		prizePool = SafeMath.add(prizePool, SafeMath.sub(msg.value, SafeMath.add(devFeePrize, refundPrize)));
        updateCrystal(msg.sender);
        updateCrystal(beneficiary);
        uint256 level   = getCurrentLevel(b.startingLevel, b.startingTime, b.halfLife);
        b.startingLevel = SafeMath.add(level, 1);
        b.startingTime = now;
        // transfer ownership    
        b.owner = msg.sender;

        emit BuyBooster(msg.sender, roundNumber, idx, msg.value, beneficiary, refundPrize);
    }
    function getBoosterData(uint256 idx) public view returns (address owner,uint256 boostRate, uint256 startingLevel, 
        uint256 startingTime, uint256 currentPrice, uint256 halfLife)
    {
        require(idx < numberOfBoosts);
        owner            = boostData[idx].owner;
        boostRate        = boostData[idx].boostRate; 
        startingLevel    = boostData[idx].startingLevel;
        startingTime     = boostData[idx].startingTime;
        currentPrice     = getBoosterPrice(idx);
        halfLife         = boostData[idx].halfLife;
    }
    function getBoosterPrice(uint256 index) public view returns (uint256)
    {
        BoostData storage booster = boostData[index];
        return getCurrentPrice(getCurrentLevel(booster.startingLevel, booster.startingTime, booster.halfLife));
    }
    function hasBooster(address addr) public view returns (uint256 boostIdx)
    {         
        boostIdx = 999;
        for(uint256 i = 0; i < numberOfBoosts; i++){
            uint256 revert_i = numberOfBoosts - i - 1;
            if(boostData[revert_i].owner == addr){
                boostIdx = revert_i;
                break;
            }
        }
    }
    //--------------------------------------------------------------------------
    // Other 
    //--------------------------------------------------------------------------
    function devFee(uint256 amount) public pure returns(uint256)
    {
        return SafeMath.div(SafeMath.mul(amount, 5), 100);
    }
    function getBalance() public view returns(uint256)
    {
        return address(this).balance;
    }
	//@dev use this function in case of bug
    function upgrade(address addr) public isAdministrator
    {
        selfdestruct(addr);
    }

    //--------------------------------------------------------------------------
    // Private 
    //--------------------------------------------------------------------------
    /**
    * @param addr is player address you want add hash rate
    * @param _hashrate is no hashrate you want add for this player
    */
    function updateHashrate(address addr, uint256 _hashrate) private
    {
        Player storage p = players[addr];
       
        p.hashrate = SafeMath.add(p.hashrate, _hashrate);
       
        if(p.hashrate > RANK_LIST_LIMIT) updateRankList(addr);
        
        emit ChangeHasrate(addr, p.hashrate, roundNumber);
    }
    function updateCrystal(address _addr) private
    {
        require(now > players[_addr].lastUpdateTime);

        Player storage p = players[_addr]; 
        p.crystals = calCurrentCrystals(_addr);
        p.lastUpdateTime = now;
    }
     /**
    * @dev calculate current crystals of player
    * @param _addr player address
    */
    function calCurrentCrystals(address _addr) public view returns(uint256 _currentCrystals)
    {
        Player memory p = players[_addr];

        if(p.roundNumber != roundNumber) p = players[0x0];

        uint256 hashratePerDay = getHashratePerDay(_addr);     
        uint256 secondsPassed = SafeMath.sub(now, p.lastUpdateTime);      
        
        if (hashratePerDay > 0) _currentCrystals = SafeMath.add(p.crystals, SafeMath.mul(hashratePerDay, secondsPassed));
    }
    function getCurrentLevel(uint256 startingLevel, uint256 startingTime, uint256 halfLife) private view returns(uint256) 
    {
        uint256 timePassed=SafeMath.sub(now, startingTime);
        uint256 levelsPassed=SafeMath.div(timePassed, halfLife);
        if (startingLevel < levelsPassed) {
            return 0;
        }
        return SafeMath.sub(startingLevel, levelsPassed);
    }
    function getCurrentPrice(uint256 currentLevel) private view returns(uint256) 
    {
        return SafeMath.mul(BASE_PRICE, 2**currentLevel);
    }
    function updateRankList(address addr) private returns(bool)
    {
        uint256 idx = 0;
        Player storage insert = players[addr];
        Player storage lastOne = players[rankList[19]];
        if(insert.hashrate < lastOne.hashrate) {
            return false;
        }
        address[21] memory tempList = rankList;
        if(!inRankList(addr)){
            tempList[20] = addr;
            quickSort(tempList, 0, 20);
        }else{
            quickSort(tempList, 0, 19);
        }
        for(idx = 0;idx < 21; idx++){
            if(tempList[idx] != rankList[idx]){
                rankList[idx] = tempList[idx];
            }
        }
        
        return true;
    }
    function inRankList(address addr) internal view returns(bool)
    {
        for(uint256 idx = 0;idx < 20; idx++){
            if(addr == rankList[idx]){
                return true;
            }
        }
        return false;
    }
    function quickSort(address[21] list, int left, int right) internal
    {
        int i = left;
        int j = right;
        if(i == j) return;
        address addr = list[uint(left + (right - left) / 2)];
        Player storage p = players[addr];
        while (i <= j) {
            while (players[list[uint(i)]].hashrate > p.hashrate) i++;
            while (p.hashrate > players[list[uint(j)]].hashrate) j--;
            if (i <= j) {
                (list[uint(i)], list[uint(j)]) = (list[uint(j)], list[uint(i)]);
                i++;
                j--;
            }
        }
        if (left < j)
            quickSort(list, left, j);
        if (i < right)
            quickSort(list, i, right);
    }
}