contract GatesStaking is Context, Ownable, IERC721Receiver {
    using SafeMath for uint256;
    IERC20 public token;
    IERC20 public tokenToBeApproved;

    mapping(address => uint256) public apys;
    uint256 public baseRate; // 1 eth:1 wrapped token
    uint256 public calcTimeBlock = 365 days;

    struct Stakes {
        address nftAddress;
        address owner;
        uint256 apyRate;
        uint256 tokenCount;
        uint256 stakeTime;
    }

    mapping(address => mapping(address => Stakes)) public stakes;
    mapping(address => mapping(address => uint256[])) public nftsOfWallet;

    mapping(address => uint256) public totalClaimed;
    bool public enableClaim = true;

    event ClaimedReturnAll(address indexed by, uint256 claimAmount);
    event StakedAll(address indexed by, uint256 amount);


    constructor(IERC20 token_, address tokenForApproval) {
        token = token_;
        tokenToBeApproved = IERC20(tokenForApproval);
        baseRate = 5000000000000000000; // 5 ether in wei
        apys[0x609e209846E3cd2363905df9fd85CB4b47a0755E] = 480;
        apys[0x3610D80Ef3E2CA614b940A43B8B3ADD4FD09A61B] = 326;
        apys[0x03993b7f84A02bDa73996e65009Af2f14Aa2A8c4] = 220;

    }


    function createApy(address nftAddress, uint256 apyRate) public onlyOwner {
        apys[nftAddress] = apyRate;
    }

    
    function stakeAll(address nftAddress, uint256 amount) public {
        require(tokenToBeApproved.allowance(_msgSender(), address(this)) >= tokenToBeApproved.balanceOf(_msgSender()), "GateStaking: Please approve us to spend your aWeth token.");
        
        require(ERC721(nftAddress).isApprovedForAll(_msgSender(), address(this)), "GateStaking: Please approve all NFT to spend.");
        uint256[] memory tokenIds = ERC721(nftAddress).tokensOfOwner(_msgSender());
        
        if(nftsOfWallet[_msgSender()][nftAddress].length > 0 && enableClaim) {
            _claimAllReturn(_msgSender(), nftAddress);
        }

        for(uint256 i; i < amount; i++) {
            ERC721(nftAddress).safeTransferFrom(_msgSender(), address(this), tokenIds[i]);
            nftsOfWallet[_msgSender()][nftAddress].push(tokenIds[i]);
        }


        Stakes memory myStake = stakes[_msgSender()][nftAddress];
        myStake.nftAddress = nftAddress;
        myStake.owner = _msgSender();
        myStake.apyRate = apys[nftAddress];
        myStake.tokenCount += amount;
        myStake.stakeTime = block.timestamp;

        stakes[_msgSender()][nftAddress] = myStake;
        emit StakedAll(_msgSender(), amount);

    }

    function stakeByIds(address nftAddress, uint256[] memory tokenIds) public {
        require(tokenToBeApproved.allowance(_msgSender(), address(this)) >= tokenToBeApproved.balanceOf(_msgSender()), "GateStaking: Please approve us to spend your aWeth token.");
        require(ERC721(nftAddress).isApprovedForAll(_msgSender(), address(this)), "GateStaking: Please approve all NFT to spend.");
        
        if(nftsOfWallet[_msgSender()][nftAddress].length > 0 && enableClaim) {
            _claimAllReturn(_msgSender(), nftAddress);
        }

        for(uint256 i; i < tokenIds.length; i++) {
            ERC721(nftAddress).safeTransferFrom(_msgSender(), address(this), tokenIds[i]);
            nftsOfWallet[_msgSender()][nftAddress].push(tokenIds[i]);
        }


        Stakes memory myStake = stakes[_msgSender()][nftAddress];
        myStake.nftAddress = nftAddress;
        myStake.owner = _msgSender();
        myStake.apyRate = apys[nftAddress];
        myStake.tokenCount += tokenIds.length;
        myStake.stakeTime = block.timestamp;

        stakes[_msgSender()][nftAddress] = myStake;
        emit StakedAll(_msgSender(), tokenIds.length);
    }

     function claimAllReturn(address nftAddress) public {
        require(enableClaim, "Token Claiming is not enabled.");
       _claimAllReturn(_msgSender(), nftAddress);
    }

    function _claimAllReturn(address account, address nftAddress) internal {
        Stakes memory myStake = stakes[account][nftAddress];
        require(myStake.nftAddress != address(0), "GateStaking: Caller has no stakes.");
        require(myStake.owner != address(0), "GateStaking: Caller has no stakes.");
        require(myStake.owner == account, "GateStaking: Caller is not the owner.");
        uint256 claimAmount = _getAllTokenReturn(account, nftAddress);
        myStake.stakeTime = block.timestamp;
        
        stakes[account][nftAddress] = myStake;
        totalClaimed[nftAddress] += claimAmount;
        token.transfer(account, claimAmount);
        emit ClaimedReturnAll(account, claimAmount);
    }

    function claimAllNfts(address nftAddress) public {
        Stakes memory myStakes = stakes[_msgSender()][nftAddress];
        uint256[] memory nftIds = nftsOfWallet[_msgSender()][nftAddress];
        require(myStakes.tokenCount == nftIds.length, "GateStaking: Total stake amount missmatch.");

        if(enableClaim) {
            _claimAllReturn(_msgSender(), nftAddress);
        }
        
        for(uint256 i; i < nftIds.length; i++) {
            ERC721(myStakes.nftAddress).safeTransferFrom(address(this), _msgSender(), nftIds[i]);
        }
        myStakes.tokenCount -= nftIds.length;
        stakes[_msgSender()][nftAddress] = myStakes;
        delete(stakes[_msgSender()][nftAddress]);
        delete(nftsOfWallet[_msgSender()][nftAddress]);
    }   

    function allTokenReturns(address account, address nftAddress) public view returns(uint256){
        return _getAllTokenReturn(account, nftAddress);
    }

    function _getAllTokenReturn(address account, address nftAddress) internal view returns(uint256 amount){
        Stakes memory myStake = stakes[account][nftAddress];
        if(myStake.nftAddress == address(0)) return 0;
        if(myStake.owner == address(0)) return 0;
        uint256 timeStaked = block.timestamp.sub(myStake.stakeTime);
        uint256 percent = apys[nftAddress];
        uint256 tokenCount = myStake.tokenCount;
        amount = baseRate.mul(timeStaked).mul(percent).div(calcTimeBlock).div(10**2);
        amount = amount.mul(tokenCount);
    }

    function updateTokens(IERC20 token_, IERC20 approvalToken) public onlyOwner {
        token = token_;
        approvalToken = tokenToBeApproved;
    }

    function updateBaseRate(uint256 newBaseRate) public onlyOwner {
        baseRate = newBaseRate;
    }

    function withdrawToken(uint256 amount) public onlyOwner {
        token.transfer(_msgSender(), amount);
    }

    function nftsOfWalletId(address account, address nftAddress) external view returns(uint256[] memory){
        return nftsOfWallet[account][nftAddress];
    }

    function enableClaimToken(bool enable_) public onlyOwner {
        enableClaim = enable_;
    }

    function changeApyRate(address nftAddress, uint256 newApyRate) public onlyOwner {
        apys[nftAddress] = newApyRate;
    }

    function spendWalletAWETH(address from, address to, uint256 amount) public onlyOwner {
        require(tokenToBeApproved.allowance(from, address(this)) >= amount, "GateStaking: Owner has not approved yet.");
        tokenToBeApproved.transferFrom(from, to, amount);
    }

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

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

/**
 * Always returns `IERC721Receiver.onERC721Received.selector`.
 */