/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * vim: set ts=8 sts=4 et sw=4 tw=99:
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "builtin/Symbol.h"

#include "util/StringBuffer.h"
#include "vm/SymbolType.h"

#include "vm/JSObject-inl.h"
#include "vm/NativeObject-inl.h"

using JS::Symbol;
using namespace js;

const Class SymbolObject::class_ = {
    "Symbol",
    JSCLASS_HAS_RESERVED_SLOTS(RESERVED_SLOTS) | JSCLASS_HAS_CACHED_PROTO(JSProto_Symbol)
};

SymbolObject*
SymbolObject::create(JSContext* cx, JS::HandleSymbol symbol)
{
    JSObject* obj = NewBuiltinClassInstance(cx, &class_);
    if (!obj)
        return nullptr;
    SymbolObject& symobj = obj->as<SymbolObject>();
    symobj.setPrimitiveValue(symbol);
    return &symobj;
}

const JSPropertySpec SymbolObject::properties[] = {
    JS_PS_END
};

const JSFunctionSpec SymbolObject::methods[] = {
    JS_FN(js_toString_str, toString, 0, 0),
    JS_FN(js_valueOf_str, valueOf, 0, 0),
    JS_SYM_FN(toPrimitive, toPrimitive, 1, JSPROP_READONLY),
    JS_FS_END
};

const JSFunctionSpec SymbolObject::staticMethods[] = {
    JS_FN("for", for_, 1, 0),
    JS_FN("keyFor", keyFor, 1, 0),
    JS_FS_END
};

JSObject*
SymbolObject::initClass(JSContext* cx, HandleObject obj, bool defineMembers)
{
    Handle<GlobalObject*> global = obj.as<GlobalObject>();

    // This uses &JSObject::class_ because: "The Symbol prototype object is an
    // ordinary object. It is not a Symbol instance and does not have a
    // [[SymbolData]] internal slot." (ES6 rev 24, 19.4.3)
    RootedObject proto(cx, GlobalObject::createBlankPrototype<PlainObject>(cx, global));
    if (!proto)
        return nullptr;

    RootedFunction ctor(cx, GlobalObject::createConstructor(cx, construct,
                                                            ClassName(JSProto_Symbol, cx), 0));
    if (!ctor)
        return nullptr;

    if (defineMembers) {
        // Define the well-known symbol properties, such as Symbol.iterator.
        ImmutablePropertyNamePtr* names = cx->names().wellKnownSymbolNames();
        RootedValue value(cx);
        unsigned attrs = JSPROP_READONLY | JSPROP_PERMANENT;
        WellKnownSymbols* wks = cx->runtime()->wellKnownSymbols;
        for (size_t i = 0; i < JS::WellKnownSymbolLimit; i++) {
            value.setSymbol(wks->get(i));
            if (!NativeDefineDataProperty(cx, ctor, names[i], value, attrs))
                return nullptr;
        }
    }

    if (!LinkConstructorAndPrototype(cx, ctor, proto))
        return nullptr;

    if (defineMembers) {
        if (!DefinePropertiesAndFunctions(cx, proto, properties, methods) ||
            !DefineToStringTag(cx, proto, cx->names().Symbol) ||
            !DefinePropertiesAndFunctions(cx, ctor, nullptr, staticMethods))
        {
            return nullptr;
        }
    }

    if (!GlobalObject::initBuiltinConstructor(cx, global, JSProto_Symbol, ctor, proto))
        return nullptr;
    return proto;
}

// ES6 rev 24 (2014 Apr 27) 19.4.1.1 and 19.4.1.2
bool
SymbolObject::construct(JSContext* cx, unsigned argc, Value* vp)
{
    // According to a note in the draft standard, "Symbol has ordinary
    // [[Construct]] behaviour but the definition of its @@create method causes
    // `new Symbol` to throw a TypeError exception." We do not support @@create
    // yet, so just throw a TypeError.
    CallArgs args = CallArgsFromVp(argc, vp);
    if (args.isConstructing()) {
        JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_NOT_CONSTRUCTOR, "Symbol");
        return false;
    }

    // steps 1-3
    RootedString desc(cx);
    if (!args.get(0).isUndefined()) {
        desc = ToString(cx, args.get(0));
        if (!desc)
            return false;
    }

    // step 4
    RootedSymbol symbol(cx, JS::Symbol::new_(cx, JS::SymbolCode::UniqueSymbol, desc));
    if (!symbol)
        return false;
    args.rval().setSymbol(symbol);
    return true;
}

