// Copyright (c) 2012 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 "ui/base/ime/ibus_client_impl.h"

#include <ibus.h>
#include <X11/X.h>
#include <X11/Xlib.h>
#undef FocusIn
#undef FocusOut

#include "base/basictypes.h"
#include "base/i18n/char_iterator.h"
#include "base/logging.h"
#include "base/utf_string_conversions.h"
#include "ui/base/ime/composition_text.h"

// input_method_ibus.cc assumes X and IBus use the same mask for Lock, Control,
// Shift, Alt, and buttons. Check if the assumption is really correct.
COMPILE_ASSERT(IBUS_LOCK_MASK == LockMask, test_mask);
COMPILE_ASSERT(IBUS_CONTROL_MASK == ControlMask, test_mask);
COMPILE_ASSERT(IBUS_SHIFT_MASK == ShiftMask, test_mask);
COMPILE_ASSERT(IBUS_MOD1_MASK == Mod1Mask, test_mask);
COMPILE_ASSERT(IBUS_BUTTON1_MASK == Button1Mask, test_mask);
COMPILE_ASSERT(IBUS_BUTTON2_MASK == Button2Mask, test_mask);
COMPILE_ASSERT(IBUS_BUTTON3_MASK == Button3Mask, test_mask);
COMPILE_ASSERT(IBUS_RELEASE_MASK == ui::kIBusReleaseMask, test_mask);

