/*
 * 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 CANON 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 CANON 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.
 */

#pragma once

#include "BuiltinNames.h"
#include "JSDOMBinding.h"
#include <runtime/IterationKind.h>
#include <runtime/IteratorPrototype.h>
#include <runtime/JSDestructibleObject.h>
#include <runtime/IteratorOperations.h>

namespace WebCore {

typedef JSC::IterationKind IterationKind;

void addValueIterableMethods(JSC::JSGlobalObject&, JSC::JSObject&);

template<typename JSWrapper, typename DOMWrapped>
class JSDOMIteratorMapPrototype : public JSC::JSNonFinalObject {
public:
    typedef JSC::JSNonFinalObject Base;

    static JSDOMIteratorMapPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
    {
        JSDOMIteratorMapPrototype* prototype = new (NotNull, JSC::allocateCell<JSDOMIteratorMapPrototype>(vm.heap)) JSDOMIteratorMapPrototype(vm, structure);
        prototype->finishCreation(vm, globalObject);
        return prototype;
    }

    DECLARE_INFO;

    static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
    {
        return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
    }

    static JSC::EncodedJSValue iteratorCreate(JSC::ExecState& state, IterationKind kind, const char* propertyName);

    template<typename IteratorValue>
    static void appendForEachArguments(JSC::ExecState& state, JSDOMGlobalObject* globalObject, JSC::MarkedArgumentBuffer& arguments, IteratorValue& value);

    static JSC::EncodedJSValue iteratorForEach(JSC::ExecState& state, const char* propertyName);

private:
    JSDOMIteratorMapPrototype(JSC::VM& vm, JSC::Structure* structure) : Base(vm, structure) { }

    static JSC::EncodedJSValue JSC_HOST_CALL next(JSC::ExecState*);

    void finishCreation(JSC::VM&, JSC::JSGlobalObject*);
};

template<typename JSWrapper, typename DOMWrapped>
class JSDOMIteratorSetPrototype : public JSC::JSNonFinalObject {
public:
    typedef JSC::JSNonFinalObject Base;

    static JSDOMIteratorSetPrototype* create(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::Structure* structure)
    {
    	JSDOMIteratorSetPrototype* prototype = new (NotNull, JSC::allocateCell<JSDOMIteratorSetPrototype>(vm.heap)) JSDOMIteratorSetPrototype(vm, structure);
        prototype->finishCreation(vm, globalObject);
        return prototype;
    }

    DECLARE_INFO;

    static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
    {
        return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
    }

    static JSC::EncodedJSValue iteratorCreate(JSC::ExecState& state, IterationKind kind, const char* propertyName);

    template<typename IteratorValue>
    static void appendForEachArguments(JSC::ExecState& state, JSDOMGlobalObject* globalObject, JSC::MarkedArgumentBuffer& arguments, IteratorValue& value);

    static JSC::EncodedJSValue iteratorForEach(JSC::ExecState& state, const char* propertyName);

private:
    JSDOMIteratorSetPrototype(JSC::VM& vm, JSC::Structure* structure) : Base(vm, structure) { }

    static JSC::EncodedJSValue JSC_HOST_CALL next(JSC::ExecState*);

    void finishCreation(JSC::VM&, JSC::JSGlobalObject*);
};

template<typename JSWrapper, typename DOMWrapped>
class JSDOMIteratorMap: public JSDOMObject {
public:
    typedef JSDOMObject Base;

    DECLARE_INFO;

    static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
    {
        return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
    }

    static JSDOMIteratorMap* create(JSC::VM& vm, JSC::Structure* structure, JSWrapper& iteratedObject, IterationKind kind)
    {
    	JSDOMIteratorMap* instance = new (NotNull, JSC::allocateCell<JSDOMIteratorMap>(vm.heap)) JSDOMIteratorMap(structure, iteratedObject, kind);
        instance->finishCreation(vm);
        return instance;
    }

    static JSDOMIteratorMapPrototype<JSWrapper, DOMWrapped>* createPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
    {
        return JSDOMIteratorMapPrototype<JSWrapper, DOMWrapped>::create(vm, globalObject,
            JSDOMIteratorMapPrototype<JSWrapper, DOMWrapped>::createStructure(vm, globalObject, globalObject->iteratorPrototype()));
    }

