/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __generator = (this && this.__generator) || function (thisArg, body) {
    var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
    return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
    function verb(n) { return function (v) { return step([n, v]); }; }
    function step(op) {
        if (f) throw new TypeError("Generator is already executing.");
        while (g && (g = 0, op[0] && (_ = 0)), _) try {
            if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
            if (y = 0, t) op = [op[0] & 2, t.value];
            switch (op[0]) {
                case 0: case 1: t = op; break;
                case 4: _.label++; return { value: op[1], done: false };
                case 5: _.label++; y = op[1]; op = [0]; continue;
                case 7: op = _.ops.pop(); _.trys.pop(); continue;
                default:
                    if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
                    if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
                    if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
                    if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
                    if (t[2]) _.ops.pop();
                    _.trys.pop(); continue;
            }
            op = body.call(thisArg, _);
        } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
        if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
    }
};
import nextTick from "next-tick";
import config from "../../../config";
import { formatError, MediaError, } from "../../../errors";
import log from "../../../log";
import objectAssign from "../../../utils/object_assign";
import { getLeftSizeOfRange } from "../../../utils/ranges";
import SharedReference from "../../../utils/reference";
import TaskCanceller, { CancellationError, } from "../../../utils/task_canceller";
import SegmentBuffersStore from "../../segment_buffers";
import AdaptationStream from "../adaptation";
import getAdaptationSwitchStrategy from "./utils/get_adaptation_switch_strategy";
/**
 * Create a single PeriodStream:
 *   - Lazily create (or reuse) a SegmentBuffer for the given type.
 *   - Create a Stream linked to an Adaptation each time it changes, to
 *     download and append the corresponding segments to the SegmentBuffer.
 *   - Announce when the Stream is full or is awaiting new Segments through
 *     events
 *
 * @param {Object} args - Various arguments allowing the `PeriodStream` to
 * determine which Adaptation and which Representation to choose, as well as
 * which segments to load from it.
 * You can check the corresponding type for more information.
 * @param {Object} callbacks - The `PeriodStream` relies on a system of
 * callbacks that it will call on various events.
 *
 * Depending on the event, the caller may be supposed to perform actions to
 * react upon some of them.
 *
 * This approach is taken instead of a more classical EventEmitter pattern to:
 *   - Allow callbacks to be called synchronously after the
 *     `AdaptationStream` is called.
 *   - Simplify bubbling events up, by just passing through callbacks
 *   - Force the caller to explicitely handle or not the different events.
 *
 * Callbacks may start being called immediately after the `AdaptationStream`
 * call and may be called until either the `parentCancelSignal` argument is
 * triggered, or until the `error` callback is called, whichever comes first.
 * @param {Object} parentCancelSignal - `CancellationSignal` allowing, when
 * triggered, to immediately stop all operations the `PeriodStream` is
 * doing.
 */
