/*
 * Copyright (c) 2013 Intel Corporation
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

#include "config.h"
#include <glib/gi18n-lib.h>

#include "window.h"
#include "css-editor.h"

#include "gtkalertdialog.h"
#include "gtkcssprovider.h"
#include "gtkfiledialog.h"
#include "gtklabel.h"
#include "gtkstyleproviderprivate.h"
#include "gtktextiter.h"
#include "gtktextview.h"
#include "gtktogglebutton.h"
#include "gtkswitch.h"
#include "gtktooltip.h"
#include "gtksettings.h"
#include "gtkdropdown.h"

#include "gtk/css/gtkcss.h"

struct _GtkInspectorCssEditorPrivate
{
  GtkWidget *view;
  GtkTextBuffer *text;
  GdkDisplay *display;
  GtkCssProvider *provider;
  GtkSwitch *enable_switch;
  GtkDropDown *color_scheme;
  GtkDropDown *contrast;
  GtkDropDown *reduced_motion;
  guint timeout;
  GList *errors;
  gboolean show_deprecations;
};

typedef struct {
  GError *error;
  GtkTextIter start;
  GtkTextIter end;
} CssError;

static void
css_error_free (gpointer data)
{
  CssError *error = data;
  g_error_free (error->error);
  g_free (error);
}

static gboolean
query_tooltip_cb (GtkWidget             *widget,
                  int                    x,
                  int                    y,
                  gboolean               keyboard_tip,
                  GtkTooltip            *tooltip,
                  GtkInspectorCssEditor *ce)
{
  GtkTextIter iter;
  GList *l;

  if (keyboard_tip)
    {
      int offset;

      g_object_get (ce->priv->text, "cursor-position", &offset, NULL);
      gtk_text_buffer_get_iter_at_offset (ce->priv->text, &iter, offset);
    }
  else
    {
      int bx, by, trailing;

      gtk_text_view_window_to_buffer_coords (GTK_TEXT_VIEW (ce->priv->view), GTK_TEXT_WINDOW_TEXT,
                                             x, y, &bx, &by);
      gtk_text_view_get_iter_at_position (GTK_TEXT_VIEW (ce->priv->view), &iter, &trailing, bx, by);
    }

  for (l = ce->priv->errors; l; l = l->next)
    {
      CssError *css_error = l->data;

      if (g_error_matches (css_error->error,
                           GTK_CSS_PARSER_WARNING,
                           GTK_CSS_PARSER_WARNING_DEPRECATED) &&
          !ce->priv->show_deprecations)
        continue;

      if (gtk_text_iter_in_range (&iter, &css_error->start, &css_error->end))
        {
          gtk_tooltip_set_text (tooltip, css_error->error->message);
          return TRUE;
        }
    }

  return FALSE;
}

G_DEFINE_TYPE_WITH_PRIVATE (GtkInspectorCssEditor, gtk_inspector_css_editor, GTK_TYPE_BOX)

static char *
get_autosave_path (void)
{
  return g_build_filename (g_get_user_cache_dir (), "gtk-4.0", "inspector-css-autosave", NULL);
}

static void
set_initial_text (GtkInspectorCssEditor *ce)
{
  char *initial_text = NULL;
  char *autosave_file = NULL;
  gsize len;

  autosave_file = get_autosave_path ();

  if (g_file_get_contents (autosave_file, &initial_text, &len, NULL))
    gtk_switch_set_active (GTK_SWITCH (ce->priv->enable_switch), FALSE);
  else
    initial_text = g_strconcat ("/*\n",
                                _("You can type here any CSS rule recognized by GTK."), "\n",
                                _("You can temporarily disable this custom CSS by toggling the switch above."), "\n\n",
                                _("Changes are applied instantly and globally, for the whole application."), "\n",
                                "*/\n\n", NULL);
  gtk_text_buffer_set_text (GTK_TEXT_BUFFER (ce->priv->text), initial_text, -1);
  g_free (initial_text);
  g_free (autosave_file);
}

