contract SparkleMuffinzERC721 is ERC721Enumerable, Ownable, ReentrancyGuard {
    using Address for address;

    event Mint(address indexed owner, uint256 indexed tokenId);

    uint256 internal _tokenIds;
    uint256 internal _reserved;
    string internal _baseTokenURI;

    uint256 public constant MAX_SUPPLY = 8888;
    uint256 public constant MAX_RESERVED = 30;

    uint256 public mintPrice = 0.1 ether;
    string public unrevealedTokenURI;

    bytes32 public merkleRoot = "";
    mapping(address => uint256) public whitelistRemaining;
    mapping(address => bool) public whitelistUsed;

    bool public revealed;
    bool public presaleActive;
    bool public saleActive;
    mapping(address => uint256) public presaleMints;

    constructor(string memory URI, bytes32 root) ERC721("SparkleMuffinz", "SPMZ") {
        unrevealedTokenURI = URI;
        merkleRoot = root;
    }

    // VIEW FUNCTIONS

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

    // ADMIN FUNCTIONALITY

    function setBaseTokenURI(string memory URI) public onlyOwner {
        _baseTokenURI = URI;
    }

    function setUnrevealedTokenURI(string memory URI) public onlyOwner {
        unrevealedTokenURI = URI;
    }

    function reveal(string memory URI) public onlyOwner {
        require(!revealed, "NFTs already revealed");
        revealed = true;
        _baseTokenURI = URI;
    }

    function setMintPrice(uint256 _mintPrice) external onlyOwner {
        mintPrice = _mintPrice;
    }

    function setMerkleRoot(bytes32 _merkleRoot) public onlyOwner {
        merkleRoot = _merkleRoot;
    }

    function flipPresaleStatus() public onlyOwner {
        presaleActive = !presaleActive;
    }

    function flipSaleStatus() public onlyOwner {
        saleActive = !saleActive;
    }

    function reserve(uint256 amount, address to) public onlyOwner {
        require(_reserved + amount <= MAX_RESERVED, "Exceeds maximum number of reserved tokens");

        _mintWithoutValidation(to, amount);
        _reserved += amount;
    }

    // MINTING FUNCTIONS

    // verifies a merkle proof for whitelist checking
    function verify(
        bytes32 root,
        bytes32 leaf,
        bytes32[] memory proof
    ) public pure returns (bool) {
        return MerkleProof.verify(proof, root, leaf);
    }

    // function to mint based off of whitelist allocation
    function presaleMint(
        uint256 amount,
        uint256 totalAllocation,
        bytes32 leaf,
        bytes32[] memory proof
    ) external payable {
        require(presaleActive, "Presale not active");

        // Create storage element tracking user mints if this is the first mint for them
        if (!whitelistUsed[msg.sender]) {
            // Verify that (msg.sender, amount) correspond to Merkle leaf
            require(
                keccak256(abi.encodePacked(msg.sender, totalAllocation)) == leaf,
                "Sender and amount don't match Merkle leaf"
            );

            // Verify that (leaf, proof) matches the Merkle root
            require(verify(merkleRoot, leaf, proof), "Not a valid leaf in the Merkle tree");

            whitelistUsed[msg.sender] = true;
            whitelistRemaining[msg.sender] = totalAllocation;
        }

        require(amount > 0, "Invalid mint amount");
        require(msg.value >= amount * mintPrice, "Not enough ETH sent");
        require(whitelistRemaining[msg.sender] >= amount, "Can't mint more than remaining allocation");

        whitelistRemaining[msg.sender] -= amount;
        _mintWithoutValidation(msg.sender, amount);
    }

    // function to mint in public sale
    function publicMint(uint256 amount) external payable {
        require(saleActive, "Public sale not active");
        require(amount > 0, "Invalid mint amount");
        require(msg.value >= amount * mintPrice, "Not enough ETH sent");

        _mintWithoutValidation(msg.sender, amount);
    }

    // internal function for minting without checks
    function _mintWithoutValidation(address to, uint256 amount) internal {
        require(_tokenIds + amount < MAX_SUPPLY, "Exceeds maximum number of tokens");

        for (uint256 i = 0; i < amount; i++) {
            _mint(to, _tokenIds);
            emit Mint(to, _tokenIds);
            _tokenIds += 1;
        }
    }

    /**
     * @dev Returns the wallet of a given wallet. Mainly for ease for frontend devs.
     * @param wallet The wallet to get the tokens of.
     */
    function walletOfOwner(address wallet) public view returns (uint256[] memory) {
        uint256 tokenCount = balanceOf(wallet);

        uint256[] memory tokenIds = new uint256[](tokenCount);
        for (uint256 i; i < tokenCount; i++) {
            tokenIds[i] = tokenOfOwnerByIndex(wallet, i);
        }
        return tokenIds;
    }

    // WITHDRAWAL FUNCTIONALITY

    function withdraw() public onlyOwner nonReentrant {
        Address.sendValue(payable(owner()), address(this).balance);
    }
}
function _mintWithoutValidation(address to, uint256 amount) internal {
    require(_tokenIds + amount < MAX_SUPPLY, "Exceeds maximum number of tokens");

    for (uint256 i = 0; i < amount; i++) {
        _mint(to, _tokenIds);
        emit Mint(to, _tokenIds);
        _tokenIds += 1;
    }
}

// internal function for minting without checks