// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

library Address {
    function isContract(address account) internal view returns (bool) {
        uint256 size;
        assembly {
            size := extcodesize(account)
        }
        return size > 0;
    }

    function sendValue(address payable recipient, uint256 amount) internal {
        require(address(this).balance >= amount, "Address: insufficient balance");

        (bool success, ) = recipient.call{value: amount}("");
        require(success, "Address: unable to send value, recipient may have reverted");
    }

    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionCall(target, data, "Address: low-level call failed");
    }

    function functionCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }

    function functionCallWithValue(address target, bytes memory data, uint256 value) internal returns (bytes memory) {
        return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
    }

    function functionCallWithValue(address target, bytes memory data, uint256 value, string memory errorMessage) internal returns (bytes memory) {
        require(address(this).balance >= value, "Address: insufficient balance for call");
        require(isContract(target), "Address: call to non-contract");

        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        return functionStaticCall(target, data, "Address: low-level static call failed");
    }

    function functionStaticCall(address target, bytes memory data, string memory errorMessage) internal view returns (bytes memory) {
        require(isContract(target), "Address: static call to non-contract");

        (bool success, bytes memory returndata) = target.staticcall(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionDelegateCall(target, data, "Address: low-level delegate call failed");
    }

    function functionDelegateCall(address target, bytes memory data, string memory errorMessage) internal returns (bytes memory) {
        require(isContract(target), "Address: delegate call to non-contract");

        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResult(success, returndata, errorMessage);
    }

    function verifyCallResult(bool success, bytes memory returndata, string memory errorMessage) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            if (returndata.length > 0) {
                assembly {
                    let returndata_size := mload(returndata)
                    revert(add(32, returndata), returndata_size)
                }
            } else {
                revert(errorMessage);
            }
        }
    }
}

library SafeMath {
    function mul(uint256 a, uint256 b) internal pure returns (uint256 c) {
        if (a == 0) {
            return 0;
        }
        c = a * b;
        assert(c / a == b);
        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return a / b;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        assert(b <= a);
        return a - b;
    }

    function safeSub(uint256 a, uint256 b) internal pure returns (uint256) {
        if (b > a) {
            return 0;
        } else {
            return a - b;
        }
    }

    function add(uint256 a, uint256 b) internal pure returns (uint256 c) {
        c = a + b;
        assert(c >= a);
        return c;
    }

    function max(uint256 a, uint256 b) internal pure returns (uint256) {
        return a >= b ? a : b;
    }

    function min(uint256 a, uint256 b) internal pure returns (uint256) {
        return a < b ? a : b;
    }
}

library SafeERC20 {
    using Address for address;

    function safeTransfer(IERC20 token, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
    }

    function safeTransferFrom(IERC20 token, address from, address to, uint256 value) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
    }

    function safeApprove(IERC20 token, address spender, uint256 value) internal {
        require((value == 0) || (token.allowance(address(this), spender) == 0), "SafeERC20: approve from non-zero to non-zero allowance");
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
    }

    function safeIncreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        uint256 newAllowance = token.allowance(address(this), spender) + value;
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
    }

    function safeDecreaseAllowance(IERC20 token, address spender, uint256 value) internal {
        unchecked {
            uint256 oldAllowance = token.allowance(address(this), spender);
            require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
            uint256 newAllowance = oldAllowance - value;
            _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
        }
    }

    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
        if (returndata.length > 0) {
            require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
        }
    }
}

