/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */

#include "mozilla/dom/GamepadManager.h"

#include "mozilla/dom/Gamepad.h"
#include "mozilla/dom/GamepadAxisMoveEvent.h"
#include "mozilla/dom/GamepadButtonEvent.h"
#include "mozilla/dom/GamepadEvent.h"
#include "mozilla/dom/GamepadEventChannelChild.h"
#include "mozilla/dom/GamepadMonitoring.h"

#include "mozilla/ipc/BackgroundChild.h"
#include "mozilla/ipc/PBackgroundChild.h"
#include "mozilla/ClearOnShutdown.h"
#include "mozilla/Preferences.h"
#include "mozilla/StaticPtr.h"

#include "nsAutoPtr.h"
#include "nsGlobalWindow.h"
#include "nsIDOMEvent.h"
#include "nsIDOMDocument.h"
#include "nsIDOMWindow.h"
#include "nsIObserver.h"
#include "nsIObserverService.h"
#include "nsIServiceManager.h"
#include "nsThreadUtils.h"
#include "mozilla/Services.h"
#include "mozilla/Unused.h"

#include <cstddef>

using namespace mozilla::ipc;

namespace mozilla {
namespace dom {

namespace {

const char* kGamepadEnabledPref = "dom.gamepad.enabled";
const char* kGamepadEventsEnabledPref =
  "dom.gamepad.non_standard_events.enabled";

const nsTArray<RefPtr<nsGlobalWindow>>::index_type NoIndex =
  nsTArray<RefPtr<nsGlobalWindow>>::NoIndex;

bool sShutdown = false;

StaticRefPtr<GamepadManager> gGamepadManagerSingleton;
const uint32_t VR_GAMEPAD_IDX_OFFSET = 0x01 << 16;

} // namespace

NS_IMPL_ISUPPORTS(GamepadManager, nsIObserver)

GamepadManager::GamepadManager()
  : mEnabled(false),
    mNonstandardEventsEnabled(false),
    mShuttingDown(false)
{}

nsresult
GamepadManager::Init()
{
  mEnabled = IsAPIEnabled();
  mNonstandardEventsEnabled =
    Preferences::GetBool(kGamepadEventsEnabledPref, false);
  nsCOMPtr<nsIObserverService> observerService =
    mozilla::services::GetObserverService();

  if (NS_WARN_IF(!observerService)) {
    return NS_ERROR_FAILURE;
  }

  nsresult rv;
  rv = observerService->AddObserver(this,
                                    NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID,
                                    false);

  if (NS_WARN_IF(NS_FAILED(rv))) {
    return rv;
  }

  return NS_OK;
}

NS_IMETHODIMP
GamepadManager::Observe(nsISupports* aSubject,
                        const char* aTopic,
                        const char16_t* aData)
{
  nsCOMPtr<nsIObserverService> observerService =
    mozilla::services::GetObserverService();
  if (observerService) {
    observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID);
  }
  BeginShutdown();
  return NS_OK;
}

void
GamepadManager::StopMonitoring()
{
  for (uint32_t i = 0; i < mChannelChildren.Length(); ++i) {
    mChannelChildren[i]->SendGamepadListenerRemoved();
  }
  mChannelChildren.Clear();
  mGamepads.Clear();
}

void
GamepadManager::BeginShutdown()
{
  mShuttingDown = true;
  StopMonitoring();
  // Don't let windows call back to unregister during shutdown
  for (uint32_t i = 0; i < mListeners.Length(); i++) {
    mListeners[i]->SetHasGamepadEventListener(false);
  }
  mListeners.Clear();
  sShutdown = true;
}

void
GamepadManager::AddListener(nsGlobalWindow* aWindow)
{
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsInnerWindow());
  MOZ_ASSERT(NS_IsMainThread());

  if (!mEnabled || mShuttingDown) {
    return;
  }

  if (mListeners.IndexOf(aWindow) != NoIndex) {
    return; // already exists
  }

  mListeners.AppendElement(aWindow);

