// Contract: CryptoBlobs
// Target function: function accountData(address account, bool getENS, bool getTokens)
Collectiv has access to modify these values using {manageContract}.
    // Additionally, we can modify _contractOwner and _contractCandidate in [Ownable].

    /// @notice The name of the token. We may shorten the name after the initial mint.
    string internal _name = "CryptoBlobs.com | SuperCollectiv";
    /// @notice If the integrated trading platform is paused.
    bool internal _tradingPaused;
    /// @notice If sacrificing is paused.
    bool internal _sacrificingPaused;
    /// @notice The maximum number of souls the token URI will support.
    uint256 internal _URIMaxSoulsLimit = 100;
    /// @notice ENS reverse records address.
    address internal _reverseRecordsAddress = 0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C;
    /// @notice Contract URI for the contract.
    string internal _contractURI;
    /// @notice Token URI prefix for single URI schema.
    string internal _tokenURIPrefix;
    /// @notice Token URI prefixes for multi URI schema.
    mapping(uint256 => string) internal _tokenURIPrefixes;

    /**
        @param tokensOwned The number of CryptoBlobs owned.
        @param mintedPurchase The number of CryptoBlobs purchased.
        @param mintedBogos The number of CryptoBlobs claimed from BOGO deals.
        @param mintedGiveaway The number of CryptoBlobs claimed from the giveaway.
        @param tokensBurned The number of CryptoBlobs burned.
        @param mintedHourlyDrops The number of CryptoBlobs claimed from hourly drops.
        @param mintedSacrifices The number of CryptoBlobs claimed from the sacrifice promo.
        @param mintedReferrals The number of CryptoBlobs claimed from referring.
        @param mintedSpecial The number of CryptoBlobs gifted from SuperCollectiv. Max number is 255.
        @param tokensOptimized The number of CryptoBlobs optimized.
        @param referralPurchases The number of CryptoBlobs purchased with account's referral link.
        @param timestampHourlyDropLastClaimed Timestamp the account last claimed their hourly drop or initialized it.
        @param timestampReferralLinkUsed Timestamp the account used a referral link.
        @param timestampTradesLocked Timestamp the account locked their trades on the integrated trading platform.
        @param timestampTradeHashUsed Timestamp when trade hashes have been cancelled or completed by the account.
        @param approvals Addresses the account has approved to manage all CryptoBlobs owned by them.
    */
    struct Account {
        // Order is optimized.
        uint16 tokensOwned;
        uint16 mintedPurchase;
        uint8 mintedBogos;
        uint8 mintedGiveaway;
        uint16 tokensBurned;
        uint8 mintedHourlyDrops;
        uint8 mintedSacrifices;
        uint8 mintedReferrals;
        uint16 mintedSpecial;
        uint16 tokensOptimized;
        uint16 referralPurchases;
        uint32 timestampHourlyDropLastClaimed;
        uint32 timestampReferralLinkUsed;
        uint32 timestampTradesLocked;
        mapping(bytes32 => uint256) timestampTradeHashUsed;
        mapping(address => uint256) approvals;
        uint256 preapprovedStatus;
    }

    /**
        @param mintTimestamp Timestamp minted.
        @param burnTimestamp Timestamp burned.
        @param souls Before burn: The number of souls harnessed. After burn: The number of souls transferred to burnTo.
        @param burnTo CryptoBlob ID transferred souls to.
        @param account Before burn: Address of the owner. After burn: Address that last owned it.
        @param approval Address of the approved token operator.
    */
    struct Token {
        uint32 mintTimestamp;
        uint32 burnTimestamp;
        uint16 souls;
        uint16 burnTo;
        address account;
        address approval;
    }

    /**
        @notice Batch transaction data for {batch}.
    */
    struct BatchData {
        uint256 category;
        address account1;
        address account2;
        uint256 tokenId;
        uint256[] tokenIds;
        bool approved;
        bytes32 hash;
    }

    /**
        @notice The name of the token.
    */
    function name() public view virtual override returns (string memory) {
        return _name;
    }

    /**
        @notice The symbol of the token.
    */
    function symbol() public view virtual override returns (string memory) {
        return _symbol;
    }

    /**
        @notice If an interface ID is supported. 
        @dev Supported interfaces: IERC165 (0x01ffc9a7), IERC721 (0x80ac58cd), IERC721Metadata (0x5b5e139f), IERC721Enumerable (0x780e9d63).
        @param interfaceId Interface ID to check.
        @return If `interfaceId` is supported.
    */
    function supportsInterface(bytes4 interfaceId)
        public
        view
        virtual
        override
        returns (bool)
    {
        return interfaceId == type(IERC165).interfaceId ||          // 0x01ffc9a7
            interfaceId == type(IERC721).interfaceId ||             // 0x80ac58cd
            interfaceId == type(IERC721Metadata).interfaceId ||     // 0x5b5e139f
            interfaceId == type(IERC721Enumerable).interfaceId;     // 0x780e9d63
    }

    /**
        @notice Returns a token ID owned by `owner` at a given index in its inventory.
        @param owner The address of the account.
        @param index Inventory index of `owner` to read. Uses index zero.
        @return The token ID of the CryptoBlob at the index.
    */
    function tokenOfOwnerByIndex(address owner, uint256 index)
        public
        view
        virtual
        override
        returns (uint256)
    {
        unchecked {
            uint256 tokensMinted = _tokensMinted(); // Saves gas.
            uint256 indexOfOwner; // Iterates when inventory index does not match `index`.
            address accountOfToken;
            uint256 balance = balanceOf(owner);
            if (index < balance) {
                for (uint256 i; i < tokensMinted; i++) {
                    uint256 tokenId = _getTokenId(i);
                    if (_token[tokenId].account != address(0)) {
                        accountOfToken = _token[tokenId].account; // Store address.
                    }
                    // If the CryptoBlob last checked is in the owner's inventory.
                    if (accountOfToken == owner && !_isTokenSacrificed(tokenId)) {
                        if (indexOfOwner != index) {
                            indexOfOwner++; // Iterate if incorrect index.
                        } else {
                            return tokenId; // Return if the index matches.
                        }
                    }
                }
            }
            revert OWNERS_BALANCE_IS_INSUFFICENT_FOR_THE_INDEX();
        }
    }

    /**
        @notice The circulating supply of CryptoBlobs.
    */
    function totalSupply() public view virtual override returns (uint256) {
        unchecked {
            return _tokensMinted() - _tokensBurned; // Cannot underflow.
        }
    }

    /**
        @notice Returns a token ID at a given `index`.
        @param index Index of the circulating supply to read.
        @return The token ID of the CryptoBlob at the index.
    */
    function tokenByIndex(uint256 index)
        public
        view
        virtual
        override
        returns (uint256)
    {
        unchecked {
            uint256 tokensMinted = _tokensMinted();
            uint256 tokenIdsIndex;
            if (index < tokensMinted) {
                // Check each minted CryptoBlob and return the ID when the index is reached.
                for (uint256 i; i < tokensMinted; i++) {
                    uint256 tokenId = _getTokenId(i);
                    if (!_isTokenSacrificed(tokenId)) {
                        if (tokenIdsIndex != index) {
                            tokenIdsIndex++; // Iterate if incorrect index.
                        } else {
                            return tokenId; // Return if the index matches.
                        }
                    }
                }
            }
            revert INVALID_CRYPTOBLOB_ID();
        }
    }

    /**
        @notice The balance of an account.
        @param owner The address of the account.
        @return The balance of `owner`.
    */
    function balanceOf(address owner)
        public
        view
        virtual
        override
        returns (uint256)
    {
        if (owner == address(0)) revert THE_ZERO_ADDRESS_CANNOT_HAVE_AN_ACCOUNT();
        return _account[owner].tokensOwned;
    }

    /**
        @notice The owner of a CryptoBlob ID.
        @param tokenId The token ID of the CryptoBlob.
        @return The owner of `tokenId`.
    */
    function ownerOf(uint256 tokenId)
        public
        view
        virtual
        override
        returns (address)
    {
        unchecked {
            _revertIfTokenIsInvalid(tokenId);
            uint256 index = _getTokenIndex(tokenId);
            while (true) {
                if (_token[tokenId].account == address(0)) {
                    tokenId = _getTokenId(--index);
                } else {
                    return _token[tokenId].account;
                }
            }
            revert INVALID_CRYPTOBLOB_ID();
        }
    }

    /**
        @notice The token URI of a CryptoBlob ID.
        @dev Unminted CryptoBlobs revert with an error.
        @dev Sacrificed CryptoBlobs still have metadata.
        @param tokenId The token ID of the CryptoBlob.
        @return The token URI of `tokenId`.
    */
    function tokenURI(uint256 tokenId)
        public
        view
        virtual
        override
        returns (string memory)
    {
        unchecked {
            if (_getTokenIndex(tokenId) >= _tokensMinted()) revert INVALID_CRYPTOBLOB_ID();

            uint256 souls;
            if (!_isTokenSacrificed(tokenId)) souls = _token[tokenId].souls + 1; // Get souls harnessed and +1 for self.
            if (souls > _URIMaxSoulsLimit) souls = _URIMaxSoulsLimit; // Limit souls.

            if (bytes(_tokenURIPrefix).length > 0) {
                // Single URI with an extension.
                return string(abi.encodePacked(_tokenURIPrefix, _toPaddedString(souls), "/", _toPaddedString(tokenId)));
            } else if (bytes(_tokenURIPrefixes[souls]).length > 0) {
                // Multi URI with no extension.
                return string(abi.encodePacked(_tokenURIPrefixes[souls], _toPaddedString(tokenId)));
            } else {
                // If no token URI.
                return "";
            }
        }
    }

    /**
        @notice Returns the approved token operator for `tokenId`.
        @dev Get all approved token operators with {tokenDataByCategory} category 1.
        @param tokenId The token ID of the CryptoBlob.
        @return The address of the account that can manage `tokenId`.
    */
    function getApproved(uint256 tokenId)
        public
        view
        virtual
        override
        returns (address)
    {
        _revertIfTokenIsInvalid(tokenId);
        return _token[tokenId].approval;
    }

    /**
        @notice Returns if `operator` is an approved account operator of `owner`.
        @dev Account operators can do everything the owner can do expect sign messages and manage their account operators.
        @dev Preapproved services: OpenSea, X2Y2, LooksRare, Rarible, 0x Protocol v4, Element, SudoSwap, NFTX, and NFTTrader.
        @dev You can revoke these services using {setApprovalForAll} or visit CryptoBlobs.com for a batch tx to revoke all at once.
        @dev SuperCollectiv will permanently disable preapproved services if any issues are discovered with these contracts.
        @dev Services that list your account operators will not detect that these addresses have
        @dev been approved unless you reapprove each service with {setApprovalForAll} or {batch}.
        @param owner The address of the account that owns the CryptoBlobs.
        @param operator The address of the account to the check the status of.
        @return If `operator` is an account operator of `owner`.
    */
    function isApprovedForAll(address owner, address operator)
        public
        view
        virtual
        override
        returns (bool)
    {

        // Preapproved services:
        // OpenSea, X2Y2, LooksRare, Rarible, 0x Protocol v4, Element, SudoSwap, NFTX, and NFTTrader.
        // You can manage these services like usual with {setApprovalForAll} or {batch}.

        uint256 status = _account[owner].approvals[operator];
        return (
                    status == 2 || // If approved.
                    (
                        status == 0 && // If default value (never approved or revoked by the owner).
                        _account[operator].preapprovedStatus == 2 && // If preapproved service.
                        owner != operator // If not self.
                    )
                );
    }

    /**
        @notice The contract URI for the CryptoBlobs smart contract.
    */
    function contractURI() public view returns (string memory) {
        return _contractURI;
    }

    /**
        @notice All-in-one method to get general data for the contract.
        @dev Enter the zero address to query {generalData} without checking an NFT.
        @param contractAddress Contract address of the NFT.
        @param tokenId Token ID of the NFT.
        @return data General contract data.
        @return owner Owner of the NFT.
        @return isERC721 If the token is an ERC721 NFT.
        @return nftsRedeemed NFT IDs redeemed in the collection.
        @return contractOwner Contract owner.
        @return contractCandidate Contract candidate.
        @return contractBalance Contract balance.
    */
    function generalData(address contractAddress, uint256 tokenId) 
        public 
        view 
        returns (
            uint256[12] memory data, 
            address owner, 
            bool isERC721, 
            uint256[] memory nftsRedeemed,
            address contractOwner,
            address contractCandidate,
            uint256 contractBalance
        ) 
    {
        unchecked {

            // Supply data.
            data[0] = _tokensMintedPurchase;
            data[1] = _tokensMintedFree;
            data[2] = _tokensBurned;

            // Temporarily hold tokensMinted.
            data[3] = _tokensMinted();

            // Time until next hourly drop.
            if (data[3] < _sacrificeAndHourlyDropsOffersEnds) data[4] = _hourlyDropDuration - (block.timestamp % _hourlyDropDuration);

            // Statuses.
            if (_tradingPaused) data[5] = 1;
            if (_sacrificingPaused) data[6] = 1;
            
            // Get price.
            if (data[3] != _maxSupply) (data[7], data[8], data[9], data[10], data[11]) = _marketPrice(data[0], data[1]);

            // Check NFT.
            if (contractAddress != address(0)) (owner, isERC721, nftsRedeemed) = _checkNFT(contractAddress, tokenId);

            // Block timestamp.
            data[3] = block.timestamp;

            return (data, owner, isERC721, nftsRedeemed, _contractOwner, _contractCandidate, address(this).balance);
        }
    }

    /**
        @notice Check an NFT.
        @param contractAddress Contract address of the NFT.
        @param tokenId Token ID of the NFT.
        @return owner Owner of the NFT.
        @return isERC721 If the token is an ERC721 NFT.
        @return nftsRedeemed NFT IDs redeemed in the collection.
    */
    function _checkNFT(address contractAddress, uint256 tokenId) internal view returns (address owner, bool isERC721, uint256[] memory nftsRedeemed) {
        if (_isContractPastConstruction(contractAddress)) {
            try IERC721(contractAddress).ownerOf(tokenId) returns (address _owner) { owner = _owner; } catch {}
            try IERC165(contractAddress).supportsInterface(type(IERC721).interfaceId) returns (bool _isERC721) { isERC721 = _isERC721; } catch {}
            //try IERC721Metadata(contractAddress).tokenURI(tokenId) returns (string memory _tokenURI) { if (bytes(_tokenURI).length == 0) isERC721 = false; } catch {}
            nftsRedeemed = _nftsRedeemed[contractAddress];
        }
        return (owner, isERC721, nftsRedeemed);
    }

    /**
        @notice Redeem an NFT.
        @param contractAddress Contract address of the NFT.
        @param tokenId Token ID of the NFT.
        @return redeemed If the NFT was redeemed (true if no revert).
    */
    function _redeemNFT(address contractAddress, uint256 tokenId) internal returns (bool redeemed) {
        unchecked {
            // Get NFT data.
            (address owner, bool isERC721, uint256[] memory nftsRedeemed) = _checkNFT(contractAddress, tokenId);

            // Revert if not owner, not ERC721, or limit reached.
            if (owner != msg.sender || !isERC721 || nftsRedeemed.length >= _nftsPerCollectionLimit) revert NFT_IS_NOT_ELIGIBLE();

            // Revert if the NFT has been redeemed.
            for (uint256 i; i < nftsRedeemed.length; i++) {
                if (nftsRedeemed[i] == tokenId) revert NFT_IS_NOT_ELIGIBLE();
            }

            // Add to redeemed array.
            _nftsRedeemed[contractAddress].push(tokenId);

            return true;
        }
    }