/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
/*
 * This file is part of the Collabora Office project.
 *
 * 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/.
 */
#ifdef __EMSCRIPTEN__
#include <com/sun/star/frame/XModel.hpp>

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

#include <bridges/emscriptencxxabi/cxxabi.hxx>
#include <cpo/uno/Any.hxx>
#include <com/sun/star/uno/Reference.hxx>
#include <com/sun/star/uno/RuntimeException.hpp>
#include <cpo/uno/Type.hxx>
#include <cpo/uno/TypeClass.hpp>
#include <com/sun/star/uno/XInterface.hpp>
#include <comphelper/processfactory.hxx>
#include <cppuhelper/exc_hlp.hxx>
#include <o3tl/any.hxx>
#include <o3tl/temporary.hxx>
#include <o3tl/unreachable.hxx>
#include <rtl/ustring.hxx>
#include <sal/log.hxx>
#include <sfx2/viewsh.hxx>
#include <static/unoembindhelpers/PrimaryBindings.hxx>
#include <typelib/typeclass.h>
#include <typelib/typedescription.h>
#include <typelib/typedescription.hxx>
#include <uno/data.h>

#include <cassert>
#include <cstddef>
#include <cstdint>
#include <stdexcept>
#include <string>
#include <typeinfo>
#include <utility>

using namespace emscripten;
using namespace css::uno;
using namespace cpo::uno;

template <> struct emscripten::smart_ptr_trait<cpo::uno::Type>
{
    using PointerType = cpo::uno::Type;
    using element_type = typelib_TypeDescriptionReference;
    static typelib_TypeDescriptionReference* get(cpo::uno::Type const& ptr)
    {
        return ptr.getTypeLibType();
    }
    static sharing_policy get_sharing_policy() { return sharing_policy::NONE; }
    static cpo::uno::Type* share(typelib_TypeDescriptionReference* v)
    {
        return new cpo::uno::Type(v);
    }
    static cpo::uno::Type* construct_null() { return new cpo::uno::Type(); }
};

EM_JS(void, jsRegisterChar, (std::type_info const* raw),
// clang-format off
{
    Module.registerType(raw, {
        name: 'sal_Unicode',
        fromWireType(ptr) {
            let str = String.fromCharCode(Module.HEAPU16[ptr >> 1]);
            return str;
        },
        toWireType(destructors, value) {
            if (typeof value != 'string' || value.length !== 1) {
                Module.throwBindingError(
                    'Cannot pass anything but 1-element string to C++ char16_t');
            }
            let data = Module._malloc(2);
            Module.HEAPU16[data >> 1] = value.charCodeAt(0);
            if (destructors !== null) {
                destructors.push(Module._free, data);
            }
            return data;
        },
        argPackAdvance: 8,
        readValueFromPointer(pointer) {
            return this.fromWireType(Module.HEAPU32[((pointer)>>2)]);
        },
        destructorFunction(ptr) {
            Module._free(ptr);
        },
    });
}
// clang-format on
);

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Winvalid-pp-token"
EM_JS(void, jsRegisterString, (std::type_info const* raw),
// clang-format off
{
    Module.registerType(raw, {
        name: 'rtl::OUString',
        fromWireType(ptr) {
            let data = Module.HEAPU32[ptr >> 2];
            let length = Module.HEAPU32[(data >> 2) + 1];
            let buffer = data + 8;
            let str = '';
            for (let i = 0; i < length; ++i) {
                let c = Module.HEAPU16[(buffer >> 1) + i];
                str += String.fromCharCode(c);
            }
            Module.rtl_uString_release(data);
            Module._free(ptr);
            return str;
        },
        toWireType(destructors, value) {
            if (typeof value != 'string') {
                Module.throwBindingError('Cannot pass non-string to C++ OUString');
            }
            let data = Module._malloc(8 + (value.length + 1) * 2);
            Module.HEAPU32[data >> 2] = 1;
            Module.HEAPU32[(data >> 2) + 1] = value.length;
            let buffer = data + 8;
            for (let i = 0; i < value.length; ++i) {
                Module.HEAPU16[(buffer >> 1) + i] = value.charCodeAt(i);
            }
            Module.HEAPU16[(buffer >> 1) + value.length] = 0;
            let ptr = Module._malloc(4);
            Module.HEAPU32[ptr >> 2] = data;
            if (destructors !== null) {
                destructors.push(Module._free, ptr);
            }
            return ptr;
        },
        argPackAdvance: 8,
        readValueFromPointer(pointer) {
            return this.fromWireType(Module.HEAPU32[((pointer)>>2)]);
        },
        destructorFunction(ptr) {
            Module._free(ptr);
        },
    });
}
// clang-format on
);
#pragma clang diagnostic pop

