/* ide-cursor.c
 *
 * Copyright 2017 Anoop Chandu <anoopchandu96@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN "ide-cursor"

#include "config.h"

#include <dazzle.h>

#include "ide-source-view.h"
#include "ide-cursor.h"
#include "ide-text-util.h"

struct _IdeCursor
{
  GObject                      parent_instance;

  IdeSourceView               *source_view;
  GtkSourceSearchContext      *search_context;

  GList                       *cursors;

  GtkTextTag                  *highlight_tag;

  DzlSignalGroup              *operations_signals;

  guint                        overwrite : 1;
};

typedef struct
{
  GtkTextMark *selection_bound;
  GtkTextMark *insert;
} VirtualCursor;

G_DEFINE_FINAL_TYPE (IdeCursor, ide_cursor, G_TYPE_OBJECT)

enum {
  PROP_0,
  PROP_IDE_SOURCE_VIEW,
  N_PROPS
};

static GParamSpec *properties [N_PROPS];

static void ide_cursor_set_visible        (IdeCursor     *self,
                                           GtkTextBuffer *buffer,
                                           gboolean       visible);

static void ide_cursor_set_real_cursor    (IdeCursor     *self,
                                           GtkTextBuffer *buffer,
                                           VirtualCursor *vc);

static void ide_cursor_set_virtual_cursor (IdeCursor     *self,
                                           GtkTextBuffer *buffer,
                                           VirtualCursor *vc);

static void
ide_cursor_dispose (GObject *object)
{
  IdeCursor *self = (IdeCursor *)object;
  GtkTextBuffer *buffer = NULL;

  g_return_if_fail (IDE_IS_CURSOR (self));

  if (self->source_view != NULL)
    {
      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->source_view));
      if (self->highlight_tag != NULL)
        gtk_text_tag_table_remove (gtk_text_buffer_get_tag_table (buffer),
                                   self->highlight_tag);
      g_clear_weak_pointer (&self->source_view);
    }

  if (self->operations_signals != NULL)
    {
      dzl_signal_group_set_target (self->operations_signals, NULL);
      g_clear_object (&self->operations_signals);
    }

  g_clear_object (&self->highlight_tag);
  g_clear_object (&self->search_context);

  if (buffer != NULL && self->cursors != NULL)
    {
      for (const GList *iter = self->cursors; iter != NULL; iter = iter->next)
        {
          VirtualCursor *vc;

          vc = iter->data;

          if (buffer != NULL)
            {
              gtk_text_buffer_delete_mark (buffer, vc->insert);
              gtk_text_buffer_delete_mark (buffer, vc->selection_bound);
            }

          g_slice_free (VirtualCursor, vc);
        }

    }

  g_clear_pointer (&self->cursors, g_list_free);

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


/* toggles the visibility of cursors */
static void
ide_cursor_set_visible (IdeCursor      *self,
                        GtkTextBuffer  *buffer,
                        gboolean        visible)
{
  g_assert (IDE_IS_CURSOR (self));
  g_assert (GTK_IS_TEXT_BUFFER (buffer));

  if (self->cursors != NULL)
    {
      for (const GList *iter = self->cursors; iter != NULL; iter = iter->next)
        {
          VirtualCursor *vc;
          GtkTextIter selection_bound, insert;

          vc = iter->data;
          gtk_text_buffer_get_iter_at_mark (buffer, &selection_bound, vc->selection_bound);
          gtk_text_buffer_get_iter_at_mark (buffer, &insert, vc->insert);

          if (gtk_text_iter_equal (&insert, &selection_bound))
            {
              if (self->overwrite)
                {
                  gtk_text_iter_forward_char (&selection_bound);
                }
              else
                {
                  gtk_text_mark_set_visible (vc->insert, visible);
                  continue;
                }
            }

          if (visible)
            gtk_text_buffer_apply_tag (buffer, self->highlight_tag, &selection_bound, &insert);
          else
            gtk_text_buffer_remove_tag (buffer, self->highlight_tag, &selection_bound, &insert);
        }
    }
}

