/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995-1997 Spencer Kimball and Peter Mattis
 *
 * gimppropgui.c
 * Copyright (C) 2002-2017  Michael Natterer <mitch@gimp.org>
 *                          Sven Neumann <sven@gimp.org>
 *
 * 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 <https://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <string.h>

#include <gegl.h>
#include <gegl-paramspecs.h>
#include <gtk/gtk.h>

#include "libgimpcolor/gimpcolor.h"
#include "libgimpconfig/gimpconfig.h"
#include "libgimpwidgets/gimpwidgets.h"

#include "propgui-types.h"

#include "gegl/gimp-gegl-utils.h"

#include "operations/gimp-operation-config.h"

#include "core/gimpcontext.h"

#include "widgets/gimpcolorpanel.h"
#include "widgets/gimpmessagebox.h"
#include "widgets/gimppropwidgets.h"

#include "gimppropgui.h"
#include "gimppropgui-channel-mixer.h"
#include "gimppropgui-color-balance.h"
#include "gimppropgui-color-rotate.h"
#include "gimppropgui-color-to-alpha.h"
#include "gimppropgui-convolution-matrix.h"
#include "gimppropgui-diffraction-patterns.h"
#include "gimppropgui-eval.h"
#include "gimppropgui-focus-blur.h"
#include "gimppropgui-generic.h"
#include "gimppropgui-hue-saturation.h"
#include "gimppropgui-motion-blur-circular.h"
#include "gimppropgui-motion-blur-linear.h"
#include "gimppropgui-motion-blur-zoom.h"
#include "gimppropgui-newsprint.h"
#include "gimppropgui-panorama-projection.h"
#include "gimppropgui-recursive-transform.h"
#include "gimppropgui-shadows-highlights.h"
#include "gimppropgui-spiral.h"
#include "gimppropgui-supernova.h"
#include "gimppropgui-utils.h"
#include "gimppropgui-vignette.h"

#include "gimp-intl.h"


#define HAS_KEY(p,k,v) gimp_gegl_param_spec_has_key (p, k, v)


static gboolean      gimp_prop_string_to_boolean       (GBinding       *binding,
                                                        const GValue   *from_value,
                                                        GValue         *to_value,
                                                        gpointer        user_data);
static void          gimp_prop_config_notify           (GObject        *config,
                                                        GParamSpec     *pspec,
                                                        GtkWidget      *widget);
static void          gimp_prop_widget_show             (GtkWidget      *widget,
                                                        GObject        *config);
static void          gimp_prop_free_label_ref          (GWeakRef       *label_ref);

/*  public functions  */

GtkWidget *
gimp_prop_widget_new (GObject                  *config,
                      const gchar              *property_name,
                      GeglRectangle            *area,
                      GimpContext              *context,
                      GimpCreatePickerFunc      create_picker_func,
                      GimpCreateControllerFunc  create_controller_func,
                      gpointer                  creator,
                      const gchar             **label)
{
  GParamSpec *pspec;

  g_return_val_if_fail (G_IS_OBJECT (config), NULL);

  pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (config),
                                        property_name);

  return gimp_prop_widget_new_from_pspec (config, pspec, area, context,
                                          create_picker_func,
                                          create_controller_func,
                                          creator,
                                          label);
}