  // IPDL child has been created
  if (!mChannelChildren.IsEmpty()) {
    return;
  }

  PBackgroundChild *actor = BackgroundChild::GetForCurrentThread();
  //Try to get the PBackground Child actor
  if (actor) {
    ActorCreated(actor);
  } else {
    Unused << BackgroundChild::GetOrCreateForCurrentThread(this);
  }
}

void
GamepadManager::RemoveListener(nsGlobalWindow* aWindow)
{
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsInnerWindow());

  if (mShuttingDown) {
    // Doesn't matter at this point. It's possible we're being called
    // as a result of our own destructor here, so just bail out.
    return;
  }

  if (mListeners.IndexOf(aWindow) == NoIndex) {
    return; // doesn't exist
  }

  for (auto iter = mGamepads.Iter(); !iter.Done(); iter.Next()) {
      aWindow->RemoveGamepad(iter.Key());
  }

  mListeners.RemoveElement(aWindow);

  if (mListeners.IsEmpty()) {
    StopMonitoring();
  }
}

already_AddRefed<Gamepad>
GamepadManager::GetGamepad(uint32_t aIndex) const
{
  RefPtr<Gamepad> gamepad;
  if (mGamepads.Get(aIndex, getter_AddRefs(gamepad))) {
    return gamepad.forget();
  }

  return nullptr;
}

uint32_t GamepadManager::GetGamepadIndexWithServiceType(uint32_t aIndex,
                                                        GamepadServiceType aServiceType)
{
  uint32_t newIndex = 0;

  switch (aServiceType) {
    case GamepadServiceType::Standard:
    {
     MOZ_ASSERT(aIndex <= VR_GAMEPAD_IDX_OFFSET);
     newIndex = aIndex;
     break;
    }
    default:
     MOZ_ASSERT(false);
     break;
  }

  return newIndex;
}

void
GamepadManager::AddGamepad(uint32_t aIndex,
                           const nsAString& aId,
                           GamepadMappingType aMapping,
                           GamepadServiceType aServiceType,
                           uint32_t aNumButtons,
                           uint32_t aNumAxes)
{
  //TODO: bug 852258: get initial button/axis state
  RefPtr<Gamepad> gamepad =
    new Gamepad(nullptr,
                aId,
                0, // index is set by global window
                aMapping,
                aNumButtons,
                aNumAxes);

  uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);

  // We store the gamepad related to its index given by the parent process,
  // and no duplicate index is allowed.
  MOZ_ASSERT(!mGamepads.Get(newIndex, nullptr));
  mGamepads.Put(newIndex, gamepad);
  NewConnectionEvent(newIndex, true);
}

void
GamepadManager::RemoveGamepad(uint32_t aIndex, GamepadServiceType aServiceType)
{
  uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);

  RefPtr<Gamepad> gamepad = GetGamepad(newIndex);
  if (!gamepad) {
    NS_WARNING("Trying to delete gamepad with invalid index");
    return;
  }
  gamepad->SetConnected(false);
  NewConnectionEvent(newIndex, false);
  mGamepads.Remove(newIndex);
}

void
GamepadManager::NewButtonEvent(uint32_t aIndex, GamepadServiceType aServiceType,
                               uint32_t aButton, bool aPressed, double aValue)
{
  if (mShuttingDown) {
    return;
  }

  uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);

  RefPtr<Gamepad> gamepad = GetGamepad(newIndex);
  if (!gamepad) {
    return;
  }

  gamepad->SetButton(aButton, aPressed, aValue);

  // Hold on to listeners in a separate array because firing events
  // can mutate the mListeners array.
  nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
  MOZ_ASSERT(!listeners.IsEmpty());

  for (uint32_t i = 0; i < listeners.Length(); i++) {

    MOZ_ASSERT(listeners[i]->IsInnerWindow());

    // Only send events to non-background windows
    if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
        listeners[i]->GetOuterWindow()->IsBackground()) {
      continue;
    }

    bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex);

    RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex);
    if (listenerGamepad) {
      listenerGamepad->SetButton(aButton, aPressed, aValue);
      if (firstTime) {
        FireConnectionEvent(listeners[i], listenerGamepad, true);
      }
      if (mNonstandardEventsEnabled) {
        // Fire event
        FireButtonEvent(listeners[i], listenerGamepad, aButton, aValue);
      }
    }
  }
}