/* sets real cursor at virtual cursor position */
static void
ide_cursor_set_real_cursor (IdeCursor     *self,
                            GtkTextBuffer *buffer,
                            VirtualCursor *vc)
{
  GtkTextIter selection_bound, insert;

  g_assert (IDE_IS_CURSOR (self));
  g_assert (GTK_IS_TEXT_BUFFER (buffer));

  gtk_text_buffer_get_iter_at_mark (buffer, &selection_bound, vc->selection_bound);
  gtk_text_buffer_get_iter_at_mark (buffer, &insert, vc->insert);

  gtk_text_buffer_select_range (buffer, &insert, &selection_bound);
}

/* sets virutal cursor at actual cursor position */
static void
ide_cursor_set_virtual_cursor (IdeCursor     *self,
                               GtkTextBuffer *buffer,
                               VirtualCursor *vc)
{
  GtkTextIter selection_bound, insert;

  g_assert (IDE_IS_CURSOR (self));
  g_assert (GTK_IS_TEXT_BUFFER (buffer));

  gtk_text_buffer_get_iter_at_mark (buffer, &insert, gtk_text_buffer_get_insert (buffer));
  gtk_text_buffer_get_iter_at_mark (buffer, &selection_bound, gtk_text_buffer_get_selection_bound (buffer));

  gtk_text_buffer_move_mark (buffer, vc->selection_bound, &selection_bound);
  gtk_text_buffer_move_mark (buffer, vc->insert, &insert);
}

void
ide_cursor_remove_cursors (IdeCursor *self)
{
  g_return_if_fail (IDE_IS_CURSOR (self));

  if (self->cursors != NULL)
    {
      GtkTextBuffer *buffer;
      GtkTextIter begin, end;
      VirtualCursor *vc;

      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->source_view));

      ide_cursor_set_visible (self, buffer, FALSE);

      for (const GList *iter = self->cursors; iter != NULL; iter = iter->next)
        {
          vc = iter->data;

          gtk_text_buffer_delete_mark (buffer, vc->insert);
          gtk_text_buffer_delete_mark (buffer, vc->selection_bound);

          g_slice_free (VirtualCursor, vc);
        }

      g_clear_pointer (&self->cursors, g_list_free);

      gtk_text_buffer_get_bounds (buffer, &begin, &end);
      gtk_text_buffer_remove_tag (buffer, self->highlight_tag, &begin, &end);
    }
}

static void
ide_cursor_add_cursor_by_column (IdeCursor *self)
{
  GtkTextBuffer *buffer;
  GtkTextIter begin, end, temp;
  gint begin_line, begin_offset, end_line, end_offset, offset;
  GtkTextIter iter;

  g_assert (IDE_IS_CURSOR (self));

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->source_view));

  if (!gtk_text_buffer_get_selection_bounds (buffer, &begin, &end))
    return;

  gtk_text_buffer_get_iter_at_mark (buffer, &temp, gtk_text_buffer_get_insert (buffer));
  offset = gtk_text_iter_get_line_offset (&temp);

  begin_line = gtk_text_iter_get_line (&begin);
  begin_offset = gtk_text_iter_get_line_offset (&begin);
  end_line = gtk_text_iter_get_line (&end);
  end_offset = gtk_text_iter_get_line_offset (&end);

  if (begin_line == end_line)
    return;

  for (int i = begin_line; i <= end_line; i++)
    {
      VirtualCursor *vc;

      if ((i == begin_line && offset < begin_offset) ||
          (i == end_line && offset > end_offset))
        continue;

      gtk_text_buffer_get_iter_at_line_offset (buffer, &iter, i, offset);

      vc = g_slice_new0 (VirtualCursor);
      vc->selection_bound = gtk_text_buffer_create_mark (buffer, NULL, &iter, FALSE);

      vc->insert = gtk_text_buffer_create_mark (buffer, NULL, &iter, FALSE);
      self->cursors = g_list_prepend (self->cursors, vc);

      if (self->overwrite)
        {
          GtkTextIter iter1 = iter;

          gtk_text_iter_forward_char (&iter1);
          gtk_text_buffer_apply_tag (buffer, self->highlight_tag, &iter, &iter1);
        }
      else
        {
          gtk_text_mark_set_visible (vc->insert, TRUE);
        }
    }

  gtk_text_buffer_select_range (buffer, &iter, &iter);
}