export default function PeriodStream(_a, callbacks, parentCancelSignal) {
    var _this = this;
    var bufferType = _a.bufferType, content = _a.content, garbageCollectors = _a.garbageCollectors, playbackObserver = _a.playbackObserver, representationEstimator = _a.representationEstimator, segmentFetcherCreator = _a.segmentFetcherCreator, segmentBuffersStore = _a.segmentBuffersStore, options = _a.options, wantedBufferAhead = _a.wantedBufferAhead, maxVideoBufferSize = _a.maxVideoBufferSize;
    var period = content.period;
    /**
     * Emits the chosen Adaptation for the current type.
     * `null` when no Adaptation is chosen (e.g. no subtitles)
     * `undefined` at the beginning (it can be ignored.).
     */
    var adaptationRef = new SharedReference(undefined, parentCancelSignal);
    callbacks.periodStreamReady({ type: bufferType, period: period, adaptationRef: adaptationRef });
    if (parentCancelSignal.isCancelled()) {
        return;
    }
    var currentStreamCanceller;
    var isFirstAdaptationSwitch = true;
    adaptationRef.onUpdate(function (adaptation) {
        // As an IIFE to profit from async/await while respecting onUpdate's signature
        (function () { return __awaiter(_this, void 0, void 0, function () {
            var streamCanceller, segmentBufferStatus, periodEnd, DELTA_POSITION_AFTER_RELOAD, relativePosAfterSwitch, readyState, segmentBuffer, playbackInfos, strategy, _i, _a, _b, start, end;
            var _c;
            return __generator(this, function (_d) {
                switch (_d.label) {
                    case 0:
                        if (adaptation === undefined) {
                            return [2 /*return*/];
                        }
                        streamCanceller = new TaskCanceller();
                        streamCanceller.linkToSignal(parentCancelSignal);
                        currentStreamCanceller === null || currentStreamCanceller === void 0 ? void 0 : currentStreamCanceller.cancel(); // Cancel oreviously created stream if one
                        currentStreamCanceller = streamCanceller;
                        if (!(adaptation === null)) return [3 /*break*/, 7];
                        log.info("Stream: Set no ".concat(bufferType, " Adaptation. P:"), period.start);
                        segmentBufferStatus = segmentBuffersStore.getStatus(bufferType);
                        if (!(segmentBufferStatus.type === "initialized")) return [3 /*break*/, 5];
                        log.info("Stream: Clearing previous ".concat(bufferType, " SegmentBuffer"));
                        if (!SegmentBuffersStore.isNative(bufferType)) return [3 /*break*/, 1];
                        return [2 /*return*/, askForMediaSourceReload(0, true, streamCanceller.signal)];
                    case 1:
                        periodEnd = (_c = period.end) !== null && _c !== void 0 ? _c : Infinity;
                        if (!(period.start > periodEnd)) return [3 /*break*/, 2];
                        log.warn("Stream: Can't free buffer: period's start is after its end");
                        return [3 /*break*/, 4];
                    case 2: return [4 /*yield*/, segmentBufferStatus.value.removeBuffer(period.start, periodEnd, streamCanceller.signal)];
                    case 3:
                        _d.sent();
                        if (streamCanceller.isUsed()) {
                            return [2 /*return*/]; // The stream has been cancelled
                        }
                        _d.label = 4;
                    case 4: return [3 /*break*/, 6];
                    case 5:
                        if (segmentBufferStatus.type === "uninitialized") {
                            segmentBuffersStore.disableSegmentBuffer(bufferType);
                            if (streamCanceller.isUsed()) {
                                return [2 /*return*/]; // The stream has been cancelled
                            }
                        }
                        _d.label = 6;
                    case 6:
                        callbacks.adaptationChange({ type: bufferType, adaptation: null, period: period });
                        if (streamCanceller.isUsed()) {
                            return [2 /*return*/]; // Previous call has provoken Stream cancellation by side-effect
                        }
                        return [2 /*return*/, createEmptyAdaptationStream(playbackObserver, wantedBufferAhead, bufferType, { period: period }, callbacks, streamCanceller.signal)];
                    case 7:
                        DELTA_POSITION_AFTER_RELOAD = config.getCurrent().DELTA_POSITION_AFTER_RELOAD;
                        relativePosAfterSwitch = isFirstAdaptationSwitch ? 0 :
                            bufferType === "audio" ? DELTA_POSITION_AFTER_RELOAD.trackSwitch.audio :
                                bufferType === "video" ? DELTA_POSITION_AFTER_RELOAD.trackSwitch.video :
                                    DELTA_POSITION_AFTER_RELOAD.trackSwitch.other;
                        isFirstAdaptationSwitch = false;
                        if (SegmentBuffersStore.isNative(bufferType) &&
                            segmentBuffersStore.getStatus(bufferType).type === "disabled") {
                            return [2 /*return*/, askForMediaSourceReload(relativePosAfterSwitch, true, streamCanceller.signal)];
                        }
                        log.info("Stream: Updating ".concat(bufferType, " adaptation"), "A: ".concat(adaptation.id), "P: ".concat(period.start));
                        callbacks.adaptationChange({ type: bufferType, adaptation: adaptation, period: period });
                        if (streamCanceller.isUsed()) {
                            return [2 /*return*/]; // Previous call has provoken cancellation by side-effect
                        }
                        readyState = playbackObserver.getReadyState();
                        segmentBuffer = createOrReuseSegmentBuffer(segmentBuffersStore, bufferType, adaptation, options);
                        playbackInfos = { currentTime: playbackObserver.getCurrentTime(), readyState: readyState };
                        strategy = getAdaptationSwitchStrategy(segmentBuffer, period, adaptation, playbackInfos, options);
                        if (strategy.type === "needs-reload") {
                            return [2 /*return*/, askForMediaSourceReload(relativePosAfterSwitch, true, streamCanceller.signal)];
                        }
                        return [4 /*yield*/, segmentBuffersStore.waitForUsableBuffers(streamCanceller.signal)];
                    case 8:
                        _d.sent();
                        if (streamCanceller.isUsed()) {
                            return [2 /*return*/]; // The Stream has since been cancelled
                        }
                        if (!(strategy.type === "flush-buffer" || strategy.type === "clean-buffer")) return [3 /*break*/, 13];
                        _i = 0, _a = strategy.value;
                        _d.label = 9;
                    case 9:
                        if (!(_i < _a.length)) return [3 /*break*/, 12];
                        _b = _a[_i], start = _b.start, end = _b.end;
                        return [4 /*yield*/, segmentBuffer.removeBuffer(start, end, streamCanceller.signal)];
                    case 10:
                        _d.sent();
                        if (streamCanceller.isUsed()) {
                            return [2 /*return*/]; // The Stream has since been cancelled
                        }
                        _d.label = 11;
                    case 11:
                        _i++;
                        return [3 /*break*/, 9];
                    case 12:
                        if (strategy.type === "flush-buffer") {
                            callbacks.needsBufferFlush();
                            if (streamCanceller.isUsed()) {
                                return [2 /*return*/]; // Previous callback cancelled the Stream by side-effect
                            }
                        }
                        _d.label = 13;
                    case 13:
                        garbageCollectors.get(segmentBuffer)(streamCanceller.signal);
                        createAdaptationStream(adaptation, segmentBuffer, streamCanceller.signal);
                        return [2 /*return*/];
                }
            });
        }); })().catch(function (err) {
            if (err instanceof CancellationError) {
                return;
            }
            currentStreamCanceller === null || currentStreamCanceller === void 0 ? void 0 : currentStreamCanceller.cancel();
            callbacks.error(err);
        });
    }, { clearSignal: parentCancelSignal, emitCurrentValue: true });
    /**
     * @param {Object} adaptation
     * @param {Object} segmentBuffer
     * @param {Object} cancelSignal
     */
    function createAdaptationStream(adaptation, segmentBuffer, cancelSignal) {
        var manifest = content.manifest;
        var adaptationPlaybackObserver = createAdaptationStreamPlaybackObserver(playbackObserver, segmentBuffer);
        AdaptationStream({ content: { manifest: manifest, period: period, adaptation: adaptation }, options: options, playbackObserver: adaptationPlaybackObserver, representationEstimator: representationEstimator, segmentBuffer: segmentBuffer, segmentFetcherCreator: segmentFetcherCreator, wantedBufferAhead: wantedBufferAhead, maxVideoBufferSize: maxVideoBufferSize }, __assign(__assign({}, callbacks), { error: onAdaptationStreamError }), cancelSignal);
        function onAdaptationStreamError(error) {
            // Stream linked to a non-native media buffer should not impact the
            // stability of the player. ie: if a text buffer sends an error, we want
            // to continue playing without any subtitles
            if (!SegmentBuffersStore.isNative(bufferType)) {
                log.error("Stream: ".concat(bufferType, " Stream crashed. Aborting it."), error instanceof Error ? error : "");
                segmentBuffersStore.disposeSegmentBuffer(bufferType);
                var formattedError = formatError(error, {
                    defaultCode: "NONE",
                    defaultReason: "Unknown `AdaptationStream` error",
                });
                callbacks.warning(formattedError);
                if (cancelSignal.isCancelled()) {
                    return; // Previous callback cancelled the Stream by side-effect
                }
                return createEmptyAdaptationStream(playbackObserver, wantedBufferAhead, bufferType, { period: period }, callbacks, cancelSignal);
            }
            log.error("Stream: ".concat(bufferType, " Stream crashed. Stopping playback."), error instanceof Error ? error : "");
            callbacks.error(error);
        }
    }
    /**
     * Regularly ask to reload the MediaSource on each playback observation
     * performed by the playback observer.
     *
     * @param {number} timeOffset - Relative position, compared to the current
     * playhead, at which we should restart playback after reloading.
     * For example `-2` will reload 2 seconds before the current position.
     * @param {boolean} stayInPeriod - If `true`, we will control that the position
     * we reload at, after applying `timeOffset`, is still part of the Period
     * `period`.
     *
     * If it isn't we will re-calculate that reloaded position to be:
     *   - either the Period's start if the calculated position is before the
     *     Period's start.
     *   - either the Period'end start if the calculated position is after the
     *     Period's end.
     * @param {Object} cancelSignal
     */
    function askForMediaSourceReload(timeOffset, stayInPeriod, cancelSignal) {
        // We begin by scheduling a micro-task to reduce the possibility of race
        // conditions where `askForMediaSourceReload` would be called synchronously before
        // the next observation (which may reflect very different playback conditions)
        // is actually received.
        // It can happen when `askForMediaSourceReload` is called as a side-effect of
        // the same event that triggers the playback observation to be emitted.
        nextTick(function () {
            playbackObserver.listen(function () {
                callbacks.waitingMediaSourceReload({ bufferType: bufferType, period: period, timeOffset: timeOffset, stayInPeriod: stayInPeriod });
            }, { includeLastObservation: true, clearSignal: cancelSignal });
        });
    }
}
/**
 * @param {string} bufferType
 * @param {Object} adaptation
 * @returns {Object}
 */
