// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "win8/metro_driver/ime/text_service.h"

#include <msctf.h>
#include <stddef.h>
#include <stdint.h>

#include "base/logging.h"
#include "base/macros.h"
#include "base/win/scoped_variant.h"
#include "ui/metro_viewer/ime_types.h"
#include "win8/metro_driver/ime/text_service_delegate.h"
#include "win8/metro_driver/ime/text_store.h"
#include "win8/metro_driver/ime/text_store_delegate.h"

// Architecture overview of input method support on Ash mode:
//
// Overview:
// On Ash mode, the system keyboard focus is owned by the metro_driver process
// while most of event handling are still implemented in the browser process.
// Thus the metro_driver basically works as a proxy that simply forwards
// keyevents to the metro_driver process. IME support must be involved somewhere
// in this flow.
//
// In short, we need to interact with an IME in the metro_driver process since
// TSF (Text Services Framework) runtime wants to processes keyevents while
// (and only while) the attached UI thread owns keyboard focus.
//
// Due to this limitation, we need to split IME handling into two parts, one
// is in the metro_driver process and the other is in the browser process.
// The metro_driver process is responsible for implementing the primary data
// store for the composition text and wiring it up with an IME via TSF APIs.
// On the other hand, the browser process is responsible for calculating
// character position in the composition text whenever the composition text
// is updated.
//
// IPC overview:
// Fortunately, we don't need so many IPC messages to support IMEs. In fact,
// only 4 messages are required to enable basic IME functionality.
//
//   metro_driver process -> browser process
//     Message Type:
//       - MetroViewerHostMsg_ImeCompositionChanged
//       - MetroViewerHostMsg_ImeTextCommitted
//     Message Routing:
//       TextServiceImpl
//         -> ChromeAppViewAsh
//         -- (process boundary) --
//         -> RemoteWindowTreeHostWin
//         -> RemoteInputMethodWin
//
//   browser process -> metro_driver process
//     Message Type:
//       - MetroViewerHostMsg_ImeCancelComposition
//       - MetroViewerHostMsg_ImeTextInputClientUpdated
//     Message Routing:
//       RemoteInputMethodWin
//         -> RemoteWindowTreeHostWin
//         -- (process boundary) --
//         -> ChromeAppViewAsh
//         -> TextServiceImpl
//
// Note that a keyevent may be forwarded through a different path. When a
// keyevent is not handled by an IME, such keyevent and subsequent character
// events will be sent from the metro_driver process to the browser process as
// following IPC messages.
//  - MetroViewerHostMsg_KeyDown
//  - MetroViewerHostMsg_KeyUp
//  - MetroViewerHostMsg_Character
//
// How TextServiceImpl works:
// Here is the list of the major tasks that are handled in TextServiceImpl.
//  - Manages a session object obtained from TSF runtime. We need them to call
//    most of TSF APIs.
//  - Handles OnDocumentChanged event. Whenever the document type is changed,
//    TextServiceImpl destroyes the current document and initializes new one
//    according to the given |input_scopes|.
//  - Stores the |composition_character_bounds_| passed from OnDocumentChanged
//    event so that an IME or on-screen keyboard can query the character
//    position synchronously.
// The most complicated part is the OnDocumentChanged handler. Since some IMEs
// such as Japanese IMEs drastically change their behavior depending on
// properties exposed from the virtual document, we need to set up a lot
// properties carefully and correctly. See DocumentBinding class in this file
// about what will be involved in this multi-phase construction. See also
// text_store.cc and input_scope.cc for more underlying details.