static void
ide_cursor_add_cursor_by_position (IdeCursor *self)
{
  GtkTextIter insert_iter, selection_bound_iter;
  VirtualCursor *vc;
  GtkTextBuffer *buffer;

  g_assert (IDE_IS_CURSOR (self));

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->source_view));

  gtk_text_buffer_get_iter_at_mark (buffer, &insert_iter, gtk_text_buffer_get_insert (buffer));
  gtk_text_buffer_get_iter_at_mark (buffer, &selection_bound_iter, gtk_text_buffer_get_selection_bound (buffer));

  vc = g_slice_new0 (VirtualCursor);
  vc->selection_bound = gtk_text_buffer_create_mark (buffer, NULL, &insert_iter, FALSE);
  vc->insert = gtk_text_buffer_create_mark (buffer, NULL, &selection_bound_iter, FALSE);
  self->cursors = g_list_prepend (self->cursors, vc);

  if (gtk_text_iter_equal (&insert_iter, &selection_bound_iter))
    {
      if (self->overwrite)
        {
          gtk_text_iter_forward_char (&selection_bound_iter);
          gtk_text_buffer_apply_tag (buffer, self->highlight_tag, &insert_iter, &selection_bound_iter);
        }
      else
        {
          gtk_text_mark_set_visible (vc->insert, TRUE);
        }
    }
  else
    {
      gtk_text_buffer_apply_tag (buffer, self->highlight_tag, &insert_iter, &selection_bound_iter);
    }
}

static void
ide_cursor_add_cursor_by_match (IdeCursor *self)
{
  g_autofree gchar *text = NULL;
  GtkTextIter begin, end, match_begin, match_end;
  gboolean has_wrapped_around = FALSE;
  VirtualCursor *vc;
  GtkSourceSearchContext *search_context;
  GtkSourceSearchSettings *search_settings;
  GtkTextBuffer *buffer;

  g_assert (IDE_IS_CURSOR (self));

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->source_view));

  if (!gtk_text_buffer_get_selection_bounds (buffer, &begin, &end))
    return;
  gtk_text_iter_order (&begin, &end);

  text = gtk_text_buffer_get_text (buffer, &begin, &end, FALSE);

  search_context = self->search_context;
  search_settings = gtk_source_search_context_get_settings (search_context);

  if (g_strcmp0 (gtk_source_search_settings_get_search_text (search_settings), text) != 0)
    gtk_source_search_settings_set_search_text (search_settings, text);

  if (!gtk_source_search_context_forward (search_context, &end,
                                          &match_begin, &match_end, &has_wrapped_around))
    return;

  if (self->cursors == NULL)
    {
      vc = g_slice_new0 (VirtualCursor);
      vc->selection_bound = gtk_text_buffer_create_mark (buffer, NULL, &begin, FALSE);
      vc->insert = gtk_text_buffer_create_mark (buffer, NULL, &end, FALSE);

      self->cursors = g_list_prepend (self->cursors, vc);

      gtk_text_buffer_apply_tag (buffer, self->highlight_tag, &begin, &end);
    }

  vc = g_slice_new0 (VirtualCursor);
  vc->selection_bound = gtk_text_buffer_create_mark (buffer, NULL, &match_begin, FALSE);
  vc->insert = gtk_text_buffer_create_mark (buffer, NULL, &match_end, FALSE);

  self->cursors = g_list_prepend (self->cursors, vc);

  gtk_text_buffer_apply_tag (buffer, self->highlight_tag, &match_begin, &match_end);

  gtk_text_buffer_select_range (buffer, &match_begin, &match_end);

  ide_source_view_scroll_mark_onscreen (self->source_view, vc->insert, TRUE, 0.5, 0.5);
}

void
ide_cursor_add_cursor (IdeCursor      *self,
                       IdeCursorType   type)
{
  g_return_if_fail (IDE_IS_CURSOR (self));
  g_return_if_fail (type<=IDE_CURSOR_MATCH);

  if (type == IDE_CURSOR_COLUMN)
    ide_cursor_add_cursor_by_column (self);
  else if (type == IDE_CURSOR_SELECT)
    ide_cursor_add_cursor_by_position (self);
  else if (type == IDE_CURSOR_MATCH)
    ide_cursor_add_cursor_by_match (self);
}

