/*
 * Copyright (C) 2016 Apple 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. AND ITS CONTRIBUTORS ``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 ITS 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.
 */

#include "config.h"
#include "TypedArrayCTest.h"

#include "JavaScript.h"
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <wtf/Assertions.h>

extern "C" void JSSynchronousGarbageCollectForDebugging(JSContextRef);

static void id(void*, void*) { }
static void freePtr(void* ptr, void*)
{
    free(ptr);
}

static const unsigned numLengths = 3;

static const unsigned lengths[numLengths] =
{
    0,
    1,
    10,
};

static const unsigned byteSizes[kJSTypedArrayTypeArrayBuffer] =
{
    1, // kJSTypedArrayTypeInt8Array
    2, // kJSTypedArrayTypeInt16Array
    4, // kJSTypedArrayTypeInt32Array
    1, // kJSTypedArrayTypeUint8Array
    1, // kJSTypedArrayTypeUint8ClampedArray
    2, // kJSTypedArrayTypeUint16Array
    4, // kJSTypedArrayTypeUint32Array
    4, // kJSTypedArrayTypeFloat32Array
    8, // kJSTypedArrayTypeFloat64Array
};

static const char* typeToString[kJSTypedArrayTypeArrayBuffer] =
{
    "kJSTypedArrayTypeInt8Array",
    "kJSTypedArrayTypeInt16Array",
    "kJSTypedArrayTypeInt32Array",
    "kJSTypedArrayTypeUint8Array",
    "kJSTypedArrayTypeUint8ClampedArray",
    "kJSTypedArrayTypeUint16Array",
    "kJSTypedArrayTypeUint32Array",
    "kJSTypedArrayTypeFloat32Array",
    "kJSTypedArrayTypeFloat64Array",
};

inline int unexpectedException(const char* name)
{
    fprintf(stderr, "%s FAILED: unexpected exception\n", name);
    return 1;
}

static int assertEqualsAsNumber(JSGlobalContextRef context, JSValueRef value, double expectedValue)
{
    double number = JSValueToNumber(context, value, nullptr);
    if (number != expectedValue && !(isnan(number) && isnan(expectedValue))) {
        fprintf(stderr, "assertEqualsAsNumber FAILED: %p, %lf\n", value, expectedValue);
        return 1;
    }
    return 0;
}

static int testAccess(JSGlobalContextRef context, JSObjectRef typedArray, JSTypedArrayType type, unsigned elementLength, void* expectedPtr = nullptr, JSObjectRef expectedBuffer = nullptr, unsigned expectedOffset = 0)
{
    JSValueRef exception = nullptr;
    // Test typedArray basic functions.
    JSTypedArrayType actualType = JSValueGetTypedArrayType(context, typedArray, &exception);
    if (type != actualType || exception) {
        fprintf(stderr, "TypedArray type FAILED: %p, got: %s, expected: %s\n", typedArray, typeToString[actualType], typeToString[type]);
        return 1;
    }

    unsigned length = JSObjectGetTypedArrayLength(context, typedArray, &exception);
    if (elementLength != length || exception) {
        fprintf(stderr, "TypedArray length FAILED: %p (%s), got: %d, expected: %d\n", typedArray, typeToString[type], length, elementLength);
        return 1;
    }

    unsigned byteLength = JSObjectGetTypedArrayByteLength(context, typedArray, &exception);
    unsigned expectedLength = byteSizes[type] * elementLength;
    if (byteLength != expectedLength || exception) {
        fprintf(stderr, "TypedArray byteLength FAILED: %p (%s), got: %d, expected: %d\n", typedArray, typeToString[type], byteLength, expectedLength);
        return 1;
    }

    unsigned offset = JSObjectGetTypedArrayByteOffset(context, typedArray, &exception);
    if (expectedOffset != offset || exception) {
        fprintf(stderr, "TypedArray byteOffset FAILED: %p (%s), got: %d, expected: %d\n", typedArray, typeToString[type], offset, expectedOffset);
        return 1;
    }

    void* ptr = JSObjectGetTypedArrayBytesPtr(context, typedArray, &exception);
    if (exception)
        return unexpectedException("TypedArray get bytes ptr");

    JSObjectRef buffer = JSObjectGetTypedArrayBuffer(context, typedArray, &exception);
    if (exception)
        return unexpectedException("TypedArray get buffer");

    void* bufferPtr = JSObjectGetArrayBufferBytesPtr(context, buffer, &exception);
    if (exception)
        return unexpectedException("ArrayBuffer get bytes ptr");

    if (bufferPtr != ptr) {
        fprintf(stderr, "FAIL: TypedArray bytes ptr and ArrayBuffer byte ptr were not the same: %p (%s) TypedArray: %p, ArrayBuffer: %p\n", typedArray, typeToString[type], ptr, bufferPtr);
        return 1;
    }

    if (expectedPtr && ptr != expectedPtr) {
        fprintf(stderr, "FAIL: TypedArray bytes ptr and the ptr used to construct the array were not the same: %p (%s) TypedArray: %p, bytes ptr: %p\n", typedArray, typeToString[type], ptr, expectedPtr);
        return 1;
    }

    if (expectedBuffer && expectedBuffer != buffer) {
        fprintf(stderr, "FAIL: TypedArray buffer and the ArrayBuffer buffer used to construct the array were not the same: %p (%s) TypedArray buffer: %p, data: %p\n", typedArray, typeToString[type], buffer, expectedBuffer);
        return 1;
    }

    return 0;
}

