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

        // Constant variables
        // ------------------------------------------------------------------------
        uint public constant publicSupply = 5217;
        uint public constant presaleSupply = 2560;
        uint public constant maxSupply = presaleSupply + publicSupply;
        uint public constant cost = 0.14 ether;
        uint public constant maxMintPerTx = 5;
        
        // Mapping variables
        // ------------------------------------------------------------------------
        mapping(address => bool) public presalerList;
        mapping(address => uint256) public presalerListPurchases;
        mapping(address => uint256) public publicListPurchases;
        
        // Public variables
        // ------------------------------------------------------------------------
        uint256 public publicAmountMinted;
        uint256 public presalePurchaseLimit = 1;
        uint256 public publicPurchaseLimit = 5;
        uint256 public reservedAmountMinted;

        // URI variables
        // ------------------------------------------------------------------------
        string private contractURI;
        string private baseTokenURI = "";

        // Deployer address
        // ------------------------------------------------------------------------
        address public constant creatorAddress = 0x8707db291A13d9585b9Bc8e2289F64EbB6f5B672;
        address private signerAddress = 0x8707db291A13d9585b9Bc8e2289F64EbB6f5B672; // tbd

        bool public saleLive;
        bool public presaleLive;
        bool public revealed = false;
        bool public metadataLock;

        // Constructor
        // ------------------------------------------------------------------------
        constructor(string memory unrevealedBaseURI) ERC721("NekoZamurai", "NEKO") {
            setBaseURI(unrevealedBaseURI);
            // setBaseURI("ipfs://QmXTsae772LDETthjkCNR4pPrEWShJ9UbuP7gCKWi26XV6/"); // testing purposes
        }
        
        modifier notLocked {
            require(!metadataLock, "Contract metadata methods are locked");
            _;
        }

        // Mint function
        // ------------------------------------------------------------------------
        function mintPublic(uint256 mintQuantity) external payable {
            require(saleLive, "Sale closed");
            require(!presaleLive, "Only available for presale");
            require(totalSupply() < maxSupply, "Out of stock");
            require(publicAmountMinted + mintQuantity <= publicSupply, "Out of public stock");
            require(publicListPurchases[msg.sender] + mintQuantity <= publicPurchaseLimit, "Public mint limit exceeded");
            require(mintQuantity <= maxMintPerTx, "Exceeded mint limit");
            require(cost * mintQuantity <= msg.value, "Insufficient funds"); 
            
            for(uint256 i = 1; i <= mintQuantity; i++) {
                publicAmountMinted++;
                publicListPurchases[msg.sender]++;
                _safeMint(msg.sender, totalSupply() + 1);
            }
        }

        // Presale mint function
        // ------------------------------------------------------------------------
        function mintPresale(uint256 mintQuantity) external payable {
            uint256 callSupply = totalSupply();

            require(!saleLive && presaleLive, "Presale closed");
            require(presalerList[msg.sender], "Not on presale list");   
            require(callSupply < maxSupply, "Out of stock");
            require(publicAmountMinted + mintQuantity <= publicSupply, "Out of public stock");
            require(presalerListPurchases[msg.sender] + mintQuantity <= presalePurchaseLimit, "Presale mint limit exceeded");
            require(cost * mintQuantity <= msg.value, "Insufficient funds");
            
            presalerListPurchases[msg.sender] += mintQuantity;
            publicAmountMinted += mintQuantity;

            for (uint256 i = 0; i < mintQuantity; i++) {
                _safeMint(msg.sender, callSupply + 1);
            }
        }
        

        // Reserved mint function
        // ------------------------------------------------------------------------
        function mintReserved(uint256 mintQuantity) external onlyOwner {
            require(saleLive, "Sale closed");
            require(totalSupply() < maxSupply, "Out of stock");
            require(reservedAmountMinted + mintQuantity <= presaleSupply, "Out of reserved stock");
            
            for(uint256 i = 1; i <= mintQuantity; i++) {
                reservedAmountMinted++;
                _safeMint(msg.sender, totalSupply() + 1);
            }
            
        }
        
        // Get cost amount
        // ------------------------------------------------------------------------
        function getCost(uint256 _count) public pure returns (uint256) {
            return cost.mul(_count);
        }
        
        // Get hash transaction
        // ------------------------------------------------------------------------
        function hashTransaction(address sender, uint256 qty, string memory nonce) public pure returns(bytes32) {
            bytes32 hash = keccak256(abi.encodePacked(
            "\x19Ethereum Signed Message:\n32",
            keccak256(abi.encodePacked(sender, qty, nonce)))
            );
            
            return hash;
        }
        
        // Get match from address signer
        // ------------------------------------------------------------------------
        function matchAddressSigner(bytes32 hash, bytes memory signature) private view returns(bool) {
            return signerAddress == hash.recover(signature);
        }

        //  Get total supply leftover amount
        // ------------------------------------------------------------------------
        function getUnsoldRemaining() external view returns (uint256) {
            return maxSupply - totalSupply();
        }
        
        // Get contract URI
        // ------------------------------------------------------------------------
        function getContractURI() public view returns (string memory) {
            return contractURI;
        }
        
        // Get base token URI
        // ------------------------------------------------------------------------
        function tokenURI(uint256 tokenId) public view virtual override returns (string memory) {
            require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");

            if (!revealed) {
                return baseTokenURI;
            }
            
            return bytes(baseTokenURI).length > 0 ? string(abi.encodePacked(baseTokenURI, tokenId.toString(), ".json")): "";
        }
        
        // Owner functions
        // Toggle reveal function (True by default)
        // ------------------------------------------------------------------------
        function reveal() public onlyOwner {
            revealed = true;
        }
        
        // Togle lock metadata function (True by default)
        // ------------------------------------------------------------------------
        function lockMetadata() external onlyOwner {
            metadataLock = true;
        }
        
        // Toggle presale (False by default)
        // ------------------------------------------------------------------------
        function togglePresale() external onlyOwner {
            presaleLive = !presaleLive;
        }
        
        // Toggle sale (False by default)
        // ------------------------------------------------------------------------
        function toggleSale() external onlyOwner {
            saleLive = !saleLive;
        }
        
        // Add address to presael list
        // ------------------------------------------------------------------------
        function addToPresaleList(address[] calldata entries) external onlyOwner {
            for(uint256 i = 0; i < entries.length; i++) {
                address entry = entries[i];
                require(entry != address(0), "Null address");
                require(!presalerList[entry], "Duplicate entry");

                presalerList[entry] = true;
            }   
        }

        // Remove address from presale list
        // ------------------------------------------------------------------------
        function removeFromPresaleList(address[] calldata entries) external onlyOwner {
            for(uint256 i = 0; i < entries.length; i++) {
                address entry = entries[i];
                require(entry != address(0), "Null address");
                
                presalerList[entry] = false;
            }
        }

        // Approve signer address
        // ------------------------------------------------------------------------
        function setSignerAddress(address addr) external onlyOwner {
            signerAddress = addr;
        }
        
        // Set contract URI
        // ------------------------------------------------------------------------
        function setContractURI(string calldata URI) external onlyOwner notLocked {
            contractURI = URI;
        }
        
        // Set base URI
        // ------------------------------------------------------------------------
        function setBaseURI(string memory baseURI) public onlyOwner notLocked {
            baseTokenURI = baseURI;
        }
        
        // Withdrawal function
        // ------------------------------------------------------------------------
        function withdraw() external onlyOwner {
            uint256 balance = address(this).balance;
            require(balance > 0);
            (bool success, ) = payable(msg.sender).call{value: address(this).balance}("");
            require(success);
        }
        
    }
