/* -*- 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 "PointerEventHandler.h"
#include "nsIFrame.h"
#include "PointerEvent.h"
#include "mozilla/PresShell.h"

namespace mozilla {

using namespace dom;

static bool sPointerEventEnabled = true;
static bool sPointerEventImplicitCapture = false;

class PointerInfo final
{
public:
  uint16_t mPointerType;
  bool mActiveState;
  bool mPrimaryState;
  bool mPreventMouseEventByContent;
  explicit PointerInfo(bool aActiveState, uint16_t aPointerType,
                       bool aPrimaryState)
    : mPointerType(aPointerType)
    , mActiveState(aActiveState)
    , mPrimaryState(aPrimaryState)
    , mPreventMouseEventByContent(false)
  {
  }
};

// Keeps a map between pointerId and element that currently capturing pointer
// with such pointerId. If pointerId is absent in this map then nobody is
// capturing it. Additionally keep information about pending capturing content.
static nsClassHashtable<nsUint32HashKey,
                        PointerCaptureInfo>* sPointerCaptureList;

// Keeps information about pointers such as pointerId, activeState, pointerType,
// primaryState
static nsClassHashtable<nsUint32HashKey, PointerInfo>* sActivePointersIds;

/* static */ void
PointerEventHandler::Initialize()
{
  static bool initialized = false;
  if (initialized) {
    return;
  }
  initialized = true;
  Preferences::AddBoolVarCache(&sPointerEventEnabled,
                               "dom.w3c_pointer_events.enabled", true);
  Preferences::AddBoolVarCache(&sPointerEventImplicitCapture,
                               "dom.w3c_pointer_events.implicit_capture", true);
}

/* static */ void
PointerEventHandler::InitializeStatics()
{
  MOZ_ASSERT(!sPointerCaptureList, "InitializeStatics called multiple times!");
  sPointerCaptureList =
    new nsClassHashtable<nsUint32HashKey, PointerCaptureInfo>;
  sActivePointersIds = new nsClassHashtable<nsUint32HashKey, PointerInfo>;
}

/* static */ void
PointerEventHandler::ReleaseStatics()
{
  MOZ_ASSERT(sPointerCaptureList, "ReleaseStatics called without Initialize!");
  delete sPointerCaptureList;
  sPointerCaptureList = nullptr;
  delete sActivePointersIds;
  sActivePointersIds = nullptr;
}

/* static */ bool
PointerEventHandler::IsPointerEventEnabled()
{
  return sPointerEventEnabled;
}

/* static */ bool
PointerEventHandler::IsPointerEventImplicitCaptureForTouchEnabled()
{
  return sPointerEventEnabled && sPointerEventImplicitCapture;
}

/* static */ void
PointerEventHandler::UpdateActivePointerState(WidgetMouseEvent* aEvent)
{
  if (!IsPointerEventEnabled() || !aEvent) {
    return;
  }
  switch (aEvent->mMessage) {
  case eMouseEnterIntoWidget:
    // In this case we have to know information about available mouse pointers
    sActivePointersIds->Put(aEvent->pointerId,
                            new PointerInfo(false, aEvent->inputSource, true));
    break;
  case ePointerDown:
    // In this case we switch pointer to active state
    if (WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent()) {
      sActivePointersIds->Put(pointerEvent->pointerId,
                              new PointerInfo(true, pointerEvent->inputSource,
                                              pointerEvent->mIsPrimary));
    }
    break;
  case ePointerCancel:
    // pointercancel means a pointer is unlikely to continue to produce pointer
    // events. In that case, we should turn off active state or remove the
    // pointer from active pointers.
  case ePointerUp:
    // In this case we remove information about pointer or turn off active state
    if (WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent()) {
      if(pointerEvent->inputSource != nsIDOMMouseEvent::MOZ_SOURCE_TOUCH) {
        sActivePointersIds->Put(pointerEvent->pointerId,
                                new PointerInfo(false,
                                                pointerEvent->inputSource,
                                                pointerEvent->mIsPrimary));
      } else {
        sActivePointersIds->Remove(pointerEvent->pointerId);
      }
    }
    break;
  case eMouseExitFromWidget:
    // In this case we have to remove information about disappeared mouse
    // pointers
    sActivePointersIds->Remove(aEvent->pointerId);
    break;
  default:
    break;
  }
}