GtkWidget *
gimp_prop_widget_new_from_pspec (GObject                  *config,
                                 GParamSpec               *pspec,
                                 GeglRectangle            *area,
                                 GimpContext              *context,
                                 GimpCreatePickerFunc      create_picker_func,
                                 GimpCreateControllerFunc  create_controller_func,
                                 gpointer                  creator,
                                 const gchar             **label)
{
  GtkWidget *widget = NULL;

  g_return_val_if_fail (G_IS_OBJECT (config), NULL);
  g_return_val_if_fail (pspec != NULL, NULL);
  g_return_val_if_fail (GIMP_IS_CONTEXT (context), NULL);
  g_return_val_if_fail (label != NULL, NULL);

  *label = NULL;

  if (GEGL_IS_PARAM_SPEC_SEED (pspec))
    {
      widget = gimp_prop_random_seed_new (config, pspec->name);

      *label = g_param_spec_get_nick (pspec);
    }
  else if (G_IS_PARAM_SPEC_INT (pspec)   ||
           G_IS_PARAM_SPEC_UINT (pspec)  ||
           G_IS_PARAM_SPEC_FLOAT (pspec) ||
           G_IS_PARAM_SPEC_DOUBLE (pspec))
    {
      gdouble lower;
      gdouble upper;
      gdouble step;
      gdouble page;
      gint    digits;

      if (GEGL_IS_PARAM_SPEC_DOUBLE (pspec))
        {
          GeglParamSpecDouble *gspec = GEGL_PARAM_SPEC_DOUBLE (pspec);

          lower  = gspec->ui_minimum;
          upper  = gspec->ui_maximum;
          step   = gspec->ui_step_small;
          page   = gspec->ui_step_big;
          digits = gspec->ui_digits;
        }
      else if (GEGL_IS_PARAM_SPEC_INT (pspec))
        {
          GeglParamSpecInt *gspec = GEGL_PARAM_SPEC_INT (pspec);

          lower  = gspec->ui_minimum;
          upper  = gspec->ui_maximum;
          step   = gspec->ui_step_small;
          page   = gspec->ui_step_big;
          digits = 0;
        }
      else
        {
          gdouble value;

          /* Get the min and max for the given property. */
          _gimp_prop_widgets_get_numeric_values (config, pspec,
                                                 &value, &lower, &upper,
                                                 G_STRFUNC);

          if ((upper - lower <= 1.0) &&
              (G_IS_PARAM_SPEC_FLOAT (pspec) ||
               G_IS_PARAM_SPEC_DOUBLE (pspec)))
            {
              step   = 0.01;
              page   = 0.1;
              digits = 4;
            }
          else if ((upper - lower <= 10.0) &&
                   (G_IS_PARAM_SPEC_FLOAT (pspec) ||
                    G_IS_PARAM_SPEC_DOUBLE (pspec)))
            {
              step   = 0.1;
              page   = 1.0;
              digits = 3;
            }
          else
            {
              step   = 1.0;
              page   = 10.0;
              digits = (G_IS_PARAM_SPEC_FLOAT (pspec) ||
                        G_IS_PARAM_SPEC_DOUBLE (pspec)) ? 2 : 0;
            }
        }

      widget = gimp_prop_spin_scale_new (config, pspec->name,
                                         step, page, digits);

      if (HAS_KEY (pspec, "unit", "degree") &&
          (upper - lower) == 360.0)
        {
          GtkWidget *hbox;
          GtkWidget *dial;

          gtk_spin_button_set_wrap (GTK_SPIN_BUTTON (widget), TRUE);

          hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);

          gtk_box_pack_start (GTK_BOX (hbox), widget, TRUE, TRUE, 0);
          gtk_widget_show (widget);

          dial = gimp_prop_angle_dial_new (config, pspec->name);
          g_object_set (dial,
                        "clockwise-angles", HAS_KEY (pspec, "direction", "cw"),
                        NULL);
          gtk_box_pack_start (GTK_BOX (hbox), dial, FALSE, FALSE, 0);
          gtk_widget_show (dial);

          gimp_help_set_help_data (hbox, g_param_spec_get_blurb (pspec), NULL);
          gimp_prop_gui_bind_label (hbox, widget);

          widget = hbox;
        }
      else if (HAS_KEY (pspec, "unit", "kelvin"))
        {
          GtkWidget *hbox;
          GtkWidget *button;

          hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);

          gtk_box_pack_start (GTK_BOX (hbox), widget, TRUE, TRUE, 0);
          gtk_widget_show (widget);

          button = gimp_prop_kelvin_presets_new (config, pspec->name);
          gtk_box_pack_start (GTK_BOX (hbox), button, FALSE, FALSE, 0);
          gtk_widget_show (button);

          gimp_help_set_help_data (hbox, g_param_spec_get_blurb (pspec), NULL);
          gimp_prop_gui_bind_label (hbox, widget);

          widget = hbox;
        }
      else
        {
          gimp_prop_gui_bind_label (widget, widget);

          if (area &&
              (HAS_KEY (pspec, "unit", "pixel-coordinate") ||
               HAS_KEY (pspec, "unit", "pixel-distance")) &&
              (HAS_KEY (pspec, "axis", "x") ||
               HAS_KEY (pspec, "axis", "y")))
            {
              gdouble min = lower;
              gdouble max = upper;

              if (HAS_KEY (pspec, "unit", "pixel-coordinate"))
                {
                  /* limit pixel coordinate scales to the actual area */

                  gint off_x = area->x;
                  gint off_y = area->y;

                  if (HAS_KEY (pspec, "axis", "x"))
                    {
                      min = MAX (lower, off_x);
                      max = MIN (upper, off_x + area->width);
                    }
                  else if (HAS_KEY (pspec, "axis","y"))
                    {
                      min = MAX (lower, off_y);
                      max = MIN (upper, off_y + area->height);
                    }
                }
              else if (HAS_KEY (pspec, "unit", "pixel-distance"))
                {
                  /* limit pixel distance scales to the same value on the
                   * x and y axes, so linked values have the same range,
                   * we use MAX (width, height), see issue #2540
                   */

                  max = MIN (upper, MAX (area->width, area->height));
                }

              gimp_spin_scale_set_scale_limits (GIMP_SPIN_SCALE (widget),
                                                min, max);
            }
        }
    }
  else if (G_IS_PARAM_SPEC_STRING (pspec))
    {
      *label = g_param_spec_get_nick (pspec);

      if (GIMP_IS_PARAM_SPEC_CONFIG_PATH (pspec))
        {
          widget =
            gimp_prop_file_chooser_button_new (config, pspec->name,
                                               g_param_spec_get_nick (pspec),
                                               GTK_FILE_CHOOSER_ACTION_OPEN);
        }
      else if (HAS_KEY (pspec, "multiline", "true"))
        {
          GtkTextBuffer *buffer;
          GtkWidget     *view;

          buffer = gimp_prop_text_buffer_new (config, pspec->name, -1);
          view = gtk_text_view_new_with_buffer (buffer);
          g_object_unref (buffer);

          widget = gtk_scrolled_window_new (NULL, NULL);
          gtk_widget_set_size_request (widget, -1, 150);
          gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (widget),
                                               GTK_SHADOW_IN);
          gtk_container_add (GTK_CONTAINER (widget), view);
          gtk_widget_show (view);
        }
      else if (HAS_KEY (pspec, "error", "true"))
        {
          GtkWidget *l;

          widget = gimp_message_box_new (GIMP_ICON_WILBER_EEK);
          gimp_message_box_set_primary_text (GIMP_MESSAGE_BOX (widget), "%s",
                                             *label);
          gimp_message_box_set_text (GIMP_MESSAGE_BOX (widget), "%s", "");

          l = GIMP_MESSAGE_BOX (widget)->label[1];

          g_object_bind_property (config, pspec->name,
                                  l,  "label",
                                  G_BINDING_SYNC_CREATE);
          g_object_bind_property_full (config, pspec->name,
                                       widget, "visible",
                                       G_BINDING_SYNC_CREATE,
                                       gimp_prop_string_to_boolean,
                                       NULL,
                                       NULL, NULL);
          *label = NULL;
        }
      else
        {
          widget = gimp_prop_entry_new (config, pspec->name, -1);
        }

    }
  else if (G_IS_PARAM_SPEC_BOOLEAN (pspec))
    {
      widget = gimp_prop_check_button_new (config, pspec->name,
                                           g_param_spec_get_nick (pspec));

      gimp_prop_gui_bind_label (widget, widget);
    }
  else if (G_IS_PARAM_SPEC_ENUM (pspec))
    {
      widget = gimp_prop_enum_combo_box_new (config, pspec->name, 0, 0);
      gimp_int_combo_box_set_label (GIMP_INT_COMBO_BOX (widget),
                                    g_param_spec_get_nick (pspec));

      gimp_prop_gui_bind_label (widget, widget);
    }
  else if (GIMP_IS_PARAM_SPEC_COLOR (pspec))
    {
      gboolean   has_alpha;
      GtkWidget *button;

      has_alpha = gimp_param_spec_color_has_alpha (pspec);

      widget = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 4);

      button = gimp_prop_color_button_new (config, pspec->name,
                                           g_param_spec_get_nick (pspec),
                                           128, 24,
                                           has_alpha ?
                                           GIMP_COLOR_AREA_SMALL_CHECKS :
                                           GIMP_COLOR_AREA_FLAT);
      gimp_color_button_set_update (GIMP_COLOR_BUTTON (button), TRUE);
      gimp_color_panel_set_context (GIMP_COLOR_PANEL (button), context);
      gtk_box_pack_start (GTK_BOX (widget), button, TRUE, TRUE, 0);
      gtk_widget_show (button);

      gimp_prop_gui_bind_tooltip (button, widget);

      if (create_picker_func)
        {
          button = create_picker_func (creator,
                                       pspec->name,
                                       GIMP_ICON_COLOR_PICKER_GRAY,
                                       _("Pick color from the image"),
                                       /* pick_abyss = */ FALSE,
                                       NULL, NULL);
          gtk_box_pack_start (GTK_BOX (widget), button, FALSE, FALSE, 0);
          gtk_widget_show (button);
        }

      *label = g_param_spec_get_nick (pspec);
    }
  else
    {
      g_warning ("%s: not supported: %s (%s)\n", G_STRFUNC,
                 g_type_name (G_TYPE_FROM_INSTANCE (pspec)), pspec->name);
    }

  /* if we have any keys for dynamic properties, listen to config's notify
   * signal, and update the properties accordingly.
   */
  if (gegl_param_spec_get_property_key (pspec, "sensitive") ||
      gegl_param_spec_get_property_key (pspec, "visible")   ||
      gegl_param_spec_get_property_key (pspec, "label")     ||
      gegl_param_spec_get_property_key (pspec, "description"))
    {
      g_object_set_data (G_OBJECT (widget), "gimp-prop-pspec", pspec);

      g_signal_connect_object (config, "notify",
                               G_CALLBACK (gimp_prop_config_notify),
                               widget, 0);

      if (gegl_param_spec_get_property_key (pspec, "visible"))
        {
          /* a bit of a hack: if we have a dynamic "visible" property key,
           * connect to the widget's "show" signal, so that we can intercept
           * our caller's gtk_widget_show() call, and keep the widget hidden if
           * necessary.
           */
          g_signal_connect (widget, "show",
                            G_CALLBACK (gimp_prop_widget_show),
                            config);
        }

      /* update all the properties now */
      gimp_prop_config_notify (config, NULL, widget);
    }

  gtk_widget_show (widget);

  return widget;
}


