// Contract: CryptoBlobs
// Target function: function tokenDataByCategory(uint256 category, uint256 index, uint256 amount)
 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;
        }
    }

    /**
        @notice Account data for an account.
        @dev The zero address cannot have an account.
        @param account The address of the account.
        @param getENS Optionally get the ENS name of `account`.
        @param getTokens Optionally get the owned and burned tokens of `account`.
        @return data 0 owned, 1 burned, 2 optimized, 3 purchased, 4 special, 5 free, (minted: 6 bogos, 7 drops, 8 sacrifices, 9 referrals, 10 giveaway), (claimable: 11 total, 12 drops, 13 sacrifices, 14 referrals, 15 giveaway), 16 referral purchases, (timestamps: 17 hourly drop last claimed, 18 referral link used, 19 trades locked), 20 is contract, 21 wallet balance.
        @return ensName The ENS name of `account`.
        @return ownedData Data for CryptoBlobs owned by the account (ID, souls, mint timestamp, approval as uint).
        @return burnedData Data for CryptoBlobs burned by the account (ID, ID upgraded, souls transferred, mint timestamp, burn timestamp).
    */
    function accountData(address account, bool getENS, bool getTokens)
        public
        view
        returns (
            uint256[22] memory data,
            string memory ensName,
            uint256[] memory ownedData,
            uint256[] memory burnedData
        )
    {
        unchecked {
            // Token stats
            data[0] = balanceOf(account);
            data[1] = _account[account].tokensBurned;
            data[2] = _account[account].tokensOptimized;
            // Minted amounts
            // Total minted = mintedPurchase + mintedSpecial + mintedFree
            data[3] = _account[account].mintedPurchase;
            data[4] = _account[account].mintedSpecial;
            // data[5] mintedFree total below
            // mintedFree breakdown: 6-10
            data[6] = _account[account].mintedBogos;
            data[7] = _account[account].mintedHourlyDrops;
            data[8] = _account[account].mintedSacrifices;
            data[9] = _account[account].mintedReferrals;
            data[10] = _account[account].mintedGiveaway;
            // Claimables: 11-15
            (data[5], data[12], data[13], data[14], data[15]) = _accountToClaimable(account);
            data[11] = data[12] + data[13] + data[14] + data[15];
            // Miscellaneous
            data[16] = _account[account].referralPurchases;
            // Timestamps
            data[17] = _account[account].timestampHourlyDropLastClaimed;
            data[18] = _account[account].timestampReferralLinkUsed;
            data[19] = _account[account].timestampTradesLocked;
            // Bools
            if (_isContractPastConstruction(account)) data[20] = 1;
            // Ether balance
            data[21] = address(account).balance;

            // Optionally get ENS name.
            if (getENS) {
                address[] memory addresses = new address[](1);
                addresses[0] = account;
                ensName = addressesToENS(addresses)[0];
            }

            // Optionally get tokens.
            if (getTokens) {

                uint256[4] memory variables;
                // 0 = ownedIndex
                // 1 = burnedIndex
                // 2 = tokensChecked
                // 3 = mintTimestampOfToken

                // If currently owns CryptoBlobs or had sacrificed any.
                if (data[0] + data[1] > 0) {

                    // Create owned array.
                    uint256 ownedDataAmount = 4;
                    ownedData = new uint256[](data[0] * ownedDataAmount);
                    // Each owned ID returns:
                    // - token ID
                    // - souls owned
                    // - mint timestamp
                    // - approval (as uint, must convert to hex)

                    // Create burned array.
                    uint256 burnedDataAmount = 5;
                    burnedData = new uint256[](data[1] * burnedDataAmount);
                    // Each burned ID returns:
                    // - token ID
                    // - token ID upgraded
                    // - souls transferred to upgraded ID
                    // - mint timestamp
                    // - burn timestamp

                    // Get all data for the account until all is found.
                    address accountOfToken;
                    while (data[0] + data[1] != variables[0] + variables[1]) {
                        uint256 tokenId = _getTokenId(variables[2]++);
                        // Get owner or last owner.
                        if (_token[tokenId].account != address(0)) {
                            accountOfToken = _token[tokenId].account;
                        }
                        // Get minted timestamp.
                        if (_token[tokenId].mintTimestamp != 0) {
                            variables[3] = _token[tokenId].mintTimestamp;
                        }
                        if (accountOfToken == account) {
                            // If account matches query.
                            if (!_isTokenSacrificed(tokenId)) { // If owned.
                                ownedData[variables[0] * ownedDataAmount] = tokenId;
                                ownedData[(variables[0] * ownedDataAmount) + 1] = _token[tokenId].souls + 1;
                                ownedData[(variables[0] * ownedDataAmount) + 2] = variables[3];
                                ownedData[(variables[0] * ownedDataAmount) + 3] = uint160(_token[tokenId].approval);
                                variables[0]++;
                            } else { // If sacrificed.
                                burnedData[variables[1] * burnedDataAmount] = tokenId;
                                burnedData[(variables[1] * burnedDataAmount) + 1] = _token[tokenId].burnTo;
                                burnedData[(variables[1] * burnedDataAmount) + 2] = _token[tokenId].souls;
                                burnedData[(variables[1] * burnedDataAmount) + 3] = variables[3];
                                burnedData[(variables[1] * burnedDataAmount) + 4] = _token[tokenId].burnTimestamp;
                                variables[1]++;
                            }
                        }
                    }
                }
            }
            return (data, ensName, ownedData, burnedData);
        }
    }

    /**
        @notice Account data for multiple accounts by category.
        @dev See past event logs for more data. Use {addressesToENS} for ENS names.
        @dev The zero address cannot have an account and returns from it should be ignored. This is not checked to optimize calls.
        @param accounts The addresses of the accounts.
        @param category (0: owned, burned, optimized, purchased), (1: owned, burned, optimized), 2 total claimable, 3 trades locked, (4: wallet balance, is contract)
        @return data The uint data of all accounts, based on the category.
    */
    function accountDataByCategory(address[] memory accounts, uint256 category)
        external
        view
        returns (uint256[] memory data)
    {
        unchecked {

            // The following categories can be queried from logs.
            // tokensMinted
            // mintedFree
            // mintedHourlyDrops
            // mintedSacrifices
            // mintedReferrals
            // mintedGiveaway
            // mintedBogos
            // mintedSpecial
            // referralPurchases
            // timestampHourlyDropLastClaimed
            // timestampReferralLinkUsed

            // Use {accountData} for claimable specifics.
            // claimableHourlyDrops
            // claimableSacrifices
            // claimableReferrals
            // claimableGiveaway

            uint256 accountsAmount = accounts.length;
            if (category == 0) {
                // Owned, burned, optimized, purchased.
                // Data for the leaderboards.
                uint256 amount = 4;
                data = new uint256[](accountsAmount * amount);
                for (uint256 i; i < accountsAmount; i++) {
                    data[i * amount] = balanceOf(accounts[i]);
                    data[i * amount + 1] = _account[accounts[i]].tokensBurned;
                    data[i * amount + 2] = _account[accounts[i]].tokensOptimized;
                    data[i * amount + 3] = _account[accounts[i]].mintedPurchase;
                }
            } else if (category == 1) {
                // Owned, burned, optimized.
                // Alternative for category 0 because purchases can be retrieved from past logs.
                uint256 amount = 3;
                data = new uint256[](accountsAmount * amount);
                for (uint256 i; i < accountsAmount; i++) {
                    data[i * amount] = balanceOf(accounts[i]);
                    data[i * amount + 1] = _account[accounts[i]].tokensBurned;
                    data[i * amount + 2] = _account[accounts[i]].tokensOptimized;
                }
            } else if (category == 2) {
                // Total claimable.
                data = new uint256[](accountsAmount);
                for (uint256 i; i < accountsAmount; i++) {
                    (
                        ,
                        uint256 claimableHourlyDrops, 
                        uint256 claimableSacrifices, 
                        uint256 claimableReferrals, 
                        uint256 claimableGiveaway
                    ) = _accountToClaimable(accounts[i]);
                    data[i] = claimableHourlyDrops + claimableSacrifices + claimableReferrals + claimableGiveaway;
                }
            } else if (category == 3) {
                // Timestamp trades locked.
                data = new uint256[](accountsAmount);
                for (uint256 i; i < accountsAmount; i++) {
                    data[i] = _account[accounts[i]].timestampTradesLocked;
                }
            } else {
                // Wallet balance and is contract.
                uint256 amount = 2;
                data = new uint256[](accountsAmount * amount);
                for (uint256 i; i < accountsAmount; i++) {
                    data[i * amount] = address(accounts[i]).balance;
                    if (_isContractPastConstruction(accounts[i])) data[i * amount + 1] = 1;
                }
            }
            return data;
        }
    }

    /**
        @notice Reverse records for the primary ENS name of accounts.
        @dev _reverseRecordsAddress Can be updated to support additional features.
        @param accounts The addresses of the accounts.
        @return ensNames ENS names of `accounts`.
    */
    function addressesToENS(address[] memory accounts) public view returns (string[] memory ensNames) {
        return IReverseRecords(_reverseRecordsAddress).getNames(accounts);
    }

    /**
        @notice Token data for a CryptoBlob ID.
        @param tokenId The token ID of the CryptoBlob.
        @param getENS Optionally get the ENS names of accounts.
        @return owner The owner of `tokenId`.
        @return approval The approved token operator of `tokenId`.
        @return burner The last owner of `tokenId` before it was burned.
        @return mintTimestamp The timestamp `tokenId` was minted.
        @return burnTimestamp The timestamp `tokenId` was burned.
        @return ownedSouls The number of souls `tokenId` possesses, rewarded by {sacrifice}.
        @return burnSouls The number of souls `tokenId` transferred after being burned.
        @return burnTo The CryptoBlob ID `tokenId` transferred its soul(s) to.
        @return distance Optimization distance/rank.
        @return ensNames The ENS name of the owner, approval, and burner.
    */
    function tokenData(uint256 tokenId, bool getENS)
        external
        view
        returns (
            address owner,
            address approval,
            address burner,
            uint256 mintTimestamp,
            uint256 burnTimestamp,
            uint256 ownedSouls,
            uint256 burnSouls,
            uint256 burnTo,
            uint256 distance,
            string[] memory ensNames
        )
    {
        unchecked {
            if (!_isTokenSacrificed(tokenId)) {
                owner = ownerOf(tokenId); // Reverts if not yet minted.
                approval = _token[tokenId].approval;
                ownedSouls = _token[tokenId].souls + 1; // +1 for self.
            } else {
                burner = _token[tokenId].account;
                burnTimestamp = _token[tokenId].burnTimestamp;
                burnTo = _token[tokenId].burnTo;
                burnSouls = _token[tokenId].souls;
            }
            uint256 index = _getTokenIndex(tokenId);
            while (mintTimestamp == 0) {
                if (_token[tokenId].mintTimestamp == 0) {
                    if (_token[tokenId].account == address(0)) {
                        distance++;
                    }
                    tokenId = _getTokenId(--index);
                } else {
                    mintTimestamp = _token[tokenId].mintTimestamp;
                }
            }
            if (getENS) {
                address[] memory addresses = new address[](3); // Create array.
                addresses[0] = owner;
                addresses[1] = approval;
                addresses[2] = burner;
                ensNames = addressesToENS(addresses); // Get ENS names.
            }
            return (
                owner,
                approval,
                burner,
                mintTimestamp,
                burnTimestamp,
                ownedSouls,
                burnSouls,
                burnTo,
                distance,
                ensNames
            );
        }
    }