void
ide_cursor_insert_text (IdeCursor *self,
                        gchar     *text,
                        gint       len)
{
  g_return_if_fail (IDE_IS_CURSOR (self));

  if (self->cursors != NULL)
    {
      GtkTextBuffer *buffer;

      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->source_view));

      ide_cursor_set_visible (self, buffer, FALSE);
      ide_cursor_set_virtual_cursor (self, buffer, self->cursors->data);

      for (const GList *iter = self->cursors->next; iter != NULL; iter = iter->next)
        {
          VirtualCursor *vc = iter->data;
          GtkTextIter begin, end;

          gtk_text_buffer_get_iter_at_mark (buffer, &begin, vc->insert);
          gtk_text_buffer_get_iter_at_mark (buffer, &end, vc->selection_bound);

          if (gtk_text_iter_equal (&begin, &end))
            {
              if (self->overwrite)
                {
                  gtk_text_iter_forward_char (&end);
                  gtk_text_buffer_delete (buffer, &begin, &end);
                  gtk_text_buffer_get_iter_at_mark (buffer, &end, vc->insert);
                }
              gtk_text_buffer_insert (buffer, &end, text, len);
            }
          else
            {
              gtk_text_buffer_delete (buffer, &begin, &end);
              gtk_text_buffer_get_iter_at_mark (buffer, &end, vc->insert);
              gtk_text_buffer_insert (buffer, &end, text, len);
            }
        }

      ide_cursor_set_visible (self, buffer, TRUE);
    }
}

static void
ide_cursor_backspace (GtkTextView *text_view,
                      IdeCursor   *self)
{
  g_assert (GTK_IS_TEXT_VIEW (text_view));
  g_assert (IDE_IS_CURSOR (self));

  if (self->cursors != NULL)
    {
      GtkTextBuffer *buffer;

      buffer = gtk_text_view_get_buffer (text_view);

      ide_cursor_set_visible (self, buffer, FALSE);
      ide_cursor_set_virtual_cursor (self, buffer, self->cursors->data);

      gtk_text_buffer_begin_user_action (buffer);

      for (const GList *iter = self->cursors->next; iter != NULL; iter = iter->next)
        {
          VirtualCursor *vc = iter->data;
          GtkTextIter begin, end;

          gtk_text_buffer_get_iter_at_mark (buffer, &begin, vc->selection_bound);
          gtk_text_buffer_get_iter_at_mark (buffer, &end, vc->insert);

          if (gtk_text_iter_equal (&begin, &end))
            gtk_text_buffer_backspace (buffer, &end, TRUE, gtk_text_view_get_editable (text_view));
          else
            gtk_text_buffer_delete (buffer, &begin, &end);
        }

      gtk_text_buffer_end_user_action (buffer);

      ide_cursor_set_visible (self, buffer, TRUE);
    }
}

static void
ide_cursor_delete_from_cursor (GtkTextView    *text_view,
                               GtkDeleteType   delete_type,
                               gint            count,
                               IdeCursor      *self)
{
  g_assert (IDE_IS_SOURCE_VIEW (text_view));
  g_assert (IDE_IS_CURSOR (self));

  if (self->cursors != NULL)
    {
      GtkTextBuffer *buffer;
      GtkTextIter ins;
      GtkTextMark *mark;

      buffer = gtk_text_view_get_buffer (text_view);

      gtk_text_buffer_get_iter_at_mark (buffer, &ins, gtk_text_buffer_get_insert(buffer));
      mark = gtk_text_buffer_create_mark (buffer, NULL, &ins, FALSE);

      ide_cursor_set_visible (self, buffer, FALSE);
      ide_cursor_set_virtual_cursor (self, buffer, self->cursors->data);

      gtk_text_buffer_begin_user_action (buffer);

      for (const GList *iter = self->cursors->next; iter != NULL; iter = iter->next)
        {
          VirtualCursor *vc = iter->data;
          GtkTextIter begin, end;

          gtk_text_buffer_get_iter_at_mark (buffer, &begin, vc->selection_bound);
          gtk_text_buffer_get_iter_at_mark (buffer, &end, vc->insert);

          ide_cursor_set_real_cursor (self, buffer, vc);

          if (delete_type == GTK_DELETE_PARAGRAPHS)
            ide_text_util_delete_line (text_view, count);
          else
            GTK_TEXT_VIEW_GET_CLASS (text_view)->delete_from_cursor (text_view,
                                                                     delete_type,
                                                                     count);
          ide_cursor_set_virtual_cursor (self, buffer, vc);
        }

      gtk_text_buffer_end_user_action (buffer);

      ide_cursor_set_visible (self, buffer, TRUE);
      gtk_text_buffer_get_iter_at_mark (buffer, &ins, mark);
      gtk_text_buffer_select_range (buffer, &ins, &ins);
    }
}

