#include <wasm-rt.h>
#include <client.h>
#include <fugg.h>

#include <emscripten.h>
#include <emscripten/bind.h>

#include <iostream>

#include "embind.h"


#define TRACE_EM_CALLS 0

namespace fugg {
    namespace embind {
        std::unordered_map<TypeID, const TypeInfo *const> kTypeMap = { };
        std::unordered_map<std::string, const FunctionEntry *const> kFunctionMap = { };
        std::unordered_map<emscripten::internal::EM_METHOD_CALLER, MethodCaller*> kMethodCallers = { };
    }
}

using namespace fugg::embind;


GenericWireType to_wasm_wire_type(
    const TypeID& type,
    const GenericWireType& val
) {
    auto type_info = kTypeMap.at(type);

    switch(type_info->type) {
        case Type::Void:
        case Type::Bool:
        case Type::Integer:
        case Type::Float:
        case Type::Emval:
            // Do nothing.
            return val;

        case Type::String: {            
            struct WireData {
                size_t length;
                char data[1];
            };
            
            // Strings just need to have their pointer offset.
            return fugg::RuntimePointer<WireData> { val }.as_raw();
        } break;

        case Type::MemoryView: {
            // TOOD:
            assert(0);
        } break;

        default:
            assert(0);
    }
}


void* from_wasm_wire_type(
    const size_t arg_count,
    const TypeID* types_, 
    void* args_
) {
    auto args = reinterpret_cast<emscripten::internal::GenericWireType*>(args_);
    
    for(auto i = 0; i < arg_count; i++) {
        const auto& type_info = kTypeMap[types_[i]];
        auto val = &args[i];

        switch(type_info->type) {
            case Type::Void:
            case Type::Bool:
            case Type::Integer:
            case Type::Float:
            case Type::Emval:
                // Do nothing.
                break;

            case Type::String: {
                // Offset the pointer.
                val->w[0].p = fugg::wasm::kMemory.address_of(val->w[0].p);
            } break;

            case Type::MemoryView: {
                struct WireData {
                    const size_t length;
                    void* data;
                };

                const auto& data = reinterpret_cast<WireData*>(val);
                data->data = fugg::ModulePointer<void> { data->data };
            } break;

            default:
                assert(0);
        }   
    }
    
    return args;
}

/* import: 'a' 'b' */
/* __emval_decref */
WASM_IMPORT_IMPL(env, __emval_decref) = [](u32 a) {
    emscripten::internal::_emval_decref(
        reinterpret_cast<emscripten::EM_VAL>(a)
    );
};

// void (*Z_aZ_b)(u32) = [](u32 a) {
//     emscripten::internal::_emval_decref(
//         reinterpret_cast<emscripten::EM_VAL>(a)
//     );
// };
/* import: 'a' 'c' */
/* __emval_new_cstring */
WASM_IMPORT_IMPL(env, __emval_new_cstring) = [](u32 value_) {
    const auto& value = fugg::ModulePointer<const char> { value_ };

#if TRACE_EM_CALLS
    std::cout << "_emval_new_cstring("
              << value 
              << ")" << std::endl;
#endif

    // Returns a handle, so no interop required.
    return reinterpret_cast<u32>(
        emscripten::internal::_emval_new_cstring(value)
    );
};
/* __emval_set_property */
WASM_IMPORT_IMPL(env, __emval_set_property) = [](u32 a, u32 b, u32 c) {
    emscripten::internal::_emval_set_property(
        reinterpret_cast<emscripten::EM_VAL>(a),
        reinterpret_cast<emscripten::EM_VAL>(b),
        reinterpret_cast<emscripten::EM_VAL>(c)
    );
};
/* __emval_get_property */
WASM_IMPORT_IMPL(env, __emval_get_property) = [](u32 a, u32 b) {
    return reinterpret_cast<u32>(
        emscripten::internal::_emval_get_property(
            reinterpret_cast<emscripten::EM_VAL>(a),
            reinterpret_cast<emscripten::EM_VAL>(b)
        )
    );
};