namespace
{
void copyStruct(typelib_CompoundTypeDescription* desc, void const* source, void* dest)
{
    if (desc->pBaseTypeDescription != nullptr)
    {
        copyStruct(desc->pBaseTypeDescription, source, dest);
    }
    for (sal_Int32 i = 0; i != desc->nMembers; ++i)
    {
        uno_type_copyData(
            static_cast<char*>(dest) + desc->pMemberOffsets[i],
            const_cast<char*>(static_cast<char const*>(source) + desc->pMemberOffsets[i]),
            desc->ppTypeRefs[i], cpp_acquire);
    }
}

Reference<css::frame::XModel> getCurrentModelFromViewSh()
{
    SfxViewShell* pSh = nullptr;
    pSh = SfxViewShell::Current();
    if (!pSh)
    {
        return {};
    }
    return pSh->GetCurrentDocument();
}

struct LessType
{
    bool operator()(cpo::uno::Type const& type1, cpo::uno::Type const& type2) const
    {
        return type1.getTypeLibType() < type2.getTypeLibType();
    }
};

std::map<cpo::uno::Type, std::type_info const*, LessType> unoTypes;

std::type_info const* getTypeId(cpo::uno::Type const& type)
{
    auto const i = unoTypes.find(type);
    if (i == unoTypes.end())
    {
        throw std::runtime_error("unregistered UNO type");
    }
    return i->second;
}

Any constructAny(const cpo::uno::Type& rUnoType, const val& rObject)
{
    switch (rUnoType.getTypeClass())
    {
        case TypeClass_VOID:
            return {};
        case TypeClass_BOOLEAN:
            return Any{ rObject.as<bool>() };
        case TypeClass_BYTE:
            return Any{ rObject.as<sal_Int8>() };
        case TypeClass_SHORT:
            return Any{ rObject.as<sal_Int16>() };
        case TypeClass_UNSIGNED_SHORT:
            return Any{ rObject.as<sal_uInt16>() };
        case TypeClass_LONG:
            return Any{ rObject.as<sal_Int32>() };
        case TypeClass_UNSIGNED_LONG:
            return Any{ rObject.as<sal_uInt32>() };
        case TypeClass_HYPER:
            return Any{ rObject.as<sal_Int64>() };
        case TypeClass_UNSIGNED_HYPER:
            return Any{ rObject.as<sal_uInt64>() };
        case TypeClass_FLOAT:
            return Any{ rObject.as<float>() };
        case TypeClass_DOUBLE:
            return Any{ rObject.as<double>() };
        case TypeClass_CHAR:
            return Any{ rObject.as<char16_t>() };
        case TypeClass_STRING:
            return Any{ OUString(rObject.as<std::u16string>()) };
        case TypeClass_TYPE:
            return cpo::uno::Any(rObject.as<cpo::uno::Type>());
        case TypeClass_SEQUENCE:
        case TypeClass_STRUCT:
        case TypeClass_EXCEPTION:
        case TypeClass_INTERFACE:
        {
            emscripten::internal::EM_DESTRUCTORS destructors = nullptr;
            emscripten::internal::EM_GENERIC_WIRE_TYPE result
                = _emval_as(rObject.as_handle(), getTypeId(rUnoType), &destructors);
            emscripten::internal::DestructorsRunner dr(destructors);
            return cpo::uno::Any(emscripten::internal::fromGenericWireType<void const*>(result),
                                 rUnoType);
        }
        case TypeClass_ENUM:
        {
            emscripten::internal::EM_DESTRUCTORS destructors = nullptr;
            emscripten::internal::EM_GENERIC_WIRE_TYPE result
                = _emval_as(rObject.as_handle(), getTypeId(rUnoType), &destructors);
            emscripten::internal::DestructorsRunner dr(destructors);
            return cpo::uno::Any(
                &o3tl::temporary(emscripten::internal::fromGenericWireType<sal_Int32>(result)),
                rUnoType);
        }
        default:
            throw std::invalid_argument("bad type class");
    }
}
}

namespace unoembindhelpers::detail
{
void registerUnoType(cpo::uno::Type const& type, std::type_info const* id) { unoTypes[type] = id; }
}

