/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "nsWindowBase.h"

#include "mozilla/MiscEvents.h"
#include "KeyboardLayout.h"
#include "WinUtils.h"
#include "npapi.h"
#include "nsAutoPtr.h"

using namespace mozilla;
using namespace mozilla::widget;

static const wchar_t kUser32LibName[] =  L"user32.dll";
bool nsWindowBase::sTouchInjectInitialized = false;
InjectTouchInputPtr nsWindowBase::sInjectTouchFuncPtr;

bool
nsWindowBase::DispatchPluginEvent(const MSG& aMsg)
{
  if (!ShouldDispatchPluginEvent()) {
    return false;
  }
  WidgetPluginEvent pluginEvent(true, ePluginInputEvent, this);
  LayoutDeviceIntPoint point(0, 0);
  InitEvent(pluginEvent, &point);
  NPEvent npEvent;
  npEvent.event = aMsg.message;
  npEvent.wParam = aMsg.wParam;
  npEvent.lParam = aMsg.lParam;
  pluginEvent.mPluginEvent.Copy(npEvent);
  pluginEvent.mRetargetToFocusedDocument = true;
  return DispatchWindowEvent(&pluginEvent);
}

bool
nsWindowBase::ShouldDispatchPluginEvent()
{
  return PluginHasFocus();
}

// static
bool
nsWindowBase::InitTouchInjection()
{
  if (!sTouchInjectInitialized) {
    // Initialize touch injection on the first call
    HMODULE hMod = LoadLibraryW(kUser32LibName);
    if (!hMod) {
      return false;
    }

    InitializeTouchInjectionPtr func =
      (InitializeTouchInjectionPtr)GetProcAddress(hMod, "InitializeTouchInjection");
    if (!func) {
      WinUtils::Log("InitializeTouchInjection not available.");
      return false;
    }

    if (!func(TOUCH_INJECT_MAX_POINTS, TOUCH_FEEDBACK_DEFAULT)) {
      WinUtils::Log("InitializeTouchInjection failure. GetLastError=%d", GetLastError());
      return false;
    }

    sInjectTouchFuncPtr =
      (InjectTouchInputPtr)GetProcAddress(hMod, "InjectTouchInput");
    if (!sInjectTouchFuncPtr) {
      WinUtils::Log("InjectTouchInput not available.");
      return false;
    }
    sTouchInjectInitialized = true;
  }
  return true;
}

bool
nsWindowBase::InjectTouchPoint(uint32_t aId, LayoutDeviceIntPoint& aPoint,
                               POINTER_FLAGS aFlags, uint32_t aPressure,
                               uint32_t aOrientation)
{
  if (aId > TOUCH_INJECT_MAX_POINTS) {
    WinUtils::Log("Pointer ID exceeds maximum. See TOUCH_INJECT_MAX_POINTS.");
    return false;
  }

  POINTER_TOUCH_INFO info;
  memset(&info, 0, sizeof(POINTER_TOUCH_INFO));

  info.touchFlags = TOUCH_FLAG_NONE;
  info.touchMask = TOUCH_MASK_CONTACTAREA|TOUCH_MASK_ORIENTATION|TOUCH_MASK_PRESSURE;
  info.pressure = aPressure;
  info.orientation = aOrientation;

  info.pointerInfo.pointerFlags = aFlags;
  info.pointerInfo.pointerType =  PT_TOUCH;
  info.pointerInfo.pointerId = aId;
  info.pointerInfo.ptPixelLocation.x = aPoint.x;
  info.pointerInfo.ptPixelLocation.y = aPoint.y;

  info.rcContact.top = info.pointerInfo.ptPixelLocation.y - 2;
  info.rcContact.bottom = info.pointerInfo.ptPixelLocation.y + 2;
  info.rcContact.left = info.pointerInfo.ptPixelLocation.x - 2;
  info.rcContact.right = info.pointerInfo.ptPixelLocation.x + 2;

  if (!sInjectTouchFuncPtr(1, &info)) {
    WinUtils::Log("InjectTouchInput failure. GetLastError=%d", GetLastError());
    return false;
  }
  return true;
}

void nsWindowBase::ChangedDPI()
{
  if (mWidgetListener) {
    nsIPresShell* presShell = mWidgetListener->GetPresShell();
    if (presShell) {
      presShell->BackingScaleFactorChanged();
    }
    mWidgetListener->UIResolutionChanged();
  }
}

nsresult
nsWindowBase::SynthesizeNativeTouchPoint(uint32_t aPointerId,
                                         nsIWidget::TouchPointerState aPointerState,
                                         LayoutDeviceIntPoint aPoint,
                                         double aPointerPressure,
                                         uint32_t aPointerOrientation,
                                         nsIObserver* aObserver)
{
  AutoObserverNotifier notifier(aObserver, "touchpoint");

  if (gfxPrefs::APZTestFailsWithNativeInjection() || !InitTouchInjection()) {
    // If we don't have touch injection from the OS, or if we are running a test
    // that cannot properly inject events to satisfy the OS requirements (see bug
    // 1313170)  we can just fake it and synthesize the events from here.
    MOZ_ASSERT(NS_IsMainThread());
    if (aPointerState == TOUCH_HOVER) {
      return NS_ERROR_UNEXPECTED;
    }

    if (!mSynthesizedTouchInput) {
      mSynthesizedTouchInput = MakeUnique<MultiTouchInput>();
    }

    WidgetEventTime time = CurrentMessageWidgetEventTime();
    LayoutDeviceIntPoint pointInWindow = aPoint - WidgetToScreenOffset();
    MultiTouchInput inputToDispatch = UpdateSynthesizedTouchState(
        mSynthesizedTouchInput.get(), time.mTime, time.mTimeStamp,
        aPointerId, aPointerState, pointInWindow, aPointerPressure,
        aPointerOrientation);
    DispatchTouchInput(inputToDispatch);
    return NS_OK;
  }

  bool hover = aPointerState & TOUCH_HOVER;
  bool contact = aPointerState & TOUCH_CONTACT;
  bool remove = aPointerState & TOUCH_REMOVE;
  bool cancel = aPointerState & TOUCH_CANCEL;

  // win api expects a value from 0 to 1024. aPointerPressure is a value
  // from 0.0 to 1.0.
  uint32_t pressure = (uint32_t)ceil(aPointerPressure * 1024);

  // If we already know about this pointer id get it's record
  PointerInfo* info = mActivePointers.Get(aPointerId);

  // We know about this pointer, send an update
  if (info) {
    POINTER_FLAGS flags = POINTER_FLAG_UPDATE;
    if (hover) {
      flags |= POINTER_FLAG_INRANGE;
    } else if (contact) {
      flags |= POINTER_FLAG_INCONTACT|POINTER_FLAG_INRANGE;
    } else if (remove) {
      flags = POINTER_FLAG_UP;
      // Remove the pointer from our tracking list. This is nsAutPtr wrapped,
      // so shouldn't leak.
      mActivePointers.Remove(aPointerId);
    }

    if (cancel) {
      flags |= POINTER_FLAG_CANCELED;
    }

    return !InjectTouchPoint(aPointerId, aPoint, flags,
                             pressure, aPointerOrientation) ?
      NS_ERROR_UNEXPECTED : NS_OK;
  }

  // Missing init state, error out
  if (remove || cancel) {
    return NS_ERROR_INVALID_ARG;
  }

  // Create a new pointer
  info = new PointerInfo(aPointerId, aPoint);

  POINTER_FLAGS flags = POINTER_FLAG_INRANGE;
  if (contact) {
    flags |= POINTER_FLAG_INCONTACT|POINTER_FLAG_DOWN;
  }

  mActivePointers.Put(aPointerId, info);
  return !InjectTouchPoint(aPointerId, aPoint, flags,
                           pressure, aPointerOrientation) ?
    NS_ERROR_UNEXPECTED : NS_OK;
}

nsresult
nsWindowBase::ClearNativeTouchSequence(nsIObserver* aObserver)
{
  AutoObserverNotifier notifier(aObserver, "cleartouch");
  if (!sTouchInjectInitialized) {
    return NS_OK;
  }

  // cancel all input points
  for (auto iter = mActivePointers.Iter(); !iter.Done(); iter.Next()) {
    nsAutoPtr<PointerInfo>& info = iter.Data();
    InjectTouchPoint(info.get()->mPointerId, info.get()->mPosition,
                     POINTER_FLAG_CANCELED);
    iter.Remove();
  }

  nsBaseWidget::ClearNativeTouchSequence(nullptr);

  return NS_OK;
}

bool
nsWindowBase::HandleAppCommandMsg(const MSG& aAppCommandMsg,
                                  LRESULT *aRetValue)
{
  ModifierKeyState modKeyState;
  NativeKey nativeKey(this, aAppCommandMsg, modKeyState);
  bool consumed = nativeKey.HandleAppCommandMessage();
  *aRetValue = consumed ? 1 : 0;
  return consumed;
}