contract ZethrSlots is ZethrGame {

  /****************************
   * GAME SPECIFIC
   ****************************/

  // Slots-specific bet structure
  struct Bet {
    // Must contain these in this order
    uint56 tokenValue;
    uint48 blockNumber;
    uint8 tier;
    // Game specific
    uint8 numSpins;
  }

  /****************************
   * FIELDS
   ****************************/

  // Sections with identical multipliers compressed for optimization
  uint[14] MULTIPLIER_BOUNDARIES = [uint(299), 3128, 44627, 46627, 49127, 51627, 53127, 82530, 150423, 310818, 364283, 417748, 471213, ~uint256(0)];

  // Maps indexes of results sections to multipliers as x/100
  uint[14] MULTIPLIERS = [uint(5000), 2000, 300, 1100, 750, 900, 1300, 250, 150, 100, 200, 125, 133, 250];

  // The holding contract for jackpot tokens
  JackpotHolding public jackpotHoldingContract;

  /****************************
   * CONSTRUCTOR
   ****************************/

  constructor (address _controllerAddress, uint _resolverPercentage, string _name)
  ZethrGame(_controllerAddress, _resolverPercentage, _name)
  public
  {
  }

  /****************************
   * USER METHODS
   ****************************/

  /** @dev Retrieve the results of the last spin of a plyer, for web3 calls.
    * @param _playerAddress The address of the player
    */
  function getLastSpinOutput(address _playerAddress)
  public view
  returns (uint winAmount, uint lossAmount, uint jackpotAmount, uint jackpotWins, uint[] memory output)
  {
    // Cast to Bet and read from storage
    Bet storage playerBetInStorage = getBet(_playerAddress);
    Bet memory playerBet = playerBetInStorage;

    // Safety check
    require(playerBet.blockNumber != 0);

    (winAmount, lossAmount, jackpotAmount, jackpotWins, output) = getSpinOutput(playerBet.blockNumber, playerBet.numSpins, playerBet.tokenValue.mul(1e14), _playerAddress);

    return (winAmount, lossAmount, jackpotAmount, jackpotWins, output);
  }
    
    event SlotsResult(
        uint    _blockNumber,
        address _target,
        uint    _numSpins,
        uint    _tokenValue,
        uint    _winAmount,
        uint    _lossAmount,
        uint[]  _output
    );
    
  /** @dev Retrieve the results of the spin, for web3 calls.
    * @param _blockNumber The block number of the spin
    * @param _numSpins The number of spins of this bet
    * @param _tokenValue The total number of tokens bet
    * @param _target The address of the better
    * @return winAmount The total number of tokens won
    * @return lossAmount The total number of tokens lost
    * @return jackpotAmount The total amount of tokens won in the jackpot
    * @return output An array of all of the results of a multispin
    */
  function getSpinOutput(uint _blockNumber, uint _numSpins, uint _tokenValue, address _target)
  public view
  returns (uint winAmount, uint lossAmount, uint jackpotAmount, uint jackpotWins, uint[] memory output)
  {
    output = new uint[](_numSpins);
    // Where the result sections start and stop

    // If current block for the first spin is older than 255 blocks, ALL spins are losses
    if (block.number - _blockNumber > 255) {
      // Output stays 0 for eveything, this is a loss
      // No jackpot wins
      // No wins
      // Loss is the total tokens bet
      lossAmount = (_tokenValue.mul(_numSpins).mul(99)).div(100);
      jackpotAmount = _tokenValue.mul(_numSpins).div(100);
    } else {

      for (uint i = 0; i < _numSpins; i++) {
        // Store the output
        output[i] = random(1000000, _blockNumber, _target, i) + 1;

        if (output[i] < 2) {
          // Jackpot get
          jackpotWins++;
          lossAmount += _tokenValue;
        } else if (output[i] > 506856) {
          // Player loss
          lossAmount += (_tokenValue.mul(99)).div(100);
          jackpotAmount += _tokenValue.div(100);
        } else {
          // Player win

          // Iterate over the array of win results to find the correct multiplier array index
          uint index;
          for (index = 0; index < MULTIPLIER_BOUNDARIES.length; index++) {
            if (output[i] < MULTIPLIER_BOUNDARIES[index]) break;
          }
          // Use index to find the correct multipliers
          winAmount += _tokenValue.mul(MULTIPLIERS[index]).div(100);
        }
      }
    }
    emit SlotsResult(_blockNumber, _target, _numSpins, _tokenValue, winAmount, lossAmount, output);
    return (winAmount, lossAmount, jackpotAmount, jackpotWins, output);
  }

  /** @dev Retrieve the results of the spin, for contract calls.
    * @param _blockNumber The block number of the spin
    * @param _numSpins The number of spins of this bet
    * @param _tokenValue The total number of tokens bet
    * @param _target The address of the better
    * @return winAmount The total number of tokens won
    * @return lossAmount The total number of tokens lost
    * @return jackpotAmount The total amount of tokens won in the jackpot
    */
  function getSpinResults(uint _blockNumber, uint _numSpins, uint _tokenValue, address _target)
  public
  returns (uint winAmount, uint lossAmount, uint jackpotAmount, uint jackpotWins)
  {
    // Where the result sections start and stop

    // If current block for the first spin is older than 255 blocks, ALL spins are losses
    if (block.number - _blockNumber > 255) {
      // Output stays 0 for eveything, this is a loss
      // No jackpot wins
      // No wins
      // Loss is the total tokens bet
      lossAmount = (_tokenValue.mul(_numSpins).mul(99)).div(100);
      jackpotAmount = _tokenValue.mul(_numSpins).div(100);
    } else {

      uint result;

      for (uint i = 0; i < _numSpins; i++) {
        // Store the output
        result = random(1000000, _blockNumber, _target, i) + 1;

        if (result < 2) {
          // Jackpot get
          jackpotWins++;
        } else if (result > 506856) {
          // Player loss
          lossAmount += (_tokenValue.mul(99)).div(100);
          jackpotAmount += _tokenValue.div(100);
        } else {
          // Player win

          // Iterate over the array of win results to find the correct multiplier array index
          uint index;
          for (index = 0; index < MULTIPLIER_BOUNDARIES.length; index++) {
            if (result < MULTIPLIER_BOUNDARIES[index]) break;
          }
          // Use index to find the correct multipliers
          winAmount += _tokenValue.mul(MULTIPLIERS[index]).div(100);
        }
      }
    }
    return (winAmount, lossAmount, jackpotAmount, jackpotWins);
  }

  /****************************
   * OWNER METHODS
   ****************************/

  /** @dev Set the address of the jackpot contract
    * @param _jackpotAddress The address of the jackpot contract
    */
  function ownerSetJackpotAddress(address _jackpotAddress)
  public
  ownerOnly
  {
    jackpotHoldingContract = JackpotHolding(_jackpotAddress);
  }

  /****************************
   * INTERNALS
   ****************************/

  /** @dev Returs the bet struct of a player
    * @param _playerAddress The address of the player
    * @return Bet The bet of the player
    */
  function getBet(address _playerAddress)
  internal view
  returns (Bet storage)
  {
    // Cast BetBase to Bet
    BetBase storage betBase = bets[_playerAddress];

    Bet storage playerBet;
    assembly {
    // tmp is pushed onto stack and points to betBase slot in storage
      let tmp := betBase_slot

    // swap1 swaps tmp and playerBet pointers
      swap1
    }
    // tmp is popped off the stack

    // playerBet now points to betBase
    return playerBet;
  }

  /** @dev Resturns a random number
    * @param _blockn The block number to base the random number off of
    * @param _entropy Data to use in the random generation
    * @param _index Data to use in the random generation
    * @return randomNumber The random number to return
    */
  function maxRandom(uint _blockn, address _entropy, uint _index)
  private view
  returns (uint256 randomNumber)
  {
    return uint256(keccak256(
        abi.encodePacked(
          blockhash(_blockn),
          _entropy,
          _index
        )));
  }

  /** @dev Returns a random number
    * @param _upper The upper end of the range, exclusive
    * @param _blockn The block number to use for the random number
    * @param _entropy An address to be used for entropy
    * @param _index A number to get the next random number
    * @return randomNumber The random number
    */
  function random(uint256 _upper, uint256 _blockn, address _entropy, uint _index)
  internal view
  returns (uint256 randomNumber)
  {
    return maxRandom(_blockn, _entropy, _index) % _upper;
  }

  /****************************
   * OVERRIDDEN METHODS
   ****************************/

  /** @dev Resolves the bet of the supplied player.
    * @param _playerAddress The address of the player whos bet we are resolving
    * @return totalProfit The total profit the player earned, positive or negative
    */
  function finishBetFrom(address _playerAddress)
  internal
  returns (int /*totalProfit*/)
  {
    // Memory vars to hold data as we compute it
    uint winAmount;
    uint lossAmount;
    uint jackpotAmount;
    uint jackpotWins;

    // Cast to Bet and read from storage
    Bet storage playerBetInStorage = getBet(_playerAddress);
    Bet memory playerBet = playerBetInStorage;

    // Player should not be able to resolve twice!
    require(playerBet.blockNumber != 0);

    // Safety check
    require(playerBet.blockNumber != 0);
    playerBetInStorage.blockNumber = 0;

    // Iterate over the number of spins and calculate totals:
    //  - player win amount
    //  - bankroll win amount
    //  - jackpot wins
    (winAmount, lossAmount, jackpotAmount, jackpotWins) = getSpinResults(playerBet.blockNumber, playerBet.numSpins, playerBet.tokenValue.mul(1e14), _playerAddress);

    // Figure out the token bankroll address
    address tokenBankrollAddress = controller.getTokenBankrollAddressFromTier(playerBet.tier);
    ZethrTokenBankrollInterface bankroll = ZethrTokenBankrollInterface(tokenBankrollAddress);

    // Call into the bankroll to do some token accounting
    bankroll.gameTokenResolution(winAmount, _playerAddress, jackpotAmount, address(jackpotHoldingContract), playerBet.tokenValue.mul(1e14).mul(playerBet.numSpins));

    // Pay out jackpot if won
    if (jackpotWins > 0) {
      for (uint x = 0; x < jackpotWins; x++) {
        jackpotHoldingContract.gamePayOutWinner(_playerAddress);
      }
    }

    // Grab the position of the player in the pending bets queue
    uint index = pendingBetsMapping[_playerAddress];

    // Remove the player from the pending bets queue by setting the address to 0x0
    pendingBetsQueue[index] = address(0x0);

    // Delete the player's bet by setting the mapping to zero
    pendingBetsMapping[_playerAddress] = 0;

    emit Result(_playerAddress, playerBet.tokenValue.mul(1e14), int(winAmount) - int(lossAmount) - int(jackpotAmount));

    // Return all bet results + total *player* profit
    return (int(winAmount) - int(lossAmount) - int(jackpotAmount));
  }

  /** @dev Places a bet. Callable only by token bankrolls
    * @param _player The player that is placing the bet
    * @param _tokenCount The total number of tokens bet
    * @param _tier The div rate tier the player falls in
    * @param _data The game-specific data, encoded in bytes-form
    */
  function execute(address _player, uint _tokenCount, uint _tier, bytes _data)
  isNotPaused
  bankrollOnly
  betIsValid(_tokenCount, _tier, _data)
  hasNotBetThisBlock(_player)
  public
  {
    Bet storage playerBet = getBet(_player);

    // Check for a player bet and resolve if necessary
    if (playerBet.blockNumber != 0) {
      finishBetFrom(_player);
    }

    uint8 spins = uint8(_data[0]);

    // Set bet information
    playerBet.tokenValue = uint56(_tokenCount.div(spins).div(1e14));
    playerBet.blockNumber = uint48(block.number);
    playerBet.tier = uint8(_tier);
    playerBet.numSpins = spins;

    // Add player to the pending bets queue
    pendingBetsQueue.length++;
    pendingBetsQueue[queueTail] = _player;
    queueTail++;

    // Add the player's position in the queue to the pending bets mapping
    pendingBetsMapping[_player] = queueTail - 1;

    // Emit event
    emit Wager(_player, _tokenCount, _data);
  }

  /** @dev Determines if a supplied bet is valid
    * @param _tokenCount The total number of tokens bet
    * @param _data The game-specific bet data
    * @return bool Whether or not the bet is valid
    */
  function isBetValid(uint _tokenCount, uint /*_divRate*/, bytes _data)
  public view
  returns (bool)
  {
    // Since the max win is 50x (for slots), the bet size must be
    // <= 1/50 * the maximum profit.
    uint8 spins = uint8(_data[0]);
    return (_tokenCount.div(spins).mul(50) <= getMaxProfit()) && (_tokenCount.div(spins) >= minBet);
  }
}
/* The actual game contract.
 *
 * This contract contains the actual game logic,
 * including placing bets (execute), resolving bets,
 * and resolving expired bets.
 */
