/*============================================================================

The Medical Imaging Interaction Toolkit (MITK)

Copyright (c) German Cancer Research Center (DKFZ)
All rights reserved.

Use of this source code is governed by a 3-clause BSD license that can be
found in the LICENSE file.

============================================================================*/

#include <usModule.h>
#include <usModuleEvent.h>
#include <usModuleContext.h>
#include <usGetModuleContext.h>
#include <usLDAPProp.h>
#include <usServiceFindHook.h>
#include <usServiceEventListenerHook.h>
#include <usServiceListenerHook.h>
#include <usSharedLibrary.h>

#include "usTestingMacros.h"
#include "usTestingConfig.h"

using namespace us;

namespace {

#ifdef US_PLATFORM_WINDOWS
  static const std::string LIB_PATH = US_RUNTIME_OUTPUT_DIRECTORY;
#else
  static const std::string LIB_PATH = US_LIBRARY_OUTPUT_DIRECTORY;
#endif

class TestServiceListener
{
public:

  void ServiceChanged(const ServiceEvent serviceEvent)
  {
    this->events.push_back(serviceEvent);
  }

  std::vector<ServiceEvent> events;
};

class TestServiceEventListenerHook : public ServiceEventListenerHook
{
private:

  int id;

public:

  TestServiceEventListenerHook(int id)
  : id(id)
  {
  }

  typedef ShrinkableMap<ModuleContext*, ShrinkableVector<ServiceListenerHook::ListenerInfo> > MapType;

  void Event(const ServiceEvent& /*event*/, MapType& listeners) override
  {
    US_TEST_CONDITION_REQUIRED(listeners.size() > 0 && listeners.find(GetModuleContext()) != listeners.end(), "Check listener content");
    ShrinkableVector<ServiceListenerHook::ListenerInfo>& listenerInfos = listeners[GetModuleContext()];

    // listener count should be 2 because the event listener hooks are called with
    // the list of listeners before filtering them according to their LDAP filter
    if (id == 1)
    {
      US_TEST_CONDITION(listenerInfos.size() == 2, "2 service listeners expected");
      US_TEST_CONDITION(listenerInfos[0].IsRemoved() == false, "Listener is not removed");
      US_TEST_CONDITION(listenerInfos[1].IsRemoved() == false, "Listener is not removed");
      US_TEST_CONDITION(!(listenerInfos[0] == listenerInfos[1]), "listener info inequality");
    }
    else
    {
      // there is already one listener filtered out
      US_TEST_CONDITION(listenerInfos.size() == 1, "1 service listener expected");
      US_TEST_CONDITION(listenerInfos[0].IsRemoved() == false, "Listener is not removed");
    }
    if (listenerInfo.IsNull())
    {
      listenerInfo = listenerInfos[0];
    }
    else
    {
      US_TEST_CONDITION(listenerInfo == listenerInfos[0], "Equal listener info objects");
    }

    // Remove the listener without a filter from the list
    for(ShrinkableVector<ServiceListenerHook::ListenerInfo>::iterator infoIter = listenerInfos.begin();
        infoIter != listenerInfos.end();)
    {
      if (infoIter->GetFilter().empty())
      {
        infoIter = listenerInfos.erase(infoIter);
      }
      else
      {
        ++infoIter;
      }
    }
    US_TEST_CONDITION(listenerInfos.size() == 1, "One listener with LDAP filter should remain");

    ordering.push_back(id);
  }

  ServiceListenerHook::ListenerInfo listenerInfo;

  static std::vector<int> ordering;
};

std::vector<int> TestServiceEventListenerHook::ordering;


class TestServiceFindHook : public ServiceFindHook
{
private:
  int id;

public:

  TestServiceFindHook(int id)
    : id(id)
  {
  }

  void Find(const ModuleContext* context, const std::string& /*name*/,
            const std::string& /*filter*/, ShrinkableVector<ServiceReferenceBase>& references) override
  {
    US_TEST_CONDITION(context == GetModuleContext(), "Module context");

    references.clear();
    ordering.push_back(id);
  }

  static std::vector<int> ordering;
};

std::vector<int> TestServiceFindHook::ordering;

class TestServiceListenerHook : public ServiceListenerHook
{
private:
  int id;

public:

  TestServiceListenerHook(int id)
    : id(id)
  {
  }

  void Added(const std::vector<ListenerInfo>& listeners) override
  {
    for (std::vector<ListenerInfo>::const_iterator iter = listeners.begin();
         iter != listeners.end(); ++iter)
    {
      if (iter->IsRemoved() || iter->GetModuleContext() != GetModuleContext()) continue;
      listenerInfos.insert(*iter);
      lastAdded = listeners.back();
      ordering.push_back(id);
    }
  }