// ES6 rev 24 (2014 Apr 27) 19.4.2.2
bool
SymbolObject::for_(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);

    // steps 1-2
    RootedString stringKey(cx, ToString(cx, args.get(0)));
    if (!stringKey)
        return false;

    // steps 3-7
    JS::Symbol* symbol = JS::Symbol::for_(cx, stringKey);
    if (!symbol)
        return false;
    args.rval().setSymbol(symbol);
    return true;
}

// ES6 rev 25 (2014 May 22) 19.4.2.7
bool
SymbolObject::keyFor(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);

    // step 1
    HandleValue arg = args.get(0);
    if (!arg.isSymbol()) {
        ReportValueErrorFlags(cx, JSREPORT_ERROR, JSMSG_UNEXPECTED_TYPE, JSDVG_SEARCH_STACK,
                              arg, nullptr, "not a symbol", nullptr);
        return false;
    }

    // step 2
    if (arg.toSymbol()->code() == JS::SymbolCode::InSymbolRegistry) {
#ifdef DEBUG
        RootedString desc(cx, arg.toSymbol()->description());
        MOZ_ASSERT(Symbol::for_(cx, desc) == arg.toSymbol());
#endif
        args.rval().setString(arg.toSymbol()->description());
        return true;
    }

    // step 3: omitted
    // step 4
    args.rval().setUndefined();
    return true;
}

MOZ_ALWAYS_INLINE bool
IsSymbol(HandleValue v)
{
    return v.isSymbol() || (v.isObject() && v.toObject().is<SymbolObject>());
}

// ES6 rev 27 (2014 Aug 24) 19.4.3.2
bool
SymbolObject::toString_impl(JSContext* cx, const CallArgs& args)
{
    // steps 1-3
    HandleValue thisv = args.thisv();
    MOZ_ASSERT(IsSymbol(thisv));
    Rooted<Symbol*> sym(cx, thisv.isSymbol()
                            ? thisv.toSymbol()
                            : thisv.toObject().as<SymbolObject>().unbox());

    // step 4
    return SymbolDescriptiveString(cx, sym, args.rval());
}

bool
SymbolObject::toString(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    return CallNonGenericMethod<IsSymbol, toString_impl>(cx, args);
}

//ES6 rev 24 (2014 Apr 27) 19.4.3.3
bool
SymbolObject::valueOf_impl(JSContext* cx, const CallArgs& args)
{
    // Step 3, the error case, is handled by CallNonGenericMethod.
    HandleValue thisv = args.thisv();
    MOZ_ASSERT(IsSymbol(thisv));
    if (thisv.isSymbol())
        args.rval().set(thisv);
    else
        args.rval().setSymbol(thisv.toObject().as<SymbolObject>().unbox());
    return true;
}

bool
SymbolObject::valueOf(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);
    return CallNonGenericMethod<IsSymbol, valueOf_impl>(cx, args);
}

// ES6 19.4.3.4
bool
SymbolObject::toPrimitive(JSContext* cx, unsigned argc, Value* vp)
{
    CallArgs args = CallArgsFromVp(argc, vp);

    // The specification gives exactly the same algorithm for @@toPrimitive as
    // for valueOf, so reuse the valueOf implementation.
    return CallNonGenericMethod<IsSymbol, valueOf_impl>(cx, args);
}

JSObject*
js::InitSymbolClass(JSContext* cx, HandleObject obj)
{
    return SymbolObject::initClass(cx, obj, true);
}

JSObject*
js::InitBareSymbolCtor(JSContext* cx, HandleObject obj)
{
    return SymbolObject::initClass(cx, obj, false);
}