static void
autosave_contents (GtkInspectorCssEditor *ce)
{
  char *autosave_file = NULL;
  char *dir = NULL;
  char *contents;
  GtkTextIter start, end;

  gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (ce->priv->text), &start, &end);
  contents = gtk_text_buffer_get_text (GTK_TEXT_BUFFER (ce->priv->text), &start, &end, TRUE);
  autosave_file = get_autosave_path ();
  dir = g_path_get_dirname (autosave_file);
  g_mkdir_with_parents (dir, 0755);
  g_file_set_contents (autosave_file, contents, -1, NULL);

  g_free (dir);
  g_free (autosave_file);
  g_free (contents);
}

static void
enable_switch_changed (GtkSwitch             *sw,
                       GParamSpec            *pspec,
                       GtkInspectorCssEditor *ce)
{
  if (!ce->priv->display)
    return;

  if (gtk_switch_get_active (sw))
    gtk_style_context_add_provider_for_display (ce->priv->display,
                                                GTK_STYLE_PROVIDER (ce->priv->provider),
                                                GTK_STYLE_PROVIDER_PRIORITY_INSPECTOR);
  else
    gtk_style_context_remove_provider_for_display (ce->priv->display,
                                                   GTK_STYLE_PROVIDER (ce->priv->provider));
}

static void
toggle_deprecations (GtkToggleButton       *button,
                     GtkInspectorCssEditor *ce)
{
  GtkTextTagTable *tags;
  GtkTextTag *tag;
  PangoUnderline underline;

  if (!ce->priv->display)
    return;

  ce->priv->show_deprecations = gtk_toggle_button_get_active (button);

  tags = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (ce->priv->text));
  tag = gtk_text_tag_table_lookup (tags, "deprecation");
  if (ce->priv->show_deprecations)
    underline = PANGO_UNDERLINE_SINGLE;
  else
    underline = PANGO_UNDERLINE_NONE;

  g_object_set (tag, "underline", underline, NULL);
}

static char *
get_current_text (GtkTextBuffer *buffer)
{
  GtkTextIter start, end;

  gtk_text_buffer_get_start_iter (buffer, &start);
  gtk_text_buffer_get_end_iter (buffer, &end);
  gtk_text_buffer_remove_all_tags (buffer, &start, &end);

  return gtk_text_buffer_get_text (buffer, &start, &end, FALSE);
}

static void
save_to_file (GtkInspectorCssEditor *ce,
              GFile                 *file)
{
  GError *error = NULL;
  char *text;

  text = get_current_text (ce->priv->text);

  g_file_replace_contents (file, text, strlen (text),
                           NULL,
                           FALSE,
                           G_FILE_CREATE_NONE,
                           NULL,
                           NULL,
                           &error);

  if (error != NULL)
    {
      GtkAlertDialog *alert;

      alert = gtk_alert_dialog_new (_("Saving CSS failed"));
      gtk_alert_dialog_set_detail (alert, error->message);
      gtk_alert_dialog_show (alert, GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (ce))));
      g_object_unref (alert);
      g_error_free (error);
    }

  g_free (text);
}

static void
save_response (GObject *source,
               GAsyncResult *result,
               gpointer data)
{
  GtkFileDialog *dialog = GTK_FILE_DIALOG (source);
  GtkInspectorCssEditor *ce = data;
  GError *error = NULL;
  GFile *file;

  file = gtk_file_dialog_save_finish (dialog, result, &error);
  if (file)
    {
      save_to_file (ce, file);
      g_object_unref (file);
    }
  else
    {
      g_print ("Error saving css: %s\n", error->message);
      g_error_free (error);
    }
}

static void
save_clicked (GtkButton             *button,
              GtkInspectorCssEditor *ce)
{
  GtkFileDialog *dialog;

  dialog = gtk_file_dialog_new ();
  gtk_file_dialog_set_initial_name (dialog, "custom.css");
  gtk_file_dialog_save (dialog,
                        GTK_WINDOW (gtk_widget_get_root (GTK_WIDGET (ce))),
                        NULL,
                        save_response, ce);
  g_object_unref (dialog);
}

