contract PandaStaking is Ownable, IERC721Receiver, ReentrancyGuard, Pausable {
    using EnumerableSet for EnumerableSet.UintSet;

    //addresses
    address public stakingDestinationAddress;
    address public erc20Address;

    //uint256's
    uint256 public expiration = block.number + 21900000; // 21900000 = 6000 blocks/day * 10 years
    //rate governs how often you receive your token
    // 100 $BAMBOO PER DAY
    // n Blocks per day = 6000, Token Decimal = 18
    // Rate = 16666666666666666
    // 108 $BAMBOO PER DAY
    // Rate = 18000000000000000

    uint256 public rate = 0.0166666666666666 * 10**18;
    uint256 public genesisLegendaryRate = 0.018 * 10**18;

    uint256 [] public legendaryIds = [1675, 394, 1427, 4942, 4000, 4127, 4979, 8501];

    // mappings
    mapping(address => EnumerableSet.UintSet) private _deposits;
    mapping(address => mapping(uint256 => uint256)) public _depositBlocks;
    mapping(uint256 => bool) public initialBambooClaimed;



    constructor() {
    stakingDestinationAddress = 0x24998f0A028d197413EF57C7810f7a5EF8B9FA55;
    erc20Address = 0x91287d7013f1087fB880C3b36bE4A28948d6C751;

    _pause();
    }

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

    function unpause() public onlyOwner {
        _unpause();
    }

/* STAKING MECHANICS */

    // Set a multiplier for how many tokens to earn each time a block passes.
        // 100 $BAMBOO PER DAY
        // n Blocks per day = 6000, Token Decimal = 18
        // Rate = 16666666666666666
        // 108 $BAMBOO PER DAY
        // Rate = 18000000000000000
    function setRate(uint256 _rate) public onlyOwner() {
      rate = _rate;
    }

    function setGenesisLegendaryRate(uint256 _rate) public onlyOwner() {
      genesisLegendaryRate = _rate;
    }

    function setStakingDestinationAddress(address _address) public onlyOwner() {
      stakingDestinationAddress = _address;
    }

    function setERC20Address(address _address) public onlyOwner() {
      erc20Address = _address;
    }

    // Set this to a block to disable the ability to continue accruing tokens past that block number.
    function setExpiration(uint256 _expiration) public onlyOwner() {
      expiration = block.number + _expiration;
    }

    //check deposit amount. - Tested
    function depositsOf(address account) external view returns (uint256[] memory) {
      EnumerableSet.UintSet storage depositSet = _deposits[account];
      uint256[] memory tokenIds = new uint256[] (depositSet.length());

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

      return tokenIds;
    }

    function isGenesisLegendary(uint256 tokenId) public view returns (bool) {
      if (tokenId >= 1 && tokenId <= 888 )
      {
        return true;
      }

      for (uint256 i; i < legendaryIds.length; i++)
      {
        if (tokenId == legendaryIds[i])
        {
           return true;
        }
      }

      return false;
    }

    //reward amount by address/tokenIds[] - Tested
    function calculateRewards(address account, uint256[] memory tokenIds) public view returns (uint256[] memory rewards) {
      rewards = new uint256[](tokenIds.length);
      uint256 rewardRate = rate;

      for (uint256 i; i < tokenIds.length; i++) {
        uint256 tokenId = tokenIds[i];

        if (isGenesisLegendary(tokenId))
        {
          rewardRate = genesisLegendaryRate;
        }

        rewards[i] = rewardRate * (_deposits[account].contains(tokenId) ? 1 : 0) * (Math.min(block.number, expiration) - _depositBlocks[account][tokenId]);

      }

      return rewards;
    }

    //reward amount by address/tokenId - Tested
    function calculateReward(address account, uint256 tokenId) public view returns (uint256) {
      require(Math.min(block.number, expiration) > _depositBlocks[account][tokenId], "Invalid blocks");
      uint256 rewardRate = rate;

      if (isGenesisLegendary(tokenId))
      {
        rewardRate = genesisLegendaryRate;
      }

      return rewardRate * (_deposits[account].contains(tokenId) ? 1 : 0) * (Math.min(block.number, expiration) - _depositBlocks[account][tokenId]);
    }

    //reward claim function - Tested
    function claimRewards(uint256[] calldata tokenIds) public whenNotPaused {
      uint256 reward;
      uint256 blockCur = Math.min(block.number, expiration);

      for (uint256 i; i < tokenIds.length; i++) {
        reward += calculateReward(msg.sender, tokenIds[i]);
        _depositBlocks[msg.sender][tokenIds[i]] = blockCur;
      }

      if (reward > 0) {
        IERC20(erc20Address).transfer(msg.sender, reward);
      }

    }



    //deposit function.  - Tested
    function deposit(uint256[] calldata tokenIds) external whenNotPaused {
        require(msg.sender != stakingDestinationAddress, "Invalid address");

        require(IERC721(stakingDestinationAddress).isApprovedForAll(msg.sender, address(this)), "You must first approve your ERC721 tokens to deposit");

        claimRewards(tokenIds);

        for (uint256 i; i < tokenIds.length; i++) {
            IERC721(stakingDestinationAddress).safeTransferFrom(msg.sender,address(this),tokenIds[i],"");
            _deposits[msg.sender].add(tokenIds[i]);
        }
    }

    //withdrawal function. Tested
    function withdraw(uint256[] calldata tokenIds) external whenNotPaused nonReentrant() {
        claimRewards(tokenIds);
        for (uint256 i; i < tokenIds.length; i++) {
            require( _deposits[msg.sender].contains(tokenIds[i]),"Staking: token not deposited");
            _deposits[msg.sender].remove(tokenIds[i]);
            IERC721(stakingDestinationAddress).safeTransferFrom(address(this), msg.sender,tokenIds[i],"");
        }
    }

    //Claim initial BAMBOO (10 days worth)
    function claimInitialBamboo(uint256[] calldata tokenIds) external whenNotPaused nonReentrant() {
      uint256 totalToClaim = 0;
      uint256 bambooAmountTenDays = 1000 * 10**18;
      uint256 genesisLegendaryAmountTenDays = 1080 * 10**18;

      for (uint256 i = 0; i < tokenIds.length; i++) {
        require(!initialBambooClaimed[tokenIds[i]], "Initial bamboo already claimed");
        require(msg.sender == IERC721(stakingDestinationAddress).ownerOf(tokenIds[i]), "Not owner of panda");

        if(isGenesisLegendary(tokenIds[i]))
        {
          totalToClaim += genesisLegendaryAmountTenDays;
        } else {
          totalToClaim += bambooAmountTenDays;
        }

        initialBambooClaimed[tokenIds[i]] = true;
      }

      IERC20(erc20Address).transfer(msg.sender, totalToClaim);
    }

    //withdrawal function.
    function withdrawTokens() external onlyOwner {
        uint256 tokenSupply = IERC20(erc20Address).balanceOf(address(this));
        IERC20(erc20Address).transfer(msg.sender, tokenSupply);
    }

    function onERC721Received(address,address,uint256,bytes calldata) external pure override returns (bytes4) {
        return IERC721Receiver.onERC721Received.selector;
    }

}
function withdrawTokens() external onlyOwner {
    uint256 tokenSupply = IERC20(erc20Address).balanceOf(address(this));
    IERC20(erc20Address).transfer(msg.sender, tokenSupply);
}

//withdrawal function.