// File: contracts/staking/Staking.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "./IStaking.sol";
import "../library/Manageable.sol";
import "../library/erc721A/IERC721A.sol";

error Staking__ZeroAddressProhibited();
error Staking__InvalidStakingBonusPercent();
error Staking__InvalidMaxCumulativeStakingBonusPercent();
error Staking__NotATokenOwner(address wallet, uint256 tokenId);
error Staking__EmptyTierRequirements();
error Staking__EmptyArray();
error Staking__StakingBatchIsTooBig();
error Staking__TokenIsAlreadyStaked(uint256 tokenId);
error Staking__TokenIsNotStaked(uint256 tokenId);

/**
 * @title Staking
 * @author DeployLabs.io
 *
 * @dev Staking contract for ERC721A collections. Allows to stake NFTs and get rewards.
 * Requieres {IERC721A} interface to be implemented by collection contract.
 */
contract Staking is IStaking, Manageable {
	struct WalletStakingInfo {
		uint32 lastStakingMilestoneUnixTime;
		uint32 savedStakingPoints;
		uint16 countOfStakedTokens;
	}

	/// @dev Fananees collection contract.
	IERC721A private immutable i_collectionContract;

	uint8 private s_stakingBonusPercent = 2;
	uint8 private s_maxCumulativeStakingBonusPercent = 20;

	/// @dev Staking tier requirements in staking points.
	uint32[] private s_stakingTierRequirements;

	/// @dev Mapping of token ID to their staking status.
	mapping(uint256 => bool) private s_isTokenStaked;

	/// @dev Mapping of wallet to staking info
	mapping(address => WalletStakingInfo) private s_walletStakingInfo;

	constructor(IERC721A collectionContract) {
		if (address(collectionContract) == address(0)) revert Staking__ZeroAddressProhibited();
		i_collectionContract = collectionContract;
	}

	/// @inheritdoc IStaking
	function stakeTokens(uint256[] calldata tokenIds) external {
		if (tokenIds.length == 0) revert Staking__EmptyArray();
		if (tokenIds.length > 100) revert Staking__StakingBatchIsTooBig();

		uint32 currentTime = uint32(block.timestamp);
		uint32 currentStakingPoints = getStakingPoints(msg.sender);
		uint16 currentCountOfStakedTokens = s_walletStakingInfo[msg.sender].countOfStakedTokens;

		s_walletStakingInfo[msg.sender] = WalletStakingInfo({
			lastStakingMilestoneUnixTime: currentTime,
			savedStakingPoints: currentStakingPoints,
			countOfStakedTokens: currentCountOfStakedTokens + uint16(tokenIds.length)
		});

		for (uint256 i = 0; i < tokenIds.length; i++) {
			uint256 tokenId = tokenIds[i];

			if (isTokenStaked(tokenId)) revert Staking__TokenIsAlreadyStaked(tokenId);
			if (i_collectionContract.ownerOf(tokenId) != msg.sender)
				revert Staking__NotATokenOwner(msg.sender, tokenId);

			s_isTokenStaked[tokenId] = true;
			emit TokenStaked(msg.sender, tokenId);
		}
	}

	/// @inheritdoc IStaking
	function unstakeTokens(uint256[] calldata tokenIds) external {
		if (tokenIds.length == 0) revert Staking__EmptyArray();
		if (tokenIds.length > 100) revert Staking__StakingBatchIsTooBig();

		uint32 currentTime = uint32(block.timestamp);
		uint32 currentStakingPoints = getStakingPoints(msg.sender);
		uint16 currentCountOfStakedTokens = s_walletStakingInfo[msg.sender].countOfStakedTokens;

		s_walletStakingInfo[msg.sender] = WalletStakingInfo({
			lastStakingMilestoneUnixTime: currentTime,
			savedStakingPoints: currentStakingPoints,
			countOfStakedTokens: currentCountOfStakedTokens - uint16(tokenIds.length)
		});

		for (uint256 i = 0; i < tokenIds.length; i++) {
			uint256 tokenId = tokenIds[i];

			if (!isTokenStaked(tokenId)) revert Staking__TokenIsNotStaked(tokenId);
			if (i_collectionContract.ownerOf(tokenId) != msg.sender)
				revert Staking__NotATokenOwner(msg.sender, tokenId);

			s_isTokenStaked[tokenId] = false;
			emit TokenUnstaked(msg.sender, tokenId);
		}
	}

	/**
	 * @dev Set the staking tier requirements for each tier.
	 *
	 * @param pointsPerTier Array of staking points for each tier.
	 */
	function setStakingTierRequirements(uint32[] memory pointsPerTier) external onlyManager {
		if (pointsPerTier.length == 0) revert Staking__EmptyTierRequirements();
		s_stakingTierRequirements = pointsPerTier;
	}

	/**
	 * @dev Set the staking bonus percent.
	 *
	 * @param stakingBonusPercent Staking bonus percent.
	 */
	function setStakingBonusPercent(uint8 stakingBonusPercent) external onlyManager {
		if (stakingBonusPercent > 100) revert Staking__InvalidStakingBonusPercent();
		s_stakingBonusPercent = stakingBonusPercent;
	}

	/**
	 * @dev Set the max cumulative staking bonus percent.
	 *
	 * @param maxCumulativeStakingBonusPercent Max cumulative staking bonus percent.
	 */
	function setMaxCumulativeStakingBonusPercent(
		uint8 maxCumulativeStakingBonusPercent
	) external onlyManager {
		if (maxCumulativeStakingBonusPercent > 100)
			revert Staking__InvalidMaxCumulativeStakingBonusPercent();
		s_maxCumulativeStakingBonusPercent = maxCumulativeStakingBonusPercent;
	}

	/// @inheritdoc IStaking
	function getStakingTier(address wallet) external view returns (uint16) {
		if (wallet == address(0)) revert Staking__ZeroAddressProhibited();

		uint32 stakingPoints = getStakingPoints(wallet);
		uint32[] memory stakingTierRequirementsInSeconds = s_stakingTierRequirements;

		uint16 stakingTier = 0;
		for (uint16 i = 0; i < stakingTierRequirementsInSeconds.length; i++) {
			if (stakingPoints < stakingTierRequirementsInSeconds[i]) break;
			stakingTier++;
		}

		return stakingTier;
	}

	/**
	 * @dev Get current amount of the staking points of a wallet.
	 *
	 * @param wallet Wallet to get staking points.
	 *
	 * @return Amount of the staking points.
	 */
	function getStakingPoints(address wallet) public view returns (uint32) {
		WalletStakingInfo storage walletStakingInfo = s_walletStakingInfo[wallet];
		uint32 savedStakingPoints = walletStakingInfo.savedStakingPoints;

		uint256 secondsPassed = block.timestamp - walletStakingInfo.lastStakingMilestoneUnixTime;
		uint32 basisStakingPointsPerSecond = getBasisStakingPointsPerSecond(wallet);
		uint32 stakingPointsGained = uint32((secondsPassed * basisStakingPointsPerSecond) / 100);

		return savedStakingPoints + stakingPointsGained;
	}

	/**
	 * @dev Get the amount of the staking points gained per second for a wallet.~
	 *
	 * @param wallet Wallet to get staking points per second.
	 *
	 * @return Amount of the staking points gained per second, multiplied by 100.
	 */
	function getBasisStakingPointsPerSecond(address wallet) public view returns (uint32) {
		if (wallet == address(0)) revert Staking__ZeroAddressProhibited();

		uint16 stakedTokensCount = s_walletStakingInfo[wallet].countOfStakedTokens;
		if (stakedTokensCount == 0) return 0;

		uint32 stakingBonusPercent = (stakedTokensCount - 1) * s_stakingBonusPercent;
		if (stakingBonusPercent > s_maxCumulativeStakingBonusPercent)
			stakingBonusPercent = s_maxCumulativeStakingBonusPercent;

		return 100 + stakingBonusPercent;
	}

	/**
	 * @dev Get the staking bonus percent for each additional staked token, except from the first one.
	 *
	 * @return Staking bonus percent, multiplied by 100.
	 */
	function getStakingBonusPercetnt() public view returns (uint32) {
		return s_stakingBonusPercent;
	}

	/**
	 * @dev Get the maximum cumulative staking bonus percent for all staked tokens.
	 *
	 * @return Maximum cumulative staking bonus percent.
	 */
	function getMaxCumulativeStakingBonusPercent() public view returns (uint32) {
		return s_maxCumulativeStakingBonusPercent;
	}

	/**
	 * @dev Get the stakin tier requirements in staking points needed to reach each tier.
	 *
	 * @return Array of staking points for each tier.
	 */
	function getStakingTierRequirements() public view returns (uint32[] memory) {
		return s_stakingTierRequirements;
	}

	/**
	 * @dev Get staking info for wallet.
	 *
	 * @param wallet Wallet to get staking info.
	 *
	 * @return Staking info for wallet.
	 */
	function getWalletStakingInfo(address wallet) public view returns (WalletStakingInfo memory) {
		return s_walletStakingInfo[wallet];
	}

	/// @inheritdoc IStaking
	function isTokenStaked(uint256 tokenId) public view returns (bool) {
		return s_isTokenStaked[tokenId];
	}
}