typedef GtkWidget * (* GimpPropGuiNewFunc) (GObject                  *config,
                                            GParamSpec              **param_specs,
                                            guint                     n_param_specs,
                                            GeglRectangle            *area,
                                            GimpContext              *context,
                                            GimpCreatePickerFunc      create_picker_func,
                                            GimpCreateControllerFunc  create_controller_func,
                                            gpointer                  creator);

static const struct
{
  const gchar        *config_type;
  GimpPropGuiNewFunc  gui_new_func;
}
gui_new_funcs[] =
{
  { "GimpColorBalanceConfig",
    _gimp_prop_gui_new_color_balance },
  { "GimpHueSaturationConfig",
    _gimp_prop_gui_new_hue_saturation },
  { "GimpGegl-gegl-color-rotate-config",
    _gimp_prop_gui_new_color_rotate },
  { "GimpGegl-gegl-color-to-alpha-config",
    _gimp_prop_gui_new_color_to_alpha },
  { "GimpGegl-gegl-convolution-matrix-config",
    _gimp_prop_gui_new_convolution_matrix },
  { "GimpGegl-gegl-channel-mixer-config",
    _gimp_prop_gui_new_channel_mixer },
  { "GimpGegl-gegl-diffraction-patterns-config",
    _gimp_prop_gui_new_diffraction_patterns },
  { "GimpGegl-gegl-focus-blur-config",
    _gimp_prop_gui_new_focus_blur },
  { "GimpGegl-gegl-motion-blur-circular-config",
    _gimp_prop_gui_new_motion_blur_circular },
  { "GimpGegl-gegl-motion-blur-linear-config",
    _gimp_prop_gui_new_motion_blur_linear },
  { "GimpGegl-gegl-motion-blur-zoom-config",
    _gimp_prop_gui_new_motion_blur_zoom },
  { "GimpGegl-gegl-newsprint-config",
    _gimp_prop_gui_new_newsprint },
  { "GimpGegl-gegl-panorama-projection-config",
    _gimp_prop_gui_new_panorama_projection },
  { "GimpGegl-gegl-recursive-transform-config",
    _gimp_prop_gui_new_recursive_transform },
  { "GimpGegl-gegl-shadows-highlights-config",
    _gimp_prop_gui_new_shadows_highlights },
  { "GimpGegl-gegl-spiral-config",
    _gimp_prop_gui_new_spiral },
  { "GimpGegl-gegl-supernova-config",
    _gimp_prop_gui_new_supernova },
  { "GimpGegl-gegl-vignette-config",
    _gimp_prop_gui_new_vignette },
  { NULL,
    _gimp_prop_gui_new_generic }
};


