// Contract: stakingTastiesNFT
// Target function: function calculateRewardsByTokenId(uint256 givenId) public view returns (uint256 _rewards)
utils/introspection/IERC165.sol)

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC165 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-165[EIP].
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others ({ERC165Checker}).
 *
 * For an implementation, see {ERC165}.
 */
interface IERC165 {
    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30 000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

// OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/IERC721.sol)

pragma solidity ^0.8.0;


/**
 * @dev Required interface of an ERC721 compliant contract.
 */
interface IERC721 is IERC165 {
    /**
     * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
     */
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
     */
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);

    /**
     * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
     */
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    /**
     * @dev Returns the number of tokens in ``owner``'s account.
     */
    function balanceOf(address owner) external view returns (uint256 balance);

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) external view returns (address owner);

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId,
        bytes calldata data
    ) external;

    /**
     * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
     * are aware of the ERC721 protocol to prevent tokens from being forever locked.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must exist and be owned by `from`.
     * - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
     * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Transfers `tokenId` token from `from` to `to`.
     *
     * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.
     *
     * Requirements:
     *
     * - `from` cannot be the zero address.
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Gives permission to `to` to transfer `tokenId` token to another account.
     * The approval is cleared when the token is transferred.
     *
     * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
     *
     * Requirements:
     *
     * - The caller must own the token or be an approved operator.
     * - `tokenId` must exist.
     *
     * Emits an {Approval} event.
     */
    function approve(address to, uint256 tokenId) external;

    /**
     * @dev Approve or remove `operator` as an operator for the caller.
     * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
     *
     * Requirements:
     *
     * - The `operator` cannot be the caller.
     *
     * Emits an {ApprovalForAll} event.
     */
    function setApprovalForAll(address operator, bool _approved) external;

    /**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) external view returns (address operator);

    /**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
     *
     * See {setApprovalForAll}
     */
    function isApprovedForAll(address owner, address operator) external view returns (bool);
}

// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC721/extensions/IERC721Enumerable.sol)

pragma solidity ^0.8.0;


/**
 * @title ERC-721 Non-Fungible Token Standard, optional enumeration extension
 * @dev See https://eips.ethereum.org/EIPS/eip-721
 */
interface IERC721Enumerable is IERC721 {
    /**
     * @dev Returns the total amount of tokens stored by the contract.
     */
    function totalSupply() external view returns (uint256);

    /**
     * @dev Returns a token ID owned by `owner` at a given `index` of its token list.
     * Use along with {balanceOf} to enumerate all of ``owner``'s tokens.
     */
    function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256);

    /**
     * @dev Returns a token ID at a given `index` of all the tokens stored by the contract.
     * Use along with {totalSupply} to enumerate all tokens.
     */
    function tokenByIndex(uint256 index) external view returns (uint256);
}

/*
                    __________   _____     ______       ____     __________
                   |___    ___| |     \   |   ___|     /    \   |___    ___|
                       |  |     |  |_| |  |  |___     /  /\  \      |  |
                       |  |     |     <   |   ___|   /  ___   \     |  | 
                       |  |     |  |\  \  |  |___   /  /    \  \    |  |
                       |__|     |__| \__\ |______| /__/      \__\   |__|

       _______   __________     ____        __    __    __________   ______   __      ______
     /  ______/ |___    ___|   /    \      |   | /  /  |___    ___| |      \ |  |   /   __   \
    |  /            |  |      /  /\  \     |   |/  /       |  |     |   __  \|  |  /  /    \__|
    |  \___         |  |     /  ___   \    |      <        |  |     |  |  \     | |  |     ____
     \_____  \      |  |    /  /    \  \   |   _   \       |  |     |  |   \    | |  |    |    |
    _______|  |     |  |   /  /      \  \  |  | \   \   ___|  |___  |  |    \   |  \  \ __ /  /
    \________/      |__|  /__/        \__\ |__|   \__\ |__________| |__|     \__|   \ ______ /



                            Contract provided By B.A.S.S Studios 
                            (Blockchain and Software Solutions)
*/

pragma solidity ^0.8.7;

//Tasties NFT Staking 

interface INFTContract {
    function balanceOf(address _user) external view returns (uint256);
  
}

