contract MetaPantherClub is ERC721A, Ownable, ReentrancyGuard {
    using Strings for uint256;

    //amount of tokens that have been minted so far, in total and in presale
    uint256 private numberOfTotalTokens;
    
    //declares the maximum amount of tokens that can be minted, total and in presale
    uint256 private maxTotalTokens = 7000;
    uint256 private maxTokensPresale = 1000;

    //reservedmints for the team
    uint256 private _reservedMints;
    uint256 private maxReservedMints = 500;
    
    //initial part of the URI for the metadata
    string private _currentBaseURI;
        
    //cost of mints depending on state of sale    
    uint private mintCostWhitelist = 0.15 ether;
    uint private mintCostSale = 0.2 ether;
    
    //maximum amount of mints per wallet
    uint private maxMintPresale = 4;
    
    //dummy address that we use to sign the mint transaction to make sure it is valid
    address private dummy = 0x80E4929c869102140E69550BBECC20bEd61B080c;

    //amount of mints that each address has executed
    mapping(address => uint256) public mintsPerAddress;
    
    //current state os sale
    enum State {NoSale, Presale, PublicSale}

    //defines the uri for when the NFTs have not been yet revealed
    string public unrevealedURI;
    
    //marks the timestamp of when the respective sales open
    uint256 public presaleLaunchTime;
    uint256 public publicSaleLaunchTime;
    uint256 public revealTime;
    
    //declaring initial values for variables
    constructor() ERC721A("Meta Panther Club", "MPC"){
        
        unrevealedURI = "ipfs://QmdVAw21dtCMSsYQFAf8cMwXqZ7o7TKhLepGf6mKvQX1sP/";
    }
    
    //in case somebody accidentaly sends funds or transaction to contract
    receive() payable external {}
    fallback() payable external {
        revert();
    }
    
    //visualize baseURI
    function _baseURI() internal view virtual override returns (string memory) {
        return _currentBaseURI;
    }
    
    //change baseURI in case needed for IPFS
    function changeBaseURI(string memory baseURI_) public onlyOwner {
        _currentBaseURI = baseURI_;
    }
    
    function changeUnrevealedURI(string memory unrevealedURI_) public onlyOwner {
        unrevealedURI = unrevealedURI_;
    }
    
    //gets the tokenID of NFT to be minted
    /*function tokenId() internal view returns(uint256) {
        return numberOfTotalTokens + 1;
    }*/
    
    modifier onlyValidAccess(uint8 _v, bytes32 _r, bytes32 _s) {
        require( isValidAccessMessage(msg.sender,_v,_r,_s), 'Invalid Signature' );
        _;
    }
 
    /* 
    * @dev Verifies if message was signed by owner to give access to _add for this contract.
    *      Assumes Geth signature prefix.
    * @param _add Address of agent with access
    * @param _v ECDSA signature parameter v.
    * @param _r ECDSA signature parameters r.
    * @param _s ECDSA signature parameters s.
    * @return Validity of access message for a given address.
    */
    function isValidAccessMessage(address _add, uint8 _v, bytes32 _r, bytes32 _s) view public returns (bool) {
        bytes32 hash = keccak256(abi.encodePacked(address(this), _add));
        return dummy == ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)), _v, _r, _s);
    }

    //mint a @param number of NFTs in presale
    function whitelistMint(uint256 number, uint8 _v, bytes32 _r, bytes32 _s) onlyValidAccess(_v,  _r, _s) public payable nonReentrant {
        State saleState_ = saleState();
        require(saleState_ != State.NoSale, "Sale in not open yet!");
        //require(msg.sender == tx.origin, "No transaction from smart contracts!");
        require(saleState_ != State.PublicSale, "Presale has closed, Check out Public Sale!");
        require(totalSupply() + number <= maxTokensPresale, "Not enough NFTs left to mint..");
        require(mintsPerAddress[msg.sender] + number <= maxMintPresale, "Maximum 4 Mints per Address allowed!");
        require(msg.value >= mintCost(1) * number, "Not sufficient Ether to mint this amount of NFTs (Cost = 0.15 ether each NFT)");
        
        _safeMint(msg.sender, number);
        mintsPerAddress[msg.sender] += number;

    }
    
    //mint a @param number of NFTs in presale
    function presaleMint(uint256 number) public payable nonReentrant {
        State saleState_ = saleState();
        require(saleState_ != State.NoSale, "Sale in not open yet!");
        //require(msg.sender == tx.origin, "No transaction from smart contracts!");
        require(saleState_ != State.PublicSale, "Presale has closed, Check out Public Sale!");
        require(totalSupply() + number <= maxTokensPresale, "Not enough NFTs left to mint..");
        require(mintsPerAddress[msg.sender] + number <= maxMintPresale, "Maximum 4 Mints per Address allowed!");
        require(msg.value >= mintCost(2) * number, "Not sufficient Ether to mint this amount of NFTs (Cost = 0.2 ether each NFT)");
        
        _safeMint(msg.sender, number);
        mintsPerAddress[msg.sender] += number;

    }

    //mint a @param number of NFTs in public sale
    function publicSaleMint(uint256 number) public payable nonReentrant {
        State saleState_ = saleState();
        require(saleState_ != State.NoSale, "Sale in not open yet!");
        //require(msg.sender == tx.origin, "No transaction from smart contracts!");
        require(saleState_ != State.Presale, "Presale has closed, Check out Public Sale!");
        require(totalSupply() + number <= maxTotalTokens, "Not enough NFTs left to mint..");
        require(msg.value >= mintCost(2) * number, "Not sufficient Ether to mint this amount of NFTs (Cost = 0.2 ether each NFT)");
        
        _safeMint(msg.sender, number);
        mintsPerAddress[msg.sender] += number;
    }
    
    //reserved NFTs for creator
    function reservedMints(uint256 number, address recipient) public onlyOwner {
        require(_reservedMints + number <= maxReservedMints, "Not enough NFTs left to mint..");
        _safeMint(recipient, number);
        mintsPerAddress[recipient] += number;
    }

    function tokenURI(uint256 tokenId_) public view virtual override returns (string memory) {
        require(_exists(tokenId_), "ERC721Metadata: URI query for nonexistent token");
        
        //check to see that 24 hours have passed since beginning of publicsale launch
        if (revealTime == 0) {
            return unrevealedURI;
        }
        
        else {
            string memory baseURI = _baseURI();
            return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId_.toString(), '.json')) : "";
        }    
    }
    
    //begins the minting of the NFTs
    function switchToPresale() public onlyOwner{
        State saleState_ = saleState();
        require(saleState_ == State.NoSale, "Presale has already opened!");
        presaleLaunchTime = block.timestamp;
    }
    
    //begins the public sale
    function switchToPublicSale() public onlyOwner {
        State saleState_ = saleState();
        require(saleState_ != State.PublicSale, "Public Sale is already live!");
        require(saleState_ != State.NoSale, "Cannot change to Public Sale if there has not been a Presale!");
        
        publicSaleLaunchTime = block.timestamp;
    }
    
    //se the current account balance
    function accountBalance() public onlyOwner view returns(uint) {
        return address(this).balance;
    }
    
    //change the dummy account used for signing transactions
    function changeDummy(address _dummy) public onlyOwner {
        dummy = _dummy;
    }
    
    //to see the total amount of reserved mints left 
    function reservedMintsLeft() public onlyOwner view returns(uint) {
        return maxReservedMints - _reservedMints;
    }
    
    //get the funds from the minting of the NFTs
    function withdraw() public onlyOwner nonReentrant{
        uint256 balance = accountBalance();
        require(balance > 0, "No funds to retrieve!");

        _withdraw(payable(0x90ED73fF39F26b53854230060cC067cB92C18a4a), balance); //to avoid dust eth
        
    }

    function _withdraw(address payable account, uint256 amount) internal {
        (bool sent, ) = account.call{value: amount}("");
        require(sent, "Failed to send Ether");
    }

    //see the current state of the sale
    function saleState() public view returns(State){
        if (presaleLaunchTime == 0) {
            return State.NoSale;
        }
        else if (publicSaleLaunchTime == 0) {
            return State.Presale;
        }
        else {
            return State.PublicSale;
        }
    }

    //get the current price to mint
    function mintCost(uint type_) public view returns(uint256) {
        require(type_ > 0 && type_ < 3, "Invalid Type!");
        if (type_ == 1) {
            return mintCostWhitelist;
        }
        else {
            return mintCostSale;
        }
    }

    //reveal the NFTs
    function reveal() public onlyOwner {
        revealTime = block.timestamp;
    }

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

//visualize baseURI