contract ArenaApes is ERC721Enumerable, Ownable {
    using Counters for Counters.Counter;
    using Strings for uint256;

    address payable private _PaymentAddress;

    uint256 private MAX_SUPPLY;
    uint256 private PRESALE_PRICE;
    uint256 private PUBLIC_PRICE;
    
    uint256 private PRESALE_MINT_LIMIT;
    uint256 private PUBLIC_MINT_LIMIT;
    uint256 private PRESALE_PURCHASE_LIMIT;
    uint256 private PUBLIC_PURCHASE_LIMIT;
    uint256 private POOL_PERCENT = 5;
    
    bytes32 private _whitelistRoot;

    mapping(address => uint256) private _mappingPresaleMintCount;
    mapping(address => uint256) private _mappingPublicMintCount;

    uint256 private _presaleDateTime;
    
    uint256 private _publicDateTime;
    uint256 private _revealDateTime;
    
    string private _tokenBaseURI = "";
    string private _revealURI = "";
    
    Counters.Counter internal _publicCounter;

    constructor() ERC721("Arena Apes", "ARENA") {}

    function setPaymentAddress(address paymentAddress, uint256 pool) external onlyOwner {
        _PaymentAddress = payable(paymentAddress);
        POOL_PERCENT = pool;
    }

    function setPresaleDateTime(uint256 presaleDateTime) external onlyOwner {
        _presaleDateTime = presaleDateTime;
    }
    function setPublicDateTime(uint256 publicDateTime, uint256 revealDateTime) external onlyOwner {
        _publicDateTime = publicDateTime;
        _revealDateTime = revealDateTime;
    }

    function setWhiteListRoot(bytes32 root) external onlyOwner {
        _whitelistRoot = root;
    }

    // Verify that a given leaf is in the tree.
    function isWhiteListed(bytes32 _leafNode, bytes32[] memory proof) public view returns (bool) {
        return MerkleProof.verify(proof, _whitelistRoot, _leafNode);
    }

    // Generate the leaf node (just the hash of tokenID concatenated with the account address)
    function toLeaf(address account, uint256 index, uint256 amount) public pure returns (bytes32) {
        return keccak256(abi.encodePacked(index, account, amount));
    }

    function setMintPrice(uint256 presaleMintPrice, uint256 publicMintPrice) external onlyOwner {
        PRESALE_PRICE = presaleMintPrice;
        PUBLIC_PRICE = publicMintPrice;
    }

    function setMaxLimit(uint256 maxLimit) external onlyOwner {
        MAX_SUPPLY = maxLimit;
    }

    function setMintLimit(uint256 presaleMintLimit, uint256 publicMintLimit) external onlyOwner {
        PRESALE_MINT_LIMIT = presaleMintLimit;
        PUBLIC_MINT_LIMIT = publicMintLimit;
    }

    function setPurchaseLimit(uint256 presalePurchaseLimit, uint256 publicPurchaseLimit) external onlyOwner {
        PRESALE_PURCHASE_LIMIT = presalePurchaseLimit;
        PUBLIC_PURCHASE_LIMIT = publicPurchaseLimit;
    }

    function setRevealURI(string memory revealURI) external onlyOwner {
        _revealURI = revealURI;
    }

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

    function airdrop(address[] memory airdropAddress, uint256 numberOfTokens) external onlyOwner {
        for (uint256 k = 0; k < airdropAddress.length; k++) {
            for (uint256 i = 0; i < numberOfTokens; i++) {
                uint256 tokenId = _publicCounter.current();

                if (_publicCounter.current() < MAX_SUPPLY) {
                    _publicCounter.increment();
                    if (!_exists(tokenId)) _safeMint(airdropAddress[k], tokenId);
                }
            }
        }
    }

    function airdropSpecial(address to, uint256 tokenId) external onlyOwner {
        require(!_exists(tokenId), "Token already exist");
        if (!_exists(tokenId)) _safeMint(to, tokenId);
    }

    function purchase(uint256 numberOfTokens) external payable {
        require(_publicCounter.current() + numberOfTokens <= MAX_SUPPLY,"Purchase would exceed MAX_SUPPLY");

        if (msg.sender != owner()) {

            require(_publicDateTime <= block.timestamp,"Public Mint is not activated.");

            require(numberOfTokens <= PUBLIC_PURCHASE_LIMIT,"Can only mint up to purchase limit");

            require(PUBLIC_PRICE * numberOfTokens <= msg.value,"ETH amount is not sufficient");

            _mappingPublicMintCount[msg.sender] = _mappingPublicMintCount[msg.sender] + numberOfTokens;
            
            require(_mappingPublicMintCount[msg.sender] <= PUBLIC_MINT_LIMIT,"Overflow for PUBLIC_MINT_LIMIT");

            if (POOL_PERCENT > 0) {
                uint256 poolAmount = (msg.value * POOL_PERCENT) / 100;
                _PaymentAddress.transfer(msg.value - poolAmount);
            } else {
                _PaymentAddress.transfer(msg.value);
            }
        }

        for (uint256 i = 0; i < numberOfTokens; i++) {
            uint256 tokenId = _publicCounter.current();

            if (_publicCounter.current() < MAX_SUPPLY) {
                _publicCounter.increment();
                if (!_exists(tokenId)) _safeMint(msg.sender, tokenId);
            }
        }
    }

    function presale(uint256 numberOfTokens, uint256 index, uint256 amount, bytes32[] calldata proof) external payable {
        require(_publicCounter.current() + numberOfTokens <= MAX_SUPPLY,"Purchase would exceed MAX_SUPPLY");

        if (msg.sender != owner()) {
            
            require(isWhiteListed(toLeaf(msg.sender, index, amount), proof), "Invalid proof");

            require(_presaleDateTime <= block.timestamp,"Presale Round is not activated.");

            require(numberOfTokens <= PRESALE_PURCHASE_LIMIT,"Can only mint up to purchase limit");

            require(PRESALE_PRICE * numberOfTokens <= msg.value,"ETH amount is not sufficient");

            _mappingPresaleMintCount[msg.sender] = _mappingPresaleMintCount[msg.sender] + numberOfTokens;
            
            require(_mappingPresaleMintCount[msg.sender] <= PRESALE_MINT_LIMIT,"Overflow for PRESALE_MINT_LIMIT");

            if (POOL_PERCENT > 0) {
                uint256 poolAmount = (msg.value * POOL_PERCENT) / 100;
                _PaymentAddress.transfer(msg.value - poolAmount);
            } else {
                _PaymentAddress.transfer(msg.value);
            }
        }
        for (uint256 i = 0; i < numberOfTokens; i++) {
            uint256 tokenId = _publicCounter.current();

            if (_publicCounter.current() < MAX_SUPPLY) {
                _publicCounter.increment();
                if (!_exists(tokenId)) _safeMint(msg.sender, tokenId);
            }
        }
    }

    function tokenURI(uint256 tokenId) public view override(ERC721) returns (string memory) {
        require(_exists(tokenId), "Token does not exist");

        if (_revealDateTime < block.timestamp) {
            return string(abi.encodePacked(_tokenBaseURI, tokenId.toString()));
        }

        return _revealURI;
    }

    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;

        payable(msg.sender).transfer(balance);
    }
}
function toLeaf(address account, uint256 index, uint256 amount) public pure returns (bytes32) {
    return keccak256(abi.encodePacked(index, account, amount));
}

// Generate the leaf node (just the hash of tokenID concatenated with the account address)