/*
 * Copyright (C) 2016 Canon Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

// @conditional=ENABLE(STREAMS_API)
// @internal

function privateInitializeReadableByteStreamController(stream, underlyingByteSource, highWaterMark)
{
    "use strict";

    if (!@isReadableStream(stream))
        @throwTypeError("ReadableByteStreamController needs a ReadableStream");

    // readableStreamController is initialized with null value.
    if (@getByIdDirectPrivate(stream, "readableStreamController") !== null)
        @throwTypeError("ReadableStream already has a controller");

    @putByIdDirectPrivate(this, "controlledReadableStream", stream);
    @putByIdDirectPrivate(this, "underlyingByteSource", underlyingByteSource);
    @putByIdDirectPrivate(this, "pullAgain", false);
    @putByIdDirectPrivate(this, "pulling", false);
    @readableByteStreamControllerClearPendingPullIntos(this);
    @putByIdDirectPrivate(this, "queue", @newQueue());
    @putByIdDirectPrivate(this, "started", false);
    @putByIdDirectPrivate(this, "closeRequested", false);

    let hwm = @toNumber(highWaterMark);
    if (@isNaN(hwm) || hwm < 0)
        @throwRangeError("highWaterMark value is negative or not a number");
    @putByIdDirectPrivate(this, "strategyHWM", hwm);

    let autoAllocateChunkSize = underlyingByteSource.autoAllocateChunkSize;
    if (autoAllocateChunkSize !== @undefined) {
        autoAllocateChunkSize = @toNumber(autoAllocateChunkSize);
        if (autoAllocateChunkSize <= 0 || autoAllocateChunkSize === @Infinity || autoAllocateChunkSize === -@Infinity)
            @throwRangeError("autoAllocateChunkSize value is negative or equal to positive or negative infinity");
    }
    @putByIdDirectPrivate(this, "autoAllocateChunkSize", autoAllocateChunkSize);
    @putByIdDirectPrivate(this, "pendingPullIntos", []);

    const controller = this;
    const startResult = @promiseInvokeOrNoopNoCatch(underlyingByteSource, "start", [this]).@then(() => {
        @putByIdDirectPrivate(controller, "started", true);
        @assert(!@getByIdDirectPrivate(controller, "pulling"));
        @assert(!@getByIdDirectPrivate(controller, "pullAgain"));
        @readableByteStreamControllerCallPullIfNeeded(controller);
    }, (error) => {
        if (@getByIdDirectPrivate(stream, "state") === @streamReadable)
            @readableByteStreamControllerError(controller, error);
    });

    @putByIdDirectPrivate(this, "cancel", @readableByteStreamControllerCancel);
    @putByIdDirectPrivate(this, "pull", @readableByteStreamControllerPull);

    return this;
}

function privateInitializeReadableStreamBYOBRequest(controller, view)
{
    "use strict";

    @putByIdDirectPrivate(this, "associatedReadableByteStreamController", controller);
    @putByIdDirectPrivate(this, "view", view);
}

function isReadableByteStreamController(controller)
{
    "use strict";

    // Same test mechanism as in isReadableStreamDefaultController (ReadableStreamInternals.js).
    // See corresponding function for explanations.
    return @isObject(controller) && !!@getByIdDirectPrivate(controller, "underlyingByteSource");
}

function isReadableStreamBYOBRequest(byobRequest)
{
    "use strict";

    // Same test mechanism as in isReadableStreamDefaultController (ReadableStreamInternals.js).
    // See corresponding function for explanations.
    return @isObject(byobRequest) && !!@getByIdDirectPrivate(byobRequest, "associatedReadableByteStreamController");
}

function isReadableStreamBYOBReader(reader)
{
    "use strict";

    // Spec tells to return true only if reader has a readIntoRequests internal slot.
    // However, since it is a private slot, it cannot be checked using hasOwnProperty().
    // Since readIntoRequests is initialized with an empty array, the following test is ok.
    return @isObject(reader) && !!@getByIdDirectPrivate(reader, "readIntoRequests");
}

function readableByteStreamControllerCancel(controller, reason)
{
    "use strict";

    var pendingPullIntos = @getByIdDirectPrivate(controller, "pendingPullIntos");
    if (pendingPullIntos.length > 0)
        pendingPullIntos[0].bytesFilled = 0;
    @putByIdDirectPrivate(controller, "queue", @newQueue());
    return @promiseInvokeOrNoop(@getByIdDirectPrivate(controller, "underlyingByteSource"), "cancel", [reason]);
}

function readableByteStreamControllerError(controller, e)
{
    "use strict";

    @assert(@getByIdDirectPrivate(@getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === @streamReadable);
    @readableByteStreamControllerClearPendingPullIntos(controller);
    @putByIdDirectPrivate(controller, "queue", @newQueue());
    @readableStreamError(@getByIdDirectPrivate(controller, "controlledReadableStream"), e);
}

function readableByteStreamControllerClose(controller)
{
    "use strict";

    @assert(!@getByIdDirectPrivate(controller, "closeRequested"));
    @assert(@getByIdDirectPrivate(@getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === @streamReadable);

    if (@getByIdDirectPrivate(controller, "queue").size > 0) {
        @putByIdDirectPrivate(controller, "closeRequested", true);
        return;
    }

    var pendingPullIntos = @getByIdDirectPrivate(controller, "pendingPullIntos");
    if (pendingPullIntos.length > 0) {
        if (pendingPullIntos[0].bytesFilled > 0) {
            const e = @makeTypeError("Close requested while there remain pending bytes");
            @readableByteStreamControllerError(controller, e);
            throw e;
        }
    }

    @readableStreamClose(@getByIdDirectPrivate(controller, "controlledReadableStream"));
}

function readableByteStreamControllerClearPendingPullIntos(controller)
{
    "use strict";

    @readableByteStreamControllerInvalidateBYOBRequest(controller);
    @putByIdDirectPrivate(controller, "pendingPullIntos", []);
}

function readableByteStreamControllerGetDesiredSize(controller)
{
    "use strict";

    const stream = @getByIdDirectPrivate(controller, "controlledReadableStream");
    const state = @getByIdDirectPrivate(stream, "state");

    if (state === @streamErrored)
        return null;
    if (state === @streamClosed)
        return 0;

    return @getByIdDirectPrivate(controller, "strategyHWM") - @getByIdDirectPrivate(controller, "queue").size;
}

function readableStreamHasBYOBReader(stream)
{
    "use strict";

    const reader = @getByIdDirectPrivate(stream, "reader");
    return reader !== @undefined && @isReadableStreamBYOBReader(reader);
}

function readableStreamHasDefaultReader(stream)
{
    "use strict";

    const reader = @getByIdDirectPrivate(stream, "reader");
    return reader !== @undefined && @isReadableStreamDefaultReader(reader);
}

function readableByteStreamControllerHandleQueueDrain(controller) {

    "use strict";

    @assert(@getByIdDirectPrivate(@getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === @streamReadable);
    if (!@getByIdDirectPrivate(controller, "queue").size && @getByIdDirectPrivate(controller, "closeRequested"))
        @readableStreamClose(@getByIdDirectPrivate(controller, "controlledReadableStream"));
    else
        @readableByteStreamControllerCallPullIfNeeded(controller);
}

function readableByteStreamControllerPull(controller)
{
    "use strict";

    const stream = @getByIdDirectPrivate(controller, "controlledReadableStream");
    @assert(@readableStreamHasDefaultReader(stream));

    if (@getByIdDirectPrivate(controller, "queue").size > 0) {
        @assert(@getByIdDirectPrivate(@getByIdDirectPrivate(stream, "reader"), "readRequests").length === 0);
        const entry = @getByIdDirectPrivate(controller, "queue").content.@shift();
        @getByIdDirectPrivate(controller, "queue").size -= entry.byteLength;
        @readableByteStreamControllerHandleQueueDrain(controller);
        let view;
        try {
            view = new @Uint8Array(entry.buffer, entry.byteOffset, entry.byteLength);
        } catch (error) {
            return @Promise.@reject(error);
        }
        return @Promise.@resolve({value: view, done: false});
    }

    if (@getByIdDirectPrivate(controller, "autoAllocateChunkSize") !== @undefined) {
        let buffer;
        try {
            buffer = new @ArrayBuffer(@getByIdDirectPrivate(controller, "autoAllocateChunkSize"));
        } catch (error) {
            return @Promise.@reject(error);
        }
        const pullIntoDescriptor = {
            buffer,
            byteOffset: 0,
            byteLength: @getByIdDirectPrivate(controller, "autoAllocateChunkSize"),
            bytesFilled: 0,
            elementSize: 1,
            ctor: @Uint8Array,
            readerType: 'default'
        };
        @getByIdDirectPrivate(controller, "pendingPullIntos").@push(pullIntoDescriptor);
    }

    const promise = @readableStreamAddReadRequest(stream);
    @readableByteStreamControllerCallPullIfNeeded(controller);
    return promise;
}

function readableByteStreamControllerShouldCallPull(controller)
{
    "use strict";

    const stream = @getByIdDirectPrivate(controller, "controlledReadableStream");

    if (@getByIdDirectPrivate(stream, "state") !== @streamReadable)
        return false;
    if (@getByIdDirectPrivate(controller, "closeRequested"))
        return false;
    if (!@getByIdDirectPrivate(controller, "started"))
        return false;
    if (@readableStreamHasDefaultReader(stream) && @getByIdDirectPrivate(@getByIdDirectPrivate(stream, "reader"), "readRequests").length > 0)
        return true;
    if (@readableStreamHasBYOBReader(stream) && @getByIdDirectPrivate(@getByIdDirectPrivate(stream, "reader"), "readIntoRequests").length > 0)
        return true;
    if (@readableByteStreamControllerGetDesiredSize(controller) > 0)
        return true;
    return false;
}

function readableByteStreamControllerCallPullIfNeeded(controller)
{
    "use strict";

    if (!@readableByteStreamControllerShouldCallPull(controller))
        return;

    if (@getByIdDirectPrivate(controller, "pulling")) {
        @putByIdDirectPrivate(controller, "pullAgain", true);
        return;
    }

    @assert(!@getByIdDirectPrivate(controller, "pullAgain"));
    @putByIdDirectPrivate(controller, "pulling", true);
    @promiseInvokeOrNoop(@getByIdDirectPrivate(controller, "underlyingByteSource"), "pull", [controller]).@then(() => {
        @putByIdDirectPrivate(controller, "pulling", false);
        if (@getByIdDirectPrivate(controller, "pullAgain")) {
            @putByIdDirectPrivate(controller, "pullAgain", false);
            @readableByteStreamControllerCallPullIfNeeded(controller);
        }
    }, (error) => {
        if (@getByIdDirectPrivate(@getByIdDirectPrivate(controller, "controlledReadableStream"), "state") === @streamReadable)
            @readableByteStreamControllerError(controller, error);
    });
}

function transferBufferToCurrentRealm(buffer)
{
    "use strict";

    // FIXME: Determine what should be done here exactly (what is already existing in current
    // codebase and what has to be added). According to spec, Transfer operation should be
    // performed in order to transfer buffer to current realm. For the moment, simply return
    // received buffer.
    return buffer;
}

function readableByteStreamControllerEnqueue(controller, chunk)
{
    "use strict";

    const stream = @getByIdDirectPrivate(controller, "controlledReadableStream");
    @assert(!@getByIdDirectPrivate(controller, "closeRequested"));
    @assert(@getByIdDirectPrivate(stream, "state") === @streamReadable);
    const buffer = chunk.buffer;
    const byteOffset = chunk.byteOffset;
    const byteLength = chunk.byteLength;
    const transferredBuffer = @transferBufferToCurrentRealm(buffer);

    if (@readableStreamHasDefaultReader(stream)) {
        if (!@getByIdDirectPrivate(@getByIdDirectPrivate(stream, "reader"), "readRequests").length)
            @readableByteStreamControllerEnqueueChunk(controller, transferredBuffer, byteOffset, byteLength);
        else {
            @assert(!@getByIdDirectPrivate(controller, "queue").content.length);
            let transferredView = new @Uint8Array(transferredBuffer, byteOffset, byteLength);
            @readableStreamFulfillReadRequest(stream, transferredView, false);
        }
        return;
    }

    if (@readableStreamHasBYOBReader(stream)) {
        @readableByteStreamControllerEnqueueChunk(controller, transferredBuffer, byteOffset, byteLength);
        @readableByteStreamControllerProcessPullDescriptors(controller);
        return;
    }

    @assert(!@isReadableStreamLocked(stream));
    @readableByteStreamControllerEnqueueChunk(controller, transferredBuffer, byteOffset, byteLength);
}

// Spec name: readableByteStreamControllerEnqueueChunkToQueue.
function readableByteStreamControllerEnqueueChunk(controller, buffer, byteOffset, byteLength)
{
    "use strict";

    @getByIdDirectPrivate(controller, "queue").content.@push({
        buffer: buffer,
        byteOffset: byteOffset,
        byteLength: byteLength
    });
    @getByIdDirectPrivate(controller, "queue").size += byteLength;
}

function readableByteStreamControllerRespondWithNewView(controller, view)
{
    "use strict";

    @assert(@getByIdDirectPrivate(controller, "pendingPullIntos").length > 0);

    let firstDescriptor = @getByIdDirectPrivate(controller, "pendingPullIntos")[0];

    if (firstDescriptor.byteOffset + firstDescriptor.bytesFilled !== view.byteOffset)
        @throwRangeError("Invalid value for view.byteOffset");

    if (firstDescriptor.byteLength !== view.byteLength)
        @throwRangeError("Invalid value for view.byteLength");

    firstDescriptor.buffer = view.buffer;
    @readableByteStreamControllerRespondInternal(controller, view.byteLength);
}

function readableByteStreamControllerRespond(controller, bytesWritten)
{
    "use strict";

    bytesWritten = @toNumber(bytesWritten);

    if (@isNaN(bytesWritten) || bytesWritten === @Infinity || bytesWritten < 0 )
        @throwRangeError("bytesWritten has an incorrect value");

    @assert(@getByIdDirectPrivate(controller, "pendingPullIntos").length > 0);

    @readableByteStreamControllerRespondInternal(controller, bytesWritten);
}

function readableByteStreamControllerRespondInternal(controller, bytesWritten)
{
    "use strict";

    let firstDescriptor = @getByIdDirectPrivate(controller, "pendingPullIntos")[0];
    let stream = @getByIdDirectPrivate(controller, "controlledReadableStream");

    if (@getByIdDirectPrivate(stream, "state") === @streamClosed) {
        if (bytesWritten !== 0)
            @throwTypeError("bytesWritten is different from 0 even though stream is closed");
        @readableByteStreamControllerRespondInClosedState(controller, firstDescriptor);
    } else {
        @assert(@getByIdDirectPrivate(stream, "state") === @streamReadable);
        @readableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor);
    }
}

function readableByteStreamControllerRespondInReadableState(controller, bytesWritten, pullIntoDescriptor)
{
    "use strict";

    if (pullIntoDescriptor.bytesFilled + bytesWritten > pullIntoDescriptor.byteLength)
        @throwRangeError("bytesWritten value is too great");

    @assert(@getByIdDirectPrivate(controller, "pendingPullIntos").length === 0 || @getByIdDirectPrivate(controller, "pendingPullIntos")[0] === pullIntoDescriptor);
    @readableByteStreamControllerInvalidateBYOBRequest(controller);
    pullIntoDescriptor.bytesFilled += bytesWritten;

    if (pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize)
        return;

    @readableByteStreamControllerShiftPendingDescriptor(controller);
    const remainderSize = pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize;

    if (remainderSize > 0) {
        const end = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled;
        const remainder = @cloneArrayBuffer(pullIntoDescriptor.buffer, end - remainderSize, remainderSize);
        @readableByteStreamControllerEnqueueChunk(controller, remainder, 0, remainder.byteLength);
    }

    pullIntoDescriptor.buffer = @transferBufferToCurrentRealm(pullIntoDescriptor.buffer);
    pullIntoDescriptor.bytesFilled -= remainderSize;
    @readableByteStreamControllerCommitDescriptor(@getByIdDirectPrivate(controller, "controlledReadableStream"), pullIntoDescriptor);
    @readableByteStreamControllerProcessPullDescriptors(controller);
}

function readableByteStreamControllerRespondInClosedState(controller, firstDescriptor)
{
    "use strict";

    firstDescriptor.buffer = @transferBufferToCurrentRealm(firstDescriptor.buffer);
    @assert(firstDescriptor.bytesFilled === 0);

    if (@readableStreamHasBYOBReader(@getByIdDirectPrivate(controller, "controlledReadableStream"))) {
        while (@getByIdDirectPrivate(@getByIdDirectPrivate(@getByIdDirectPrivate(controller, "controlledReadableStream"), "reader"), "readIntoRequests").length > 0) {
            let pullIntoDescriptor = @readableByteStreamControllerShiftPendingDescriptor(controller);
            @readableByteStreamControllerCommitDescriptor(@getByIdDirectPrivate(controller, "controlledReadableStream"), pullIntoDescriptor);
        }
    }
}

// Spec name: readableByteStreamControllerProcessPullIntoDescriptorsUsingQueue (shortened for readability).
function readableByteStreamControllerProcessPullDescriptors(controller)
{
    "use strict";

    @assert(!@getByIdDirectPrivate(controller, "closeRequested"));
    while (@getByIdDirectPrivate(controller, "pendingPullIntos").length > 0) {
        if (@getByIdDirectPrivate(controller, "queue").size === 0)
            return;
        let pullIntoDescriptor = @getByIdDirectPrivate(controller, "pendingPullIntos")[0];
        if (@readableByteStreamControllerFillDescriptorFromQueue(controller, pullIntoDescriptor)) {
            @readableByteStreamControllerShiftPendingDescriptor(controller);
            @readableByteStreamControllerCommitDescriptor(@getByIdDirectPrivate(controller, "controlledReadableStream"), pullIntoDescriptor);
        }
    }
}

// Spec name: readableByteStreamControllerFillPullIntoDescriptorFromQueue (shortened for readability).
function readableByteStreamControllerFillDescriptorFromQueue(controller, pullIntoDescriptor)
{
    "use strict";

    const currentAlignedBytes = pullIntoDescriptor.bytesFilled - (pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize);
    const maxBytesToCopy = @getByIdDirectPrivate(controller, "queue").size < pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled ?
                @getByIdDirectPrivate(controller, "queue").size : pullIntoDescriptor.byteLength - pullIntoDescriptor.bytesFilled;
    const maxBytesFilled = pullIntoDescriptor.bytesFilled + maxBytesToCopy;
    const maxAlignedBytes = maxBytesFilled - (maxBytesFilled % pullIntoDescriptor.elementSize);
    let totalBytesToCopyRemaining = maxBytesToCopy;
    let ready = false;

    if (maxAlignedBytes > currentAlignedBytes) {
        totalBytesToCopyRemaining = maxAlignedBytes - pullIntoDescriptor.bytesFilled;
        ready = true;
    }

    while (totalBytesToCopyRemaining > 0) {
        let headOfQueue = @getByIdDirectPrivate(controller, "queue").content[0];
        const bytesToCopy = totalBytesToCopyRemaining < headOfQueue.byteLength ? totalBytesToCopyRemaining : headOfQueue.byteLength;
        // Copy appropriate part of pullIntoDescriptor.buffer to headOfQueue.buffer.
        // Remark: this implementation is not completely aligned on the definition of CopyDataBlockBytes
        // operation of ECMAScript (the case of Shared Data Block is not considered here, but it doesn't seem to be an issue).
        const destStart = pullIntoDescriptor.byteOffset + pullIntoDescriptor.bytesFilled;
        // FIXME: As indicated in comments of bug 172717, access to set is not safe. However, using prototype.@set.@call does
        // not work (@set is undefined). A safe way to do that is needed.
        new @Uint8Array(pullIntoDescriptor.buffer).set(new @Uint8Array(headOfQueue.buffer, headOfQueue.byteOffset, bytesToCopy), destStart);

        if (headOfQueue.byteLength === bytesToCopy)
            @getByIdDirectPrivate(controller, "queue").content.@shift();
        else {
            headOfQueue.byteOffset += bytesToCopy;
            headOfQueue.byteLength -= bytesToCopy;
        }

        @getByIdDirectPrivate(controller, "queue").size -= bytesToCopy;
        @assert(@getByIdDirectPrivate(controller, "pendingPullIntos").length === 0 || @getByIdDirectPrivate(controller, "pendingPullIntos")[0] === pullIntoDescriptor);
        @readableByteStreamControllerInvalidateBYOBRequest(controller);
        pullIntoDescriptor.bytesFilled += bytesToCopy;
        totalBytesToCopyRemaining -= bytesToCopy;
    }

    if (!ready) {
        @assert(@getByIdDirectPrivate(controller, "queue").size === 0);
        @assert(pullIntoDescriptor.bytesFilled > 0);
        @assert(pullIntoDescriptor.bytesFilled < pullIntoDescriptor.elementSize);
    }

    return ready;
}

// Spec name: readableByteStreamControllerShiftPendingPullInto (renamed for consistency).
function readableByteStreamControllerShiftPendingDescriptor(controller)
{
    "use strict";

    let descriptor = @getByIdDirectPrivate(controller, "pendingPullIntos").@shift();
    @readableByteStreamControllerInvalidateBYOBRequest(controller);
    return descriptor;
}

function readableByteStreamControllerInvalidateBYOBRequest(controller)
{
    "use strict";

    if (@getByIdDirectPrivate(controller, "byobRequest") === @undefined)
        return;
    const byobRequest = @getByIdDirectPrivate(controller, "byobRequest");
    @putByIdDirectPrivate(byobRequest, "associatedReadableByteStreamController", @undefined);
    @putByIdDirectPrivate(byobRequest, "view", @undefined);
    @putByIdDirectPrivate(controller, "byobRequest", @undefined);
}

// Spec name: readableByteStreamControllerCommitPullIntoDescriptor (shortened for readability).
function readableByteStreamControllerCommitDescriptor(stream, pullIntoDescriptor)
{
    "use strict";

    @assert(@getByIdDirectPrivate(stream, "state") !== @streamErrored);
    let done = false;
    if (@getByIdDirectPrivate(stream, "state") === @streamClosed) {
        @assert(!pullIntoDescriptor.bytesFilled);
        done = true;
    }
    let filledView = @readableByteStreamControllerConvertDescriptor(pullIntoDescriptor);
    if (pullIntoDescriptor.readerType === "default")
        @readableStreamFulfillReadRequest(stream, filledView, done);
    else {
        @assert(pullIntoDescriptor.readerType === "byob");
        @readableStreamFulfillReadIntoRequest(stream, filledView, done);
    }
}

// Spec name: readableByteStreamControllerConvertPullIntoDescriptor (shortened for readability).
function readableByteStreamControllerConvertDescriptor(pullIntoDescriptor)
{
    "use strict";

    @assert(pullIntoDescriptor.bytesFilled <= pullIntoDescriptor.byteLength);
    @assert(pullIntoDescriptor.bytesFilled % pullIntoDescriptor.elementSize === 0);

    return new pullIntoDescriptor.ctor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, pullIntoDescriptor.bytesFilled / pullIntoDescriptor.elementSize);
}

function readableStreamFulfillReadIntoRequest(stream, chunk, done)
{
    "use strict";

    @getByIdDirectPrivate(@getByIdDirectPrivate(stream, "reader"), "readIntoRequests").@shift().@resolve.@call(@undefined, {value: chunk, done: done});
}

function readableStreamBYOBReaderRead(reader, view)
{
    "use strict";

    const stream = @getByIdDirectPrivate(reader, "ownerReadableStream");
    @assert(!!stream);

    @putByIdDirectPrivate(stream, "disturbed", true);
    if (@getByIdDirectPrivate(stream, "state") === @streamErrored)
        return @Promise.@reject(@getByIdDirectPrivate(stream, "storedError"));

    return @readableByteStreamControllerPullInto(@getByIdDirectPrivate(stream, "readableStreamController"), view);
}

function readableByteStreamControllerPullInto(controller, view)
{
    "use strict";

    const stream = @getByIdDirectPrivate(controller, "controlledReadableStream");
    let elementSize = 1;
    // Spec describes that in the case where view is a TypedArray, elementSize
    // should be set to the size of an element (e.g. 2 for UInt16Array). For
    // DataView, BYTES_PER_ELEMENT is undefined, contrary to the same property
    // for TypedArrays.
    // FIXME: Getting BYTES_PER_ELEMENT like this is not safe (property is read-only
    // but can be modified if the prototype is redefined). A safe way of getting
    // it would be to determine which type of ArrayBufferView view is an instance
    // of based on typed arrays private variables. However, this is not possible due
    // to bug 167697, which prevents access to typed arrays through their private
    // names unless public name has already been met before.
    if (view.BYTES_PER_ELEMENT !== @undefined)
        elementSize = view.BYTES_PER_ELEMENT;

    // FIXME: Getting constructor like this is not safe. A safe way of getting
    // it would be to determine which type of ArrayBufferView view is an instance
    // of, and to assign appropriate constructor based on this (e.g. ctor =
    // @Uint8Array). However, this is not possible due to bug 167697, which
    // prevents access to typed arrays through their private names unless public
    // name has already been met before.
    const ctor = view.constructor;

    const pullIntoDescriptor = {
        buffer: view.buffer,
        byteOffset: view.byteOffset,
        byteLength: view.byteLength,
        bytesFilled: 0,
        elementSize,
        ctor,
        readerType: 'byob'
    };

    if (@getByIdDirectPrivate(controller, "pendingPullIntos").length) {
        pullIntoDescriptor.buffer = @transferBufferToCurrentRealm(pullIntoDescriptor.buffer);
        @getByIdDirectPrivate(controller, "pendingPullIntos").@push(pullIntoDescriptor);
        return @readableStreamAddReadIntoRequest(stream);
    }

    if (@getByIdDirectPrivate(stream, "state") === @streamClosed) {
        const emptyView = new ctor(pullIntoDescriptor.buffer, pullIntoDescriptor.byteOffset, 0);
        return @Promise.@resolve({ value: emptyView, done: true });
    }

    if (@getByIdDirectPrivate(controller, "queue").size > 0) {
        if (@readableByteStreamControllerFillDescriptorFromQueue(controller, pullIntoDescriptor)) {
            const filledView = @readableByteStreamControllerConvertDescriptor(pullIntoDescriptor);
            @readableByteStreamControllerHandleQueueDrain(controller);
            return @Promise.@resolve({ value: filledView, done: false });
        }
        if (@getByIdDirectPrivate(controller, "closeRequested")) {
            const e = @makeTypeError("Closing stream has been requested");
            @readableByteStreamControllerError(controller, e);
            return @Promise.@reject(e);
        }
    }

    pullIntoDescriptor.buffer = @transferBufferToCurrentRealm(pullIntoDescriptor.buffer);
    @getByIdDirectPrivate(controller, "pendingPullIntos").@push(pullIntoDescriptor);
    const promise = @readableStreamAddReadIntoRequest(stream);
    @readableByteStreamControllerCallPullIfNeeded(controller);
    return promise;
}

function readableStreamAddReadIntoRequest(stream)
{
    "use strict";

    @assert(@isReadableStreamBYOBReader(@getByIdDirectPrivate(stream, "reader")));
    @assert(@getByIdDirectPrivate(stream, "state") === @streamReadable || @getByIdDirectPrivate(stream, "state") === @streamClosed);

    const readRequest = @newPromiseCapability(@Promise);
    @getByIdDirectPrivate(@getByIdDirectPrivate(stream, "reader"), "readIntoRequests").@push(readRequest);

    return readRequest.@promise;
}