/* import: 'a' 'k' */
/* __emval_get_method_caller */
WASM_IMPORT_IMPL(env, __emval_get_method_caller) = [](u32 argCount_, u32 argTypes_) {
    const auto& arg_count = argCount_;
    const auto& arg_types = fugg::ptr_array<TypeID>(argTypes_, argCount_);

#if TRACE_EM_CALLS
    std::cout << "_emval_get_method_caller("
              << arg_count << ", "
              << arg_types << ")" << std::endl;
#endif

    const auto& method_caller = emscripten::internal::_emval_get_method_caller(arg_count, arg_types);
    if(!kMethodCallers.count(method_caller)) {
        const auto& return_type = arg_types[0];

        kMethodCallers.emplace(method_caller, new MethodCaller {
            .return_type = return_type,
            // First argument is return type, so reduce argument count by 1
            .arg_count = arg_count - 1,
            .arg_types = &arg_types[1]
        });

#if TRACE_EM_CALLS
        std::cout << "Added method caller with id " << reinterpret_cast<void*>(method_caller)
                  << " -> " << kTypeMap[return_type]->name << std::endl;
#endif
    }

    return reinterpret_cast<u32>(method_caller);
};
/* import: 'a' 'l' */
/* __emval_take_value */
WASM_IMPORT_IMPL(env, __emval_take_value) = [](u32 type_, u32 argv_) {
    TypeID type = fugg::ModulePointer<TypeID>(type_);
    auto argv = fugg::ModulePointer<void>(argv_);

#if TRACE_EM_CALLS
    std::cout << "_emval_take_value(" 
              << type << ", " 
              << argv << ")" << std::endl;
#endif

    return reinterpret_cast<u32>(
        emscripten::internal::_emval_take_value(
            type,
            // EM_VAR_ARGS calls `readValueFromPointer` so interop is required.
            // emscripten/val.h:396: Constructing array of size 1.
            from_wasm_wire_type(1, &type, argv)    
        )
    );
};
/* import: 'a' 'o' */
/* __emval_run_destructors */
WASM_IMPORT_IMPL(env, __emval_run_destructors) = [](u32 a) {
    // EM_DESTRUCTORS is a handle, so no interop required.
    const auto& destructors = reinterpret_cast<Destructors>(a);

#if TRACE_EM_CALLS
    std::cout << "_emval_run_destructors(" 
              << reinterpret_cast<void*>(a) << ")" << std::endl;
#endif

    emscripten::internal::_emval_run_destructors(destructors);
};
/* __emval_get_global */
WASM_IMPORT_IMPL(env, __emval_get_global) = [](u32 name_) {
    const auto& name = fugg::ModulePointer<const char>(name_);

#if TRACE_EM_CALLS
    std::cout << "_emval_get_global("
              << name 
              << ")" << std::endl;
#endif

    // This should be fine because the return value of _emval_get_global is just some number, not a real address.
    return reinterpret_cast<u32>(
        emscripten::internal::_emval_get_global(name)
    );
};
/* import: 'a' 't' */
/* __emval_call_void_method */
WASM_IMPORT_IMPL(env, __emval_call_void_method) = [](u32 caller_, u32 handle_, u32 methodName_, u32 argv_) {
    // EM_METHOD_CALLER is used as an id, so no interop is required.
    const auto& caller = reinterpret_cast<emscripten::internal::EM_METHOD_CALLER>(caller_);
    const auto& handle = reinterpret_cast<Value>(handle_);
    const auto& method_name = fugg::ModulePointer<const char>(methodName_);
    const auto& argv = fugg::ModulePointer<void>(argv_);

    const auto& method_caller = kMethodCallers[caller];

#if TRACE_EM_CALLS
    std::cout << "_emval_call_void_method("
              << caller << ", "
              << handle << ", "
              << method_name << ", "
              << argv << ")" << std::endl;
#endif

    emscripten::internal::_emval_call_void_method(
        caller, 
        handle, 
        method_name, 
        from_wasm_wire_type(method_caller->arg_count, method_caller->arg_types, argv)
    );
};
/* import: 'a' 'y' */
/* __emval_as */
WASM_IMPORT_IMPL(env, __emval_as) = [](u32 a, u32 b, u32 c) {
    // EM_VAL is just a number of the handle, so no interop required.
    const auto& value = reinterpret_cast<emscripten::EM_VAL>(a);
    
    const auto& return_type = fugg::ModulePointer<TypeID>(b);
    const auto& destructors = fugg::ModulePointer<emscripten::internal::EM_DESTRUCTORS>(c);

#if TRACE_EM_CALLS
    std::cout << "_emval_as("
              << value << ", " 
              << return_type << " (" << kTypeMap[return_type]->name << ")" << ", "
              << destructors 
              << std::endl; 
#endif
       
    return to_wasm_wire_type(
        return_type,
        emscripten::internal::_emval_as(value, return_type, destructors)
    );
};
/* import: 'a' 'E' */
/* __emval_incref */
WASM_IMPORT_IMPL(env, __emval_incref) = [](u32 a) {
    emscripten::internal::_emval_incref(
        reinterpret_cast<emscripten::EM_VAL>(a)
    );
};
/* import: 'a' 'H' */
/* __emval_call_method */
WASM_IMPORT_IMPL(env, __emval_call_method) = [](u32 a, u32 b, u32 c, u32 d, u32 argv_) {
    const auto& caller = reinterpret_cast<emscripten::internal::EM_METHOD_CALLER>(a);
    const auto& handle = reinterpret_cast<emscripten::EM_VAL>(b);
    const auto& method_name = fugg::ModulePointer<const char>(c);
    const auto& destructors = fugg::ModulePointer<emscripten::internal::EM_DESTRUCTORS>(d);

    const auto& method_caller = kMethodCallers.at(caller);
    const auto& argv = fugg::ModulePointer<void>(argv_);

#if TRACE_EM_CALLS
    std::cout << "_emval_call_method("
              << caller << ", "
              << handle << ", "
              << method_name << ", "
              << destructors << ", "
              << argv << ")" << std::endl;
#endif

    auto result = emscripten::internal::_emval_call_method(
        caller, 
        handle, 
        method_name, 
        destructors, 
        from_wasm_wire_type(method_caller->arg_count, method_caller->arg_types, argv)
    );
  
    return to_wasm_wire_type(
        method_caller->return_type,
        result
    );
};
/* __emval_get_module_property */
WASM_IMPORT_IMPL(env, __emval_get_module_property) = [](u32 a) {
    const auto& name = fugg::ModulePointer<const char>(a);

#if TRACE_EM_CALLS
    std::cout << "_emval_get_module_property(" << name << ")" << std::endl;
#endif

    return reinterpret_cast<u32>(
        emscripten::internal::_emval_get_module_property(name)
    );
};
/* __emval_typeof */
WASM_IMPORT_IMPL(env, __emval_typeof) = [](u32 a) {
    return reinterpret_cast<u32>(
        emscripten::internal::_emval_typeof(
            reinterpret_cast<emscripten::EM_VAL>(a)
        )
    );
};