function createOrReuseSegmentBuffer(segmentBuffersStore, bufferType, adaptation, options) {
    var segmentBufferStatus = segmentBuffersStore.getStatus(bufferType);
    if (segmentBufferStatus.type === "initialized") {
        log.info("Stream: Reusing a previous SegmentBuffer for the type", bufferType);
        // eslint-disable-next-line @typescript-eslint/no-unsafe-return
        return segmentBufferStatus.value;
    }
    var codec = getFirstDeclaredMimeType(adaptation);
    var sbOptions = bufferType === "text" ? options.textTrackOptions : undefined;
    // eslint-disable-next-line @typescript-eslint/no-unsafe-return
    return segmentBuffersStore.createSegmentBuffer(bufferType, codec, sbOptions);
}
/**
 * Get mime-type string of the first representation declared in the given
 * adaptation.
 * @param {Adaptation} adaptation
 * @returns {string}
 */
function getFirstDeclaredMimeType(adaptation) {
    var representations = adaptation.getPlayableRepresentations();
    if (representations.length === 0) {
        var noRepErr = new MediaError("NO_PLAYABLE_REPRESENTATION", "No Representation in the chosen " +
            adaptation.type + " Adaptation can be played", { adaptation: adaptation });
        throw noRepErr;
    }
    return representations[0].getMimeTypeString();
}
/**
 * Create AdaptationStream's version of a playback observer.
 * @param {Object} initialPlaybackObserver
 * @param {Object} segmentBuffer
 * @returns {Object}
 */