  void Removed(const std::vector<ListenerInfo>& listeners) override
  {
    for (std::vector<ListenerInfo>::const_iterator iter = listeners.begin();
         iter != listeners.end(); ++iter)
    {
      listenerInfos.erase(*iter);
      ordering.push_back(id*10);
    }
    lastRemoved = listeners.back();
  }

  static std::vector<int> ordering;

  std::unordered_set<ListenerInfo> listenerInfos;
  ListenerInfo lastAdded;
  ListenerInfo lastRemoved;
};

std::vector<int> TestServiceListenerHook::ordering;


void TestEventListenerHook()
{
  ModuleContext* context = GetModuleContext();

  TestServiceListener serviceListener1;
  TestServiceListener serviceListener2;
  context->AddServiceListener(&serviceListener1, &TestServiceListener::ServiceChanged);
  context->AddServiceListener(&serviceListener2, &TestServiceListener::ServiceChanged, LDAPProp(ServiceConstants::OBJECTCLASS()) == "bla");

  TestServiceEventListenerHook serviceEventListenerHook1(1);
  ServiceProperties hookProps1;
  hookProps1[ServiceConstants::SERVICE_RANKING()] = 10;
  ServiceRegistration<ServiceEventListenerHook> eventListenerHookReg1 =
      context->RegisterService<ServiceEventListenerHook>(&serviceEventListenerHook1, hookProps1);

  TestServiceEventListenerHook serviceEventListenerHook2(2);
  ServiceProperties hookProps2;
  hookProps2[ServiceConstants::SERVICE_RANKING()] = 0;
  ServiceRegistration<ServiceEventListenerHook> eventListenerHookReg2 =
      context->RegisterService<ServiceEventListenerHook>(&serviceEventListenerHook2, hookProps2);

  std::vector<int> expectedOrdering;
  expectedOrdering.push_back(1);
  expectedOrdering.push_back(1);
  expectedOrdering.push_back(2);
  US_TEST_CONDITION(serviceEventListenerHook1.ordering == expectedOrdering, "Event listener hook call order");

  US_TEST_CONDITION(serviceListener1.events.empty(), "service event of service event listener hook");
  US_TEST_CONDITION(serviceListener2.events.empty(), "no service event for filtered listener");

  SharedLibrary libA(LIB_PATH, "TestModuleA");
  try
  {
    libA.Load();
  }
  catch (const std::exception& e)
  {
    US_TEST_FAILED_MSG(<< "Load module exception: " << e.what())
  }

  expectedOrdering.push_back(1);
  expectedOrdering.push_back(2);
  US_TEST_CONDITION(serviceEventListenerHook1.ordering == expectedOrdering, "Event listener hook call order");

  libA.Unload();

  US_TEST_CONDITION(serviceListener1.events.empty(), "no service event due to service event listener hook");
  US_TEST_CONDITION(serviceListener2.events.empty(), "no service event for filtered listener due to service event listener hook");

  eventListenerHookReg2.Unregister();
  eventListenerHookReg1.Unregister();

  context->RemoveServiceListener(&serviceListener1, &TestServiceListener::ServiceChanged);
  context->RemoveServiceListener(&serviceListener2, &TestServiceListener::ServiceChanged);
}

void TestListenerHook()
{
  ModuleContext* context = GetModuleContext();

  TestServiceListener serviceListener1;
  TestServiceListener serviceListener2;
  context->AddServiceListener(&serviceListener1, &TestServiceListener::ServiceChanged);
  context->AddServiceListener(&serviceListener2, &TestServiceListener::ServiceChanged, LDAPProp(ServiceConstants::OBJECTCLASS()) == "bla");

  TestServiceListenerHook serviceListenerHook1(1);
  ServiceProperties hookProps1;
  hookProps1[ServiceConstants::SERVICE_RANKING()] = 0;
  ServiceRegistration<ServiceListenerHook> listenerHookReg1 =
      context->RegisterService<ServiceListenerHook>(&serviceListenerHook1, hookProps1);

  TestServiceListenerHook serviceListenerHook2(2);
  ServiceProperties hookProps2;
  hookProps2[ServiceConstants::SERVICE_RANKING()] = 10;
  ServiceRegistration<ServiceListenerHook> listenerHookReg2 =
      context->RegisterService<ServiceListenerHook>(&serviceListenerHook2, hookProps2);

  // check if hooks got notified about the existing listeners
  US_TEST_CONDITION_REQUIRED(serviceListenerHook1.listenerInfos.size() == 2, "Notification about existing listeners")
  const std::size_t listenerInfoSizeOld = serviceListenerHook1.listenerInfos.size() - 2;

  context->AddServiceListener(&serviceListener1, &TestServiceListener::ServiceChanged);
  ServiceListenerHook::ListenerInfo lastAdded = serviceListenerHook1.lastAdded;

  std::vector<int> expectedOrdering;
  expectedOrdering.push_back(1);
  expectedOrdering.push_back(1);
  expectedOrdering.push_back(2);
  expectedOrdering.push_back(2);
  expectedOrdering.push_back(20);
  expectedOrdering.push_back(10);
  expectedOrdering.push_back(2);
  expectedOrdering.push_back(1);
  US_TEST_CONDITION(serviceListenerHook1.ordering == expectedOrdering, "Listener hook call order");

  context->AddServiceListener(&serviceListener1, &TestServiceListener::ServiceChanged, LDAPProp(ServiceConstants::OBJECTCLASS()) == "blub");
  US_TEST_CONDITION(lastAdded == serviceListenerHook1.lastRemoved, "Same ListenerInfo object)");
  US_TEST_CONDITION(!(lastAdded == serviceListenerHook1.lastAdded), "New ListenerInfo object)");

  expectedOrdering.push_back(20);
  expectedOrdering.push_back(10);
  expectedOrdering.push_back(2);
  expectedOrdering.push_back(1);
  US_TEST_CONDITION(serviceListenerHook1.ordering == expectedOrdering, "Listener hook call order");

  context->RemoveServiceListener(&serviceListener1, &TestServiceListener::ServiceChanged);
  context->RemoveServiceListener(&serviceListener2, &TestServiceListener::ServiceChanged);

  expectedOrdering.push_back(20);
  expectedOrdering.push_back(10);
  expectedOrdering.push_back(20);
  expectedOrdering.push_back(10);
  US_TEST_CONDITION(serviceListenerHook1.ordering == expectedOrdering, "Listener hook call order");

  US_TEST_CONDITION_REQUIRED(serviceListenerHook1.listenerInfos.size() == listenerInfoSizeOld, "Removed listener infos")

  listenerHookReg2.Unregister();
  listenerHookReg1.Unregister();
}

void TestFindHook()
{
  ModuleContext* context = GetModuleContext();

  TestServiceFindHook serviceFindHook1(1);
  ServiceProperties hookProps1;
  hookProps1[ServiceConstants::SERVICE_RANKING()] = 0;
  ServiceRegistration<ServiceFindHook> findHookReg1 =
      context->RegisterService<ServiceFindHook>(&serviceFindHook1, hookProps1);

  TestServiceFindHook serviceFindHook2(2);
  ServiceProperties hookProps2;
  hookProps2[ServiceConstants::SERVICE_RANKING()] = 10;
  ServiceRegistration<ServiceFindHook> findHookReg2 =
      context->RegisterService<ServiceFindHook>(&serviceFindHook2, hookProps2);

  std::vector<int> expectedOrdering;
  US_TEST_CONDITION(serviceFindHook1.ordering == expectedOrdering, "Find hook call order");

  TestServiceListener serviceListener;
  context->AddServiceListener(&serviceListener, &TestServiceListener::ServiceChanged);

  SharedLibrary libA(LIB_PATH, "TestModuleA");
  try
  {
    libA.Load();
  }
  catch (const std::exception& e)
  {
    US_TEST_FAILED_MSG(<< "Load module exception: " << e.what())
  }

  US_TEST_CONDITION(serviceListener.events.size() == 1, "Service registered");

  std::vector<ServiceReferenceU> refs = context->GetServiceReferences("us::TestModuleAService");
  US_TEST_CONDITION(refs.empty(), "Empty references");
  ServiceReferenceU ref = context->GetServiceReference("us::TestModuleAService");
  US_TEST_CONDITION(!ref, "Invalid reference (filtered out)");

  expectedOrdering.push_back(2);
  expectedOrdering.push_back(1);
  expectedOrdering.push_back(2);
  expectedOrdering.push_back(1);

  US_TEST_CONDITION(serviceFindHook1.ordering == expectedOrdering, "Find hook call order");

  findHookReg2.Unregister();
  findHookReg1.Unregister();

  refs = context->GetServiceReferences("us::TestModuleAService");
  US_TEST_CONDITION(!refs.empty(), "Non-empty references");
  ref = context->GetServiceReference("us::TestModuleAService");
  US_TEST_CONDITION(ref, "Valid reference");

  libA.Unload();

  context->RemoveServiceListener(&serviceListener, &TestServiceListener::ServiceChanged);
}

} // end unnamed namespace

int usServiceHooksTest(int /*argc*/, char* /*argv*/[])
{
  US_TEST_BEGIN("ServiceHooksTest");

  TestListenerHook();
  TestFindHook();
  TestEventListenerHook();

  US_TEST_END()
}