library EnumerableSet {

    struct Set {
        bytes32[] _values;
        mapping(bytes32 => uint256) _indexes;
    }

    function _add(Set storage set, bytes32 value) private returns (bool) {
        if (!_contains(set, value)) {
            set._values.push(value);
            set._indexes[value] = set._values.length;
            return true;
        } else {
            return false;
        }
    }

    function _remove(Set storage set, bytes32 value) private returns (bool) {
        uint256 valueIndex = set._indexes[value];

        if (valueIndex != 0) {
            uint256 toDeleteIndex = valueIndex - 1;
            uint256 lastIndex = set._values.length - 1;

            if (lastIndex != toDeleteIndex) {
                bytes32 lastValue = set._values[lastIndex];

                set._values[toDeleteIndex] = lastValue;
                set._indexes[lastValue] = valueIndex;
            }

            set._values.pop();

            delete set._indexes[value];

            return true;
        } else {
            return false;
        }
    }

    function _contains(Set storage set, bytes32 value) private view returns (bool) {
        return set._indexes[value] != 0;
    }

    function _length(Set storage set) private view returns (uint256) {
        return set._values.length;
    }

    function _at(Set storage set, uint256 index) private view returns (bytes32) {
        return set._values[index];
    }

    function _values(Set storage set) private view returns (bytes32[] memory) {
        return set._values;
    }

    // Bytes32Set

    struct Bytes32Set {
        Set _inner;
    }

    function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
        return _add(set._inner, value);
    }

    function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
        return _remove(set._inner, value);
    }

    function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
        return _contains(set._inner, value);
    }

    function length(Bytes32Set storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
        return _at(set._inner, index);
    }

    function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
        bytes32[] memory store = _values(set._inner);
        bytes32[] memory result;

        assembly {
            result := store
        }

        return result;
    }

    // AddressSet

    struct AddressSet {
        Set _inner;
    }

    function add(AddressSet storage set, address value) internal returns (bool) {
        return _add(set._inner, bytes32(uint256(uint160(value))));
    }

    function remove(AddressSet storage set, address value) internal returns (bool) {
        return _remove(set._inner, bytes32(uint256(uint160(value))));
    }

    function contains(AddressSet storage set, address value) internal view returns (bool) {
        return _contains(set._inner, bytes32(uint256(uint160(value))));
    }

    function length(AddressSet storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    function at(AddressSet storage set, uint256 index) internal view returns (address) {
        return address(uint160(uint256(_at(set._inner, index))));
    }

    function values(AddressSet storage set) internal view returns (address[] memory) {
        bytes32[] memory store = _values(set._inner);
        address[] memory result;

        assembly {
            result := store
        }

        return result;
    }

    // UintSet

    struct UintSet {
        Set _inner;
    }

    function add(UintSet storage set, uint256 value) internal returns (bool) {
        return _add(set._inner, bytes32(value));
    }

    function remove(UintSet storage set, uint256 value) internal returns (bool) {
        return _remove(set._inner, bytes32(value));
    }

    function contains(UintSet storage set, uint256 value) internal view returns (bool) {
        return _contains(set._inner, bytes32(value));
    }

    function length(UintSet storage set) internal view returns (uint256) {
        return _length(set._inner);
    }

    function at(UintSet storage set, uint256 index) internal view returns (uint256) {
        return uint256(_at(set._inner, index));
    }

    function values(UintSet storage set) internal view returns (uint256[] memory) {
        bytes32[] memory store = _values(set._inner);
        uint256[] memory result;

        assembly {
            result := store
        }

        return result;
    }
}

interface INFTFeeClaim {
    function depositRewards(address _nft, address _token, uint256 _amount) external;
}

interface IERC20 {
    function totalSupply() external view returns (uint256);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

interface IERC165 {
    function supportsInterface(bytes4 interfaceId) external view returns (bool);
}

interface IERC721 is IERC165 {
    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
    event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
    event ApprovalForAll(address indexed owner, address indexed operator, bool approved);

    function balanceOf(address owner) external view returns (uint256 balance);
    function ownerOf(uint256 tokenId) external view returns (address owner);
    function safeTransferFrom(address from, address to, uint256 tokenId) external;
    function transferFrom(address from, address to, uint256 tokenId) external;
    function approve(address to, uint256 tokenId) external;
    function getApproved(uint256 tokenId) external view returns (address operator);
    function setApprovalForAll(address operator, bool _approved) external;
    function isApprovedForAll(address owner, address operator) external view returns (bool);
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
}

interface IRewardsNFT {
    function totalSupply() external view returns (uint256);
    function remainingItems() external view returns (uint256);
}

interface ITokensRecoverable {
    function recoverTokens(IERC20 token) external;
}

contract Ownable {
    address public owner;

    modifier onlyOwner() {
        require(msg.sender == owner);
        _;
    }

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    constructor() {
        owner = msg.sender;
    }

    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0));
        emit OwnershipTransferred(owner, newOwner);
        owner = newOwner;
    }
}

