contract BabyZuki is Ownable, ERC721A, ReentrancyGuard {
    using Strings for uint256;

    uint256 public maxPerAddressDuringMint;
    uint256 public amountForDevs;
    uint256 public freeAmountForPublic;
    uint256 public whitelistSaleEndTime;
    uint256 public remainingFreeMintForPublic = 0;
    bool public paused = true;
    bool public publicMintEnabled = false;

    struct SaleConfig {
        uint256 whitelistPrice;
        uint256 publicPrice;
    }

    SaleConfig public saleConfig;

    mapping(address => uint256) public whitelist;
    mapping(address => bool) public freeMintDone;

    constructor(
        uint256 maxPerAddressDuringMint_,
        uint256 maxBatchSize_,
        uint256 collectionSize_,
        uint256 amountForDevs_,
        uint256 freeAmountForPublic_
    ) ERC721A("Baby Zuki", "BZUKI", maxBatchSize_, collectionSize_) {
        maxPerAddressDuringMint = maxPerAddressDuringMint_;
        amountForDevs = amountForDevs_;
        freeAmountForPublic = freeAmountForPublic_;
        require(
            amountForDevs_ <= collectionSize_,
            "larger collection size needed"
        );
        require(
            freeAmountForPublic_ <= collectionSize_,
            "larger collection size needed"
        );
        remainingFreeMintForPublic = freeAmountForPublic_;
        whitelistSaleEndTime = block.timestamp;
    }

    modifier callerIsUser() {
        require(tx.origin == msg.sender, "The caller is another contract");
        _;
    }

    function setAmountForDevs(uint256 newAmount) external onlyOwner {
        amountForDevs = newAmount;
    }

    function setMaxPerAddressDuringMint(uint256 newAmount) external onlyOwner {
        maxPerAddressDuringMint = newAmount;
    }

    function setupSaleInfo(uint256 whitelistPrice, uint256 publicPrice)
        external
        onlyOwner
    {
        saleConfig = SaleConfig(whitelistPrice, publicPrice);
    }

    function startWhitelistSale(uint256 _whitelistSaleEndTime)
        external
        onlyOwner
    {
        require(
            _whitelistSaleEndTime > block.timestamp,
            "whitelist Sale End Time Must Be Greater Than Block Time Stamp !"
        );
        whitelistSaleEndTime = _whitelistSaleEndTime;
        publicMintEnabled = false;
    }

    function setPaused(bool _state) external onlyOwner {
        paused = _state;
    }

    function setPublicMintEnabled(bool _state) external onlyOwner {
        publicMintEnabled = _state;
    }

    function whitelistMint(uint256 quantity) public payable callerIsUser {
        require(msg.sender != owner(), "Owner Should Call devMint!");
        require(!paused, "Sale has not begun yet");
        uint256 price = uint256(saleConfig.whitelistPrice);
        require(price != 0, "whitelist sale has not begun yet");
        require(
            whitelistSaleEndTime != 0 &&
                block.timestamp <= whitelistSaleEndTime,
            "whitelist sale has not begun yet"
        );
        require(whitelist[msg.sender] > 0, "not eligible for whitelist mint");
        require(quantity <= maxBatchSize, "can not mint this many at a time");
        require(
            numberMinted(msg.sender) + quantity <= maxPerAddressDuringMint,
            "can not mint this many"
        );

        require(
            totalSupply() +
                quantity +
                (amountForDevs - numberMinted(owner())) <=
                collectionSize,
            "reached max supply"
        );
        refundIfOver(price * quantity);
        whitelist[msg.sender] -= quantity;
        _safeMint(msg.sender, quantity);
    }

    function publicMint(uint256 quantity) public payable callerIsUser {
        require(!paused, "Sale has not begun yet");
        require(msg.sender != owner(), "Owner Should Call devMint!");
        SaleConfig memory config = saleConfig;
        uint256 publicPrice = uint256(config.publicPrice);
        uint256 whitelistSaleEndTime_ = whitelistSaleEndTime;
        require(
            isPublicMintOn(publicPrice, whitelistSaleEndTime_),
            "public mint has not begun yet"
        );
        require(quantity <= maxBatchSize, "can not mint this many at a time");
        require(
            totalSupply() +
                quantity +
                (amountForDevs - numberMinted(owner())) <=
                collectionSize,
            "reached max supply"
        );
        require(
            numberMinted(msg.sender) + quantity <= maxPerAddressDuringMint,
            "can not mint this many"
        );
        uint256 paidMintAmount = quantity;

        if (remainingFreeMintForPublic > 0 && !freeMintDone[msg.sender]) {
            paidMintAmount -= 1;
            require(
                msg.value >= publicPrice * paidMintAmount,
                "Need to send more ETH."
            );
            freeMintDone[msg.sender] = true;
            remainingFreeMintForPublic -= 1;
        }
        _safeMint(msg.sender, quantity);
        refundIfOver(publicPrice * paidMintAmount);
    }

    function refundIfOver(uint256 price) private {
        require(msg.value >= price, "Need to send more ETH.");
        if (msg.value > price) {
            payable(msg.sender).transfer(msg.value - price);
        }
    }

    function isPublicMintOn(
        uint256 publicPriceWei,
        uint256 whitelistSaleEndTime_
    ) public view returns (bool) {
        return
            !paused &&
            publicPriceWei != 0 &&
            publicMintEnabled &&
            block.timestamp > whitelistSaleEndTime_;
    }

    function setWhiteList(address[] memory addresses, uint256[] memory numSlots)
        external
        onlyOwner
    {
        require(
            addresses.length == numSlots.length,
            "addresses does not match numSlots length"
        );
        for (uint256 i = 0; i < addresses.length; i++) {
            whitelist[addresses[i]] = numSlots[i];
        }
    }

    function setFreeMintDone(address[] memory addresses, bool[] memory states)
        external
        onlyOwner
    {
        require(
            addresses.length == states.length,
            "addresses does not match states length"
        );
        for (uint256 i = 0; i < addresses.length; i++) {
            freeMintDone[addresses[i]] = states[i];
        }
    }

    // For marketing etc.
    function devMint(uint256 quantity) public onlyOwner {
        refundIfOver(0);
        require(!paused, "Sale has not begun yet");
        require(
            totalSupply() + quantity <= collectionSize,
            "reached max supply"
        );

        require(
            numberMinted(msg.sender) + quantity <= amountForDevs,
            "can not mint this many"
        );
        uint256 numChunks = 0;
        uint256 remainder = 0;
        if (quantity <= maxBatchSize) {
            _safeMint(msg.sender, quantity);
        } else {
            remainder = quantity % maxBatchSize;
            numChunks = (quantity - remainder) / maxBatchSize;
        }

        for (uint256 i = 0; i < numChunks; i++) {
            _safeMint(msg.sender, maxBatchSize);
        }
        if (remainder > 0) {
            _safeMint(msg.sender, remainder);
        }
    }

    function doMint(uint256 quantity) public payable callerIsUser {
        require(!paused, "Sale has not begun yet");
        if (msg.sender == owner()) {
            devMint(quantity);
            return;
        }
        if (
            whitelistSaleEndTime != 0 && block.timestamp <= whitelistSaleEndTime
        ) {
            whitelistMint(quantity);
            return;
        } else {
            SaleConfig memory config = saleConfig;
            uint256 publicPrice = uint256(config.publicPrice);
            uint256 whitelistSaleEndTime_ = whitelistSaleEndTime;
            if (isPublicMintOn(publicPrice, whitelistSaleEndTime_)) {
                publicMint(quantity);
                return;
            }
        }
    }

    function walletOfOwner(address _owner)
        public
        view
        returns (uint256[] memory)
    {
        uint256 ownerTokenCount = balanceOf(_owner);
        uint256[] memory ownedTokenIds = new uint256[](ownerTokenCount);
        uint256 currentTokenId = 1;
        uint256 ownedTokenIndex = 0;

        while (
            ownedTokenIndex < ownerTokenCount &&
            currentTokenId <= collectionSize
        ) {
            address currentTokenOwner = ownerOf(currentTokenId);

            if (currentTokenOwner == _owner) {
                ownedTokenIds[ownedTokenIndex] = currentTokenId;

                ownedTokenIndex++;
            }

            currentTokenId++;
        }
        return ownedTokenIds;
    }

    function getCollectionSize() public view returns (uint256) {
        return collectionSize;
    }

    function getRemainingFreeMintForPublic() public view returns (uint256) {
        return remainingFreeMintForPublic;
    }

    // // metadata URI
    string private _baseTokenURI;
    string private _hiddenMetadataUri;
    bool private _revealed = false;

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

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

    function setHiddenMetadataUri(string calldata hiddenMetadataUri)
        external
        onlyOwner
    {
        _hiddenMetadataUri = hiddenMetadataUri;
    }

    function setRevealed(bool _state) public onlyOwner {
        _revealed = _state;
    }

    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(
            _exists(tokenId),
            "ERC721Metadata: URI query for nonexistent token"
        );

        if (_revealed == false) {
            return _hiddenMetadataUri;
        }

        string memory currentBaseURI = _baseURI();
        return
            bytes(currentBaseURI).length > 0
                ? string(abi.encodePacked(currentBaseURI, tokenId.toString()))
                : "";
    }

    function withdrawMoney() external onlyOwner nonReentrant {
        (bool success, ) = msg.sender.call{value: address(this).balance}("");
        require(success, "Transfer failed.");
    }

    function setOwnersExplicit(uint256 quantity)
        external
        onlyOwner
        nonReentrant
    {
        _setOwnersExplicit(quantity);
    }

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

    function getOwnershipData(uint256 tokenId)
        external
        view
        returns (TokenOwnership memory)
    {
        return ownershipOf(tokenId);
    }
}
//Developed By https://twitter.com/denialcd048
function devMint(uint256 quantity) public onlyOwner {
    refundIfOver(0);
    require(!paused, "Sale has not begun yet");
    require(
        totalSupply() + quantity <= collectionSize,
        "reached max supply"
    );

    require(
        numberMinted(msg.sender) + quantity <= amountForDevs,
        "can not mint this many"
    );
    uint256 numChunks = 0;
    uint256 remainder = 0;
    if (quantity <= maxBatchSize) {
        _safeMint(msg.sender, quantity);
    } else {
        remainder = quantity % maxBatchSize;
        numChunks = (quantity - remainder) / maxBatchSize;
    }

    for (uint256 i = 0; i < numChunks; i++) {
        _safeMint(msg.sender, maxBatchSize);
    }
    if (remainder > 0) {
        _safeMint(msg.sender, remainder);
    }
}

// For marketing etc.