// Contract: CryptoBlobs
// Target function: function _accountToMintedFree(address account) internal view returns (uint256 mintedFree)
    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);
        }
    }

    /**
        @notice Check if trade hashes have been used and if an account can access token IDs.
        @dev Token ID is checked if number is <= 25000, else the hash is checked.
        @dev Hashes must be converted to uint.
        @param accounts The addresses of the accounts.
        @param numbers The hashes (as uint) or token IDs to check.
        @return data Data returned.
    */
    function batchTradeHashUsedAndHasAccess(
        address[] memory accounts,
        uint256[] memory numbers
    ) 
        external
        view
        returns (
            uint256[] memory data
        )
    {
        uint256 maxSupply = _maxSupply; // Get max supply.
        uint256 accountsAmount = accounts.length; // Get return length.
        data = new uint256[](accountsAmount); // Create array.
        for (uint256 i; i < accountsAmount; i++) {
            if (numbers[i] > maxSupply) { // If checking hash.
                data[i] = _account[accounts[i]].timestampTradeHashUsed[bytes32(numbers[i])]; // Convert to bytes32 and check if used.
            } else {
                bool hasAccess;
                (hasAccess,) = _hasAccess(accounts[i], numbers[i]);
                if (hasAccess) data[i] = 1; // Return 1 if account has access.
            }
        }
        return data;
    }

    /**
        @notice Returns the status for a preapproved service.
        @dev SuperCollectiv can only manage services preapproved in the constructor.
        @dev 0x1E0049783F008A0085193E00003D00cd54003c71 (OpenSea).
        @dev 0xF849de01B080aDC3A814FaBE1E2087475cF2E354 (X2Y2).
        @dev 0xf42aa99F011A1fA7CDA90E5E98b277E306BcA83e (LooksRare).
        @dev 0x4feE7B061C97C9c496b01DbcE9CDb10c02f0a0Be (Rarible).
        @dev 0xDef1C0ded9bec7F1a1670819833240f027b25EfF (0x Protocol, Coinbase NFT, and more).
        @dev 0x20F780A973856B93f63670377900C1d2a50a77c4 (Element).
        @dev 0x2B2e8cDA09bBA9660dCA5cB6233787738Ad68329 (SudoSwap).
        @dev 0x0fc584529a2AEfA997697FAfAcbA5831faC0c22d (NFTX).
        @dev 0x657E383EdB9A7407E468acBCc9Fe4C9730c7C275 (NFTTrader).
        @param account The address of the account.
        @return status 0 = service is permanently disabled, 1 = service is temporarily disabled, 2 = service is enabled.
    */
    function preapprovedServiceStatus(address account) external view returns (uint256 status) {
        return _account[account].preapprovedStatus;
    }

    /**
        @notice The amount of CryptoBlobs an account can claim for free from offers. Contracts aren't eligible.
        @param account The address of the account.
        @return mintedFree The amount minted for free.
        @return claimableHourlyDrops The amount claimable from the hourly drops.
        @return claimableSacrifices The amount claimable from the sacrifice promo.
        @return claimableReferrals The amount claimable from the referral program.
        @return claimableGiveaway The amount claimable from the giveaway.
    */
    function _accountToClaimable(address account) internal view returns (
            uint256 mintedFree,
            uint256 claimableHourlyDrops,
            uint256 claimableSacrifices,
            uint256 claimableReferrals,
            uint256 claimableGiveaway
        )
    {
        unchecked {
            // Calculate how many the account has claimed for free.
            mintedFree = _accountToMintedFree(account);

            // Offers are limited to non-contracts and if the account has not reached its limit of 10 free CryptoBlobs.
            if (!_isContractPastConstruction(account) && mintedFree < _maxFreePerAccount) {

                // Priority: claimableReferrals, claimableHourlyDrops, claimableSacrifices, claimableGiveaway.
                // The priority system will 'overwrite' CryptoBlobs you've earned in the order above.

                uint256 mintedFreeFuture = mintedFree;
                uint256 tokensMinted = _tokensMinted();

                // Return if 25K+ have been minted.
                if (tokensMinted >= _referralProgramEnds) 
                    return (
                        mintedFree, 
                        claimableHourlyDrops, 
                        claimableSacrifices, 
                        claimableReferrals, 
                        claimableGiveaway
                    );
                
                // REFERRAL PROGRAM (claimableReferrals)
                // Granted when:
                // - under 25K mints
                // - 1 for every 5 purchased by referred accounts
                if ((_account[account].referralPurchases / _referralsPerReward) > _account[account].mintedReferrals) {
                    claimableReferrals = (_account[account].referralPurchases / _referralsPerReward) - _account[account].mintedReferrals;
                    mintedFreeFuture += claimableReferrals;
                    if (mintedFreeFuture >= _maxFreePerAccount) { // If limit reached or surpassed.
                        return (
                            mintedFree, 
                            claimableHourlyDrops, 
                            claimableSacrifices, 
                            claimableReferrals - (mintedFreeFuture - _maxFreePerAccount), // Remove any over limit.
                            claimableGiveaway
                        );
                    }
                }
                
                // Return if 20K+ have been minted.
                if (tokensMinted >= _sacrificeAndHourlyDropsOffersEnds)
                    return (
                        mintedFree, 
                        claimableHourlyDrops, 
                        claimableSacrifices, 
                        claimableReferrals, 
                        claimableGiveaway
                    ); 

                // HOURLY DROP (claimableHourlyDrops)
                // Granted when:
                // - under 20K mints
                // - has been initialized (when 5 or more purchased)
                // - not initialized or claimed hourly drop today
                if (
                    _account[account].timestampHourlyDropLastClaimed != 0 && // If initialized.
                    _account[account].timestampHourlyDropLastClaimed / _hourlyDropDuration != block.timestamp / _hourlyDropDuration // If not initialized or claimed today.
                ) {
                    claimableHourlyDrops = 1; // Grant 1.
                    mintedFreeFuture += claimableHourlyDrops;
                    if (mintedFreeFuture >= _maxFreePerAccount) { // If limit reached or surpassed.
                        return (
                            mintedFree, 
                            claimableHourlyDrops, // Only 1 granted so mintedFreeFuture cannot be > _maxFreePerAccount, only equal to.
                            claimableSacrifices, 
                            claimableReferrals, 
                            claimableGiveaway
                        );
                    }
                }

                // SACRIFICE PROMO (claimableSacrifices)
                // Granted when:
                // - under 20K mints
                // - 1 for every 10 burned
                if ((_account[account].tokensBurned / _burnsPerReward) > _account[account].mintedSacrifices) {
                    claimableSacrifices = (_account[account].tokensBurned / _burnsPerReward) - _account[account].mintedSacrifices;
                    mintedFreeFuture += claimableSacrifices;
                    if (mintedFreeFuture >= _maxFreePerAccount) { // If limit reached or surpassed.
                        return (
                            mintedFree, 
                            claimableHourlyDrops, 
                            claimableSacrifices - (mintedFreeFuture - _maxFreePerAccount), // Remove any over limit.
                            claimableReferrals, 
                            claimableGiveaway
                        );
                    }
                }

                // Return if 10K+ have been minted.
                if (tokensMinted >= _giveawayEnds)
                    return (
                        mintedFree, 
                        claimableHourlyDrops, 
                        claimableSacrifices, 
                        claimableReferrals, 
                        claimableGiveaway
                    ); 

                // GIVEAWAY (claimableGiveaway)
                // Granted when:
                // - under 10K mints
                // - not claimed from the giveaway
                // - NFT redeeming is valid
                if (_account[account].mintedGiveaway == 0) { // If not claimed from the giveaway.
                    claimableGiveaway = 1; // Grant 1.
                    mintedFreeFuture += claimableGiveaway;
                    if (mintedFreeFuture >= _maxFreePerAccount) { // If limit reached or surpassed.
                        return (
                            mintedFree, 
                            claimableHourlyDrops, 
                            claimableSacrifices, 
                            claimableReferrals, 
                            claimableGiveaway // Only 1 granted so mintedFreeFuture cannot be > _maxFreePerAccount, only equal to.
                        );
                    }
                }
            }
            return (mintedFree, claimableHourlyDrops, claimableSacrifices, claimableReferrals, claimableGiveaway);
        }
    }