// File: contracts/library/erc721A/IERC721A.sol



// SPDX-License-Identifier: MIT
// ERC721A Contracts v4.2.0
// Creator: Chiru Labs

pragma solidity ^0.8.4;

/**
 * @dev Interface of ERC721A.
 */
interface IERC721A {
    /**
     * The caller must own the token or be an approved operator.
     */
    error ApprovalCallerNotOwnerNorApproved();

    /**
     * The token does not exist.
     */
    error ApprovalQueryForNonexistentToken();

    /**
     * The caller cannot approve to their own address.
     */
    error ApproveToCaller();

    /**
     * Cannot query the balance for the zero address.
     */
    error BalanceQueryForZeroAddress();

    /**
     * Cannot mint to the zero address.
     */
    error MintToZeroAddress();

    /**
     * The quantity of tokens minted must be more than zero.
     */
    error MintZeroQuantity();

    /**
     * The token does not exist.
     */
    error OwnerQueryForNonexistentToken();

    /**
     * The caller must own the token or be an approved operator.
     */
    error TransferCallerNotOwnerNorApproved();

    /**
     * The token must be owned by `from`.
     */
    error TransferFromIncorrectOwner();

    /**
     * Cannot safely transfer to a contract that does not implement the
     * ERC721Receiver interface.
     */
    error TransferToNonERC721ReceiverImplementer();