namespace metro_driver {
namespace {

// Japanese IME expects the default value of this compartment is
// TF_SENTENCEMODE_PHRASEPREDICT to emulate IMM32 behavior. This value is
// managed per thread, thus setting this value at once is sufficient. This
// value never affects non-Japanese IMEs.
bool InitializeSentenceMode(ITfThreadMgr* thread_manager,
                            TfClientId client_id) {
  base::win::ScopedComPtr<ITfCompartmentMgr> thread_compartment_manager;
  HRESULT hr = thread_compartment_manager.QueryFrom(thread_manager);
  if (FAILED(hr)) {
    LOG(ERROR) << "QueryFrom failed. hr = " << hr;
    return false;
  }
  base::win::ScopedComPtr<ITfCompartment> sentence_compartment;
  hr = thread_compartment_manager->GetCompartment(
      GUID_COMPARTMENT_KEYBOARD_INPUTMODE_SENTENCE,
      sentence_compartment.Receive());
  if (FAILED(hr)) {
    LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr;
    return false;
  }

  base::win::ScopedVariant sentence_variant;
  sentence_variant.Set(TF_SENTENCEMODE_PHRASEPREDICT);
  hr = sentence_compartment->SetValue(client_id, sentence_variant.ptr());
  if (FAILED(hr)) {
    LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr;
    return false;
  }
  return true;
}

// Initializes |context| as disabled context where IMEs will be disabled.
bool InitializeDisabledContext(ITfContext* context, TfClientId client_id) {
  base::win::ScopedComPtr<ITfCompartmentMgr> compartment_mgr;
  HRESULT hr = compartment_mgr.QueryFrom(context);
  if (FAILED(hr)) {
    LOG(ERROR) << "QueryFrom failed. hr = " << hr;
    return false;
  }

  base::win::ScopedComPtr<ITfCompartment> disabled_compartment;
  hr = compartment_mgr->GetCompartment(GUID_COMPARTMENT_KEYBOARD_DISABLED,
                                       disabled_compartment.Receive());
  if (FAILED(hr)) {
    LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr;
    return false;
  }

  base::win::ScopedVariant variant;
  variant.Set(1);
  hr = disabled_compartment->SetValue(client_id, variant.ptr());
  if (FAILED(hr)) {
    LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr;
    return false;
  }

  base::win::ScopedComPtr<ITfCompartment> empty_context;
  hr = compartment_mgr->GetCompartment(GUID_COMPARTMENT_EMPTYCONTEXT,
                                       empty_context.Receive());
  if (FAILED(hr)) {
    LOG(ERROR) << "ITfCompartment::GetCompartment failed. hr = " << hr;
    return false;
  }

  base::win::ScopedVariant empty_context_variant;
  empty_context_variant.Set(static_cast<int32_t>(1));
  hr = empty_context->SetValue(client_id, empty_context_variant.ptr());
  if (FAILED(hr)) {
    LOG(ERROR) << "ITfCompartment::SetValue failed. hr = " << hr;
    return false;
  }

  return true;
}

bool IsPasswordField(const std::vector<InputScope>& input_scopes) {
  return std::find(input_scopes.begin(), input_scopes.end(), IS_PASSWORD) !=
      input_scopes.end();
}

// A class that manages the lifetime of the event callback registration. When
// this object is destroyed, corresponding event callback will be unregistered.
class EventSink {
 public:
  EventSink(DWORD cookie, base::win::ScopedComPtr<ITfSource> source)
      : cookie_(cookie),
        source_(source) {}
  ~EventSink() {
    if (!source_.get() || cookie_ != TF_INVALID_COOKIE)
      return;
    source_->UnadviseSink(cookie_);
    cookie_ = TF_INVALID_COOKIE;
    source_.Release();
  }

 private:
  DWORD cookie_;
  base::win::ScopedComPtr<ITfSource> source_;
  DISALLOW_COPY_AND_ASSIGN(EventSink);
};

scoped_ptr<EventSink> CreateTextEditSink(ITfContext* context,
                                         ITfTextEditSink* text_store) {
  DCHECK(text_store);
  base::win::ScopedComPtr<ITfSource> source;
  DWORD cookie = TF_INVALID_EDIT_COOKIE;
  HRESULT hr = source.QueryFrom(context);
  if (FAILED(hr)) {
    LOG(ERROR) << "QueryFrom failed, hr = " << hr;
    return scoped_ptr<EventSink>();
  }
  hr = source->AdviseSink(IID_ITfTextEditSink, text_store, &cookie);
  if (FAILED(hr)) {
    LOG(ERROR) << "AdviseSink failed, hr = " << hr;
    return scoped_ptr<EventSink>();
  }
  return scoped_ptr<EventSink>(new EventSink(cookie, source));
}

// A set of objects that should have the same lifetime. Following things
// are maintained.
//  - TextStore: a COM object that abstracts text buffer. This object is
//      actually implemented by us in text_store.cc
//  - ITfDocumentMgr: a focusable unit in TSF. This object is implemented by
//      TSF runtime and works as a container of TextStore.
//  - EventSink: an object that ensures that the event callback between
//      TSF runtime and TextStore is unregistered when this object is destroyed.
class DocumentBinding {
 public:
  ~DocumentBinding() {
    if (!document_manager_.get())
      return;
    document_manager_->Pop(TF_POPF_ALL);
  }

