contract Oddworx is ERC20, Pausable {

    IGenzee immutable public _genzeeContract;
    bool public nftHoldRewardsActive = true;
    uint256 public NFT_STAKING_WEEKLY_REWARD_AMOUNT = 20 * 10 ** 18; // Doesn't have to be public
    uint256 constant public NFT_HOLDING_WEEKLY_REWARD_AMOUNT = 10 * 10 ** 18;
    
    struct StakingData {
        address ownerAddress; // 160 bits
        uint96 timestamp; // 96 bits
    }

    mapping(address => bool) public adminAddresses;
    mapping(uint256 => StakingData) public stakedNft;
    mapping(uint256 => uint256) public latestUnstakedClaimTimestamp;
    
    constructor(address _nftContractAddress) ERC20("Oddworx", "ODDX", 18) {
        _mint(msg.sender, 1000000000 * 10 ** 18);
        _genzeeContract = IGenzee(_nftContractAddress);
        adminAddresses[msg.sender] = true;
    }

    /// @notice emitted when an item is purchased
    /// @param user address of the user that purchased an item
    /// @param itemSKU the SKU of the item purchased
    /// @param price the amount paid for the item
    event ItemPurchased(address indexed user, uint256 itemSKU, uint256 price);

    /// @notice emitted when a user stakes a token
    /// @param user address of the user that staked the Genzee
    /// @param genzee the id of the Genzee staked
    event StakedNft(address indexed user, uint256 indexed genzee);

    /// @notice emitted when a user unstakes a token
    /// @param user address of the user that unstaked the Genzee
    /// @param genzee the id of the Genzee unstaked
    /// @param amount the amount of ODDX claimed
    event UnstakedNft(address indexed user, uint256 indexed genzee, uint256 amount);

    /// @notice emitted when a user claim NFT rewards
    /// @param user address of the user that claimed ODDX
    /// @param genzee the id of the Genzee that generated the rewards
    /// @param amount the amount of ODDX claimed
    event UserClaimedNftRewards(address indexed user, uint256 indexed genzee, uint256 amount);

    modifier onlyAdmin() {
        if (adminAddresses[msg.sender]  != true) revert NotAdmin();
        _;
    }

    /*///////////////////////////////////////////////////////////////
                             General Functions
    //////////////////////////////////////////////////////////////*/
    function mint(address to, uint256 amount) public onlyAdmin {
        _mint(to, amount);
    }

    function burn(address _from, uint256 amount) public onlyAdmin {
        if (balanceOf[_from] < amount) revert InvalidInput();
        _burn(_from, amount);
    }

    function toggleAdminContract(address _adminAddress) public onlyAdmin {
         adminAddresses[_adminAddress] = !adminAddresses[_adminAddress];
    }

    function pause() public onlyAdmin {
        _pause();
    }

    function unpause() public onlyAdmin {
        _unpause();
    }

    /*///////////////////////////////////////////////////////////////
                             Shop features
    //////////////////////////////////////////////////////////////*/
    /// @notice Buy item in shop by burning Oddx.
    /// @param itemSKU A unique ID used to identify shop products.
    /// @param amount Amount of Oddx to burn.
    function buyItem(uint itemSKU, uint amount) public whenNotPaused {
        if (balanceOf[msg.sender] < amount) revert InvalidInput();
        _burn(msg.sender, amount);
        emit ItemPurchased(msg.sender, itemSKU, amount);
    }

    /*///////////////////////////////////////////////////////////////
                             NFT Staking Rewards
    //////////////////////////////////////////////////////////////*/
    /// @notice stake genzees in this contract. 
    /// The Genzee owners need to approve this contract for transfering
    /// before calling this function.
    /// @param genzeeIds list of genzee ids to stake.
    function stakeNfts(uint256[] calldata genzeeIds) external whenNotPaused {
        if (genzeeIds.length == 0) revert InvalidInput();
        uint256 genzeeId;
        for (uint256 i; i < genzeeIds.length; i++) {
            genzeeId = genzeeIds[i];
            // No need to check ownership since transferFrom already checks that
            // and the caller of this function should be the Genzee owner
            stakedNft[genzeeId] = StakingData(msg.sender, uint96(block.timestamp));
            _genzeeContract.transferFrom(msg.sender, address(this), genzeeId);
            emit StakedNft(msg.sender, genzeeId);
        }
    }

    /// @notice unstake genzees back to they rightful owners and pay them what it's owed in ODDX.
    /// @param genzeeIds list of genzee ids to unstake.
    function unstakeNfts(uint256[] calldata genzeeIds) external whenNotPaused {
        if (genzeeIds.length == 0) revert InvalidInput();

        // total rewards amount to claim (all genzees)
        uint256 totalRewards;

        // loop variables

        // rewards for current genzee in the loop below
        uint256 rewards;

        // current genzeeid in the loop below
        uint256 genzeeId;

        // staking information for the current genzee in the loop below
        StakingData memory stake;

        for (uint256 i; i < genzeeIds.length; i++) {
            genzeeId = genzeeIds[i];
            stake = stakedNft[genzeeId];

            if (stake.ownerAddress != msg.sender && stake.ownerAddress != address(0))  {
                revert NotOwnerOfToken();
            }

            rewards = _stakedRewardsForTimestamp(genzeeId);
            totalRewards += rewards;

            // Reset timestamp for unstaked rewards
            latestUnstakedClaimTimestamp[genzeeId] = block.timestamp;

            // No need to run safeTransferFrom because we're returning the NFT to
            // an address that was holding it before
            _genzeeContract.transferFrom(address(this), stake.ownerAddress, genzeeId);

            delete stakedNft[genzeeId];

            emit UnstakedNft(msg.sender, genzeeId, rewards);
        }

        if (totalRewards > 0) {
            _mint(msg.sender, totalRewards);
        }
    }

    /// @notice Claim Staked NFT rewards in ODDX. Also resets general holding
    /// timestamp so users can't double-claim for holding/staking.
    /// @param genzeeIds list of genzee ids to claim rewards for.
    function claimStakedNftRewards(uint256[] calldata genzeeIds) external whenNotPaused {
        if (genzeeIds.length == 0) revert InvalidInput();

        // total rewards amount to claim (all genzees)
        uint256 totalRewards;

        // loop variables

        // rewards for current genzee in the loop below
        uint256 rewards;

        // current genzeeid in the loop below
        uint256 genzeeId;

        // staking information for the current genzee in the loop below
        StakingData memory stake;

        for (uint256 i; i < genzeeIds.length; i++) {
            genzeeId = genzeeIds[i];
            stake = stakedNft[genzeeId];

            if (stake.ownerAddress != msg.sender) revert NotOwnerOfToken();

            rewards = _stakedRewardsForTimestamp(stake.timestamp);
            totalRewards += rewards;

            stakedNft[genzeeId].timestamp = uint96(block.timestamp);

            emit UserClaimedNftRewards(msg.sender, genzeeId, rewards);
        }

        if (totalRewards == 0) revert NoTokensToClaim();
        _mint(msg.sender, totalRewards);
    }

    /// @notice Calculate staking rewards owed per week.
    /// @param genzeeId NFT id to check.
    /// @return Returns amount of tokens available to claim
    function calculateStakedNftRewards(uint256 genzeeId) external view returns (uint256) {
        uint256 timestamp = stakedNft[genzeeId].timestamp;
        return _stakedRewardsForTimestamp(timestamp);
    }

    function _stakedRewardsForTimestamp(uint256 timestamp) private view returns (uint256) {
        return timestamp > 0
            ? NFT_STAKING_WEEKLY_REWARD_AMOUNT * ((block.timestamp - timestamp) / 1 weeks)
            : 0;
    }

    /// @notice Updates amount users are rewarded weekly.
    /// @param newAmount new amount to use, supply number in wei.
    function updateNftStakedRewardAmount(uint256 newAmount) external onlyAdmin {
        NFT_STAKING_WEEKLY_REWARD_AMOUNT = newAmount;
    }


    /*///////////////////////////////////////////////////////////////
                             NFT Holding Rewards
    //////////////////////////////////////////////////////////////*/
    /// @notice Claim rewards for NFT if we are the holder. 
    /// Rewards are attached to the NFT, so if it's transferred, new owner can claim tokens
    /// But the intital rewards can't be reclaimed
    /// @param genzeeIds list of genzee ids to stake.
    function claimNftHoldRewards(uint256[] calldata genzeeIds) external whenNotPaused {
        if (!nftHoldRewardsActive) revert NftHoldRewardsNotActive();
        if (genzeeIds.length == 0) revert InvalidInput();

        // total rewards amount to claim (all genzees)
        uint256 totalRewards;

        // loop variables

        // rewards for current genzee in the loop below
        uint256 rewards;

        // current genzeeid in the loop below
        uint256 genzeeId;

        // last time owner claimed rewards for the genzee
        uint256 latestClaimTimestamp;

        for (uint256 i; i < genzeeIds.length; i++) {
            genzeeId = genzeeIds[i];
            if (_genzeeContract.ownerOf(genzeeId) != msg.sender) revert NotOwnerOfToken();
            latestClaimTimestamp = latestUnstakedClaimTimestamp[genzeeId];

            rewards = _unstakedRewardsForTimestamp(latestClaimTimestamp);
            totalRewards += rewards;

            // Set claim timestamp to now
            latestUnstakedClaimTimestamp[genzeeId] = block.timestamp;

            emit UserClaimedNftRewards(msg.sender, genzeeId, rewards);
        }

        if (totalRewards == 0) revert NoTokensToClaim();
        _mint(msg.sender, totalRewards);
    }

    function calculateNftHoldReward(uint256 _nftIndex) external view returns (uint256)  {
        uint256 timestamp = latestUnstakedClaimTimestamp[_nftIndex];
        return _unstakedRewardsForTimestamp(timestamp);
    }

    function _unstakedRewardsForTimestamp(uint256 timestamp) private view returns (uint256) {
        return (timestamp > 0)
            ? NFT_HOLDING_WEEKLY_REWARD_AMOUNT * ((block.timestamp - timestamp) / 1 weeks)
            : NFT_HOLDING_WEEKLY_REWARD_AMOUNT;
    }

    function toggleNftHoldRewards() external onlyAdmin {
        nftHoldRewardsActive = !nftHoldRewardsActive;
    }
}
/// @title Oddworx
/// @author Mytchall / Alephao
/// @notice General functions to mint, burn, reward NFT holders incl staking
function buyItem(uint itemSKU, uint amount) public whenNotPaused {
    if (balanceOf[msg.sender] < amount) revert InvalidInput();
    _burn(msg.sender, amount);
    emit ItemPurchased(msg.sender, itemSKU, amount);
}

/// @notice Buy item in shop by burning Oddx.
/// @param itemSKU A unique ID used to identify shop products.
/// @param amount Amount of Oddx to burn.