static int testConstructors(JSGlobalContextRef context, JSTypedArrayType type, unsigned length)
{
    int failed = 0;
    JSValueRef exception = nullptr;
    JSObjectRef typedArray;

    // Test create with length.
    typedArray = JSObjectMakeTypedArray(context, type, length, &exception);
    failed = failed || exception || testAccess(context, typedArray, type, length);

    void* ptr = calloc(length, byteSizes[type]); // This is to be freed by data
    JSObjectRef data = JSObjectMakeArrayBufferWithBytesNoCopy(context, ptr, length * byteSizes[type], freePtr, nullptr, &exception);
    failed = failed || exception;

    // Test create with existing ptr.
    typedArray = JSObjectMakeTypedArrayWithBytesNoCopy(context, type, ptr, length * byteSizes[type], id, nullptr, &exception);
    failed = failed || exception || testAccess(context, typedArray, type, length, ptr);

    // Test create with existing ArrayBuffer.
    typedArray = JSObjectMakeTypedArrayWithArrayBuffer(context, type, data, &exception);
    failed = failed || exception || testAccess(context, typedArray, type, length, ptr, data);

    // Test create with existing ArrayBuffer and offset.
    typedArray = JSObjectMakeTypedArrayWithArrayBufferAndOffset(context, type, data, 0, length, &exception);
    failed = failed || exception || testAccess(context, typedArray, type, length, ptr, data);

    typedArray = JSObjectMakeTypedArrayWithArrayBufferAndOffset(context, type, data, byteSizes[type], length-1, &exception);
    if (!length)
        failed = failed || !exception;
    else
        failed = failed || testAccess(context, typedArray, type, length-1, ptr, data, byteSizes[type]) || exception;

    exception = nullptr;
    typedArray = JSObjectMakeTypedArrayWithArrayBufferAndOffset(context, type, data, byteSizes[type], 3, &exception);
    if (length < 2)
        failed = failed || !exception;
    else
        failed = failed || testAccess(context, typedArray, type, 3, ptr, data, byteSizes[type]) || exception;

    if (byteSizes[type] > 1) {
        exception = nullptr;
        typedArray = JSObjectMakeTypedArrayWithArrayBufferAndOffset(context, type, data, 1, length-1, &exception);
        failed = failed || !exception;
    }

    typedArray = JSObjectMakeTypedArrayWithArrayBufferAndOffset(context, type, data, byteSizes[type], length, &exception);
    failed = failed || !exception;

    exception = nullptr;
    typedArray = JSObjectMakeTypedArrayWithArrayBufferAndOffset(context, type, data, byteSizes[type], 0, &exception);
    if (!length)
        failed = failed || !exception;
    else
        failed = failed || testAccess(context, typedArray, type, 0, ptr, data, byteSizes[type]) || exception;

    return failed;
}

template <typename Functor>
static int forEachTypedArrayType(const Functor& functor)
{
    int failed = 0;
    for (unsigned i = 0; i < kJSTypedArrayTypeArrayBuffer; i++)
        failed = failed || functor(static_cast<JSTypedArrayType>(i));
    return failed;
}

int testTypedArrayCAPI()
{
    int failed = 0;
    JSGlobalContextRef context = JSGlobalContextCreate(nullptr);

    failed = failed || forEachTypedArrayType([&](JSTypedArrayType type) {
        int failed = 0;
        for (unsigned i = 0; i < numLengths; i++)
            failed = failed || testConstructors(context, type, lengths[i]);
        return failed;
    });

    // Test making a typedArray from scratch length.
    volatile JSObjectRef typedArray = JSObjectMakeTypedArray(context, kJSTypedArrayTypeUint32Array, 10, nullptr);
    JSObjectRef data = JSObjectGetTypedArrayBuffer(context, typedArray, nullptr);
    unsigned* buffer = static_cast<unsigned*>(JSObjectGetArrayBufferBytesPtr(context, data, nullptr));

    ASSERT(JSObjectGetTypedArrayLength(context, typedArray, nullptr) == 10);

    // Test buffer is connected to typedArray.
    buffer[1] = 1;
    JSValueRef v = JSObjectGetPropertyAtIndex(context, typedArray, 1, nullptr);
    failed = failed || assertEqualsAsNumber(context, v, 1);

    // Test passing a buffer from a new array to an old array
    typedArray = JSObjectMakeTypedArrayWithBytesNoCopy(context, kJSTypedArrayTypeUint32Array, buffer, 40, id, nullptr, nullptr);
    buffer = static_cast<unsigned*>(JSObjectGetTypedArrayBytesPtr(context, typedArray, nullptr));
    ASSERT(buffer[1] == 1);
    buffer[1] = 20;
    ASSERT(((unsigned*)JSObjectGetArrayBufferBytesPtr(context, data, nullptr))[1] == 20);

    // Test constructing with data and the data returned are the same even with an offset.
    typedArray = JSObjectMakeTypedArrayWithArrayBufferAndOffset(context, kJSTypedArrayTypeUint32Array, data, 4, 9, nullptr);
    failed = failed || assertEqualsAsNumber(context, JSObjectGetPropertyAtIndex(context, typedArray, 0, nullptr), 20);
    ASSERT(data == JSObjectGetTypedArrayBuffer(context, typedArray, nullptr));

    // Test attempting to allocate an array too big for memory.
    forEachTypedArrayType([&](JSTypedArrayType type) {
        JSValueRef exception = nullptr;
        JSObjectMakeTypedArray(context, type, UINT_MAX, &exception);
        return !exception;
    });

    JSGlobalContextRelease(context);

    if (!failed)
        printf("PASS: Typed Array C API Tests.\n");
    else
        printf("FAIL: Some Typed Array C API Tests failed.\n");

    return failed;
}