  static scoped_ptr<DocumentBinding> Create(
      ITfThreadMgr* thread_manager,
      TfClientId client_id,
      const std::vector<InputScope>& input_scopes,
      HWND window_handle,
      TextStoreDelegate* delegate) {
    base::win::ScopedComPtr<ITfDocumentMgr> document_manager;
    HRESULT hr = thread_manager->CreateDocumentMgr(document_manager.Receive());
    if (FAILED(hr)) {
      LOG(ERROR) << "ITfThreadMgr::CreateDocumentMgr failed. hr = " << hr;
      return scoped_ptr<DocumentBinding>();
    }

    // Note: In our IPC protocol, an empty |input_scopes| is used to indicate
    // that an IME must be disabled in this context. In such case, we need not
    // instantiate TextStore.
    const bool use_null_text_store = input_scopes.empty();

    scoped_refptr<TextStore> text_store;
    if (!use_null_text_store) {
      text_store = TextStore::Create(window_handle, input_scopes, delegate);
      if (!text_store.get()) {
        LOG(ERROR) << "Failed to create TextStore.";
        return scoped_ptr<DocumentBinding>();
      }
    }

    base::win::ScopedComPtr<ITfContext> context;
    DWORD edit_cookie = TF_INVALID_EDIT_COOKIE;
    hr = document_manager->CreateContext(
        client_id,
        0,
        static_cast<ITextStoreACP*>(text_store.get()),
        context.Receive(),
        &edit_cookie);
    if (FAILED(hr)) {
      LOG(ERROR) << "ITfDocumentMgr::CreateContext failed. hr = " << hr;
      return scoped_ptr<DocumentBinding>();
    }

    // If null-TextStore is used or |input_scopes| looks like a password field,
    // set special properties to tell IMEs to be disabled.
    if ((use_null_text_store || IsPasswordField(input_scopes)) &&
        !InitializeDisabledContext(context.get(), client_id)) {
      LOG(ERROR) << "InitializeDisabledContext failed.";
      return scoped_ptr<DocumentBinding>();
    }

    scoped_ptr<EventSink> text_edit_sink;
    if (!use_null_text_store) {
      text_edit_sink = CreateTextEditSink(context.get(), text_store.get());
      if (!text_edit_sink) {
        LOG(ERROR) << "CreateTextEditSink failed.";
        return scoped_ptr<DocumentBinding>();
      }
    }
    hr = document_manager->Push(context.get());
    if (FAILED(hr)) {
      LOG(ERROR) << "ITfDocumentMgr::Push failed. hr = " << hr;
      return scoped_ptr<DocumentBinding>();
    }
    return scoped_ptr<DocumentBinding>(
        new DocumentBinding(text_store,
                            document_manager,
                            text_edit_sink.Pass()));
  }

  ITfDocumentMgr* document_manager() const { return document_manager_.get(); }

  scoped_refptr<TextStore> text_store() const {
    return text_store_;
  }

 private:
  DocumentBinding(scoped_refptr<TextStore> text_store,
                  base::win::ScopedComPtr<ITfDocumentMgr> document_manager,
                  scoped_ptr<EventSink> text_edit_sink)
      : text_store_(text_store),
        document_manager_(document_manager),
        text_edit_sink_(text_edit_sink.Pass()) {}

  scoped_refptr<TextStore> text_store_;
  base::win::ScopedComPtr<ITfDocumentMgr> document_manager_;
  scoped_ptr<EventSink> text_edit_sink_;

