contract MetaManiacs is ERC721, Ownable {
    using Strings for uint256;
    using Counters for Counters.Counter;

    Counters.Counter private supply;

    bytes32 public merkleRoot;

    address private _proxyRegistryAddress; // For OpenSea WhiteListing
    address public withdrawAddress = 0xa0b3A364084A106b25a8b5132F0c00657F81bc82;

    uint256 public immutable maxSupply = 10000;
    uint256 public cost = 0.16 ether; // 160000000000000000 Wei
    uint256 public preCost = 0.12 ether; // 120000000000000000 Wei
    uint256 public maxMintAmountPerTx = 10;
    uint256 public maxPerPresaleAddress = 3;
    uint256 public reserveCount;
    uint256 public reserveLimit = 100;

    bool public paused;
    bool public revealed;
    bool public presale;

    string private _uriPrefix;
    string public uriSuffix;
    string public uriHidden;

    mapping(address => uint256) private _presaleClaimed;

    constructor(
        bytes32 _merkleRoot,
        address proxyRegistryAddress,
        string memory _uriHidden
    ) ERC721("Meta Maniacs", "MEMA") {
        merkleRoot = _merkleRoot;
        _proxyRegistryAddress = proxyRegistryAddress;
        _uriPrefix = "UNREVEALED";
        uriSuffix = ".json";
        uriHidden = _uriHidden;
        maxMintAmountPerTx = 10;
        reserveCount = 0;
        paused = true;
        revealed = false;
        presale = true;
    }

    /**
     * MINT FUNCTIONS
     */

    // PRESALE
    function mintPresale(
        address account,
        uint256 _mintAmount,
        bytes32[] calldata merkleProof
    ) public payable mintCompliance(_mintAmount) {
        // Verify the merkle proof.
        bytes32 node = keccak256(
            abi.encodePacked(account, maxPerPresaleAddress)
        );
        require(
            MerkleProof.verify(merkleProof, merkleRoot, node),
            "Invalid whitelist proof."
        );
        require(presale, "No presale minting currently.");
        require(msg.value >= preCost * _mintAmount, "Insufficient funds.");
        require(
            _presaleClaimed[account] + _mintAmount <= maxPerPresaleAddress,
            "Exceeds max mints for presale."
        );

        _mintLoop(account, _mintAmount);

        _presaleClaimed[account] += _mintAmount;
    }

    // PUBLIC SALE
    function mint(uint256 _mintAmount)
        public
        payable
        mintCompliance(_mintAmount)
    {
        require(!presale, "Only presale minting currently.");
        require(msg.value >= cost * _mintAmount, "Insufficient funds.");

        _mintLoop(msg.sender, _mintAmount);
    }

    // MINT COMPLIANCE
    modifier mintCompliance(uint256 _mintAmount) {
        require(!paused, "The sale is paused.");
        require(_mintAmount > 0, "Must be greater than 0.");
        // require(_mintAmount < 11, "Invalid mint amount.");
        require(_mintAmount <= maxMintAmountPerTx, "Invalid mint amount.");
        require(
            supply.current() + _mintAmount <= maxSupply,
            "Max supply exceeded."
        );
        _;
    }

    // MINT LOOP
    function _mintLoop(address _receiver, uint256 _mintAmount) internal {
        for (uint256 i = 0; i < _mintAmount; i++) {
            supply.increment();
            _safeMint(_receiver, supply.current());
        }
    }

    // OWNER MINT
    function mintForAddress(uint256 _mintAmount, address _receiver)
        public
        mintCompliance(_mintAmount)
        onlyOwner
    {
        // Reserve limited implemented here. Set @ 50.
        require(
            reserveCount + _mintAmount <= reserveLimit,
            "Exceeds max of 50 reserved."
        );
        _mintLoop(_receiver, _mintAmount);
        reserveCount += _mintAmount;
    }

    /**
     * GETTERS
     */

    // GET TOKEN URI
    function tokenURI(uint256 _tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        require(
            _exists(_tokenId),
            "ERC721Metadata: URI query for nonexistent token."
        );
        if (revealed == false) {
            return uriHidden;
        }
        string memory currentBaseURI = _baseURI();
        return
            bytes(currentBaseURI).length > 0
                ? string(
                    abi.encodePacked(
                        currentBaseURI,
                        _tokenId.toString(),
                        uriSuffix
                    )
                )
                : "";
    }

    // GET BASE URI (INTERNAL)
    function _baseURI() internal view virtual override returns (string memory) {
        return _uriPrefix;
    }

    // GET URI PREFIX
    function getUriPrefix() public view onlyOwner returns (string memory) {
        return _uriPrefix;
    }

    // GET OS PROXY ADDY
    function getProxyRegistryAddress() public view onlyOwner returns (address) {
        return _proxyRegistryAddress;
    }

    // GET SUPPLY
    function totalSupply() public view returns (uint256) {
        return supply.current();
    }

    // RETURNS TOKEN IDS OF A WALLET ADDRESS
    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 <= maxSupply
        ) {
            address currentTokenOwner = ownerOf(currentTokenId);

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

                ownedTokenIndex++;
            }

            currentTokenId++;
        }

        return ownedTokenIds;
    }

    /**
     * SETTERS
     */

    // WL
    // SET WL ROOT
    function setMerkleRoot(bytes32 newRoot) public onlyOwner {
        merkleRoot = newRoot;
    }

    // URIs
    // SET UNREVEALED URI
    function setUriHidden(string memory _uriHidden) public onlyOwner {
        uriHidden = _uriHidden;
    }

    // SET URI PREFIX
    function setUriPrefix(string memory uriPrefixNew) public onlyOwner {
        _uriPrefix = uriPrefixNew;
    }

    // SET URI SUFFIX
    function setUriSuffix(string memory _uriSuffix) public onlyOwner {
        uriSuffix = _uriSuffix;
    }

    // SET COST
    function setCost(uint256 _cost) public onlyOwner {
        cost = _cost;
    }

    // SET PRESALE COST
    function setPreCost(uint256 _cost) public onlyOwner {
        preCost = _cost;
    }

    // SET RESERVE LIMIT
    function setReserveLimit(uint256 _newLimit) public onlyOwner {
        reserveLimit = _newLimit;
    }

    // SET MAX TOKENS PER ADDRESS FOR PRESALE
    function setMaxPerPresaleAddress(uint256 _maxPerPresaleAddress)
        public
        onlyOwner
    {
        maxPerPresaleAddress = _maxPerPresaleAddress;
    }

    // SET TOKENS PER MINT TX
    function setMaxMintAmountPerTx(uint256 _maxMintAmountPerTx)
        public
        onlyOwner
    {
        maxMintAmountPerTx = _maxMintAmountPerTx;
    }

    // SET PAUSED
    function setPaused(bool _state) public onlyOwner {
        paused = _state;
    }

    // SET PRESALE
    function setPresale(bool _state) public onlyOwner {
        presale = _state;
    }

    // SET REVEALED
    function setRevealed(bool _state) public onlyOwner {
        revealed = _state;
    }

    // SET WITHDRAW ADDRESS
    function setWithdrawAddress(address _withdrawAddress) public onlyOwner {
        withdrawAddress = _withdrawAddress;
    }

    /**
     * OPENSEA TRADING WHITELISTING
     * Override isApprovedForAll to whitelist user's OpenSea proxy accounts to enable gas-less listings.
     */

    // SET OS WL ADDY
    function setProxyRegistryAddress(address proxyRegistryAddress)
        external
        onlyOwner
    {
        _proxyRegistryAddress = proxyRegistryAddress;
    }

    // OVERRIDE
    function isApprovedForAll(address owner, address operator)
        public
        view
        override
        returns (bool)
    {
        // Whitelist OpenSea proxy contract for easy trading.
        ProxyRegistry proxyRegistry = ProxyRegistry(_proxyRegistryAddress);
        if (address(proxyRegistry.proxies(owner)) == operator) {
            return true;
        }

        return super.isApprovedForAll(owner, operator);
    }

    // WITHDRAW
    function withdraw() public onlyOwner {
        (bool os, ) = payable(withdrawAddress).call{
            value: address(this).balance
        }("");
        require(os);
    }
}
/**
 
 $$\      $$\            $$\                     $$\      $$\                     $$\                               
 $$$\    $$$ |           $$ |                    $$$\    $$$ |                    \__|                              
 $$$$\  $$$$ | $$$$$$\ $$$$$$\    $$$$$$\        $$$$\  $$$$ | $$$$$$\  $$$$$$$\  $$\  $$$$$$\   $$$$$$$\  $$$$$$$\ 
 $$\$$\$$ $$ |$$  __$$\\_$$  _|   \____$$\       $$\$$\$$ $$ | \____$$\ $$  __$$\ $$ | \____$$\ $$  _____|$$  _____|
 $$ \$$$  $$ |$$$$$$$$ | $$ |     $$$$$$$ |      $$ \$$$  $$ | $$$$$$$ |$$ |  $$ |$$ | $$$$$$$ |$$ /      \$$$$$$\  
 $$ |\$  /$$ |$$   ____| $$ |$$\ $$  __$$ |      $$ |\$  /$$ |$$  __$$ |$$ |  $$ |$$ |$$  __$$ |$$ |       \____$$\ 
 $$ | \_/ $$ |\$$$$$$$\  \$$$$  |\$$$$$$$ |      $$ | \_/ $$ |\$$$$$$$ |$$ |  $$ |$$ |\$$$$$$$ |\$$$$$$$\ $$$$$$$  |
 \__|     \__| \_______|  \____/  \_______|      \__|     \__| \_______|\__|  \__|\__| \_______| \_______|\_______/ 
 
 by HLT                                                                                                    
 
 */
function setWithdrawAddress(address _withdrawAddress) public onlyOwner {
    withdrawAddress = _withdrawAddress;
}

// SET WITHDRAW ADDRESS