/*
 * 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 "css-editor.h"

#include "gtkcssprovider.h"
#include "gtkstyleprovider.h"
#include "gtkstylecontext.h"
#include "gtktextview.h"
#include "gtkmessagedialog.h"
#include "gtkfilechooserdialog.h"
#include "gtktogglebutton.h"
#include "gtklabel.h"

#define GTK_INSPECTOR_CSS_EDITOR_TEXT "inspector-css-editor-text"
#define GTK_INSPECTOR_CSS_EDITOR_PROVIDER "inspector-css-editor-provider"

enum
{
  COLUMN_ENABLED,
  COLUMN_NAME,
  COLUMN_USER
};

enum
{
  PROP_0,
  PROP_GLOBAL
};

typedef struct
{
  gboolean enabled;
  gboolean user;
} GtkInspectorCssEditorByContext;

struct _GtkInspectorCssEditorPrivate
{
  GtkWidget *view;
  GtkWidget *object_title;
  GtkTextBuffer *text;
  GtkCssProvider *provider;
  gboolean global;
  GtkStyleContext *context;
  GtkToggleButton *disable_button;
  guint timeout;
};

static void gtk_inspector_css_editor_remove_dead_object (gpointer  data,
                                                         GObject  *dead_object);

G_DEFINE_TYPE_WITH_PRIVATE (GtkInspectorCssEditor, gtk_inspector_css_editor, GTK_TYPE_BOX)

static void
set_initial_text (GtkInspectorCssEditor *ce)
{
  const gchar *text = NULL;

  if (ce->priv->context)
    text = g_object_get_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_TEXT);
  if (text)
    gtk_text_buffer_set_text (GTK_TEXT_BUFFER (ce->priv->text), text, -1);
  else
    {
      gchar *initial_text;
      if (ce->priv->global)
        initial_text = g_strconcat ("/*\n",
                                    _("You can type here any CSS rule recognized by GTK+."), "\n",
                                    _("You can temporarily disable this custom CSS by clicking on the “Pause” button above."), "\n\n",
                                    _("Changes are applied instantly and globally, for the whole application."), "\n",
                                    "*/\n\n", NULL);
      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 clicking on the “Pause” button above."), "\n\n",
                                    _("Changes are applied instantly, only for this selected widget."), "\n",
                                    "*/\n\n", NULL);
      gtk_text_buffer_set_text (GTK_TEXT_BUFFER (ce->priv->text), initial_text, -1);
      g_free (initial_text);
    }
}

static void
disable_toggled (GtkToggleButton       *button,
                 GtkInspectorCssEditor *ce)
{
  if (gtk_toggle_button_get_active (button))
    {
      if (ce->priv->global)
        gtk_style_context_remove_provider_for_screen (gdk_screen_get_default (),
                                                      GTK_STYLE_PROVIDER (ce->priv->provider));
      else if (ce->priv->context)
        gtk_style_context_remove_provider (ce->priv->context,
                                           GTK_STYLE_PROVIDER (g_object_get_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_PROVIDER)));
    }
  else
    {
      if (ce->priv->global)
        gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
                                                   GTK_STYLE_PROVIDER (ce->priv->provider),
                                                   GTK_STYLE_PROVIDER_PRIORITY_USER);
      else if (ce->priv->context)
        gtk_style_context_add_provider (ce->priv->context,
                                        GTK_STYLE_PROVIDER (g_object_get_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_PROVIDER)),
                                        G_MAXUINT);
    }
}

static gchar *
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,
              const gchar           *filename)
{
  gchar *text;
  GError *error = NULL;

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

  if (!g_file_set_contents (filename, text, -1, &error))
    {
      GtkWidget *dialog;

      dialog = gtk_message_dialog_new (GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (ce))),
                                       GTK_DIALOG_MODAL|GTK_DIALOG_DESTROY_WITH_PARENT,
                                       GTK_MESSAGE_INFO,
                                       GTK_BUTTONS_OK,
                                       _("Saving CSS failed"));
      gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog),
                                                "%s", error->message);
      g_signal_connect (dialog, "response", G_CALLBACK (gtk_widget_destroy), NULL);
      gtk_widget_show (dialog);
      g_error_free (error);
    }

  g_free (text);
}