void
GamepadManager::FireButtonEvent(EventTarget* aTarget,
                                Gamepad* aGamepad,
                                uint32_t aButton,
                                double aValue)
{
  nsString name = aValue == 1.0L ? NS_LITERAL_STRING("gamepadbuttondown") :
                                   NS_LITERAL_STRING("gamepadbuttonup");
  GamepadButtonEventInit init;
  init.mBubbles = false;
  init.mCancelable = false;
  init.mGamepad = aGamepad;
  init.mButton = aButton;
  RefPtr<GamepadButtonEvent> event =
    GamepadButtonEvent::Constructor(aTarget, name, init);

  event->SetTrusted(true);

  bool defaultActionEnabled = true;
  aTarget->DispatchEvent(event, &defaultActionEnabled);
}

void
GamepadManager::NewAxisMoveEvent(uint32_t aIndex, GamepadServiceType aServiceType,
                                 uint32_t aAxis, double aValue)
{
  if (mShuttingDown) {
    return;
  }

  uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);

  RefPtr<Gamepad> gamepad = GetGamepad(newIndex);
  if (!gamepad) {
    return;
  }
  gamepad->SetAxis(aAxis, aValue);

  // Hold on to listeners in a separate array because firing events
  // can mutate the mListeners array.
  nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
  MOZ_ASSERT(!listeners.IsEmpty());

  for (uint32_t i = 0; i < listeners.Length(); i++) {

    MOZ_ASSERT(listeners[i]->IsInnerWindow());

    // Only send events to non-background windows
    if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
        listeners[i]->GetOuterWindow()->IsBackground()) {
      continue;
    }

    bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex);

    RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex);
    if (listenerGamepad) {
      listenerGamepad->SetAxis(aAxis, aValue);
      if (firstTime) {
        FireConnectionEvent(listeners[i], listenerGamepad, true);
      }
      if (mNonstandardEventsEnabled) {
        // Fire event
        FireAxisMoveEvent(listeners[i], listenerGamepad, aAxis, aValue);
      }
    }
  }
}

void
GamepadManager::FireAxisMoveEvent(EventTarget* aTarget,
                                  Gamepad* aGamepad,
                                  uint32_t aAxis,
                                  double aValue)
{
  GamepadAxisMoveEventInit init;
  init.mBubbles = false;
  init.mCancelable = false;
  init.mGamepad = aGamepad;
  init.mAxis = aAxis;
  init.mValue = aValue;
  RefPtr<GamepadAxisMoveEvent> event =
    GamepadAxisMoveEvent::Constructor(aTarget,
                                      NS_LITERAL_STRING("gamepadaxismove"),
                                      init);

  event->SetTrusted(true);

  bool defaultActionEnabled = true;
  aTarget->DispatchEvent(event, &defaultActionEnabled);
}

void
GamepadManager::NewPoseEvent(uint32_t aIndex, GamepadServiceType aServiceType,
                             const GamepadPoseState& aPose)
{
  if (mShuttingDown) {
    return;
  }

  uint32_t newIndex = GetGamepadIndexWithServiceType(aIndex, aServiceType);

  RefPtr<Gamepad> gamepad = GetGamepad(newIndex);
  if (!gamepad) {
    return;
  }
  gamepad->SetPose(aPose);

  // Hold on to listeners in a separate array because firing events
  // can mutate the mListeners array.
  nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
  MOZ_ASSERT(!listeners.IsEmpty());

  for (uint32_t i = 0; i < listeners.Length(); i++) {

    MOZ_ASSERT(listeners[i]->IsInnerWindow());

    // Only send events to non-background windows
    if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
        listeners[i]->GetOuterWindow()->IsBackground()) {
      continue;
    }

    bool firstTime = MaybeWindowHasSeenGamepad(listeners[i], newIndex);

    RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(newIndex);
    if (listenerGamepad) {
      listenerGamepad->SetPose(aPose);
      if (firstTime) {
        FireConnectionEvent(listeners[i], listenerGamepad, true);
      }
    }
  }
}

