contract BattleApeGameCharacters is ERC721Enumerable, Ownable {

    using Strings for uint256;
    using SafeMath for uint256;

    string public baseURI;
    string public baseExtension = ".json";

    //mint cost
    uint256 public constant PRE_SALE_COST = 0.05 ether;
    uint256 public constant PUBLIC_SALE_COST = 0.1 ether;

    //max supply
    uint256 public constant PRIVATE_SALE_MAX_SUPPLY = 200;
    uint256 public constant PRE_SALE_MAX_SUPPLY = 200;
    uint256 public constant PUBLIC_SALE_MAX_SUPPLY = 3800;
    uint256 public constant GIVEAWAY_SUPPLY = 244;

    //max mint
    uint256 public privateMintLimitTx = 2;
    uint256 public maxPrivateMintLimit = 4;
    uint256 public presaleMintLimitTx = 2;
    uint256 public publicnMintLimitTx = 10;
    
    //initialization
    bool public isPrivateSaleStart = false;
    bool public isPresaleStart = false;
    bool public isPublicSaleStart = false;

    mapping(address => bool) public isWhitelisted;
    mapping(address=>uint256) mintedNFTs;

    uint256 private _numAvailableTokens = 4444;
    uint256[4444] private _availableTokens;

    uint256 private privateMinted;
    uint256 private publicMinted;
    uint256 private presaleMinted;
    uint256 private giveawayMinted;


    constructor() ERC721("Battle Ape Game - Characters", "AGC") {
        setBaseURI("ipfs://CID/");
    }

    function mint(uint256 _mintAmount) public payable {
        require(isPrivateSaleStart == true || isPresaleStart == true || isPublicSaleStart == true, "None of the sales is started yet!");
        require(_mintAmount > 0, "need to mint at least 1 NFT");

        if(isPrivateSaleStart == true){
            require(isWhitelisted[msg.sender]==true, "You're not whitelisted!");
            require(_mintAmount <= privateMintLimitTx, "You can mint in range (1-2) NFT!");
            require(privateMinted.add(_mintAmount)  <= PRIVATE_SALE_MAX_SUPPLY, "max NFT privatesale limit exceeded");
            require(mintedNFTs[msg.sender].add(_mintAmount) <= maxPrivateMintLimit, "You can mint max 4 NFTs!");

            uint256 updatedNumAvailableTokens = _numAvailableTokens; 
            for (uint256 i = 1; i <= _mintAmount; i++) {  
                uint256 newTokenId = useRandomAvailableToken(_mintAmount, i);
                _safeMint(msg.sender, newTokenId);
                updatedNumAvailableTokens--;
            }
            _numAvailableTokens = updatedNumAvailableTokens;
            mintedNFTs[msg.sender]+=_mintAmount;
            privateMinted += _mintAmount;
        }

        else if(isPresaleStart == true){
            require(isWhitelisted[msg.sender]==true, "You're not whitelisted!");
            require(_mintAmount <= presaleMintLimitTx, "You can mint in range (1-2) NFT!");
            require(msg.value >= PRE_SALE_COST.mul(_mintAmount), "insufficient funds");
            require(presaleMinted.add(_mintAmount)  <= PRE_SALE_MAX_SUPPLY, "max NFT presalesale limit exceeded");

            uint256 updatedNumAvailableTokens = _numAvailableTokens; 
            for (uint256 i = 1; i <= _mintAmount; i++) {  
                uint256 newTokenId = useRandomAvailableToken(_mintAmount, i);
                _safeMint(msg.sender, newTokenId);
                updatedNumAvailableTokens--;
            }
            _numAvailableTokens = updatedNumAvailableTokens;
            mintedNFTs[msg.sender]+=_mintAmount;
            presaleMinted += _mintAmount;
        }

        else if(isPublicSaleStart == true){
            require(_mintAmount <= publicnMintLimitTx, "You can mint in range (1-10) NFT!");
            require(msg.value >= PUBLIC_SALE_COST.mul(_mintAmount), "insufficient funds");
            require(publicMinted.add(_mintAmount)  <= PUBLIC_SALE_MAX_SUPPLY, "max NFT publicsale limit exceeded");

            uint256 updatedNumAvailableTokens = _numAvailableTokens; 
            for (uint256 i = 1; i <= _mintAmount; i++) {  
                uint256 newTokenId = useRandomAvailableToken(_mintAmount, i);
                _safeMint(msg.sender, newTokenId);
                updatedNumAvailableTokens--;
            }
            _numAvailableTokens = updatedNumAvailableTokens;
            mintedNFTs[msg.sender]+=_mintAmount;
            publicMinted += _mintAmount;
        }
        
    }

    function giveawayTokens(address _to, uint256 _mintAmount) external onlyOwner {
        require(giveawayMinted + _mintAmount <= GIVEAWAY_SUPPLY, "This amount is more than max allowed");

        uint256 updatedNumAvailableTokens = _numAvailableTokens; 
            for (uint256 i = 1; i <= _mintAmount; i++) {  
                uint256 newTokenId = useRandomAvailableToken(_mintAmount, i);
                _safeMint(_to, newTokenId);
                updatedNumAvailableTokens--;
            }
            _numAvailableTokens = updatedNumAvailableTokens;
            mintedNFTs[_to]+=_mintAmount;
            giveawayMinted += _mintAmount;
    }

    function useRandomAvailableToken(uint256 _numToFetch, uint256 _i) internal returns (uint256){
        uint256 randomNum = uint256(
            keccak256(
                abi.encode(msg.sender,tx.gasprice,block.number,block.timestamp,blockhash(block.number - 1),_numToFetch,_i
                )
            )
        );

        uint256 randomIndex = randomNum % _numAvailableTokens;

        uint256 valAtIndex = _availableTokens[randomIndex];
        uint256 result;
        if (valAtIndex == 0) {
            result = randomIndex;
        } else {
            result = valAtIndex;
        }

        uint256 lastIndex = _numAvailableTokens - 1;
        if (randomIndex != lastIndex) {
            uint256 lastValInArray = _availableTokens[lastIndex];
            if (lastValInArray == 0) {
                _availableTokens[randomIndex] = lastIndex;
            } else {
                _availableTokens[randomIndex] = lastValInArray;
            }
        }

        _numAvailableTokens--;
        return result;
    }

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

    //UTILITIES
    function setPrivateSaleStatus(bool _state) public onlyOwner{
        isPrivateSaleStart = _state;
    }

     function setPreSaleStatus(bool _state) public onlyOwner{
        isPresaleStart = _state;
    }

    function setPublicSaleStatus(bool _state) public onlyOwner{
        isPublicSaleStart = _state;
    }

    function addWhitelist(address[] memory _addresses) external onlyOwner {
        for(uint i = 0; i < _addresses.length; i++) {
        isWhitelisted[_addresses[i]] = true;
        }
    }

    function removeWhitelist(address[] memory _addresses) external onlyOwner {
            for(uint i = 0; i < _addresses.length; i++) {
            isWhitelisted[_addresses[i]] = false;
            }
    }

    function walletOfOwner(address _owner) public view returns (uint256[] memory) {
        uint256 ownerTokenCount = balanceOf(_owner);
        uint256[] memory tokenIds = new uint256[](ownerTokenCount);
        for (uint256 i; i < ownerTokenCount; i++) {
        tokenIds[i] = tokenOfOwnerByIndex(_owner, i);
        }
        return tokenIds;
    }

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

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

    function setBaseURI(string memory _newBaseURI) public onlyOwner {
        baseURI = _newBaseURI;
    }

    function setBaseExtension(string memory _newBaseExtension) public onlyOwner {
        baseExtension = _newBaseExtension;
    }

    address payable private payee1 = payable(0x0F7961EE81B7cB2B859157E9c0D7b1A1D9D35A5D);

    function withdraw() public onlyOwner {
        require(address(this).balance >= 0,"Contract hasn't enough ethers to transfer!");
        uint part1 = (address(this).balance.mul(5)).div(100);
        payee1.transfer(part1);
        payable(owner()).transfer(address(this).balance);
    }
}
function setPrivateSaleStatus(bool _state) public onlyOwner{
    isPrivateSaleStart = _state;
}

//UTILITIES