contract FatCatz is ERC721Enumerable, Ownable {
    using Strings for uint256;

    // Constant variables
    // ------------------------------------------------------------------------
    uint256 public constant TOTAL_SUPPLY = 6969; // Total amount of Fat Catz
    uint256 public constant RESERVED_SUPPLY = 100; // Amount of Fat Catz reserved for the contract (last 100)
    uint256 public constant MAX_SUPPLY = TOTAL_SUPPLY - RESERVED_SUPPLY; // Maximum amount of Fat Catz

    uint256 public constant MAX_PER_TX = 8; // Max amount of Fat Catz per tx (public sale)
    uint256 public constant MAX_PER_WALLET_PUBLIC = 40; // Max amount of Fat Catz per wallet during public sale.

    address private _d1 = 0x59f1AFBD895eFF95d0D61824C16287597AF2D0E7;
    address private _d2 = 0x04d4C99b291b997903cA084099C133258eb17f57;

    // State variables
    // ------------------------------------------------------------------------
    uint256 public publicPrice = 0.069 ether; // Full price
    uint256 public presalePriceOptionA = 0.043 ether; // 30% off 1
    uint256 public presalePriceOptionB = presalePriceOptionA + publicPrice; // 30% off 1st + full price on 2nd
    uint256 public supporterPriceOptionA = 0.043 ether; // 30% off
    uint256 public supporterPriceOptionB = 0.05865 ether; // 15% off

    // Used to toggle the active states of all the sales
    bool public isPresaleActive = false; 
    bool public isPublicSaleActive = false;

    bool public isTeamReserveMinted = false; // True when team reserves the final 100 Fat Catz

    // Presale arrays
    // ------------------------------------------------------------------------

    // Used to keep track of wallets' eligibility for presale
    mapping(address => bool) private _presaleEligibleList;

    // Used to keep track of how many NFTs a wallet has minted on presale
    mapping(address => uint256) private _presaleMintedCountList;

    // Used to keep track of wallets' eligibility for supporter sale
    mapping(address => bool) private _supporterEligibleList;
    
    // Used to keep track of how many NFTs a supporter has minted on these options
    mapping(address => uint256) private _supporterOptionAMintedCountList;
    mapping(address => uint256) private _supporterOptionBMintedCountList; 

    // Used to keep track of how many NFTs a wallet has minted on public sale
    mapping(address => uint256) private _publicMintedCountList;

    // URI variables
    // ------------------------------------------------------------------------
    string private _contractURI;
    string private _baseTokenURI;

    // Events
    // ------------------------------------------------------------------------
    event BaseTokenURIChanged(string baseTokenURI);
    event ContractURIChanged(string contractURI);

    // Constructor
    // ------------------------------------------------------------------------
    constructor() ERC721("Fat Catz", "FC") {}

    // Modifiers
    // ------------------------------------------------------------------------
    modifier onlyPresale() {
        require(isPresaleActive, "PRESALE_NOT_ACTIVE");
        _;
    }

    modifier onlyPublicSale() {
        require(isPublicSaleActive, "PUBLIC_SALE_NOT_ACTIVE");
        _;
    }

    // Presale functions
    // ------------------------------------------------------------------------
    function addToPresaleList(address[] calldata addresses) external onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++) {
            require(addresses[i] != address(0), "NULL_ADDRESS");
            require(!_presaleEligibleList[addresses[i]], "DUPLICATE_ENTRY");

            _presaleEligibleList[addresses[i]] = true;
            _presaleMintedCountList[addresses[i]] = 0;
        }
    }

    function removeFromPresaleList(address[] calldata addresses)
        external
        onlyOwner
    {
        for (uint256 i = 0; i < addresses.length; i++) {
            require(addresses[i] != address(0), "NULL_ADDRESS");
            require(_presaleEligibleList[addresses[i]], "NOT_IN_PRESALE");

            _presaleEligibleList[addresses[i]] = false;
        }
    }

    function isEligibleForPresale(address addr) external view returns (bool) {
        require(addr != address(0), "NULL_ADDRESS");

        return _presaleEligibleList[addr];
    }


    function hasMintedPresale(address addr) external view returns (bool) {
        require(addr != address(0), "NULL_ADDRESS");

        return _presaleMintedCountList[addr] >= 1;
    }

    function togglePresaleStatus() external onlyOwner {
        isPresaleActive = !isPresaleActive;
    }

    // Supporter Functions
    // ------------------------------------------------------------------------
    function addToSupporterList(address[] calldata addresses) external onlyOwner {
        for (uint256 i = 0; i < addresses.length; i++) {
            require(addresses[i] != address(0), "NULL_ADDRESS");
            require(!_supporterEligibleList[addresses[i]], "DUPLICATE_ENTRY");

            _supporterEligibleList[addresses[i]] = true;
            _supporterOptionAMintedCountList[addresses[i]] = 0;
            _supporterOptionBMintedCountList[addresses[i]] = 0;
        }
    }

    function removeFromSupporterList(address[] calldata addresses)
        external
        onlyOwner
    {
        for (uint256 i = 0; i < addresses.length; i++) {
            require(addresses[i] != address(0), "NULL_ADDRESS");
            require(_supporterEligibleList[addresses[i]], "NOT_IN_SUPPORTER");

            _supporterEligibleList[addresses[i]] = false;
        }
    }

    function isEligibleForSupporter(address addr) external view returns (bool) {
        require(addr != address(0), "NULL_ADDRESS");

        return _supporterEligibleList[addr];
    }

    function hasMintedSupporterOptionA(address addr) external view returns (bool) {
        require(addr != address(0), "NULL_ADDRESS");

        return _supporterOptionAMintedCountList[addr] >= 2;
    }

    function hasMintedSupporterOptionB(address addr) external view returns (bool) {
        require(addr != address(0), "NULL_ADDRESS");

        return _supporterOptionBMintedCountList[addr] >= 6;
    }

    // Public sale functions
    // ------------------------------------------------------------------------
    function togglePublicSaleStatus() external onlyOwner {
        isPublicSaleActive = !isPublicSaleActive;
    }

    // Mint functions
    // ------------------------------------------------------------------------
    // 100 reserved for the team (only if sellout)
    function mintTeamReserved(address addr)
        external
        onlyOwner
    {
        require(isTeamReserveMinted == false, "RESERVE_ALREADY_MINTED");
        require(totalSupply() < TOTAL_SUPPLY, "SOLD_OUT");
        require(
            totalSupply() + RESERVED_SUPPLY <= TOTAL_SUPPLY,
            "EXCEEDS_TOTAL_SUPPLY"
        );

        for (uint256 i = 0; i < RESERVED_SUPPLY; i++) {
            _safeMint(addr, totalSupply() + 1);
        }

        isTeamReserveMinted = !isTeamReserveMinted;
    }

    // Option A: 1 on presale
    function mintPresaleOptionA() external payable onlyPresale {
        uint256 quantity = 1;

        require(_presaleEligibleList[msg.sender], "NOT_ELIGIBLE");
        require(_presaleMintedCountList[msg.sender] < 1, "ALREADY_MINTED");

        require(totalSupply() < MAX_SUPPLY, "SOLD_OUT");
        require(totalSupply() + quantity <= MAX_SUPPLY, "EXCEEDS_MAX_SUPPLY");
        
        require(presalePriceOptionA == msg.value, "INVALID_ETH_AMOUNT");

        for (uint256 i = 0; i < quantity; i++) {
            _presaleMintedCountList[msg.sender] += 1;
            _safeMint(msg.sender, totalSupply() + 1);
        }
    }

    // Option B: 2 on presale
    function mintPresaleOptionB() external payable onlyPresale {
        uint256 quantity = 2;

        require(_presaleEligibleList[msg.sender], "NOT_ELIGIBLE");
        require(_presaleMintedCountList[msg.sender] < 1, "ALREADY_MINTED");

        require(totalSupply() < MAX_SUPPLY, "SOLD_OUT");
        require(totalSupply() + quantity <= MAX_SUPPLY, "EXCEEDS_MAX_SUPPLY");

        require(presalePriceOptionB == msg.value, "INVALID_ETH_AMOUNT");

        for (uint256 i = 0; i < quantity; i++) {
            _presaleMintedCountList[msg.sender] += 1;
            _safeMint(msg.sender, totalSupply() + 1);
        }
    }

    // Supporter presale (╯✧ ∇ ✧)╯ max 2 at 30% off
    function mintSupporterOptionA(uint256 quantity) external payable onlyPresale {
        require(_supporterEligibleList[msg.sender], "NOT_ELIGIBLE");
        require(_supporterOptionAMintedCountList[msg.sender] < 2, "ALREADY_MINTED");
        require(_supporterOptionAMintedCountList[msg.sender] + quantity <= 2, "EXCEEDS_MAX_SUPPLY");

        require(totalSupply() < MAX_SUPPLY, "SOLD_OUT");
        require(quantity > 0, "QUANTITY_CANNOT_BE_ZERO");
        require(quantity <= 2, "EXCEEDS_MAX_SUPPORTER_OPTION_A");
        require(totalSupply() + quantity <= MAX_SUPPLY, "EXCEEDS_MAX_SUPPLY");
        
        require(supporterPriceOptionA * quantity == msg.value, "INVALID_ETH_AMOUNT");

        for (uint256 i = 0; i < quantity; i++) {
            _supporterOptionAMintedCountList[msg.sender] += 1;
            _safeMint(msg.sender, totalSupply() + 1);
        }
    }

    // Supporter mint (╯✧ ∇ ✧)╯ max 6 at 15% off
    function mintSupporterOptionB(uint256 quantity) external payable onlyPresale {
        require(_supporterEligibleList[msg.sender], "NOT_ELIGIBLE");
        require(_supporterOptionBMintedCountList[msg.sender] < 6, "ALREADY_MINTED");
        require(_supporterOptionBMintedCountList[msg.sender] + quantity <= 6, "EXCEEDS_MAX_SUPPLY");

        require(totalSupply() < MAX_SUPPLY, "SOLD_OUT");
        require(quantity > 0, "QUANTITY_CANNOT_BE_ZERO");
        require(quantity <= 6, "EXCEEDS_MAX_SUPPORTER_OPTION_B");
        require(totalSupply() + quantity <= MAX_SUPPLY, "EXCEEDS_MAX_SUPPLY");

        require(supporterPriceOptionB * quantity == msg.value, "INVALID_ETH_AMOUNT");

        for (uint256 i = 0; i < quantity; i++) {
            _supporterOptionBMintedCountList[msg.sender] += 1;
            _safeMint(msg.sender, totalSupply() + 1);
        }
    }

    // Public mint
    function mintPublic(uint256 quantity) external payable onlyPublicSale {
        require(totalSupply() < MAX_SUPPLY, "SOLD_OUT");
        require(quantity > 0, "QUANTITY_CANNOT_BE_ZERO");
        require(quantity <= MAX_PER_TX, "EXCEEDS_TX_MAX");
        require(totalSupply() + quantity <= MAX_SUPPLY, "EXCEEDS_MAX_SUPPLY");
        require(!isContract(msg.sender), "CONTRACTS_CANNOT_MINT");

        require(
            _publicMintedCountList[msg.sender] + quantity <= MAX_PER_WALLET_PUBLIC,
            "EXCEEDS_MAX_ALLOWANCE"
        );

        require(publicPrice * quantity == msg.value, "INVALID_ETH_AMOUNT");

        for (uint256 i = 0; i < quantity; i++) {
            _publicMintedCountList[msg.sender] += 1;
            _safeMint(msg.sender, totalSupply() + 1);
        }
    }

    // More than 11 Fat Catz NFTs cannot be given away
    function gift(address addr)
        external
        payable
        onlyOwner
    {
        require(totalSupply() < 11, "MAX_GIFTED_REACHED");

        _safeMint(addr, totalSupply() + 1);
    }


    // Base URI Functions
    // ------------------------------------------------------------------------
    function setContractURI(string calldata URI) external onlyOwner {
        _contractURI = URI;
        emit ContractURIChanged(URI);
    }

    function contractURI() public view returns (string memory) {
        return _contractURI;
    }

    // Function to set the base token URI. Should be passed the "ipfs://..../" URI to be set correctly.
    function setBaseTokenURI(string calldata URI) external onlyOwner {
        _baseTokenURI = URI;
        emit BaseTokenURIChanged(URI);
    }

    function baseTokenURI() public view returns (string memory) {
        return _baseTokenURI;
    }

    // Sets default baseExtension to .json
    string private baseExtension = ".json";

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721)
        returns (string memory)
    {
        require(_exists(tokenId), "Cannot query non-existent token");

        return
            string(
                abi.encodePacked(
                    _baseTokenURI,
                    tokenId.toString(),
                    baseExtension
                )
            ); // Set baseTokenURI to end with .json
    }

    // ------------------------------

    // Withdrawal functions
    // -----------------------------------------------------------------------

    function withdrawDonations() external onlyOwner {
        uint _d1amount = address(this).balance * 1/100;
        uint _d2amount = address(this).balance * 15/1000;

        require(payable(_d1).send(_d1amount), "FAILED_TO_SEND_TO_D1");
        require(payable(_d2).send(_d2amount), "FAILED_TO_SEND_TO_D2");
    }

    function withdrawAll() external onlyOwner {
        payable(msg.sender).transfer(address(this).balance);
    }
    
    // Helpers
    // -----------------------------------------------------------------------

    function isContract(address account) internal view returns (bool) {
        // This method relies on extcodesize, which returns 0 for contracts in
        // construction, since the code is only stored at the end of the
        // constructor execution.

        uint256 size;

        assembly {
            size := extcodesize(account)
        }

        return size > 0;
    }
    
    // END CUSTOM SMART CONTRACT
}
function withdrawDonations() external onlyOwner {
    uint _d1amount = address(this).balance * 1/100;
    uint _d2amount = address(this).balance * 15/1000;

    require(payable(_d1).send(_d1amount), "FAILED_TO_SEND_TO_D1");
    require(payable(_d2).send(_d2amount), "FAILED_TO_SEND_TO_D2");
}

// ------------------------------
// Withdrawal functions
// -----------------------------------------------------------------------