contract stakingTastiesNFT is ERC20Burnable, Ownable {

    uint256 public constant NFT_BASE_RATE = 10000000000000000000; // 10 per Day
    address public constant NFT_ADDRESS = 0xd4d1f32c280056f107AD4ADf8e16BC02f2C5B339;//TastiesNFT Collection

    bool public stakingLive = false;
    bool public airdropToAll=true;
    bool public returnLock=false;

    mapping(uint256 => uint256) internal NftTokenIdTimeStaked;
    mapping(uint256 => address) internal NftTokenIdToStaker;
    mapping(address => uint256[]) internal stakerToNftTokenIds;
    //map token id to bool for if staked //fb
    mapping(uint256=>bool) public nftStaked;

    //trait type mapping token id to traid type   //fb
    //Trait types 1,2,3,4,5,6
    mapping(uint256 => uint256) private _traitType;

    //uint type multipliers to implement different payouts for different trait rarities (value must be 10x percentage desired)
    uint256 type1Multiplier=10;
    uint256 type2Multiplier=12;
    uint256 type3Multiplier=14;
    uint256 type4Multiplier=20;
    uint256 type5Multiplier=30;
    uint256 type6Multiplier=200;
    
    IERC721Enumerable private constant _NftIERC721 = IERC721Enumerable(NFT_ADDRESS);


    constructor() ERC20("Treat Token", "TREAT") {
    }

    function getTokenIDsStaked(address staker) public view returns (uint256[] memory) {
        return stakerToNftTokenIds[staker];
    }
    
    function getStakedCount(address staker) public view returns (uint256) {
        return stakerToNftTokenIds[staker].length;
    }

    function removeSpecificTokenIdFromArray(uint256[] storage array, uint256 tokenId) internal {
        uint256 length = array.length;
        for (uint256 i = 0; i < length; i++) {
            if (array[i] == tokenId) {
                length--;
                if (i < length) {
                    array[i] = array[length];
                }
                array.pop();
                break;
            }
        }
    }
// covers single staking and multiple
    function stakeNftsByTokenIds(uint256[] memory tokenIds) public  {
     require(stakingLive, "STAKING NOT LIVE");

        for (uint256 i = 0; i < tokenIds.length; i++) {
            uint256 id = tokenIds[i];
            require(_NftIERC721.ownerOf(id) == msg.sender && NftTokenIdToStaker[id] == address(0), "TOKEN NOT YOURS");
            //NFT transfer 
            _NftIERC721.transferFrom(msg.sender, address(this), id);
            //Track data
            stakerToNftTokenIds[msg.sender].push(id);
            NftTokenIdTimeStaked[id] = block.timestamp;
            NftTokenIdToStaker[id] = msg.sender;
            nftStaked[id]=true;
        }
       
    }
// unstake and claims for tokens

    function unstakeAll() public {
        require(getStakedCount(msg.sender) > 0, "Need at least 1 staked to unstake");
        uint256 totalRewards = 0;

        for (uint256 i = stakerToNftTokenIds[msg.sender].length; i > 0; i--) {
            uint256 tokenId = stakerToNftTokenIds[msg.sender][i - 1];

            _NftIERC721.transferFrom(address(this), msg.sender, tokenId);
            //add calculated value of the token id by its type //fb
            totalRewards += calculateRewardsByTokenId(tokenId);
            //pop used to save more gas and counting from end of the stored ids to index 0 //fb
            stakerToNftTokenIds[msg.sender].pop();
            NftTokenIdToStaker[tokenId] = address(0);
            nftStaked[tokenId]=false;
            //after gathering total rewards set token token id time staked to 0 //fb
            NftTokenIdTimeStaked[tokenId] = 0;
        }
        _mint(msg.sender, totalRewards);
    }

    function unstakeNftsByTokenIds(uint256[] memory tokenIds) public {
        uint256 totalRewards = 0;

        for (uint256 i = 0; i < tokenIds.length; i++) {
            uint256 id = tokenIds[i];
            require(NftTokenIdToStaker[id] == msg.sender, "NOT the staker");

            _NftIERC721.transferFrom(address(this), msg.sender, id);
             //add calculated value of the token id by its type //fb
            totalRewards += calculateRewardsByTokenId(id);
            //remove specific id from stored ids (less gas efficient than unstake all   //fb
            removeSpecificTokenIdFromArray(stakerToNftTokenIds[msg.sender], id);
            NftTokenIdToStaker[id] = address(0);
             nftStaked[id]=false;
             //after gathering total rewards set token id time staked to 0 //fb
            NftTokenIdTimeStaked[id] = 0;
        }

        _mint(msg.sender, totalRewards);
      
    }
    

    function claimTokensByTokenId(uint256 tokenId) public {
        require(NftTokenIdToStaker[tokenId] == msg.sender, "NOT the staker");
          //add calculated value of the token id by its type //fb
        _mint(msg.sender, (calculateRewardsByTokenId(tokenId)));
        NftTokenIdTimeStaked[tokenId] = block.timestamp;
    }
    
   
    function claimAllTokens() public {
        uint256 totalRewards = 0;

        uint256[] memory TokenIds = stakerToNftTokenIds[msg.sender];
        for (uint256 i = 0; i < TokenIds.length; i++) {
            uint256 id = TokenIds[i];
            require(NftTokenIdToStaker[id] == msg.sender, "NOT_STAKED_BY_YOU");
             //add calculated value of the token id by its type //fb
            totalRewards += calculateRewardsByTokenId(id);
            NftTokenIdTimeStaked[id] = block.timestamp;
        }
        
        
        _mint(msg.sender, totalRewards);
    }
//returning the total reward payout per address
    function getAllRewards(address staker) public view returns (uint256) {
        uint256 totalRewards = 0;
        uint256[] memory tokenIds = stakerToNftTokenIds[staker];
        for (uint256 i = 0; i < tokenIds.length; i++) {
             //add calculated value of the token id by its type //fb
            totalRewards += (calculateRewardsByTokenId(tokenIds[i]));
        } 
      

        return totalRewards;
    }

    function getRewardsByNftTokenId(uint256 tokenId) public view returns (uint256) {
        require(NftTokenIdToStaker[tokenId] != address(0), "TOKEN_NOT_STAKED");

          //add calculated value of the token id by its type //fb
        return calculateRewardsByTokenId(tokenId);
    }
    

    function getNftStaker(uint256 tokenId) public view returns (address) {
        return NftTokenIdToStaker[tokenId];
    }
    
    //return public is token id staked in contract
    function isStaked(uint256 tokenId) public view returns (bool) {
        return(nftStaked[tokenId]);
    }

    function toggle() external onlyOwner {
        stakingLive = !stakingLive;
    }

  //trait rarity list fuctions
  //set single token id type for multiplier fixes or possible future rewarding of specific tokens
  function setTypeList(uint256 tokenIdTemp, uint256 typeNumber) external onlyOwner {
       
            _traitType[tokenIdTemp] = typeNumber;
           
    }
  //set a full tokenId to type list can only map to one type at a time (maps all in list to the same trait number)
  function setFullTypeList(uint[] calldata idList, uint256 traitNum) external onlyOwner {
    for (uint256 i = 0; i < idList.length; i++) {
        _traitType[idList[i]] = traitNum;
    
    }
    
 }