// Copyright 2013 The Flutter 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 "flutter/shell/platform/linux/fl_accessible_text_field.h"
#include "flutter/shell/platform/linux/public/flutter_linux/fl_standard_message_codec.h"
#include "flutter/shell/platform/linux/public/flutter_linux/fl_value.h"

G_DEFINE_AUTOPTR_CLEANUP_FUNC(PangoContext, g_object_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(PangoLayout, g_object_unref)

typedef bool (*FlTextBoundaryCallback)(const PangoLogAttr* attr);

struct _FlAccessibleTextField {
  FlAccessibleNode parent_instance;

  gint selection_base;
  gint selection_extent;
  GtkEntryBuffer* buffer;
  FlutterTextDirection text_direction;
};

static void fl_accessible_text_iface_init(AtkTextIface* iface);
static void fl_accessible_editable_text_iface_init(AtkEditableTextIface* iface);

G_DEFINE_TYPE_WITH_CODE(
    FlAccessibleTextField,
    fl_accessible_text_field,
    FL_TYPE_ACCESSIBLE_NODE,
    G_IMPLEMENT_INTERFACE(ATK_TYPE_TEXT, fl_accessible_text_iface_init)
        G_IMPLEMENT_INTERFACE(ATK_TYPE_EDITABLE_TEXT,
                              fl_accessible_editable_text_iface_init))

static gchar* get_substring(FlAccessibleTextField* self,
                            glong start,
                            glong end) {
  const gchar* value = gtk_entry_buffer_get_text(self->buffer);
  if (end == -1) {
    // g_utf8_substring() accepts -1 since 2.72
    end = g_utf8_strlen(value, -1);
  }
  return g_utf8_substring(value, start, end);
}

static PangoContext* get_pango_context(FlAccessibleTextField* self) {
  PangoFontMap* font_map = pango_cairo_font_map_get_default();
  PangoContext* context = pango_font_map_create_context(font_map);
  pango_context_set_base_dir(context,
                             self->text_direction == kFlutterTextDirectionRTL
                                 ? PANGO_DIRECTION_RTL
                                 : PANGO_DIRECTION_LTR);
  return context;
}

static PangoLayout* create_pango_layout(FlAccessibleTextField* self) {
  g_autoptr(PangoContext) context = get_pango_context(self);
  PangoLayout* layout = pango_layout_new(context);
  pango_layout_set_text(layout, gtk_entry_buffer_get_text(self->buffer), -1);
  return layout;
}

static gchar* get_string_at_offset(FlAccessibleTextField* self,
                                   gint start,
                                   gint end,
                                   FlTextBoundaryCallback is_start,
                                   FlTextBoundaryCallback is_end,
                                   gint* start_offset,
                                   gint* end_offset) {
  g_autoptr(PangoLayout) layout = create_pango_layout(self);

  gint n_attrs = 0;
  const PangoLogAttr* attrs =
      pango_layout_get_log_attrs_readonly(layout, &n_attrs);

  while (start > 0 && !is_start(&attrs[start])) {
    --start;
  }
  if (start_offset != nullptr) {
    *start_offset = start;
  }

  while (end < n_attrs && !is_end(&attrs[end])) {
    ++end;
  }
  if (end_offset != nullptr) {
    *end_offset = end;
  }

  return get_substring(self, start, end);
}

static gchar* get_char_at_offset(FlAccessibleTextField* self,
                                 gint offset,
                                 gint* start_offset,
                                 gint* end_offset) {
  return get_string_at_offset(
      self, offset, offset + 1,
      [](const PangoLogAttr* attr) -> bool { return attr->is_char_break; },
      [](const PangoLogAttr* attr) -> bool { return attr->is_char_break; },
      start_offset, end_offset);
}

static gchar* get_word_at_offset(FlAccessibleTextField* self,
                                 gint offset,
                                 gint* start_offset,
                                 gint* end_offset) {
  return get_string_at_offset(
      self, offset, offset,
      [](const PangoLogAttr* attr) -> bool { return attr->is_word_start; },
      [](const PangoLogAttr* attr) -> bool { return attr->is_word_end; },
      start_offset, end_offset);
}

static gchar* get_sentence_at_offset(FlAccessibleTextField* self,
                                     gint offset,
                                     gint* start_offset,
                                     gint* end_offset) {
  return get_string_at_offset(
      self, offset, offset,
      [](const PangoLogAttr* attr) -> bool { return attr->is_sentence_start; },
      [](const PangoLogAttr* attr) -> bool { return attr->is_sentence_end; },
      start_offset, end_offset);
}

static gchar* get_line_at_offset(FlAccessibleTextField* self,
                                 gint offset,
                                 gint* start_offset,
                                 gint* end_offset) {
  g_autoptr(PangoLayout) layout = create_pango_layout(self);

  GSList* lines = pango_layout_get_lines_readonly(layout);
  while (lines != nullptr) {
    PangoLayoutLine* line = static_cast<PangoLayoutLine*>(lines->data);
    if (offset >= line->start_index &&
        offset <= line->start_index + line->length) {
      if (start_offset != nullptr) {
        *start_offset = line->start_index;
      }
      if (end_offset != nullptr) {
        *end_offset = line->start_index + line->length;
      }
      return get_substring(self, line->start_index,
                           line->start_index + line->length);
    }
    lines = lines->next;
  }

  return nullptr;
}

static gchar* get_paragraph_at_offset(FlAccessibleTextField* self,
                                      gint offset,
                                      gint* start_offset,
                                      gint* end_offset) {
  g_autoptr(PangoLayout) layout = create_pango_layout(self);

  PangoLayoutLine* start = nullptr;
  PangoLayoutLine* end = nullptr;
  gint n_lines = pango_layout_get_line_count(layout);
  for (gint i = 0; i < n_lines; ++i) {
    PangoLayoutLine* line = pango_layout_get_line(layout, i);
    if (line->is_paragraph_start) {
      end = line;
    }
    if (start != nullptr && end != nullptr && offset >= start->start_index &&
        offset <= end->start_index + end->length) {
      if (start_offset != nullptr) {
        *start_offset = start->start_index;
      }
      if (end_offset != nullptr) {
        *end_offset = end->start_index + end->length;
      }
      return get_substring(self, start->start_index,
                           end->start_index + end->length);
    }
    if (line->is_paragraph_start) {
      start = line;
    }
  }

  return nullptr;
}

static void perform_set_text_action(FlAccessibleTextField* self,
                                    const char* text) {
  g_autoptr(FlValue) value = fl_value_new_string(text);
  g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new();
  g_autoptr(GBytes) message =
      fl_message_codec_encode_message(FL_MESSAGE_CODEC(codec), value, nullptr);

  fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(self),
                                    kFlutterSemanticsActionSetText, message);
}