/* __embind_register_function */
WASM_IMPORT_IMPL(env, __embind_register_function) = [](u32 a, u32 arg_count, u32 c, u32 d, u32 invoker_, u32 function_) {
    const auto& name = fugg::ModulePointer<const char>(a);
    // This is an array, but not an array of pointers.
    const auto& arg_types = fugg::ModulePointer<emscripten::internal::TYPEID>(c);
    const auto& signature = fugg::ModulePointer<const char>(d);
    // The invoker seems to convert the types received by the function.
    const auto& invoker = fugg::function_ref<emscripten::internal::GenericFunction>(invoker_);
    const auto& function = fugg::function_ref<emscripten::internal::GenericFunction>(function_);

    kFunctionMap.emplace(name, new FunctionEntry {
        .invoker = invoker_,
        .fn = function_
    });

#if TRACE_EM_CALLS
    std::cout << "_embind_register_function("
              << name << ", " 
              << arg_count << ", "
              << arg_types << ", "
              << signature << ", "
              << invoker << ", " 
              << function 
              << ")" << std::endl;
#endif

    // if(std::string(name) != "platform_text_edited") {
        // emscripten::internal::_embind_register_function(name, arg_count, arg_types, signature, invoker, function);
    // } else {
        std::cout << "Skipping registration of " << static_cast<const char*>(name) 
                << " with " << arg_count << " arguments." << std::endl;
    // }
};
/* import: 'a' 'ua' */
/* __emval_new */
WASM_IMPORT_IMPL(env, __emval_new) = [](u32 a, u32 arg_count, u32 arg_types_, u32 argv_) {
    auto value = reinterpret_cast<emscripten::EM_VAL>(a);
    // To prevent double converting pointers in the arg_type array, create a new
    // array.
    auto arg_types_copy = new void*[arg_count];

    const auto& arg_types = fugg::ModulePointer<TypeID*>(arg_types_);
    for(int i = 0; i < arg_count; i++) 
        arg_types_copy[i] = fugg::ModulePointer<TypeID> { arg_types[i] };

    auto argv = fugg::ModulePointer<void>(argv_);

    
#if TRACE_EM_CALLS
    std::cout << "_emval_new(" 
              << value << ", "
              << arg_count << ", "
              << arg_types << "(raw: " << reinterpret_cast<void*>(arg_types_) << "), " 
              << argv << ")" << std::endl;
#endif

    const auto result = emscripten::internal::_emval_new(
        value, 
        arg_count, 
        arg_types_copy, 
        from_wasm_wire_type(arg_count, arg_types_copy, argv)
    );

    delete[] arg_types_copy;

    return reinterpret_cast<u32>(result);
};
/* import: 'a' 'ya' */
// "ya": "_emscripten_resize_heap",
WASM_IMPORT_IMPL(env, _emscripten_resize_heap) = [](u32 a) {
    printf("Flyff apparantly wants to resize the heap!");

    // Original code calls `abortOnMemoryGrowth()`
    return static_cast<u32>(0);    
};
/* import: 'a' 'za' */
/* __embind_register_std_wstring */
WASM_IMPORT_IMPL(env, __embind_register_std_wstring) = [](u32 a, u32 char_size, u32 c) {
    const auto& string_type = fugg::ModulePointer<emscripten::internal::TYPEID>(a);
    const auto& name = fugg::ModulePointer<const char>(c);

#if TRACE_EM_CALLS
    std::cout << "_embind_register_std_wstring(" 
              << string_type << ", " 
              << char_size << ", "
              << name << ")" << std::endl;
#endif

    kTypeMap.emplace(string_type, new TypeInfo { 
        .name = std::string(name),
        .type = Type::WideString
    });

    emscripten::internal::_embind_register_std_wstring(string_type, char_size, name);
};
/* import: 'a' '_a' */
/* __embind_register_float */
WASM_IMPORT_IMPL(env, __embind_register_float) = [](u32 a, u32 b, u32 size) {
    const auto& float_type = fugg::ModulePointer<emscripten::internal::TYPEID>(a);
    const auto& name = fugg::ModulePointer<const char>(b);

#if TRACE_EM_CALLS
    std::cout << "_embind_register_float(" 
              << float_type << ", "
              << name << ", "
              << size << ")" << std::endl;
#endif

    kTypeMap.emplace(float_type, new TypeInfo { 
        .name = std::string(name),
        .type = Type::Float
    });

    emscripten::internal::_embind_register_float(float_type, name, size);
};
/* import: 'a' '$a' */
/* __embind_register_std_string */
WASM_IMPORT_IMPL(env, __embind_register_std_string) = [](u32 a, u32 b) {
    const auto& string_type = fugg::ModulePointer<emscripten::internal::TYPEID>(a);
    const auto& name = fugg::ModulePointer<const char>(b);

#if TRACE_EM_CALLS
    std::cout << "_embind_register_std_string(" 
              << string_type << ", "
              << name << ")" << std::endl;
#endif

    kTypeMap.emplace(string_type, new TypeInfo { 
        .name = std::string(name),
        .type = Type::String
    });

    emscripten::internal::_embind_register_std_string(string_type, name);
};
/* import: 'a' 'Bb' */
/* __embind_register_bigint */
WASM_IMPORT_IMPL(env, __embind_register_bigint) = [](u32 registerType, u32 name_, u32 size, u32 min_range, u32 max_range, u32 f, u32 g) {
    // Does nothing in JS.  
};
/* import: 'a' 'Ob' */
/* __embind_register_emval */
WASM_IMPORT_IMPL(env, __embind_register_emval) = [](u32 a, u32 b) {
    const auto& emval_type = fugg::ModulePointer<emscripten::internal::TYPEID>(a);
    const auto& name = fugg::ModulePointer<const char>(b);

#if TRACE_EM_CALLS
    std::cout << "_embind_register_emval("
              << emval_type << ", "
              << name << ")" << std::endl;
#endif

    kTypeMap.emplace(emval_type, new TypeInfo { 
        .name = std::string(name),
        .type = Type::Emval
    });
    
    emscripten::internal::_embind_register_emval(emval_type, name);
};
/* import: 'a' 'Pb' */
/* __embind_register_bool */
WASM_IMPORT_IMPL(env, __embind_register_bool) = [](u32 a, u32 b, u32 size, u32 true_value, u32 false_value) {
    const auto& bool_type = fugg::ModulePointer<emscripten::internal::TYPEID>(a);
    const auto& name = fugg::ModulePointer<const char>(b);

#if TRACE_EM_CALLS
    std::cout << "_embind_register_bool("
              << bool_type << ", "
              << name << ", "
              << size << ", "
              << true_value << ", "
              << false_value << ")" << std::endl;
#endif

    kTypeMap.emplace(bool_type, new TypeInfo { 
        .name = std::string(name),
        .type = Type::Bool
    });

    emscripten::internal::_embind_register_bool(bool_type, name, size, true_value, false_value);
};
/* import: 'a' 'Qb' */
/* __embind_register_void */
WASM_IMPORT_IMPL(env, __embind_register_void) = [](u32 a, u32 b) {
    const auto& void_type = fugg::ModulePointer<emscripten::internal::TYPEID>(a);
    const auto& name = fugg::ModulePointer<const char>(b);

#if TRACE_EM_CALLS
    std::cout << "_embind_register_void("
              << void_type << ", "
              << name << ")" << std::endl;
#endif

    kTypeMap.emplace(void_type, new TypeInfo { 
        .name = std::string(name),
        .type = Type::Void
    });

    emscripten::internal::_embind_register_void(void_type, name);
};

