contract nft is ERC721Enumerable, Ownable {
    //so tokenID can be changed to string
    using Strings for uint256;
    
    //declares the maximum amount of tokens that can be minted, total and in presale
    uint256 private maxTotalTokens;
    
    //initial part of the URI for the metadata
    string private _currentBaseURI;
        
    //cost of mints depending on state of sale    
    uint private mintCost_ = 0.2 ether;
    
    //the amount of reserved mints that have currently been executed by owner
    uint private reservedMints = 0;
    //the maximum amount of reserved mints allowed for owner
    uint private maxReservedMints = 5;
    
    //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;
    
    //maximum amount of mints allowed for each wallet
    uint private maxMints = 3;
    
    //current state os sale
    enum State {NoSale, OpenSale}

    //fixed unix times for different sales
    uint256 private saleLaunchTime;

    //variable to see if whitelist is active or not
    bool private whitelist;
    
    //declaring initial values for variables
    constructor() ERC721('Assholios', 'ASS') {
        //the max value tokens to mint
        maxTotalTokens = 70;

        //initially the whitelist will be off
        whitelist = false;

        _currentBaseURI = 'ipfs://QmTPHW4cTJMStAKjFQRDgXeHfaa3gtkTeyRuXYFmtmUwJL/';


    }
    
    //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_;
    }

    //in case we want to add more NFTs to the collection
    function addMoreTokens(uint number) public onlyOwner {
        maxTotalTokens += number;
    }
    
    modifier onlyValidAccess(uint8 _v, bytes32 _r, bytes32 _s) {
        require(whitelist == true, 'Whitelist is currently deactivated!');
        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 mint(uint tokenId_) public payable {
        require(saleState() != State.NoSale, "Sale in not open yet!");
        require(tokenId_ <= maxTotalTokens, 'Inexistent Token Id!');
        require(whitelist == false, 'Whitelist is currently active!');
        require(totalSupply() < maxTotalTokens - (maxReservedMints - reservedMints), "Not enough NFTs left to mint..");
        require(msg.value >= mintCost(), 'Insufficient Funds to mint this number of NFTs!');
        require(mintsPerAddress[msg.sender] < maxMints, 'Not allowed to mint this number of NFTs, max amount per address is 3!');
        require(!_exists(tokenId_), 'This Token has already been minted!');

        _safeMint(msg.sender, tokenId_);
        mintsPerAddress[msg.sender] += 1;         

    }

    function wlMint(uint tokenId_, uint8 _v, bytes32 _r, bytes32 _s) onlyValidAccess(_v,  _r, _s) public payable {
        require(saleState() != State.NoSale, "Sale in not open yet!");
        require(tokenId_ <= maxTotalTokens, 'Inexistent Token Id!');
        require(totalSupply() < maxTotalTokens - (maxReservedMints - reservedMints), "Not enough NFTs left to mint..");
        require(msg.value >= mintCost(), 'Insufficient Funds to mint this number of NFTs!');
        require(mintsPerAddress[msg.sender] < maxMints, 'Not allowed to mint this number of NFTs, max amount per address is 3!');
        require(!_exists(tokenId_), 'This Token has already been minted!');
        
        _safeMint(msg.sender, tokenId_);
        mintsPerAddress[msg.sender] += 1;          

    }

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

        string memory baseURI = _baseURI();
        return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId_.toString(), '.json')) : "";
            
    }
    
    //reserved NFTs for creator
    function reservedMintCreator(uint tokenId_) public onlyOwner {
        require(reservedMints < maxReservedMints, "Not enough Reserved NFTs for Creator left to mint..");
        require(!_exists(tokenId_), 'This Token has already been minted!');
        
        _safeMint(msg.sender, tokenId_);
        mintsPerAddress[msg.sender] += 1;
        
    }

    //to start the sale of the NFTs
    function openSale() public onlyOwner {
        require(saleState() == State.NoSale, 'Sale is already Open!');
        //times for launches and reveal
        saleLaunchTime = block.timestamp; 
    }

    //burn the tokens that have not been sold yet
    function burnTokens() public onlyOwner {
        maxTotalTokens = totalSupply();
    }
    
    //se the current account balance
    function accountBalance() public onlyOwner view returns(uint) {
        return address(this).balance;
    }
    
    //get funds recieved from NFT minting
    function withdraw() public onlyOwner {
        uint256 balance = accountBalance();
        require(balance > 0, 'No Funds to withdraw, Balance is 0');
        (bool sent, ) = payable(msg.sender).call{value: balance}("");
        require(sent, "Failed to send Ether");
    }
    
    //change the dummy account used for signing transactions
    function changeDummy(address _dummy) public onlyOwner {
        dummy = _dummy;
    }
    
    //see the total amount of reserved mints that have left
    function reservedMintsLeft() public onlyOwner view returns(uint) {
        return maxReservedMints - reservedMints;
    }

    //see the current cost for minting
    function mintCost() public view returns(uint) {
        return mintCost_;
    }

    //change the price to mint
    function changeMintCost(uint256 _mintCost) public onlyOwner {
        mintCost_ = _mintCost;
    }

    //seee the current state of the sale
    function saleState() public view returns(State) {
        if (saleLaunchTime == 0) {
            return State.NoSale;
        }
        else {
            return State.OpenSale;
        }
        
    }

    //change the current state of whitelist
    function toggleWhitelist() public onlyOwner {
        whitelist = !whitelist;
    }

    //see if the whitelist is active
    function whitelistIsActive() public view returns(bool) {
        return whitelist;
    }

    //see the total amount of nfts that are available to mint
    function totalNFTs() public view returns(uint) {
        return maxTotalTokens;
    }
   
    //see if a specific tokenId has been minted
    function isMinted(uint tokenId_) public view returns(bool) {
        if (_exists(tokenId_)) {
            return true;
        }
        else {
            return false;
        }
    }
}
function openSale() public onlyOwner {
    require(saleState() == State.NoSale, 'Sale is already Open!');
    //times for launches and reveal
    saleLaunchTime = block.timestamp; 
}

//to start the sale of the NFTs