static void perform_set_selection_action(FlAccessibleTextField* self,
                                         gint base,
                                         gint extent) {
  g_autoptr(FlValue) value = fl_value_new_map();
  fl_value_set_string_take(value, "base", fl_value_new_int(base));
  fl_value_set_string_take(value, "extent", fl_value_new_int(extent));

  g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new();
  g_autoptr(GBytes) message =
      fl_message_codec_encode_message(FL_MESSAGE_CODEC(codec), value, nullptr);

  fl_accessible_node_perform_action(
      FL_ACCESSIBLE_NODE(self), kFlutterSemanticsActionSetSelection, message);
}

// Implements GObject::dispose.
static void fl_accessible_text_field_dispose(GObject* object) {
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(object);

  g_clear_object(&self->buffer);

  G_OBJECT_CLASS(fl_accessible_text_field_parent_class)->dispose(object);
}

// Implements FlAccessibleNode::set_value.
static void fl_accessible_text_field_set_value(FlAccessibleNode* node,
                                               const gchar* value) {
  g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(node));
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(node);

  if (g_strcmp0(gtk_entry_buffer_get_text(self->buffer), value) == 0) {
    return;
  }

  gtk_entry_buffer_set_text(self->buffer, value, -1);
}

// Implements FlAccessibleNode::set_text_selection.
static void fl_accessible_text_field_set_text_selection(FlAccessibleNode* node,
                                                        gint base,
                                                        gint extent) {
  g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(node));
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(node);

  gboolean caret_moved = extent != self->selection_extent;
  gboolean has_selection = base != extent;
  gboolean had_selection = self->selection_base != self->selection_extent;
  gboolean selection_changed = (has_selection || had_selection) &&
                               (caret_moved || base != self->selection_base);

  self->selection_base = base;
  self->selection_extent = extent;

  if (selection_changed) {
    g_signal_emit_by_name(self, "text-selection-changed", nullptr);
  }

  if (caret_moved) {
    g_signal_emit_by_name(self, "text-caret-moved", extent, nullptr);
  }
}

