// Contract: CryptoBlobs
// Target function: function isTradeApprovedAndValid(
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
            );
        }
    }


    /**
        @notice Information about all tokens. Uses index 0.
        @dev Most providers will successfully call this function.
        @dev Data can be split by specifying an index and an amount, which uses token indices.
        @param category 0 owners, 1 approvals, 2 burners, 3 mintTimestamps, 4 burnTimestamps, 5 ownedSouls, 6 burnSouls, 7 burnTos, 8 distance.
        @param index Index to start from when splitting data. Default is 0.
        @param amount Amount to query when splitting data. Default is 0.
        @return data The number data of all tokens, based on the category. Addresses are uints, which will need to be conversed to hex and checksummed.
    */
    function tokenDataByCategory(uint256 category, uint256 index, uint256 amount)
        external
        view
        returns (uint256[] memory data)
    {
        unchecked {
            uint256 tokensMinted = _tokensMinted();
            uint256 toIndex;
            uint256 fromIndex;
            if (amount == 0) { // Getting all data.
                amount = _maxSupply;
                toIndex = tokensMinted;
            } else { // Getting range.
                // Precheck for accurate data. 
                if (category == 0 || category == 3 || category == 8) {
                    // toIndex temporarily holds precheck amount.
                    uint maxPrecheck = _purchaseLimit + _maxFreePerAccount;
                    if (index > maxPrecheck) {
                        toIndex = maxPrecheck;
                    } else if (index != 0) {
                        toIndex = index % (maxPrecheck);
                    }
                }
                fromIndex = index - toIndex; // Cannot underflow because index > toIndex (precheck).
                toIndex += fromIndex + amount; // toIndex is calculated here.
                // Limit amount.
                if (toIndex > tokensMinted) {
                    toIndex = tokensMinted;
                    amount = toIndex - index; // Cannot underflow because toIndex always > index.
                }
            }
            data = new uint256[](amount); // Create uint array.
            uint256 holdNumber;
            if (category != 8) {
                while (fromIndex < toIndex) {
                    // Check all minted CryptoBlobs.
                    uint256 tokenId = _getTokenId(fromIndex); // Get token ID.
                    bool sacrificed = _isTokenSacrificed(tokenId);
                    uint256 dataRetrieved;
                    if (category == 0) {
                        // Update account of token.
                        if (_token[tokenId].account != address(0)) {
                            holdNumber = uint160(_token[tokenId].account);
                        }
                        // Add address if not sacrificed.
                        if (!sacrificed) {
                            dataRetrieved = holdNumber;
                        }
                    } else if (category == 1) {
                        // Add approval.
                        dataRetrieved = uint160(_token[tokenId].approval);
                    } else if (category == 2) {
                        // Update account of token.
                        if (_token[tokenId].account != address(0)) {
                            holdNumber = uint160(_token[tokenId].account);
                        }
                        // Add address if sacrificed.
                        if (sacrificed) {
                            dataRetrieved = holdNumber;
                        }
                    } else if (category == 3) {
                        // Update latest mint timestamp.
                        if (_token[tokenId].mintTimestamp != 0) {
                            holdNumber = _token[tokenId].mintTimestamp;
                        }
                        // Add mint timestamp.
                        dataRetrieved = holdNumber;
                    } else if (category == 4) {
                        // Add burn timestamp.
                        dataRetrieved = _token[tokenId].burnTimestamp;
                    } else if (category == 5 && !sacrificed) {
                        // Add souls if not sacrificed.
                        dataRetrieved = _token[tokenId].souls + 1;
                    } else if (category == 6 && sacrificed) {
                        // Add souls transferred if sacrificed.
                        dataRetrieved = _token[tokenId].souls;
                    } else if (category == 7 && sacrificed) {
                        // Add token ID upgraded.
                        dataRetrieved = _token[tokenId].burnTo;
                    }
                    // Add data to array.
                    if (amount == _maxSupply) {
                        if (dataRetrieved != 0) {
                            data[tokenId - 1] = dataRetrieved;
                        }
                    } else if (fromIndex >= index) {
                        if (dataRetrieved != 0) {
                            data[fromIndex - index] = dataRetrieved;
                        }
                    }
                    fromIndex++;
                }
            } else if (category == 8) {
                uint256 tokensChecked;
                while (fromIndex < toIndex) {
                    if (_token[_getTokenId(toIndex - tokensChecked - 1)].account == address(0)) {
                        holdNumber++;
                    } else if (holdNumber > 0) {
                        for (uint256 j; j < holdNumber + 1; j++) {
                            if (amount == _maxSupply) {
                                data[_getTokenId(toIndex - tokensChecked - 1 + j) - 1] = j;
                            } else if (toIndex - tokensChecked - 1 + j >= index) {
                                data[toIndex - tokensChecked - 1 + j - index] = j;
                            }
                        }
                        delete holdNumber;
                    }
                    tokensChecked++;
                    fromIndex++;
                }
            }
            return data;
        }
    }