contract BunnyStakingContract is Ownable, ReentrancyGuard, IERC721Receiver {
    using SafeMath for uint256;
    using SafeERC20 for IPillToken;
    using EnumerableSet for EnumerableSet.UintSet; 

    // Info of each user.
    struct UserInfo {
      uint256 amount;
      uint256 harvestedReward;      
      mapping(uint256 => AppliedItem) item;
      EnumerableSet.UintSet nftIds;
      mapping(uint256 => uint256) lastRewardBlock;
    }

    struct AppliedItem {
      uint256 endBlock;
      uint256 itemId;
    }

    struct FadeAwayBunny {
        bytes32 genes;
        uint64 birthTime;
        uint32 matronId;
        uint32 sireId;
        uint16 generation;
    }

    IPillToken public rewardToken;
    FadeAwayBunnyNFT public nftAddress;
    ERC1155 public itemAddress;
    mapping(address => UserInfo) userInfo;
    mapping(uint256 => uint16) public itemRate; // same with rewardRate, 10000 = 100%
    uint256 public finalRewardBlock; // The block number when token rewarding has to end.
    uint256 public rewardPerDay = 100 * 1e18; // 100 PILL per day
    uint256 public itemEffectBlock = 50400; // 7 days with 12s each block
    uint256 public blockPerDay = 7200; // 1 day with 12s each block
    address burnAddress = 0x000000000000000000000000000000000000dEaD;

    event Deposit(address indexed user, uint256 indexed nftId, uint256 indexed itemId);
    event Withdraw(address indexed user, uint256 indexed nftId);
    event Harvest(address indexed user, uint256 indexed nftId, uint256 amount);
    event ApplyItem(address indexed user, uint256 indexed nftId, uint256 indexed itemId);

    constructor(
        IPillToken _rewardToken,        
        FadeAwayBunnyNFT _nftAddress,
        uint256 _finalRewardBlock
    ) {        
        rewardToken = _rewardToken;
        nftAddress = _nftAddress;
        finalRewardBlock = _finalRewardBlock;
    }

    // Update item effect block by the owner
    function setItemEffectBlock(uint256 _itemEffectBlock) public onlyOwner {
      itemEffectBlock = _itemEffectBlock;
    }

    // Update reward rate by the owner
    function setRewardPerDay(uint256 _rewardPerDay) public onlyOwner {
      rewardPerDay = _rewardPerDay;
    }

    // Update final reward block by the owner
    function setFinalRewardBlock(uint256 _finalRewardBlock) public onlyOwner {
      finalRewardBlock = _finalRewardBlock;
    }

    function getUserInfo(address _user) external view returns (uint256, uint256) {
        UserInfo storage user = userInfo[_user];

        return (user.amount, user.harvestedReward);
    }

    function getApliedItemInfo(address _user, uint256 _tokenId ) external view returns (AppliedItem memory) {
        UserInfo storage user = userInfo[_user];
        return user.item[_tokenId];
    }

     //check deposited nft. 
    function depositsOf(address _user)
      external 
      view 
      returns (uint256[] memory)
    {
      UserInfo storage user = userInfo[_user];
      EnumerableSet.UintSet storage depositSet = user.nftIds;
      uint256[] memory tokenIds = new uint256[] (depositSet.length());

      for (uint256 i; i < depositSet.length(); i++) {
        tokenIds[i] = depositSet.at(i);
      }

      return tokenIds;
    }

    function deposit(uint256 _nftId, uint256 _itemId) public {
      UserInfo storage user = userInfo[msg.sender];

      nftAddress.safeTransferFrom(address(msg.sender), address(this), _nftId);
      user.amount = user.amount.add(1);
      user.lastRewardBlock[_nftId] = block.number;      
      user.nftIds.add(_nftId);

      if (_itemId != 0) {
        applyItem(_nftId, _itemId);
      }
    }

    function batchDeposit(uint256[] memory _nftIds) public nonReentrant {
        uint256 i;
        for (i = 0; i < _nftIds.length; i++) {
            deposit(_nftIds[i], 0);
        }
    }

    function viewNftRate(uint256 _nftId) public view returns (uint16) {
      (bytes32 genes, , , , uint16 generation) = nftAddress.bunnies(_nftId);

      if (generation == 0) {
        return 10000;
      }

      bytes2[2] memory earnRate = [bytes2(0), 0];
      assembly {
        mstore(earnRate, genes)
        mstore(add(earnRate, 2), genes)
        }
      uint16 earnRateInt = uint16(earnRate[1]);

      if (earnRateInt < 180) {
        return 10000;
      } else if (earnRateInt < 231) {
        return 15000;
      } else {
        return 20000;
      }
    }

    function isUserStakedNft(address _user, uint256 _nftId) public view returns (bool) {
      UserInfo storage user = userInfo[_user];
      
      return user.nftIds.contains(_nftId);
    }

    function viewReward(address _user, uint256 _nftId) public view returns (uint256) {
      UserInfo storage user = userInfo[_user];
      uint16 nftRate = viewNftRate(_nftId);
      uint256 maxBlock;

      if (block.number > finalRewardBlock) {
        maxBlock = finalRewardBlock;
      } else {
        maxBlock = block.number;
      }

      if (user.lastRewardBlock[_nftId] >= maxBlock) {
        return 0;
      }

      if (user.item[_nftId].itemId != 0 && user.lastRewardBlock[_nftId] <= user.item[_nftId].endBlock) {
        if (maxBlock <= user.item[_nftId].endBlock) {
          return rewardPerDay.mul(maxBlock - user.lastRewardBlock[_nftId]).mul(itemRate[user.item[_nftId].itemId]).mul(nftRate).div(1e8).div(blockPerDay);
        } else {
          uint256 itemPeriod = user.item[_nftId].endBlock - user.lastRewardBlock[_nftId];
          uint256 normalPeriod = maxBlock - user.item[_nftId].endBlock;
          uint256 tmpItemRate = itemRate[user.item[_nftId].itemId];
          uint256 itemPeriodReward = rewardPerDay.mul(itemPeriod).mul(tmpItemRate).mul(nftRate).div(1e8).div(blockPerDay);
          uint256 normalPeriodReward = rewardPerDay.mul(normalPeriod).mul(nftRate).div(10000).div(blockPerDay);
          return itemPeriodReward + normalPeriodReward;
        }
      } else {
        return rewardPerDay.mul(maxBlock - user.lastRewardBlock[_nftId]).mul(nftRate).div(10000).div(blockPerDay);
      }
    }

    function harvest(uint256 _nftId) public {
      require(isUserStakedNft(msg.sender, _nftId), "harvest:: this nft is not yours");
      UserInfo storage user = userInfo[msg.sender];
      uint256 reward = viewReward(msg.sender, _nftId);

      if (reward == 0) {
        return;
      }

      user.lastRewardBlock[_nftId] = block.number;
      user.harvestedReward = user.harvestedReward + reward;
      rewardToken.mint(msg.sender, reward);

      emit Harvest(msg.sender, _nftId, reward);
    }

    function harvestAll() public nonReentrant {
      UserInfo storage user = userInfo[msg.sender];
      EnumerableSet.UintSet storage depositSet = user.nftIds;

      for (uint256 i; i < depositSet.length(); i++) {
        harvest(depositSet.at(i));
      }
    }

    function batchHarvest(uint256[] memory _nftIds) public nonReentrant {
        uint256 i;
        for (i = 0; i < _nftIds.length; i++) {
            harvest(_nftIds[i]);
        }
    }

    function batchWithdraw(uint256[] memory _nftIds) public nonReentrant {
        uint256 i;
        for (i = 0; i < _nftIds.length; i++) {
            withdraw(_nftIds[i]);
        }
    }

    function withdraw(uint256 _nftId) public {
      require(isUserStakedNft(msg.sender, _nftId), "withdraw:: this nft is not yours");
      UserInfo storage user = userInfo[msg.sender];
      harvest(_nftId);
      user.amount = user.amount.sub(1);
      nftAddress.safeTransferFrom(address(this), address(msg.sender), _nftId);
      user.nftIds.remove(_nftId);   
      emit Withdraw(msg.sender, _nftId);
    }

    function applyItem(uint256 _nftId, uint256 _itemId) public nonReentrant {
      require(isUserStakedNft(msg.sender, _nftId), "applyItem:: this nft is not yours!");
      require(itemAddress.balanceOf(msg.sender, _itemId) > 0, "applyItem:: you dont own the item!");
      UserInfo storage user = userInfo[msg.sender];
      require(block.number >= user.item[_nftId].endBlock, "applyItem:: only 1 ecstasy can be used at a time!");
      
      harvest(_nftId);
      itemAddress.safeTransferFrom(address(msg.sender), burnAddress, _itemId, 1, "");
      user.item[_nftId].endBlock = block.number + itemEffectBlock;
      user.item[_nftId].itemId = _itemId;
      emit ApplyItem(msg.sender, _nftId, _itemId);
    }

    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external pure override returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }
}
function depositsOf(address _user)
  external 
  view 
  returns (uint256[] memory)
{
  UserInfo storage user = userInfo[_user];
  EnumerableSet.UintSet storage depositSet = user.nftIds;
  uint256[] memory tokenIds = new uint256[] (depositSet.length());

  for (uint256 i; i < depositSet.length(); i++) {
    tokenIds[i] = depositSet.at(i);
  }

  return tokenIds;
}

//check deposited nft. 