function createAdaptationStreamPlaybackObserver(initialPlaybackObserver, segmentBuffer) {
    return initialPlaybackObserver.deriveReadOnlyObserver(function transform(observationRef, cancellationSignal) {
        var newRef = new SharedReference(constructAdaptationStreamPlaybackObservation(), cancellationSignal);
        observationRef.onUpdate(emitAdaptationStreamPlaybackObservation, {
            clearSignal: cancellationSignal,
            emitCurrentValue: false,
        });
        return newRef;
        function constructAdaptationStreamPlaybackObservation() {
            var baseObservation = observationRef.getValue();
            var buffered = segmentBuffer.getBufferedRanges();
            var bufferGap = getLeftSizeOfRange(buffered, baseObservation.position.last);
            return objectAssign({}, baseObservation, { bufferGap: bufferGap });
        }
        function emitAdaptationStreamPlaybackObservation() {
            newRef.setValue(constructAdaptationStreamPlaybackObservation());
        }
    });
}
/**
 * Create empty AdaptationStream, linked to a Period.
 * This AdaptationStream will never download any segment and just emit a "full"
 * event when reaching the end.
 * @param {Object} playbackObserver
 * @param {Object} wantedBufferAhead
 * @param {string} bufferType
 * @param {Object} content
 * @param {Object} callbacks
 * @param {Object} cancelSignal
 */
function createEmptyAdaptationStream(playbackObserver, wantedBufferAhead, bufferType, content, callbacks, cancelSignal) {
    var period = content.period;
    var hasFinishedLoading = false;
    wantedBufferAhead.onUpdate(sendStatus, { emitCurrentValue: false, clearSignal: cancelSignal });
    playbackObserver.listen(sendStatus, { includeLastObservation: false, clearSignal: cancelSignal });
    sendStatus();
    function sendStatus() {
        var observation = playbackObserver.getReference().getValue();
        var wba = wantedBufferAhead.getValue();
        var position = observation.position.last;
        if (period.end !== undefined && position + wba >= period.end) {
            log.debug("Stream: full \"empty\" AdaptationStream", bufferType);
            hasFinishedLoading = true;
        }
        callbacks.streamStatusUpdate({ period: period, bufferType: bufferType, position: position, imminentDiscontinuity: null,
            isEmptyStream: true, hasFinishedLoading: hasFinishedLoading, neededSegments: [] });
    }
}