static void
settings_changed (GtkDropDown           *dropdown,
                  GParamSpec            *pspec,
                  GtkInspectorCssEditor *ce)
{
  GtkInterfaceColorScheme color_scheme = gtk_drop_down_get_selected (ce->priv->color_scheme) + 1;
  GtkInterfaceColorScheme system_color_scheme;
  GtkInterfaceContrast contrast = gtk_drop_down_get_selected (ce->priv->contrast) + 1;
  GtkInterfaceContrast system_contrast;
  GtkReducedMotion reduced_motion = gtk_drop_down_get_selected (ce->priv->reduced_motion);
  GtkReducedMotion system_reduced_motion;

  g_object_get (gtk_settings_get_for_display (ce->priv->display),
                "gtk-interface-color-scheme", &system_color_scheme,
                "gtk-interface-contrast", &system_contrast,
                "gtk-interface-reduced-motion", &system_reduced_motion,
                NULL);

  if (color_scheme == GTK_INTERFACE_COLOR_SCHEME_DEFAULT)
    color_scheme = system_color_scheme;

  if (contrast == GTK_INTERFACE_CONTRAST_NO_PREFERENCE)
    contrast = system_contrast;

  if (reduced_motion == GTK_REDUCED_MOTION_NO_PREFERENCE)
    reduced_motion = system_reduced_motion;

  g_object_set (ce->priv->provider,
                "prefers-color-scheme", color_scheme,
                "prefers-contrast", contrast,
                "prefers-reduced-motion", reduced_motion,
                NULL);
}

static void
system_settings_changed (GtkSettings           *settings,
                         GParamSpec            *pspec,
                         GtkInspectorCssEditor *ce)
{
  settings_changed (NULL, NULL, ce);
}

static void
update_style (GtkInspectorCssEditor *ce)
{
  char *text;

  g_clear_list (&ce->priv->errors, css_error_free);

  text = get_current_text (ce->priv->text);
  gtk_css_provider_load_from_string (ce->priv->provider, text);
  g_free (text);
}

static void
update_timeout (gpointer data)
{
  GtkInspectorCssEditor *ce = data;

  ce->priv->timeout = 0;

  autosave_contents (ce);
  update_style (ce);
}

static void
text_changed (GtkTextBuffer         *buffer,
              GtkInspectorCssEditor *ce)
{
  if (ce->priv->timeout != 0)
    g_source_remove (ce->priv->timeout);

  ce->priv->timeout = g_timeout_add_once (100, update_timeout, ce);

  g_clear_list (&ce->priv->errors, css_error_free);
}

static void
show_parsing_error (GtkCssProvider        *provider,
                    GtkCssSection         *section,
                    const GError          *error,
                    GtkInspectorCssEditor *ce)
{
  const char *tag_name;
  GtkTextBuffer *buffer = GTK_TEXT_BUFFER (ce->priv->text);
  const GtkCssLocation *start, *end;
  CssError *css_error;

  css_error = g_new (CssError, 1);
  css_error->error = g_error_copy (error);

  start = gtk_css_section_get_start_location (section);
  gtk_text_buffer_get_iter_at_line_index (buffer,
                                          &css_error->start,
                                          CLAMP (start->lines - 1, 0, INT_MAX),
                                          CLAMP (start->line_bytes, 0, INT_MAX));
  end = gtk_css_section_get_end_location (section);
  gtk_text_buffer_get_iter_at_line_index (buffer,
                                          &css_error->end,
                                          CLAMP (end->lines - 1, 0, INT_MAX),
                                          CLAMP (end->line_bytes, 0, INT_MAX));

  if (error->domain == GTK_CSS_PARSER_WARNING)
    {
      if (error->code == GTK_CSS_PARSER_WARNING_DEPRECATED)
        tag_name = "deprecation";
      else
        tag_name = "warning";
    }
  else
    tag_name = "error";

  if (gtk_text_iter_equal (&css_error->start, &css_error->end))
    gtk_text_iter_forward_char (&css_error->end);

  gtk_text_buffer_apply_tag_by_name (buffer, tag_name, &css_error->start, &css_error->end);

  ce->priv->errors = g_list_prepend (ce->priv->errors, css_error);
}

