contract MightyMongooses is ERC721, ERC721Enumerable,ERC721URIStorage, Pausable, Ownable,ERC721Burnable

{
    using Address for address payable;
    using SafeMath for uint256;
    
    bytes4 constant private MALE = bytes4(keccak256("Male"));
    bytes4 constant private FEMALE = bytes4(keccak256("Female"));
    
    uint256 private genCreation;
    uint256 public breedCount;
    uint256 public mintNftCount = 0;
    uint256 public constant MAXNFT_LIMIT = 8888;
    uint256 public maxNftLimit = MAXNFT_LIMIT;
    uint256 public maxBreedLimit = 6000;
    uint256 public eligibleDurationForBreed = 86400 * 2;
    uint256 public DurationToGestation = 172800;
    uint256 public tokenPrice;
    uint256 public childPrice;
    uint256 public maximumBreedPerUser = 5; 
    uint256 public maxWhitelistSize = 800;
    uint256 public TotalWhitelistNFT = 0;
    uint256 public durationForWhitelist = 86400 * 6;
    uint256 public mintDateForWhitelist = block.timestamp;
    uint256 public maxTokensAtOnce = 20;
    uint256 internal nonce = 0;
    uint256[MAXNFT_LIMIT] public indices;
    
    bool public mintSignStatus;
    bool public breedPause = true;
    bool public publicSale = false;
    bool public presalePause= false;
  
    address public feeAddress;

    uint256 public totalPresaleUser;
    mapping (address => bool) public presaleStore;
    
    struct nftData {
        string gender;
        uint256 birthTime;
        uint256 motherId;
        uint256 fatherId;
        uint256 generation;
        uint256 nextReserve;
    }
    
    struct userStore {
        uint256 breedCount;
        uint256 nftCount;
        uint256 gestationTime;
        bool gestation;
    }

    
    mapping (address => userStore) public breedStore;
    mapping (uint256 => nftData) public nftStore;
    mapping(address => uint256[]) public userTokens;
    string public baseURI;

    event AddedToWhitelist(address indexed account);
    event RemovedFromWhitelist(address indexed account);
    event MintToken(uint256 tokenID);
    event GestationApproved(uint256 gestationTime);
    
    constructor() ERC721("MightyMongooses", "MightyMongooses (MME)") {
        baseURI = "www.mightymongooses.com";
        genCreation = type(uint256).max % block.timestamp;
        feeAddress = msg.sender;
        tokenPrice = 0.07e18;
        childPrice = 0.05e18;
    }
    
    receive() external payable {}

    function _baseURI() internal view override returns (string memory) {
        return baseURI;
    }
    
    function setBaseURI(string memory _URI) external onlyOwner {
        baseURI = _URI;
    }

    function setChildURI(string memory _URI) external onlyOwner{
        _setChildURI(_URI);
    }

    function setPresalePause(bool status) external onlyOwner {
        presalePause = status;
    }

    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }
    
    function eligibleDurationForBreedUpdate(uint256 newDuration) public onlyOwner {
        eligibleDurationForBreed = newDuration;
    }
    
    function tokenEmergencySafe(address token,address user,uint256 amount) public onlyOwner{
        IERC20(token).transfer(user,amount);
    }
    
    function ethEmergencySafe(address user,uint256 amount) public onlyOwner {
        payable(user).transfer(amount);
    }
    
    function getUesrAllTokens(address account) public view returns (uint256) {
        return breedStore[account].nftCount;
    }
    
    function tokenPriceUpdate(uint256 amount) public onlyOwner {
        tokenPrice = amount;
    }
    
    function childPriceUpdate(uint256 amount) public onlyOwner {
        childPrice = amount;
    }
    
    function feeAddressUpdate(address account) public onlyOwner {
        require(account != address(0), "Should be a zero address");
        feeAddress = account;
    }
    
    function breedMaximumUpdate(uint256 amount) public onlyOwner {
        maximumBreedPerUser = amount;
    }
    
    function readyToBreed(uint256 id) public view returns (bool){
        return ((nftStore[id].birthTime + eligibleDurationForBreed) <= block.timestamp);
    }
    
    function userAllTokens(address account) public view returns (uint256[] memory){
        return userTokens[account];
    }
    
    function mintWithSignPause(bool status) public onlyOwner{
        mintSignStatus = status;
    }
    
    function maxNftLimitUpdate(uint256 amount) public onlyOwner {
       maxNftLimit = amount;
    }
    
    function maxBreedLimitUpdate(uint256 amount) public onlyOwner {
       maxBreedLimit = amount;
    }
     
    function breedPauseUpdate(bool status) public onlyOwner {
        breedPause = status;
    }
    
    function DurationToGestationUpdate(uint256 amount) public onlyOwner {
        DurationToGestation = amount;
    }
    
    function updateMintDateForWhitelist(uint256 amount) public onlyOwner {
        mintDateForWhitelist = amount;
    }
    
    function updateDurationForWhitelist(uint256 amount) public onlyOwner {
        durationForWhitelist = amount;
    }

    function updateMaxTokensAtOnce(uint256 _count) public onlyOwner {
        maxTokensAtOnce = _count;
    }
    
    function updatemaxWhitelistSize(uint256 _maxWhitelistSize) public onlyOwner {
        maxWhitelistSize =  _maxWhitelistSize;
    }
    
    function gestationApprove(uint256 maleId,uint256 femaleId) external whenNotPaused {
        address user = _msgSender();
        require(!breedPause, "Unable to breed Now");
        require(breedCount < maxBreedLimit, "breed limit exceed");
        require((maleId % 2 == 0) && (femaleId % 2 == 1), "Male or Female id invalid");
        require(ownerOf(maleId) == user && ownerOf(femaleId) == user, "callable address is invaild");
        require(bytes4(keccak256(abi.encodePacked(nftStore[maleId].gender))) == MALE, "Men NFT should only callable");
        require(bytes4(keccak256(abi.encodePacked(nftStore[femaleId].gender))) == FEMALE, "Female NFT should only callable");
        require(nftStore[maleId].generation == nftStore[femaleId].generation , "Breed is not proceed with parent");
        
        require(breedStore[user].breedCount < maximumBreedPerUser, "Breed limit reached");
        require(!breedStore[user].gestation, "Already in gestation");
        
        if(nftStore[maleId].fatherId != 0 && nftStore[femaleId].fatherId != 0){
            require(readyToBreed(maleId) && readyToBreed(femaleId), "Not eligible for breed");
        }
        
        breedStore[user].gestation = true;
        breedStore[user].gestationTime = block.timestamp;
        
        emit GestationApproved(breedStore[user].gestationTime);
    }

 function safeMint(address to, uint256 numberOfTokens) external payable whenNotPaused{
        require(
            numberOfTokens <= maxTokensAtOnce,
            "Exceeds max number of Mongooses in one transaction"
        );
        require(
            mintNftCount + numberOfTokens <= maxNftLimit,
            "Total NFT limit exceeded in public sale"
        );
        
        require(publicSale, "Public sale must be active");

        if(to != owner()){
            require(msg.value == tokenPrice.mul(numberOfTokens), "Invalid token price.");
            payable(feeAddress).sendValue(msg.value);
        }else {
            if(msg.value > 0){
                payable(feeAddress).sendValue(msg.value);
            }
        }
        
        for(uint256 i = 1; i <= numberOfTokens; i++) {
            uint256 tokenID = randomIndex(0);
	    if (!_exists(tokenID))
	    {
            safeMintWithRandomIndex(to, tokenID,false);
	    }
	    else
	    {
	    i--;
	    }
        }
    }

      
    function mintMongooses(address to, uint256 numberOfTokens) external payable whenNotPaused{
        require(
            numberOfTokens <= maxTokensAtOnce,
            "Exceeds max number of Mongooses in one transaction"
        );
        require(
            mintNftCount + numberOfTokens <= maxNftLimit,
            "Total NFT limit exceeded in public sale"
        );
        
        require(publicSale, "Public sale must be active");

        if(to != owner()){
            require(msg.value == tokenPrice.mul(numberOfTokens), "Invalid token price.");
            payable(feeAddress).sendValue(msg.value);
        }else {
            if(msg.value > 0){
                payable(feeAddress).sendValue(msg.value);
            }
        }
        
        for(uint256 i = 1; i <= numberOfTokens; i++) {
            uint256 tokenID = randomIndex(0);
            safeMintWithRandomIndex(to, tokenID,false);
        }
    }
    
 function presaleMint(address to,uint256 numberOfTokens) external payable{
        require(!presalePause, "Presale Paused");
        require(presaleStore[_msgSender()], "User is not Whitelisted");
        require(
            numberOfTokens <= maxTokensAtOnce,
            "Exceeds max number of Mongooses in one transaction"
        );
        require(
            mintNftCount + numberOfTokens <= maxWhitelistSize,
            "Whitelist NFT limit reached"
        );
        
        require(mintDateForWhitelist + durationForWhitelist > block.timestamp, "Presale expired");
        if(msg.sender != owner()){
            require(tokenPrice.mul(numberOfTokens) <= msg.value, "Invalid Token Price");
            payable(feeAddress).sendValue(msg.value);
        }else {
            if(msg.value > 0){
                payable(feeAddress).sendValue(msg.value);
            }
        }
       
        for (uint256 i = 0; i < numberOfTokens; i++) {
           mintNftCount++;
           TotalWhitelistNFT++;
           safeMintWithRandomIndex(to,mintNftCount,true);
       }
    }

    
    function randomIndex(uint256 tokenId) private returns (uint256) {
        uint256 totalSize = (maxNftLimit - mintNftCount) + TotalWhitelistNFT;
        
        uint256 index = !publicSale ? tokenId : uint(keccak256(abi.encodePacked(nonce, msg.sender, block.difficulty, block.timestamp))) % totalSize;
        uint256 value = 0;
    
        if (indices[index] != 0) {
          value = indices[index];
        } else {
          value = index;
        }
    
        if (indices[totalSize - 1] == 0) { 
          indices[index] = totalSize - 1;
        } else {
          indices[index] = indices[totalSize - 1];
        }
    
        nonce++;
    
        return value.add(1);
    }

