contract GNAUTS is ERC721Enumerable, Ownable {
  using Strings for uint256;

  string public baseURI;
  string public baseExtension = ".json";
  string public notRevealedUri;
  uint256 public cost = 0.25 ether;
  uint256 public maxSupply = 777;
  uint256 public maxMintAmount = 2;
  uint256 public nftPerAddressLimit = 2;
  bool public paused = true;
  bool public revealed = false;
  bool public onlyWhitelisted = true;
  address[] public whitelistedAddresses;
  mapping(address => uint256) public addressMintedBalance;

  //PROVENANCE

  string public PROVENANCE = "";
  uint256 public startingIndexBlock;
  uint256 public startingIndex;

  // NAMES
  event Named(uint256 indexed index, string name);
  mapping(uint256 => string) private _tokenNames;
  mapping(string => bool) private _namesUsed;

  constructor(
    string memory _name,
    string memory _symbol,
    string memory _initBaseURI,
    string memory _initNotRevealedUri
  ) ERC721(_name, _symbol) {
    setBaseURI(_initBaseURI);
    setNotRevealedURI(_initNotRevealedUri);
  }

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

  // public
  function mint(uint256 _mintAmount) public payable {
    require(!paused, "the contract is paused");
    uint256 supply = totalSupply();
    require(_mintAmount > 0, "need to mint at least 1 NFT");
    require(_mintAmount <= maxMintAmount, "max mint amount per session exceeded");
    require(supply + _mintAmount <= maxSupply, "max NFT limit exceeded");

    if (msg.sender != owner()) {
        if(onlyWhitelisted == true) {
            require(isWhitelisted(msg.sender), "user is not whitelisted");
            uint256 ownerMintedCount = addressMintedBalance[msg.sender];
            require(ownerMintedCount + _mintAmount <= nftPerAddressLimit, "max NFT per address exceeded");
        }
        require(msg.value >= cost * _mintAmount, "insufficient funds");
    }

    for (uint256 i = 1; i <= _mintAmount; i++) {
      addressMintedBalance[msg.sender]++;
      _safeMint(msg.sender, supply + i);
    }


    // If we haven't set the starting index and this is either 1) the last saleable token or 2) the contract is not revealed for the first time
    if (startingIndexBlock == 0 && (totalSupply() == maxSupply)) {
            startingIndexBlock = block.number;
    }
  }

  function isWhitelisted(address _user) public view returns (bool) {
    for (uint i = 0; i < whitelistedAddresses.length; i++) {
      if (whitelistedAddresses[i] == _user) {
          return true;
      }
    }
    return false;
  }

  function walletOfOwner(address _owner)
    public
    view
    returns (uint256[] memory)
  {
    uint256 ownerTokenCount = balanceOf(_owner);
    uint256[] memory tokenIds = new uint256[](ownerTokenCount);
    for (uint256 i; i < ownerTokenCount; i++) {
      tokenIds[i] = tokenOfOwnerByIndex(_owner, i);
    }
    return tokenIds;
  }

  function tokenURI(uint256 tokenId)
    public
    view
    virtual
    override
    returns (string memory)
  {
    require(
      _exists(tokenId),
      "ERC721Metadata: URI query for nonexistent token"
    );

    if(revealed == false) {
        return notRevealedUri;
    }

    string memory currentBaseURI = _baseURI();
    return bytes(currentBaseURI).length > 0
        ? string(abi.encodePacked(currentBaseURI, tokenId.toString(), baseExtension))
        : "";
  }

    /**
     * Set the starting index for the collection
     */
    function setStartingIndex() public {
        require(startingIndex == 0, "Starting index is already set");
        require(startingIndexBlock != 0, "Starting index block must be set");

        startingIndex = uint(blockhash(startingIndexBlock)) % maxSupply;
        // 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)) % maxSupply;
        }
        // Prevent default sequence
        if (startingIndex == 0) {
            startingIndex = startingIndex + 1;
        }
    }

    /**
     * Set the starting index block for the collection, essentially unblocking
     * setting starting index
     */
    function emergencySetStartingIndexBlock() public onlyOwner {
        require(startingIndex == 0, "Starting index is already set");

        startingIndexBlock = block.number;
    }

