/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
 * This file is part of the LibreOffice 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/.
 *
 * This file incorporates work covered by the following license notice:
 *
 *   Licensed to the Apache Software Foundation (ASF) under one or more
 *   contributor license agreements. See the NOTICE file distributed
 *   with this work for additional information regarding copyright
 *   ownership. The ASF licenses this file to you under the Apache
 *   License, Version 2.0 (the "License"); you may not use this file
 *   except in compliance with the License. You may obtain a copy of
 *   the License at http://www.apache.org/licenses/LICENSE-2.0 .
 */

#include <helper/mischelper.hxx>
#include <com/sun/star/lang/IllegalArgumentException.hpp>
#include <com/sun/star/lang/XComponent.hpp>
#include <com/sun/star/lang/XEventListener.hpp>
#include <com/sun/star/lang/XServiceInfo.hpp>
#include <com/sun/star/ui/XContextChangeEventMultiplexer.hpp>
#include <com/sun/star/ui/ContextChangeEventMultiplexer.hpp>
#include <com/sun/star/uno/XComponentContext.hpp>

#include <comphelper/compbase.hxx>
#include <cppuhelper/supportsservice.hxx>
#include <rtl/ref.hxx>
#include <osl/diagnose.h>

#include <algorithm>
#include <map>
#include <vector>

using namespace css;
using namespace css::uno;

