// Contract: FAMCO
// Target function: function transferFrom(
9
     *
     * Emits an {Approval} event.
     */
    function approve(address spender, uint256 amount) external returns (bool);

    /**
     * @dev Moves `amount` tokens from `sender` to `recipient` using the
     * allowance mechanism. `amount` is then deducted from the caller's
     * allowance.
     *
     * Returns a boolean value indicating whether the operation succeeded.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);

    /**
     * @dev Emitted when `value` tokens are moved from one account (`from`) to
     * another (`to`).
     *
     * Note that `value` may be zero.
     */
    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * @dev Emitted when the allowance of a `spender` for an `owner` is set by
     * a call to {approve}. `value` is the new allowance.
     */
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

/**
 * @dev Provides information about the current execution context, including the
 * sender of the transaction and its data. While these are generally available
 * via msg.sender and msg.data, they should not be accessed in such a direct
 * manner, since when dealing with meta-transactions the account sending and
 * paying for execution may not be the actual sender (as far as an application
 * is concerned).
 *
 * This contract is only required for intermediate, library-like contracts.
 */
abstract contract Context {
    function _msgSender() internal view virtual returns (address) {
        return msg.sender;
    }

    function _msgData() internal view virtual returns (bytes calldata) {
        return msg.data;
    }
}

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;
    }
}

library SafeMath {
    /**
     * @dev Returns the addition of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `+` operator.
     *
     * Requirements:
     *
     * - Addition cannot overflow.
     */
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting on
     * overflow (when the result is negative).
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return sub(a, b, "SafeMath: subtraction overflow");
    }

    /**
     * @dev Returns the subtraction of two unsigned integers, reverting with custom message on
     * overflow (when the result is negative).
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b <= a, errorMessage);
        uint256 c = a - b;

        return c;
    }

    /**
     * @dev Returns the multiplication of two unsigned integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `*` operator.
     *
     * Requirements:
     *
     * - Multiplication cannot overflow.
     */
    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        // Gas optimization: this is cheaper than requiring 'a' not being zero, but the
        // benefit is lost if 'b' is also tested.
        // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");

        return c;
    }

    /**
     * @dev Returns the integer division of two unsigned integers. Reverts on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return div(a, b, "SafeMath: division by zero");
    }

    /**
     * @dev Returns the integer division of two unsigned integers. Reverts with custom message on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator. Note: this function uses a
     * `revert` opcode (which leaves remaining gas untouched) while Solidity
     * uses an invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b > 0, errorMessage);
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * Reverts when dividing by zero.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return mod(a, b, "SafeMath: modulo by zero");
    }

    /**
     * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo),
     * Reverts with custom message when dividing by zero.
     *
     * Counterpart to Solidity's `%` operator. This function uses a `revert`
     * opcode (which leaves remaining gas untouched) while Solidity uses an
     * invalid opcode to revert (consuming all remaining gas).
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b != 0, errorMessage);
        return a % b;
    }
}

contract Ownable {

    address private owner;
    
    // event for EVM logging
    event OwnerSet(address indexed oldOwner, address indexed newOwner);
    
    // modifier to check if caller is owner
    modifier onlyOwner() {
        // If the first argument of 'require' evaluates to 'false', execution terminates and all
        // changes to the state and to Ether balances are reverted.
        // This used to consume all gas in old EVM versions, but not anymore.
        // It is often a good idea to use 'require' to check if functions are called correctly.
        // As a second argument, you can also provide an explanation about what went wrong.
        require(msg.sender == owner, "Caller is not owner");
        _;
    }
    
    /**
     * @dev Set contract deployer as owner
     */
    constructor() {
        owner = msg.sender; // 'msg.sender' is sender of current call, contract deployer for a constructor
        emit OwnerSet(address(0), owner);
    }

    /**
     * @dev Change owner
     * @param newOwner address of new owner
     */
    function changeOwner(address newOwner) public onlyOwner {
        emit OwnerSet(owner, newOwner);
        owner = newOwner;
    }

    /**
     * @dev Return owner address 
     * @return address of owner
     */
    function getOwner() external view returns (address) {
        return owner;
    }
}