    JSC::JSValue next(JSC::ExecState&);

protected:
    JSDOMIteratorMap(JSC::Structure* structure, JSWrapper& iteratedObject, IterationKind kind)
        : Base(structure, iteratedObject.globalObject())
        , m_iterator(iteratedObject.wrapped().createIterator())
        , m_kind(kind)
    {
    }

    template<typename IteratorValue>
    ALWAYS_INLINE JSC::JSValue asJS(JSC::ExecState&, IteratorValue&);

    static void destroy(JSC::JSCell*);

    Optional<typename DOMWrapped::Iterator> m_iterator;
    IterationKind m_kind;
};

template<typename JSWrapper, typename DOMWrapped>
class JSDOMIteratorSet: public JSDOMObject {
public:
    typedef JSDOMObject Base;

    DECLARE_INFO;

    static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
    {
        return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info());
    }

    static JSDOMIteratorSet* create(JSC::VM& vm, JSC::Structure* structure, JSWrapper& iteratedObject, IterationKind kind)
    {
    	JSDOMIteratorSet* instance = new (NotNull, JSC::allocateCell<JSDOMIteratorSet>(vm.heap)) JSDOMIteratorSet(structure, iteratedObject, kind);
        instance->finishCreation(vm);
        return instance;
    }

    static JSDOMIteratorSetPrototype<JSWrapper, DOMWrapped>* createPrototype(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
    {
        return JSDOMIteratorSetPrototype<JSWrapper, DOMWrapped>::create(vm, globalObject,
			JSDOMIteratorSetPrototype<JSWrapper, DOMWrapped>::createStructure(vm, globalObject, globalObject->iteratorPrototype()));
    }

    JSC::JSValue next(JSC::ExecState&);

protected:
    JSDOMIteratorSet(JSC::Structure* structure, JSWrapper& iteratedObject, IterationKind kind)
        : Base(structure, iteratedObject.globalObject())
        , m_iterator(iteratedObject.wrapped().createIterator())
        , m_kind(kind)
    {
    }

    template<typename IteratorValue>
    ALWAYS_INLINE JSC::JSValue asJS(JSC::ExecState&, IteratorValue&);

    static void destroy(JSC::JSCell*);

    Optional<typename DOMWrapped::Iterator> m_iterator;
    IterationKind m_kind;
};

template<typename JSWrapper, typename DOMWrapped>
JSC::EncodedJSValue JSDOMIteratorMapPrototype<JSWrapper, DOMWrapped>::iteratorCreate(JSC::ExecState& state, IterationKind kind, const char* propertyName)
{
    auto wrapper = JSC::jsDynamicCast<JSWrapper*>(state.thisValue());
    if (UNLIKELY(!wrapper))
        return throwThisTypeError(state, JSWrapper::info()->className, propertyName);
    JSDOMGlobalObject& globalObject = *wrapper->globalObject();
    return JSC::JSValue::encode(JSDOMIteratorMap<JSWrapper, DOMWrapped>::create(globalObject.vm(), getDOMStructure<JSDOMIteratorMap<JSWrapper, DOMWrapped>>(globalObject.vm(), &globalObject), *wrapper, kind));
}

template<typename JSWrapper, typename DOMWrapped>
JSC::EncodedJSValue  JSDOMIteratorSetPrototype<JSWrapper, DOMWrapped>::iteratorCreate(JSC::ExecState& state, IterationKind kind, const char* propertyName)
{
    auto wrapper = JSC::jsDynamicCast<JSWrapper*>(state.thisValue());
    if (UNLIKELY(!wrapper))
        return throwThisTypeError(state, JSWrapper::info()->className, propertyName);
    JSDOMGlobalObject& globalObject = *wrapper->globalObject();
    return JSC::JSValue::encode(JSDOMIteratorSet<JSWrapper, DOMWrapped>::create(globalObject.vm(), getDOMStructure<JSDOMIteratorSet<JSWrapper, DOMWrapped>>(globalObject.vm(), &globalObject), *wrapper, kind));
}

template<typename JSWrapper, typename DOMWrapped>
template<typename IteratorValue>
JSC::JSValue JSDOMIteratorMap<JSWrapper, DOMWrapped>::asJS(JSC::ExecState& state, IteratorValue& value)
{
    ASSERT(value);

    switch (m_kind) {
    case IterationKind::Keys:
		return toJS(&state, globalObject(), value->first);
    case IterationKind::Values:
		return toJS(&state, globalObject(), value->second);
    case IterationKind::Entries:
    	return jsPairOrNull(state, globalObject(), value->first, value->second);
    }

    ASSERT_NOT_REACHED();
    return JSC::jsNull();
}

template<typename JSWrapper, typename DOMWrapped>
template<typename IteratorValue>
JSC::JSValue JSDOMIteratorSet<JSWrapper, DOMWrapped>::asJS(JSC::ExecState& state, IteratorValue& value)
{
    ASSERT(value);
    JSC::JSValue result = toJS(&state, globalObject(), value);

    switch (m_kind) {
    case IterationKind::Keys:
    case IterationKind::Values:
        return result;
    case IterationKind::Entries:
    	return jsPairOrNull(state, globalObject(), result, result);
    }

    ASSERT_NOT_REACHED();
    return JSC::jsNull();
}

template<typename JSWrapper, typename DOMWrapped>
template<typename IteratorValue>
void JSDOMIteratorMapPrototype<JSWrapper, DOMWrapped>::appendForEachArguments(JSC::ExecState& state, JSDOMGlobalObject* globalObject, JSC::MarkedArgumentBuffer& arguments, IteratorValue& value)
{
    ASSERT(value);
    arguments.append(toJS(&state, globalObject, value->second));
    arguments.append(toJS(&state, globalObject, value->first));
}

template<typename JSWrapper, typename DOMWrapped>
template<typename IteratorValue>
void JSDOMIteratorSetPrototype<JSWrapper, DOMWrapped>::appendForEachArguments(JSC::ExecState& state, JSDOMGlobalObject* globalObject, JSC::MarkedArgumentBuffer& arguments, IteratorValue& value)
{
    ASSERT(value);
    JSC::JSValue argument = toJS(&state, globalObject, *value);
    arguments.append(argument);
    arguments.append(argument);
}

template<typename JSWrapper, typename DOMWrapped>
JSC::EncodedJSValue JSDOMIteratorMapPrototype<JSWrapper, DOMWrapped>::iteratorForEach(JSC::ExecState& state, const char* propertyName)
{
	JSC::VM& vm = state.vm();

	JSWrapper* wrapper = JSC::jsDynamicCast<JSWrapper*>(state.thisValue());
    if (UNLIKELY(!wrapper))
        return throwThisTypeError(state, JSWrapper::info()->className, propertyName);

    JSC::JSValue callback = state.argument(0);
    JSC::JSValue thisValue = state.argument(1);

    auto callData = JSC::getCallData(vm, callback);
    if (callData.type == JSC::CallData::Type::None)
        return throwVMTypeError(&state, vm);

    auto iterator = wrapper->wrapped().createIterator();
    while (auto value = iterator.next()) {
        JSC::MarkedArgumentBuffer arguments;
        appendForEachArguments(state, wrapper->globalObject(), arguments, value);
        arguments.append(wrapper);
        JSC::call(&state, callback, callData, thisValue, arguments);
        if (vm.exception())
            break;
    }
    return JSC::JSValue::encode(JSC::jsUndefined());
}

template<typename JSWrapper, typename DOMWrapped>
JSC::EncodedJSValue JSDOMIteratorSetPrototype<JSWrapper, DOMWrapped>::iteratorForEach(JSC::ExecState& state, const char* propertyName)
{
	JSC::VM& vm = state.vm();

	JSWrapper* wrapper = JSC::jsDynamicCast<JSWrapper*>(state.thisValue());
    if (UNLIKELY(!wrapper))
        return throwThisTypeError(state, JSWrapper::info()->className, propertyName);

    JSC::JSValue callback = state.argument(0);
    JSC::JSValue thisValue = state.argument(1);

    auto callData = JSC::getCallData(vm, callback);
    if (callData.type == JSC::CallData::Type::None)
        return throwVMTypeError(&state, vm);

    auto iterator = wrapper->wrapped().createIterator();
    while (auto value = iterator.next()) {
        JSC::MarkedArgumentBuffer arguments;
        appendForEachArguments(state, wrapper->globalObject(), arguments, value);
        arguments.append(wrapper);
        JSC::call(&state, callback, callData, thisValue, arguments);
        if (vm.exception())
            break;
    }
    return JSC::JSValue::encode(JSC::jsUndefined());
}

template<typename JSWrapper, typename DOMWrapped>
void JSDOMIteratorMap<JSWrapper, DOMWrapped>::destroy(JSCell* cell)
{
	JSDOMIteratorMap<JSWrapper, DOMWrapped>* thisObject = static_cast<JSDOMIteratorMap<JSWrapper, DOMWrapped>*>(cell);
    thisObject->JSDOMIteratorMap<JSWrapper, DOMWrapped>::~JSDOMIteratorMap();
}

template<typename JSWrapper, typename DOMWrapped>
void JSDOMIteratorSet<JSWrapper, DOMWrapped>::destroy(JSCell* cell)
{
	JSDOMIteratorSet<JSWrapper, DOMWrapped>* thisObject = static_cast<JSDOMIteratorSet<JSWrapper, DOMWrapped>*>(cell);
    thisObject->JSDOMIteratorSet<JSWrapper, DOMWrapped>::~JSDOMIteratorSet();
}

template<typename JSWrapper, typename DOMWrapped>
ALWAYS_INLINE JSC::JSValue JSDOMIteratorMap<JSWrapper, DOMWrapped>::next(JSC::ExecState& state)
{
    if (m_iterator) {
        auto iteratorValue = m_iterator.value().next();
        if (iteratorValue) {
        	return createIteratorResultObject(vm(), &state, asJS(state, iteratorValue), false);
        }
        m_iterator = Nullopt;
    }
    return createIteratorResultObject(vm(), &state, JSC::jsUndefined(), true);
}

template<typename JSWrapper, typename DOMWrapped>
ALWAYS_INLINE JSC::JSValue JSDOMIteratorSet<JSWrapper, DOMWrapped>::next(JSC::ExecState& state)
{
    if (m_iterator) {
        auto iteratorValue = m_iterator.value().next();
        if (iteratorValue) {
        	return createIteratorResultObject(vm(), &state, asJS(state, iteratorValue), false);
        }
        m_iterator = Nullopt;
    }
    return createIteratorResultObject(vm(), &state, JSC::jsUndefined(), true);
}

template<typename JSWrapper, typename DOMWrapped>
JSC::EncodedJSValue JSC_HOST_CALL JSDOMIteratorMapPrototype<JSWrapper, DOMWrapped>::next(JSC::ExecState* state)
{
    JSDOMIteratorMap<JSWrapper, DOMWrapped>* iterator = JSC::jsDynamicCast<JSDOMIteratorMap<JSWrapper, DOMWrapped>*>(state->thisValue());
    if (UNLIKELY(!iterator))
        return JSC::JSValue::encode(throwTypeError(state, state->vm(), ASCIILiteral("Cannot call next() on a non-Iterator object")));

    return JSC::JSValue::encode(iterator->next(*state));
}

template<typename JSWrapper, typename DOMWrapped>
JSC::EncodedJSValue JSC_HOST_CALL JSDOMIteratorSetPrototype<JSWrapper, DOMWrapped>::next(JSC::ExecState* state)
{
    JSDOMIteratorSet<JSWrapper, DOMWrapped>* iterator = JSC::jsDynamicCast<JSDOMIteratorSet<JSWrapper, DOMWrapped>*>(state->thisValue());
    if (UNLIKELY(!iterator))
        return JSC::JSValue::encode(throwTypeError(state, state->vm(), ASCIILiteral("Cannot call next() on a non-Iterator object")));

    return JSC::JSValue::encode(iterator->next(*state));
}

template<typename JSWrapper, typename DOMWrapped>
void JSDOMIteratorMapPrototype<JSWrapper, DOMWrapped>::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
    Base::finishCreation(vm);
    ASSERT(inherits(info()));

    JSC_NATIVE_INTRINSIC_FUNCTION_WITHOUT_TRANSITION(vm.propertyNames->next, next, 0, 0, JSC::NoIntrinsic);
}

template<typename JSWrapper, typename DOMWrapped>
void JSDOMIteratorSetPrototype<JSWrapper, DOMWrapped>::finishCreation(JSC::VM& vm, JSC::JSGlobalObject* globalObject)
{
    Base::finishCreation(vm);
    ASSERT(inherits(info()));

    JSC_NATIVE_INTRINSIC_FUNCTION_WITHOUT_TRANSITION(vm.propertyNames->next, next, 0, 0, JSC::NoIntrinsic);
}

}
