/*
 *  Copyright (C) 1999-2001 Harri Porten (porten@kde.org)
 *  Copyright (C) 2004-2017 Apple Inc. All rights reserved.
 *  Copyright (C) 2007 Samuel Weinig <sam@webkit.org>
 *  Copyright (C) 2013 Michael Pruett <michael@68k.org>
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "config.h"
#include "JSDOMExceptionHandling.h"

#include "CachedScript.h"
#include "DOMException.h"
#include "DOMWindow.h"
#include "JSDOMException.h"
#include "JSDOMPromiseDeferred.h"
#include "JSDOMWindow.h"
#include "ScriptExecutionContext.h"
#include <JavaScriptCore/ErrorHandlingScope.h>
#include <JavaScriptCore/Exception.h>
#include <JavaScriptCore/ExceptionHelpers.h>
#include <JavaScriptCore/ScriptCallStack.h>
#include <JavaScriptCore/ScriptCallStackFactory.h>
#include <wtf/text/StringBuilder.h>

namespace WebCore {
using namespace JSC;

void reportException(ExecState* exec, JSValue exceptionValue, CachedScript* cachedScript)
{
    VM& vm = exec->vm();
    RELEASE_ASSERT(vm.currentThreadIsHoldingAPILock());
    auto* exception = jsDynamicCast<JSC::Exception*>(vm, exceptionValue);
    if (!exception) {
        exception = vm.lastException();
        if (!exception)
            exception = JSC::Exception::create(exec->vm(), exceptionValue, JSC::Exception::DoNotCaptureStack);
    }

    reportException(exec, exception, cachedScript);
}

String retrieveErrorMessage(ExecState& state, VM& vm, JSValue exception, CatchScope& catchScope)
{
    // FIXME: <http://webkit.org/b/115087> Web Inspector: WebCore::reportException should not evaluate JavaScript handling exceptions
    // If this is a custom exception object, call toString on it to try and get a nice string representation for the exception.
    String errorMessage;
    if (auto* error = jsDynamicCast<ErrorInstance*>(vm, exception))
        errorMessage = error->sanitizedToString(&state);
    else
        errorMessage = exception.toWTFString(&state);

    // We need to clear any new exception that may be thrown in the toString() call above.
    // reportException() is not supposed to be making new exceptions.
    catchScope.clearException();
    vm.clearLastException();
    return errorMessage;
}

void reportException(ExecState* exec, JSC::Exception* exception, CachedScript* cachedScript, ExceptionDetails* exceptionDetails)
{
    VM& vm = exec->vm();
    auto scope = DECLARE_CATCH_SCOPE(vm);

    RELEASE_ASSERT(vm.currentThreadIsHoldingAPILock());
    if (isTerminatedExecutionException(vm, exception))
        return;

    ErrorHandlingScope errorScope(exec->vm());

    auto callStack = Inspector::createScriptCallStackFromException(exec, exception);
    scope.clearException();
    vm.clearLastException();

    auto* globalObject = jsCast<JSDOMGlobalObject*>(exec->lexicalGlobalObject());
    if (auto* window = jsDynamicCast<JSDOMWindow*>(vm, globalObject)) {
        if (!window->wrapped().isCurrentlyDisplayedInFrame())
            return;
    }

    int lineNumber = 0;
    int columnNumber = 0;
    String exceptionSourceURL;
    if (auto* callFrame = callStack->firstNonNativeCallFrame()) {
        lineNumber = callFrame->lineNumber();
        columnNumber = callFrame->columnNumber();
        exceptionSourceURL = callFrame->sourceURL();
    }

    auto errorMessage = retrieveErrorMessage(*exec, vm, exception->value(), scope);
    globalObject->scriptExecutionContext()->reportException(errorMessage, lineNumber, columnNumber, exceptionSourceURL, exception, callStack->size() ? callStack.ptr() : nullptr, cachedScript);

    if (exceptionDetails) {
        exceptionDetails->message = errorMessage;
        exceptionDetails->lineNumber = lineNumber;
        exceptionDetails->columnNumber = columnNumber;
        exceptionDetails->sourceURL = exceptionSourceURL;
    }
}

void reportCurrentException(ExecState* exec)
{
    VM& vm = exec->vm();
    auto scope = DECLARE_CATCH_SCOPE(vm);
    auto* exception = scope.exception();
    scope.clearException();
    reportException(exec, exception);
}

JSValue createDOMException(ExecState* exec, ExceptionCode ec, const String& message)
{
    if (ec == ExistingExceptionError)
        return jsUndefined();

    // FIXME: Handle other WebIDL exception types.
    if (ec == TypeError) {
        if (message.isEmpty())
            return createTypeError(exec);
        return createTypeError(exec, message);
    }

    if (ec == RangeError) {
        if (message.isEmpty())
            return createRangeError(exec, "Bad value"_s);
        return createRangeError(exec, message);
    }

    if (ec == StackOverflowError)
        return createStackOverflowError(exec);

    // FIXME: All callers to createDOMException need to pass in the correct global object.
    // For now, we're going to assume the lexicalGlobalObject. Which is wrong in cases like this:
    // frames[0].document.createElement(null, null); // throws an exception which should have the subframe's prototypes.
    JSDOMGlobalObject* globalObject = deprecatedGlobalObjectForPrototype(exec);
    JSValue errorObject = toJS(exec, globalObject, DOMException::create(ec, message));

    ASSERT(errorObject);
    addErrorInfo(exec, asObject(errorObject), true);
    return errorObject;
}

JSValue createDOMException(ExecState& state, Exception&& exception)
{
    return createDOMException(&state, exception.code(), exception.releaseMessage());
}

void propagateExceptionSlowPath(JSC::ExecState& state, JSC::ThrowScope& throwScope, Exception&& exception)
{
    throwScope.assertNoException();
    throwException(&state, throwScope, createDOMException(state, WTFMove(exception)));
}

static EncodedJSValue throwTypeError(JSC::ExecState& state, JSC::ThrowScope& scope, const String& errorMessage)
{
    return throwVMTypeError(&state, scope, errorMessage);
}

static void appendArgumentMustBe(StringBuilder& builder, unsigned argumentIndex, const char* argumentName, const char* interfaceName, const char* functionName)
{
    builder.append("Argument ", argumentIndex + 1, " ('", argumentName, "') to ");
    if (!functionName)
        builder.append("the ", interfaceName, " constructor");
    else
        builder.append(interfaceName, '.', functionName);
    builder.appendLiteral(" must be ");
}

void throwNotSupportedError(JSC::ExecState& state, JSC::ThrowScope& scope, ASCIILiteral message)
{
    scope.assertNoException();
    throwException(&state, scope, createDOMException(&state, NotSupportedError, message));
}

void throwInvalidStateError(JSC::ExecState& state, JSC::ThrowScope& scope, ASCIILiteral message)
{
    scope.assertNoException();
    throwException(&state, scope, createDOMException(&state, InvalidStateError, message));
}

void throwSecurityError(JSC::ExecState& state, JSC::ThrowScope& scope, const String& message)
{
    scope.assertNoException();
    throwException(&state, scope, createDOMException(&state, SecurityError, message));
}

JSC::EncodedJSValue throwArgumentMustBeEnumError(JSC::ExecState& state, JSC::ThrowScope& scope, unsigned argumentIndex, const char* argumentName, const char* functionInterfaceName, const char* functionName, const char* expectedValues)
{
    StringBuilder builder;
    appendArgumentMustBe(builder, argumentIndex, argumentName, functionInterfaceName, functionName);
    builder.append("one of: ", expectedValues);
    return throwVMTypeError(&state, scope, builder.toString());
}

JSC::EncodedJSValue throwArgumentMustBeFunctionError(JSC::ExecState& state, JSC::ThrowScope& scope, unsigned argumentIndex, const char* argumentName, const char* interfaceName, const char* functionName)
{
    StringBuilder builder;
    appendArgumentMustBe(builder, argumentIndex, argumentName, interfaceName, functionName);
    builder.appendLiteral("a function");
    return throwVMTypeError(&state, scope, builder.toString());
}

JSC::EncodedJSValue throwArgumentTypeError(JSC::ExecState& state, JSC::ThrowScope& scope, unsigned argumentIndex, const char* argumentName, const char* functionInterfaceName, const char* functionName, const char* expectedType)
{
    StringBuilder builder;
    appendArgumentMustBe(builder, argumentIndex, argumentName, functionInterfaceName, functionName);
    builder.append("an instance of ", expectedType);
    return throwVMTypeError(&state, scope, builder.toString());
}

void throwAttributeTypeError(JSC::ExecState& state, JSC::ThrowScope& scope, const char* interfaceName, const char* attributeName, const char* expectedType)
{
    throwTypeError(state, scope, makeString("The ", interfaceName, '.', attributeName, " attribute must be an instance of ", expectedType));
}

JSC::EncodedJSValue throwRequiredMemberTypeError(JSC::ExecState& state, JSC::ThrowScope& scope, const char* memberName, const char* dictionaryName, const char* expectedType)
{
    StringBuilder builder;
    builder.appendLiteral("Member ");
    builder.append(dictionaryName);
    builder.append('.');
    builder.append(memberName);
    builder.appendLiteral(" is required and must be an instance of ");
    builder.append(expectedType);
    return throwVMTypeError(&state, scope, builder.toString());
}

JSC::EncodedJSValue throwConstructorScriptExecutionContextUnavailableError(JSC::ExecState& state, JSC::ThrowScope& scope, const char* interfaceName)
{
    return throwVMError(&state, scope, createReferenceError(&state, makeString(interfaceName, " constructor associated execution context is unavailable")));
}

void throwSequenceTypeError(JSC::ExecState& state, JSC::ThrowScope& scope)
{
    throwTypeError(state, scope, "Value is not a sequence"_s);
}

void throwNonFiniteTypeError(ExecState& state, JSC::ThrowScope& scope)
{
    throwTypeError(&state, scope, "The provided value is non-finite"_s);
}

String makeGetterTypeErrorMessage(const char* interfaceName, const char* attributeName)
{
    return makeString("The ", interfaceName, '.', attributeName, " getter can only be used on instances of ", interfaceName);
}

JSC::EncodedJSValue throwGetterTypeError(JSC::ExecState& state, JSC::ThrowScope& scope, const char* interfaceName, const char* attributeName)
{
    return throwVMGetterTypeError(&state, scope, makeGetterTypeErrorMessage(interfaceName, attributeName));
}

JSC::EncodedJSValue rejectPromiseWithGetterTypeError(JSC::ExecState& state, const char* interfaceName, const char* attributeName)
{
    return createRejectedPromiseWithTypeError(state, makeGetterTypeErrorMessage(interfaceName, attributeName), RejectedPromiseWithTypeErrorCause::NativeGetter);
}

bool throwSetterTypeError(JSC::ExecState& state, JSC::ThrowScope& scope, const char* interfaceName, const char* attributeName)
{
    throwTypeError(state, scope, makeString("The ", interfaceName, '.', attributeName, " setter can only be used on instances of ", interfaceName));
    return false;
}

String makeThisTypeErrorMessage(const char* interfaceName, const char* functionName)
{
    return makeString("Can only call ", interfaceName, '.', functionName, " on instances of ", interfaceName);
}

EncodedJSValue throwThisTypeError(JSC::ExecState& state, JSC::ThrowScope& scope, const char* interfaceName, const char* functionName)
{
    return throwTypeError(state, scope, makeThisTypeErrorMessage(interfaceName, functionName));
}

JSC::EncodedJSValue rejectPromiseWithThisTypeError(DeferredPromise& promise, const char* interfaceName, const char* methodName)
{
    promise.reject(TypeError, makeThisTypeErrorMessage(interfaceName, methodName));
    return JSValue::encode(jsUndefined());
}

JSC::EncodedJSValue rejectPromiseWithThisTypeError(JSC::ExecState& state, const char* interfaceName, const char* methodName)
{
    return createRejectedPromiseWithTypeError(state, makeThisTypeErrorMessage(interfaceName, methodName), RejectedPromiseWithTypeErrorCause::InvalidThis);
}

void throwDOMSyntaxError(JSC::ExecState& state, JSC::ThrowScope& scope, ASCIILiteral message)
{
    scope.assertNoException();
    throwException(&state, scope, createDOMException(&state, SyntaxError, message));
}

void throwDataCloneError(JSC::ExecState& state, JSC::ThrowScope& scope)
{
    scope.assertNoException();
    throwException(&state, scope, createDOMException(&state, DataCloneError));
}

} // namespace WebCore
