/**
 * 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.
 */
import nextTick from "next-tick";
import config from "../../../config";
import { formatError } from "../../../errors";
import log from "../../../log";
import cancellableSleep from "../../../utils/cancellable_sleep";
import noop from "../../../utils/noop";
import objectAssign from "../../../utils/object_assign";
import SharedReference, { createMappedReference, } from "../../../utils/reference";
import TaskCanceller from "../../../utils/task_canceller";
import RepresentationStream from "../representation";
import createRepresentationEstimator from "./utils/create_representation_estimator";
/**
 * Create new `AdaptationStream` whose task will be to download the media data
 * for a given Adaptation (i.e. "track").
 *
 * It will rely on the IRepresentationEstimator to choose at any time the
 * best Representation for this Adaptation and then run the logic to download
 * and push the corresponding segments in the SegmentBuffer.
 *
 * @param {Object} args - Various arguments allowing the `AdaptationStream` to
 * determine which Representation to choose and which segments to load from it.
 * You can check the corresponding type for more information.
 * @param {Object} callbacks - The `AdaptationStream` 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 `AdaptationStream` is
 * doing.
 */
export default function AdaptationStream(_a, callbacks, parentCancelSignal) {
    var playbackObserver = _a.playbackObserver, content = _a.content, options = _a.options, representationEstimator = _a.representationEstimator, segmentBuffer = _a.segmentBuffer, segmentFetcherCreator = _a.segmentFetcherCreator, wantedBufferAhead = _a.wantedBufferAhead, maxVideoBufferSize = _a.maxVideoBufferSize;
    var directManualBitrateSwitching = options.manualBitrateSwitchingMode === "direct";
    var manifest = content.manifest, period = content.period, adaptation = content.adaptation;
    /** Allows to cancel everything the `AdaptationStream` is doing. */
    var adapStreamCanceller = new TaskCanceller();
    adapStreamCanceller.linkToSignal(parentCancelSignal);
    /**
     * The buffer goal ratio base itself on the value given by `wantedBufferAhead`
     * to determine a more dynamic buffer goal for a given Representation.
     *
     * It can help in cases such as : the current browser has issues with
     * buffering and tells us that we should try to bufferize less data :
     * https://developers.google.com/web/updates/2017/10/quotaexceedederror
     */
    var bufferGoalRatioMap = new Map();
    /**
     * Emit the currently chosen `Representation`.
     * `null` if no Representation is chosen for now.
     */
    var currentRepresentation = new SharedReference(null, adapStreamCanceller.signal);
    var _b = createRepresentationEstimator(content, representationEstimator, currentRepresentation, playbackObserver, function (err) {
        adapStreamCanceller.cancel();
        callbacks.error(err);
    }, adapStreamCanceller.signal), estimateRef = _b.estimateRef, abrCallbacks = _b.abrCallbacks;
    /** Allows the `RepresentationStream` to easily fetch media segments. */
    var segmentFetcher = segmentFetcherCreator
        .createSegmentFetcher(adaptation.type, 
    /* eslint-disable @typescript-eslint/unbound-method */
    { onRequestBegin: abrCallbacks.requestBegin,
        onRequestEnd: abrCallbacks.requestEnd,
        onProgress: abrCallbacks.requestProgress,
        onMetrics: abrCallbacks.metrics });
    /* eslint-enable @typescript-eslint/unbound-method */
    /** Stores the last emitted bitrate. */
    var previousBitrate;
    /** Emit at each bitrate estimate done by the IRepresentationEstimator. */
    estimateRef.onUpdate(function (_a) {
        var bitrate = _a.bitrate;
        if (bitrate === undefined) {
            return;
        }
        if (bitrate === previousBitrate) {
            return;
        }
        previousBitrate = bitrate;
        log.debug("Stream: new ".concat(adaptation.type, " bitrate estimate"), bitrate);
        callbacks.bitrateEstimationChange({ type: adaptation.type, bitrate: bitrate });
    }, { emitCurrentValue: true, clearSignal: adapStreamCanceller.signal });
    recursivelyCreateRepresentationStreams(true);
    /**
     * Create `RepresentationStream`s starting with the Representation of the last
     * estimate performed.
     * Each time a new estimate is made, this function will create a new
     * `RepresentationStream` corresponding to that new estimate.
     * @param {boolean} isFirstEstimate - Whether this is the first time we're
     * creating a `RepresentationStream` in the corresponding `AdaptationStream`.
     * This is important because manual quality switches might need a full reload
     * of the MediaSource _except_ if we are talking about the first quality chosen.
     */
    function recursivelyCreateRepresentationStreams(isFirstEstimate) {
        /**
         * `TaskCanceller` triggered when the current `RepresentationStream` is
         * terminating and as such the next one might be immediately created
         * recursively.
         */
        var repStreamTerminatingCanceller = new TaskCanceller();
        repStreamTerminatingCanceller.linkToSignal(adapStreamCanceller.signal);
        var _a = estimateRef.getValue(), representation = _a.representation, manual = _a.manual;
        if (representation === null) {
            return;
        }
        // A manual bitrate switch might need an immediate feedback.
        // To do that properly, we need to reload the MediaSource
        if (directManualBitrateSwitching && manual && !isFirstEstimate) {
            var DELTA_POSITION_AFTER_RELOAD_1 = config.getCurrent().DELTA_POSITION_AFTER_RELOAD;
            // We begin by scheduling a micro-task to reduce the possibility of race
            // conditions where the inner logic would be called synchronously before
            // the next observation (which may reflect very different playback conditions)
            // is actually received.
            return nextTick(function () {
                if (repStreamTerminatingCanceller.isUsed()) {
                    return;
                }
                var newManual = estimateRef.getValue().manual;
                if (!newManual) {
                    return;
                }
                var timeOffset = DELTA_POSITION_AFTER_RELOAD_1.bitrateSwitch;
                return callbacks.waitingMediaSourceReload({ bufferType: adaptation.type, period: period, timeOffset: timeOffset, stayInPeriod: true });
            });
        }
        /**
         * Emit when the current RepresentationStream should be terminated to make
         * place for a new one (e.g. when switching quality).
         */
        var terminateCurrentStream = new SharedReference(null, repStreamTerminatingCanceller.signal);
        /** Allows to stop listening to estimateRef on the following line. */
        estimateRef.onUpdate(function (estimate) {
            if (estimate.representation === null ||
                estimate.representation.id === representation.id) {
                return;
            }
            if (estimate.urgent) {
                log.info("Stream: urgent Representation switch", adaptation.type);
                return terminateCurrentStream.setValue({ urgent: true });
            }
            else {
                log.info("Stream: slow Representation switch", adaptation.type);
                return terminateCurrentStream.setValue({ urgent: false });
            }
        }, { clearSignal: repStreamTerminatingCanceller.signal, emitCurrentValue: true });
        /**
         * "Fast-switching" is a behavior allowing to replace low-quality segments
         * (i.e. with a low bitrate) with higher-quality segments (higher bitrate) in
         * the buffer.
         * This threshold defines a bitrate from which "fast-switching" is disabled.
         * For example with a fastSwitchThreshold set to `100`, segments with a
         * bitrate of `90` can be replaced. But segments with a bitrate of `100`
         * onward won't be replaced by higher quality segments.
         * Set to `undefined` to indicate that there's no threshold (anything can be
         * replaced by higher-quality segments).
         */
        var fastSwitchThreshold = new SharedReference(0);
        if (options.enableFastSwitching) {
            estimateRef.onUpdate(function (estimate) {
                fastSwitchThreshold.setValueIfChanged(estimate === null || estimate === void 0 ? void 0 : estimate.knownStableBitrate);
            }, { clearSignal: repStreamTerminatingCanceller.signal, emitCurrentValue: true });
        }
        var repInfo = { type: adaptation.type, period: period, representation: representation };
        currentRepresentation.setValue(representation);
        if (adapStreamCanceller.isUsed()) {
            return; // previous callback has stopped everything by side-effect
        }
        callbacks.representationChange(repInfo);
        if (adapStreamCanceller.isUsed()) {
            return; // previous callback has stopped everything by side-effect
        }
        var representationStreamCallbacks = {
            streamStatusUpdate: callbacks.streamStatusUpdate,
            encryptionDataEncountered: callbacks.encryptionDataEncountered,
            manifestMightBeOufOfSync: callbacks.manifestMightBeOufOfSync,
            needsManifestRefresh: callbacks.needsManifestRefresh,
            inbandEvent: callbacks.inbandEvent,
            warning: callbacks.warning,
            error: function (err) {
                adapStreamCanceller.cancel();
                callbacks.error(err);
            },
            addedSegment: function (segmentInfo) {
                abrCallbacks.addedSegment(segmentInfo);
                if (adapStreamCanceller.isUsed()) {
                    return;
                }
                callbacks.addedSegment(segmentInfo);
            },
            terminating: function () {
                if (repStreamTerminatingCanceller.isUsed()) {
                    return; // Already handled
                }
                repStreamTerminatingCanceller.cancel();
                return recursivelyCreateRepresentationStreams(false);
            },
        };
        createRepresentationStream(representation, terminateCurrentStream, fastSwitchThreshold, representationStreamCallbacks);
    }
    /**
     * Create and returns a new `RepresentationStream`, linked to the
     * given Representation.
     * @param {Object} representation
     * @param {Object} terminateCurrentStream
     * @param {Object} fastSwitchThreshold
     * @param {Object} representationStreamCallbacks
     */
    function createRepresentationStream(representation, terminateCurrentStream, fastSwitchThreshold, representationStreamCallbacks) {
        /**
         * `TaskCanceller` triggered when the `RepresentationStream` calls its
         * `terminating` callback.
         */
        var terminatingRepStreamCanceller = new TaskCanceller();
        terminatingRepStreamCanceller.linkToSignal(adapStreamCanceller.signal);
        var bufferGoal = createMappedReference(wantedBufferAhead, function (prev) {
            return prev * getBufferGoalRatio(representation);
        }, terminatingRepStreamCanceller.signal);
        var maxBufferSize = adaptation.type === "video" ?
            maxVideoBufferSize :
            new SharedReference(Infinity);
        log.info("Stream: changing representation", adaptation.type, representation.id, representation.bitrate);
        var updatedCallbacks = objectAssign({}, representationStreamCallbacks, {
            error: function (err) {
                var _a;
                var formattedError = formatError(err, {
                    defaultCode: "NONE",
                    defaultReason: "Unknown `RepresentationStream` error",
                });
                if (formattedError.code !== "BUFFER_FULL_ERROR") {
                    representationStreamCallbacks.error(err);
                }
                else {
                    var wba = wantedBufferAhead.getValue();
                    var lastBufferGoalRatio = (_a = bufferGoalRatioMap.get(representation.id)) !== null && _a !== void 0 ? _a : 1;
                    // 70%, 49%, 34.3%, 24%, 16.81%, 11.76%, 8.24% and 5.76%
                    var newBufferGoalRatio = lastBufferGoalRatio * 0.7;
                    if (newBufferGoalRatio <= 0.05 || wba * newBufferGoalRatio <= 2) {
                        throw formattedError;
                    }
                    bufferGoalRatioMap.set(representation.id, newBufferGoalRatio);
                    // We wait 4 seconds to let the situation evolve by itself before
                    // retrying loading segments with a lower buffer goal
                    cancellableSleep(4000, adapStreamCanceller.signal).then(function () {
                        return createRepresentationStream(representation, terminateCurrentStream, fastSwitchThreshold, representationStreamCallbacks);
                    }).catch(noop);
                }
            },
            terminating: function () {
                terminatingRepStreamCanceller.cancel();
                representationStreamCallbacks.terminating();
            },
        });
        RepresentationStream({ playbackObserver: playbackObserver, content: { representation: representation, adaptation: adaptation, period: period, manifest: manifest }, segmentBuffer: segmentBuffer, segmentFetcher: segmentFetcher, terminate: terminateCurrentStream,
            options: { bufferGoal: bufferGoal, maxBufferSize: maxBufferSize, drmSystemId: options.drmSystemId, fastSwitchThreshold: fastSwitchThreshold } }, updatedCallbacks, adapStreamCanceller.signal);
    }
    /**
     * @param {Object} representation
     * @returns {number}
     */
    function getBufferGoalRatio(representation) {
        var oldBufferGoalRatio = bufferGoalRatioMap.get(representation.id);
        var bufferGoalRatio = oldBufferGoalRatio !== undefined ? oldBufferGoalRatio :
            1;
        if (oldBufferGoalRatio === undefined) {
            bufferGoalRatioMap.set(representation.id, bufferGoalRatio);
        }
        return bufferGoalRatio;
    }
}
