contract FANDAS is ERC721A, Ownable, ReentrancyGuard {

    using Address for address;
    using Strings for uint256;
    using MerkleProof for bytes32[];

    string public baseURI;
    string public baseExtension = ".json";

    uint256 public presaleMintPrice = 0.04 ether;
    uint256 public publicMintPrice = 0.05 ether;
    uint256 public maxMintPerWallet = 3;
    uint256 public maxMintPerTx = 3;


    bool public whitelistOpen = true;

    bytes32 whitelistMerkleRoot = 0xaccf7d34fbd84c5cbcb9dc624ce046254c6c77b603b2de1ce6c3376632db7c4c;

    mapping(address => uint256) public MintedAmountWhitelist;
    mapping(address => uint256) public MintedAmountPublic;


    // ===== Constructor =====
    constructor() ERC721A("Frenly Pandas", "FANDAS", maxMintPerWallet, 10000) {
        
    }

    function changePublicPrice(uint256 _price) public onlyOwner
    {
        publicMintPrice = _price;
    }

    function devMint(uint256 amount) public onlyOwner
    {
        _safeMint(msg.sender, amount);
    }

    // ===== Modifier =====
    function _onlySender() private view {
        require(msg.sender == tx.origin);
    }

    modifier onlySender {
        _onlySender();
        _;
    }

    modifier whitelistIsOpen {
        require(whitelistOpen == true);
        _;
    }

    modifier whitelistIsClosed {
        require(whitelistOpen == false);
        _;
    }

    function flipSale() public onlyOwner
    {
        whitelistOpen = !whitelistOpen;
    }

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

    function whitelistMint(bytes32[] calldata _merkleProof, uint256 amount) public payable nonReentrant onlySender whitelistIsOpen
    {      
        require(msg.value >= presaleMintPrice, "Minting a Panda Costs 0.05 Ether Each!");
        
        bytes32 leaf = keccak256(abi.encodePacked(msg.sender));
        require(MerkleProof.verify(_merkleProof, whitelistMerkleRoot, leaf), "Invalid proof.");

        require(
            amount <= maxMintPerTx,
            "Minting amount exceeds allowance per tx"
        );

        require(
            MintedAmountWhitelist[msg.sender] + amount <= maxMintPerWallet,
            "Minting amount exceeds allowance per wallet"
        );
        MintedAmountWhitelist[msg.sender] += amount;

        require((totalSupply() + amount) <= collectionSize, "Sold out!");
        _safeMint(msg.sender, amount);
        
    }


    function publicMint(uint256 amount) public payable nonReentrant onlySender whitelistIsClosed
    {      
        require(msg.value >= publicMintPrice, "Minting a Panda Costs 0.06 Ether Each!");
        
        require(
            amount <= maxMintPerTx,
            "Minting amount exceeds allowance per tx"
        );
        
        require(
            MintedAmountPublic[msg.sender] + amount <= maxMintPerWallet,
            "Minting amount exceeds allowance per wallet"
        );


        MintedAmountPublic[msg.sender] += amount;

        require((totalSupply() + amount) <= collectionSize, "Sold out!");
        _safeMint(msg.sender, amount);
        
    }

    function _withdraw(address payable address_, uint256 amount_) internal {
        (bool success, ) = payable(address_).call{value: amount_}("");
        require(success, "Transfer failed");
    }

    function withdrawEther() external onlyOwner {
        _withdraw(payable(msg.sender), address(this).balance);
    }

    function withdrawEtherTo(address payable to_) external onlyOwner {
        _withdraw(to_, address(this).balance);
    }
    
    function setBaseURI(string memory _newBaseURI) public onlyOwner {
        baseURI = _newBaseURI;
    }
    function _baseURI() internal view virtual override returns (string memory) {
        return baseURI;
    }   
    function tokenURI(uint256 tokenId) public view virtual override returns (string memory)
    {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

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

    function walletOfOwner(address address_) public virtual view returns (uint256[] memory) {
        uint256 _balance = balanceOf(address_);
        uint256[] memory _tokens = new uint256[] (_balance);
        uint256 _index;
        uint256 _loopThrough = totalSupply();
        for (uint256 i = 0; i < _loopThrough; i++) {
            bool _exists = _exists(i);
            if (_exists) {
                if (ownerOf(i) == address_) { _tokens[_index] = i; _index++; }
            }
            else if (!_exists && _tokens[_balance - 1] == 0) { _loopThrough++; }
        }
        return _tokens;
    }
}
function _onlySender() private view {
    require(msg.sender == tx.origin);
}

// ===== Modifier =====