/**
 * 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 };
    }
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
    if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
        if (ar || !(i in from)) {
            if (!ar) ar = Array.prototype.slice.call(from, 0, i);
            ar[i] = from[i];
        }
    }
    return to.concat(ar || Array.prototype.slice.call(from));
};
import nextTick from "next-tick";
import config from "../../../config";
import { MediaError } from "../../../errors";
import log from "../../../log";
import { createMappedReference, } from "../../../utils/reference";
import SortedList from "../../../utils/sorted_list";
import TaskCanceller from "../../../utils/task_canceller";
import WeakMapMemory from "../../../utils/weak_map_memory";
import { BufferGarbageCollector, } from "../../segment_buffers";
import PeriodStream from "../period";
import getTimeRangesForContent from "./get_time_ranges_for_content";
/**
 * Create and manage the various "Streams" needed for the content to
 * play:
 *
 *   - Create or dispose SegmentBuffers depending on the chosen Adaptations.
 *
 *   - Push the right segments to those SegmentBuffers depending on the user's
 *     preferences, the current position, the bandwidth, the decryption
 *     conditions...
 *
 *   - Concatenate Streams for adaptation from separate Periods at the right
 *     time, to allow smooth transitions between periods.
 *
 *   - Call various callbacks to notify of its health and issues
 *
 * @param {Object} content
 * @param {Object} playbackObserver - Emit position information
 * @param {Object} representationEstimator - Emit bitrate estimates and best
 * Representation to play.
 * @param {Object} segmentBuffersStore - Will be used to lazily create
 * SegmentBuffer instances associated with the current content.
 * @param {Object} segmentFetcherCreator - Allow to download segments.
 * @param {Object} options
 * @param {Object} callbacks - The `StreamOrchestrator` 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
 *     `StreamOrchestrator` 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 `StreamOrchestrator`
 * call and may be called until either the `parentCancelSignal` argument is
 * triggered, or until the `error` callback is called, whichever comes first.
 * @param {Object} orchestratorCancelSignal - `CancellationSignal` allowing,
 * when triggered, to immediately stop all operations the `PeriodStream` is
 * doing.
 */
