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

#ifndef mozilla_widget_GeckoEditableSupport_h
#define mozilla_widget_GeckoEditableSupport_h

#include "nsAppShell.h"
#include "nsIWidget.h"
#include "nsTArray.h"

#include "mozilla/java/GeckoEditableChildNatives.h"
#include "mozilla/java/SessionTextInputWrappers.h"
#include "mozilla/TextEventDispatcher.h"
#include "mozilla/TextEventDispatcherListener.h"
#include "mozilla/UniquePtr.h"

class nsWindow;

namespace mozilla {

class TextComposition;

namespace dom {
class BrowserChild;
}

namespace widget {

class GeckoEditableSupport final
    : public TextEventDispatcherListener,
      public java::GeckoEditableChild::Natives<GeckoEditableSupport> {
  /*
      Rules for managing IME between Gecko and Java:

      * Gecko controls the text content, and Java shadows the Gecko text
         through text updates
      * Gecko and Java maintain separate selections, and synchronize when
         needed through selection updates and set-selection events
      * Java controls the composition, and Gecko shadows the Java
         composition through update composition events
  */

  using EditableBase = java::GeckoEditableChild::Natives<GeckoEditableSupport>;
  using EditableClient = java::SessionTextInput::EditableClient;
  using EditableListener = java::SessionTextInput::EditableListener;

  enum FlushChangesFlag {
    // Not retrying.
    FLUSH_FLAG_NONE,
    // Retrying due to IME text changes during flush.
    FLUSH_FLAG_RETRY,
    // Retrying due to IME sync exceptions during flush.
    FLUSH_FLAG_RECOVER
  };

  enum RemoveCompositionFlag { CANCEL_IME_COMPOSITION, COMMIT_IME_COMPOSITION };

  const bool mIsRemote;
  jni::NativeWeakPtr<GeckoViewSupport> mWindow;  // Parent only
  RefPtr<TextEventDispatcher> mDispatcher;
  java::GeckoEditableChild::GlobalRef mEditable;
  bool mEditableAttached;
  InputContext mInputContext;
  AutoTArray<UniquePtr<mozilla::WidgetEvent>, 4> mIMEKeyEvents;
  IMENotification::TextChangeData mIMEPendingTextChange;
  RefPtr<TextRangeArray> mIMERanges;
  RefPtr<Runnable> mDisposeRunnable;
  int32_t mIMEMaskEventsCount;         // Mask events when > 0.
  int32_t mIMEFocusCount;              // We are focused when > 0.
  bool mIMEDelaySynchronizeReply;      // We reply asynchronously when true.
  int32_t mIMEActiveSynchronizeCount;  // The number of replies being delayed.
  int32_t mIMEActiveCompositionCount;  // The number of compositions expected.
  uint32_t mDisposeBlockCount;
  bool mIMESelectionChanged;
  bool mIMETextChangedDuringFlush;
  bool mIMEMonitorCursor;

  // The cached selection data
  struct Selection {
    Selection() : mStartOffset(-1), mEndOffset(-1) {}

    void Reset() {
      mStartOffset = -1;
      mEndOffset = -1;
    }

    bool IsValid() const { return mStartOffset >= 0 && mEndOffset >= 0; }

    int32_t mStartOffset;
    int32_t mEndOffset;
  } mCachedSelection;

  nsIWidget* GetWidget() const;
  nsWindow* GetNsWindow() const;

  nsresult BeginInputTransaction(TextEventDispatcher* aDispatcher) {
    if (mIsRemote) {
      return aDispatcher->BeginInputTransaction(this);
    } else {
      return aDispatcher->BeginNativeInputTransaction();
    }
  }

  virtual ~GeckoEditableSupport() {}

  RefPtr<TextComposition> GetComposition() const;
  bool RemoveComposition(RemoveCompositionFlag aFlag = COMMIT_IME_COMPOSITION);
  void SendIMEDummyKeyEvent(nsIWidget* aWidget, EventMessage msg);
  void AddIMETextChange(const IMENotification::TextChangeDataBase& aChange);
  void PostFlushIMEChanges();
  void FlushIMEChanges(FlushChangesFlag aFlags = FLUSH_FLAG_NONE);
  void FlushIMEText(FlushChangesFlag aFlags = FLUSH_FLAG_NONE);
  void AsyncNotifyIME(int32_t aNotification);
  void UpdateCompositionRects();
  bool DoReplaceText(int32_t aStart, int32_t aEnd, jni::String::Param aText);
  bool DoUpdateComposition(int32_t aStart, int32_t aEnd, int32_t aFlags);
  void OnNotifyIMEOfCompositionEventHandled();
  void NotifyIMEContext(const InputContext& aContext,
                        const InputContextAction& aAction);

 public:
  template <typename Functor>
  static void OnNativeCall(Functor&& aCall) {
    struct IMEEvent : nsAppShell::LambdaEvent<Functor> {
      explicit IMEEvent(Functor&& l)
          : nsAppShell::LambdaEvent<Functor>(std::move(l)) {}

      bool IsUIEvent() const override {
        using GES = GeckoEditableSupport;
        if (this->lambda.IsTarget(&GES::OnKeyEvent) ||
            this->lambda.IsTarget(&GES::OnImeReplaceText) ||
            this->lambda.IsTarget(&GES::OnImeUpdateComposition)) {
          return true;
        }
        return false;
      }

      void Run() override {
        if (NS_WARN_IF(!this->lambda.GetNativeObject())) {
          // Ignore stale calls after disposal.
          jni::GetGeckoThreadEnv()->ExceptionClear();
          return;
        }
        nsAppShell::LambdaEvent<Functor>::Run();
      }
    };
    nsAppShell::PostEvent(mozilla::MakeUnique<IMEEvent>(std::move(aCall)));
  }

  static void SetOnBrowserChild(dom::BrowserChild* aBrowserChild);

  // Constructor for main process GeckoEditableChild.
  GeckoEditableSupport(jni::NativeWeakPtr<GeckoViewSupport> aWindow,
                       java::GeckoEditableChild::Param aEditableChild)
      : mIsRemote(!aWindow.IsAttached()),
        mWindow(aWindow),
        mEditable(aEditableChild),
        mEditableAttached(!mIsRemote),
        mIMERanges(new TextRangeArray()),
        mIMEMaskEventsCount(1),  // Mask IME events since there's no focus yet
        mIMEFocusCount(0),
        mIMEDelaySynchronizeReply(false),
        mIMEActiveSynchronizeCount(0),
        mDisposeBlockCount(0),
        mIMESelectionChanged(false),
        mIMETextChangedDuringFlush(false),
        mIMEMonitorCursor(false) {}

  // Constructor for content process GeckoEditableChild.
  explicit GeckoEditableSupport(java::GeckoEditableChild::Param aEditableChild)
      : GeckoEditableSupport(nullptr, aEditableChild) {}

  NS_DECL_ISUPPORTS

  // TextEventDispatcherListener methods
  NS_IMETHOD NotifyIME(TextEventDispatcher* aTextEventDispatcher,
                       const IMENotification& aNotification) override;

  NS_IMETHOD_(IMENotificationRequests) GetIMENotificationRequests() override;

  NS_IMETHOD_(void)
  OnRemovedFrom(TextEventDispatcher* aTextEventDispatcher) override;

  NS_IMETHOD_(void)
  WillDispatchKeyboardEvent(TextEventDispatcher* aTextEventDispatcher,
                            WidgetKeyboardEvent& aKeyboardEvent,
                            uint32_t aIndexOfKeypress, void* aData) override;

  void SetInputContext(const InputContext& aContext,
                       const InputContextAction& aAction);

  InputContext GetInputContext();

  bool HasIMEFocus() const { return mIMEFocusCount != 0; }

  void AddBlocker() { mDisposeBlockCount++; }

  void ReleaseBlocker() {
    mDisposeBlockCount--;

    if (!mDisposeBlockCount && mDisposeRunnable) {
      if (HasIMEFocus()) {
        // If we have IME focus, GeckoEditableChild is already attached again.
        // So disposer is unnecessary.
        mDisposeRunnable = nullptr;
        return;
      }

      RefPtr<GeckoEditableSupport> self(this);
      RefPtr<Runnable> disposer = std::move(mDisposeRunnable);

      nsAppShell::PostEvent(
          [self = std::move(self), disposer = std::move(disposer)] {
            self->mEditableAttached = false;
            disposer->Run();
          });
    }
  }

  bool IsGeckoEditableUsed() const { return mDisposeBlockCount != 0; }

  // GeckoEditableChild methods
  using EditableBase::AttachNative;
  using EditableBase::DisposeNative;

  const java::GeckoEditableChild::Ref& GetJavaEditable() { return mEditable; }

  void OnWeakNonIntrusiveDetach(already_AddRefed<Runnable> aDisposer) {
    RefPtr<GeckoEditableSupport> self(this);
    nsAppShell::PostEvent(
        [self = std::move(self), disposer = RefPtr<Runnable>(aDisposer)] {
          if (self->IsGeckoEditableUsed()) {
            // Current calling stack uses GeckoEditableChild, so we should
            // not dispose it now.
            self->mDisposeRunnable = disposer;
            return;
          }
          self->mEditableAttached = false;
          disposer->Run();
        });
  }

  // Transfer to a new parent.
  void TransferParent(jni::Object::Param aEditableParent);

  // Handle an Android KeyEvent.
  void OnKeyEvent(int32_t aAction, int32_t aKeyCode, int32_t aScanCode,
                  int32_t aMetaState, int32_t aKeyPressMetaState, int64_t aTime,
                  int32_t aDomPrintableKeyValue, int32_t aRepeatCount,
                  int32_t aFlags, bool aIsSynthesizedImeKey,
                  jni::Object::Param originalEvent);

  // Synchronize Gecko thread with the InputConnection thread.
  void OnImeSynchronize();

  // Replace a range of text with new text.
  void OnImeReplaceText(int32_t aStart, int32_t aEnd, jni::String::Param aText);

  // Add styling for a range within the active composition.
  void OnImeAddCompositionRange(int32_t aStart, int32_t aEnd,
                                int32_t aRangeType, int32_t aRangeStyle,
                                int32_t aRangeLineStyle, bool aRangeBoldLine,
                                int32_t aRangeForeColor,
                                int32_t aRangeBackColor,
                                int32_t aRangeLineColor);

  // Update styling for the active composition using previous-added ranges.
  void OnImeUpdateComposition(int32_t aStart, int32_t aEnd, int32_t aFlags);

  // Set cursor mode whether IME requests
  void OnImeRequestCursorUpdates(int aRequestMode);

  // Commit current composition to sync Gecko text state with Java.
  void OnImeRequestCommit();

  // Insert image from software keyboard.
  void OnImeInsertImage(jni::ByteArray::Param aData,
                        jni::String::Param aMimeType);
};

}  // namespace widget
}  // namespace mozilla

#endif  // mozilla_widget_GeckoEditableSupport_h