static void
create_provider (GtkInspectorCssEditor *ce)
{
  ce->priv->provider = gtk_css_provider_new ();

  g_signal_connect (ce->priv->provider, "parsing-error",
                    G_CALLBACK (show_parsing_error), ce);
}

static void
destroy_provider (GtkInspectorCssEditor *ce)
{
  g_signal_handlers_disconnect_by_func (ce->priv->provider, show_parsing_error, ce);
  g_clear_object (&ce->priv->provider);
}

static void
add_provider (GtkInspectorCssEditor *ce,
              GdkDisplay *display)
{
  GtkSettings *settings = gtk_settings_get_for_display (display);

  g_signal_connect_object (settings, "notify::gtk-interface-color-scheme",
                           G_CALLBACK (system_settings_changed), ce, G_CONNECT_DEFAULT);
  g_signal_connect_object (settings, "notify::gtk-interface-contrast",
                           G_CALLBACK (system_settings_changed), ce, G_CONNECT_DEFAULT);
  g_signal_connect_object (settings, "notify::gtk-interface-reduced-motion",
                           G_CALLBACK (system_settings_changed), ce, G_CONNECT_DEFAULT);

  system_settings_changed (settings, NULL, ce);

  gtk_style_context_add_provider_for_display (display,
                                              GTK_STYLE_PROVIDER (ce->priv->provider),
                                              GTK_STYLE_PROVIDER_PRIORITY_USER);
}

static void
remove_provider (GtkInspectorCssEditor *ce,
                 GdkDisplay *display)
{
  gtk_style_context_remove_provider_for_display (display,
                                                 GTK_STYLE_PROVIDER (ce->priv->provider));
}

static void
gtk_inspector_css_editor_init (GtkInspectorCssEditor *ce)
{
  ce->priv = gtk_inspector_css_editor_get_instance_private (ce);
  gtk_widget_init_template (GTK_WIDGET (ce));
}

static void
constructed (GObject *object)
{
  GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object);

  G_OBJECT_CLASS (gtk_inspector_css_editor_parent_class)->constructed (object);

  create_provider (ce);
}

static void
finalize (GObject *object)
{
  GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object);

  if (ce->priv->timeout != 0)
    g_source_remove (ce->priv->timeout);

  if (ce->priv->display)
    remove_provider (ce, ce->priv->display);
  destroy_provider (ce);

  g_list_free_full (ce->priv->errors, css_error_free);

  G_OBJECT_CLASS (gtk_inspector_css_editor_parent_class)->finalize (object);
}

static void
gtk_inspector_css_editor_class_init (GtkInspectorCssEditorClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);
  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

  object_class->constructed = constructed;
  object_class->finalize = finalize;

  gtk_widget_class_set_template_from_resource (widget_class, "/org/gtk/libgtk/inspector/css-editor.ui");
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, text);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, view);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, enable_switch);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, color_scheme);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, contrast);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, reduced_motion);
  gtk_widget_class_bind_template_callback (widget_class, enable_switch_changed);
  gtk_widget_class_bind_template_callback (widget_class, toggle_deprecations);
  gtk_widget_class_bind_template_callback (widget_class, save_clicked);
  gtk_widget_class_bind_template_callback (widget_class, text_changed);
  gtk_widget_class_bind_template_callback (widget_class, query_tooltip_cb);
  gtk_widget_class_bind_template_callback (widget_class, settings_changed);
}

void
gtk_inspector_css_editor_set_display (GtkInspectorCssEditor *ce,
                                      GdkDisplay *display)
{
  ce->priv->display = display;
  add_provider (ce, display);
  set_initial_text (ce);
}

// vim: set et sw=2 ts=2:
