contract KOBOL_STAKING is ReentrancyGuard, Pausable, Ownable, IERC721Receiver {
    using Counters for Counters.Counter;
    using Strings for uint256;
    using SafeMath for uint256;
    using EnumerableSet for EnumerableSet.UintSet;
    using SafeERC20 for IERC20;
    /* ------------------------ NFT Staking ------------------------- */
    bool public _enableHarvest = false;

    bytes32 private HASH_ROOT;

    address public NFT_TOKEN_ADDRESS;
    address public REWARD_TOKEN_ADDRESS;

    uint256 public TOKEN_REWARD_PER_DAY_MYSTIC = 2904.30321 ether;
    uint256 public TOKEN_REWARD_PER_DAY_LEGUNDARY = 1936.20214 ether;
    uint256 public TOKEN_REWARD_PER_DAY_EPIC = 193.62021 ether;
    uint256 public TOKEN_REWARD_PER_DAY_RARE = 48.40505 ether;
    
    struct UserInfo {
        uint256 rewards;
        uint256 lastUpdated;
    }

    mapping(address => EnumerableSet.UintSet) private Mystic_Blanaces;
    mapping(address => EnumerableSet.UintSet) private Legundary_Blanaces;
    mapping(address => EnumerableSet.UintSet) private Epic_Blanaces;
    mapping(address => EnumerableSet.UintSet) private Rare_Blanaces;

    mapping(address => UserInfo) public userInfo;

    address[] public stakerList;

    /* --------------------------------------------------------------------------------- */
    
    function setEnableHavest (bool _bEnabled) external onlyOwner {
        _enableHarvest = _bEnabled;
    }

    function withdraw() external onlyOwner {
        uint256 balance = address(this).balance;
        payable(_msgSender()).transfer(balance);
    }

    function withdrawToken() external onlyOwner {
        IERC20(REWARD_TOKEN_ADDRESS).safeTransfer(_msgSender(), IERC20(REWARD_TOKEN_ADDRESS).balanceOf(address(this)));
    }

    function userHoldNFT(address _owner) public view returns(uint256[] memory){
        uint256 tokenCount = IERC721(NFT_TOKEN_ADDRESS).balanceOf(_owner);
        if (tokenCount == 0) {
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = IERC721Enumerable(NFT_TOKEN_ADDRESS).tokenOfOwnerByIndex(_owner, index);
            }
            return result;
        }
    }

    /* --------------------------------------------------------------------- */
    function setNFTAddress(address _nftAddress) public onlyOwner {
        NFT_TOKEN_ADDRESS = _nftAddress;
    }
    function setTokenRewardAddress(address _tokenAddress) public onlyOwner {
        REWARD_TOKEN_ADDRESS = _tokenAddress;
    }

    function setTokenRewardAmountPerDay(uint256 mysticReward, uint256 legundaryReward, uint256 epicReward, uint256 rareReward) public onlyOwner {
        TOKEN_REWARD_PER_DAY_MYSTIC = mysticReward;
        TOKEN_REWARD_PER_DAY_LEGUNDARY = legundaryReward;
        TOKEN_REWARD_PER_DAY_EPIC = epicReward;
        TOKEN_REWARD_PER_DAY_RARE = rareReward;
    }

    function setHashRoot(bytes32 _root) public onlyOwner {
        HASH_ROOT = _root;
    }
    // Verify that a given leaf is in the tree.
    function isHashValid(bytes32 _leafNode, bytes32[] memory _proof) public view returns (bool) {
        return MerkleProof.verify(_proof, HASH_ROOT, _leafNode);
    }

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

    function addStakerList(address _user) internal{
        for (uint256 i = 0; i < stakerList.length; i++) {
            if (stakerList[i] == _user)
                return;
        }
        stakerList.push(_user);
    }

    function userStakeInfo(address _owner) external view returns(UserInfo memory){
         return userInfo[_owner];
    }
    
    function userStakedNFT_Mystic(address _owner) public view returns(uint256[] memory){
        uint256 tokenCount = Mystic_Blanaces[_owner].length();
        if (tokenCount == 0) {
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = Mystic_Blanaces[_owner].at(index);
            }
            return result;
        }
    }

    function userStakedNFT_Legundary(address _owner) public view returns(uint256[] memory){
        uint256 tokenCount = Legundary_Blanaces[_owner].length();
        if (tokenCount == 0) {
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = Legundary_Blanaces[_owner].at(index);
            }
            return result;
        }
    }

    function userStakedNFT_Epic(address _owner) public view returns(uint256[] memory){
        uint256 tokenCount = Epic_Blanaces[_owner].length();
        if (tokenCount == 0) {
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = Epic_Blanaces[_owner].at(index);
            }
            return result;
        }
    }

    function userStakedNFT_Rare(address _owner) public view returns(uint256[] memory){
        uint256 tokenCount = Rare_Blanaces[_owner].length();
        if (tokenCount == 0) {
            return new uint256[](0);
        } else {
            uint256[] memory result = new uint256[](tokenCount);
            uint256 index;
            for (index = 0; index < tokenCount; index++) {
                result[index] = Rare_Blanaces[_owner].at(index);
            }
            return result;
        }
    }

    function isStaked_Mystic( address account ,uint256 tokenId) public view returns (bool) {
        return Mystic_Blanaces[account].contains(tokenId);
    }
    function isStaked_Legundary( address account ,uint256 tokenId) public view returns (bool) {
        return Legundary_Blanaces[account].contains(tokenId);
    }
    function isStaked_Epic( address account ,uint256 tokenId) public view returns (bool) {
        return Epic_Blanaces[account].contains(tokenId);
    }
    function isStaked_Rare( address account ,uint256 tokenId) public view returns (bool) {
        return Rare_Blanaces[account].contains(tokenId);
    }

    function earned(address account) public view returns (uint256) {
        uint256 blockTime = block.timestamp;

        UserInfo memory user = userInfo[account];

        uint256 amount_mystic = blockTime.sub(user.lastUpdated).mul(Mystic_Blanaces[account].length()).mul(TOKEN_REWARD_PER_DAY_MYSTIC).div(1 days);
        uint256 amount_legundary = blockTime.sub(user.lastUpdated).mul(Legundary_Blanaces[account].length()).mul(TOKEN_REWARD_PER_DAY_LEGUNDARY).div(1 days);
        uint256 amount_epic = blockTime.sub(user.lastUpdated).mul(Epic_Blanaces[account].length()).mul(TOKEN_REWARD_PER_DAY_EPIC).div(1 days);
        uint256 amount_rare = blockTime.sub(user.lastUpdated).mul(Rare_Blanaces[account].length()).mul(TOKEN_REWARD_PER_DAY_RARE).div(1 days);

        return user.rewards.add(amount_mystic).add(amount_legundary).add(amount_epic).add(amount_rare);
    }

    function totalEarned() public view returns (uint256) {
        uint256 totalEarning = 0;
        for (uint256 i = 0; i < stakerList.length; i++) {
            totalEarning += earned(stakerList[i]);
        }
        return totalEarning;
    }

    function totalStakedCount_Mystic() public view returns (uint256) {
        uint256 totalCount = 0;
        for (uint256 i = 0; i < stakerList.length; i++) {
            totalCount += Mystic_Blanaces[stakerList[i]].length();
        }
        return totalCount;
    }

    function totalStakedCount_Legundary() public view returns (uint256) {
        uint256 totalCount = 0;
        for (uint256 i = 0; i < stakerList.length; i++) {
            totalCount += Legundary_Blanaces[stakerList[i]].length();
        }
        return totalCount;
    }

    function totalStakedCount_Epic() public view returns (uint256) {
        uint256 totalCount = 0;
        for (uint256 i = 0; i < stakerList.length; i++) {
            totalCount += Epic_Blanaces[stakerList[i]].length();
        }
        return totalCount;
    }

    function totalStakedCount_Rare() public view returns (uint256) {
        uint256 totalCount = 0;
        for (uint256 i = 0; i < stakerList.length; i++) {
            totalCount += Rare_Blanaces[stakerList[i]].length();
        }
        return totalCount;
    }

    function totalStakedMembers_Mystic() public view returns (uint256) {
        uint256 totalMembers = 0;
        for (uint256 i = 0; i < stakerList.length; i++) {
            if (Mystic_Blanaces[stakerList[i]].length() > 0) totalMembers += 1;
        }
        return totalMembers;
    }

    function totalStakedMembers_Legundary() public view returns (uint256) {
        uint256 totalMembers = 0;
        for (uint256 i = 0; i < stakerList.length; i++) {
            if (Legundary_Blanaces[stakerList[i]].length() > 0) totalMembers += 1;
        }
        return totalMembers;
    }

    function totalStakedMembers_Epic() public view returns (uint256) {
        uint256 totalMembers = 0;
        for (uint256 i = 0; i < stakerList.length; i++) {
            if (Epic_Blanaces[stakerList[i]].length() > 0) totalMembers += 1;
        }
        return totalMembers;
    }

    function totalStakedMembers_Rare() public view returns (uint256) {
        uint256 totalMembers = 0;
        for (uint256 i = 0; i < stakerList.length; i++) {
            if (Rare_Blanaces[stakerList[i]].length() > 0) totalMembers += 1;
        }
        return totalMembers;
    }

    
    function stake( uint256[] calldata _tokenIDList, uint256[] calldata _indexList, uint256[] calldata _gradeList, bytes32[][] calldata _proofList) public nonReentrant whenNotPaused {
        require(IERC721(NFT_TOKEN_ADDRESS).isApprovedForAll(_msgSender(),address(this)),"Not approve nft to staker address");

        addStakerList(_msgSender());

        UserInfo storage user = userInfo[_msgSender()];
        user.rewards = earned(_msgSender());
        user.lastUpdated = block.timestamp;

        for (uint256 i = 0; i < _tokenIDList.length; i++) {
            require (isHashValid(toLeaf(_tokenIDList[i], _indexList[i], _gradeList[i]), _proofList[i]), "Invalid Hash");
            
            IERC721(NFT_TOKEN_ADDRESS).safeTransferFrom(_msgSender(), address(this), _tokenIDList[i]);
            
            if (_gradeList[i] == 0) {

                Mystic_Blanaces[_msgSender()].add(_tokenIDList[i]);
            
            } else if (_gradeList[i] == 1) {
                
                Legundary_Blanaces[_msgSender()].add(_tokenIDList[i]);
            
            } else if (_gradeList[i] == 2) {
                
                Epic_Blanaces[_msgSender()].add(_tokenIDList[i]);
            
            } else if (_gradeList[i] == 3) {
                
                Rare_Blanaces[_msgSender()].add(_tokenIDList[i]);
            
            }
        }
    }

    function unstake( uint256[] memory  tokenIdList) public nonReentrant {
        UserInfo storage user = userInfo[_msgSender()];
        user.rewards = earned(_msgSender());
        user.lastUpdated = block.timestamp;

        for (uint256 i = 0; i < tokenIdList.length; i++) {

            require(
                isStaked_Mystic(_msgSender(), tokenIdList[i]) || 
                isStaked_Legundary(_msgSender(), tokenIdList[i]) || 
                isStaked_Epic(_msgSender(), tokenIdList[i]) || 
                isStaked_Rare(_msgSender(), tokenIdList[i]), "Not staked this nft");        

            IERC721(NFT_TOKEN_ADDRESS).safeTransferFrom(address(this) , _msgSender(), tokenIdList[i], "");

            if (isStaked_Mystic(_msgSender(), tokenIdList[i])) {
                
                Mystic_Blanaces[_msgSender()].remove(tokenIdList[i]);
            
            } else if (isStaked_Legundary(_msgSender(), tokenIdList[i])) {
                
                Legundary_Blanaces[_msgSender()].remove(tokenIdList[i]);
            
            } else if (isStaked_Epic(_msgSender(), tokenIdList[i])) {
                
                Epic_Blanaces[_msgSender()].remove(tokenIdList[i]);
            
            } else if (isStaked_Rare(_msgSender(), tokenIdList[i])) {
                
                Rare_Blanaces[_msgSender()].remove(tokenIdList[i]);
            
            }
        }
    }

    function harvest() public nonReentrant {
        require(_enableHarvest == true, "Harvest is not activated");
        
        UserInfo storage user = userInfo[_msgSender()];
        user.rewards = earned(_msgSender());
        user.lastUpdated = block.timestamp;

        require(IERC20(REWARD_TOKEN_ADDRESS).balanceOf(address(this)) >= user.rewards,"Reward token amount is small");

        if (user.rewards > 0) {
            IERC20(REWARD_TOKEN_ADDRESS).safeTransfer(_msgSender(), user.rewards);
        }

        user.rewards = 0;
    }

    function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) {
        return this.onERC721Received.selector;
    }

    receive() external payable {}
}
function setNFTAddress(address _nftAddress) public onlyOwner {
    NFT_TOKEN_ADDRESS = _nftAddress;
}

/* --------------------------------------------------------------------- */