namespace {

typedef comphelper::WeakComponentImplHelper <
    css::ui::XContextChangeEventMultiplexer,
    css::lang::XServiceInfo,
    css::lang::XEventListener
    > ContextChangeEventMultiplexerInterfaceBase;

class ContextChangeEventMultiplexer
    : public ContextChangeEventMultiplexerInterfaceBase
{
public:
    ContextChangeEventMultiplexer();
    ContextChangeEventMultiplexer(const ContextChangeEventMultiplexer&) = delete;
    ContextChangeEventMultiplexer& operator=(const ContextChangeEventMultiplexer&) = delete;

    virtual void disposing(std::unique_lock<std::mutex>&) override;

    // XContextChangeEventMultiplexer
    virtual void SAL_CALL addContextChangeEventListener (
        const css::uno::Reference<css::ui::XContextChangeEventListener>& rxListener,
        const css::uno::Reference<css::uno::XInterface>& rxEventFocus) override;
    virtual void SAL_CALL removeContextChangeEventListener (
        const css::uno::Reference<css::ui::XContextChangeEventListener>& rxListener,
        const css::uno::Reference<css::uno::XInterface>& rxEventFocus) override;
    virtual void SAL_CALL removeAllContextChangeEventListeners (
        const css::uno::Reference<css::ui::XContextChangeEventListener>& rxListener) override;
    virtual void SAL_CALL broadcastContextChangeEvent (
        const css::ui::ContextChangeEventObject& rContextChangeEventObject,
        const css::uno::Reference<css::uno::XInterface>& rxEventFocus) override;

    // XServiceInfo
    virtual OUString SAL_CALL getImplementationName() override;
    virtual sal_Bool SAL_CALL supportsService  (
        const OUString& rsServiceName) override;
    virtual css::uno::Sequence< OUString> SAL_CALL getSupportedServiceNames() override;

    // XEventListener
    virtual void SAL_CALL disposing (
        const css::lang::EventObject& rEvent) override;

    typedef ::std::vector<css::uno::Reference<css::ui::XContextChangeEventListener> > ListenerContainer;
    class FocusDescriptor
    {
    public:
        ListenerContainer maListeners;
        OUString msCurrentApplicationName;
        OUString msCurrentContextName;
    };
    typedef ::std::map<css::uno::Reference<css::uno::XInterface>, FocusDescriptor> ListenerMap;
    ListenerMap maListeners;

    /** Notify all listeners in the container that is associated with
        the given event focus.

        Typically called twice from broadcastEvent(), once for the
        given event focus and once for NULL.
    */
    void BroadcastEventToSingleContainer (
        const css::ui::ContextChangeEventObject& rEventObject,
        const css::uno::Reference<css::uno::XInterface>& rxEventFocus);
    FocusDescriptor* GetFocusDescriptor (
        const css::uno::Reference<css::uno::XInterface>& rxEventFocus,
        const bool bCreateWhenMissing);
};

ContextChangeEventMultiplexer::ContextChangeEventMultiplexer()
{
}

void ContextChangeEventMultiplexer::disposing(std::unique_lock<std::mutex>& rGuard)
{
    ListenerMap aListeners;
    aListeners.swap(maListeners);

    rGuard.unlock();

    css::uno::Reference<css::uno::XInterface> xThis (static_cast<XWeak*>(this));
    css::lang::EventObject aEvent (xThis);
    for (auto const& container : aListeners)
    {
        // Unregister from the focus object.
        Reference<lang::XComponent> xComponent (container.first, UNO_QUERY);
        if (xComponent.is())
            xComponent->removeEventListener(this);

        // Tell all listeners that we are being disposed.
        const FocusDescriptor& rFocusDescriptor (container.second);
        for (auto const& listener : rFocusDescriptor.maListeners)
        {
            listener->disposing(aEvent);
        }
    }
}

// XContextChangeEventMultiplexer
void SAL_CALL ContextChangeEventMultiplexer::addContextChangeEventListener (
    const css::uno::Reference<css::ui::XContextChangeEventListener>& rxListener,
    const css::uno::Reference<css::uno::XInterface>& rxEventFocus)
{
    if ( ! rxListener.is())
        throw css::lang::IllegalArgumentException(
            u"can not add an empty reference"_ustr,
            static_cast<XWeak*>(this),
            0);

    FocusDescriptor* pFocusDescriptor = GetFocusDescriptor(rxEventFocus, true);
    if (pFocusDescriptor != nullptr)
    {
        ListenerContainer& rContainer (pFocusDescriptor->maListeners);
        if (::std::find(rContainer.begin(), rContainer.end(), rxListener) != rContainer.end())
        {
            // The listener was added for the same event focus
            // previously.  That is an error.
            throw css::lang::IllegalArgumentException(u"listener added twice"_ustr, static_cast<XWeak*>(this), 0);
        }
        rContainer.push_back(rxListener);
    }

    // Send out an initial event that informs the new listener about
    // the current context.
    if (!(rxEventFocus.is() && pFocusDescriptor!=nullptr))
        return;

    css::ui::ContextChangeEventObject aEvent (
                nullptr,
                pFocusDescriptor->msCurrentApplicationName,
                pFocusDescriptor->msCurrentContextName);
    rxListener->notifyContextChangeEvent(aEvent);
}

void SAL_CALL ContextChangeEventMultiplexer::removeContextChangeEventListener (
    const css::uno::Reference<css::ui::XContextChangeEventListener>& rxListener,
    const css::uno::Reference<css::uno::XInterface>& rxEventFocus)
{
    if ( ! rxListener.is())
        throw css::lang::IllegalArgumentException(
            u"can not remove an empty reference"_ustr,
            static_cast<XWeak*>(this), 0);

    FocusDescriptor* pFocusDescriptor = GetFocusDescriptor(rxEventFocus, false);
    if (pFocusDescriptor == nullptr)
        return;

    ListenerContainer& rContainer (pFocusDescriptor->maListeners);
    const ListenerContainer::iterator iListener (
        ::std::find(rContainer.begin(), rContainer.end(), rxListener));
    if (iListener != rContainer.end())
    {
        rContainer.erase(iListener);

        // We hold on to the focus descriptor even when the last listener has been removed.
        // This allows us to keep track of the current context and send it to new listeners.
    }

}

void SAL_CALL ContextChangeEventMultiplexer::removeAllContextChangeEventListeners (
    const css::uno::Reference<css::ui::XContextChangeEventListener>& rxListener)
{
    if ( ! rxListener.is())
        throw css::lang::IllegalArgumentException(
            u"can not remove an empty reference"_ustr,
            static_cast<XWeak*>(this), 0);

    for (auto& rContainer : maListeners)
    {
        const ListenerContainer::iterator iListener (
            ::std::find(rContainer.second.maListeners.begin(), rContainer.second.maListeners.end(), rxListener));
        if (iListener != rContainer.second.maListeners.end())
        {
            rContainer.second.maListeners.erase(iListener);

            // We hold on to the focus descriptor even when the last listener has been removed.
            // This allows us to keep track of the current context and send it to new listeners.
        }
    }
}

void SAL_CALL ContextChangeEventMultiplexer::broadcastContextChangeEvent (
    const css::ui::ContextChangeEventObject& rEventObject,
    const css::uno::Reference<css::uno::XInterface>& rxEventFocus)
{
    // Remember the current context.
    if (rxEventFocus.is())
    {
        FocusDescriptor* pFocusDescriptor = GetFocusDescriptor(rxEventFocus, true);
        if (pFocusDescriptor != nullptr)
        {
            pFocusDescriptor->msCurrentApplicationName = rEventObject.ApplicationName;
            pFocusDescriptor->msCurrentContextName = rEventObject.ContextName;
        }
    }

    BroadcastEventToSingleContainer(rEventObject, rxEventFocus);
    if (rxEventFocus.is())
        BroadcastEventToSingleContainer(rEventObject, nullptr);
}

void ContextChangeEventMultiplexer::BroadcastEventToSingleContainer (
    const css::ui::ContextChangeEventObject& rEventObject,
    const css::uno::Reference<css::uno::XInterface>& rxEventFocus)
{
    FocusDescriptor* pFocusDescriptor = GetFocusDescriptor(rxEventFocus, false);
    if (pFocusDescriptor != nullptr)
    {
        // Create a copy of the listener container to avoid problems
        // when one of the called listeners calls add... or remove...
        ListenerContainer aContainer (pFocusDescriptor->maListeners);
        for (auto const& listener : aContainer)
        {
            listener->notifyContextChangeEvent(rEventObject);
        }
    }
}

ContextChangeEventMultiplexer::FocusDescriptor* ContextChangeEventMultiplexer::GetFocusDescriptor (
    const css::uno::Reference<css::uno::XInterface>& rxEventFocus,
    const bool bCreateWhenMissing)
{
    ListenerMap::iterator iDescriptor (maListeners.find(rxEventFocus));
    if (iDescriptor == maListeners.end() && bCreateWhenMissing)
    {
        // Listen for the focus being disposed.
        Reference<lang::XComponent> xComponent (rxEventFocus, UNO_QUERY);
        if (xComponent.is())
            xComponent->addEventListener(this);

        // Create a new listener container for the event focus.
        iDescriptor = maListeners.emplace(
                rxEventFocus,
                FocusDescriptor()).first;
    }
    if (iDescriptor != maListeners.end())
        return &iDescriptor->second;
    else
        return nullptr;
}

OUString SAL_CALL ContextChangeEventMultiplexer::getImplementationName()
{
    return u"org.apache.openoffice.comp.framework.ContextChangeEventMultiplexer"_ustr;
}

sal_Bool SAL_CALL ContextChangeEventMultiplexer::supportsService ( const OUString& rsServiceName)
{
    return cppu::supportsService(this, rsServiceName);
}

css::uno::Sequence<OUString> SAL_CALL ContextChangeEventMultiplexer::getSupportedServiceNames()
{
    // it's a singleton, not a service
    return css::uno::Sequence<OUString>();
}

void SAL_CALL ContextChangeEventMultiplexer::disposing ( const css::lang::EventObject& rEvent)
{
    ListenerMap::iterator iDescriptor (maListeners.find(rEvent.Source));

    if (iDescriptor == maListeners.end())
    {
        OSL_ASSERT(iDescriptor != maListeners.end());
        return;
    }

    // Should we notify the remaining listeners?

    maListeners.erase(iDescriptor);
}

}