static void
ide_cursor_delete_selection (IdeSourceView *source_view,
                             IdeCursor     *self)
{
  GtkTextBuffer *buffer;
  gboolean editable;

  g_assert (IDE_IS_SOURCE_VIEW (source_view));
  g_assert (IDE_IS_CURSOR (self));

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view));
  editable = gtk_text_view_get_editable (GTK_TEXT_VIEW (source_view));

  if (!editable)
    return;

  if (self->cursors != NULL)
    {
      ide_cursor_set_visible (self, buffer, FALSE);
      ide_cursor_set_virtual_cursor (self, buffer, self->cursors->data);

      gtk_text_buffer_begin_user_action (buffer);

      for (const GList *iter = self->cursors->next; iter != NULL; iter = iter->next)
        {
          VirtualCursor *vc = iter->data;
          GtkTextIter begin, end;

          gtk_text_buffer_get_iter_at_mark (buffer, &begin, vc->selection_bound);
          gtk_text_buffer_get_iter_at_mark (buffer, &end, vc->insert);

          gtk_text_iter_order (&begin, &end);

          if (gtk_text_iter_is_end (&end) && gtk_text_iter_starts_line (&begin))
            gtk_text_iter_backward_char (&begin);

          gtk_text_buffer_delete (buffer, &begin, &end);
        }

      gtk_text_buffer_end_user_action (buffer);
      ide_cursor_set_visible (self, buffer, TRUE);
    }
}

static void
ide_cursor_move_cursor (GtkTextView    *text_view,
                        GtkMovementStep step,
                        gint            count,
                        gboolean        extend_selection,
                        IdeCursor      *self)
{
  g_assert (GTK_IS_TEXT_VIEW (text_view));
  g_assert (IDE_IS_CURSOR (self));

  if (self->cursors != NULL)
    {
      GtkTextBuffer *buffer;
      GtkTextIter sel, ins;

      buffer = gtk_text_view_get_buffer (text_view);

      gtk_text_buffer_get_iter_at_mark (buffer, &ins, gtk_text_buffer_get_insert (buffer));
      gtk_text_buffer_get_iter_at_mark (buffer, &sel, gtk_text_buffer_get_selection_bound (buffer));

      ide_cursor_set_visible (self, buffer, FALSE);
      ide_cursor_set_virtual_cursor (self, buffer, self->cursors->data);

      for (const GList *iter = self->cursors->next; iter != NULL; iter = iter->next)
        {
          ide_cursor_set_real_cursor (self, buffer, iter->data);

          GTK_TEXT_VIEW_GET_CLASS (text_view)->move_cursor (text_view,
                                                            step,
                                                            count,
                                                            extend_selection);

          ide_cursor_set_virtual_cursor (self, buffer, iter->data);
        }

      ide_cursor_set_visible (self, buffer, TRUE);
      gtk_text_buffer_select_range (buffer, &ins, &sel);

      gtk_text_view_scroll_mark_onscreen (text_view, gtk_text_buffer_get_insert (buffer));
    }
}

