contract RandomWorld is ChainlinkClient, Ownable {
  using SafeMath for uint;
  uint constant HOUSE_EDGE_PERCENT = 1;
  uint constant HOUSE_EDGE_MINIMUM_AMOUNT = 0.0003 ether;

  uint constant BET_AMOUNT_MIN = 0.01 ether;
  uint constant BET_AMOUNT_MAX = 1000 ether;
  uint constant BET_EXPIRATION_BLOCKS = 60;

  uint constant MAX_MODULO = 100;
  uint constant MAX_MASKABLE_MODULO = 40;
  uint constant MAX_BET_MASK = 2 ** MAX_MASKABLE_MODULO;

  // population count
  uint constant POPCOUNT_MULT = 0x0000000000002000000000100000000008000000000400000000020000000001;
  uint constant POPCOUNT_MASK = 0x0001041041041041041041041041041041041041041041041041041041041041;
  uint constant POPCOUNT_MODULO = 0x3F;

  uint public oraclePayment;
  address public oracle;
  bytes32 public jobId;
  uint public bankFund;

  struct Bet {
    uint8 modulo;
    uint8 choice;
    uint amount;
    uint winAmount;
    uint placeBlockNumber;
    uint randomNumber;
    bool isActive;
    address player;
  }

  mapping (bytes32 => Bet) public bets;

  event LogParticipant(address indexed player, uint indexed modulo, uint choice, uint amount, bytes32 requestId);
  event LogClosedBet(address indexed player, uint indexed modulo, uint choice, uint randomNumber, uint amount, uint winAmount, bytes32 requestId);
  event LogDistributeReward(address indexed addr, uint reward);
  event LogRefund(address indexed addr, uint amount, bytes32 requestId);
  event LogRecharge(address indexed addr, uint amount);
  event LogDealerWithdraw(address indexed addr, uint amount);

  /**
   * @notice Deploy the contract with a specified address for the LINK
   * and Oracle contract addresses
   * @dev Sets the storage for the specified addresses
   */
  constructor(address _oracle, bytes32 _jobId, uint _oraclePayment) public {
    setPublicChainlinkToken();
    oracle = _oracle;
    jobId = _jobId;
    oraclePayment = _oraclePayment;
  }

  /**
   * @notice get a random number
   * @param _modulo The modulo indicates which random factor was chosen
   * @param _choice The choice indicates user's choice
   */
  function placeBet(uint _modulo, uint _choice) external payable {
    // validate user input
    uint amount = msg.value;
    require(_modulo > 1 && _modulo <= MAX_MODULO, "modulo should between 2 ~ 100");
    require(amount >= BET_AMOUNT_MIN && amount <= BET_AMOUNT_MAX, 'bet amount should between 0.01 ~ 1000');
    uint populationCount;
    if (_modulo < MAX_MASKABLE_MODULO) {
      require(_choice < MAX_BET_MASK, "choice too large");
      populationCount = (_choice * POPCOUNT_MULT & POPCOUNT_MASK) % POPCOUNT_MODULO;
      require(populationCount < _modulo, "winning rate out of range");
    } else {
      require(_choice < _modulo, "choice large than modulo");
      populationCount = _choice;
    }

    // validate contract balance
    uint houseEdge = amount * HOUSE_EDGE_PERCENT / 100;
    if (houseEdge < HOUSE_EDGE_MINIMUM_AMOUNT) {
      houseEdge = HOUSE_EDGE_MINIMUM_AMOUNT;
    }
    uint winAmount = (amount - houseEdge).mul(_modulo) / populationCount;
    require(bankFund.add(winAmount) <= address(this).balance, 'contract balance is not enough');
    // lock winAmount into this contract. Make sure contract is solvent
    bankFund = bankFund.add(winAmount);

    // start to request a random number
    Chainlink.Request memory req = buildChainlinkRequest(jobId, this, this.closeBet.selector);
    req.addUint("min", 0);
    req.addUint("max", 899);
    bytes32 requestId = sendChainlinkRequestTo(oracle, req, oraclePayment);

    Bet storage bet = bets[requestId];
    require (bet.player == address(0), 'bet should be none');

    bet.modulo = uint8(_modulo);
    bet.choice = uint8(_choice);
    bet.amount = amount;
    bet.winAmount = winAmount;
    bet.placeBlockNumber = block.number;
    bet.isActive = true;
    bet.player = msg.sender;

    emit LogParticipant(msg.sender, _modulo, _choice, amount, requestId);
  }

  /**
   * @notice The closeBet to close specific bet with requeistId
   * @dev The recordChainlinkFulfillment protects this function from being called
   * by anyone other than the oracle address that the request was sent to
   * @param _requestId The ID that was generated for the request
   * @param _data The answer provided by the oracle
   */
  function closeBet(bytes32 _requestId, uint256 _data)
    public
    recordChainlinkFulfillment(_requestId)
  {
    Bet storage bet = bets[_requestId];
    require(bet.isActive, 'this bet is not active');
    bet.randomNumber = _data;

    // get win amount
    uint result = _data % bet.modulo;
    uint winAmount = 0;
    if (bet.modulo <= MAX_MASKABLE_MODULO) {
      if ((2 ** result) & bet.choice != 0) {
        winAmount = bet.winAmount;
      }
    } else {
      if (result < bet.choice) {
        winAmount = bet.winAmount;
      }
    }

    if (winAmount > 0) {
      bet.player.transfer(winAmount);
      emit LogDistributeReward(bet.player, winAmount);
    }

    // release winAmount banking
    bankFund = bankFund.sub(bet.winAmount);
    bet.isActive = false;
    emit LogClosedBet(bet.player, bet.modulo, bet.choice, _data, bet.amount, winAmount, _requestId);
  }

  /**
   * @notice could be invoked by anyone
   * @dev The refundBet function to refund expired bet
   * @param _requestId The ID that was generated for the request
   */
  function refundBet(bytes32 _requestId) external {
    Bet storage bet = bets[_requestId];

    uint amount = bet.amount;
    address player = bet.player;

    require(block.number > bet.placeBlockNumber + BET_EXPIRATION_BLOCKS, 'this bet has not expired');
    require(bet.isActive, 'this bet is not active');

    player.transfer(amount);
    // release winAmount deposit
    bankFund = bankFund.sub(bet.winAmount);
    bet.isActive = false;

    emit LogRefund(player, amount, _requestId);
  }

  /**
   * @dev increase balance to let more people participant
   */
  function recharge() public payable {
    emit LogRecharge(msg.sender, msg.value);
  }

  /**
   * @dev owner can withdraw the remain ether
   */
  function withdraw(uint _amount) external onlyOwner {
    require(_amount <= address(this).balance - bankFund, 'cannot withdraw amount greater than (balance - bankFund)');
    owner.transfer(_amount);
    emit LogDealerWithdraw(owner, _amount);
  }

  /**
   * @dev get the balance which can be used
   */
  function getAvailableBalance() public view returns (uint) {
    return address(this).balance - bankFund;
  }

  /**
   * @notice Returns the address of the LINK token
   * @dev This is the public implementation for chainlinkTokenAddress, which is
   * an internal method of the ChainlinkClient contract
   */
  function getChainlinkToken() public view returns (address) {
    return chainlinkTokenAddress();
  }

  /**
   * @notice Allows the owner to withdraw any LINK balance on the contract
   */
  function withdrawLink() public onlyOwner {
    LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress());
    require(link.transfer(msg.sender, link.balanceOf(address(this))), "Unable to transfer");
  }

  /**
   * @notice Call this method if no response is received within 5 minutes
   * @param _requestId The ID that was generated for the request to cancel
   * @param _payment The payment specified for the request to cancel
   * @param _callbackFunctionId The bytes4 callback function ID specified for
   * the request to cancel
   * @param _expiration The expiration generated for the request to cancel
   */
  function cancelRequest(
    bytes32 _requestId,
    uint256 _payment,
    bytes4 _callbackFunctionId,
    uint256 _expiration
  )
    public
    onlyOwner
  {
    cancelChainlinkRequest(_requestId, _payment, _callbackFunctionId, _expiration);
  }
}
/**
 * @title RandomWorld is an example contract which requests data from
 * the Chainlink network
 * @dev This contract is designed to work on multiple networks, including
 * local test networks
 */
function closeBet(bytes32 _requestId, uint256 _data)
  public
  recordChainlinkFulfillment(_requestId)
{
  Bet storage bet = bets[_requestId];
  require(bet.isActive, 'this bet is not active');
  bet.randomNumber = _data;

  // get win amount
  uint result = _data % bet.modulo;
  uint winAmount = 0;
  if (bet.modulo <= MAX_MASKABLE_MODULO) {
    if ((2 ** result) & bet.choice != 0) {
      winAmount = bet.winAmount;
    }
  } else {
    if (result < bet.choice) {
      winAmount = bet.winAmount;
    }
  }

  if (winAmount > 0) {
    bet.player.transfer(winAmount);
    emit LogDistributeReward(bet.player, winAmount);
  }

  // release winAmount banking
  bankFund = bankFund.sub(bet.winAmount);
  bet.isActive = false;
  emit LogClosedBet(bet.player, bet.modulo, bet.choice, _data, bet.amount, winAmount, _requestId);
}

/**
 * @notice The closeBet to close specific bet with requeistId
 * @dev The recordChainlinkFulfillment protects this function from being called
 * by anyone other than the oracle address that the request was sent to
 * @param _requestId The ID that was generated for the request
 * @param _data The answer provided by the oracle
 */