// Contract: MYTHICAL
// Target function: function beginStaking(address _stakingContract, uint256 _dailyYield) public onlyOwner
error NewOwnerAddressZero();
error TokenURINonexistentToken();
error TokensZeroBalance();
error TokensOfOwnerNullValue();
error CallerNotStakingHelper();
error CallerNotOwner();

/**
 * Built to optimize for lower gas during batch mints and transfers. 
 * A new locking mechanism has been added to protect users from all attempted scams.
 * New "Phantom Staking" is supported allowing for users to participate in token staking without large gas overhead or transferring tokens.
 *
 * Assumes serials are sequentially minted starting at 0 (e.g. 0, 1, 2, 3..).
 *
 */
abstract contract ERC721LPhantomStakeable {
    using Address for address;
    using Strings for uint256;

    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);
    event Locked(address indexed owner, uint256 unlockCooldown);
    event Unlocked(address indexed owner, uint256 unlockTimestamp);
    event Staked(address indexed owner, uint256 stakedTimestamp);
    event Unstaked(address indexed owner, uint256 unstakedTimestamp);
    event StakingEventStarted(address indexed stakingContract, uint256 dailyYield, uint256 timestamp);
    event StakingEventEnded(uint256 timestamp);
    event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

    struct addressData {
        uint64 balance;
        bool staked;
        bool locked;
        uint64 lockedUnlockCooldown;
        uint64 lockedUnlockTimestamp;
    }

    struct collectionData {
        string name;
        string symbol;
        address owner;
        uint256 index;
        uint256 burned;
    }

    struct stakingData {
        IStakingHelper stakingContract;
        bool stakingStatus;
    }

    collectionData internal _collectionData;
    stakingData internal _stakingData;

    mapping(uint256 => address) internal _ownerships;
    mapping(address => addressData) internal _addressData;
    mapping(uint256 => address) internal _tokenApprovals;
    mapping(address => mapping(address => bool)) private  _operatorApprovals;

    constructor(string memory _name, string memory _symbol) {
        _collectionData.name = _name;
        _collectionData.symbol = _symbol;
        _transferOwnership(_msgSender());
    }

    function _msgSender() internal view returns (address) {
        return msg.sender;
    }

    /**
     * @dev Returns whether `tokenId` exists.
     *
     * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}.
     *
     * Tokens start existing when they are minted (`_mint` or `_safeMint`),
     */
    function _exists(uint256 tokenId) public view virtual returns (bool) {
        return tokenId < _collectionData.index;
    }

    /**
     * @dev Returns the owner of the `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function ownerOf(uint256 tokenId) public view virtual returns (address) {
        unchecked {
            if (tokenId < _collectionData.index) {
                address ownership = _ownerships[tokenId];
                if (ownership != address(0)) {
                    return ownership;
                }
                    while (true) {
                        tokenId--;
                        ownership = _ownerships[tokenId];

                        if (ownership != address(0)) {
                            return ownership;
                        }
                         
                    }
                }
            }

        revert OwnerOfNullValue();
    }

    /**
     * @dev Returns the number of tokens in `_owner`'s account.
     */
    function balanceOf(address _address) public view returns (uint256) {
        if (_address == address(0)) revert BalanceOfZeroAddress();
        return uint256(_addressData[_address].balance);
    }

    /**
     * @dev Returns whether `_owner`'s tokens are currently unlocked.
     */
    function isUnlocked(address _owner) public view returns (bool) {
        return 
            !_addressData[_owner].locked && 
            _addressData[_owner].lockedUnlockTimestamp < block.timestamp && 
            (_stakingData.stakingStatus ? !_addressData[_owner].staked : true);
    }

    /**
     * @dev Mints `quantity` tokens and transfers them to `to`.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     *
     * Emits a {Transfer} event.
     */
    function _mint(address to, uint256 quantity) internal {
        if (to == address(0)) revert MintZeroAddress();
        if (quantity == 0) revert MintZeroQuantity();

        if (_stakingData.stakingStatus && _addressData[to].staked && _stakingData.stakingContract.needsRewardsUpdate(to)){
            _stakingData.stakingContract.aggregateRewards(to);
        }

        unchecked {
            uint256 updatedIndex = _collectionData.index;
            _addressData[to].balance += uint64(quantity);
            _ownerships[updatedIndex] = to;
            
            for (uint256 i; i < quantity; i++) {
                emit Transfer(address(0), to, updatedIndex++);
            }

            _collectionData.index = updatedIndex;
        }
    }

    /**
     * @dev See Below {ERC721L-_safeMint}.
     */
    function _safeMint(address to, uint256 quantity) internal {
        _safeMint(to, quantity, '');
    }

    /**
     * @dev Safely mints `quantity` tokens and transfers them to `to`.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - If `to` refers to a smart contract, it must implement {onERC721Received}, which is called for each safe transfer.
     *
     * Emits a {Transfer} event.
     */
    function _safeMint(address to, uint256 quantity, bytes memory _data) internal {
        if (to == address(0)) revert SafeMintZeroAddress();
        if (quantity == 0) revert SafeMintZeroQuantity();

        if (_stakingData.stakingStatus && _addressData[to].staked && _stakingData.stakingContract.needsRewardsUpdate(to)){
            _stakingData.stakingContract.aggregateRewards(to);
        }

        unchecked {
            uint256 updatedIndex = _collectionData.index;
            _addressData[to].balance += uint64(quantity);
            _ownerships[updatedIndex] = to;
            
            for (uint256 i; i < quantity; i++) {
                emit Transfer(address(0), to, updatedIndex);
                if(to.code.length > 0 &&
                        ERC721TokenReceiver(to).onERC721Received(_msgSender(), address(0), updatedIndex, _data) !=
                        ERC721TokenReceiver.onERC721Received.selector) revert SafeMintUnsafeDestination();
                updatedIndex++;
            }

            _collectionData.index = updatedIndex;
        }
    }

    /**
     * @dev Transfers `tokenId` from `from` to `to`.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - `tokenId` token must be owned by `from`.
     * - `from` must not have tokens locked.
     *
     * Emits a {Transfer} event.
     */
    function transferFrom(address from, address to, uint256 tokenId) public {
        address currentOwner = ownerOf(tokenId);
        if (!isUnlocked(from)) revert TokensLocked();
        if (_msgSender() != currentOwner &&
            getApproved(tokenId) != _msgSender() &&
            !isApprovedForAll(currentOwner,_msgSender())) revert CallerLacksTransferPermissions();
        if (currentOwner != from) revert TransferFromNotOwner();
        if (to == address(0)) revert TransferZeroAddress();

        if (_stakingData.stakingStatus && _addressData[to].staked && _stakingData.stakingContract.needsRewardsUpdate(to)){
            _stakingData.stakingContract.aggregateRewards(to);
        }

        delete _tokenApprovals[tokenId]; 
        unchecked {
            _addressData[from].balance -= 1;
            _addressData[to].balance += 1;
            _ownerships[tokenId] = to;
            uint256 nextTokenId = tokenId + 1;
            if (_ownerships[nextTokenId] == address(0) && nextTokenId < _collectionData.index) {
                _ownerships[nextTokenId] = currentOwner;
            }
        }

        emit Transfer(from, to, tokenId);
    }

    /**
     * @dev See Below {ERC721L-safeTransferFrom}.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId) public virtual {
        safeTransferFrom(from, to, tokenId, '');
    }

    /**
     * @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 {ERC721TokenReceiver}, which is called upon a safe transfer.
     * - `from` must not have tokens locked.
     *
     * Emits a {Transfer} event.
     */
    function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory _data) public virtual {
        transferFrom(from, to, tokenId);
        if (to.code.length > 0 &&
                ERC721TokenReceiver(to).onERC721Received(_msgSender(), address(0), tokenId, _data) !=
                ERC721TokenReceiver.onERC721Received.selector) revert SafeTransferUnsafeDestination();
    }

    /**
     * @dev Batch transfers `quantity` tokens sequentially starting at `startID` from `from` to `to`.
     *
     * Requirements:
     *
     * - `to` cannot be the zero address.
     * - `startID` token and all sequential tokens must exist and be owned by `from`.
     * - `from` must not have tokens locked.
     *
     * Emits `quantity` number of {Transfer} events.
     */
    function batchTransferFrom(address from, address to, uint256 startID, uint256 quantity) public virtual {
        _batchTransferFrom(from, to, startID, quantity, false, '');
    }

    /**
     * @dev See Below {ERC721L-batchSafeTransferFrom}.
     */
    function batchSafeTransferFrom(address from, address to, uint256 startID, uint256 quantity) public virtual {
        batchSafeTransferFrom(from, to, startID, quantity, '');
    }

    /**
     * @dev Safely batch transfers `quantity` tokens sequentially starting at `startID` 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.
     * - `startID` token and all sequential tokens 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 {ERC721TokenReceiver}, which is called upon a safe transfer.
     * - `from` must not have tokens locked.
     *
     * Emits `quantity` number of {Transfer} events.
     */
    function batchSafeTransferFrom(address from, address to, uint256 startID, uint256 quantity, bytes memory _data) public virtual {
        _batchTransferFrom(from, to, startID, quantity, true, _data);
    }

    function _batchTransferFrom (address from, address to, uint256 startID, uint256 quantity, bool safeTransferCheck, bytes memory _data) internal {
        if (!isUnlocked(from)) revert TokensLocked();
        if (_msgSender() != from && !isApprovedForAll(from,_msgSender())) revert CallerLacksTransferPermissions();
        if (!multiOwnerCheck(from, startID, quantity)) revert TokensNotBatchable();
        if (to == address(0)) revert TransferZeroAddress();

        if (_stakingData.stakingStatus && _addressData[to].staked && _stakingData.stakingContract.needsRewardsUpdate(to)){
            _stakingData.stakingContract.aggregateRewards(to);
        }

        unchecked {
            for (uint256 i; i < quantity; i++) {
                uint256 currentToken = startID + i;
                delete _tokenApprovals[currentToken];

                if (i == 0){
                    _ownerships[currentToken] = to;
                } else {
                    delete _ownerships[currentToken];
                }
                emit Transfer(from, to, currentToken);
                if (safeTransferCheck){
                    if(to.code.length > 0 &&
                        ERC721TokenReceiver(to).onERC721Received(_msgSender(), address(0), currentToken, _data) !=
                        ERC721TokenReceiver.onERC721Received.selector) revert SafeTransferUnsafeDestination();
                }
            }

            _addressData[from].balance -= uint64(quantity);
            _addressData[to].balance += uint64(quantity);
            uint256 nextTokenId = startID + quantity;
            if (_ownerships[nextTokenId] == address(0) && nextTokenId < _collectionData.index) {
                _ownerships[nextTokenId] = from;
            }
        }
    }

    /**
     * @dev Returns the total amount of tokens stored by the contract.
     */
    function totalSupply() public view returns (uint256) {
        unchecked {
            return _collectionData.index - _collectionData.burned;
        }
    }

    /**
     * @dev Returns whether `_addressToCheck` is the owner of `quantity` tokens sequentially starting from `startID`.
     *
     * Requirements:
     *
     * - `startID` token and all sequential tokens must exist.
     */
    function multiOwnerCheck(address _addressToCheck, uint256 startID, uint256 quantity) internal view returns (bool) {
        if (quantity < 2) revert BatchQuantityTooSmall();
        unchecked {
            for (uint256 i; i < quantity; i++) {
                if (ownerOf(startID + i) != _addressToCheck){
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * @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.
     * - Owner must not have tokens locked.
     *
     * Emits an {ApprovalForAll} event.
     */
    function setApprovalForAll(address operator, bool approved) public {
        if (operator == _msgSender()) revert ApprovalToOwner();
        if (!isUnlocked(_msgSender())) revert TokensLocked();

        _operatorApprovals[_msgSender()][operator] = approved;
        emit ApprovalForAll(_msgSender(), operator, approved);
    }

    /**
     * @dev Returns if the `operator` is allowed to manage all of the assets of `_owner` and tokens are unlocked for `_owner`.
     *
     * See {setApprovalForAll}
     */
    function isApprovedForAll(address _owner, address operator) public view returns (bool) {
        return !isUnlocked(_owner) ? false : _operatorApprovals[_owner][operator];
    }

    /**
     * @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) public {
        address tokenOwner = ownerOf(tokenId);
        if (to == tokenOwner) revert ApprovalToOwner();
        if (_msgSender() != tokenOwner && !isApprovedForAll(tokenOwner, _msgSender())) revert CallerLacksApprovalPermissions();
        _tokenApprovals[tokenId] = to;
        emit Approval(tokenOwner, to, tokenId);
    }

    /**
     * @dev Returns the account approved for `tokenId` token.
     *
     * Requirements:
     *
     * - `tokenId` must exist.
     */
    function getApproved(uint256 tokenId) public view returns (address) {
        if (!_exists(tokenId)) revert getApprovedNonexistentToken();
        return _tokenApprovals[tokenId];
    }

    /**
     * @dev Stakes `_owner`'s tokens, if optionallock is enabled cannot be unstaked for optionalStakingLockPeriod days.
     * Requirements:
     *
     * - The `caller` cannot have their tokens staked currently.
     *
     * Emits an {Staked} event.
     */
    function stake(address _owner) public stakingHelper {
        if (!_stakingData.stakingStatus) revert stakingNotActive();
        if (_addressData[_owner].staked) revert TokensStaked();

        _addressData[_owner].staked = true;
        emit Staked(_msgSender(), block.timestamp);
    }

    /**
     * @dev Unstakes `_owner`'s tokens if they arent in a locked staking cycle.
     * Requirements:
     *
     * - The `_owner` cannot have their tokens unstaked currently.
     * - The `_owner` cannot have their tokens optionally locked currently.
     *
     * Emits an {Unstaked} event.
     */
    function unstake(address _owner) public stakingHelper {
        if (!_addressData[_owner].staked) revert TokensUnstaked();

        delete _addressData[_owner].staked;
        emit Unstaked(_owner, block.timestamp);
    }