GtkWidget *
gimp_prop_gui_new (GObject                  *config,
                   GType                     owner_type,
                   GParamFlags               flags,
                   GeglRectangle            *area,
                   GimpContext              *context,
                   GimpCreatePickerFunc      create_picker_func,
                   GimpCreateControllerFunc  create_controller_func,
                   gpointer                  creator)
{
  GtkWidget    *gui = NULL;
  GParamSpec  **param_specs;
  guint         n_param_specs;

  g_return_val_if_fail (G_IS_OBJECT (config), NULL);
  g_return_val_if_fail (GIMP_IS_CONTEXT (context), NULL);

  param_specs = gimp_operation_config_list_properties (config,
                                                       owner_type, flags,
                                                       &n_param_specs);

  if (param_specs)
    {
      const gchar *config_type_name = G_OBJECT_TYPE_NAME (config);
      gint         i;

      for (i = 0; i < G_N_ELEMENTS (gui_new_funcs); i++)
        {
          if (! gui_new_funcs[i].config_type ||
              ! strcmp (gui_new_funcs[i].config_type, config_type_name))
            {
              g_printerr ("GUI new func match: %s\n",
                          gui_new_funcs[i].config_type ?
                          gui_new_funcs[i].config_type : "generic fallback");

              gui = gui_new_funcs[i].gui_new_func (config,
                                                   param_specs, n_param_specs,
                                                   area,
                                                   context,
                                                   create_picker_func,
                                                   create_controller_func,
                                                   creator);
              break;
            }
        }

      g_free (param_specs);
    }
  else
    {
      gui = gtk_label_new (_("This operation has no editable properties"));
      gimp_label_set_attributes (GTK_LABEL (gui),
                                 PANGO_ATTR_STYLE, PANGO_STYLE_ITALIC,
                                 -1);
      g_object_set (gui,
                    "margin-top",    4,
                    "margin-bottom", 4,
                    NULL);
    }

  gtk_widget_show (gui);

  return gui;
}

