import * as Constants from './Constants.js';
import { InnertubeError, Platform, streamToIterable } from './Utils.js';
export async function download(options, actions, playability_status, streaming_data, player, cpn) {
    if (playability_status?.status === 'UNPLAYABLE')
        throw new InnertubeError('Video is unplayable', { error_type: 'UNPLAYABLE' });
    if (playability_status?.status === 'LOGIN_REQUIRED')
        throw new InnertubeError('Video is login required', { error_type: 'LOGIN_REQUIRED' });
    if (!streaming_data)
        throw new InnertubeError('Streaming data not available.', { error_type: 'NO_STREAMING_DATA' });
    const opts = {
        quality: '360p',
        type: 'video+audio',
        format: 'mp4',
        range: undefined,
        ...options
    };
    const format = chooseFormat(opts, streaming_data);
    const format_url = await format.decipher(player);
    // If we're not downloading the video in chunks, we just use fetch once.
    if (opts.type === 'video+audio' && !options.range) {
        const response = await actions.session.http.fetch_function(`${format_url}&cpn=${cpn}`, {
            method: 'GET',
            headers: Constants.STREAM_HEADERS,
            redirect: 'follow'
        });
        // Throw if the response is not 2xx
        if (!response.ok)
            throw new InnertubeError('The server responded with a non 2xx status code', { error_type: 'FETCH_FAILED', response });
        const body = response.body;
        if (!body)
            throw new InnertubeError('Could not get ReadableStream from fetch Response.', { error_type: 'FETCH_FAILED', response });
        return body;
    }
    // We need to download in chunks.
    const chunk_size = 1048576 * 10; // 10MB
    let chunk_start = (options.range ? options.range.start : 0);
    let chunk_end = (options.range ? options.range.end : chunk_size);
    let must_end = false;
    let cancel;
    return new Platform.shim.ReadableStream({
        start() {
        },
        pull: async (controller) => {
            if (must_end) {
                controller.close();
                return;
            }
            if ((chunk_end >= (format.content_length ? format.content_length : 0)) || options.range) {
                must_end = true;
            }
            return new Promise(async (resolve, reject) => {
                try {
                    cancel = new AbortController();
                    const response = await actions.session.http.fetch_function(`${format_url}&cpn=${cpn}&range=${chunk_start}-${chunk_end || ''}`, {
                        method: 'GET',
                        headers: {
                            ...Constants.STREAM_HEADERS
                            // XXX: use YouTube's range parameter instead of a Range header.
                            // Range: `bytes=${chunk_start}-${chunk_end}`
                        },
                        signal: cancel.signal
                    });
                    // Throw if the response is not 2xx
                    if (!response.ok)
                        throw new InnertubeError('The server responded with a non 2xx status code', {
                            error_type: 'FETCH_FAILED',
                            response
                        });
                    const body = response.body;
                    if (!body)
                        throw new InnertubeError('Could not get ReadableStream from fetch Response.', {
                            error_type: 'FETCH_FAILED',
                            response
                        });
                    for await (const chunk of streamToIterable(body)) {
                        controller.enqueue(chunk);
                    }
                    chunk_start = chunk_end + 1;
                    chunk_end += chunk_size;
                    resolve();
                }
                catch (e) {
                    reject(e);
                }
            });
        },
        async cancel(reason) {
            cancel.abort(reason);
        }
    }, {
        highWaterMark: 1, // TODO: better value?
        size(chunk) {
            return chunk.byteLength;
        }
    });
}
/**
 * Selects the format that best matches the given options.
 * @param options - Options
 * @param streaming_data - Streaming data
 */
export function chooseFormat(options, streaming_data) {
    if (!streaming_data)
        throw new InnertubeError('Streaming data not available');
    const formats = [
        ...(streaming_data.formats || []),
        ...(streaming_data.adaptive_formats || [])
    ];
    if (options.itag) {
        const candidates = formats.filter((format) => format.itag === options.itag);
        if (!candidates.length)
            throw new InnertubeError('No matching formats found', { options });
        return candidates[0];
    }
    const requires_audio = options.type ? options.type.includes('audio') : true;
    const requires_video = options.type ? options.type.includes('video') : true;
    const language = options.language || 'original';
    const quality = options.quality || 'best';
    let best_width = -1;
    const is_best = ['best', 'bestefficiency'].includes(quality);
    const use_most_efficient = quality !== 'best';
    let candidates = formats.filter((format) => {
        if (requires_audio && !format.has_audio)
            return false;
        if (requires_video && !format.has_video)
            return false;
        if (options.codec && !format.mime_type.includes(options.codec))
            return false;
        if (options.format !== 'any' && !format.mime_type.includes(options.format || 'mp4'))
            return false;
        if (!is_best && format.quality_label !== quality)
            return false;
        if (format.width && (best_width < format.width))
            best_width = format.width;
        return true;
    });
    if (!candidates.length)
        throw new InnertubeError('No matching formats found', { options });
    if (is_best && requires_video)
        candidates = candidates.filter((format) => format.width === best_width);
    if (requires_audio && !requires_video) {
        const audio_only = candidates.filter((format) => {
            if (language !== 'original') {
                return !format.has_video && !format.has_text && format.language === language;
            }
            return !format.has_video && !format.has_text && format.is_original;
        });
        if (audio_only.length > 0) {
            candidates = audio_only;
        }
    }
    if (use_most_efficient) {
        // Sort by bitrate (lower is better)
        candidates.sort((a, b) => a.bitrate - b.bitrate);
    }
    else {
        // Sort by bitrate (higher is better)
        candidates.sort((a, b) => b.bitrate - a.bitrate);
    }
    return candidates[0];
}
export { toDash } from './DashManifest.js';
//# sourceMappingURL=FormatUtils.js.map