export default function StreamOrchestrator(content, playbackObserver, representationEstimator, segmentBuffersStore, segmentFetcherCreator, options, callbacks, orchestratorCancelSignal) {
    var manifest = content.manifest, initialPeriod = content.initialPeriod;
    var maxBufferAhead = options.maxBufferAhead, maxBufferBehind = options.maxBufferBehind, wantedBufferAhead = options.wantedBufferAhead, maxVideoBufferSize = options.maxVideoBufferSize;
    var _a = config.getCurrent(), MINIMUM_MAX_BUFFER_AHEAD = _a.MINIMUM_MAX_BUFFER_AHEAD, MAXIMUM_MAX_BUFFER_AHEAD = _a.MAXIMUM_MAX_BUFFER_AHEAD, MAXIMUM_MAX_BUFFER_BEHIND = _a.MAXIMUM_MAX_BUFFER_BEHIND;
    // Keep track of a unique BufferGarbageCollector created per
    // SegmentBuffer.
    var garbageCollectors = new WeakMapMemory(function (segmentBuffer) {
        var _a, _b;
        var bufferType = segmentBuffer.bufferType;
        var defaultMaxBehind = (_a = MAXIMUM_MAX_BUFFER_BEHIND[bufferType]) !== null && _a !== void 0 ? _a : Infinity;
        var maxAheadHigherBound = (_b = MAXIMUM_MAX_BUFFER_AHEAD[bufferType]) !== null && _b !== void 0 ? _b : Infinity;
        return function (gcCancelSignal) {
            BufferGarbageCollector({ segmentBuffer: segmentBuffer, playbackObserver: playbackObserver, maxBufferBehind: createMappedReference(maxBufferBehind, function (val) {
                    return Math.min(val, defaultMaxBehind);
                }, gcCancelSignal),
                maxBufferAhead: createMappedReference(maxBufferAhead, function (val) {
                    var _a;
                    var lowerBound = Math.max(val, (_a = MINIMUM_MAX_BUFFER_AHEAD[bufferType]) !== null && _a !== void 0 ? _a : 0);
                    return Math.min(lowerBound, maxAheadHigherBound);
                }, gcCancelSignal) }, gcCancelSignal);
        };
    });
    // Create automatically the right `PeriodStream` for every possible types
    for (var _i = 0, _b = segmentBuffersStore.getBufferTypes(); _i < _b.length; _i++) {
        var bufferType = _b[_i];
        manageEveryStreams(bufferType, initialPeriod);
    }
    /**
     * Manage creation and removal of Streams for every Periods for a given type.
     *
     * Works by creating consecutive Streams through the
     * `manageConsecutivePeriodStreams` function, and restarting it when the
     * current position goes out of the bounds of these Streams.
     * @param {string} bufferType - e.g. "audio" or "video"
     * @param {Period} basePeriod - Initial Period downloaded.
     */
    function manageEveryStreams(bufferType, basePeriod) {
        /** Each Period for which there is currently a Stream, chronologically */
        var periodList = new SortedList(function (a, b) { return a.start - b.start; });
        /**
         * When set to `true`, all the currently active PeriodStream will be destroyed
         * and re-created from the new current position if we detect it to be out of
         * their bounds.
         * This is set to false when we're in the process of creating the first
         * PeriodStream, to avoid interferences while no PeriodStream is available.
         */
        var enableOutOfBoundsCheck = false;
        /** Cancels currently created `PeriodStream`s. */
        var currentCanceller = new TaskCanceller();
        currentCanceller.linkToSignal(orchestratorCancelSignal);
        // Restart the current Stream when the wanted time is in another period
        // than the ones already considered
        playbackObserver.listen(function (_a) {
            var _b, _c;
            var position = _a.position;
            var time = (_b = position.pending) !== null && _b !== void 0 ? _b : position.last;
            if (!enableOutOfBoundsCheck || !isOutOfPeriodList(time)) {
                return;
            }
            log.info("Stream: Destroying all PeriodStreams due to out of bounds situation", bufferType, time);
            enableOutOfBoundsCheck = false;
            while (periodList.length() > 0) {
                var period = periodList.get(periodList.length() - 1);
                periodList.removeElement(period);
                callbacks.periodStreamCleared({ type: bufferType, period: period });
            }
            currentCanceller.cancel();
            currentCanceller = new TaskCanceller();
            currentCanceller.linkToSignal(orchestratorCancelSignal);
            var nextPeriod = (_c = manifest.getPeriodForTime(time)) !== null && _c !== void 0 ? _c : manifest.getNextPeriod(time);
            if (nextPeriod === undefined) {
                log.warn("Stream: The wanted position is not found in the Manifest.");
                enableOutOfBoundsCheck = true;
                return;
            }
            launchConsecutiveStreamsForPeriod(nextPeriod);
        }, { clearSignal: orchestratorCancelSignal, includeLastObservation: true });
        manifest.addEventListener("decipherabilityUpdate", function (evt) {
            onDecipherabilityUpdates(evt).catch(function (err) {
                currentCanceller.cancel();
                callbacks.error(err);
            });
        }, orchestratorCancelSignal);
        return launchConsecutiveStreamsForPeriod(basePeriod);
        /**
         * @param {Object} period
         */
        function launchConsecutiveStreamsForPeriod(period) {
            var consecutivePeriodStreamCb = __assign(__assign({}, callbacks), { waitingMediaSourceReload: function (payload) {
                    // Only reload the MediaSource when the more immediately required
                    // Period is the one asking for it
                    var firstPeriod = periodList.head();
                    if (firstPeriod === undefined ||
                        firstPeriod.id !== payload.period.id) {
                        callbacks.lockedStream({ bufferType: payload.bufferType,
                            period: payload.period });
                    }
                    else {
                        callbacks.needsMediaSourceReload({
                            timeOffset: payload.timeOffset,
                            minimumPosition: payload.stayInPeriod ? payload.period.start : undefined,
                            maximumPosition: payload.stayInPeriod ? payload.period.end : undefined,
                        });
                    }
                }, periodStreamReady: function (payload) {
                    enableOutOfBoundsCheck = true;
                    periodList.add(payload.period);
                    callbacks.periodStreamReady(payload);
                }, periodStreamCleared: function (payload) {
                    periodList.removeElement(payload.period);
                    callbacks.periodStreamCleared(payload);
                }, error: function (err) {
                    currentCanceller.cancel();
                    callbacks.error(err);
                } });
            manageConsecutivePeriodStreams(bufferType, period, consecutivePeriodStreamCb, currentCanceller.signal);
        }
        /**
         * Returns true if the given time is either:
         *   - less than the start of the chronologically first Period
         *   - more than the end of the chronologically last Period
         * @param {number} time
         * @returns {boolean}
         */
        function isOutOfPeriodList(time) {
            var head = periodList.head();
            var last = periodList.last();
            if (head == null || last == null) { // if no period
                return true;
            }
            return head.start > time ||
                (last.end == null ? Infinity :
                    last.end) < time;
        }
        /**
         * React to a Manifest's decipherability updates.
         * @param {Array.<Object>} updates
         * @returns {Promise}
         */
        function onDecipherabilityUpdates(updates) {
            return __awaiter(this, void 0, void 0, function () {
                var segmentBufferStatus, ofCurrentType, segmentBuffer, resettedContent, undecipherableContent, undecipherableRanges, rangesToRemove, period, _i, _a, _b, start, end;
                return __generator(this, function (_c) {
                    switch (_c.label) {
                        case 0:
                            segmentBufferStatus = segmentBuffersStore.getStatus(bufferType);
                            ofCurrentType = updates
                                .filter(function (update) { return update.adaptation.type === bufferType; });
                            if (
                            // No update concerns the current type of data
                            ofCurrentType.length === 0 ||
                                segmentBufferStatus.type !== "initialized" ||
                                // The update only notifies of now-decipherable streams
                                ofCurrentType.every(function (x) { return x.representation.decipherable === true; })) {
                                // Data won't have to be removed from the buffers, no need to stop the
                                // current Streams.
                                return [2 /*return*/];
                            }
                            segmentBuffer = segmentBufferStatus.value;
                            resettedContent = ofCurrentType.filter(function (update) {
                                return update.representation.decipherable === undefined;
                            });
                            undecipherableContent = ofCurrentType.filter(function (update) {
                                return update.representation.decipherable === false;
                            });
                            undecipherableRanges = getTimeRangesForContent(segmentBuffer, undecipherableContent);
                            rangesToRemove = getTimeRangesForContent(segmentBuffer, resettedContent);
                            // First close all Stream currently active so they don't continue to
                            // load and push segments.
                            enableOutOfBoundsCheck = false;
                            log.info("Stream: Destroying all PeriodStreams for decipherability matters", bufferType);
                            while (periodList.length() > 0) {
                                period = periodList.get(periodList.length() - 1);
                                periodList.removeElement(period);
                                callbacks.periodStreamCleared({ type: bufferType, period: period });
                            }
                            currentCanceller.cancel();
                            currentCanceller = new TaskCanceller();
                            currentCanceller.linkToSignal(orchestratorCancelSignal);
                            _i = 0, _a = __spreadArray(__spreadArray([], undecipherableRanges, true), rangesToRemove, true);
                            _c.label = 1;
                        case 1:
                            if (!(_i < _a.length)) return [3 /*break*/, 4];
                            _b = _a[_i], start = _b.start, end = _b.end;
                            if (!(start < end)) return [3 /*break*/, 3];
                            if (orchestratorCancelSignal.isCancelled()) {
                                return [2 /*return*/];
                            }
                            return [4 /*yield*/, segmentBuffer.removeBuffer(start, end, orchestratorCancelSignal)];
                        case 2:
                            _c.sent();
                            _c.label = 3;
                        case 3:
                            _i++;
                            return [3 /*break*/, 1];
                        case 4:
                            // Schedule micro task before checking the last playback observation
                            // to reduce the risk of race conditions where the next observation
                            // was going to be emitted synchronously.
                            nextTick(function () {
                                var _a;
                                if (orchestratorCancelSignal.isCancelled()) {
                                    return;
                                }
                                var observation = playbackObserver.getReference().getValue();
                                if (needsFlushingAfterClean(observation, undecipherableRanges)) {
                                    // Bind to Period start and end
                                    callbacks.needsDecipherabilityFlush();
                                    if (orchestratorCancelSignal.isCancelled()) {
                                        return;
                                    }
                                }
                                else if (needsFlushingAfterClean(observation, rangesToRemove)) {
                                    callbacks.needsBufferFlush();
                                    if (orchestratorCancelSignal.isCancelled()) {
                                        return;
                                    }
                                }
                                var lastPosition = (_a = observation.position.pending) !== null && _a !== void 0 ? _a : observation.position.last;
                                var newInitialPeriod = manifest.getPeriodForTime(lastPosition);
                                if (newInitialPeriod == null) {
                                    callbacks.error(new MediaError("MEDIA_TIME_NOT_FOUND", "The wanted position is not found in the Manifest."));
                                    return;
                                }
                                launchConsecutiveStreamsForPeriod(newInitialPeriod);
                            });
                            return [2 /*return*/];
                    }
                });
            });
        }
    }
    /**
     * Create lazily consecutive PeriodStreams:
     *
     * It first creates the `PeriodStream` for `basePeriod` and - once it becomes
     * full - automatically creates the next chronological one.
     * This process repeats until the `PeriodStream` linked to the last Period is
     * full.
     *
     * If an "old" `PeriodStream` becomes active again, it destroys all
     * `PeriodStream` coming after it (from the last chronological one to the
     * first).
     *
     * To clean-up PeriodStreams, each one of them are also automatically
     * destroyed once the current position is superior or equal to the end of
     * the concerned Period.
     *
     * The "periodStreamReady" callback is alled each times a new `PeriodStream`
     * is created.
     *
     * The "periodStreamCleared" callback is called each times a PeriodStream is
     * destroyed (this callback is though not called if it was destroyed due to
     * the given `cancelSignal` emitting or due to a fatal error).
     * @param {string} bufferType - e.g. "audio" or "video"
     * @param {Period} basePeriod - Initial Period downloaded.
     * @param {Object} consecutivePeriodStreamCb - Callbacks called on various
     * events. See type for more information.
     * @param {Object} cancelSignal - `CancellationSignal` allowing to stop
     * everything that this function was doing. Callbacks in
     * `consecutivePeriodStreamCb` might still be sent as a consequence of this
     * signal emitting.
     */
    function manageConsecutivePeriodStreams(bufferType, basePeriod, consecutivePeriodStreamCb, cancelSignal) {
        log.info("Stream: Creating new Stream for", bufferType, basePeriod.start);
        /**
         * Contains properties linnked to the next chronological `PeriodStream` that
         * may be created here.
         */
        var nextStreamInfo = null;
        /** Emits when the `PeriodStream` linked to `basePeriod` should be destroyed. */
        var currentStreamCanceller = new TaskCanceller();
        currentStreamCanceller.linkToSignal(cancelSignal);
        // Stop current PeriodStream when the current position goes over the end of
        // that Period.
        playbackObserver.listen(function (_a, stopListeningObservations) {
            var _b, _c;
            var position = _a.position;
            var wantedPosition = (_b = position.pending) !== null && _b !== void 0 ? _b : position.last;
            if (basePeriod.end !== undefined && wantedPosition >= basePeriod.end) {
                var nextPeriod = manifest.getPeriodAfter(basePeriod);
                // Handle special wantedPosition === basePeriod.end cases
                if (basePeriod.containsTime(wantedPosition, nextPeriod)) {
                    return;
                }
                log.info("Stream: Destroying PeriodStream as the current playhead moved above it", bufferType, basePeriod.start, (_c = position.pending) !== null && _c !== void 0 ? _c : position.last, basePeriod.end);
                stopListeningObservations();
                consecutivePeriodStreamCb.periodStreamCleared({ type: bufferType,
                    period: basePeriod });
                currentStreamCanceller.cancel();
            }
        }, { clearSignal: cancelSignal, includeLastObservation: true });
        var periodStreamArgs = { bufferType: bufferType, content: { manifest: manifest, period: basePeriod }, garbageCollectors: garbageCollectors, maxVideoBufferSize: maxVideoBufferSize, segmentFetcherCreator: segmentFetcherCreator, segmentBuffersStore: segmentBuffersStore, options: options, playbackObserver: playbackObserver, representationEstimator: representationEstimator, wantedBufferAhead: wantedBufferAhead };
        var periodStreamCallbacks = __assign(__assign({}, consecutivePeriodStreamCb), { streamStatusUpdate: function (value) {
                if (value.hasFinishedLoading) {
                    var nextPeriod = manifest.getPeriodAfter(basePeriod);
                    if (nextPeriod !== null) {
                        // current Stream is full, create the next one if not
                        checkOrCreateNextPeriodStream(nextPeriod);
                    }
                }
                else if (nextStreamInfo !== null) {
                    // current Stream is active, destroy next Stream if created
                    log.info("Stream: Destroying next PeriodStream due to current one being active", bufferType, nextStreamInfo.period.start);
                    consecutivePeriodStreamCb
                        .periodStreamCleared({ type: bufferType, period: nextStreamInfo.period });
                    nextStreamInfo.canceller.cancel();
                    nextStreamInfo = null;
                }
                consecutivePeriodStreamCb.streamStatusUpdate(value);
            }, error: function (err) {
                if (nextStreamInfo !== null) {
                    nextStreamInfo.canceller.cancel();
                    nextStreamInfo = null;
                }
                currentStreamCanceller.cancel();
                consecutivePeriodStreamCb.error(err);
            } });
        PeriodStream(periodStreamArgs, periodStreamCallbacks, currentStreamCanceller.signal);
        /**
         * Create `PeriodStream` for the next Period, specified under `nextPeriod`.
         * @param {Object} nextPeriod
         */
        function checkOrCreateNextPeriodStream(nextPeriod) {
            if (nextStreamInfo !== null) {
                if (nextStreamInfo.period.id === nextPeriod.id) {
                    return;
                }
                log.warn("Stream: Creating next `PeriodStream` while one was already created.", bufferType, nextPeriod.id, nextStreamInfo.period.id);
                consecutivePeriodStreamCb.periodStreamCleared({ type: bufferType,
                    period: nextStreamInfo.period });
                nextStreamInfo.canceller.cancel();
            }
            var nextStreamCanceller = new TaskCanceller();
            nextStreamCanceller.linkToSignal(cancelSignal);
            nextStreamInfo = { canceller: nextStreamCanceller,
                period: nextPeriod };
            manageConsecutivePeriodStreams(bufferType, nextPeriod, consecutivePeriodStreamCb, nextStreamInfo.canceller.signal);
        }
    }
}
/**
 * Returns `true` if low-level buffers have to be "flushed" after the given
 * `cleanedRanges` time ranges have been removed from an audio or video
 * SourceBuffer, to prevent playback issues.
 * @param {Object} observation
 * @param {Array.<Object>} cleanedRanges
 * @returns {boolean}
 */
function needsFlushingAfterClean(observation, cleanedRanges) {
    if (cleanedRanges.length === 0) {
        return false;
    }
    var curPos = observation.position.last;
    // Based on the playback direction, we just check whether we may encounter
    // the corresponding ranges, without seeking or re-switching playback
    // direction which is expected to lead to a low-level flush anyway.
    // There's a 5 seconds security, just to be sure.
    return observation.speed >= 0 ?
        cleanedRanges[cleanedRanges.length - 1].end >= curPos - 5 :
        cleanedRanges[0].start <= curPos + 5;
}