contract Whitelist is Ownable {
    mapping(address => bool) public whitelist;

    event WhitelistedAddressAdded(address addr);
    event WhitelistedAddressRemoved(address addr);

    modifier onlyWhitelisted() {
        require(whitelist[msg.sender] || msg.sender == owner, "NOT_WHITELISTED");
        _;
    }

    function isWhitelisted(address addr) public view returns (bool whitelisted) {
        if (whitelist[addr] || msg.sender == owner) {
            return true;
        }
        return false;
    }

    function addAddressToWhitelist(address addr) onlyOwner public returns(bool success) {
        if (!whitelist[addr]) {
            whitelist[addr] = true;
            emit WhitelistedAddressAdded(addr);
            success = true;
        }
    }

    function addAddressesToWhitelist(address[] memory addrs) onlyOwner public returns(bool success) {
        for (uint256 i = 0; i < addrs.length; i++) {
            if (addAddressToWhitelist(addrs[i])) {
                success = true;
            }
        }
        return success;
    }

    function removeAddressFromWhitelist(address addr) onlyOwner public returns(bool success) {
        if (whitelist[addr]) {
            whitelist[addr] = false;
            emit WhitelistedAddressRemoved(addr);
            success = true;
        }
        return success;
    }

    function removeAddressesFromWhitelist(address[] memory addrs) onlyOwner public returns(bool success) {
        for (uint256 i = 0; i < addrs.length; i++) {
            if (removeAddressFromWhitelist(addrs[i])) {
                success = true;
            }
        }
        return success;
    }
}

abstract contract ReentrancyGuard {
    uint256 private constant _NOT_ENTERED = 1;
    uint256 private constant _ENTERED = 2;
    uint256 private _status;

    constructor() {
        _status = _NOT_ENTERED;
    }

    modifier nonReentrant() {
        require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
        _status = _ENTERED;
        _;
        _status = _NOT_ENTERED;
    }
}

abstract contract TokensRecoverable is Whitelist, ITokensRecoverable {
    using SafeERC20 for IERC20;

    function recoverTokens(IERC20 token) public override onlyOwner {
        require (canRecoverTokens(token));
        token.safeTransfer(msg.sender, token.balanceOf(address(this)));
    }

    function canRecoverTokens(IERC20 token) internal virtual view returns (bool) { 
        return address(token) != address(this); 
    }
}