    /**
     * Cannot transfer to the zero address.
     */
    error TransferToZeroAddress();

    /**
     * The token does not exist.
     */
    error URIQueryForNonexistentToken();

    /**
     * The `quantity` minted with ERC2309 exceeds the safety limit.
     */
    error MintERC2309QuantityExceedsLimit();

    /**
     * The `extraData` cannot be set on an unintialized ownership slot.
     */
    error OwnershipNotInitializedForExtraData();

    // =============================================================
    //                            STRUCTS
    // =============================================================

    struct TokenOwnership {
        // The address of the owner.
        address addr;
        // Stores the start time of ownership with minimal overhead for tokenomics.
        uint64 startTimestamp;
        // Whether the token has been burned.
        bool burned;
        // Arbitrary data similar to `startTimestamp` that can be set via {_extraData}.
        uint24 extraData;
    }

    // =============================================================
    //                         TOKEN COUNTERS
    // =============================================================

    /**
     * @dev Returns the total number of tokens in existence.
     * Burned tokens will reduce the count.
     * To get the total number of tokens minted, please see {_totalMinted}.
     */
    function totalSupply() external view returns (uint256);

    // =============================================================
    //                            IERC165
    // =============================================================

    /**
     * @dev Returns true if this contract implements the interface defined by
     * `interfaceId`. See the corresponding
     * [EIP section](https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified)
     * to learn more about how these ids are created.
     *
     * This function call must use less than 30000 gas.
     */
    function supportsInterface(bytes4 interfaceId) external view returns (bool);

    // =============================================================
    //                            IERC721
    // =============================================================

    /**
     * @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`,
     * 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 be 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,
        bytes calldata data
    ) external;

    /**
     * @dev Equivalent to `safeTransferFrom(from, to, tokenId, '')`.
     */
    function safeTransferFrom(
        address from,
        address to,
        uint256 tokenId
    ) external;

    /**
     * @dev Transfers `tokenId` 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);

    // =============================================================
    //                        IERC721Metadata
    // =============================================================

    /**
     * @dev Returns the token collection name.
     */
    function name() external view returns (string memory);

    /**
     * @dev Returns the token collection symbol.
     */
    function symbol() external view returns (string memory);

    /**
     * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
     */
    function tokenURI(uint256 tokenId) external view returns (string memory);

    // =============================================================
    //                           IERC2309
    // =============================================================

    /**
     * @dev Emitted when tokens in `fromTokenId` to `toTokenId`
     * (inclusive) is transferred from `from` to `to`, as defined in the
     * [ERC2309](https://eips.ethereum.org/EIPS/eip-2309) standard.
     *
     * See {_mintERC2309} for more details.
     */
    event ConsecutiveTransfer(uint256 indexed fromTokenId, uint256 toTokenId, address indexed from, address indexed to);
}
// File: contracts/library/Manageable.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;

import "@openzeppelin/contracts/access/Ownable.sol";

error Manageable__WalletIsNotAManager();
error Manageable__ZeroAddressProhibited();

/**
 * @title Manageable
 * @author DeployLabs.io
 *
 * @dev This module is an extention of {Ownable} contract. It will make available
 * the modifier `onlyManager`, which can be applied to your functions to restrict their use to managers.
 * Managers can be added and removed by the owner.
 * Note, that the owner of the contract is also considered a manager.
 */