void
GamepadManager::NewConnectionEvent(uint32_t aIndex, bool aConnected)
{
  if (mShuttingDown) {
    return;
  }

  RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
  if (!gamepad) {
    return;
  }

  // Hold on to listeners in a separate array because firing events
  // can mutate the mListeners array.
  nsTArray<RefPtr<nsGlobalWindow>> listeners(mListeners);
  MOZ_ASSERT(!listeners.IsEmpty());

  if (aConnected) {
    for (uint32_t i = 0; i < listeners.Length(); i++) {

      MOZ_ASSERT(listeners[i]->IsInnerWindow());

      // Only send events to non-background windows
      if (!listeners[i]->AsInner()->IsCurrentInnerWindow() ||
          listeners[i]->GetOuterWindow()->IsBackground()) {
        continue;
      }

      // We don't fire a connected event here unless the window
      // has seen input from at least one device.
      if (!listeners[i]->HasSeenGamepadInput()) {
        continue;
      }

      SetWindowHasSeenGamepad(listeners[i], aIndex);

      RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aIndex);
      if (listenerGamepad) {
        // Fire event
        FireConnectionEvent(listeners[i], listenerGamepad, aConnected);
      }
    }
  } else {
    // For disconnection events, fire one at every window that has received
    // data from this gamepad.
    for (uint32_t i = 0; i < listeners.Length(); i++) {

      // Even background windows get these events, so we don't have to
      // deal with the hassle of syncing the state of removed gamepads.

      if (WindowHasSeenGamepad(listeners[i], aIndex)) {
        RefPtr<Gamepad> listenerGamepad = listeners[i]->GetGamepad(aIndex);
        if (listenerGamepad) {
          listenerGamepad->SetConnected(false);
          // Fire event
          FireConnectionEvent(listeners[i], listenerGamepad, false);
          listeners[i]->RemoveGamepad(aIndex);
        }
      }
    }
  }
}

void
GamepadManager::FireConnectionEvent(EventTarget* aTarget,
                                    Gamepad* aGamepad,
                                    bool aConnected)
{
  nsString name = aConnected ? NS_LITERAL_STRING("gamepadconnected") :
                               NS_LITERAL_STRING("gamepaddisconnected");
  GamepadEventInit init;
  init.mBubbles = false;
  init.mCancelable = false;
  init.mGamepad = aGamepad;
  RefPtr<GamepadEvent> event =
    GamepadEvent::Constructor(aTarget, name, init);

  event->SetTrusted(true);

  bool defaultActionEnabled = true;
  aTarget->DispatchEvent(event, &defaultActionEnabled);
}

void
GamepadManager::SyncGamepadState(uint32_t aIndex, Gamepad* aGamepad)
{
  if (mShuttingDown || !mEnabled) {
    return;
  }

  RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
  if (!gamepad) {
    return;
  }

  aGamepad->SyncState(gamepad);
}

// static
bool
GamepadManager::IsServiceRunning()
{
  return !!gGamepadManagerSingleton;
}

// static
already_AddRefed<GamepadManager>
GamepadManager::GetService()
{
  if (sShutdown) {
    return nullptr;
  }

  if (!gGamepadManagerSingleton) {
    RefPtr<GamepadManager> manager = new GamepadManager();
    nsresult rv = manager->Init();
    if(NS_WARN_IF(NS_FAILED(rv))) {
      return nullptr;
    }
    gGamepadManagerSingleton = manager;
    ClearOnShutdown(&gGamepadManagerSingleton);
  }

  RefPtr<GamepadManager> service(gGamepadManagerSingleton);
  return service.forget();
}