// Implements FlAccessibleNode::set_text_direction.
static void fl_accessible_text_field_set_text_direction(
    FlAccessibleNode* node,
    FlutterTextDirection direction) {
  g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(node));
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(node);

  self->text_direction = direction;
}

// Overrides FlAccessibleNode::perform_action.
void fl_accessible_text_field_perform_action(FlAccessibleNode* self,
                                             FlutterSemanticsAction action,
                                             GBytes* data) {
  FlAccessibleNodeClass* parent_class =
      FL_ACCESSIBLE_NODE_CLASS(fl_accessible_text_field_parent_class);

  switch (action) {
    case kFlutterSemanticsActionMoveCursorForwardByCharacter:
    case kFlutterSemanticsActionMoveCursorBackwardByCharacter:
    case kFlutterSemanticsActionMoveCursorForwardByWord:
    case kFlutterSemanticsActionMoveCursorBackwardByWord: {
      // These actions require a boolean argument that indicates whether the
      // selection should be extended or collapsed when moving the cursor.
      g_autoptr(FlValue) extend_selection = fl_value_new_bool(false);
      g_autoptr(FlStandardMessageCodec) codec = fl_standard_message_codec_new();
      g_autoptr(GBytes) message = fl_message_codec_encode_message(
          FL_MESSAGE_CODEC(codec), extend_selection, nullptr);
      parent_class->perform_action(self, action, message);
      break;
    }
    default:
      parent_class->perform_action(self, action, data);
      break;
  }
}

// Implements AtkText::get_character_count.
static gint fl_accessible_text_field_get_character_count(AtkText* text) {
  g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), 0);
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);

  return gtk_entry_buffer_get_length(self->buffer);
}

// Implements AtkText::get_text.
static gchar* fl_accessible_text_field_get_text(AtkText* text,
                                                gint start_offset,
                                                gint end_offset) {
  g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), nullptr);
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);

  return get_substring(self, start_offset, end_offset);
}

// Implements AtkText::get_string_at_offset.
static gchar* fl_accessible_text_field_get_string_at_offset(
    AtkText* text,
    gint offset,
    AtkTextGranularity granularity,
    gint* start_offset,
    gint* end_offset) {
  g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), nullptr);
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);

  switch (granularity) {
    case ATK_TEXT_GRANULARITY_CHAR:
      return get_char_at_offset(self, offset, start_offset, end_offset);
    case ATK_TEXT_GRANULARITY_WORD:
      return get_word_at_offset(self, offset, start_offset, end_offset);
    case ATK_TEXT_GRANULARITY_SENTENCE:
      return get_sentence_at_offset(self, offset, start_offset, end_offset);
    case ATK_TEXT_GRANULARITY_LINE:
      return get_line_at_offset(self, offset, start_offset, end_offset);
    case ATK_TEXT_GRANULARITY_PARAGRAPH:
      return get_paragraph_at_offset(self, offset, start_offset, end_offset);
    default:
      return nullptr;
  }
}

// Implements AtkText::get_text_at_offset (deprecated but still commonly used).
static gchar* fl_accessible_text_field_get_text_at_offset(
    AtkText* text,
    gint offset,
    AtkTextBoundary boundary_type,
    gint* start_offset,
    gint* end_offset) {
  switch (boundary_type) {
    case ATK_TEXT_BOUNDARY_CHAR:
      return fl_accessible_text_field_get_string_at_offset(
          text, offset, ATK_TEXT_GRANULARITY_CHAR, start_offset, end_offset);
      break;
    case ATK_TEXT_BOUNDARY_WORD_START:
    case ATK_TEXT_BOUNDARY_WORD_END:
      return fl_accessible_text_field_get_string_at_offset(
          text, offset, ATK_TEXT_GRANULARITY_WORD, start_offset, end_offset);
      break;
    case ATK_TEXT_BOUNDARY_SENTENCE_START:
    case ATK_TEXT_BOUNDARY_SENTENCE_END:
      return fl_accessible_text_field_get_string_at_offset(
          text, offset, ATK_TEXT_GRANULARITY_SENTENCE, start_offset,
          end_offset);
      break;
    case ATK_TEXT_BOUNDARY_LINE_START:
    case ATK_TEXT_BOUNDARY_LINE_END:
      return fl_accessible_text_field_get_string_at_offset(
          text, offset, ATK_TEXT_GRANULARITY_LINE, start_offset, end_offset);
      break;
    default:
      return nullptr;
  }
}

