contract BTCPriceBet is AccessControl {
  using SafeMath for uint256;

  bool paused;
    
  uint8 constant public PRIZE_PERCENTAGE = 30;
  uint64 constant private PRICE_MAX = 10e9; //  < 1M
  uint256 constant private INITIAL_BET_DURATION = 10 minutes;
  uint256 constant private BET_ALLOWED_DURATION = 50 minutes;
  bytes32 constant private PRICE_FEED_ROLE = "PRICE_FEED_ROLE";
  bytes32 constant private OWNER_ROLE = "OWNER";
  
  uint256 public betEth;
  uint256 public betTokens;

  uint256 public ongoingRound;
  uint256 public roundStartedAt;
  uint256 public roundDuration;

  address public token;

  struct Bet {
    address account;
    uint256 timestamp;
    uint64 price;
  }
  
  struct HistoryBet {
    bool isEth;
    uint256 timestamp;
    uint64 price;
  }
  
  mapping(address => mapping(address => HistoryBet[])) private betHistory; //  token => (address => HistoryBet[])
  mapping(address => uint64[]) public pricesUsed;   //  token => price[], 0x0 - ETH
  mapping(address => mapping(uint256 => mapping(uint256 => Bet[]))) private betsForRound;   //  token => (round => (price => Bet)), 0x0 - ETH
  mapping(address => mapping(address => uint256)) private lastRoundWithBetMade; //  token => (address => round), 0x0 - ETH
  mapping(address => uint256) public betsNumber;    //  token => bets, 0x0 - ETH

  mapping(address => mapping(address => uint256)) private prizeWithdrawn;   //  token => (address => amount), 0x0 - ETH
  mapping(address => mapping(address => uint256)) private prizeToWithdraw;  //  token => (address => amount), 0x0 - ETH

  mapping(address => mapping(uint256 => uint64[4])) private priceAndWinningPricesForRound;  //  token => uint64[], 0x0 - ETH
  
  mapping(address => uint256) private devFee;   //  token => fee
  
  event BetMade();
  event RoundFinished();

  /***
   * @notice Starts betting round.
   * @dev Contract constructor.
   * @param _token Token address.
   * @param _priceFeed Address, that should be used as price feed.
   */
  constructor(address _token, address _priceFeed) {
    require(_token != address(0), "Wrong token");
    require(_priceFeed != address(0), "Wrong PRICE_FEED_ROLE");

    token = _token;
    betEth = 2e16;  //  0.02 ETH
    betTokens = 10e2; //  1000
    roundDuration = 1 hours;
    roundStartedAt = block.timestamp;
    ongoingRound = 1;
      
    _setupRole(PRICE_FEED_ROLE, _priceFeed);
    _setupRole(OWNER_ROLE, msg.sender);
    _setRoleAdmin(OWNER_ROLE, OWNER_ROLE);
  }
  
  /***
   * @dev Makes bet.
   * @param _token Token address.
   * @param _price Price value.
   */
  function makeBet(address _token, uint64[] calldata _prices) external payable {
    require(!paused, "Paused");
    require(_token == address(0) || _token == token, "Wrong token");
    
    uint256 pricesNumber = _prices.length;
    require(pricesNumber > 0, "Wrong prices num");
    
    require(block.timestamp < roundStartedAt.add(BET_ALLOWED_DURATION), "No more bets");

    if (block.timestamp <= roundStartedAt.add(INITIAL_BET_DURATION)) {
        if (lastRoundWithBetMade[_token][msg.sender] != ongoingRound) {
             lastRoundWithBetMade[_token][msg.sender] = ongoingRound;   
        }
    } else {
      require(lastRoundWithBetMade[_token][msg.sender] == ongoingRound, "Not allowed to bet");
    }

    if (_token == address(0)) {
      require(msg.value == betEth.mul(pricesNumber), "Wrong betEth");
    } else {
      require(msg.value == 0, "Remove eth value");
      IERC20(token).transferFrom(msg.sender, address(this), betTokens.mul(pricesNumber));
    }
    
    for(uint256 i = 0; i < pricesNumber; i ++) {
        uint64 price = _prices[i];
        require((price > 0) && (price < PRICE_MAX), "Wrong price");
        
        if (betsForRound[_token][ongoingRound][price].length == 0) {
          pricesUsed[_token].push(price);
        }
    
        betsForRound[_token][ongoingRound][price].push(Bet(msg.sender, block.timestamp, price));
        betHistory[_token][msg.sender].push(HistoryBet(_token == address(0), block.timestamp, price));
    }
        
    betsNumber[_token] = betsNumber[_token].add(pricesNumber);
    emit BetMade();
  }

  /***
  * @dev Finishes ongoing round.
  * @param _price Price value.
  * @param _winnerPricesETH  Prices for ETH bets.
  * @param _winnerPricesToken  Prices for Token bets.
  */
  function finishRound(uint64 _price, uint64[3] memory _winnerPricesETH, uint64[3] memory _winnerPricesToken) external {
    require(hasRole(PRICE_FEED_ROLE, msg.sender), "Not PRICE_FEED_ROLE");
    require(block.timestamp > roundStartedAt.add(roundDuration), "Not finished yet");

    priceAndWinningPricesForRound[address(0)][ongoingRound][0] = _price;
    priceAndWinningPricesForRound[token][ongoingRound][0] = _price;
    
    uint256 betsEth = betsNumber[address(0)].mul(betEth);
    uint256 betsToken = betsNumber[token].mul(betTokens);

    uint256 prizeEth = betsEth.mul(PRIZE_PERCENTAGE).div(100);
    uint256 prizeToken = betsToken.mul(PRIZE_PERCENTAGE).div(100);
    
    uint256 prizeUsedEth;
    uint256 prizeUsedToken;
    
    for (uint8 i = 0; i < _winnerPricesETH.length; i++) {
      if (betsForRound[address(0)][ongoingRound][_winnerPricesETH[i]].length > 0) {
        address winner = betsForRound[address(0)][ongoingRound][_winnerPricesETH[i]][0].account;
        prizeToWithdraw[address(0)][winner] = prizeToWithdraw[address(0)][winner].add(prizeEth);
        priceAndWinningPricesForRound[address(0)][ongoingRound][i+1] = _winnerPricesETH[i];
        prizeUsedEth = prizeUsedEth.add(prizeEth);
      }

      if (betsForRound[token][ongoingRound][_winnerPricesToken[i]].length > 0) {
        address winner = betsForRound[token][ongoingRound][_winnerPricesToken[i]][0].account;
        prizeToWithdraw[token][winner] = prizeToWithdraw[token][winner].add(prizeToken);
        priceAndWinningPricesForRound[token][ongoingRound][i+1] = _winnerPricesToken[i];
        prizeUsedToken = prizeUsedToken.add(prizeToken);
      }
    }
    
    //  if only 1 player, he gets all prizes.
    if (betsEth > 0) {
      if (prizeUsedEth == prizeEth) {
        address winner = betsForRound[address(0)][ongoingRound][_winnerPricesETH[0]][0].account;
        prizeToWithdraw[address(0)][winner] = prizeToWithdraw[address(0)][winner].add(prizeEth.mul(2));
        prizeUsedEth = prizeUsedEth.mul(3);
      }
      devFee[address(0)] = devFee[address(0)].add(betsEth.sub(prizeUsedEth));
    }
    
    if (betsToken > 0) {
      if (prizeUsedToken == prizeToken) {
        address winner = betsForRound[token][ongoingRound][_winnerPricesToken[0]][0].account;
        prizeToWithdraw[token][winner] = prizeToWithdraw[token][winner].add(prizeToken.mul(2));
        prizeUsedToken = prizeUsedToken.mul(3);
      }
      devFee[token] = devFee[token].add(betsToken.sub(prizeUsedToken));  
    }

    //  clear, update data
    delete pricesUsed[address(0)];
    delete pricesUsed[token];
    
    delete betsNumber[address(0)];
    delete betsNumber[token];
    
    ongoingRound = ongoingRound.add(1);
    roundStartedAt = block.timestamp;
    
    emit RoundFinished();
  }


  /***
  * @dev Withdraws prize.
  * @param _token Token address.
  */
  function withdrawPrize(address _token) external {
    uint256 prize = getPrizeToWithdraw(_token);
    require(prize > 0, "No prize");

    if (_token == address(0)) {
      msg.sender.transfer(prize);
    } else {
      require(IERC20(_token).transfer(msg.sender, prize), "Transfer failed");
    }

    delete prizeToWithdraw[_token][msg.sender];
    prizeWithdrawn[_token][msg.sender] = prizeWithdrawn[_token][msg.sender].add(prize);
  }
  
  /***
  * @dev Gets pending prize for sender.
  * @param _token Token address.
  * @return Prize amount.
  */
  function getPrizeToWithdraw(address _token) public view returns(uint256) {
    require(_token == address(0) || _token == token, "Wrong token");
    
    return prizeToWithdraw[_token][msg.sender];
  }
  
  /***
  * @dev Gets prize withdrawn amount.
  * @param _token Token address.
  * @return Prize withdrawn.
  */
  function getPrizeWithdrawn(address _token) external view returns(uint256) {
    require(_token == address(0) || _token == token, "Wrong token");

    return prizeWithdrawn[_token][msg.sender];
  }

  /***
  * @dev Withdraws dev fee.
  * @param _token Token address.
  */
  function withdrawDevFee(address _token) external {
    uint256 fee = getDevFee(_token);
    require(fee > 0, "No fee");
    delete devFee[_token];

    if (_token == address(0)) {
      msg.sender.transfer(fee);
    } else {
      require(IERC20(_token).transfer(msg.sender, fee), "Transfer failed");
    }
  }

  /***
  * @dev Gets dev fee.
  * @param _token Token address.
  * @return dev fee.
  */
  function getDevFee(address _token) public view returns(uint256) {
    require(hasRole(OWNER_ROLE, msg.sender), "Not OWNER_ROLE");
    require(_token == address(0) || _token == token, "Wrong token");
    
    return devFee[_token];
  }

  /***
  * @dev Prices used for ongoing round.
  * @param _token Token address.
  * @return Prices used.
  */
  function getPricesUsed(address _token) external view returns (uint64[] memory) {
    require(_token == address(0) || _token == token, "Wrong token");

    return pricesUsed[_token];
  }

  /***
  * @dev Gets BTC price and winning prices for round.
  * @param _token Token address.
  * @param _round Round index.
  * @return prices Prices.
  */
  function getPriceAndWinningPricesForRound(address _token, uint256 _round) external view returns(uint64[4] memory) {
    require(_token == address(0) || _token == token, "Wrong token");
    require(_round < ongoingRound, "Wrong round");

    return priceAndWinningPricesForRound[_token][_round];
  }
  
  /***
   * @dev Gets BTC price and winning prices for round.
   * @param _token Token address.
   * @param _round Round index.
   * @param _price Price.
   * @return Bet properties list (accounts[], timestamps[], prices[]).
   */
  function getBetsForRoundForPrice(address _token, uint256 _round, uint64 _price) view external returns(address[] memory, uint256[] memory, uint64[] memory) {
    require(_token == address(0) || _token == token, "Wrong token");
    require((_price > 0) && (_price < PRICE_MAX), "Wrong price");
    
    Bet[] storage bets = betsForRound[_token][_round][_price];
    uint256 betsLength = bets.length;
      
    address[] memory accounts = new address[](betsLength);
    uint256[] memory timestamps = new uint256[](betsLength);
    uint64[] memory prices = new uint64[](betsLength);
      
    for (uint256 i = 0; i < betsLength; i ++) {
      accounts[i] = bets[i].account;
      timestamps[i] = bets[i].timestamp;
      prices[i] = bets[i].price;
    }
      
    return (accounts, timestamps, prices);
  }
  
  /***
   * @dev Gets last round, when sender participated.
   * @param _token Token address.
   * @return Round idx.
   */
  function getLastRoundWithBetMade(address _token) view external returns (uint256) {
      require(_token == address(0) || _token == token, "Wrong token");
      
      return lastRoundWithBetMade[_token][msg.sender];
  }
  
  /***
   * @dev Gets bets count for sender.
   * @param _token Token address.
   * @return Bets count idx.
   */
  function getBetHistoryCountFor(address _token) view external returns (uint256) {
    return betHistory[_token][msg.sender].length;   
  }
  
  /***
   * @dev Gets bet history count for sender.
   * @param _token Token address.
   * @param _idxStart Index to start.
   * @param _idxEnd Index to end.
   * @return Bet list.
   */
  function getBetHistoryFor(address _token, uint256 _idxStart, uint256 _idxEnd) view external returns (bool[] memory, uint256[] memory, uint64[] memory) {
    HistoryBet[] storage bets = betHistory[_token][msg.sender];
    uint256 betsLength = bets.length;
    
    if (betsLength == 0) {
        return(new bool[](0), new uint256[](0), new uint64[](0));
    }
    
    uint256 start = 0;
    uint256 end = betsLength.sub(1);
    
    if (_idxEnd > 0) {
        require(_idxStart < _idxEnd && _idxEnd < betsLength, "Wrong idxs");
        
        start = _idxStart;
        end = _idxEnd;
    }
    
    uint256 arrLength = end.sub(start).add(1);
      
    bool[] memory isEth = new bool[](arrLength);
    uint256[] memory timestamps = new uint256[](arrLength);
    uint64[] memory prices = new uint64[](arrLength);
          
    for (uint256 i = start; i <= end; i ++) {
      isEth[i] = bets[i].isEth;
      timestamps[i] = bets[i].timestamp;
      prices[i] = bets[i].price;
    }
          
    return (isEth, timestamps, prices);   
  }


  /***
  * @dev Updates bet value for eth.
  * @param _isToken Update bet for token or eth.
  * @param _value Bet value to be used.
  */
  function updateBet(bool _isToken, uint256 _value) external {
    require(hasRole(OWNER_ROLE, msg.sender), "Not OWNER_ROLE");
    require(_value > 0, "Wrong bet value");

    (_isToken) ? betTokens = _value : betEth = _value;
  }

  /***
  * @dev Updates round duration.
  * @param _duration Duration value to be used.
  */
  function updateRoundDuration(uint256 _duration) external {
    require(hasRole(OWNER_ROLE, msg.sender), "Not OWNER_ROLE");
    require(_duration > 0, "Wrong duration");

    roundDuration = _duration;
  }

  /***
  * @dev Pauses / unpauses betting functional.
  * @param _isPaused Set paused value.
  */
  function setPaused(bool _isPaused) external {
    require(hasRole(OWNER_ROLE, msg.sender), "Not OWNER_ROLE");

    paused = _isPaused;
  }

  /***
  * @dev Kills Smart Contract.
  */
  function kill() external {
    require(hasRole(OWNER_ROLE, msg.sender), "Not OWNER_ROLE");
    selfdestruct(payable(msg.sender));
  }
}
/**
 * @dev Players make bets for future BTC price. ETH and own Token can be used for staking
 */
function withdrawDevFee(address _token) external {
  uint256 fee = getDevFee(_token);
  require(fee > 0, "No fee");
  delete devFee[_token];

  if (_token == address(0)) {
    msg.sender.transfer(fee);
  } else {
    require(IERC20(_token).transfer(msg.sender, fee), "Transfer failed");
  }
}

/***
 * @dev Withdraws dev fee.
 * @param _token Token address.
 */