contract Blacklist is ERC721A, Ownable {
    using Strings for uint;
    using ECDSA for bytes32;

    constructor() ERC721A("Blacklist", "BL") {}
    
    uint256 public constant maxSupply = 8888;
    uint256 public totalMints = 0;
    uint256 devReserve = 80;
    uint256 maxMintPerCall = 3;
    uint256 mintPrice = 0.1 ether;
    uint256 devMinted = 0;

    uint256 presaleMaxMint = 8568;

    struct mintTracker {
        uint256 amount;
    }

    bool public blacklistedSaleStatus = false;
    bool public publicSaleStatus = false;

    string private _baseTokenURI;

    address private signer = 0xad4cE29E1443ff40C72984e800a55c93C89abE21;

    mapping(address => mintTracker) private _minttracker;
    mapping(address => uint) devMintsTo;

    modifier mintChecks(uint numNFTs) {
        unchecked {
            require(tx.origin == msg.sender);
            require(numNFTs <= maxMintPerCall, "Number to mint exceeds maximum per tx");
            require(numNFTs + totalSupply() <= maxSupply - devReserve + devMinted, "Mint would exceed supply");
            require(msg.value >= mintPrice * numNFTs, "Incorrect ether value");
        }
        _;
    }

    // Minting

    function publicMint(uint256 numNFTs) public payable mintChecks(numNFTs) {
        require(publicSaleStatus, "Sale has not started");

        _safeMint(msg.sender, numNFTs);
    }

    function whitelistMint(uint256 numNFTs, bytes calldata signature) public payable mintChecks(numNFTs) {
        unchecked {
            require(blacklistedSaleStatus, "Sale has not started");
            require(verifySignature(abi.encodePacked(msg.sender), signature), "Invalid signature provided");
            require(_numberMinted(msg.sender) + numNFTs - devMintsTo[msg.sender] <= presaleMaxMint, "Can't mint that many in pre-sale");
            require(_minttracker[msg.sender].amount + numNFTs <= 2, "Can't mint more than 2 NFTs during PRE-SALE.");
        }
        _minttracker[msg.sender].amount = _minttracker[msg.sender].amount + numNFTs;
        _safeMint(msg.sender, numNFTs);
    }

    // View only

    function tokenURI(uint tokenId) public view override returns(string memory) {
        require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
        return string(abi.encodePacked(_baseTokenURI, tokenId.toString(), ".json"));
    }

    function getMintedAmountForAddress(address _address) public view returns(uint256){
        return _minttracker[_address].amount;
    }

    function getMintedAmountForMyself() public view returns(uint256){
        return _minttracker[msg.sender].amount;
    }

    // ERC-721 stuff

    function numberMinted(address owner) public view returns (uint256) {
        return _numberMinted(owner);
    }

    function getOwnershipData(uint256 tokenId) external view returns (TokenOwnership memory) {
        return ownershipOf(tokenId);
    }

    // Internal

    function verifySignature(bytes memory data, bytes calldata signature) internal view returns(bool) {
        return (keccak256(data).toEthSignedMessageHash().recover(signature) == signer);
    }

    // Owner Only

    function devMint(address to, uint256 numNFTs) external onlyOwner {
        require((devMinted += numNFTs) <= devReserve, "Not enough in the dev reserve, mint a smaller amount");
        devMintsTo[msg.sender] += numNFTs;
        _safeMint(to, numNFTs);
    }

    function setSaleStates(bool new_publicSaleStatus, bool new_blacklistedSaleStatus) external onlyOwner{
        publicSaleStatus = new_publicSaleStatus;
        blacklistedSaleStatus = new_blacklistedSaleStatus;
    }

    function setSigner(address new_signer) external onlyOwner {
        signer = new_signer;
    }

    function setMintPrice(uint256 new_mintPrice) external onlyOwner {
        mintPrice = new_mintPrice;
    }

    function setDevReserve(uint256 new_devReserve) external onlyOwner {
        devReserve = new_devReserve;
    }

    function setBaseURI(string calldata new_baseURI) external onlyOwner {
        _baseTokenURI = new_baseURI;
    }

    function setMaxMint(uint256 new_presaleMaxMint) public onlyOwner {
        presaleMaxMint = new_presaleMaxMint;
    }

    function getSigner() public view returns(address){
        return signer;
    }

    function getMintPrice() public view returns(uint256){
        return mintPrice;
    }

    function getDevMinted() public view returns(uint256){
        return devMinted;
    }

    function getPresaleMaxMint() public view returns(uint256){
        return presaleMaxMint;
    }

    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        payable(0xd7C6B6e70dA164132d3E73E894Dfca3E58756eA0).transfer(balance);
    }
}
function tokenURI(uint tokenId) public view override returns(string memory) {
    require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
    return string(abi.encodePacked(_baseTokenURI, tokenId.toString(), ".json"));
}

// View only