// Implements AtkText::get_caret_offset.
static gint fl_accessible_text_field_get_caret_offset(AtkText* text) {
  g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), -1);
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);

  return self->selection_extent;
}

// Implements AtkText::set_caret_offset.
static gboolean fl_accessible_text_field_set_caret_offset(AtkText* text,
                                                          gint offset) {
  g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false);
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);

  perform_set_selection_action(self, offset, offset);
  return true;
}

// Implements AtkText::get_n_selections.
static gint fl_accessible_text_field_get_n_selections(AtkText* text) {
  g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), 0);
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);

  if (self->selection_base == self->selection_extent) {
    return 0;
  }

  return 1;
}

// Implements AtkText::get_selection.
static gchar* fl_accessible_text_field_get_selection(AtkText* text,
                                                     gint selection_num,
                                                     gint* start_offset,
                                                     gint* end_offset) {
  g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), nullptr);
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);

  if (selection_num != 0 || self->selection_base == self->selection_extent) {
    return nullptr;
  }

  gint start = MIN(self->selection_base, self->selection_extent);
  gint end = MAX(self->selection_base, self->selection_extent);

  if (start_offset != nullptr) {
    *start_offset = start;
  }
  if (end_offset != nullptr) {
    *end_offset = end;
  }

  return get_substring(self, start, end);
}

// Implements AtkText::add_selection.
static gboolean fl_accessible_text_field_add_selection(AtkText* text,
                                                       gint start_offset,
                                                       gint end_offset) {
  g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false);
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);

  if (self->selection_base != self->selection_extent) {
    return false;
  }

  perform_set_selection_action(self, start_offset, end_offset);
  return true;
}

// Implements AtkText::remove_selection.
static gboolean fl_accessible_text_field_remove_selection(AtkText* text,
                                                          gint selection_num) {
  g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false);
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);

  if (selection_num != 0 || self->selection_base == self->selection_extent) {
    return false;
  }

  perform_set_selection_action(self, self->selection_extent,
                               self->selection_extent);
  return true;
}

// Implements AtkText::set_selection.
static gboolean fl_accessible_text_field_set_selection(AtkText* text,
                                                       gint selection_num,
                                                       gint start_offset,
                                                       gint end_offset) {
  g_return_val_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(text), false);
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(text);

  if (selection_num != 0) {
    return false;
  }

  perform_set_selection_action(self, start_offset, end_offset);
  return true;
}

// Implements AtkEditableText::set_text_contents.
static void fl_accessible_text_field_set_text_contents(
    AtkEditableText* editable_text,
    const gchar* string) {
  g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);

  perform_set_text_action(self, string);
}

// Implements AtkEditableText::insert_text.
static void fl_accessible_text_field_insert_text(AtkEditableText* editable_text,
                                                 const gchar* string,
                                                 gint length,
                                                 gint* position) {
  g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);

  *position +=
      gtk_entry_buffer_insert_text(self->buffer, *position, string, length);

  perform_set_text_action(self, gtk_entry_buffer_get_text(self->buffer));
  perform_set_selection_action(self, *position, *position);
}

// Implements AtkEditableText::delete_text.
static void fl_accessible_node_delete_text(AtkEditableText* editable_text,
                                           gint start_pos,
                                           gint end_pos) {
  g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);

  gtk_entry_buffer_delete_text(self->buffer, start_pos, end_pos - start_pos);

  perform_set_text_action(self, gtk_entry_buffer_get_text(self->buffer));
  perform_set_selection_action(self, start_pos, start_pos);
}

// Implement AtkEditableText::copy_text.
static void fl_accessible_text_field_copy_text(AtkEditableText* editable_text,
                                               gint start_pos,
                                               gint end_pos) {
  g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);

  perform_set_selection_action(self, start_pos, end_pos);

  fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(editable_text),
                                    kFlutterSemanticsActionCopy, nullptr);
}