function safeMintWithRandomIndex(address to, uint256 tokenID,bool status) private {
        _safeMint(to, tokenID);
        emit MintToken(tokenID);
        
        string memory _gender = ((tokenID % 2) == 0) ? "Male" : "Female";
        userTokens[to].push(tokenID);
        nftStore[tokenID] =  nftData({
            birthTime: block.timestamp,
            gender: _gender,
            motherId: 0,
            fatherId: 0,
            generation: 0,
            nextReserve: 0
        });
        
        breedStore[to].nftCount++;
        if(!status) {
            mintNftCount++;
        }
        genCreation++;
    }

    function mintWithPromotion(address to,uint256 id) external whenNotPaused onlyOwner {
        require(mintNftCount < maxNftLimit, "NFT Limit reached");

        safeMintWithRandomIndex(to,id,false);        
    }
    
    function giveBirth(uint256 maleId,uint256 femaleId) external payable whenNotPaused {
        address user = _msgSender();
        require(breedCount < maxBreedLimit, "Max Breed limit exceed");
        require((maleId % 2 == 0) && (femaleId % 2 == 1), "Male or Female id invalid");
        require(breedStore[user].breedCount < maximumBreedPerUser, "Max Breed per person Limit reached");
        require(breedStore[user].gestation, "Gestation failed");
        require(breedStore[user].gestationTime + DurationToGestation < block.timestamp, "Gestation time failed");
        require(ownerOf(maleId) == user && ownerOf(femaleId) == user, "callable address is invaild");
        require(readyToBreed(maleId) && readyToBreed(femaleId), "Not eligible for breed");
        require(bytes4(keccak256(abi.encodePacked(nftStore[maleId].gender))) == MALE, "Men NFT should only callable");
        require(bytes4(keccak256(abi.encodePacked(nftStore[femaleId].gender))) == FEMALE, "Female NFT should only callable");
        require(nftStore[maleId].generation == nftStore[femaleId].generation , "Breed is not proceed with parent");

        if(msg.sender != owner()){
            require(msg.value >= childPrice, "token Price is invalid");
            payable(feeAddress).sendValue(msg.value);
        }else {
            if(msg.value > 0){
                payable(feeAddress).sendValue(msg.value);
            }
        }
        
        uint256 tokenID = breedCount + maxNftLimit + 1;
        _safeMint(user, tokenID);
        emit MintToken(tokenID);
        
        setChild(tokenID);
        string memory _gender = ((tokenID % 2) == 0) ? "Male" : "Female";
        userTokens[msg.sender].push(tokenID);
        
        nftStore[tokenID] = nftData({
             birthTime: block.timestamp,
             gender: _gender,
             motherId: femaleId,
             fatherId: maleId,
             generation: nftStore[maleId].generation + 1,
             nextReserve: 0
        });
        
        breedStore[user] = userStore({
            breedCount: breedStore[user].breedCount + 1,
            nftCount : breedStore[user].nftCount + 1,
            gestationTime: block.timestamp,
            gestation: false
        });
        
        breedCount++;
        genCreation++;
    }
    
    function addPresaleUser(address[] memory users) public onlyOwner {
        for(uint256 i; i<users.length; i++){
            if(!presaleStore[users[i]]){
                presaleStore[users[i]] = true;
                totalPresaleUser++;
            }
        }
    }

    function removePresaleUser(address[] memory users) public onlyOwner {
        for(uint256 i; i<users.length; i++){
            if(presaleStore[users[i]]){
                presaleStore[users[i]] = false;
                totalPresaleUser--;
            }
        }
    }
    
   
    function enablePublicSale() public onlyOwner {
        publicSale = true;
    }
    
    function disablePublicSale() public onlyOwner {
        publicSale = false;
    }

    function getChainID() public view returns (uint256) {
        uint256 id;
        assembly {
            id := chainid()
        }
        return id;
    }

    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override(ERC721) {
        
        super.transferFrom(from, to, tokenId);
        
        uint len = userTokens[from].length;
        
        for (uint i = 0; i < len; i++) {
            if(userTokens[from][i] == tokenId) {
                userTokens[from][i] = userTokens[from][len- 1];
                userTokens[from].pop();
                breedStore[from].nftCount--;
                userTokens[to].push(tokenId);
                breedStore[to].nftCount++;
                break;
            }
        }
    }
    
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) public virtual override(ERC721) {
        super.safeTransferFrom(from, to, tokenId);
        
        uint len = userTokens[from].length;
        
        for (uint i = 0; i < len; i++) {
            if(userTokens[from][i] == tokenId) {
                userTokens[from][i] = userTokens[from][len- 1];
                userTokens[from].pop();
                breedStore[from].nftCount--;
                userTokens[to].push(tokenId);
                breedStore[to].nftCount++;
                break;
            }
        }
    }
    
    
    function _beforeTokenTransfer(address from, address to, uint256 tokenId)
        internal
        whenNotPaused
        override(ERC721, ERC721Enumerable)
    {
        super._beforeTokenTransfer(from, to, tokenId);
    }
    // The following functions are overrides required by Solidity.

    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }
    
    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }
}
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
    super._burn(tokenId);
}

// The following functions are overrides required by Solidity.