contract FAMCO is Context, ERC165, IERC721, IERC721Metadata, ReentrancyGuard, Ownable {
    using Address for address;
    using SafeMath for uint256;

    // Token name
    string private constant _name = "FAMCO";

    // Token symbol
    string private constant _symbol = "FAMCO";

    // total number of NFTs Minted
    uint256 private _totalSupply;

    // current tokenID to mint at
    uint256 public currentSupplyIndex;

    // Total Number Of Reward Shares (accounts for differing rarities)
    uint256 public totalShares;

    // Mapping from token ID to owner address
    mapping(uint256 => address) private _owners;

    // Mapping owner address to token count
    mapping(address => uint256) private _balances;

    // Mapping from token ID to approved address
    mapping(uint256 => address) private _tokenApprovals;

    // Mapping from owner to operator approvals
    mapping(address => mapping(address => bool)) private _operatorApprovals;

    // Mapping from address to permission to deposit rewards
    mapping ( address => bool ) public canGiveRewards;

    // Maps a TokenID to a rarity
    mapping ( uint256 => uint8 ) public tokenIDToRarity;

    // owner rewards structure
    struct OwnerRewards {
        uint256 share; // accounts for rarity of different NFTs
        uint256 totalExcluded; // aka reward debt
    }

    // Mapping from owner to excluded rewards
    mapping(address => OwnerRewards) public ownerRewards;

    // total dividends for tracking
    uint256 private dividendsPerShare;

    // total rewards received
    uint256 public totalRewards;

    // precision factor
    uint256 private PRECISION = 10**9;

    // base URI
    string private baseURI = "";
    string private ending = ".json";

    // Token Rarity Structure
    struct TokenRarity {
        uint256 cost;
        uint8 multiplier;
    }

    // cost for minting NFT
    mapping ( uint8 => TokenRarity ) public tokenRarity;

    // Enable Trading
    bool public tradingEnabled = false;

    // Toggle URI based on rarity vs tokenID
    bool public UriByRarity = true;

    // Mint Token
    address public mintToken = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
    address public rewardToken = 0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
    address public mintRecipient;

    ////////////////////////////////////////////////
    ///////////     OWNER FUNCTIONS      ///////////
    ////////////////////////////////////////////////

    function __init__(address mintRecipient_) external onlyOwner {
        require(
            mintRecipient_ != address(0),
            'Zero Addr'
        );
        tokenRarity[0].cost = 500 * 10**6;
        tokenRarity[1].cost = 1000 * 10**6;
        tokenRarity[2].cost = 2000 * 10**6;
        tokenRarity[3].cost = 5000 * 10**6;
        tokenRarity[0].multiplier = 1;
        tokenRarity[1].multiplier = 2;
        tokenRarity[2].multiplier = 4;
        tokenRarity[3].multiplier = 10;
        mintRecipient = mintRecipient_;
        canGiveRewards[mintRecipient_] = true;
    }

    function resumeTrading() external onlyOwner {
        tradingEnabled = true;
    }

    function pauseTrading() external onlyOwner {
        tradingEnabled = false;
    }

    function withdraw() external onlyOwner {
        (bool s,) = payable(msg.sender).call{value: address(this).balance}("");
        require(s);
    }

    function withdrawToken(address token_) external onlyOwner {
        require(token_ != address(0), 'Zero Address');
        IERC20(token_).transfer(msg.sender, IERC20(token_).balanceOf(address(this)));
    }

    function setBaseURI(string calldata newURI) external onlyOwner {
        baseURI = newURI;
    }

    function setURIExtention(string calldata newExtention) external onlyOwner {
        ending = newExtention;
    }

    function setMintToken(address newMintToken) external onlyOwner {
        mintToken = newMintToken;
    }

    function setMintRecipient(address mintRecipient_) external onlyOwner {
        mintRecipient = mintRecipient_;
    }

    function setRewardToken(address rewardToken_) external onlyOwner {
        rewardToken = rewardToken_;
    }

    function setRarityCost(uint8 rarity, uint256 cost) external onlyOwner {
        require(tokenRarity[rarity].multiplier > 0, 'Multiplier Not Set');
        tokenRarity[rarity].cost = cost;
    }

    function setRarityMultiplier(uint8 rarity, uint8 multiplier) external onlyOwner {
        require(tokenRarity[rarity].multiplier == 0, 'Multiplier Already Set');
        tokenRarity[rarity].multiplier = multiplier;
    }

    function setCanGiveRewards(address account, bool canGive) external onlyOwner {
        canGiveRewards[account] = canGive;
    }

    function setURIByRarity(bool isUriBasedOnRarity) external onlyOwner {
        UriByRarity = isUriBasedOnRarity;
    }

    /**
        Example:
        [ 0x123...abc, 0x456...def ]
        [ 5, 10 ]
        [ 3, 0 ]
        
        Gives the result:
        0x123...abc gets 5 Platinum NFTs
        0x456...def gets 10 Bronze NFTs

     */
    function ownerMint(address[] calldata tos, uint256[] calldata nums, uint8[] calldata raritys) external onlyOwner {
        
        uint addrLen = tos.length;
        require(addrLen == nums.length && nums.length == raritys.length, 'Invalid Lengths');
        
        for (uint i = 0; i < addrLen;) {
            for (uint j = 0; j < nums[i];) {
                _safeMint(tos[i], currentSupplyIndex, raritys[i]);
                unchecked { ++j; }
            }
            unchecked { ++i; }
        }
    }


    ////////////////////////////////////////////////
    ///////////     PUBLIC FUNCTIONS     ///////////
    ////////////////////////////////////////////////


    /** 
     * Mints `numberOfMints` NFTs To Caller
     */
    function mint(address to, uint256 num, uint8 rarity) external {
        require(
            tradingEnabled,
            'Trading Not Enabled'
        );
        require(
            num > 0, 
            'Invalid Number'
        );
        require(
            tokenRarity[rarity].cost > 0 && tokenRarity[rarity].multiplier > 0,
            'Invalid Rarity'
        );
        
        // determine cost in USDC
        uint256 cost = tokenRarity[rarity].cost * num;

        // ensure user has balance and approval of this amount
        require(
            IERC20(mintToken).balanceOf(msg.sender) >= cost,
            'Insufficient USDC Balance'
        );
        require(
            IERC20(mintToken).allowance(msg.sender, address(this)) >= cost,
            'Insufficient Allowance'
        );
        require(
            IERC20(mintToken).transferFrom(msg.sender, mintRecipient, cost),
            'Error Transfer From'
        );

        for (uint i = 0; i < num;) {
            _safeMint(to, currentSupplyIndex, rarity);
            unchecked { ++i; }
        }
    }

    function claimRewards() external nonReentrant {
        require(
            _balances[msg.sender] > 0,
            'Zero Balance'
        );
        _claimRewards(msg.sender);
    }


    function burn(uint256 tokenID) external nonReentrant {
        require(_isApprovedOrOwner(_msgSender(), tokenID), "caller not owner nor approved");
        _claimRewards(_owners[tokenID]);
        _burn(tokenID);
    }

    function giveRewards(uint256 amount) external nonReentrant {
        require(
            IERC20(rewardToken).balanceOf(msg.sender) >= amount,
            'Insufficient Balance'
        );
        require(
            IERC20(rewardToken).allowance(msg.sender, address(this)) >= amount,
            'Insufficient BTC Allowance'
        );
        require(
            IERC20(rewardToken).transferFrom(msg.sender, address(this), amount),
            'Failure Transfer From'
        );

        // give rewards
        unchecked {
            dividendsPerShare += ( amount * PRECISION ) / totalShares;
            totalRewards += amount;
        }        
    }

    receive() external payable {}

    /**
     * @dev See {IERC721-approve}.
     */
    function approve(address to, uint256 tokenId) public override {
        address wpowner = ownerOf(tokenId);
        require(to != wpowner, "ERC721: approval to current owner");

        require(
            _msgSender() == wpowner || isApprovedForAll(wpowner, _msgSender()),
            "ERC721: not approved or owner"
        );

        _approve(to, tokenId);
    }

    /**
     * @dev See {IERC721-setApprovalForAll}.
     */
    function setApprovalForAll(address _operator, bool approved) public override {
        _setApprovalForAll(_msgSender(), _operator, approved);
    }