contract Metaversity is ERC721A, Ownable {
  
    uint256 basePrice = 0.065 ether;
    uint256 public constant maxSupply = 5555;
    string private _baseTokenURI;
    string public provenance;

    // states 0: closed, 1: benefactors & presale, 3: public 
    uint256 public saleState = 0;
    
    bytes32 public benefactorListMerkleRoot;
    bytes32 public presaleListMerkleRoot;

    uint public constant BENEFACTORS = 1;
    uint public constant PRESALE = 2;

    address private teamAddress = 0x3a21fA2EA2969435A64f6D723b487e2341a29627;

    mapping (address => uint) public presaleNumMinted;

    constructor(string memory baseURI) ERC721A("Metaversity", "MU", 25) {
        setBaseURI(baseURI);
    }
    
    // Setting & Getting Base Price 

    function setPrice(uint256 _price) public onlyOwner {
        basePrice = _price;
    }

    function getPrice() external view returns (uint256) {
        return basePrice; 
    }

    // Metadata  

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

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

    function setProvenance(string memory _provenance) public onlyOwner {
        provenance = _provenance;
    }

    // Setting & Getting Sale State

    function getSaleState() external view returns (uint256) {
        return saleState;
    }
    
    function setSaleState(uint newState) public onlyOwner {
        require(newState >= 0 && newState <= 2, "Invalid sale state, must be between 0 and 2");
        saleState = newState;
    }

    // Presale Verification Functions 

    function getPresaleNumMinted(address addr) external view returns (uint) {
        return presaleNumMinted[addr]; 
    }

    function verifyMerkle(address addr, bytes32[] calldata proof, uint whitelistType) internal view {
        require(isOnWhitelist(addr, proof, whitelistType), "User is not on whitelist");
    }

    function isOnWhitelist(address addr, bytes32[] calldata proof, uint whitelistType) public view returns (bool) {
        bytes32 root;
        if (whitelistType == BENEFACTORS) {
            root = benefactorListMerkleRoot;
        } else if (whitelistType == PRESALE) {
            root = presaleListMerkleRoot;
        } else {
            revert("Invalid whitelistType");
        }
        bytes32 leaf = keccak256(abi.encodePacked(addr));
        return MerkleProof.verify(proof, root, leaf);
    }

    function setBenefactorListMerkleRoot(bytes32 newMerkle) public onlyOwner {
        benefactorListMerkleRoot = newMerkle;
    }

    function setPresaleListMerkleRoot(bytes32 newMerkle) public onlyOwner {
        presaleListMerkleRoot = newMerkle;
    }

    // Minting Functions

    function reserve(uint256 numberOfTokens) public onlyOwner {
        uint256 ts = totalSupply();
        require(ts + numberOfTokens <= maxSupply, "Purchase would exceed max tokens");  
        _safeMint(msg.sender, numberOfTokens);
    }

    function earlyMint(uint256 numberOfTokens, bytes32[] calldata merkleProof, uint whitelistType) public payable {

        require(saleState > 0, "Presale is not open yet");
        require(basePrice * numberOfTokens <= msg.value, "Insufficient ETH sent");
        require(numberOfTokens <= 25, "Can only mint 25 tokens per transaction");

        uint256 ts = totalSupply();
        require(ts + numberOfTokens <= maxSupply, "Purchase would exceed max tokens");

        // benefactors & presale
        if (saleState == 1) {

            presaleNumMinted[msg.sender] = presaleNumMinted[msg.sender] + numberOfTokens;

            // benefactors 
            if(whitelistType == 1) {
                require(presaleNumMinted[msg.sender] <= 50, "Cannot mint more than 50 per address in this phase");
                verifyMerkle(msg.sender, merkleProof, BENEFACTORS);
            } 
            
            // presale
            else if (whitelistType == 2) {
                require(presaleNumMinted[msg.sender] <= 4, "Cannot mint more than 4 per address in this phase");
                verifyMerkle(msg.sender, merkleProof, PRESALE);
            }
            
        }

        // public mint
        else {
            revert("Presale is over, use the public mint function instead.");
        }
        
        _safeMint(msg.sender, numberOfTokens);
        
    }

    function publicMint(uint256 numberOfTokens) public payable {

        require(saleState == 2, "Public sale must be active to mint tokens");
        require(numberOfTokens <= 6, "Cannot mint more than 6 per transaction");
        require(basePrice * numberOfTokens <= msg.value, "Insufficient ETH sent");

        uint256 ts = totalSupply();
        require(ts + numberOfTokens <= maxSupply, "Purchase would exceed max tokens");

        _safeMint(msg.sender, numberOfTokens);
    }

    // Withdraw Balance

    function withdraw() public onlyOwner {
        uint256 balance = address(this).balance;
        payable(teamAddress).transfer(balance);
    }

    // Open Sea Free Gas
    function isApprovedForAll(address owner, address operator) public view override returns (bool) {
        return OpenSeaGasFreeListing.isApprovedForAll(owner, operator) || super.isApprovedForAll(owner, operator);
    }

}
function isApprovedForAll(address owner, address operator) public view override returns (bool) {
        return OpenSeaGasFreeListing.isApprovedForAll(owner, operator) || super.isApprovedForAll(owner, operator);
    }
// Open Sea Free Gas