function getSpinOutput(uint _blockNumber, uint _numSpins, uint _tokenValue, address _target)
public view
returns (uint winAmount, uint lossAmount, uint jackpotAmount, uint jackpotWins, uint[] memory output)
{
  output = new uint[](_numSpins);
  // Where the result sections start and stop

  // If current block for the first spin is older than 255 blocks, ALL spins are losses
  if (block.number - _blockNumber > 255) {
    // Output stays 0 for eveything, this is a loss
    // No jackpot wins
    // No wins
    // Loss is the total tokens bet
    lossAmount = (_tokenValue.mul(_numSpins).mul(99)).div(100);
    jackpotAmount = _tokenValue.mul(_numSpins).div(100);
  } else {

    for (uint i = 0; i < _numSpins; i++) {
      // Store the output
      output[i] = random(1000000, _blockNumber, _target, i) + 1;

      if (output[i] < 2) {
        // Jackpot get
        jackpotWins++;
        lossAmount += _tokenValue;
      } else if (output[i] > 506856) {
        // Player loss
        lossAmount += (_tokenValue.mul(99)).div(100);
        jackpotAmount += _tokenValue.div(100);
      } else {
        // Player win

        // Iterate over the array of win results to find the correct multiplier array index
        uint index;
        for (index = 0; index < MULTIPLIER_BOUNDARIES.length; index++) {
          if (output[i] < MULTIPLIER_BOUNDARIES[index]) break;
        }
        // Use index to find the correct multipliers
        winAmount += _tokenValue.mul(MULTIPLIERS[index]).div(100);
      }
    }
  }
  emit SlotsResult(_blockNumber, _target, _numSpins, _tokenValue, winAmount, lossAmount, output);
  return (winAmount, lossAmount, jackpotAmount, jackpotWins, output);
}

/** @dev Retrieve the results of the spin, for web3 calls.
 * @param _blockNumber The block number of the spin
 * @param _numSpins The number of spins of this bet
 * @param _tokenValue The total number of tokens bet
 * @param _target The address of the better
 * @return winAmount The total number of tokens won
 * @return lossAmount The total number of tokens lost
 * @return jackpotAmount The total amount of tokens won in the jackpot
 * @return output An array of all of the results of a multispin
 */