contract DynamicDuelers is ERC721, ERC721Enumerable, AccessControl, Ownable {
    
  
  // Provenance
  string public duelerHash = "";

  // Total supply = 10000 = 9100 + 200 + 200 + 500
  uint16 constant retailSupply = 9100;
  uint256 public devReserve = 200;
  uint256 public stakerSupply = 200;
  uint256 public presaleSupply = 500;
  
  uint256 public devMinted = 0;
  uint256 public stakerMinted = 0;
  uint256 public presaleMinted = 0;

  // Internals
  string private _baseTokenURI;
  uint256 public startingIndexBlock;
  uint256 public startingIndex;  

  // Sale
  bool public hasSaleStarted = false;
  bool public hasPresaleStarted = false; 
  uint64 public priceIncreaseAt = 8500;
  uint16 public MAX_MINT_PER_CALL = 15;
  
  
  // Access control role set up
  
  bytes32 public constant MODERATOR_ROLE = keccak256("MODERATOR_ROLE");
  bytes32 public constant STAKER_ROLE = keccak256("STAKER_ROLE");
  
  // Contract is coded in a way that msg.sender will never have the ownership, even if specified in the constructor owner variable
  // Message sender is instead given the moderator role, with limited access
  
  constructor() ERC721("Dynamic Duelers", "HEROES") { 
    _setupRole(DEFAULT_ADMIN_ROLE, msg.sender); // Temporarily gives the message sender admin access as a workaround for AccessControl limitations
    grantRole(MODERATOR_ROLE, msg.sender);
    _setRoleAdmin(STAKER_ROLE, MODERATOR_ROLE);
  }

  // Grants the list of addresses the staker role, useful, to save GAS
  
  function addMultipleStakerRoles(address[] memory _stakers) public onlyRole(MODERATOR_ROLE){
  for(uint i = 0; i < _stakers.length; i++) grantRole(STAKER_ROLE, _stakers[i]);
  }
  
  // Everything concerning token URI

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

  function setBaseURI(string memory baseURI) public onlyOwner{
    _baseTokenURI = baseURI;
  }
  
  function getBaseURI() external view returns(string memory) {
    return _baseTokenURI;
  }
  
  // Sets the price for the NFT's 
  
  function getHeroPrice() public view returns (uint256) {
    uint currentSupply = totalSupply();
    if (currentSupply >= 0 && currentSupply < 501){
        return 35000000000000000;
    }else if(currentSupply >= 501 && currentSupply < 5000){
        return 65000000000000000;
    }else if(currentSupply >= 5001){
        return 70000000000000000;
    }
   }
    
  function priceRange() public view returns (uint256) {
    uint256 range = priceIncreaseAt - totalSupply();
    return range;
  }
    
  function mint(uint256 numNFTs) public payable {
    require(hasSaleStarted, "Sale has not started");
    require(retailSupply > totalSupply() - devMinted - stakerMinted - presaleMinted, "Sale has ended");
    require(numNFTs > 0 && numNFTs <= MAX_MINT_PER_CALL, "Exceeds MAX_MINT_PER_CALL");
    require(numNFTs <= priceRange(), "Number of NFTs would exceed the price range");
    require(retailSupply >= totalSupply() + numNFTs - devMinted - stakerMinted - presaleMinted, "Exceeds retail supply");
    require(msg.value >= getHeroPrice() * numNFTs, "Incorrect ether value");

    for (uint i = 0; i < numNFTs; i++) {
      uint mintIndex = totalSupply() + 1; // +1 so it doesn't start on index 0.
      _safeMint(msg.sender, mintIndex);
    }

    // If we haven't set the starting index and this is either 1) the last saleable token or 2) the first token to be sold after
    // the end of pre-sale, set the starting index block
    if (startingIndexBlock == 0 && (totalSupply() == retailSupply)) {
      startingIndexBlock = block.number;
    } 
  }
  
  function presaleMint(uint256 numNFTs) public payable {
    require(hasPresaleStarted, "Sale has not started");
    require(presaleSupply > 0, "Presale supply is depleted");
    require(msg.value >= getHeroPrice() * numNFTs, "Incorrect ether value");
    require(presaleSupply - numNFTs >= 0, "Exceeds presaile supply, mint fewer NFT's");
    require(numNFTs <= priceRange(), "Number of NFTs would exceed the price range");
    
    for (uint i = 0; i < numNFTs; i++) {
      uint mintIndex = totalSupply() + 1; // +1 so it doesn't start on index 0.
      _safeMint(msg.sender, mintIndex);
    }
      
    if (startingIndexBlock == 0 && (totalSupply() == retailSupply)) {
      startingIndexBlock = block.number;}
      
    presaleSupply = presaleSupply - numNFTs;
    presaleMinted = presaleMinted + numNFTs;
  }
  
  // minting function for stakes 
  
  function mintForStakers(uint256 numNFTs) public payable onlyRole(STAKER_ROLE){
    require(hasSaleStarted, "Sale has not started");
    require(stakerSupply > 0, "Staker reserve is empty!");
    require(msg.value >= getHeroPrice() * numNFTs, "Incorrect ether value");
    require(stakerSupply - numNFTs >= 0, "Exceeds retail supply");
    require(numNFTs <= priceRange(), "Number of NFTs would exceed the price range");
    
    for (uint i = 0; i < numNFTs; i++) {
      uint mintIndex = totalSupply() + 1; // +1 so it doesn't start on index 0.
      _safeMint(msg.sender, mintIndex);
    }
      
    if (startingIndexBlock == 0 && (totalSupply() == retailSupply)) {
      startingIndexBlock = block.number;}
      
    stakerSupply = stakerSupply - numNFTs;
    stakerMinted = stakerMinted + numNFTs;
  }
  
  function devMint(uint256 numNFTs) public onlyOwner{
      require(numNFTs <= devReserve, "Not enough in the dev reserve, mint a smaller ammount");
      
      for (uint i = 0; i < numNFTs; i++) {
      uint mintIndex = totalSupply() + 1; // +1 so it doesn't start on index 0.
      _safeMint(msg.sender, mintIndex);
        }
      
      devReserve = devReserve - numNFTs;
      devMinted = devMinted + numNFTs;
      
      if (startingIndexBlock == 0 && (totalSupply() == retailSupply)) {
      startingIndexBlock = block.number;}
  }

  function flipSaleState() public onlyRole(MODERATOR_ROLE) {
    hasSaleStarted = !hasSaleStarted;
  }
  
  function flipPresaleState() public onlyRole(MODERATOR_ROLE){
      hasPresaleStarted = !hasPresaleStarted;
  }
  
  function withdraw() public onlyOwner{
    require(payable(msg.sender).send(address(this).balance));
  }
  function _beforeTokenTransfer(address from, address to, uint256 tokenId)
    internal
    override(ERC721, ERC721Enumerable)
  {
    super._beforeTokenTransfer(from, to, tokenId);
  }

  function _burn(uint256 tokenId) internal override(ERC721) {
    super._burn(tokenId);
  }

  function tokenURI(uint256 tokenId)
    public
    view
    override(ERC721)
    returns (string memory)
  {
    return super.tokenURI(tokenId);
  }
  function supportsInterface(bytes4 interfaceId)
    public
    view
    override(ERC721, ERC721Enumerable, AccessControl)
    returns (bool)
  {
    return super.supportsInterface(interfaceId);
  }

  function setStartingIndex() public {
    require(startingIndex == 0, "Starting index is already set");
    require(startingIndexBlock != 0, "Starting index block must be set");
    
    startingIndex = uint(blockhash(startingIndexBlock)) % retailSupply;
    // Just a sanity case in the worst case if this function is called late (EVM only stores last 256 block hashes)
    if (block.number - startingIndexBlock > 255) {
      startingIndex = uint(blockhash(block.number - 1)) % retailSupply;
    }
    // Prevent default sequence
    if (startingIndex == 0) {
      startingIndex = startingIndex + 1;
    }
  }

  function emergencySetStartingIndexBlock() public onlyRole(MODERATOR_ROLE){
    require(startingIndex == 0, "Starting index is already set");
    startingIndexBlock = block.number;
  }

  function setProvenanceHash(string memory provenanceHash) public onlyRole(MODERATOR_ROLE){
    duelerHash = provenanceHash;
  }
}
function mintForStakers(uint256 numNFTs) public payable onlyRole(STAKER_ROLE){
  require(hasSaleStarted, "Sale has not started");
  require(stakerSupply > 0, "Staker reserve is empty!");
  require(msg.value >= getHeroPrice() * numNFTs, "Incorrect ether value");
  require(stakerSupply - numNFTs >= 0, "Exceeds retail supply");
  require(numNFTs <= priceRange(), "Number of NFTs would exceed the price range");
    
  for (uint i = 0; i < numNFTs; i++) {
    uint mintIndex = totalSupply() + 1; // +1 so it doesn't start on index 0.
    _safeMint(msg.sender, mintIndex);
  }
      
  if (startingIndexBlock == 0 && (totalSupply() == retailSupply)) {
    startingIndexBlock = block.number;}
      
  stakerSupply = stakerSupply - numNFTs;
  stakerMinted = stakerMinted + numNFTs;
}

// minting function for stakes 