void
gimp_prop_gui_bind_container (GtkWidget *source,
                              GtkWidget *target)
{
  g_object_bind_property (source, "sensitive",
                          target, "sensitive",
                          G_BINDING_SYNC_CREATE);
  g_object_bind_property (source, "visible",
                          target, "visible",
                          G_BINDING_SYNC_CREATE);
}

void
gimp_prop_gui_bind_label (GtkWidget *source,
                          GtkWidget *target)
{
  GWeakRef    *label_ref;
  const gchar *label;

  /* we want to update "target"'s "label" property whenever the label
   * expression associated with "source" is reevaluated, however, "source"
   * might not itself have a "label" property we can bind to.  just keep around
   * a reference to "target", and update its label manually.
   */
  g_return_if_fail (g_object_get_data (G_OBJECT (source),
                                       "gimp-prop-label-ref") == NULL);

  label_ref = g_slice_new (GWeakRef);

  g_weak_ref_init (label_ref, target);

  g_object_set_data_full (G_OBJECT (source),
                          "gimp-prop-label-ref", label_ref,
                          (GDestroyNotify) gimp_prop_free_label_ref);

  label = g_object_get_data (G_OBJECT (source), "gimp-prop-label");

  if (label)
    g_object_set (target, "label", label, NULL);

  /* note that "source" might be its own label widget, in which case there's no
   * need to bind the rest of the properties.
   */
  if (source != target)
    gimp_prop_gui_bind_tooltip (source, target);
}