/* static */ void
PointerEventHandler::SetPointerCaptureById(uint32_t aPointerId,
                                           nsIContent* aContent)
{
  MOZ_ASSERT(aContent);
  if (nsIDOMMouseEvent::MOZ_SOURCE_MOUSE == GetPointerType(aPointerId)) {
    nsIPresShell::SetCapturingContent(aContent, CAPTURE_PREVENTDRAG);
  }

  PointerCaptureInfo* pointerCaptureInfo = GetPointerCaptureInfo(aPointerId);
  if (pointerCaptureInfo) {
    pointerCaptureInfo->mPendingContent = aContent;
  } else {
    sPointerCaptureList->Put(aPointerId, new PointerCaptureInfo(aContent));
  }
}

/* static */ PointerCaptureInfo*
PointerEventHandler::GetPointerCaptureInfo(uint32_t aPointerId)
{
  PointerCaptureInfo* pointerCaptureInfo = nullptr;
  sPointerCaptureList->Get(aPointerId, &pointerCaptureInfo);
  return pointerCaptureInfo;
}

/* static */ void
PointerEventHandler::ReleasePointerCaptureById(uint32_t aPointerId)
{
  PointerCaptureInfo* pointerCaptureInfo = GetPointerCaptureInfo(aPointerId);
  if (pointerCaptureInfo && pointerCaptureInfo->mPendingContent) {
    if (nsIDOMMouseEvent::MOZ_SOURCE_MOUSE == GetPointerType(aPointerId)) {
      nsIPresShell::SetCapturingContent(nullptr, CAPTURE_PREVENTDRAG);
    }
    pointerCaptureInfo->mPendingContent = nullptr;
  }
}

/* static */ void
PointerEventHandler::ReleaseAllPointerCapture()
{
  for (auto iter = sPointerCaptureList->Iter(); !iter.Done(); iter.Next()) {
    PointerCaptureInfo* data = iter.UserData();
    if (data && data->mPendingContent) {
      ReleasePointerCaptureById(iter.Key());
    }
  }
}

/* static */ bool
PointerEventHandler::GetPointerInfo(uint32_t aPointerId, bool& aActiveState)
{
  PointerInfo* pointerInfo = nullptr;
  if (sActivePointersIds->Get(aPointerId, &pointerInfo) && pointerInfo) {
    aActiveState = pointerInfo->mActiveState;
    return true;
  }
  return false;
}

/* static */ void
PointerEventHandler::CheckPointerCaptureState(WidgetPointerEvent* aEvent)
{
  // Handle pending pointer capture before any pointer events except
  // gotpointercapture / lostpointercapture.
  if (!aEvent) {
    return;
  }
  MOZ_ASSERT(IsPointerEventEnabled());
  MOZ_ASSERT(aEvent->mClass == ePointerEventClass);

  PointerCaptureInfo* captureInfo = GetPointerCaptureInfo(aEvent->pointerId);

  if (captureInfo &&
      captureInfo->mPendingContent != captureInfo->mOverrideContent) {
    // cache captureInfo->mPendingContent since it may be changed in the pointer
    // event listener
    nsIContent* pendingContent = captureInfo->mPendingContent.get();
    if (captureInfo->mOverrideContent) {
      DispatchGotOrLostPointerCaptureEvent(/* aIsGotCapture */ false, aEvent,
                                           captureInfo->mOverrideContent);
    }
    if (pendingContent) {
      DispatchGotOrLostPointerCaptureEvent(/* aIsGotCapture */ true, aEvent,
                                           pendingContent);
    }

    captureInfo->mOverrideContent = pendingContent;
    if (captureInfo->Empty()) {
      sPointerCaptureList->Remove(aEvent->pointerId);
    }
  }
}