/*
    -------------------------------------
    NAMING
    -------------------------------------
    */
    function setName(uint256 tokenId, string memory name) public {
        require(revealed, "Collection hasnt been revealed yet!");
        address owner = ownerOf(tokenId);
        require(msg.sender == owner, "Address is not the owner of NFT!");
        require(validateName(name) == true, "Name is not valid!");
        require(isNameUsed(name) == false, "This name is already being used.");
        if (bytes(_tokenNames[tokenId]).length > 0) {
            _namesUsed[toLower(_tokenNames[tokenId])] = false;
        }
        _namesUsed[toLower(name)] = true;
        _tokenNames[tokenId] = name;
        emit Named(tokenId, name);
    }

    function tokenNameByIndex(uint256 index)
        public
        view
        returns (string memory)
    {
        return _tokenNames[index];
    }

    function isNameUsed(string memory nameString) public view returns (bool) {
        return _namesUsed[toLower(nameString)];
    }

    function validateName(string memory str) public pure returns (bool) {
        bytes memory b = bytes(str);
        if (b.length < 3) return false;
        if (b.length > 32) return false;
        if (b[0] == 0x20) return false;
        if (b[b.length - 1] == 0x20) return false;
        bytes1 lastChar = b[0];
        for (uint256 i; i < b.length; i++) {
            bytes1 char = b[i];
            if (char == 0x20 && lastChar == 0x20) return false;
            if (
                !(char >= 0x41 && char <= 0x5A) &&
                !(char >= 0x61 && char <= 0x7A) &&
                !(char == 0x20)
            ) return false;
            lastChar = char;
        }
        return true;
    }

    function toLower(string memory str) public pure returns (string memory) {
        bytes memory bStr = bytes(str);
        bytes memory bLower = new bytes(bStr.length);
        for (uint256 i = 0; i < bStr.length; i++) {
            if ((uint8(bStr[i]) >= 65) && (uint8(bStr[i]) <= 90)) {
                bLower[i] = bytes1(uint8(bStr[i]) + 32);
            } else {
                bLower[i] = bStr[i];
            }
        }
        return string(bLower);
    }


    /*
    -------------------------------------
    ONLY OWNER
    -------------------------------------
    */
  function reveal() public onlyOwner {
      revealed = true;
  }

      /*
    * Set provenance once it's calculated
    */
  function setProvenanceHash(string memory provenanceHash) public onlyOwner {
    PROVENANCE = provenanceHash;
  }

  function setNftPerAddressLimit(uint256 _limit) public onlyOwner {
    nftPerAddressLimit = _limit;
  }

  function setCost(uint256 _newCost) public onlyOwner {
    cost = _newCost;
  }

  function setmaxMintAmount(uint256 _newmaxMintAmount) public onlyOwner {
    maxMintAmount = _newmaxMintAmount;
  }

  function setBaseURI(string memory _newBaseURI) public onlyOwner {
    baseURI = _newBaseURI;
  }

  function setBaseExtension(string memory _newBaseExtension) public onlyOwner {
    baseExtension = _newBaseExtension;
  }

  function setNotRevealedURI(string memory _notRevealedURI) public onlyOwner {
    notRevealedUri = _notRevealedURI;
  }

  function pause(bool _state) public onlyOwner {
    paused = _state;
  }

  function setOnlyWhitelisted(bool _state) public onlyOwner {
    onlyWhitelisted = _state;
  }

  function whitelistUsers(address[] calldata _users) public onlyOwner {
    delete whitelistedAddresses;
    whitelistedAddresses = _users;
  }

  function withdraw() public payable onlyOwner {
    // This will pay devs % of the initial sale.
    // =============================================================================
    uint256 addressOneAmt = address(this).balance * 15 / 100;
    uint256 addressTwoAmt = address(this).balance * 5 / 100;

    // =============================================================================
    (bool hs, ) = payable(0x14AB4457bfA3AA1057fdee9687c60A108e6F5aC1).call{value: addressOneAmt}("");
    require(hs);
    // =============================================================================

    (bool fs, ) = payable(0x30909DA8B6ae3afA42fEf31AaCd0c6b5F12E898C).call{value: addressTwoAmt}("");
    require(fs);

    // This will payout the SYNCRONAUT DAO GNOSIS SAFE the remainder of the contract balance.
    // =============================================================================
    (bool os, ) = payable(0x375b2E5ffc8bC5db38e8F2B3CdECF8341F58bf2B).call{value: address(this).balance}("");
    require(os);
    // =============================================================================
  }
}
function reveal() public onlyOwner {
    revealed = true;
}

/*
 -------------------------------------
 ONLY OWNER
 -------------------------------------
 */