contract RewardsDistributor is INFTFeeClaim, Whitelist, ReentrancyGuard, TokensRecoverable {
    using Address for address;
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    using EnumerableSet for EnumerableSet.UintSet;
    using EnumerableSet for EnumerableSet.AddressSet;

    ////////////////
    // INTERFACES //
    ////////////////

    EnumerableSet.AddressSet private allowedTokens;
    EnumerableSet.AddressSet private allowedNFTs;

    ///////////////
    // MODIFIERS //
    ///////////////

    modifier checkAuth(address _nft) {
        require(isWhitelisted(msg.sender) || msg.sender == items[_nft].manager);
        _;
    }

    modifier checkNFT(address _nft) {
        require(isAllowedNFT(_nft), "NFT_NOT_ALLOWED");
        _;
    }

    modifier checkToken(address _token) {
        require(isAllowedToken(_token), "TOKEN_NOT_ALLOWED");
        _;
    }

    /////////////
    // STRUCTS //
    /////////////

    struct Stats {
        uint256 deposited;
        uint256 xDeposited;

        uint256 claimed;
        uint256 xClaimed;
    }

    struct Users {
        uint256 lastClaim;
        uint256 totalClaims;

        mapping(address => Stats) stats;
    }

    struct Collection {
        address manager;
        EnumerableSet.UintSet ignoredItems;
    }

    //////////////
    // MAPPINGS //
    //////////////

    mapping(address => mapping(address => uint256)) private _claimableOf;
    mapping(address => Collection) private items;
    mapping(address => Stats) private stats;
    mapping(address => Users) private users;

    ////////////////////////////
    // CONSTRUCTOR & FALLBACK //
    ////////////////////////////

    constructor() {
        addAddressToWhitelist(msg.sender);
    }

    /////////////////////
    // WRITE FUNCTIONS //
    /////////////////////

    // Claim any entitlements by token address
    function claimPayout(address _token) public nonReentrant {

        uint256 amount = _claimableOf[_token][msg.sender];

        require (_claimableOf[_token][msg.sender] > 0, "No payout available");

        users[msg.sender].stats[_token].claimed += amount;
        users[msg.sender].stats[_token].xClaimed += 1;

        users[msg.sender].totalClaims += 1;
        users[msg.sender].lastClaim = block.timestamp;

        stats[_token].claimed += amount;
        stats[_token].xClaimed += 1;
        
        _claimableOf[_token][msg.sender] = 0;
        
        IERC20(_token).safeTransfer(msg.sender, amount);
    }

    ////////////////////
    // VIEW FUNCTIONS //
    ////////////////////

    // Get last claim of a user
    function lastClaimOf(address _user) public view returns (uint256) {
        return users[_user].lastClaim;
    }

    // Get total claims of a user
    function totalClaimsOf(address _user) public view returns (uint256) {
        return users[_user].totalClaims;
    }

    // Get owner of an NFT item by id
    function getOwnerOf(address _nft, uint256 _id) public view returns (address) {
        return IERC721(_nft).ownerOf(_id);
    }

    // Get claimable balance of a holder by token address
    function claimableOf(address _user, address _token) public view returns (uint256) {
        return _claimableOf[_token][_user];
    }

    // IGNORED ITEMS - Good to filter reward recipients by ID

    // Get ignored status by item id
    function isIgnoredItem(address _nft, uint itemId) public view returns (bool) {
        return items[_nft].ignoredItems.contains(itemId);
    }

    // Get total ignored items by NFT
    function totalIgnoredItems(address _nft) public view returns (uint256) {
        return items[_nft].ignoredItems.length();
    }

    // Get ignored item of collection at index
    function ignoredItemAt(address _nft, uint256 index) public view returns (uint256) {
        return items[_nft].ignoredItems.at(index);
    }

    // ALLOWED NFTs - Prevents use by incompatible models of NFT

    // Get permission status by NFT address
    function isAllowedNFT(address _nft) public view returns (bool) {
        return allowedNFTs.contains(_nft);
    }

    // Get total allowed NFTs
    function totalAllowedNFTs() public view returns (uint256) {
        return allowedNFTs.length();
    }

    // Get ignored NFTs at index
    function allowedNFTAt(uint256 index) public view returns (address) {
        return allowedNFTs.at(index);
    }

    // ALLOWED TOKENS - Stops airdrop spam of unwanted tokens

    // Get permission status by token address
    function isAllowedToken(address _token) public view returns (bool) {
        return allowedTokens.contains(_token);
    }

    // Get total allowed tokens
    function totalAllowedTokens() public view returns (uint256) {
        return allowedTokens.length();
    }

    // Get ignored token at index
    function allowedTokenAt(uint256 index) public view returns (address) {
        return allowedTokens.at(index);
    }

    // GENERAL PURPOSE

    // Token recoverability status by address
    function canRecoverTokens(IERC20 _token) internal override view returns (bool) {
        return address(_token) != address(this); 
    }

    //////////////////////////
    // RESTRICTED FUNCTIONS //
    //////////////////////////

    // Deposit rewards in any token to any set of NFTs
    function depositRewards(address _nft, address _token, uint256 _amount) override public checkAuth(_nft) checkToken(_token) {
        
        calculatePayouts(_nft, _token, _amount);
        
        IERC20(_token).transferFrom(msg.sender, address(this), _amount);

        users[msg.sender].stats[_token].deposited += _amount;
        users[msg.sender].stats[_token].xDeposited += 1;

        stats[_token].deposited += _amount;
        stats[_token].xDeposited += 1;
    }

    // Set ignored items by NFT and ID
    function setIgnoredItem(address _nft, uint itemId, bool add) public checkAuth(_nft) {
        if (add) { 
            items[_nft].ignoredItems.add(itemId); 
        } else { 
            items[_nft].ignoredItems.remove(itemId); 
        }
    }

    //////////////////////////
    // OWNER-ONLY FUNCTIONS //
    //////////////////////////

    // Set allowed NFT by address
    function setAllowedNFT(address _nft, bool add) public onlyWhitelisted() {
        if (add) { 
            allowedNFTs.add(_nft); 
        } else { 
            allowedNFTs.remove(_nft); 
        }
    }

    // Set allowed token by address
    function setAllowedToken(address _token, bool add) public onlyWhitelisted() {
        if (add) { 
            allowedTokens.add(_token); 
        } else { 
            allowedTokens.remove(_token); 
        }
    }

    // Set manager of an NFT project
    function setManager(address _nft, address _manager) public onlyWhitelisted() {
        items[_nft].manager = _manager;
    }

    ////////////////////////
    // INTERNAL FUNCTIONS //
    ////////////////////////

    // Calculate payouts
    function calculatePayouts(address _nft, address _token, uint256 _amount) internal {

        // Get total minted NFTs
        uint totalMinted = IRewardsNFT(_nft).totalSupply();

        // Filter to eligible items
        uint eligibleItems = (totalMinted.sub(totalIgnoredItems(_nft)));
        
        // Find the amount per item
        uint amountPerItem = _amount / eligibleItems;
        
        // For each minted item,
        for (uint i = 1; i <= eligibleItems; i++) {

            // Find the owner,
            address currentNftOwner = getOwnerOf(_nft, i);
            
            // and if it's not ignored, 
            if (!isIgnoredItem(_nft, i)) {

                // Credit them with the corresponding amount
                _claimableOf[currentNftOwner][_token] += amountPerItem;
            }
        }
    }
}