/* static */ void
PointerEventHandler::ImplicitlyCapturePointer(nsIFrame* aFrame,
                                              WidgetEvent* aEvent)
{
  MOZ_ASSERT(aEvent->mMessage == ePointerDown);
  if (!aFrame || !IsPointerEventEnabled() ||
      !IsPointerEventImplicitCaptureForTouchEnabled()) {
    return;
  }
  WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent();
  NS_WARNING_ASSERTION(pointerEvent,
                       "Call ImplicitlyCapturePointer with non-pointer event");
  if (pointerEvent->inputSource != nsIDOMMouseEvent::MOZ_SOURCE_TOUCH) {
    // We only implicitly capture the pointer for touch device.
    return;
  }
  nsCOMPtr<nsIContent> target;
  aFrame->GetContentForEvent(aEvent, getter_AddRefs(target));
  while (target && !target->IsElement()) {
    target = target->GetParent();
  }
  if (NS_WARN_IF(!target)) {
    return;
  }
  SetPointerCaptureById(pointerEvent->pointerId, target);
}

/* static */ void
PointerEventHandler::ImplicitlyReleasePointerCapture(WidgetEvent* aEvent)
{
  MOZ_ASSERT(aEvent);
  if (aEvent->mMessage != ePointerUp && aEvent->mMessage != ePointerCancel) {
    return;
  }
  WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent();
  ReleasePointerCaptureById(pointerEvent->pointerId);
  CheckPointerCaptureState(pointerEvent);
}

/* static */ nsIContent*
PointerEventHandler::GetPointerCapturingContent(uint32_t aPointerId)
{
  PointerCaptureInfo* pointerCaptureInfo = GetPointerCaptureInfo(aPointerId);
  if (pointerCaptureInfo) {
    return pointerCaptureInfo->mOverrideContent;
  }
  return nullptr;
}

/* static */ nsIFrame*
PointerEventHandler::GetPointerCapturingFrame(nsIFrame* aFrameUnderCursor,
                                              WidgetGUIEvent* aEvent)
{
  if (!IsPointerEventEnabled() || (aEvent->mClass != ePointerEventClass &&
                                   aEvent->mClass != eMouseEventClass) ||
      aEvent->mMessage == ePointerDown || aEvent->mMessage == eMouseDown) {
    // Pointer capture should only be applied to all pointer events and mouse
    // events except ePointerDown and eMouseDown;
    return aFrameUnderCursor;
  }

  WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent();
  if (!mouseEvent) {
    return aFrameUnderCursor;
  }

  // Find the content which captures the pointer.
  nsIContent* capturingContent =
    GetPointerCapturingContent(mouseEvent->pointerId);

  if (!capturingContent) {
    return aFrameUnderCursor;
  }
  // Return the content's primary frame as the target frame.
  nsIFrame* capturingFrame = capturingContent->GetPrimaryFrame();
  return capturingFrame ? capturingFrame : aFrameUnderCursor;
}

/* static */ void
PointerEventHandler::ReleaseIfCaptureByDescendant(nsIContent* aContent)
{
  // We should check that aChild does not contain pointer capturing elements.
  // If it does we should release the pointer capture for the elements.
  for (auto iter = sPointerCaptureList->Iter(); !iter.Done(); iter.Next()) {
    PointerCaptureInfo* data = iter.UserData();
    if (data && data->mPendingContent &&
        nsContentUtils::ContentIsDescendantOf(data->mPendingContent,
                                              aContent)) {
      ReleasePointerCaptureById(iter.Key());
    }
  }
}

/* static */ void
PointerEventHandler::PreHandlePointerEventsPreventDefault(
                       WidgetPointerEvent* aPointerEvent,
                       WidgetGUIEvent* aMouseOrTouchEvent)
{
  if (!aPointerEvent->mIsPrimary || aPointerEvent->mMessage == ePointerDown) {
    return;
  }
  PointerInfo* pointerInfo = nullptr;
  if (!sActivePointersIds->Get(aPointerEvent->pointerId, &pointerInfo) ||
      !pointerInfo) {
    // The PointerInfo for active pointer should be added for normal cases. But
    // in some cases, we may receive mouse events before adding PointerInfo in
    // sActivePointersIds. (e.g. receive mousemove before eMouseEnterIntoWidget
    // or change preference 'dom.w3c_pointer_events.enabled' from off to on).
    // In these cases, we could ignore them because they are not the events
    // between a DefaultPrevented pointerdown and the corresponding pointerup.
    return;
  }
  if (!pointerInfo->mPreventMouseEventByContent) {
    return;
  }
  aMouseOrTouchEvent->PreventDefault(false);
  if (aPointerEvent->mMessage == ePointerUp) {
    pointerInfo->mPreventMouseEventByContent = false;
  }
}