abstract contract Manageable is Ownable {
	/// @dev Mapping of wallet to their manager status.
	mapping(address => bool) private s_managers;

	/// @dev Throws if called by any account other than a manager.
	modifier onlyManager() {
		_checkManager();
		_;
	}

	constructor() {}

	/// @dev Add a list of addresses to a list of managers.
	function addManagers(address[] calldata wallets) external onlyOwner {
		for (uint256 index = 0; index < wallets.length; index++) {
			_addManager(wallets[index]);
		}
	}

	/// @dev Remove a list of addresses from a list of managers.
	function removeManagers(address[] calldata wallets) external onlyOwner {
		for (uint256 index = 0; index < wallets.length; index++) {
			_removeManager(wallets[index]);
		}
	}

	/// @dev Check, whether a wallet is a manager or not.
	function isManager(address wallet) public view returns (bool) {
		if (owner() == wallet) return true;
		return s_managers[wallet];
	}

	/// @dev Add an address to a list of managers.
	function _addManager(address wallet) internal {
		if (wallet == address(0)) revert Manageable__ZeroAddressProhibited();

		s_managers[wallet] = true;
	}

	/// @dev Remove an address from a list of managers.
	function _removeManager(address wallet) internal {
		if (!isManager(wallet)) revert Manageable__WalletIsNotAManager();
		if (wallet == address(0)) revert Manageable__ZeroAddressProhibited();

		s_managers[wallet] = false;
	}

	/// @dev Throws if the sender is not a manager.
	function _checkManager() internal view {
		if (!isManager(_msgSender())) revert Manageable__WalletIsNotAManager();
	}
}

// File: contracts/staking/IStaking.sol



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

interface IStaking {
	/// @dev Emitted when a token is staked.
	event TokenStaked(address owner, uint256 indexed tokenId);
	/// @dev Emitted when a token is unstaked.
	event TokenUnstaked(address owner, uint256 indexed tokenId);

	/**
	 * @dev Stake a list of tokens.
	 * Each extra token should give a bonus of X% or Y% to staking points gain.
	 *
	 * @param tokenIds List of token IDs to stake.
	 */
	function stakeTokens(uint256[] calldata tokenIds) external;

	/**
	 * @dev Unstake a list of tokens.
	 * Progress is saved on the wallet, so tokens can be unstaked and staked again.
	 *
	 * @param tokenIds List of token IDs to unstake.
	 */
	function unstakeTokens(uint256[] calldata tokenIds) external;

	/**
	 * @dev Check if a token is staked.
	 *
	 * @param tokenId Token ID to check.
	 *
	 * @return True if token is staked.
	 */
	function isTokenStaked(uint256 tokenId) external view returns (bool);

	/**
	 * @dev Get current staking tier of a wallet.
	 *
	 * @param wallet Wallet address to check.
	 *
	 * @return Current staking tier.
	 */
	function getStakingTier(address wallet) external view returns (uint16);
}

// File: @openzeppelin/contracts/access/Ownable.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)

pragma solidity ^0.8.0;

import "../utils/Context.sol";

/**
 * @dev Contract module which provides a basic access control mechanism, where
 * there is an account (an owner) that can be granted exclusive access to
 * specific functions.
 *
 * By default, the owner account will be the one that deploys the contract. This
 * can later be changed with {transferOwnership}.
 *
 * This module is used through inheritance. It will make available the modifier
 * `onlyOwner`, which can be applied to your functions to restrict their use to
 * the owner.
 */
abstract contract Ownable is Context {
    address private _owner;

    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    /**
     * @dev Initializes the contract setting the deployer as the initial owner.
     */
    constructor() {
        _transferOwnership(_msgSender());
    }

    /**
     * @dev Throws if called by any account other than the owner.
     */
    modifier onlyOwner() {
        _checkOwner();
        _;
    }

    /**
     * @dev Returns the address of the current owner.
     */
    function owner() public view virtual returns (address) {
        return _owner;
    }

    /**
     * @dev Throws if the sender is not the owner.
     */
    function _checkOwner() internal view virtual {
        require(owner() == _msgSender(), "Ownable: caller is not the owner");
    }

    /**
     * @dev Leaves the contract without owner. It will not be possible to call
     * `onlyOwner` functions anymore. Can only be called by the current owner.
     *
     * NOTE: Renouncing ownership will leave the contract without an owner,
     * thereby removing any functionality that is only available to the owner.
     */
    function renounceOwnership() public virtual onlyOwner {
        _transferOwnership(address(0));
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Can only be called by the current owner.
     */
    function transferOwnership(address newOwner) public virtual onlyOwner {
        require(newOwner != address(0), "Ownable: new owner is the zero address");
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal virtual {
        address oldOwner = _owner;
        _owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }
}

// File: @openzeppelin/contracts/utils/Context.sol



// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts v4.4.1 (utils/Context.sol)

pragma solidity ^0.8.0;

/**
 * @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;
    }
}