contract BoosterPack is IBoosterPack, Ownable, ReentrancyGuard {

  // Address where funds are collected
  address public payee;

  // Address where elementeum funds are payed to users from
  address public funder;

  // Address that is permitted to call destroyContract
  address public permittedDestroyer;

  // ETH price per card
  uint256 public pricePerCard = 50 finney;

  uint256[] public cardTypeIds;
  uint16 private totalWeight;
  mapping (uint16 => uint256) private rollToCard;
  mapping (uint256 => uint256) private cardToElementeumReturned;
  bytes32 private lastHash;

  IEtherLegendsToken public etherLegendsToken;
  IERC20 public elementeumToken;

  constructor(address payeeWallet, address funderWallet) public 
    Ownable() 
    ReentrancyGuard() {
    payee = payeeWallet;
    funder = funderWallet;
    lastHash = keccak256(abi.encodePacked(block.number));
  }

  /**
   * @dev fallback function ***DO NOT OVERRIDE***
   */
  function () external payable {
    purchaseCards(msg.sender);
  }

  function destroyContract() external {
    require(msg.sender == permittedDestroyer, "caller is not the permitted destroyer - should be address of BoosterPackFactory");
    address payable payableOwner = address(uint160(owner()));
    selfdestruct(payableOwner);
  }

  function setEtherLegendsToken(address addr) external {
    _requireOnlyOwner();
    etherLegendsToken = IEtherLegendsToken(addr);
  }  

  function setElementeumERC20ContractAddress(address addr) external {
    _requireOnlyOwner();
    elementeumToken = IERC20(addr);
  }    

  function setPricePerCard(uint256 price) public {
    _requireOnlyOwner();
    pricePerCard = price;
  }

  function permitDestruction(address addr) external {
    _requireOnlyOwner();
    require(addr != address(0));
    permittedDestroyer = addr;
  }

  function setDropWeights(uint256[] calldata tokenTypeIds, uint8[] calldata weights, uint256[] calldata elementeumsReturned) external {
    _requireOnlyOwner();
    require(
      tokenTypeIds.length > 0 && 
      tokenTypeIds.length == weights.length && 
      tokenTypeIds.length == elementeumsReturned.length, 
      "array lengths are not the same");

    for(uint256 i = 0; i < tokenTypeIds.length; i++) {
      setDropWeight(tokenTypeIds[i], weights[i], elementeumsReturned[i]);
    }    
  }

  function setDropWeight(uint256 tokenTypeId, uint8 weight, uint256 elementeumReturned) public {
    _requireOnlyOwner();    
    require(etherLegendsToken.hasTokenDefinition(tokenTypeId), "card is not defined");
    totalWeight += weight;
    for(uint16 i = totalWeight - weight; i < totalWeight; i++) {
      rollToCard[i] = tokenTypeId;
    }
    cardToElementeumReturned[tokenTypeId] = elementeumReturned;
    cardTypeIds.push(tokenTypeId);
  }

  function getNumberOfCards() external view returns (uint256) {
    return cardTypeIds.length;
  }

  function getCardTypeIdAtIndex(uint256 index) external view returns (uint256) {
    require(index < cardTypeIds.length, "Index Out Of Range");
    return cardTypeIds[index];
  }

  function getPricePerCard() external view returns (uint256) {
    return pricePerCard;
  }

  function getCardTypeIds() external view returns (uint256[] memory) {
    return cardTypeIds;
  }  

  function purchaseCards(address beneficiary) public payable nonReentrant {
    require(msg.sender == tx.origin, "caller must be transaction origin (only human)");    
    require(msg.value >= pricePerCard, "purchase price not met");
    require(pricePerCard > 0, "price per card must be greater than 0");
    require(totalWeight > 0, "total weight must be greater than 0");

    uint256 numberOfCards = _min(msg.value / pricePerCard, (gasleft() - 100000) / 200000);
    uint256 totalElementeumToReturn = 0;
    bytes32 tempLastHash =  lastHash;    
    for(uint256 i = 0; i < numberOfCards; i++) {
        tempLastHash = keccak256(abi.encodePacked(block.number, tempLastHash, msg.sender, gasleft()));
        uint16 randNumber = uint16(uint256(tempLastHash) % (totalWeight));        
        uint256 cardType = rollToCard[randNumber];

        etherLegendsToken.mintTokenOfType(beneficiary, cardType);        
        totalElementeumToReturn += cardToElementeumReturned[cardType];                
    }

    lastHash = tempLastHash; // Save in the blockchain for next tx
    
    if(totalElementeumToReturn > 0) {
      uint256 elementeumThatCanBeReturned = _min(totalElementeumToReturn, _min(elementeumToken.allowance(funder, address(this)), elementeumToken.balanceOf(funder)));
      if(elementeumThatCanBeReturned > 0) {
        elementeumToken.transferFrom(funder, beneficiary, elementeumThatCanBeReturned);      
      }            
    }

    uint256 change = msg.value - (pricePerCard * numberOfCards); //This amount to be refunded as it was unused
    address payable payableWallet = address(uint160(payee));
    payableWallet.transfer(pricePerCard  * numberOfCards);
    if(change > 0) {
      msg.sender.transfer(change);
    }
  }

  function _min(uint256 a, uint256 b) internal pure returns (uint256) {
    return a < b ? a : b;
  }  

  function _requireOnlyOwner() internal view {
    require(isOwner(), "Ownable: caller is not the owner");
  }
}
function () external payable {
  purchaseCards(msg.sender);
}

/**
 * @dev fallback function ***DO NOT OVERRIDE***
 */