/*
 ╔═╗ ╔╗╔═══╗╔╗╔═╗╔═══╗    ╔════╗╔═══╗╔═╗╔═╗╔╗ ╔╗╔═══╗╔═══╗╔══╗
 ║║╚╗║║║╔══╝║║║╔╝║╔═╗║    ╚══╗ ║║╔═╗║║║╚╝║║║║ ║║║╔═╗║║╔═╗║╚╣╠╝
 ║╔╗╚╝║║╚══╗║╚╝╝ ║║ ║║      ╔╝╔╝║║ ║║║╔╗╔╗║║║ ║║║╚═╝║║║ ║║ ║║ 
 ║║╚╗║║║╔══╝║╔╗║ ║║ ║║     ╔╝╔╝ ║╚═╝║║║║║║║║║ ║║║╔╗╔╝║╚═╝║ ║║ 
 ║║ ║║║║╚══╗║║║╚╗║╚═╝║    ╔╝ ╚═╗║╔═╗║║║║║║║║╚═╝║║║║╚╗║╔═╗║╔╣╠╗
 ╚╝ ╚═╝╚═══╝╚╝╚═╝╚═══╝    ╚════╝╚╝ ╚╝╚╝╚╝╚╝╚═══╝╚╝╚═╝╚╝ ╚╝╚══╝
 
 Contract made with ❤️ by NekoZamurai
 
 Socials:
 Opensea: TBA
 Twitter: https://twitter.com/NekoZamuraiNFT
 Website: https://nekozamurai.io/
 */
function togglePresale() external onlyOwner {
    presaleLive = !presaleLive;
}

// Toggle presale (False by default)
// ------------------------------------------------------------------------