/* static */ void
PointerEventHandler::PostHandlePointerEventsPreventDefault(
                       WidgetPointerEvent* aPointerEvent,
                       WidgetGUIEvent* aMouseOrTouchEvent)
{
  if (!aPointerEvent->mIsPrimary || aPointerEvent->mMessage != ePointerDown ||
      !aPointerEvent->DefaultPreventedByContent()) {
    return;
  }
  PointerInfo* pointerInfo = nullptr;
  if (!sActivePointersIds->Get(aPointerEvent->pointerId, &pointerInfo) ||
      !pointerInfo) {
    // We already added the PointerInfo for active pointer when
    // PresShell::HandleEvent handling pointerdown event.
#ifdef DEBUG
    MOZ_CRASH("Got ePointerDown w/o active pointer info!!");
#endif // #ifdef DEBUG
    return;
  }
  // PreventDefault only applied for active pointers.
  if (!pointerInfo->mActiveState) {
    return;
  }
  aMouseOrTouchEvent->PreventDefault(false);
  pointerInfo->mPreventMouseEventByContent = true;
}

/* static */ void
PointerEventHandler::DispatchPointerFromMouseOrTouch(
                       PresShell* aShell,
                       nsIFrame* aFrame,
                       WidgetGUIEvent* aEvent,
                       bool aDontRetargetEvents,
                       nsEventStatus* aStatus,
                       nsIContent** aTargetContent)
{
  MOZ_ASSERT(IsPointerEventEnabled());
  MOZ_ASSERT(aFrame);
  MOZ_ASSERT(aEvent);

  EventMessage pointerMessage = eVoidEvent;
  if (aEvent->mClass == eMouseEventClass) {
    WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent();
    // 1. If it is not mouse then it is likely will come as touch event
    // 2. We don't synthesize pointer events for those events that are not
    //    dispatched to DOM.
    if (!mouseEvent->convertToPointer ||
        !aEvent->IsAllowedToDispatchDOMEvent()) {
      return;
    }
    int16_t button = mouseEvent->button;
    switch (mouseEvent->mMessage) {
    case eMouseMove:
      button = WidgetMouseEvent::eNoButton;
      pointerMessage = ePointerMove;
      break;
    case eMouseUp:
      pointerMessage = mouseEvent->buttons ? ePointerMove : ePointerUp;
      break;
    case eMouseDown:
      pointerMessage =
        mouseEvent->buttons & ~nsContentUtils::GetButtonsFlagForButton(button) ?
        ePointerMove : ePointerDown;
      break;
    default:
      return;
    }

    WidgetPointerEvent event(*mouseEvent);
    event.pointerId = mouseEvent->pointerId;
    event.inputSource = mouseEvent->inputSource;
    event.mMessage = pointerMessage;
    event.button = button;
    event.buttons = mouseEvent->buttons;
    event.pressure = event.buttons ?
                     mouseEvent->pressure ? mouseEvent->pressure : 0.5f :
                     0.0f;
    event.convertToPointer = mouseEvent->convertToPointer = false;
    PreHandlePointerEventsPreventDefault(&event, aEvent);
    RefPtr<PresShell> shell(aShell);
    shell->HandleEvent(aFrame, &event, aDontRetargetEvents, aStatus,
                       aTargetContent);
    PostHandlePointerEventsPreventDefault(&event, aEvent);
  } else if (aEvent->mClass == eTouchEventClass) {
    WidgetTouchEvent* touchEvent = aEvent->AsTouchEvent();
    int16_t button = WidgetMouseEvent::eLeftButton;
    int16_t buttons = WidgetMouseEvent::eLeftButtonFlag;
    // loop over all touches and dispatch pointer events on each touch
    // copy the event
    switch (touchEvent->mMessage) {
    case eTouchMove:
      pointerMessage = ePointerMove;
      button = WidgetMouseEvent::eNoButton;
      break;
    case eTouchEnd:
      pointerMessage = ePointerUp;
      buttons = WidgetMouseEvent::eNoButtonFlag;
      break;
    case eTouchStart:
      pointerMessage = ePointerDown;
      break;
    case eTouchCancel:
    case eTouchPointerCancel:
      pointerMessage = ePointerCancel;
      break;
    default:
      return;
    }

    RefPtr<PresShell> shell(aShell);
    for (uint32_t i = 0; i < touchEvent->mTouches.Length(); ++i) {
      Touch* touch = touchEvent->mTouches[i];
      if (!TouchManager::ShouldConvertTouchToPointer(touch, touchEvent)) {
        continue;
      }

      WidgetPointerEvent event(touchEvent->IsTrusted(), pointerMessage,
                               touchEvent->mWidget);
      event.mIsPrimary = i == 0;
      event.pointerId = touch->Identifier();
      event.mRefPoint = touch->mRefPoint;
      event.mModifiers = touchEvent->mModifiers;
      event.mWidth = touch->RadiusX(CallerType::System);
      event.mHeight = touch->RadiusY(CallerType::System);
      event.tiltX = touch->tiltX;
      event.tiltY = touch->tiltY;
      event.mTime = touchEvent->mTime;
      event.mTimeStamp = touchEvent->mTimeStamp;
      event.mFlags = touchEvent->mFlags;
      event.button = button;
      event.buttons = buttons;
      event.inputSource = nsIDOMMouseEvent::MOZ_SOURCE_TOUCH;
      event.convertToPointer = touch->convertToPointer = false;
      PreHandlePointerEventsPreventDefault(&event, aEvent);
      shell->HandleEvent(aFrame, &event, aDontRetargetEvents, aStatus,
                         aTargetContent);
      PostHandlePointerEventsPreventDefault(&event, aEvent);
    }
  }
}

