contract Bullz is Ownable, ERC721Enumerable, ReentrancyGuard {
  using Counters for Counters.Counter;
  using Strings for uint256;

  string public imageHash;

  bool public isSaleOn = false;
  bool public isPresaleOn = false;

  uint256 public constant MAX_MINTABLE_AT_ONCE = 10;
  uint256 public constant MAX_MINTABLE_AT_ONCE_PRESALE = 3;

  uint256 private _price = 0.03 ether; // 30000000000000000
  
  string public bullzcontractURI;
  
  constructor() ERC721("Bullz Genesis", "BULLZ") {}

  uint256[4444] private _availableTokens;
  uint256 private _numAvailableTokens = 4444;
  uint256 private _numFreeRollsGiven = 0;

  mapping(address => uint256) public freeBullzRolls;
  mapping(address => bool) _whiteList;
  mapping(address => uint256) _whiteListClaimed;

  function totalBullz() public view virtual returns (uint256) {
    return 4444;
  }

  //FREE ROLLS FOR GIVEAWAYS/TEAM

  function freeRollMint() public nonReentrant() {
    require(freeBullzRolls[msg.sender] > 0, "You don't have any free rolls!");
    uint256 toMint = freeBullzRolls[msg.sender];
    freeBullzRolls[msg.sender] = 0;
    uint256 remaining = totalBullz() - totalSupply();
    if (toMint > remaining) {
      toMint = remaining;
    }
    _mint(toMint);
  }

  function getNumFreeBullzRolls(address owner) public view returns (uint256) {
    return freeBullzRolls[owner];
  }

  function addToWhitelist(address[] memory addresses) public onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++) {
          require(addresses[i] != address(0), "Can't add the null address");
        
          _whiteList[addresses[i]] = true;
          _whiteListClaimed[addresses[i]] > 0 ? _whiteListClaimed[addresses[i]] : 0;
        }
    }

  function isWhitelisted (address addr) public view returns (bool) {
      return _whiteList[addr];
  }    

  function removeFromWhiteList(address[] calldata addresses) external onlyOwner {
    for (uint256 i = 0; i < addresses.length; i++) {
      require(addresses[i] != address(0), "Can't remove the null address");
      _whiteList[addresses[i]] = false;
    }
  }    

  function whiteListClaimedBy(address owner) external view returns (uint256){
      require(owner != address(0), 'Zero address is not on  WhiteList');       
      return _whiteListClaimed[owner];
  }

  function flipPresaleState() public onlyOwner {
      isPresaleOn = !isPresaleOn;
  }

  function flipSaleState() public onlyOwner {
    isSaleOn = !isSaleOn;
  }

  function mintPresale(uint _numToMint) public payable {
        require(isPresaleOn, "Presale is not active at the moment");
        require(_whiteList[msg.sender] == true,"This address is not whitelisted for the presale.");
        require(_whiteListClaimed[msg.sender] + _numToMint <= MAX_MINTABLE_AT_ONCE_PRESALE,"Exceeds supply of presale Bullz you can mint.");
        require(_numToMint > 0, "Number of tokens can not be less than or equal to 0");
        uint256 costForMintingBullz = _price * _numToMint;
        require(costForMintingBullz <= msg.value, "Too little sent, please send more eth.");        
        if (msg.value > costForMintingBullz) {
          payable(msg.sender).transfer(msg.value - costForMintingBullz);
        }
        _whiteListClaimed[msg.sender] += _numToMint;
        _mint(_numToMint);
  }

  function mint(uint256 _numToMint) public payable nonReentrant() {
    require(isSaleOn, "Sale hasn't started.");
    uint256 totalSupply = totalSupply();
    require(totalSupply + _numToMint <= totalBullz(),"There aren't this many Bullz left.");
    uint256 costForMintingBullz = _price * _numToMint;
    require(msg.value >= costForMintingBullz,"Too little sent, please send more eth.");
    if (msg.value > costForMintingBullz) {
      payable(msg.sender).transfer(msg.value - costForMintingBullz);
    }
    _mint(_numToMint);
  }

  function _mint(uint256 _numToMint) internal {
    require(_numToMint <= MAX_MINTABLE_AT_ONCE, "Minting too many at once.");
    uint256 updatedNumAvailableTokens = _numAvailableTokens;
    for (uint256 i = 0; i < _numToMint; i++) {
      uint256 newTokenId = useRandomAvailableToken(_numToMint, i);
      _safeMint(msg.sender, newTokenId);
      updatedNumAvailableTokens--;
    }
    _numAvailableTokens = updatedNumAvailableTokens;
  }

  function useRandomAvailableToken(uint256 _numToFetch, uint256 _i)
    internal
    returns (uint256)
  {
    uint256 randomNum =uint256(
        keccak256(
          abi.encode(
            msg.sender,
            tx.gasprice,
            block.number,
            block.timestamp,
            blockhash(block.number - 1),
            _numToFetch,
            _i
          )
        )
      );
    uint256 randomIndex = randomNum % _numAvailableTokens;
    return useAvailableTokenAtIndex(randomIndex);
  }

  function useAvailableTokenAtIndex(uint256 indexToUse)
    internal
    returns (uint256)
  {
    uint256 valAtIndex = _availableTokens[indexToUse];
    uint256 result;
    if (valAtIndex == 0) {
      result = indexToUse;
    } else {
      result = valAtIndex;
    }

    uint256 lastIndex = _numAvailableTokens - 1;
    if (indexToUse != lastIndex) {
      uint256 lastValInArray = _availableTokens[lastIndex];
      if (lastValInArray == 0) {
        _availableTokens[indexToUse] = lastIndex;
      } else {
        _availableTokens[indexToUse] = lastValInArray;
      }
    }
    _numAvailableTokens--;
    return result;
  }

  function getPrice() public view returns (uint256){
    return _price;
  }

  function contractURI() public view returns (string memory){
    return bullzcontractURI;
  }

  function getBullzBelongingToOwner(address _owner)
    external
    view
    returns (uint256[] memory)
  {
    uint256 numOfBullz = balanceOf(_owner);
    if (numOfBullz == 0) {
      return new uint256[](0);
    } else {
      uint256[] memory result = new uint256[](numOfBullz);
      for (uint256 i = 0; i < numOfBullz; i++) {
        result[i] = tokenOfOwnerByIndex(_owner, i);
      }
      return result;
    }
  }

  string private _baseTokenURI;

  function _baseURI() internal view virtual override returns (string memory) {
    return _baseTokenURI;
  }

  function tokenURI(uint256 _tokenId)
    public
    view
    override
    returns (string memory)
  {
    string memory base = _baseURI();
    string memory _tokenURI = Strings.toString(_tokenId);
    string memory ending = ".json";

    if (bytes(base).length == 0) {
      return _tokenURI;
    }

    return string(abi.encodePacked(base, _tokenURI, ending));
  }
  
  function setPrice(uint256 _newPrice) public onlyOwner() {
    _price = _newPrice;
  }

  function giveFreeRoll(address receiver, uint256 numRolls) public onlyOwner {
    require(_numFreeRollsGiven < 200, "Already given max number of free rolls");
    require(freeBullzRolls[receiver] + numRolls < 21, "Cannot exceed 20 unused free rolls!");
    uint256 freeRolls = freeBullzRolls[receiver];
    freeBullzRolls[receiver] = freeRolls + numRolls;
    _numFreeRollsGiven = _numFreeRollsGiven + numRolls;
  }

  function setBaseURI(string memory baseURI) external onlyOwner {
    _baseTokenURI = baseURI;
  }

  function setContractURI(string memory _contractURI) external onlyOwner {
    bullzcontractURI = _contractURI;
  }
  
    function setImageHash(string memory _imageHash) external onlyOwner {
    imageHash = _imageHash;
  }
  
    function withdrawSafe() public onlyOwner {
    (bool success, ) = msg.sender.call{value: address(this).balance}("");
    require(success, "Transfer failed.");
  }

  function _beforeTokenTransfer(
    address from,
    address to,
    uint256 tokenId
  ) internal virtual override(ERC721Enumerable) {
    super._beforeTokenTransfer(from, to, tokenId);
  }

  function supportsInterface(bytes4 interfaceId)
    public
    view
    virtual
    override(ERC721Enumerable)
    returns (bool)
  {
    return super.supportsInterface(interfaceId);
  }
}
function freeRollMint() public nonReentrant() {
  require(freeBullzRolls[msg.sender] > 0, "You don't have any free rolls!");
  uint256 toMint = freeBullzRolls[msg.sender];
  freeBullzRolls[msg.sender] = 0;
  uint256 remaining = totalBullz() - totalSupply();
  if (toMint > remaining) {
    toMint = remaining;
  }
  _mint(toMint);
}

//FREE ROLLS FOR GIVEAWAYS/TEAM