static void
ide_cursor_movement (IdeSourceView         *source_view,
                     IdeSourceViewMovement  movement,
                     gboolean               extend_selection,
                     gboolean               exclusive,
                     gboolean               apply_count,
                     IdeCursor             *self)
{
  g_assert (IDE_IS_SOURCE_VIEW (source_view));
  g_assert (IDE_IS_CURSOR (self));

  if (self->cursors)
    {
      GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view));
      GtkTextIter sel,ins;

      gtk_text_buffer_get_iter_at_mark (buffer, &sel, gtk_text_buffer_get_selection_bound(buffer));
      gtk_text_buffer_get_iter_at_mark (buffer, &ins, gtk_text_buffer_get_insert(buffer));

      ide_cursor_set_visible (self, buffer, FALSE);
      ide_cursor_set_virtual_cursor (self, buffer, self->cursors->data);

      for (const GList *iter = self->cursors->next; iter != NULL; iter = iter->next)
        {
          ide_cursor_set_real_cursor (self, buffer, iter->data);

          IDE_SOURCE_VIEW_GET_CLASS (source_view)->movement (source_view,
                                                             movement,
                                                             extend_selection,
                                                             exclusive,
                                                             apply_count);

          ide_cursor_set_virtual_cursor (self, buffer, iter->data);
        }
      ide_cursor_set_visible (self, buffer, TRUE);
      gtk_text_buffer_select_range (buffer, &ins, &sel);
    }
}

static void
ide_cursor_select_inner (IdeSourceView *source_view,
                         const gchar   *inner_left,
                         const gchar   *inner_right,
                         gboolean       exclusive,
                         gboolean       string_mode,
                         IdeCursor     *self)
{
  g_assert (IDE_IS_SOURCE_VIEW (source_view));
  g_assert (IDE_IS_CURSOR (self));

  if (self->cursors != NULL)
    {
      GtkTextBuffer *buffer;
      GtkTextIter sel,ins;

      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (source_view));

      gtk_text_buffer_get_iter_at_mark (buffer, &sel, gtk_text_buffer_get_selection_bound (buffer));
      gtk_text_buffer_get_iter_at_mark (buffer, &ins, gtk_text_buffer_get_insert (buffer));

      ide_cursor_set_visible (self, buffer, FALSE);
      ide_cursor_set_virtual_cursor (self, buffer, self->cursors->data);

      for (const GList *iter = self->cursors->next; iter != NULL; iter = iter->next)
        {
          ide_cursor_set_real_cursor (self, buffer, iter->data);

          IDE_SOURCE_VIEW_GET_CLASS (source_view)->select_inner (source_view,
                                                                 inner_left,
                                                                 inner_right,
                                                                 exclusive,
                                                                 string_mode);

          ide_cursor_set_virtual_cursor (self, buffer, iter->data);
        }

      ide_cursor_set_visible (self, buffer, TRUE);
      gtk_text_buffer_select_range (buffer, &ins, &sel);
    }
}

static void
ide_cursor_toggle_overwrite (GtkTextView *text_view,
                             IdeCursor   *self)
{
  GtkTextBuffer *buffer;

  g_assert (GTK_IS_TEXT_VIEW (text_view));
  g_assert (IDE_IS_CURSOR (self));

  buffer = gtk_text_view_get_buffer (text_view);

  ide_cursor_set_visible (self, buffer, FALSE);
  self->overwrite = gtk_text_view_get_overwrite (text_view);
  ide_cursor_set_visible (self, buffer, TRUE);
}

static void
ide_cursor_constructed (GObject *object)
{
  IdeCursor *self = (IdeCursor *)object;
  GtkTextView *text_view;
  GtkTextBuffer *buffer;
  g_autoptr(GtkSourceSearchSettings) search_settings = NULL;

  G_OBJECT_CLASS (ide_cursor_parent_class)->constructed (object);

  text_view = GTK_TEXT_VIEW (self->source_view);

  buffer = gtk_text_view_get_buffer (text_view);

  search_settings = g_object_new (GTK_SOURCE_TYPE_SEARCH_SETTINGS,
                                  "wrap-around", FALSE,
                                  "regex-enabled", FALSE,
                                  "case-sensitive", TRUE,
                                  NULL);
  self->search_context = g_object_new (GTK_SOURCE_TYPE_SEARCH_CONTEXT,
                                       "buffer", buffer,
                                       "highlight", FALSE,
                                       "settings", search_settings,
                                       NULL);

  gtk_text_tag_table_add (gtk_text_buffer_get_tag_table (buffer),
                          self->highlight_tag);

  self->overwrite = gtk_text_view_get_overwrite (text_view);

  dzl_signal_group_set_target (self->operations_signals, self->source_view);
}

