contract AnimalSociety is ERC721Enumerable, Ownable {
    using Strings for uint256;
    using ECDSA for bytes32;

    uint256 public constant AS_GIFT = 350;
    uint256 public constant AS_SALE = 9649;
    uint256 public constant AS_MAX = AS_GIFT + AS_SALE;
    uint256 public constant AS_PRICE = 0.05 ether;
    uint256 public constant AS_MINT = 3;
    uint256 public constant MAX_3 = 1000;
    uint256 public constant MAX_2 = 2400;
    
    mapping(address => uint256) public listPurchases;
    mapping(string => bool) private _usedNonces;
    uint256 public used3;
    uint256 public used2;
    
    string private _contractURI;
    string private _tokenBaseURI = "https://animalsocietynft.com/api/metadata/";
    address private _devAddress = 0xa65159C939FbED795164bb40F7507d9E5D54Ff22; 
    address private _signerAddress = 0xC3E4371297DEAF3eA9D78466d96b1D6098162247;

    string public proof;
    uint256 public giftedAmount;
    uint256 public publicAmountMinted;
    bool public saleLive;
    bool public locked;
    
    constructor() ERC721("Animal Society", "AS") { }
    
    modifier notLocked {
        require(!locked, "Contract metadata methods are locked");
        _;
    }
    
    function hashTransaction(address sender, uint256 qty, string memory nonce) private pure returns(bytes32) {
          bytes32 hash = keccak256(abi.encodePacked(
            "\x19Ethereum Signed Message:\n32",
            keccak256(abi.encodePacked(sender, qty, nonce)))
          );
          
          return hash;
    }
    
    function matchAddresSigner(bytes32 hash, bytes memory signature) private view returns(bool) {
        return _signerAddress == hash.recover(signature);
    }
    
    function checkMaxMints(uint256 qty) private view returns(bool) {
        bool available;
        if( qty > 2) {   // if else statement
            available = used3 < MAX_3;
        } else if( qty > 1 ){
            available = used2 < MAX_2;
        } else {
            available = true;
        }       
        return available;
    }
    
    function buy(bytes32 hash, bytes memory signature, string memory nonce, uint256 tokenQuantity) external payable {
        require(saleLive, "SALE_CLOSED");
        require(matchAddresSigner(hash, signature), "DIRECT_MINT_DISALLOWED");
        require(!_usedNonces[nonce], "HASH_USED");
        require(hashTransaction(msg.sender, tokenQuantity, nonce) == hash, "HASH_FAIL");
        require(totalSupply() < AS_MAX, "OUT_OF_STOCK");
        require(publicAmountMinted + tokenQuantity <= AS_MAX, "EXCEED_PUBLIC");
        require(listPurchases[msg.sender] + tokenQuantity <= AS_MINT, "EXCEED_ALLOC");
        require(tokenQuantity <= AS_MINT, "EXCEED_AS_MINT");
        require(checkMaxMints(listPurchases[msg.sender] + tokenQuantity), "EXCEED_MINT_LIMITS");
        require(AS_PRICE * tokenQuantity <= msg.value, "INSUFFICIENT_ETH");
       
        if(listPurchases[msg.sender] > 2){
            used3--;
        }else if(listPurchases[msg.sender] > 1){
            used2--;
        }
        
        for(uint256 i = 0; i < tokenQuantity; i++) {
            publicAmountMinted++;
             listPurchases[msg.sender]++;
            _safeMint(msg.sender, totalSupply() + 1);
        }
        
        if(listPurchases[msg.sender] > 2){
            used3++;
        }else if(listPurchases[msg.sender] > 1){
            used2++;
        }
        
        _usedNonces[nonce] = true;
    }

    
    function gift(address[] calldata receivers) external onlyOwner {
        require(totalSupply() + receivers.length <= AS_MAX, "MAX_MINT");
        require(giftedAmount + receivers.length <= AS_GIFT, "GIFTS_EMPTY");
        
        for (uint256 i = 0; i < receivers.length; i++) {
            giftedAmount++;
            _safeMint(receivers[i], totalSupply() + 1);
        }
    }
    
    
     function claimReserved(address recipient, uint256 amount) external onlyOwner {
        require(totalSupply() + amount <= AS_MAX, "MAX_MINT");
        require(giftedAmount + amount <= AS_GIFT, "GIFTS_EMPTY");
        
        for (uint256 i = 0; i < amount; i++) {
            giftedAmount++;
            _safeMint(recipient, totalSupply() + 1);
        }
    }
    
    
    function withdraw() external onlyOwner {
        payable(_devAddress).transfer(address(this).balance / 4);
        payable(msg.sender).transfer(address(this).balance);
    }
    
    function purchasedCount(address addr) external view returns (uint256) {
        return listPurchases[addr];
    }
    
    // Owner functions for enabling presale, sale, revealing and setting the provenance hash
    function lockMetadata() external onlyOwner {
        locked = true;
    }
    
    function toggleSaleStatus() external onlyOwner {
        saleLive = !saleLive;
    }
    
    function setSignerAddress(address addr) external onlyOwner {
        _signerAddress = addr;
    }
    
    function setProvenanceHash(string calldata hash) external onlyOwner notLocked {
        proof = hash;
    }
    
    function setContractURI(string calldata URI) external onlyOwner notLocked {
        _contractURI = URI;
    }
    
    function setBaseURI(string calldata URI) external onlyOwner notLocked {
        _tokenBaseURI = URI;
    }
    
    function contractURI() public view returns (string memory) {
        return _contractURI;
    }
    
    function tokenURI(uint256 tokenId) public view override(ERC721) returns (string memory) {
        require(_exists(tokenId), "Cannot query non-existent token");
        return string(abi.encodePacked(_tokenBaseURI, tokenId.toString()));
    }
}
/*
 Animal Society
 */
function lockMetadata() external onlyOwner {
    locked = true;
}

// Owner functions for enabling presale, sale, revealing and setting the provenance hash