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

    uint256 public constant COINRUNNERS_MAX = 15000;
    uint256 public constant COINRUNNERS_PER_MINT = 1000;
    uint256 public COINRUNNERS_PRICE = 0.0777 ether;

    mapping(address => bool) public presalerList;
    mapping(string => bool) private _usedNonces;
    
    string private _contractURI;
    string private _tokenBaseURI = "https://coinrunnersnft.herokuapp.com/api/metadata/";
    address private _splitter = 0x45be6626d2Dc02193B8401192b08F178e6766e68;
    address private _signer = 0x8Cc1764cAbDE44437445aa87055f98F11aFD8942;

    string public proof;
    uint256 public presaleAmountMinted;
    uint256 public publicAmountMinted;
    bool public presaleLive;
    bool public saleLive;
    bool public locked;
    bool public priceLocked;
    
    constructor() ERC721("COINRUNNERSNFT", "COINRUNNERS") { }
    
    modifier notLocked {
        require(!locked, "Contract metadata methods are locked");
        _;
    }

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

    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;
        }
    }
    
    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 _signer == hash.recover(signature);
    }
    
    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], "NONCE_USED");
        require(hashTransaction(msg.sender, tokenQuantity, nonce) == hash, "HASH_FAIL");
        require(totalSupply() < COINRUNNERS_MAX, "OUT_OF_STOCK");
        require(presaleAmountMinted + publicAmountMinted + tokenQuantity <= COINRUNNERS_MAX, "EXCEED_PUBLIC");
        require(tokenQuantity <= COINRUNNERS_PER_MINT, "EXCEED_COINRUNNERS_PER_MINT");
        require(COINRUNNERS_PRICE * tokenQuantity <= msg.value, "INSUFFICIENT_ETH");
        
        for(uint256 i = 0; i < tokenQuantity; i++) {
            publicAmountMinted++;
            _safeMint(msg.sender, totalSupply() + 1);
        }
        
        _usedNonces[nonce] = true;
    }

    function presaleBuy(bytes32 hash, bytes memory signature, string memory nonce, uint256 tokenQuantity) external payable {
        require(!saleLive && presaleLive, "PRESALE_CLOSED");
        require(matchAddresSigner(hash, signature), "DIRECT_MINT_DISALLOWED");
        require(!_usedNonces[nonce], "NONCE_USED");
        require(hashTransaction(msg.sender, tokenQuantity, nonce) == hash, "HASH_FAIL");
        require(presalerList[msg.sender], "NOT_QUALIFIED");
        require(totalSupply() < COINRUNNERS_MAX, "OUT_OF_STOCK");
        require(presaleAmountMinted + tokenQuantity <= COINRUNNERS_MAX, "EXCEED_PRIVATE");
        require(tokenQuantity <= COINRUNNERS_PER_MINT, "EXCEED_COINRUNNERS_PER_MINT");
        require(COINRUNNERS_PRICE * tokenQuantity <= msg.value, "INSUFFICIENT_ETH");
        
        for (uint256 i = 0; i < tokenQuantity; i++) {
            presaleAmountMinted++;
            _safeMint(msg.sender, totalSupply() + 1);
        }
    }
    
    function withdraw() external onlyOwner {
        payable(_splitter).transfer(address(this).balance);
    }

    // Owner functions for enabling presale, sale, revealing and setting the provenance hash
    function lockMetadata() external onlyOwner {
        locked = true;
    }

    function setPrice(uint256 price) external onlyOwner {
        require(!priceLocked, "PRICE_LOCKED");
        COINRUNNERS_PRICE = price;
    }

    function togglePresale() external onlyOwner {
        if (!priceLocked) priceLocked = true;
        presaleLive = !presaleLive;
    }
    
    function toggleSale() external onlyOwner {
        if (!priceLocked) priceLocked = true;
        saleLive = !saleLive;
    }
    
    function setSigner(address addr) external onlyOwner {
        _signer = addr;
    }
    
    function setPaymentSplitter(address splitter) external onlyOwner {
        _splitter = splitter;
    }
    
    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()));
    }
}
/*
 
 COINRUNNERS NFT
 
 */
function lockMetadata() external onlyOwner {
    locked = true;
}

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