contract NFTStaking is IERC721Receiver, ERC20 {
    IERC721 internal parentNFT;
    ERC20 internal tokenContract;
    uint lockupTime = 3600*24; //3600 equals 1 hour of unix time. Lockup time set to 24 hours by default. 
    uint frequency = 3600*24; //earns 1 token per every 24 hours staked 
    uint oneOfOneMultiplier = 10;
    uint stakerCounter = 0;
    struct Stake {
        address smart_contract;
        uint tokenId;
        uint timestamp;
        address wallet;
        uint index;
    }
    Stake[] internal stakes;
    mapping(address => mapping(address => uint[])) internal getTokenIDsOfStakerPerContractAndIndex; //returns TokenID
    mapping(address => mapping(address => mapping(uint => Stake))) internal getStakeOfOwnerPerContractAndTokenID; //returns Stake record
        
    function addStaker(address smart_contract, uint _tokenId, uint _timestamp, address _wallet) internal {
        uint index = stakerCounter;
        stakerCounter += 1;
        Stake memory new_staker = Stake(smart_contract, _tokenId, _timestamp, _wallet, index);
        stakes.push(new_staker);
        getStakeOfOwnerPerContractAndTokenID[_wallet][smart_contract][_tokenId] = new_staker;
        getTokenIDsOfStakerPerContractAndIndex[_wallet][smart_contract].push(_tokenId);
    }

    function removeStaker(address smart_contract, address _wallet, uint _tokenId) internal {
        uint index = getIndexOfStake(smart_contract, _wallet, _tokenId);
        delete getStakeOfOwnerPerContractAndTokenID[_wallet][smart_contract][_tokenId];
        delete getTokenIDsOfStakerPerContractAndIndex[_wallet][smart_contract][index];
    }

    //this works as intended
    function getTokenIDsOfStaker(address smart_contract, address _wallet) public view returns ( uint[] memory ) {
        uint lengthWithoutZeros = countTokenArrayWithoutZeros(smart_contract, _wallet);
        uint lengthWithZeros = getTokenIDsOfStakerPerContractAndIndex[_wallet][smart_contract].length;
        uint[] memory tokenArray = new uint[](lengthWithoutZeros);
        uint j = 0;
        for (uint i=0; i<lengthWithZeros; i++) {
            if (getTokenIDsOfStakerPerContractAndIndex[_wallet][smart_contract][i] != 0) {
                tokenArray[j] = getTokenIDsOfStakerPerContractAndIndex[_wallet][smart_contract][i];
                j++;
            }
        }
            
    return tokenArray;
    }

    function getIndexOfStake(address smart_contract, address _wallet, uint _tokenId) internal view returns ( uint ) {
        uint index = getStakeOfOwnerPerContractAndTokenID[_wallet][smart_contract][_tokenId].index;
        return index;
    }

    function getNumberOfTotalStakes() public view returns (uint) {
        return stakes.length;
    }

    function countTokenArrayWithoutZeros(address smart_contract, address _wallet) internal view returns (uint) {
        uint oldLength = getTokenIDsOfStakerPerContractAndIndex[_wallet][smart_contract].length;
        uint newLength;
        uint tokenCounter;
        for (uint i=0; i<oldLength; i++) {
            if (getTokenIDsOfStakerPerContractAndIndex[_wallet][smart_contract][i] != 0) {
                tokenCounter += 1;
            }   
        }
        newLength = tokenCounter;
        return newLength;
    }

    function numberOfStakedTokens(address smart_contract) public view returns(uint) {
        uint _numberOfStakedTokens = ERC721(smart_contract).balanceOf(address(this));
        return _numberOfStakedTokens; 
    } 


    // map staker to total staking time 
    mapping(address => uint) public stakingTime; 
    mapping(address => uint) public claimedTime;     

    mapping(address => uint) private _balances;

    constructor(address _tokenContract) {
        tokenContract = ERC20(_tokenContract); //Token Contract
    }

    //@dev pass in contract address and array of associated tokenIDs you want to stake. 
        // *this contract must be approved by the message sender prior to the function being called or it will revert*
        // create stake record
        // transfer NFT to this address

    function stake(address smart_contract, uint[] calldata _tokenIds) public {
        require(isStakeable(smart_contract) == true, "This NFT is not stakeable at this time.");
        uint length = _tokenIds.length;
        parentNFT = ERC721(smart_contract);
        unchecked {
            for (uint i=0; i<length; i++) {
                uint _tokenId = _tokenIds[i]; //sets token ID value from input array
                require(parentNFT.ownerOf(_tokenId) == msg.sender, "You don't own one or more of the NFTs you're trying to stake.");
                addStaker(smart_contract, _tokenId, block.timestamp, msg.sender); //adds struct to array of struct
                parentNFT.safeTransferFrom(msg.sender, address(this), _tokenId, "0x00"); //calls transfer function of input contract.
            }
        }
    } 
    //@dev pass in contract address and array of associated tokenIDs you want to unstake. 
        // transfers NFTs back to the owner and records staking time earned upon unstaking. 
        // mints ERC20 token based on time earned and multiplier of contract address. 
        // deletes previous stake record

    function getOriginalStaker(address smart_contract, address _wallet, uint _tokenId) public view returns (address) {
        return getStakeOfOwnerPerContractAndTokenID[_wallet][smart_contract][_tokenId].wallet;
    }


    function unstake(address smart_contract, uint[] calldata _tokenIds) public payable {
        //always unstakeable except for cooldown {lockupTime} staker must wait before unstaking to prevent spam.
        parentNFT = ERC721(smart_contract); //creates connection with contract specific tokenIds
        uint length = _tokenIds.length;
        unchecked {
            for (uint i=0; i<length; i++) {
                uint _tokenId = _tokenIds[i];
                address original_staker = getOriginalStaker(smart_contract, msg.sender, _tokenId);          
                require(original_staker == msg.sender, "You didn't stake this one or more of these tokens.");
                require(block.timestamp - getStakeOfOwnerPerContractAndTokenID[msg.sender][smart_contract][_tokenId].timestamp > lockupTime, "Slow down cowboy, you just staked one of the tokens attempted.");
                stakingTime[msg.sender] += getStakingTime(_tokenId, smart_contract);
                parentNFT.safeTransferFrom(address(this), msg.sender, _tokenId, "0x00");
                removeStaker(smart_contract, original_staker, _tokenId);
            }
        }   
    }

    function claimRewards() public payable {
        require(stakingTime[msg.sender] > 0, "You don't have any rewards to claim.");
        tokenContract._mint(msg.sender, stakingTime[msg.sender]);
        claimedTime[msg.sender] += stakingTime[msg.sender];
        stakingTime[msg.sender] -= stakingTime[msg.sender];
    }

    function unrealizedRewardsOfTokens(address smart_contract, uint[] calldata _tokenIds) public view returns (uint256) {
        uint length = _tokenIds.length;
        uint unrealizedRewards;
        
        for (uint i=0; i<length; i++) {            
            unrealizedRewards += getStakingTime(_tokenIds[i], smart_contract);
        }
        
        return unrealizedRewards;
    }

    function updateTokenContract(address newTokenAddress) public virtual onlyOwner {
        tokenContract = ERC20(newTokenAddress);
    }

    function updateLockupTime(uint256 _timeInSeconds) public virtual onlyOwner {
        lockupTime = _timeInSeconds;
    }

    function lockupTimeLeft(address _wallet, address smart_contract, uint _tokenId) public view returns (uint) {
        uint stakedTimestamp = getStakeOfOwnerPerContractAndTokenID[_wallet][smart_contract][_tokenId].timestamp;
        uint timeStaked = (block.timestamp - stakedTimestamp);
        uint lockupTimeRemaining = lockupTime - timeStaked;
        if (lockupTimeRemaining > 0) {
            return lockupTimeRemaining;
        } else {
            return 0;
        }
    }

    function updateFrequency(uint256 _timeInSeconds) public virtual onlyOwner {
        frequency = _timeInSeconds;
    }

    function getStakingTime(uint256 _tokenId, address smart_contract) internal view returns (uint256) {        
        uint stakedTimestamp = getStakeOfOwnerPerContractAndTokenID[msg.sender][smart_contract][_tokenId].timestamp;
        return (((block.timestamp - stakedTimestamp) * 1 ether * getMultiplier(smart_contract, _tokenId)) / frequency);
    }
 
    function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) {
        return this.onERC721Received.selector;
    }
}
function addStaker(address smart_contract, uint _tokenId, uint _timestamp, address _wallet) internal {
        uint index = stakerCounter;
        stakerCounter += 1;
        Stake memory new_staker = Stake(smart_contract, _tokenId, _timestamp, _wallet, index);
        stakes.push(new_staker);
        getStakeOfOwnerPerContractAndTokenID[_wallet][smart_contract][_tokenId] = new_staker;
        getTokenIDsOfStakerPerContractAndIndex[_wallet][smart_contract].push(_tokenId);
    }
//returns Stake record