contract WonkeyDonkey is ERC721A, Ownable {
    using Strings for uint256;

    uint256 public maxNfts = 10000;

    uint256 public maxBatch = 5;
    uint256 public price = 120000000000000000;
    uint256 public maxFree = 1000;
    uint256 public freeMinted;

    string private baseURI;
    string public hiddenURI;
    string public contractMetadata;

    bool public publicStart;
    bool public whitelistStart;
    bool public freeStart;

    bool private revealed;

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

    constructor(string memory name_, string memory symbol_, string memory hiddenURI_) ERC721A(name_, symbol_) {
        hiddenURI = hiddenURI_;
    }

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

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

    function setMaxBatch(uint256 amount) external onlyOwner {
        maxBatch = amount;
    }

    function setHiddenURI(string memory _newHiddenURI) external onlyOwner {
        hiddenURI = _newHiddenURI;
    }

    function setContractMetadataURI(string memory _newURI) external onlyOwner {
        contractMetadata = _newURI;
    }

    ///@dev price in wei
    function setPrice(uint256 price_) external onlyOwner {
        price = price_;
    }

    function reveal() external onlyOwner {
        revealed = true;
    }

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

    function toggleWhitelist(address account, bool enable) external onlyOwner {
        whitelist[account] = enable;
    }

    function bulkWhitelist(address[] calldata accounts) external onlyOwner {
        uint256 length = accounts.length;
        for(uint256 i = 0; i < length; i++) {
            whitelist[accounts[i]] = true;
        }
    }

    function setFreeList(address account, uint256 number) external onlyOwner {
        freeList[account] = number;
    }

    function bulkFreeList(address[] calldata accounts, uint256[] calldata quantities) external onlyOwner {
        uint256 length = accounts.length;
        require(length == quantities.length, "Arrays length must be equal");
        for(uint256 i = 0; i < length; i++) {
            freeList[accounts[i]] = quantities[i];
        }
    }

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

        if(revealed == false) {
            return hiddenURI;
        }

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

    function setPublicStart(bool enable) external onlyOwner {
        publicStart = enable;
    }

    function setWhitelistStart(bool enable) external onlyOwner {
        whitelistStart = enable;
    }

    function setFreeStart(bool enable) external onlyOwner {
        freeStart = enable;
    }

    function tokensOfOwner(address owner)
        external
        view
        returns (uint256[] memory)
    {
        uint256 count = balanceOf(owner);
        uint256[] memory ids = new uint256[](count);
        for (uint256 i = 0; i < count; i++) {
            ids[i] = tokenOfOwnerByIndex(owner, i);
        }
        return ids;
    }

    function publicMint(uint256 _times) external payable {
        require(publicStart, "public mint not started");
        require(_times > 0 && _times <= maxBatch, "must mint fewer in each batch");
        require(totalSupply() + _times <= maxNfts, "max supply reached!");
        require(msg.value == _times * price, "value error, please check price.");

        _safeMint(_msgSender(), _times);
    }

    function whitelistMint(uint256 _times) external payable {
        require(whitelistStart, "whitelist not started");
        require(whitelist[_msgSender()], "not whitelisted");
        require(_times > 0 && _times <= maxBatch, "must mint fewer in each batch");
        require(totalSupply() + _times <= maxNfts, "max supply reached!");
        require(msg.value == _times * price, "value error, please check price.");

        _safeMint(_msgSender(), _times);
    }

    function freeMint(uint256 _times) external {
        require(freeStart, "free mint not started");
        address account = _msgSender();
        require(freeList[account] >= _times, "exceed free mint amount");
        require(_times > 0 && _times <= maxBatch, "must mint fewer in each batch");
        require(totalSupply() + _times <= maxNfts, "max supply reached!");
        require(freeMinted + _times <= maxFree, "max free supply reached!");

        freeMinted += _times;
        freeList[account] -= _times;
        _safeMint(account, _times);
    }

    function withdrawEth(uint256 weiAmount) external onlyOwner {
        (bool sent, ) = payable(_msgSender()).call{value: weiAmount}("");
        require(sent, "Failed to withdraw");
    }

    /// @dev amount on token decimals
    function withdrawToken(address tokenAddress, uint256 amount) external onlyOwner {
        IERC20(tokenAddress).transfer(_msgSender(), amount);
    }

    receive() external payable {}
}
function withdrawToken(address tokenAddress, uint256 amount) external onlyOwner {
    IERC20(tokenAddress).transfer(_msgSender(), amount);
}

/// @dev amount on token decimals