/* static */ uint16_t
PointerEventHandler::GetPointerType(uint32_t aPointerId)
{
  PointerInfo* pointerInfo = nullptr;
  if (sActivePointersIds->Get(aPointerId, &pointerInfo) && pointerInfo) {
    return pointerInfo->mPointerType;
  }
  return nsIDOMMouseEvent::MOZ_SOURCE_UNKNOWN;
}

/* static */ bool
PointerEventHandler::GetPointerPrimaryState(uint32_t aPointerId)
{
  PointerInfo* pointerInfo = nullptr;
  if (sActivePointersIds->Get(aPointerId, &pointerInfo) && pointerInfo) {
    return pointerInfo->mPrimaryState;
  }
  return false;
}

/* static */ void
PointerEventHandler::DispatchGotOrLostPointerCaptureEvent(
                       bool aIsGotCapture,
                       const WidgetPointerEvent* aPointerEvent,
                       nsIContent* aCaptureTarget)
{
  nsIDocument* targetDoc = aCaptureTarget->OwnerDoc();
  nsCOMPtr<nsIPresShell> shell = targetDoc->GetShell();
  if (NS_WARN_IF(!shell)) {
    return;
  }

  if (!aIsGotCapture && !aCaptureTarget->IsInUncomposedDoc()) {
    // If the capturing element was removed from the DOM tree, fire
    // ePointerLostCapture at the document.
    PointerEventInit init;
    init.mPointerId = aPointerEvent->pointerId;
    init.mBubbles = true;
    init.mComposed = true;
    ConvertPointerTypeToString(aPointerEvent->inputSource, init.mPointerType);
    init.mIsPrimary = aPointerEvent->mIsPrimary;
    RefPtr<PointerEvent> event;
    event = PointerEvent::Constructor(aCaptureTarget,
                                      NS_LITERAL_STRING("lostpointercapture"),
                                      init);
    bool dummy;
    targetDoc->DispatchEvent(event->InternalDOMEvent(), &dummy);
    return;
  }
  nsEventStatus status = nsEventStatus_eIgnore;
  WidgetPointerEvent localEvent(aPointerEvent->IsTrusted(),
                                aIsGotCapture ? ePointerGotCapture :
                                                ePointerLostCapture,
                                aPointerEvent->mWidget);

  localEvent.AssignPointerEventData(*aPointerEvent, true);
  DebugOnly<nsresult> rv = shell->HandleEventWithTarget(
                                    &localEvent,
                                    aCaptureTarget->GetPrimaryFrame(),
                                    aCaptureTarget, &status);

  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "DispatchGotOrLostPointerCaptureEvent failed");
}

} // namespace mozilla
