contract BearStaking is Ownable, IERC721Receiver{
  using SafeMath for uint256;

  bytes4 private constant _ERC721_RECEIVED = 0x150b7a02;

  //StakedBear Struct
  struct StakedBear{
    address owner;
    uint256 lastClaimed;
    bool isStaked;
    bool isEmergencyOut;
    uint256 emergencyOutTime;
  }

  //event 
  event RewardClaimed(uint256 tokenID, address owner);
  event Staked(address staker, uint256 tokenID, uint256 stakedTime);
  event UnStaked(uint256 tokenID, uint256 unstakedTime);

  // token_id => StakedBear
  mapping(uint256 => StakedBear) public stakedBear;
  uint256[] public bears;

  //NFT address
  address public bearNFT = 0x838E240c0FcBd12fBfFdB327d35A32DD2E73C7ea;
  //ERC20 address
  address public honey = 0x4F0c5042A11830fEaBA227840402e2409b0FC9dC;

  //reward discription 
  uint256 public rate = 10 * 10**18;
  //reward interval
  uint256 public rewardPeroid = 1 days;

  // reward = delta * rate / peroid
  function calculateReward(uint256 _lastClaimed, uint256 _rewardEndTime) view internal returns(uint256){
    return _rewardEndTime.sub(_lastClaimed).mul(rate).div(rewardPeroid); 
  }

  function batchStakeNFT(uint256[] memory _tokenIds) external {
      for(uint256 i=0;i<_tokenIds.length;i++){
          stakeNFT(_tokenIds[i]);
      }
  }  
  function stakeNFT(uint256 _tokenId) public {
    require(!stakedBear[_tokenId].isStaked, "Staking: NFT already on staked!");
    require(IERC721(bearNFT).ownerOf(_tokenId)==msg.sender, "Staking: Not the NFT owner!");
    require(
        address(this) == IERC721(bearNFT).getApproved(_tokenId) || IERC721(bearNFT).isApprovedForAll(msg.sender, address(this)), 
        "Staking: NFT not approved for the staking address OR Staking not set as operator!"
    );
    IERC721(bearNFT).safeTransferFrom(msg.sender, address(this), _tokenId, " ");
    _stakeNFT(_tokenId);
  }

  function batchUnstakeNFT(uint256[] memory _tokenIds) external {
      for(uint256 i=0;i<_tokenIds.length;i++){
          unstakeNFT(_tokenIds[i]);
      }
  }  
  function unstakeNFT(uint256 _tokenId) public {
    require(stakedBear[_tokenId].isStaked, "Staking: NFT not on staked!");
    IERC721(bearNFT).safeTransferFrom(address(this), msg.sender, _tokenId, " ");
    claimReward(_tokenId);
    _unstakeNFT(_tokenId);
  }

  function batchUnstakeWithoutRewardNFT(uint256[] memory _tokenIds) external{
      for(uint256 i=0;i<_tokenIds.length; i++){
          unstakeWithoutRewardNFT(_tokenIds[i]);
      }
  }
  function unstakeWithoutRewardNFT(uint256 _tokenId) public {
    stakedBear[_tokenId].emergencyOutTime = block.timestamp;
    stakedBear[_tokenId].isEmergencyOut = true;
    require(stakedBear[_tokenId].isStaked, "Staking: NFT not on staked!");
    IERC721(bearNFT).safeTransferFrom(address(this), msg.sender, _tokenId, " ");
    _unstakeNFT(_tokenId);
  }

  function _stakeNFT(uint256 _tokenId) internal{
    StakedBear storage bear = stakedBear[_tokenId];
    require(!bear.isStaked, "Staking: NFT already in staking!");
    bear.isStaked = true;
    bear.owner = msg.sender;
    bear.lastClaimed = block.timestamp;
    bear.isEmergencyOut = false;
    //add bear
    bears.push(_tokenId);
    emit Staked(msg.sender, _tokenId, block.timestamp);
  }

  function _unstakeNFT(uint256 _tokenId) internal {
    StakedBear storage bear = stakedBear[_tokenId];
    require(bear.isStaked, "Staking: NFT not staked!");
    require(bear.owner == msg.sender, "Staking: Not the owner of NFT!");
    bear.isStaked = false;

    emit UnStaked(_tokenId, block.timestamp);
    //remove from bears.
    for(uint256 i=0; i<bears.length; i++){
        if(bears[i] == _tokenId){
            bears[i] = bears[bears.length-1];
            bears.pop();
            return;
        }
    }
  }

  function claimReward(uint256 _tokenId) public {
    StakedBear storage bear = stakedBear[_tokenId];
    require(bear.owner != address(0), "Staking: Bear not in staking!"); 
    require(bear.owner == msg.sender, "Staking: Not the owner of NFT!"); 
    uint256 reward = 0;
    if(bear.isEmergencyOut){
        require(bear.emergencyOutTime > bear.lastClaimed, "Staking: Already Claimed Reward!");
        reward = calculateReward(bear.lastClaimed, bear.emergencyOutTime);

    }else{
        require(bear.isStaked, "Staking: Not in staking!");
        reward = calculateReward(bear.lastClaimed, block.timestamp);
    }
    bear.lastClaimed = block.timestamp;
    //transfer honey
    IERC20(honey).transfer(msg.sender, reward);
    emit RewardClaimed(_tokenId, msg.sender);
  }

  function claimBatchReward(uint256[] memory _tokenIds) external{
    for(uint i=0; i<_tokenIds.length; i++){
      claimReward(_tokenIds[i]);
    }
  }

  function getStakedBear(address wallet)  public view returns(uint256[] memory){
      uint256[] memory stakedBears = new uint256[](bears.length);
      uint256 count=0;
      for(uint256 i=0; i<bears.length; i++){
        StakedBear memory _bear = stakedBear[bears[i]];
        if(_bear.owner==wallet && _bear.isStaked){
            stakedBears[count] = bears[i];
            count++;
        }
      }
      uint256[] memory userStakedBears = new uint256[](count);
      for(uint256 i=0; i<count; i++){
          userStakedBears[i] = stakedBears[i];
      }
      return userStakedBears;
  }

  function getClaimableToken(address wallet) external view returns(uint256){
      uint256[] memory stakedBears = getStakedBear(wallet);
      uint256 reward = 0;
      for(uint256 i=0; i<stakedBears.length; i++){
          if(stakedBear[stakedBears[i]].isEmergencyOut)
            reward += calculateReward(stakedBear[stakedBears[i]].lastClaimed, stakedBear[stakedBears[i]].emergencyOutTime);
          else
            reward += calculateReward(stakedBear[stakedBears[i]].lastClaimed, block.timestamp);
      }
      return reward;
  }

  function getTotalStakedBears() external view returns(uint256){
      return bears.length;
  }
  
  //----------- Rescue Ether and tokens ------------//
  function withdraw() external payable onlyOwner {
    (bool os, ) = payable(owner()).call{value: address(this).balance}("");
    require(os);
  }

  function withdrawToken(uint256 amount) external onlyOwner{
    IERC20(honey).transfer(msg.sender, amount);
  }


  //------ Callbacks -------------------//
  /*
  Handle the ERC721 recieve.
  */
  function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata data
    )
        public override returns(bytes4)
    {
        return _ERC721_RECEIVED;
    }


  //ERC721 support 
  function supportsInterface(bytes4 interfaceID) external returns (bool){
    return interfaceID == _ERC721_RECEIVED ;
  }
}
function withdraw() external payable onlyOwner {
  (bool os, ) = payable(owner()).call{value: address(this).balance}("");
  require(os);
}

//----------- Rescue Ether and tokens ------------//