// Contract: CryptoBlobs
// Target function: function batchTradeHashUsedAndHasAccess(

                    // - 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;
        }
    }

    /**
        @notice Check if a trade is approved and valid. Returns the hash and error data.
        @param maker The address of the maker.
        @param taker The address of the taker.
        @param makerIds The CryptoBlob IDs owned by `maker` to trade with `taker`.
        @param takerIds The CryptoBlob IDs owned by `taker` to trade with `maker`.
        @param price The price in WEI the `taker` must pay to `maker` to complete the trade.
        @param expiry The UNIX time the trade automatically expires or 0 to never expire.
        @param salt A nonce to allow the same trade parameters to be used again.
        @param signature The signature of the signed hash.
        @param checkAccess Whether to verify each CryptoBlob ID.
        @return hash The hash of the trade.
        @return errors The errors of the trade. Valid trades return all zeros.
    */
    function isTradeApprovedAndValid(
        address maker,
        address taker,
        uint256[] memory makerIds,
        uint256[] memory takerIds,
        uint256 price,
        uint256 expiry,
        uint256 salt,
        bytes memory signature,
        bool checkAccess
    )
        public
        view
        returns (
            bytes32 hash,
            uint256[8] memory errors
        )
    {
        unchecked {
            // Trade data is hashed into 32 bytes and the hash gets signed.
            hash = keccak256(
                abi.encode(
                    address(this),
                    maker,
                    taker,
                    keccak256(abi.encode(makerIds)),
                    keccak256(abi.encode(takerIds)),
                    price,
                    expiry,
                    salt
                )
            );

            // Error if the maker's address does not match the recovered signer's address.
            if (!_isTradeApproved(maker, hash, signature)) errors[0] = 1;

            // Error if the maker is the zero address.
            if (maker == address(0)) errors[1] = 1;

            // Error if the maker has their trades locked (returns timestamp locked).
            errors[2] = _account[maker].timestampTradesLocked;

            // Error if the hash has been used (returns timestamp used).
            errors[3] = _account[maker].timestampTradeHashUsed[hash];

            // Error if the trade has expired. An expiry of 0 means it never expires.
            if (expiry <= block.timestamp && expiry != 0) errors[4] = 1;

            // Error if taker IDs are listed when the trade is public.
            if (taker == address(0) && takerIds.length > 0) errors[5] = 1;

            // Error if an account does not have access to the IDs.
            // `checkAccess` is false during a trade because IDs are verified in {trade} after each transfer.
            if (checkAccess) {
                for (uint256 i; i < makerIds.length; i++) {
                    (bool hasAccess,) = _hasAccess(maker, makerIds[i]);
                    if (!hasAccess) errors[6]++; // Returns amount without access.
                }
                for (uint256 i; i < takerIds.length; i++) {
                    (bool hasAccess,) = _hasAccess(taker, takerIds[i]);
                    if (!hasAccess) errors[7]++; // Returns amount without access.
                }
            }

            return (hash, errors);
        }
    }