  DISALLOW_COPY_AND_ASSIGN(DocumentBinding);
};

class TextServiceImpl : public TextService,
                        public TextStoreDelegate {
 public:
  TextServiceImpl(ITfThreadMgr* thread_manager,
                  TfClientId client_id,
                  HWND window_handle,
                  TextServiceDelegate* delegate)
      : client_id_(client_id),
        window_handle_(window_handle),
        delegate_(delegate),
        thread_manager_(thread_manager) {
    DCHECK_NE(TF_CLIENTID_NULL, client_id);
    DCHECK(window_handle != NULL);
    DCHECK(thread_manager_.get());
  }
  ~TextServiceImpl() override {
    thread_manager_->Deactivate();
  }

 private:
  // TextService overrides:
  void CancelComposition() override {
    if (!current_document_) {
      VLOG(0) << "|current_document_| is NULL due to the previous error.";
      return;
    }
    scoped_refptr<TextStore> text_store = current_document_->text_store();
    if (!text_store.get())
      return;
    text_store->CancelComposition();
  }

  void OnDocumentChanged(const std::vector<int32_t>& input_scopes,
                         const std::vector<metro_viewer::CharacterBounds>&
                             character_bounds) override {
    bool document_type_changed = input_scopes_ != input_scopes;
    input_scopes_ = input_scopes;
    composition_character_bounds_ = character_bounds;
    if (document_type_changed)
      OnDocumentTypeChanged(input_scopes);
  }

  void OnWindowActivated() override {
    if (!current_document_) {
      VLOG(0) << "|current_document_| is NULL due to the previous error.";
      return;
    }
    ITfDocumentMgr* document_manager = current_document_->document_manager();
    if (!document_manager) {
      VLOG(0) << "|document_manager| is NULL due to the previous error.";
      return;
    }
    HRESULT hr = thread_manager_->SetFocus(document_manager);
    if (FAILED(hr)) {
      LOG(ERROR) << "ITfThreadMgr::SetFocus failed. hr = " << hr;
      return;
    }
  }

  void OnCompositionChanged(
      const base::string16& text,
      int32_t selection_start,
      int32_t selection_end,
      const std::vector<metro_viewer::UnderlineInfo>& underlines) override {
    if (!delegate_)
      return;
    delegate_->OnCompositionChanged(text,
                                    selection_start,
                                    selection_end,
                                    underlines);
  }

  void OnTextCommitted(const base::string16& text) override {
    if (!delegate_)
      return;
    delegate_->OnTextCommitted(text);
  }

  RECT GetCaretBounds() override {
    if (composition_character_bounds_.empty()) {
      const RECT rect = {};
      return rect;
    }
    const metro_viewer::CharacterBounds& bounds =
        composition_character_bounds_[0];
    POINT left_top = { bounds.left, bounds.top };
    POINT right_bottom = { bounds.right, bounds.bottom };
    ClientToScreen(window_handle_, &left_top);
    ClientToScreen(window_handle_, &right_bottom);
    const RECT rect = {
      left_top.x,
      left_top.y,
      right_bottom.x,
      right_bottom.y,
    };
    return rect;
  }

  bool GetCompositionCharacterBounds(uint32_t index, RECT* rect) override {
    if (index >= composition_character_bounds_.size()) {
      return false;
    }
    const metro_viewer::CharacterBounds& bounds =
        composition_character_bounds_[index];
    POINT left_top = { bounds.left, bounds.top };
    POINT right_bottom = { bounds.right, bounds.bottom };
    ClientToScreen(window_handle_, &left_top);
    ClientToScreen(window_handle_, &right_bottom);
    SetRect(rect, left_top.x, left_top.y, right_bottom.x, right_bottom.y);
    return true;
  }

  void OnDocumentTypeChanged(const std::vector<int32_t>& input_scopes) {
    std::vector<InputScope> native_input_scopes(input_scopes.size());
    for (size_t i = 0; i < input_scopes.size(); ++i)
      native_input_scopes[i] = static_cast<InputScope>(input_scopes[i]);
    scoped_ptr<DocumentBinding> new_document =
        DocumentBinding::Create(thread_manager_.get(),
                                client_id_,
                                native_input_scopes,
                                window_handle_,
                                this);
    LOG_IF(ERROR, !new_document) << "Failed to create a new document.";
    current_document_.swap(new_document);
    OnWindowActivated();
  }

  TfClientId client_id_;
  HWND window_handle_;
  TextServiceDelegate* delegate_;
  scoped_ptr<DocumentBinding> current_document_;
  base::win::ScopedComPtr<ITfThreadMgr> thread_manager_;

  // A vector of InputScope enumeration, which represents the document type of
  // the focused text field. Note that in our IPC message protocol, an empty
  // |input_scopes_| has special meaning that IMEs must be disabled on this
  // document.
  std::vector<int32_t> input_scopes_;
  // Character bounds of the composition. When there is no composition but this
  // vector is not empty, the first element contains the caret bounds.
  std::vector<metro_viewer::CharacterBounds> composition_character_bounds_;

  DISALLOW_COPY_AND_ASSIGN(TextServiceImpl);
};

}  // namespace

scoped_ptr<TextService>
CreateTextService(TextServiceDelegate* delegate, HWND window_handle) {
  if (!delegate)
    return scoped_ptr<TextService>();
  base::win::ScopedComPtr<ITfThreadMgr> thread_manager;
  HRESULT hr = thread_manager.CreateInstance(CLSID_TF_ThreadMgr);
  if (FAILED(hr)) {
    LOG(ERROR) << "Failed to create instance of CLSID_TF_ThreadMgr. hr = "
               << hr;
    return scoped_ptr<TextService>();
  }
  TfClientId client_id = TF_CLIENTID_NULL;
  hr = thread_manager->Activate(&client_id);
  if (FAILED(hr)) {
    LOG(ERROR) << "ITfThreadMgr::Activate failed. hr = " << hr;
    return scoped_ptr<TextService>();
  }
  if (!InitializeSentenceMode(thread_manager.get(), client_id)) {
    LOG(ERROR) << "InitializeSentenceMode failed.";
    thread_manager->Deactivate();
    return scoped_ptr<TextService>();
  }
  return scoped_ptr<TextService>(new TextServiceImpl(
      thread_manager.get(), client_id, window_handle, delegate));
}

}  // namespace metro_driver