// Implements AtkEditableText::cut_text.
static void fl_accessible_text_field_cut_text(AtkEditableText* editable_text,
                                              gint start_pos,
                                              gint end_pos) {
  g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);

  perform_set_selection_action(self, start_pos, end_pos);

  fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(editable_text),
                                    kFlutterSemanticsActionCut, nullptr);
}

// Implements AtkEditableText::paste_text.
static void fl_accessible_text_field_paste_text(AtkEditableText* editable_text,
                                                gint position) {
  g_return_if_fail(FL_IS_ACCESSIBLE_TEXT_FIELD(editable_text));
  FlAccessibleTextField* self = FL_ACCESSIBLE_TEXT_FIELD(editable_text);

  perform_set_selection_action(self, position, position);

  fl_accessible_node_perform_action(FL_ACCESSIBLE_NODE(editable_text),
                                    kFlutterSemanticsActionPaste, nullptr);
}

static void fl_accessible_text_field_class_init(
    FlAccessibleTextFieldClass* klass) {
  G_OBJECT_CLASS(klass)->dispose = fl_accessible_text_field_dispose;
  FL_ACCESSIBLE_NODE_CLASS(klass)->set_value =
      fl_accessible_text_field_set_value;
  FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_selection =
      fl_accessible_text_field_set_text_selection;
  FL_ACCESSIBLE_NODE_CLASS(klass)->set_text_direction =
      fl_accessible_text_field_set_text_direction;
  FL_ACCESSIBLE_NODE_CLASS(klass)->perform_action =
      fl_accessible_text_field_perform_action;
}

static void fl_accessible_text_iface_init(AtkTextIface* iface) {
  iface->get_character_count = fl_accessible_text_field_get_character_count;
  iface->get_text = fl_accessible_text_field_get_text;
  iface->get_text_at_offset = fl_accessible_text_field_get_text_at_offset;
  iface->get_string_at_offset = fl_accessible_text_field_get_string_at_offset;

  iface->get_caret_offset = fl_accessible_text_field_get_caret_offset;
  iface->set_caret_offset = fl_accessible_text_field_set_caret_offset;

  iface->get_n_selections = fl_accessible_text_field_get_n_selections;
  iface->get_selection = fl_accessible_text_field_get_selection;
  iface->add_selection = fl_accessible_text_field_add_selection;
  iface->remove_selection = fl_accessible_text_field_remove_selection;
  iface->set_selection = fl_accessible_text_field_set_selection;
}

static void fl_accessible_editable_text_iface_init(
    AtkEditableTextIface* iface) {
  iface->set_text_contents = fl_accessible_text_field_set_text_contents;
  iface->insert_text = fl_accessible_text_field_insert_text;
  iface->delete_text = fl_accessible_node_delete_text;

  iface->copy_text = fl_accessible_text_field_copy_text;
  iface->cut_text = fl_accessible_text_field_cut_text;
  iface->paste_text = fl_accessible_text_field_paste_text;
}

static void fl_accessible_text_field_init(FlAccessibleTextField* self) {
  self->selection_base = -1;
  self->selection_extent = -1;

  self->buffer = gtk_entry_buffer_new("", 0);

  g_signal_connect_object(
      self->buffer, "inserted-text",
      G_CALLBACK(+[](FlAccessibleTextField* self, guint position, gchar* chars,
                     guint n_chars) {
        g_signal_emit_by_name(self, "text-insert", position, n_chars, chars,
                              nullptr);
      }),
      self, G_CONNECT_SWAPPED);

  g_signal_connect_object(self->buffer, "deleted-text",
                          G_CALLBACK(+[](FlAccessibleTextField* self,
                                         guint position, guint n_chars) {
                            g_autofree gchar* chars = atk_text_get_text(
                                ATK_TEXT(self), position, position + n_chars);
                            g_signal_emit_by_name(self, "text-remove", position,
                                                  n_chars, chars, nullptr);
                          }),
                          self, G_CONNECT_SWAPPED);
}

FlAccessibleNode* fl_accessible_text_field_new(FlEngine* engine, int32_t id) {
  return FL_ACCESSIBLE_NODE(g_object_new(fl_accessible_text_field_get_type(),
                                         "engine", engine, "id", id, nullptr));
}
