contract LeprechaunLegends is ERC721, ERC721URIStorage, Ownable {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    uint16 public constant maxTotalSupply = 9512;
    uint256 public price = 0.059 ether;

    address public treasuryAddress = 0xB70e29e721716b635c58DC03bA0Dd4553524FB58;

    string public baseURI = "ipfs://QmWJJqm8PDCPFjuN3rqvqw9E4ZQ4DkXcFvn8wghAKdNXgB/";

    uint8 public constant maxGifts = 80;
    uint256 public giftedAmount;

    mapping(address => bool) public goldList;
    mapping(address => uint256) public goldListPurchases;
    uint256 public goldListSaleTime = 1675227600; // 1st Feb 2023 00:00:00-0500 EST
    uint8 public constant goldListPurchaseLimit = 3;

    mapping(address => bool) public whiteList;
    mapping(address => uint256) public whiteListPurchases;
    uint256 public whiteListSaleTime = 1675314000; // 2nd Feb 2023 00:00:00-0500 EST
    uint8 public constant whiteListPurchaseLimit = 2;

    mapping(address => uint256) public publicSalePurchases;
    uint256 public publicSaleTime = 1675400400; // 3rd Feb 2023 00:00:00-0500 EST
    uint8 public constant publicSalePurchaseLimit = 10;
    bool public enforcePublicSalePurchaseLimit = true;

    constructor() ERC721("LeprechaunLegends", "LL") {}

    function count() public view returns (uint256) {
        return _tokenIdCounter.current();
    }

    function mint(address addr, uint256 qty) internal {
        require(msg.value >= price * qty, "Insufficient funds for the amount of NFTs.");
        require(count() + qty + (maxGifts - giftedAmount) <= maxTotalSupply, "You cannot mint more than the maximum total supply.");

        for (uint256 i = 0; i < qty; i++) {
            safeMint(addr);
        }
    }

    function goldListMint(uint256 qty) public payable {
        require(block.timestamp > goldListSaleTime, "The gold list sale is not active.");
        require(goldList[msg.sender], "Address is not in the gold list.");
        require(goldListPurchases[msg.sender] + qty <= goldListPurchaseLimit, "You can only mint a maximum of 3 NFT's in the gold list.");

        mint(msg.sender, qty);
        goldListPurchases[msg.sender] += qty;
    }

    function whiteListMint(uint256 qty) public payable {
        require(block.timestamp > whiteListSaleTime, "The white list sale is not active.");
        require(whiteList[msg.sender], "Address is not in the white list.");
        require(whiteListPurchases[msg.sender] + qty <= whiteListPurchaseLimit, "You can only mint a maximum of 2 NFT's in the white list.");

        mint(msg.sender, qty);
        whiteListPurchases[msg.sender] += qty;
    }

    function publicSaleMint(uint256 qty) public payable {
        require(block.timestamp > publicSaleTime, "The public sale is not active.");
        require(!enforcePublicSalePurchaseLimit || publicSalePurchases[msg.sender] + qty <= publicSalePurchaseLimit, "You can only mint a maximum of 10 NFT's in the public sale.");

        mint(msg.sender, qty);
        publicSalePurchases[msg.sender] += qty;
    }

    function gift(address[] calldata receivers) external onlyOwner {
        require(count() + receivers.length <= maxTotalSupply, "You cannot mint more than the maximum total supply.");
        require(giftedAmount + receivers.length <= maxGifts, "You cannot exceed the maximum number of gifts.");

        for (uint256 i = 0; i < receivers.length; i++) {
            giftedAmount++;
            safeMint(receivers[i]);
        }
    }

    function safeMint(address to) internal {
        uint256 tokenId = _tokenIdCounter.current();
        _tokenIdCounter.increment();
        _safeMint(to, tokenId);
    }

    function setEnforcePublicSalePurchaseLimit(bool enforced) external onlyOwner {
        enforcePublicSalePurchaseLimit = enforced;
    }

    function setBaseURI(string memory newBaseURI) external onlyOwner {
        baseURI = newBaseURI;
    }

    function setPrice(uint256 newPrice) external onlyOwner {
        price = newPrice;
    }

    function setWhiteListSaleTime(uint256 newTime) external onlyOwner {
        whiteListSaleTime = newTime;
    }

    function setGoldListSaleTime(uint256 newTime) external onlyOwner {
        goldListSaleTime = newTime;
    }

    function setPublicSaleTime(uint256 newTime) external onlyOwner {
        publicSaleTime = newTime;
    }

    function setTreasuryAddress(address payable newAddress) external onlyOwner {
        treasuryAddress = newAddress;
    }
    
    function addToGoldList(address[] calldata entries) external onlyOwner {
        for(uint256 i = 0; i < entries.length; i++) {
            address entry = entries[i];
            require(entry != address(0), "Address cannot be empty.");
            require(!goldList[entry], "Address is already in the gold list.");

            goldList[entry] = true;
        }
    }

    function removeFromGoldList(address[] calldata entries) external onlyOwner {
        for(uint256 i = 0; i < entries.length; i++) {
            address entry = entries[i];
            require(entry != address(0), "Address cannot be empty.");
            
            goldList[entry] = false;
        }
    }
    
    function addToWhiteList(address[] calldata entries) external onlyOwner {
        for(uint256 i = 0; i < entries.length; i++) {
            address entry = entries[i];
            require(entry != address(0), "Address cannot be empty.");
            require(!whiteList[entry], "Address is already in the white list.");

            whiteList[entry] = true;
        }
    }

    function removeFromWhiteList(address[] calldata entries) external onlyOwner {
        for(uint256 i = 0; i < entries.length; i++) {
            address entry = entries[i];
            require(entry != address(0), "Address cannot be empty.");

            whiteList[entry] = false;
        }
    }

    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        payable(treasuryAddress).transfer(balance);
    }

    // The following functions are overrides required by Solidity.

    function _baseURI() internal view override returns (string memory) {
        return baseURI;
    }

    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }
}
function _baseURI() internal view override returns (string memory) {
    return baseURI;
}

// The following functions are overrides required by Solidity.