static void
ide_cursor_get_property (GObject    *object,
                         guint       prop_id,
                         GValue     *value,
                         GParamSpec *pspec)
{
  IdeCursor *self = IDE_CURSOR (object);

  switch (prop_id)
    {
    case PROP_IDE_SOURCE_VIEW:
      g_value_set_object (value, self->source_view);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
ide_cursor_set_property (GObject      *object,
                         guint         prop_id,
                         const GValue *value,
                         GParamSpec   *pspec)
{
  IdeCursor *self = IDE_CURSOR (object);

  switch (prop_id)
    {
    case PROP_IDE_SOURCE_VIEW:
      g_set_weak_pointer (&self->source_view, g_value_get_object (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
    }
}

static void
ide_cursor_class_init (IdeCursorClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = ide_cursor_constructed;
  object_class->dispose = ide_cursor_dispose;
  object_class->get_property = ide_cursor_get_property;
  object_class->set_property = ide_cursor_set_property;

  properties [PROP_IDE_SOURCE_VIEW] =
    g_param_spec_object ("ide-source-view",
                         "IdeSourceView",
                         "The IdeSourceView on which cursors are there",
                         IDE_TYPE_SOURCE_VIEW,
                         (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT));

  g_object_class_install_properties (object_class, N_PROPS, properties);
}

static void
ide_cursor_init (IdeCursor *self)
{
  self->highlight_tag = g_object_new (GTK_TYPE_TEXT_TAG,
                                      "underline", PANGO_UNDERLINE_SINGLE,
                                      NULL);

  self->operations_signals = dzl_signal_group_new (IDE_TYPE_SOURCE_VIEW);

  dzl_signal_group_connect_object (self->operations_signals,
                                   "move-cursor",
                                   G_CALLBACK (ide_cursor_move_cursor),
                                   self,
                                   G_CONNECT_AFTER);
  dzl_signal_group_connect_object (self->operations_signals,
                                   "delete-from-cursor",
                                   G_CALLBACK (ide_cursor_delete_from_cursor),
                                   self,
                                   G_CONNECT_AFTER);
  dzl_signal_group_connect_object (self->operations_signals,
                                   "backspace",
                                   G_CALLBACK (ide_cursor_backspace),
                                   self,
                                   G_CONNECT_AFTER);
  dzl_signal_group_connect_object (self->operations_signals,
                                   "toggle-overwrite",
                                   G_CALLBACK (ide_cursor_toggle_overwrite),
                                   self,
                                   G_CONNECT_AFTER);
  dzl_signal_group_connect_object (self->operations_signals,
                                   "movement",
                                   G_CALLBACK (ide_cursor_movement),
                                   self,
                                   G_CONNECT_AFTER);
  dzl_signal_group_connect_object (self->operations_signals,
                                   "select-inner",
                                   G_CALLBACK (ide_cursor_select_inner),
                                   self,
                                   G_CONNECT_AFTER);
  dzl_signal_group_connect_object (self->operations_signals,
                                   "delete-selection",
                                   G_CALLBACK (ide_cursor_delete_selection),
                                   self,
                                   G_CONNECT_AFTER);
}

gboolean
ide_cursor_is_enabled (IdeCursor *self)
{
  g_return_val_if_fail (IDE_IS_CURSOR (self), FALSE);

  return (self->cursors != NULL);
}

void
ide_cursor_clear_highlight (IdeCursor *self)
{
  GtkTextBuffer *buffer;
  GtkTextIter begin, end;

  g_return_if_fail (IDE_IS_CURSOR (self));

  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (self->source_view));
  gtk_text_buffer_get_bounds (buffer, &begin, &end);
  gtk_text_buffer_remove_tag (buffer, self->highlight_tag, &begin, &end);
}