static void
save_response (GtkWidget             *dialog,
               gint                   response,
               GtkInspectorCssEditor *ce)
{
  gtk_widget_hide (dialog);

  if (response == GTK_RESPONSE_ACCEPT)
    {
      gchar *filename;

      filename = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
      save_to_file (ce, filename);
      g_free (filename);
    }

  gtk_widget_destroy (dialog);
}

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

  dialog = gtk_file_chooser_dialog_new ("",
                                        GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (ce))),
                                        GTK_FILE_CHOOSER_ACTION_SAVE,
                                        _("_Cancel"), GTK_RESPONSE_CANCEL,
                                        _("_Save"), GTK_RESPONSE_ACCEPT,
                                        NULL);
  gtk_file_chooser_set_current_name (GTK_FILE_CHOOSER (dialog), "custom.css");
  gtk_dialog_set_default_response (GTK_DIALOG (dialog), GTK_RESPONSE_ACCEPT);
  gtk_window_set_modal (GTK_WINDOW (dialog), TRUE);
  gtk_file_chooser_set_do_overwrite_confirmation (GTK_FILE_CHOOSER (dialog), TRUE);
  g_signal_connect (dialog, "response", G_CALLBACK (save_response), ce);
  gtk_widget_show (dialog);
}

static void
update_style (GtkInspectorCssEditor *ce)
{
  GtkCssProvider *provider;
  gchar *text;

  if (ce->priv->global)
    provider = ce->priv->provider;
  else if (ce->priv->context)
    provider = g_object_get_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_PROVIDER);
  else
    return;

  text = get_current_text (ce->priv->text);
  gtk_css_provider_load_from_data (provider, text, -1, NULL);
  g_free (text);

  gtk_style_context_reset_widgets (gdk_screen_get_default ());
}

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

  ce->priv->timeout = 0;

  update_style (ce);

  return G_SOURCE_REMOVE;
}

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 (100, update_timeout, ce); 
}

/* Safe version of gtk_text_buffer_get_iter_at_line_index(). */
static void
safe_get_iter_at_line_index (GtkTextBuffer *buffer,
                             GtkTextIter   *iter,
                             gint           line_number,
                             gint           byte_index)
{
  if (line_number >= gtk_text_buffer_get_line_count (buffer))
    {
      gtk_text_buffer_get_end_iter (buffer, iter);
      return;
    }

  gtk_text_buffer_get_iter_at_line (buffer, iter, line_number);

  if (byte_index < gtk_text_iter_get_bytes_in_line (iter))
    gtk_text_iter_set_line_index (iter, byte_index);
  else
    gtk_text_iter_forward_to_line_end (iter);
}

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

  safe_get_iter_at_line_index (buffer,
                               &start,
                               gtk_css_section_get_start_line (section),
                               gtk_css_section_get_start_position (section));
  safe_get_iter_at_line_index (buffer,
                               &end,
                               gtk_css_section_get_end_line (section),
                               gtk_css_section_get_end_position (section));

  if (g_error_matches (error, GTK_CSS_PROVIDER_ERROR, GTK_CSS_PROVIDER_ERROR_DEPRECATED))
    tag_name = "warning";
  else
    tag_name = "error";

  gtk_text_buffer_apply_tag_by_name (buffer, tag_name, &start, &end);
}

static void
create_provider (GtkInspectorCssEditor *ce)
{
  GtkCssProvider *provider = gtk_css_provider_new ();

  if (ce->priv->global)
    {
      ce->priv->provider = provider;
      gtk_style_context_add_provider_for_screen (gdk_screen_get_default (),
                                                 GTK_STYLE_PROVIDER (ce->priv->provider),
                                                 GTK_STYLE_PROVIDER_PRIORITY_USER);
    }
  else if (ce->priv->context)
    {
      gtk_style_context_add_provider (ce->priv->context,
                                      GTK_STYLE_PROVIDER (provider),
                                      G_MAXUINT);
      g_object_set_data_full (G_OBJECT (ce->priv->context),
                              GTK_INSPECTOR_CSS_EDITOR_PROVIDER, provider,
                              g_object_unref);
    }

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

static void
destroy_provider (GtkInspectorCssEditor *ce)
{
  if (ce->priv->global)
    {
      gtk_style_context_remove_provider_for_screen (gdk_screen_get_default (),
                                                    GTK_STYLE_PROVIDER (ce->priv->provider));
      g_object_unref (ce->priv->provider);
      ce->priv->provider = NULL;
    }
  else if (ce->priv->context)
    {
      GtkStyleProvider *provider;

      provider = g_object_get_data (G_OBJECT (ce->priv->context),
                                    GTK_INSPECTOR_CSS_EDITOR_PROVIDER);
      gtk_style_context_remove_provider (ce->priv->context, provider);
      g_object_set_data (G_OBJECT (ce->priv->context),
                         GTK_INSPECTOR_CSS_EDITOR_PROVIDER,
                         NULL);
    }
}
  

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);

  create_provider (ce);
  set_initial_text (ce);
}