namespace framework {

// right now we assume there's one matching listener
static uno::Reference<ui::XContextChangeEventListener> GetFirstListenerWith_ImplImpl(
    css::uno::Reference<css::uno::XComponentContext> const & xComponentContext,
    uno::Reference<uno::XInterface> const& xEventFocus,
    std::function<bool (uno::Reference<ui::XContextChangeEventListener> const&)> const& rPredicate)
{
    assert(xEventFocus.is()); // in current usage it's a bug if the XController is null here
    uno::Reference<ui::XContextChangeEventListener> xRet;

    rtl::Reference<ContextChangeEventMultiplexer> pMultiplexer =
        // [-loplugin:unocast]
        dynamic_cast<ContextChangeEventMultiplexer *>(ui::ContextChangeEventMultiplexer::get(xComponentContext).get());
    assert(pMultiplexer);

    ContextChangeEventMultiplexer::FocusDescriptor const*const pFocusDescriptor(
        pMultiplexer->GetFocusDescriptor(xEventFocus, false));
    if (!pFocusDescriptor)
        return xRet;

    for (auto & xListener : pFocusDescriptor->maListeners)
    {
        if (rPredicate(xListener))
        {
            assert(!xRet.is()); // generalize this if it is used for more than 1:1 mapping?
            xRet = xListener;
        }
    }
    return xRet;
}

namespace {

struct Hook
{
    Hook() { g_pGetMultiplexerListener = &GetFirstListenerWith_ImplImpl; }
    ~Hook() { g_pGetMultiplexerListener = nullptr; }
};

Hook g_hook;

}

}

extern "C" SAL_DLLPUBLIC_EXPORT css::uno::XInterface *
org_apache_openoffice_comp_framework_ContextChangeEventMultiplexer_get_implementation(
    css::uno::XComponentContext *,
    css::uno::Sequence<css::uno::Any> const &)
{
    return cppu::acquire(new ContextChangeEventMultiplexer());
}

/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