/* import: 'a' 'J' */
/* __embind_register_memory_view */
WASM_IMPORT_IMPL(env, __embind_register_memory_view) = [](u32 a, u32 b, u32 c) {
    const auto& memory_view_type = fugg::ModulePointer<emscripten::internal::TYPEID>(a);
    const auto& typed_array_index = b;
    const auto& name = fugg::ModulePointer<const char>(c);

#if TRACE_EM_CALLS
    std::cout << "_embind_register_memory_view("
              << memory_view_type << ", "
              << typed_array_index << ", "
              << name << ")" << std::endl;
#endif

    kTypeMap.emplace(memory_view_type, new TypeInfo { 
        .name = std::string(name),
        .type = Type::MemoryView
    });

    emscripten::internal::_embind_register_memory_view(memory_view_type, typed_array_index, name);
};
/* __embind_register_integer */
WASM_IMPORT_IMPL(env, __embind_register_integer) = [](u32 integer_type_, u32 name_, u32 size, u32 min_range, u32 max_range) {
    const auto& integer_type = fugg::ModulePointer<TypeID>(integer_type_);
    const auto& name = fugg::ModulePointer<const char>(name_);

#if TRACE_EM_CALLS
    std::cout << "_embind_register_integer(" 
              << integer_type << ", "
              << name << ", "
              << size << ", "
              << min_range << ", "
              << max_range << ")" << std::endl;
#endif

    kTypeMap.emplace(integer_type, new TypeInfo { 
        .name = std::string(name),
        .type = Type::Integer
    });

    emscripten::internal::_embind_register_integer(integer_type, name, size, min_range, max_range);
};