contract ExpNFT is Context, AccessControlEnumerable, ERC721, ERC721Enumerable, Ownable {
  using Counters for Counters.Counter;

  string base_uri_head = "https://exponentials.art/metadata/";
  string base_uri_tail = ".json";

  Counters.Counter private _tokenIdTracker;

  uint public pricePerNFT = 0.02 ether;

  uint constant public maxNFTPerCall = 20;
  uint constant public teamSupply = 50;

  uint constant public maxTotalSupply = 1000;
  uint constant public publicSupply = maxTotalSupply - teamSupply;

  bool saleStarted = false;

  bool public teamTokensMinted = false;
  uint public tokensBurned = 0;

  mapping(address => bool) public giveAwayWhiteList;

  constructor() ERC721("Exponentials", "EXP") {
        _setupRole(DEFAULT_ADMIN_ROLE, _msgSender());
  }

  function setPrice(uint newPrice) public {
    require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller cannot alter state");
    pricePerNFT = newPrice;
  }

  function toggleSaleState() public {
    require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller cannot alter state");
    saleStarted = !saleStarted;
  }

  function addToGiveAwayWhiteList(address[] memory users) public {
    require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller cannot alter whiteList");
    for(uint i=0; i<users.length; i++) {
      giveAwayWhiteList[users[i]] = true;
    }
  }

  function removeFromGiveAwayWhiteList(address[] memory users) public {
    require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller cannot alter whiteList");
    for(uint i=0; i<users.length; i++) {
      giveAwayWhiteList[users[i]] = false;
    }
  }

  function withdraw() public {
    require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller cannot withdraw");
    payable(owner()).call{value: address(this).balance}("");
  }

  function makeTeamMints() public {
    require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller cannot mint team tokens");
    require(!teamTokensMinted, "Team tokens have already been minted");
    for (uint i=0; i<teamSupply; i++) {
      _mint(owner(), _tokenIdTracker.current());
      _tokenIdTracker.increment();
    }
    teamTokensMinted = true;
  }

  function mint(uint _amount) public payable {
    require(_amount > 0, "Minting 0 tokens is quite useless");
    require(_amount <= maxNFTPerCall, "Amount is higher than maximum per call");
    require(saleIsActive(), "Sale is not active");

    if (giveAwayWhiteList[_msgSender()]) {
      _amount--;
      giveAwayWhiteList[_msgSender()] = false;
      _mint(_msgSender(), _tokenIdTracker.current());
      _tokenIdTracker.increment();
    }

    uint bound; 
    if (teamTokensMinted) {
      bound = maxTotalSupply;
    }
    else { 
      bound = publicSupply;
    }

    if (_amount+_tokenIdTracker.current() > bound) {
      _amount = bound-_tokenIdTracker.current();
    }

    uint amountToPay = _amount*pricePerNFT;
    require(amountToPay <= msg.value, "Provided not enough Ether for purchase");
    for (uint i=0; i<_amount; i++) {
      _mint(_msgSender(), _tokenIdTracker.current());
      _tokenIdTracker.increment();
    }
    if (msg.value > amountToPay) {
      payable(_msgSender()).call{value: msg.value - amountToPay}("");
    }
  }

  function burnMany(uint256[] calldata tokenIds) public {
    require(hasRole(DEFAULT_ADMIN_ROLE, _msgSender()), "Caller cannot alter state");
    for (uint i; i<tokenIds.length; i++) {
      _burn(tokenIds[i]);
    }
    tokensBurned += tokenIds.length;
  }

  function tokensMinted() public view returns(uint) {
    return _tokenIdTracker.current();
  }

  function saleIsActive() public view returns(bool){
    if (teamTokensMinted) {
      return (saleStarted && _tokenIdTracker.current() < maxTotalSupply);
    }
    else {
      return (saleStarted && _tokenIdTracker.current() < publicSupply);
    }
  }

  function tokenURI(uint256 _tokenId) public view override returns (string memory) {
    return string(abi.encodePacked(base_uri_head, Strings.toString(_tokenId), base_uri_tail));
  }

  /** * @dev See {IERC165-supportsInterface}.  */
  function supportsInterface(bytes4 interfaceId) public view virtual
  override(ERC721, ERC721Enumerable, AccessControlEnumerable) returns (bool)
  {
    return super.supportsInterface(interfaceId);
  }

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

}
function supportsInterface(bytes4 interfaceId) public view virtual
override(ERC721, ERC721Enumerable, AccessControlEnumerable) returns (bool)
{
  return super.supportsInterface(interfaceId);
}

/** * @dev See {IERC165-supportsInterface}.  */