import { AbortError } from '@libp2p/interface';
import { Queue } from '@libp2p/utils/queue';
import { pushable } from 'it-pushable';
import { xor as uint8ArrayXor } from 'uint8arrays/xor';
import { xorCompare as uint8ArrayXorCompare } from 'uint8arrays/xor-compare';
import { convertPeerId, convertBuffer } from '../utils.js';
import { pathEndedEvent, queryErrorEvent } from './events.js';
/**
 * Walks a path through the DHT, calling the passed query function for
 * every peer encountered that we have not seen before
 */
export async function* queryPath(options) {
    const { key, startingPeers, ourPeerId, query, alpha, path, numPaths, log, peersSeen, connectionManager, signal } = options;
    const events = pushable({
        objectMode: true
    });
    // Only ALPHA node/value lookups are allowed at any given time for each process
    // https://github.com/libp2p/specs/tree/master/kad-dht#alpha-concurrency-parameter-%CE%B1
    const queue = new Queue({
        concurrency: alpha,
        sort: (a, b) => uint8ArrayXorCompare(a.options.distance, b.options.distance)
    });
    queue.addEventListener('idle', () => {
        events.push(pathEndedEvent({
            path: {
                index: path,
                queued: queue.queued,
                running: queue.running,
                total: queue.size
            }
        }, options));
        events.end();
    });
    queue.addEventListener('error', (evt) => {
        log.error('error during query - %e', evt.detail);
    });
    signal.addEventListener('abort', () => {
        queue.abort();
        events.end(new AbortError());
    });
    // perform lookups on kadId, not the actual value
    const kadId = await convertBuffer(key, {
        signal
    });
    /**
     * Adds the passed peer to the query queue if it's not us and no other path
     * has passed through this peer
     */
    function queryPeer(peer, peerKadId) {
        if (peer == null) {
            return;
        }
        peersSeen.add(peer.id.toMultihash().bytes);
        const peerXor = uint8ArrayXor(peerKadId, kadId);
        queue.add(async () => {
            try {
                for await (const event of query({
                    ...options,
                    key,
                    peer,
                    path: {
                        index: path,
                        queued: queue.queued,
                        running: queue.running,
                        total: queue.size
                    },
                    numPaths,
                    peerKadId,
                    signal
                })) {
                    // if there are closer peers and the query has not completed, continue the query
                    if (event.name === 'PEER_RESPONSE') {
                        for (const closerPeer of event.closer) {
                            if (peersSeen.has(closerPeer.id.toMultihash().bytes)) { // eslint-disable-line max-depth
                                log('already seen %p in query', closerPeer.id);
                                continue;
                            }
                            if (ourPeerId.equals(closerPeer.id)) { // eslint-disable-line max-depth
                                log('not querying ourselves');
                                continue;
                            }
                            if (!(await connectionManager.isDialable(closerPeer.multiaddrs))) { // eslint-disable-line max-depth
                                log('not querying undialable peer');
                                continue;
                            }
                            const closerPeerKadId = await convertPeerId(closerPeer.id, {
                                signal
                            });
                            const closerPeerXor = uint8ArrayXor(closerPeerKadId, kadId);
                            // only continue query if closer peer is actually closer
                            if (uint8ArrayXorCompare(closerPeerXor, peerXor) !== -1) { // eslint-disable-line max-depth
                                log('skipping %p as they are not closer to %b than %p', closerPeer.id, key, peer);
                                continue;
                            }
                            log('querying closer peer %p', closerPeer.id);
                            queryPeer(closerPeer, closerPeerKadId);
                        }
                    }
                    events.push({
                        ...event,
                        path: {
                            index: path,
                            queued: queue.queued,
                            running: queue.running,
                            total: queue.size
                        }
                    });
                }
            }
            catch (err) {
                // yield error event if query is continuing
                events.push(queryErrorEvent({
                    from: peer.id,
                    error: err,
                    path: {
                        index: path,
                        queued: queue.queued,
                        running: queue.running - 1,
                        total: queue.size - 1
                    }
                }, options));
            }
        }, {
            distance: peerXor
        }).catch(err => {
            log.error('error during query - %e', err);
        });
    }
    // begin the query with the starting peers
    await Promise.all(startingPeers.map(async (startingPeer) => {
        queryPeer({ id: startingPeer, multiaddrs: [] }, await convertPeerId(startingPeer, {
            signal
        }));
    }));
    // yield results as they come in
    yield* events;
}
//# sourceMappingURL=query-path.js.map