contract nft is ERC721, Ownable {
    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;
    
    //initial part of the URI for the metadata
    string private _currentBaseURI;
        
    //cost of mints depending on state of sale    
    uint private mintCost_ = 0.15 ether;
    
    //maximum amount of mints per wallet
    uint public maxMint;
    
    //the amount of reserved mints that have currently been executed by creator and by marketing wallet
    uint private _reservedMints = 0;
    
    //the maximum amount of reserved mints allowed for creator and marketing wallet
    uint private maxReservedMints = 250;
    
    //dummy address that we use to sign the mint transaction to make sure it is valid
    address private dummy = 0x80E4929c869102140E69550BBECC20bEd61B080c;

    //developers wallet address
    address payable private dev = payable(0x318cBF186eB13C74533943b054959867eE44eFFE);

    //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() ERC721('The Companion', 'TC'){
        numberOfTotalTokens = 0;
        
        maxTotalTokens = 8888;
        maxMint = 10;

        unrevealedURI = "ipfs://QmeiF5rT5mR8tQA9gHxcYHGoEUev7UnTgB3TWRRGkuE2Ca/";
    }
    
    //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 presaleMint(uint256 number, uint8 _v, bytes32 _r, bytes32 _s) onlyValidAccess(_v,  _r, _s) public payable {
        State saleState_ = saleState();
        require(saleState_ != State.NoSale, "Sale in not open yet!");
        require(saleState_ != State.PublicSale, "Presale has closed, Check out Public Sale!");
        require(numberOfTotalTokens + number <= maxTotalTokens - (maxReservedMints  - _reservedMints), "Not enough NFTs left to mint..");
        require(mintsPerAddress[msg.sender] + number <= maxMint, "Maximum 10 Mints per Address allowed!");
        require(msg.value >= mintCost() * number, "Not sufficient Ether to mint this amount of NFTs (Cost = 0.15 ether each NFT)");
        
        for (uint256 i = 0; i < number; i++) {
            uint256 tid = tokenId();
            _safeMint(msg.sender, tid);
            mintsPerAddress[msg.sender] += 1;
            numberOfTotalTokens += 1;
        }

    }

    //mint a @param number of NFTs in public sale
    function publicSaleMint(uint256 number) public payable {
        State saleState_ = saleState();
        require(saleState_ == State.PublicSale, "Public Sale in not open yet!");
        require(numberOfTotalTokens + number <= maxTotalTokens - (maxReservedMints - _reservedMints), "Not enough NFTs left to mint..");
        require(mintsPerAddress[msg.sender] + number <= maxMint, "Maximum 10 Mints per Address allowed!");
        require(msg.value >= mintCost() * number, "Not sufficient Ether to mint this amount of NFTs (Cost = 0.15 ether for each NFT)");


        for (uint256 i = 0; i < number; i++) {
            uint256 tid = tokenId();
            _safeMint(msg.sender, tid);
            mintsPerAddress[msg.sender] += 1;
            numberOfTotalTokens += 1;
        }
    }
    
    //reserved NFTs for creator
    function reservedMints(uint256 number, address recipient) public onlyOwner {
        require(_reservedMints + number <= maxReservedMints, "Not enough Reserved NFTs for Creator left to mint..");
        for (uint256 i = 0; i < number; i++) {
            uint256 tid = tokenId();
            _safeMint(recipient, tid);
            mintsPerAddress[recipient] += 1;
            numberOfTotalTokens += 1;
        }
    }

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

    
    //burn the tokens that have not been sold yet
    function burnTokens() public onlyOwner {
        maxTotalTokens = numberOfTotalTokens;
    }
    
    //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;
    }
    
    //see the total amount of tokens that have been minted
    function totalSupply() public view returns(uint) {
        return numberOfTotalTokens;
    }
    
    //see the total amount of reserved mints that are left creator
    function reservedMintsLeft() public onlyOwner view returns(uint) {
        return maxReservedMints - _reservedMints;
    }
    
    //get the funds from the minting of the NFTs
    function retrieveFunds() public onlyOwner {
        uint256 balance = accountBalance();
        require(balance > 0, "No funds to retrieve!");

        _withdraw(dev, (balance * 5)/1000);
        _withdraw(payable(msg.sender), accountBalance()); //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() public view returns(uint256) {
        return mintCost_;
    }

    //change the current price to mint
    function changeMintCost(uint newCost) public onlyOwner {
        mintCost_ = newCost;
    }

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

}
function publicSaleMint(uint256 number) public payable {
    State saleState_ = saleState();
    require(saleState_ == State.PublicSale, "Public Sale in not open yet!");
    require(numberOfTotalTokens + number <= maxTotalTokens - (maxReservedMints - _reservedMints), "Not enough NFTs left to mint..");
    require(mintsPerAddress[msg.sender] + number <= maxMint, "Maximum 10 Mints per Address allowed!");
    require(msg.value >= mintCost() * number, "Not sufficient Ether to mint this amount of NFTs (Cost = 0.15 ether for each NFT)");


    for (uint256 i = 0; i < number; i++) {
        uint256 tid = tokenId();
        _safeMint(msg.sender, tid);
        mintsPerAddress[msg.sender] += 1;
        numberOfTotalTokens += 1;
    }
}

//mint a @param number of NFTs in public sale