contract PickflixGameMaster is Ownable {
  // this library helps protect against overflows for large integers
  using SafeMath for uint256;

  // fires off events for receiving and sending Ether
  event Sent(address indexed payee, uint256 amount, uint256 balance);
  event Received(address indexed payer, uint256 amount, uint256 balance);

  string public gameName;
  uint public openDate;
  uint public closeDate;
  bool public gameDone;
  
  // create a mapping for box office totals for particular movies
  // address is the token contract address
  mapping (address => uint256) public boxOfficeTotals;

  // let's make a Movie struct to make all of this code cleaner
  struct Movie {
    uint256 boxOfficeTotal;
    uint256 totalPlayerRewards;
    bool accepted;
  }
  // map token addresses to Movie structs
  mapping (address => Movie) public movies;

  // count the total number of tokens issued
  uint256 public tokensIssued = 0; // this number will change

  // more global variables, for calculating payouts and game results
  uint256 public oracleFee = 0;
  uint256 public oracleFeePercent = 0;
  uint256 public totalPlayerRewards = 0;
  uint256 public totalBoxOffice = 0;


  // owner is set to original message sender during contract migration
  constructor(string _gameName, uint _closeDate, uint _oracleFeePercent) Ownable() public {
    gameName = _gameName;
    closeDate = _closeDate;
    openDate = block.timestamp;
    gameDone = false;
    oracleFeePercent = _oracleFeePercent;
  }

  /**
    * calculate a percentage with parts per notation.
    * the value returned will be in terms of 10e precision
  */
  function percent(uint numerator, uint denominator, uint precision) private pure returns(uint quotient) {
    // caution, keep this a private function so the numbers are safe
    uint _numerator = (numerator * 10 ** (precision+1));
    // with rounding of last digit
    uint _quotient = ((_numerator / denominator)) / 10;
    return ( _quotient);
  }

  /**
  * @dev wallet can receive funds.
  */
  function () public payable {
    emit Received(msg.sender, msg.value, address(this).balance);
  }

  /**
  * @dev wallet can send funds
  */
  function sendTo(address _payee, uint256 _amount) private {
    require(_payee != 0 && _payee != address(this), "Burning tokens and self transfer not allowed");
    require(_amount > 0, "Must transfer greater than zero");
    _payee.transfer(_amount);
    emit Sent(_payee, _amount, address(this).balance);
  }

  /**
  * @dev function to see the balance of Ether in the wallet
  */
  function balanceOf() public view returns (uint256) {
    return address(this).balance;
  }

  /**
  * @dev function for the player to cash in tokens
  */
  function redeemTokens(address _player, address _tokenAddress) public returns (bool success) {
    require(acceptedToken(_tokenAddress), "Token must be a registered token");
    require(block.timestamp >= closeDate, "Game must be closed");
    require(gameDone == true, "Can't redeem tokens until results have been uploaded");
    // instantiate a token contract instance from the deployed address
    IPickFlixToken _token = IPickFlixToken(_tokenAddress);
    // check token allowance player has given to GameMaster contract
    uint256 _allowedValue = _token.allowance(_player, address(this));
    // transfer tokens to GameMaster
    _token.transferFrom(_player, address(this), _allowedValue);
    // check balance of tokens actually transfered
    uint256 _transferedTokens = _allowedValue;
    // calculate the percentage of the total token supply represented by the transfered tokens
    uint256 _playerPercentage = percent(_transferedTokens, _token.totalSupply(), 4);
    // calculate the particular player's rewards, as a percentage of total player rewards for the movie
    uint256 _playerRewards = movies[_tokenAddress].totalPlayerRewards.mul(_playerPercentage).div(10**4);
    // pay out ETH to the player
    sendTo(_player, _playerRewards);
    // return that the function succeeded
    return true;
  }

  // checks if a token is an accepted game token
  function acceptedToken(address _tokenAddress) public view returns (bool) {
    return movies[_tokenAddress].accepted;
  }

  /**
  * @dev functions to calculate game results and payouts
  */
  function calculateTokensIssued(address _tokenAddress) private view returns (uint256) {
    IPickFlixToken _token = IPickFlixToken(_tokenAddress);
    return _token.totalSupply();
  }

  function closeToken(address _tokenAddress) private {
    IPickFlixToken _token = IPickFlixToken(_tokenAddress);
    _token.closeNow();
  }

  function calculateTokenRate(address _tokenAddress) private view returns (uint256) {
    IPickFlixToken _token = IPickFlixToken(_tokenAddress);
    return _token.rate();
  }

  // "15" in this function means 15%. Change that number to raise or lower
  // the oracle fee.
  function calculateOracleFee() private view returns (uint256) {
    return balanceOf().mul(oracleFeePercent).div(100);
  }

  // this calculates how much Ether is available for player rewards
  function calculateTotalPlayerRewards() private view returns (uint256) {
    return balanceOf().sub(oracleFee);
  }

  // this calculates the total box office earnings of all movies in USD
  function calculateTotalBoxOffice(uint256[] _boxOfficeTotals) private pure returns (uint256) {
    uint256 _totalBoxOffice = 0;
    for (uint256 i = 0; i < _boxOfficeTotals.length; i++) {
      _totalBoxOffice = _totalBoxOffice.add(_boxOfficeTotals[i]);
    }
    return _totalBoxOffice;
  }

  // this calculates how much Ether to reward for each game token
  function calculateTotalPlayerRewardsPerMovie(uint256 _boxOfficeTotal) public view returns (uint256) {
    // 234 means 23.4%, using parts-per notation with three decimals of precision
    uint256 _boxOfficePercentage = percent(_boxOfficeTotal, totalBoxOffice, 4);
    // calculate the Ether rewards available for each movie
    uint256 _rewards = totalPlayerRewards.mul(_boxOfficePercentage).div(10**4);
    return _rewards;
  }

  function calculateRewardPerToken(uint256 _boxOfficeTotal, address tokenAddress) public view returns (uint256) {
    IPickFlixToken token = IPickFlixToken(tokenAddress);
    uint256 _playerBalance = token.balanceOf(msg.sender);
    uint256 _playerPercentage = percent(_playerBalance, token.totalSupply(), 4);
    // calculate the particular player's rewards, as a percentage of total player rewards for the movie
    uint256 _playerRewards = movies[tokenAddress].totalPlayerRewards.mul(_playerPercentage).div(10**4);
    return _playerRewards;
  }

  /**
  * @dev add box office results and token addresses for the movies, and calculate game results
  */
  function calculateGameResults(address[] _tokenAddresses, uint256[] _boxOfficeTotals) public onlyOwner {
    // check that there are as many box office totals as token addresses
    require(_tokenAddresses.length == _boxOfficeTotals.length, "Must have box office results per token");
    // calculate Oracle Fee and amount of Ether available for player rewards
    require(gameDone == false, "Can only submit results once");
    require(block.timestamp >= closeDate, "Game must have ended before results can be entered");
    oracleFee = calculateOracleFee();
    totalPlayerRewards = calculateTotalPlayerRewards();
    totalBoxOffice = calculateTotalBoxOffice(_boxOfficeTotals);

    // create Movies (see: Movie struct) and calculate player rewards
    for (uint256 i = 0; i < _tokenAddresses.length; i++) {
      tokensIssued = tokensIssued.add(calculateTokensIssued(_tokenAddresses[i]));
      movies[_tokenAddresses[i]] = Movie(_boxOfficeTotals[i], calculateTotalPlayerRewardsPerMovie(_boxOfficeTotals[i]), true);
    }

    // The owner will be the Factory that deploys this contract.
    owner().transfer(oracleFee);
    gameDone = true;
  }

  /**
   * @dev add box office results and token addresses for the movies, and calculate game results
   */
  function abortGame(address[] _tokenAddresses) public onlyOwner {
    // calculate Oracle Fee and amount of Ether available for player rewards
    require(gameDone == false, "Can only submit results once");
    oracleFee = 0;
    totalPlayerRewards = calculateTotalPlayerRewards();
    closeDate = block.timestamp;

    for (uint256 i = 0; i < _tokenAddresses.length; i++) {
      uint tokenSupply = calculateTokensIssued(_tokenAddresses[i]);
      tokensIssued = tokensIssued.add(tokenSupply);
      closeToken(_tokenAddresses[i]);
    }
    totalBoxOffice = tokensIssued;

    // create Movies (see: Movie struct) and calculate player rewards
    for (i = 0; i < _tokenAddresses.length; i++) {
      tokenSupply = calculateTokensIssued(_tokenAddresses[i]);
      movies[_tokenAddresses[i]] = Movie(tokenSupply, calculateTotalPlayerRewardsPerMovie(tokenSupply), true);
    }

    gameDone = true;
  }

  function killGame(address[] _tokenAddresses) public onlyOwner {
    for (uint i = 0; i < _tokenAddresses.length; i++) {
      IPickFlixToken token = IPickFlixToken(_tokenAddresses[i]);
      require(token.balanceOf(this) == token.totalSupply());
      token.kill();
    }
    selfdestruct(owner());
  }
}
function calculateTotalPlayerRewards() private view returns (uint256) {
  return balanceOf().sub(oracleFee);
}

// this calculates how much Ether is available for player rewards