EMSCRIPTEN_BINDINGS(PrimaryBindings)
{
    enum_<unoembindhelpers::uno_Sequence>("uno_Sequence")
        .value("FromSize", unoembindhelpers::uno_Sequence::FromSize);

    emscripten::class_<typelib_TypeDescriptionReference>("uno_Type")
        .smart_ptr<cpo::uno::Type>("uno_Type$")
        .class_function("Void", +[]() { return cppu::UnoType<void>::get(); })
        .class_function("Boolean", +[]() { return cppu::UnoType<bool>::get(); })
        .class_function("Byte", +[]() { return cppu::UnoType<sal_Int8>::get(); })
        .class_function("Short", +[]() { return cppu::UnoType<sal_Int16>::get(); })
        .class_function("UnsignedShort", +[]() { return cppu::UnoType<sal_uInt16>::get(); })
        .class_function("Long", +[]() { return cppu::UnoType<sal_Int32>::get(); })
        .class_function("UnsignedLong", +[]() { return cppu::UnoType<sal_uInt32>::get(); })
        .class_function("Hyper", +[]() { return cppu::UnoType<sal_Int64>::get(); })
        .class_function("UnsignedHyper", +[]() { return cppu::UnoType<sal_uInt64>::get(); })
        .class_function("Float", +[]() { return cppu::UnoType<float>::get(); })
        .class_function("Double", +[]() { return cppu::UnoType<double>::get(); })
        .class_function("Char", +[]() { return cppu::UnoType<sal_Unicode>::get(); })
        .class_function("String", +[]() { return cppu::UnoType<OUString>::get(); })
        .class_function("Type", +[]() { return cppu::UnoType<cpo::uno::Type>::get(); })
        .class_function("Any", +[]() { return cppu::UnoType<cpo::uno::Any>::get(); })
        .class_function("Sequence",
                        +[](cpo::uno::Type const& type) {
                            return cpo::uno::Type(cpo::uno::TypeClass_SEQUENCE,
                                                  "[]" + type.getTypeName());
                        })
        .class_function("Enum",
                        +[](std::u16string const& name) {
                            return cpo::uno::Type(cpo::uno::TypeClass_ENUM, OUString(name));
                        })
        .class_function("Struct",
                        +[](std::u16string const& name) {
                            return cpo::uno::Type(cpo::uno::TypeClass_STRUCT, OUString(name));
                        })
        .class_function("Exception",
                        +[](std::u16string const& name) {
                            return cpo::uno::Type(cpo::uno::TypeClass_EXCEPTION, OUString(name));
                        })
        .class_function("Interface",
                        +[](std::u16string const& name) {
                            return cpo::uno::Type(cpo::uno::TypeClass_INTERFACE, OUString(name));
                        })
        .function("getTypeClass", +[](cpo::uno::Type const& self) { return self.getTypeClass(); })
        .function(
            "getSequenceComponentType",
            +[](cpo::uno::Type const& self) {
                if (self.getTypeClass() != cpo::uno::TypeClass_SEQUENCE)
                {
                    throw std::invalid_argument("bad non-sequence type");
                }
                css::uno::TypeDescription desc;
                self.getDescription(reinterpret_cast<typelib_TypeDescription**>(&desc));
                if (!desc.is())
                {
                    throw std::invalid_argument("bad sequence type");
                }
                assert(desc.get()->eTypeClass == typelib_TypeClass_SEQUENCE);
                return cpo::uno::Type(
                    reinterpret_cast<typelib_IndirectTypeDescription const*>(desc.get())->pType);
            })
        .function("toString", +[](cpo::uno::Type const& self) {
            auto const name = self.getTypeName();
            return std::u16string(name.getStr(), name.getLength());
        });

    // Any
    class_<Any>("uno_Any")
        .constructor(&constructAny)
        .function("getType", &cpo::uno::Any::getValueType)
        .function("get", +[](cpo::uno::Any const& self) {
            switch (self.getValueTypeClass())
            {
                case cpo::uno::TypeClass_VOID:
                    return emscripten::val::undefined();
                case cpo::uno::TypeClass_BOOLEAN:
                    return emscripten::val(*o3tl::forceAccess<bool>(self));
                case cpo::uno::TypeClass_BYTE:
                    return emscripten::val(*o3tl::forceAccess<sal_Int8>(self));
                case cpo::uno::TypeClass_SHORT:
                    return emscripten::val(*o3tl::forceAccess<sal_Int16>(self));
                case cpo::uno::TypeClass_UNSIGNED_SHORT:
                    return emscripten::val(*o3tl::forceAccess<sal_uInt16>(self));
                case cpo::uno::TypeClass_LONG:
                    return emscripten::val(*o3tl::forceAccess<sal_Int32>(self));
                case cpo::uno::TypeClass_UNSIGNED_LONG:
                    return emscripten::val(*o3tl::forceAccess<sal_uInt32>(self));
                case cpo::uno::TypeClass_HYPER:
                    return emscripten::val(*o3tl::forceAccess<sal_Int64>(self));
                case cpo::uno::TypeClass_UNSIGNED_HYPER:
                    return emscripten::val(*o3tl::forceAccess<sal_uInt64>(self));
                case cpo::uno::TypeClass_FLOAT:
                    return emscripten::val(*o3tl::forceAccess<float>(self));
                case cpo::uno::TypeClass_DOUBLE:
                    return emscripten::val(*o3tl::forceAccess<double>(self));
                case cpo::uno::TypeClass_CHAR:
                    return emscripten::val(*o3tl::forceAccess<sal_Unicode>(self));
                case cpo::uno::TypeClass_STRING:
                    return emscripten::val(*o3tl::forceAccess<OUString>(self));
                case cpo::uno::TypeClass_TYPE:
                    return emscripten::val(*o3tl::forceAccess<cpo::uno::Type>(self));
                case cpo::uno::TypeClass_SEQUENCE:
                {
                    auto const seq = *static_cast<uno_Sequence* const*>(self.getValue());
                    auto const copy = std::malloc(sizeof(uno_Sequence*));
                    *static_cast<uno_Sequence**>(copy) = seq;
                    osl_atomic_increment(&seq->nRefCount);
                    emscripten::internal::WireTypePack argv(std::move(copy));
                    return emscripten::val::take_ownership(
                        _emval_take_value(getTypeId(self.getValueType()), argv));
                }
                case cpo::uno::TypeClass_ENUM:
                {
                    emscripten::internal::WireTypePack argv(
                        std::move(*static_cast<sal_Int32 const*>(self.getValue())));
                    return emscripten::val::take_ownership(
                        _emval_take_value(getTypeId(self.getValueType()), argv));
                }
                case cpo::uno::TypeClass_STRUCT:
                case cpo::uno::TypeClass_EXCEPTION:
                {
                    css::uno::TypeDescription desc(self.getValueType().getTypeLibType());
                    assert(desc.is());
                    auto const td = reinterpret_cast<typelib_CompoundTypeDescription*>(desc.get());
                    auto const copy = std::malloc(td->aBase.nSize);
                    copyStruct(td, self.getValue(), copy);
                    emscripten::internal::WireTypePack argv(std::move(copy));
                    return emscripten::val::take_ownership(
                        _emval_take_value(getTypeId(self.getValueType()), argv));
                }
                case cpo::uno::TypeClass_INTERFACE:
                {
                    auto const ifc = *static_cast<css::uno::XInterface* const*>(self.getValue());
                    auto const copy = std::malloc(sizeof(css::uno::XInterface*));
                    *static_cast<css::uno::XInterface**>(copy) = ifc;
                    if (ifc != nullptr)
                    {
                        ifc->acquire();
                    }
                    emscripten::internal::WireTypePack argv(std::move(copy));
                    return emscripten::val::take_ownership(
                        _emval_take_value(getTypeId(self.getValueType()), argv));
                }
                default:
                    O3TL_UNREACHABLE;
            };
        });

    function("getCurrentModelFromViewSh", &getCurrentModelFromViewSh);
    function("getUnoComponentContext", &comphelper::getProcessComponentContext);
    function("throwUnoException", +[](cpo::uno::Type const& type, emscripten::val const& value,
                                      emscripten::val const& toDelete) {
        auto const any = constructAny(type, value);
        auto const len = toDelete["length"].as<std::size_t>();
        for (std::size_t i = 0; i != len; ++i)
        {
            toDelete[i].call<void>("delete");
        }
        cppu::throwException(any);
    });
    function("sameUnoObject",
             +[](css::uno::Reference<css::uno::XInterface> const& ref1,
                 css::uno::Reference<css::uno::XInterface> const& ref2) { return ref1 == ref2; });
    function("rtl_uString_release",
             +[](std::uintptr_t ptr) { rtl_uString_release(reinterpret_cast<rtl_uString*>(ptr)); });
    function("getUnoExceptionFromCxaException", +[](std::uintptr_t ptr) {
        // Cf. __get_exception_message in <https://github.com/emscripten-core/emscripten/>,
        // system/lib/libcxxabi/src/cxa_exception_js_utils.cpp:
        auto const header = reinterpret_cast<__cxxabiv1::__cxa_exception const*>(ptr) - 1;
        cpo::uno::Any exc;
        OUString unoName(emscriptencxxabi::toUnoName(header->exceptionType->name()));
        typelib_TypeDescription* td = nullptr;
        typelib_typedescription_getByName(&td, unoName.pData);
        if (td == nullptr)
        {
            css::uno::RuntimeException e("exception type not found: " + unoName);
            uno_type_any_construct(
                &exc, &e, cppu::UnoType<css::uno::RuntimeException>::get().getTypeLibType(),
                cpp_acquire);
        }
        else
        {
            uno_any_construct(&exc, reinterpret_cast<void*>(ptr), td, cpp_acquire);
            typelib_typedescription_release(td);
        }
        return exc;
    });

    jsRegisterChar(&typeid(char16_t));
    jsRegisterString(&typeid(OUString));
}
#endif

/* vim:set shiftwidth=4 softtabstop=4 expandtab cinoptions=b1,g0,N-s cinkeys+=0=break: */