void
gimp_prop_gui_bind_tooltip (GtkWidget *source,
                            GtkWidget *target)
{
  g_object_bind_property (source, "tooltip-text",
                          target, "tooltip-text",
                          G_BINDING_SYNC_CREATE);
}


/*  private functions  */

static gboolean
gimp_prop_string_to_boolean (GBinding     *binding,
                             const GValue *from_value,
                             GValue       *to_value,
                             gpointer      user_data)
{
  const gchar *string = g_value_get_string (from_value);

  g_value_set_boolean (to_value, string && *string);

  return TRUE;
}

static void
gimp_prop_config_notify (GObject    *config,
                         GParamSpec *pspec,
                         GtkWidget  *widget)
{
  GParamSpec *widget_pspec;
  GWeakRef   *label_ref;
  GtkWidget  *label_widget;
  gboolean    sensitive;
  gboolean    visible;
  gchar      *label;
  gchar      *description;

  widget_pspec = g_object_get_data (G_OBJECT (widget), "gimp-prop-pspec");
  label_ref    = g_object_get_data (G_OBJECT (widget), "gimp-prop-label-ref");

  if (label_ref)
    label_widget = g_weak_ref_get (label_ref);
  else
    label_widget = NULL;

  sensitive   = gimp_prop_eval_boolean (config, widget_pspec, "sensitive", TRUE);
  visible     = gimp_prop_eval_boolean (config, widget_pspec, "visible", TRUE);
  label       = gimp_prop_eval_string (config, widget_pspec, "label",
                                       g_param_spec_get_nick (widget_pspec));
  description = gimp_prop_eval_string (config, widget_pspec, "description",
                                       g_param_spec_get_blurb (widget_pspec));

  /* we store the label in (and pass ownership over it to) the widget's
   * "gimp-prop-label" key, so that we can use it to initialize the label
   * widget's label in gimp_prop_gui_bind_label() upon binding.
   */
  g_object_set_data_full (G_OBJECT (widget), "gimp-prop-label", label, g_free);

  g_signal_handlers_block_by_func (widget,
                                   gimp_prop_widget_show, config);

  gtk_widget_set_sensitive (widget, sensitive);
  gtk_widget_set_visible (widget, visible);
  if (label_widget) g_object_set (label_widget, "label", label, NULL);
  gimp_help_set_help_data (widget, description, NULL);

  g_signal_handlers_unblock_by_func (widget,
                                     gimp_prop_widget_show, config);

  g_free (description);

  if (label_widget)
    g_object_unref (label_widget);
}

static void
gimp_prop_widget_show (GtkWidget *widget,
                       GObject   *config)
{
  GParamSpec *widget_pspec;
  gboolean    visible;

  widget_pspec = g_object_get_data (G_OBJECT (widget), "gimp-prop-pspec");

  visible = gimp_prop_eval_boolean (config, widget_pspec, "visible", TRUE);

  gtk_widget_set_visible (widget, visible);
}

static void
gimp_prop_free_label_ref (GWeakRef *label_ref)
{
  g_weak_ref_clear (label_ref);

  g_slice_free (GWeakRef, label_ref);
}