// static
bool
GamepadManager::IsAPIEnabled() {
  return Preferences::GetBool(kGamepadEnabledPref, false);
}

bool
GamepadManager::MaybeWindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex)
{
  if (!WindowHasSeenGamepad(aWindow, aIndex)) {
    // This window hasn't seen this gamepad before, so
    // send a connection event first.
    SetWindowHasSeenGamepad(aWindow, aIndex);
    return true;
  }
  return false;
}

bool
GamepadManager::WindowHasSeenGamepad(nsGlobalWindow* aWindow, uint32_t aIndex) const
{
  RefPtr<Gamepad> gamepad = aWindow->GetGamepad(aIndex);
  return gamepad != nullptr;
}

void
GamepadManager::SetWindowHasSeenGamepad(nsGlobalWindow* aWindow,
                                        uint32_t aIndex,
                                        bool aHasSeen)
{
  MOZ_ASSERT(aWindow);
  MOZ_ASSERT(aWindow->IsInnerWindow());

  if (mListeners.IndexOf(aWindow) == NoIndex) {
    // This window isn't even listening for gamepad events.
    return;
  }

  if (aHasSeen) {
    aWindow->SetHasSeenGamepadInput(true);
    nsCOMPtr<nsISupports> window = ToSupports(aWindow);
    RefPtr<Gamepad> gamepad = GetGamepad(aIndex);
    if (!gamepad) {
      return;
    }
    RefPtr<Gamepad> clonedGamepad = gamepad->Clone(window);
    aWindow->AddGamepad(aIndex, clonedGamepad);
  } else {
    aWindow->RemoveGamepad(aIndex);
  }
}

void
GamepadManager::Update(const GamepadChangeEvent& aEvent)
{
  if (aEvent.type() == GamepadChangeEvent::TGamepadAdded) {
    const GamepadAdded& a = aEvent.get_GamepadAdded();
    AddGamepad(a.index(), a.id(),
               static_cast<GamepadMappingType>(a.mapping()),
               a.service_type(),
               a.num_buttons(), a.num_axes());
    return;
  }
  if (aEvent.type() == GamepadChangeEvent::TGamepadRemoved) {
    const GamepadRemoved& a = aEvent.get_GamepadRemoved();
    RemoveGamepad(a.index(), a.service_type());
    return;
  }
  if (aEvent.type() == GamepadChangeEvent::TGamepadButtonInformation) {
    const GamepadButtonInformation& a = aEvent.get_GamepadButtonInformation();
    NewButtonEvent(a.index(), a.service_type(), a.button(),
                   a.pressed(), a.value());
    return;
  }
  if (aEvent.type() == GamepadChangeEvent::TGamepadAxisInformation) {
    const GamepadAxisInformation& a = aEvent.get_GamepadAxisInformation();
    NewAxisMoveEvent(a.index(), a.service_type(), a.axis(), a.value());
    return;
  }
  if (aEvent.type() == GamepadChangeEvent::TGamepadPoseInformation) {
    const GamepadPoseInformation& a = aEvent.get_GamepadPoseInformation();
    NewPoseEvent(a.index(), a.service_type(), a.pose_state());
    return;
  }

  MOZ_CRASH("We shouldn't be here!");

}

//Override nsIIPCBackgroundChildCreateCallback
void
GamepadManager::ActorCreated(PBackgroundChild *aActor)
{
  MOZ_ASSERT(aActor);
  GamepadEventChannelChild *child = new GamepadEventChannelChild();
  PGamepadEventChannelChild *initedChild =
    aActor->SendPGamepadEventChannelConstructor(child);
  if (NS_WARN_IF(!initedChild)) {
    ActorFailed();
    return;
  }
  MOZ_ASSERT(initedChild == child);
  child->SendGamepadListenerAdded();
  mChannelChildren.AppendElement(child);
}

//Override nsIIPCBackgroundChildCreateCallback
void
GamepadManager::ActorFailed()
{
  MOZ_CRASH("Gamepad IPC actor create failed!");
}

} // namespace dom
} // namespace mozilla