// Contract: MYTHICAL
// Target function: function stakingInformation() public view returns (address, bool)
.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);
    }

    /**
     * @dev Begins Staking Event. 
     * Requirements:
     *
     * - Staking must be inactive.
     * - `_stakingContract` cannot be null address.
     *
     * Emits a {StakingEventStarted} event.
     */
    function beginStaking(address _stakingContract, uint256 _dailyYield) public onlyOwner {
        if (_stakingData.stakingStatus) revert StakingActive();
        if (_stakingContract == address(0)) revert beginStakingZeroAddress();

        _stakingData.stakingContract = IStakingHelper(_stakingContract);
        _stakingData.stakingStatus = true;
        _stakingData.stakingContract.startSeason(_dailyYield);
        
        emit StakingEventStarted(_stakingContract, _dailyYield, block.timestamp);
    }

    /**
     * @dev Ends Staking Event.
     * Requirements:
     *
     * - Staking must be active.
     *
     * Emits a {StakingEnded} event.
     */
    function endStaking() public onlyOwner {
        if (!_stakingData.stakingStatus) revert StakingInactive();

        _stakingData.stakingContract.endSeason();
        delete _stakingData;
        
        emit StakingEventEnded(block.timestamp);
    }

    /**
     * @dev Locks `_owner`'s tokens from any form of transferring.
     * Requirements:
     *
     * - The `caller` cannot have their tokens locked currently.
     *
     * Emits a {Locked} event.
     */
    function lock(uint256 _cooldown) public {
        if (_addressData[_msgSender()].locked) revert TokensLocked();
        if (_cooldown < 1 || _cooldown > 30) revert UnsupportedCooldownDuration();

        unchecked {
            uint256 proposedCooldown = _cooldown * 1 days;
            if (block.timestamp + proposedCooldown < _addressData[_msgSender()].lockedUnlockTimestamp) revert CooldownTooSmall();
            _addressData[_msgSender()].locked = true;
            _addressData[_msgSender()].lockedUnlockCooldown = uint64(proposedCooldown);
        }
        
        emit Locked(_msgSender(), _cooldown);
    }

    /**
     * @dev Begins unlocking process for `_owner`'s tokens.
     * Requirements:
     *
     * - The `caller` cannot have their tokens unlocked currently.
     *
     * Emits an {Unlocked} event.
     */
    function unlock() public {
        if (!_addressData[_msgSender()].locked) revert TokensUnlocked();

        delete _addressData[_msgSender()].locked;
        unchecked {
            _addressData[_msgSender()].lockedUnlockTimestamp = uint64(block.timestamp + _addressData[_msgSender()].lockedUnlockCooldown);
        }
        emit Unlocked(_msgSender(), block.timestamp);
    }

    /**
     * @dev Returns the address of the current collection owner.
     */
    function owner() public view returns (address) {
        return _collectionData.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 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 onlyOwner {
        if (newOwner == address(0)) revert NewOwnerAddressZero();
        _transferOwnership(newOwner);
    }

    /**
     * @dev Transfers ownership of the contract to a new account (`newOwner`).
     * Internal function without access restriction.
     */
    function _transferOwnership(address newOwner) internal {
        address oldOwner = _collectionData.owner;
        _collectionData.owner = newOwner;
        emit OwnershipTransferred(oldOwner, newOwner);
    }

    function name() public view returns (string memory) {
        return _collectionData.name;
    }

    /**
     * @dev Returns the token collection symbol.
     */
    function symbol() public view returns (string memory) {
        return _collectionData.symbol;
    }

    /**
     * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
     */
    function tokenURI(uint256 tokenId) public view virtual returns (string memory) {
        if (!_exists(tokenId)) revert TokenURINonexistentToken();
        string memory _baseURI = baseURI();
        return bytes(_baseURI).length > 0 ? string(abi.encodePacked(_baseURI, tokenId.toString())) : '';
    }

    /**
     * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
     * token will be the concatenation of the `baseURI` and the `tokenId`. Empty
     * by default, can be overriden in child contracts.
     */
    function baseURI() public view virtual returns (string memory) {
        return '';
    }

    /**
     * @dev Returns tokenIDs owned by `_owner`.
     */
    function tokensOfOwner(address _owner) public view returns (uint256[] memory) {
        uint256 totalOwned = _addressData[_owner].balance;
        if (totalOwned == 0) revert TokensZeroBalance();
        uint256 supply = _collectionData.index;
        uint256[] memory tokenIDs = new uint256[](totalOwned);
        uint256 ownedIndex;
        address currentOwner;

        unchecked {
            for (uint256 i; i < supply; i++) {
                address currentAddress = _ownerships[i];
                if (currentAddress != address(0)) {
                    currentOwner = currentAddress;
                }
                if (currentOwner == _owner) {
                    tokenIDs[ownedIndex++] = i;
                    if (ownedIndex == totalOwned){
                        return tokenIDs;
                    }
                }
            }
        }

        revert TokensOfOwnerNullValue();
    }

    /**
     * @dev Returns `_owner`'s lock status and unlock timestamp in unix time, and personal lock cooldown in days.
     */
    function getLockData(address _owner) public view returns (bool, uint256, uint256) {
        return (_addressData[_owner].locked, 
        _addressData[_owner].lockedUnlockTimestamp, 
        _addressData[_owner].lockedUnlockCooldown);
    }

    /**
     * @dev Returns `_address`'s staking status.
     */
    function getStakingStatus(address _address) public view returns (bool) {
        return (_addressData[_address].staked);
    }

    /**
     * @dev Returns the token collection information.
     */
    function collectionInformation() public view returns (collectionData memory) {
        return _collectionData;
    }