namespace ui {
namespace internal {

namespace {

const char kClientName[] = "chrome";

XKeyEvent* GetKeyEvent(XEvent* event) {
  DCHECK(event && (event->type == KeyPress || event->type == KeyRelease));
  return &event->xkey;
}

void ProcessKeyEventDone(IBusInputContext* context,
                         GAsyncResult* res,
                         IBusClient::PendingKeyEvent* data) {
  DCHECK(context);
  DCHECK(res);
  DCHECK(data);
  const gboolean handled = ibus_input_context_process_key_event_async_finish(
      context, res, NULL);
  data->ProcessPostIME(handled);
  delete data;
}

void CreateInputContextDone(IBusBus* bus,
                            GAsyncResult* res,
                            IBusClient::PendingCreateICRequest* data) {
  DCHECK(bus);
  DCHECK(res);
  DCHECK(data);
  IBusInputContext* context =
      ibus_bus_create_input_context_async_finish(bus, res, NULL);
  if (context)
    data->StoreOrAbandonInputContext(context);
  delete data;
}

}  // namespace

IBusClientImpl::IBusClientImpl() {
}

IBusClientImpl::~IBusClientImpl() {
}

IBusBus* IBusClientImpl::GetConnection() {
  ibus_init();
  return ibus_bus_new();
}

bool IBusClientImpl::IsConnected(IBusBus* bus) {
  return ibus_bus_is_connected(bus) == TRUE;
}

void IBusClientImpl::CreateContext(IBusBus* bus,
                                   PendingCreateICRequest* request) {
  ibus_bus_create_input_context_async(
      bus,
      kClientName,
      -1,  // no timeout
      NULL,  // no cancellation object
      reinterpret_cast<GAsyncReadyCallback>(CreateInputContextDone),
      request);
}

void IBusClientImpl::DestroyProxy(IBusInputContext* context) {
  // ibus_proxy_destroy() will not really release the object, caller still need
  // to call g_object_unref() explicitly.
  ibus_proxy_destroy(IBUS_PROXY(context));
}

void IBusClientImpl::SetCapabilities(IBusInputContext* context,
                                     InlineCompositionCapability inline_type) {
  // TODO(penghuang): support surrounding text.
  guint32 capabilities =
      inline_type == INLINE_COMPOSITION ? IBUS_CAP_PREEDIT_TEXT | IBUS_CAP_FOCUS
                                        : IBUS_CAP_FOCUS;
  ibus_input_context_set_capabilities(context, capabilities);
}

void IBusClientImpl::FocusIn(IBusInputContext* context) {
  ibus_input_context_focus_in(context);
}

void IBusClientImpl::FocusOut(IBusInputContext* context) {
  ibus_input_context_focus_out(context);
}

void IBusClientImpl::Reset(IBusInputContext* context) {
  ibus_input_context_reset(context);
}

IBusClient::InputMethodType IBusClientImpl::GetInputMethodType() {
  // This object cannot know the type of the current IME, hence return NORMAL.
  return INPUT_METHOD_NORMAL;
}

void IBusClientImpl::SetCursorLocation(IBusInputContext* context,
                                       int32 x,
                                       int32 y,
                                       int32 w,
                                       int32 h) {
  ibus_input_context_set_cursor_location(context, x, y, w, h);
}

void IBusClientImpl::SendKeyEvent(IBusInputContext* context,
                                  uint32 keyval,
                                  uint32 keycode,
                                  uint32 state,
                                  PendingKeyEvent* pending_key) {
  // Note:
  // 1. We currently set timeout to -1, because ibus doesn't have a mechanism to
  // associate input method results to corresponding key event, thus there is
  // actually no way to abandon results generated by a specific key event. So we
  // actually cannot abandon a specific key event and its result but accept
  // following key events and their results. So a timeout to abandon a key event
  // will not work.
  // 2. We set GCancellable to NULL, because the operation of cancelling a async
  // request also happens asynchronously, thus it's actually useless to us.
  //
  // The fundemental problem of ibus' async API is: it uses Glib's GIO API to
  // realize async communication, but in fact, GIO API is specially designed for
  // concurrent tasks, though it supports async communication as well, the API
  // is much more complicated than an ordinary message based async communication
  // API (such as Chrome's IPC).
  // Thus it's very complicated, if not impossible, to implement a client that
  // fully utilize asynchronous communication without potential problem.
  ibus_input_context_process_key_event_async(
      context,
      keyval,
      keycode,
      state,
      -1,  // no timeout
      NULL,  // no cancellation object
      reinterpret_cast<GAsyncReadyCallback>(ProcessKeyEventDone),
      pending_key);
}

// TODO(yusukes): Write a unit test for this function once build bots start
// supporting ibus-1.4.
void IBusClientImpl::ExtractCompositionText(IBusText* text,
                                            guint cursor_position,
                                            CompositionText* out_composition) {
  out_composition->Clear();
  out_composition->text = UTF8ToUTF16(text->text);

  if (out_composition->text.empty())
    return;

  // ibus uses character index for cursor position and attribute range, but we
  // use char16 offset for them. So we need to do conversion here.
  std::vector<size_t> char16_offsets;
  size_t length = out_composition->text.length();
  base::i18n::UTF16CharIterator char_iterator(&out_composition->text);
  do {
    char16_offsets.push_back(char_iterator.array_pos());
  } while (char_iterator.Advance());

  // The text length in Unicode characters.
  guint char_length = static_cast<guint>(char16_offsets.size());
  // Make sure we can convert the value of |char_length| as well.
  char16_offsets.push_back(length);

  size_t cursor_offset =
      char16_offsets[std::min(char_length, cursor_position)];

  out_composition->selection = Range(cursor_offset);
  if (text->attrs) {
    guint i = 0;
    while (true) {
      IBusAttribute* attr = ibus_attr_list_get(text->attrs, i++);
      if (!attr)
        break;
      if (attr->type != IBUS_ATTR_TYPE_UNDERLINE &&
          attr->type != IBUS_ATTR_TYPE_BACKGROUND) {
        continue;
      }
      guint start = std::min(char_length, attr->start_index);
      guint end = std::min(char_length, attr->end_index);
      if (start >= end)
        continue;
      CompositionUnderline underline(
          char16_offsets[start], char16_offsets[end],
          SK_ColorBLACK, false /* thick */);
      if (attr->type == IBUS_ATTR_TYPE_BACKGROUND) {
        underline.thick = true;
        // If the cursor is at start or end of this underline, then we treat
        // it as the selection range as well, but make sure to set the cursor
        // position to the selection end.
        if (underline.start_offset == cursor_offset) {
          out_composition->selection.set_start(underline.end_offset);
          out_composition->selection.set_end(cursor_offset);
        } else if (underline.end_offset == cursor_offset) {
          out_composition->selection.set_start(underline.start_offset);
          out_composition->selection.set_end(cursor_offset);
        }
      } else if (attr->type == IBUS_ATTR_TYPE_UNDERLINE) {
        if (attr->value == IBUS_ATTR_UNDERLINE_DOUBLE)
          underline.thick = true;
        else if (attr->value == IBUS_ATTR_UNDERLINE_ERROR)
          underline.color = SK_ColorRED;
      }
      out_composition->underlines.push_back(underline);
    }
  }

  // Use a black thin underline by default.
  if (out_composition->underlines.empty()) {
    out_composition->underlines.push_back(CompositionUnderline(
        0, length, SK_ColorBLACK, false /* thick */));
  }
}

string16 IBusClientImpl::ExtractCommitText(IBusText* text) {
  if (!text || !text->text)
    return WideToUTF16(L"");
  return UTF8ToUTF16(text->text);
}

}  // namespace internal
}  // namespace ui