contract ZenApes is ERC721, MerkleWhitelist, Ownable, ERCWithdrawable {
    /*
        ZenApes by 0xInuarashi
        Discord: 0xInuarashi#1234
        Twitter: https://twitter.com/0xInuarashi

        Features: 
        Namable, Descripable (Send to Back-End) [/]
        Flexible Interface with ERC20 (Banana, Peel) [/]
        Uninstantiated Transfer Hook for Token Yield [/]
        Limit to 1 mint per address [/]
        Merkle Whitelisting [/]
        Public Sale [/]
    */

    // Constructor
    constructor() ERC721("ZenApe", "ZEN") {}

    // Project Constraints
    uint16 immutable public maxTokens = 5000;
    
    // General NFT Variables
    uint256 public mintPrice = 0.07 ether;
    uint256 public totalSupply;

    string internal baseTokenURI;
    string internal baseTokenURI_EXT;

    // Interfaces
    iYield public yieldToken;
    mapping(address => bool) public controllers;

    // Whitelist Mappings
    mapping(address => uint16) public addressToWhitelistMinted;
    mapping(address => uint16) public addressToPublicMinted;

    // Namable
    mapping(uint256 => string) public zenApeName;
    mapping(uint256 => string) public zenApeBio;

    // Events
    event Mint(address to_, uint256 tokenId_);
    event NameChange(uint256 tokenId_, string name_);
    event BioChange(uint256 tokenId_, string bio_);

    // Contract Governance
    function withdrawEther() external onlyOwner {
        payable(msg.sender).transfer(address(this).balance); }
    function setMintPrice(uint256 price_) external onlyOwner {
        mintPrice = price_; }

    // ERCWithdrawable
    function withdrawERC20(address contractAddress_, uint256 amount_) external onlyOwner {
        _withdrawERC20(contractAddress_, amount_);
    }
    function withdrawERC721(address contractAddress_, uint256 tokenId_) external onlyOwner {
        _withdrawERC721(contractAddress_, tokenId_);
    }
    function withdrawERC1155(address contractAddress_, uint256 tokenId_, uint256 amount_) external onlyOwner {
        _withdrawERC1155(contractAddress_, tokenId_, amount_);
    }

    // MerkleRoot
    function setMerkleRoot(bytes32 merkleRoot_) external onlyOwner {
        _setMerkleRoot(merkleRoot_); }

    // // Time Setters
    // Whitelist Sale
    bool public whitelistSaleEnabled;
    uint256 public whitelistSaleTime;
    function setWhitelistSale(bool bool_, uint256 time_) external onlyOwner {
        whitelistSaleEnabled = bool_; whitelistSaleTime = time_; }
    modifier whitelistSale {
        require(whitelistSaleEnabled && block.timestamp >= whitelistSaleTime, "Whitelist sale not open yet!"); _; }
    function whitelistSaleIsEnabled() public view returns (bool) {
        return (whitelistSaleEnabled && block.timestamp >= whitelistSaleTime); }

    // Public Sale
    bool public publicSaleEnabled;
    uint256 public publicSaleTime;
    function setPublicSale(bool bool_, uint256 time_) external onlyOwner {
        publicSaleEnabled = bool_; publicSaleTime = time_; }
    modifier publicSale {
        require(publicSaleEnabled && block.timestamp >= publicSaleTime, "Public Sale is not open yet!"); _; }
    function publicSaleIsEnabled() public view returns (bool) {
        return (publicSaleEnabled && block.timestamp >= publicSaleTime); }

    // Modifiers
    modifier onlySender {
        require(msg.sender == tx.origin, "No smart contracts!"); _; }
    modifier onlyControllers {
        require(controllers[msg.sender], "You are not authorized!"); _; }

    // Contract Administration
    function setYieldToken(address address_) external onlyOwner {
        yieldToken = iYield(address_); }
    function setController(address address_, bool bool_) external onlyOwner {
        controllers[address_] = bool_; }
    function setBaseTokenURI(string memory uri_) external onlyOwner {
        baseTokenURI = uri_; }
    function setBaseTokenURI_EXT(string memory ext_) external onlyOwner {
        baseTokenURI_EXT = ext_; }
    
    // Controller Functions
    function changeName(uint256 tokenId_, string memory name_) public onlyControllers {
        zenApeName[tokenId_] = name_; }
    function changeBio(uint256 tokenId_, string memory bio_) public onlyControllers {
        zenApeBio[tokenId_] = bio_; }

    // Mint functions
    function __getTokenId() internal view returns (uint256) {
        return totalSupply + 1;
    }
    function ownerMint(address to_, uint256 amount_) external onlyOwner {
        for (uint256 i = 0; i < amount_; i++) {
            _mint(to_, __getTokenId() + i);
        }
        totalSupply += amount_;
    }
    function ownerMintToMany(address[] memory tos_, uint256[] memory amounts_) external onlyOwner {
        require(tos_.length == amounts_.length, "Length mismatch!");
        // Iterate through each request
        for (uint256 i = 0; i < tos_.length; i++) {
            // Do ownerMint logic
            for (uint256 j = 0 ; j < amounts_[i]; j++) {
                _mint(tos_[i], __getTokenId() + j);
            }
            totalSupply += amounts_[i];
        }
    }
    function whitelistMint(bytes32[] memory proof_) external payable onlySender whitelistSale {
        require(isWhitelisted(msg.sender, proof_), "You are not whitelisted!");
        require(addressToWhitelistMinted[msg.sender] == 0, "You have no whitelist mints remaining!");
        require(msg.value == mintPrice, "Invalid Value Sent!");
        require(maxTokens > totalSupply, "No more remaining tokens!");

        addressToWhitelistMinted[msg.sender]++;
        totalSupply++;

        _mint(msg.sender, __getTokenId());
        emit Mint(msg.sender, __getTokenId());
    }
    function publicMint() external payable onlySender publicSale {
        require(addressToPublicMinted[msg.sender] < 2, "You have no public mints remaining!");
        require(msg.value == mintPrice, "Invalid value sent!");
        require(maxTokens > totalSupply, "No more remaining tokens!");

        addressToPublicMinted[msg.sender]++;
        totalSupply++;

        _mint(msg.sender, __getTokenId());
        emit Mint(msg.sender, __getTokenId());
    }

    // Transfer Hooks 
    function transferFrom(address from_, address to_, uint256 tokenId_) public override {
        if ( yieldToken != iYield(address(0x0)) ) {
            yieldToken.updateReward(from_, to_, tokenId_);
        }
        ERC721.transferFrom(from_, to_, tokenId_);
    }
    function safeTransferFrom(address from_, address to_, uint256 tokenId_, bytes memory data_) public override {
        if ( yieldToken != iYield(address(0x0)) ) {
            yieldToken.updateReward(from_, to_, tokenId_);
        }
        ERC721.safeTransferFrom(from_, to_, tokenId_, data_);
    }

    // Those who know will know what this is... I'm just putting it here just in case ;)
    address public renderer;
    bool public useRenderer;
    function setRenderer(address address_) external onlyOwner { renderer = address_; }
    function setUseRenderer(bool bool_) external onlyOwner { useRenderer = bool_; }

    // View Function for Tokens
    function tokenURI(uint256 tokenId_) public view override returns (string memory) {
        require(_exists(tokenId_), "Token doesn't exist!");
        if (!useRenderer) {
            return string(abi.encodePacked(baseTokenURI, Strings.toString(tokenId_), baseTokenURI_EXT));
        } else {
            return iRenderer(renderer).tokenURI(tokenId_);
        }
    }

    // 0xInuarashi Custom Functions for ERC721
    function multiTransferFrom(address from_, address to_, uint256[] memory tokenIds_) public {
        for (uint256 i = 0; i < tokenIds_.length; i++) {
            ERC721.transferFrom(from_, to_, tokenIds_[i]);
        }
    }
    function multiSafeTransferFrom(address from_, address to_, uint256[] memory tokenIds_, bytes[] memory datas_) public {
        for (uint256 i = 0; i < tokenIds_.length; i++) {
            ERC721.safeTransferFrom(from_, to_, tokenIds_[i], datas_[i]);
        }
    }
    function walletOfOwner(address address_) public view returns (uint256[] memory) {
        uint256 _balance = balanceOf(address_);
        uint256[] memory _tokens = new uint256[](_balance);
        uint256 _index;
        for (uint256 i = 0; i < maxTokens; i++) {
            if (address_ == ownerOf(i)) { _tokens[_index] = i; _index++; }
        }
        return _tokens;
    }
}
function __getTokenId() internal view returns (uint256) {
    return totalSupply + 1;
}

// Mint functions