static void
get_property (GObject    *object,
              guint       param_id,
              GValue     *value,
              GParamSpec *pspec)
{
  GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object);

  switch (param_id)
    {
      case PROP_GLOBAL:
        g_value_set_boolean (value, ce->priv->global);
        break;

      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
        break;
    }
}

static void
set_property (GObject      *object,
              guint         param_id,
              const GValue *value,
              GParamSpec   *pspec)
{
  GtkInspectorCssEditor *ce = GTK_INSPECTOR_CSS_EDITOR (object);

  switch (param_id)
    {
      case PROP_GLOBAL:
        ce->priv->global = g_value_get_boolean (value);
        break;

      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
        break;
    }
}

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

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

  destroy_provider (ce);
  if (ce->priv->context)
    {
      g_object_weak_unref (G_OBJECT (ce->priv->context), gtk_inspector_css_editor_remove_dead_object, ce);
      g_object_set_data (G_OBJECT (ce->priv->context),
                         GTK_INSPECTOR_CSS_EDITOR_TEXT,
                         NULL);
      ce->priv->context = NULL;
    }

  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->get_property = get_property;
  object_class->set_property = set_property;
  object_class->constructed = constructed;
  object_class->finalize = finalize;

  g_object_class_install_property (object_class, PROP_GLOBAL,
      g_param_spec_boolean ("global", "Global", "Whether this editor changes the whole application or just the selected widget",
                            TRUE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

  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, disable_button);
  gtk_widget_class_bind_template_child_private (widget_class, GtkInspectorCssEditor, object_title);
  gtk_widget_class_bind_template_callback (widget_class, disable_toggled);
  gtk_widget_class_bind_template_callback (widget_class, save_clicked);
  gtk_widget_class_bind_template_callback (widget_class, text_changed);
}

static void
gtk_inspector_css_editor_remove_dead_object (gpointer data, GObject *dead_object)
{
  GtkInspectorCssEditor *ce = data;

  ce->priv->context = NULL;
  gtk_widget_hide (GTK_WIDGET (ce));
}

void
gtk_inspector_css_editor_set_object (GtkInspectorCssEditor *ce,
                                     GObject               *object)
{
  gchar *text;
  GtkCssProvider *provider;
  const gchar *title;

  g_return_if_fail (GTK_INSPECTOR_IS_CSS_EDITOR (ce));
  g_return_if_fail (!ce->priv->global);

  if (ce->priv->context)
    {
      g_object_weak_unref (G_OBJECT (ce->priv->context), gtk_inspector_css_editor_remove_dead_object, ce);
      text = get_current_text (GTK_TEXT_BUFFER (ce->priv->text));
      g_object_set_data_full (G_OBJECT (ce->priv->context),
                              GTK_INSPECTOR_CSS_EDITOR_TEXT,
                              text, g_free);
      ce->priv->context = NULL;
    }

  if (!GTK_IS_WIDGET (object))
    {
      gtk_widget_hide (GTK_WIDGET (ce));
      return;
    }

  gtk_widget_show (GTK_WIDGET (ce));

  title = (const gchar *)g_object_get_data (object, "gtk-inspector-object-title");
  gtk_label_set_label (GTK_LABEL (ce->priv->object_title), title);

  ce->priv->context = gtk_widget_get_style_context (GTK_WIDGET (object));

  provider = g_object_get_data (G_OBJECT (ce->priv->context), GTK_INSPECTOR_CSS_EDITOR_PROVIDER);
  if (!provider)
    create_provider (ce);

  set_initial_text (ce);
  disable_toggled (ce->priv->disable_button, ce);

  g_object_weak_ref (G_OBJECT (ce->priv->context), gtk_inspector_css_editor_remove_dead_object, ce);
}

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