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

#import <UIKit/UIKit.h>

#include "mozilla/EventForwards.h"
#include "mozilla/Logging.h"
#include "mozilla/MacStringHelpers.h"
#include "mozilla/MiscEvents.h"
#include "mozilla/TextEventDispatcher.h"
#include "mozilla/TextEvents.h"
#include "mozilla/WidgetUtils.h"
#include "nsIWidget.h"
#include "nsObjCExceptions.h"
#include "nsString.h"
#include "nsWindow.h"

mozilla::LazyLogModule gIMELog("TextInputHandler");

namespace mozilla::widget {

NS_IMPL_ISUPPORTS(TextInputHandler, TextEventDispatcherListener,
                  nsISupportsWeakReference)

TextInputHandler::TextInputHandler(nsWindow* aWidget)
    : mWidget(aWidget), mDispatcher(aWidget->GetTextEventDispatcher()) {}

nsresult TextInputHandler::NotifyIME(TextEventDispatcher* aTextEventDispatcher,
                                     const IMENotification& aNotification) {
  return NS_OK;
}

IMENotificationRequests TextInputHandler::GetIMENotificationRequests() {
  return IMENotificationRequests{};
}

void TextInputHandler::OnRemovedFrom(
    TextEventDispatcher* aTextEventDispatcher) {}

void TextInputHandler::WillDispatchKeyboardEvent(
    TextEventDispatcher* aTextEventDispatcher,
    WidgetKeyboardEvent& aKeyboardEvent, uint32_t aIndexOfKeypress,
    void* aData) {}

bool TextInputHandler::InsertText(NSString* aText) {
  nsString str;
  CopyNSStringToXPCOMString(aText, str);

  MOZ_LOG(gIMELog, LogLevel::Info,
          ("%p TextInputHandler::InsertText(aText=%s)", this,
           NS_ConvertUTF16toUTF8(str).get()));

  if (Destroyed()) {
    return false;
  }

  if (str.Length() == 1) {
    char16_t charCode = str[0];
    if (charCode == 0x0a) {
      return EmulateKeyboardEvent(NS_VK_RETURN, KEY_NAME_INDEX_Enter, charCode);
    }
    if (charCode == 0x08) {
      return EmulateKeyboardEvent(NS_VK_BACK, KEY_NAME_INDEX_Backspace,
                                  charCode);
    }
    if (uint32_t keyCode = WidgetUtils::ComputeKeyCodeFromChar(charCode)) {
      return EmulateKeyboardEvent(keyCode, KEY_NAME_INDEX_USE_STRING, charCode);
    }
  }

  nsEventStatus status = nsEventStatus_eIgnore;
  RefPtr<nsWindow> widget(mWidget);
  if (!DispatchKeyDownEvent(NS_VK_PROCESSKEY, KEY_NAME_INDEX_Process, 0,
                            status)) {
    return false;
  }
  if (Destroyed()) {
    return false;
  }

  mDispatcher->CommitComposition(status, &str, nullptr);
  if (widget->Destroyed()) {
    return false;
  }

  DispatchKeyUpEvent(NS_VK_PROCESSKEY, KEY_NAME_INDEX_Process, 0, status);

  return true;
}

bool TextInputHandler::HandleCommand(Command aCommand) {
  MOZ_LOG(gIMELog, LogLevel::Info,
          ("%p   TextInputHandler::HandleCommand, aCommand=%s", this,
           ToChar(aCommand)));

  if (Destroyed()) {
    return false;
  }

  if (aCommand != Command::DeleteCharBackward) {
    return false;
  }

  nsEventStatus status = nsEventStatus_eIgnore;
  if (!DispatchKeyDownEvent(NS_VK_BACK, KEY_NAME_INDEX_Backspace, 0, status)) {
    return true;
  }
  if (Destroyed() || status == nsEventStatus_eConsumeNoDefault) {
    return true;
  }

  // TODO: Focus check

  if (!DispatchKeyPressEvent(NS_VK_BACK, KEY_NAME_INDEX_Backspace, 0, status)) {
    return true;
  }
  if (Destroyed() || status == nsEventStatus_eConsumeNoDefault) {
    return true;
  }

  // TODO: Focus check

  DispatchKeyUpEvent(NS_VK_BACK, KEY_NAME_INDEX_Backspace, 0, status);

  return true;
}

static uint32_t ComputeKeyModifiers(uint32_t aCharCode) {
  if (aCharCode >= 'A' && aCharCode <= 'Z') {
    return MODIFIER_SHIFT;
  }
  return 0;
}

static void InitKeyEvent(WidgetKeyboardEvent& aEvent, uint32_t aKeyCode,
                         KeyNameIndex aKeyNameIndex, char16_t aCharCode) {
  aEvent.mKeyCode = aKeyCode;
  aEvent.mIsRepeat = false;
  aEvent.mKeyNameIndex = aKeyNameIndex;
  // TODO(m_kato):
  // How to get native key? Then, implement NativeKeyToDOM*.h for iOS
  aEvent.mCodeNameIndex = CODE_NAME_INDEX_UNKNOWN;
  if (aEvent.mKeyNameIndex == KEY_NAME_INDEX_USE_STRING) {
    aEvent.mKeyValue = aCharCode;
  }
  aEvent.mModifiers = ComputeKeyModifiers(aCharCode);
  aEvent.mLocation = eKeyLocationStandard;
  aEvent.mTimeStamp = TimeStamp::Now();
}

bool TextInputHandler::DispatchKeyDownEvent(uint32_t aKeyCode,
                                            KeyNameIndex aKeyNameIndex,
                                            char16_t aCharCode,
                                            nsEventStatus& aStatus) {
  MOZ_ASSERT(aKeyCode);
  MOZ_ASSERT(mWidget);

  WidgetKeyboardEvent keydownEvent(true, eKeyDown, mWidget);
  InitKeyEvent(keydownEvent, aKeyCode, aKeyNameIndex, aCharCode);
  nsresult rv = mDispatcher->BeginNativeInputTransaction();
  if (NS_FAILED(rv)) {
    NS_WARNING("BeginNativeInputTransaction is failed");
    return false;
  }
  return mDispatcher->DispatchKeyboardEvent(eKeyDown, keydownEvent, aStatus);
}

bool TextInputHandler::DispatchKeyUpEvent(uint32_t aKeyCode,
                                          KeyNameIndex aKeyNameIndex,
                                          char16_t aCharCode,
                                          nsEventStatus& aStatus) {
  MOZ_ASSERT(aKeyCode);
  MOZ_ASSERT(mWidget);

  WidgetKeyboardEvent keyupEvent(true, eKeyUp, mWidget);
  InitKeyEvent(keyupEvent, aKeyCode, aKeyNameIndex, aCharCode);
  nsresult rv = mDispatcher->BeginNativeInputTransaction();
  if (NS_FAILED(rv)) {
    NS_WARNING("BeginNativeInputTransaction is failed");
    return false;
  }
  return mDispatcher->DispatchKeyboardEvent(eKeyUp, keyupEvent, aStatus);
}

bool TextInputHandler::DispatchKeyPressEvent(uint32_t aKeyCode,
                                             KeyNameIndex aKeyNameIndex,
                                             char16_t aCharCode,
                                             nsEventStatus& aStatus) {
  MOZ_ASSERT(aKeyCode);
  MOZ_ASSERT(mWidget);

  WidgetKeyboardEvent keypressEvent(true, eKeyPress, mWidget);
  InitKeyEvent(keypressEvent, aKeyCode, aKeyNameIndex, aCharCode);
  nsresult rv = mDispatcher->BeginNativeInputTransaction();
  if (NS_FAILED(rv)) {
    NS_WARNING("BeginNativeInputTransaction is failed");
    return false;
  }
  return mDispatcher->MaybeDispatchKeypressEvents(keypressEvent, aStatus);
}

bool TextInputHandler::EmulateKeyboardEvent(uint32_t aKeyCode,
                                            KeyNameIndex aKeyNameIndex,
                                            char16_t aCharCode) {
  MOZ_ASSERT(aCharCode);

  MOZ_LOG(gIMELog, LogLevel::Info,
          ("%p TextInputHandler::EmulateKeyboardEvent(aKeyCode=%x, "
           "aKeyNameIndex=%x, aCharCode=%x)",
           this, aKeyCode, aKeyNameIndex, aCharCode));

  nsEventStatus status = nsEventStatus_eIgnore;
  if (!DispatchKeyDownEvent(aKeyCode, aKeyNameIndex, aCharCode, status)) {
    return true;
  }
  if (Destroyed() || status == nsEventStatus_eConsumeNoDefault) {
    return true;
  }
  // TODO: Focus check

  if (!DispatchKeyPressEvent(aKeyCode, aKeyNameIndex, aCharCode, status)) {
    return true;
  }
  if (Destroyed() || status == nsEventStatus_eConsumeNoDefault) {
    return true;
  }
  // TODO: Focus check

  DispatchKeyUpEvent(aKeyCode, aKeyNameIndex, aCharCode, status);
  return true;
}

void TextInputHandler::OnDestroyed() { mWidget = nullptr; }

}  // namespace mozilla::widget
