/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * gimpgeglprocedure.c
 * Copyright (C) 2016-2018 Michael Natterer <mitch@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 <gtk/gtk.h>

#include "libgimpbase/gimpbase.h"
#include "libgimpconfig/gimpconfig.h"

#include "actions-types.h"

#include "config/gimpguiconfig.h"

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

#include "core/gimp.h"
#include "core/gimp-memsize.h"
#include "core/gimpcontainer.h"
#include "core/gimpcontext.h"
#include "core/gimpdisplay.h"
#include "core/gimpdrawable-operation.h"
#include "core/gimpdrawablefilter.h"
#include "core/gimpimage.h"
#include "core/gimplayermask.h"
#include "core/gimpparamspecs.h"
#include "core/gimpsettings.h"
#include "core/gimptoolinfo.h"

#include "widgets/gimpaction.h"
#include "widgets/gimpactiongroup.h"
#include "widgets/gimphelp-ids.h"
#include "widgets/gimpstringaction.h"
#include "widgets/gimpuimanager.h"

#include "tools/gimpoperationtool.h"
#include "tools/tool_manager.h"

#include "gimpgeglprocedure.h"

#include "gimp-intl.h"


static void     gimp_gegl_procedure_finalize            (GObject        *object);

static gint64   gimp_gegl_procedure_get_memsize         (GimpObject     *object,
                                                         gint64         *gui_size);

static gchar  * gimp_gegl_procedure_get_description     (GimpViewable   *viewable,
                                                         gchar         **tooltip);

static const gchar * gimp_gegl_procedure_get_help_id    (GimpProcedure  *procedure);
static const gchar * gimp_gegl_procedure_get_menu_label (GimpProcedure  *procedure);
static gboolean      gimp_gegl_procedure_get_sensitive  (GimpProcedure  *procedure,
                                                         GimpObject     *object,
                                                         const gchar   **reason);
static GimpValueArray * gimp_gegl_procedure_execute     (GimpProcedure  *procedure,
                                                         Gimp           *gimp,
                                                         GimpContext    *context,
                                                         GimpProgress   *progress,
                                                         GimpValueArray *args,
                                                         GError        **error);
static void     gimp_gegl_procedure_execute_async       (GimpProcedure  *procedure,
                                                         Gimp           *gimp,
                                                         GimpContext    *context,
                                                         GimpProgress   *progress,
                                                         GimpValueArray *args,
                                                         GimpDisplay    *display);


G_DEFINE_TYPE (GimpGeglProcedure, gimp_gegl_procedure,
               GIMP_TYPE_PROCEDURE)

#define parent_class gimp_gegl_procedure_parent_class


static void
gimp_gegl_procedure_class_init (GimpGeglProcedureClass *klass)
{
  GObjectClass       *object_class      = G_OBJECT_CLASS (klass);
  GimpObjectClass    *gimp_object_class = GIMP_OBJECT_CLASS (klass);
  GimpViewableClass  *viewable_class    = GIMP_VIEWABLE_CLASS (klass);
  GimpProcedureClass *proc_class        = GIMP_PROCEDURE_CLASS (klass);

  object_class->finalize            = gimp_gegl_procedure_finalize;

  gimp_object_class->get_memsize    = gimp_gegl_procedure_get_memsize;

  viewable_class->default_icon_name = "gimp-gegl";
  viewable_class->get_description   = gimp_gegl_procedure_get_description;

  proc_class->get_help_id           = gimp_gegl_procedure_get_help_id;
  proc_class->get_menu_label        = gimp_gegl_procedure_get_menu_label;
  proc_class->get_sensitive         = gimp_gegl_procedure_get_sensitive;
  proc_class->execute               = gimp_gegl_procedure_execute;
  proc_class->execute_async         = gimp_gegl_procedure_execute_async;
}

static void
gimp_gegl_procedure_init (GimpGeglProcedure *proc)
{
}

static void
gimp_gegl_procedure_finalize (GObject *object)
{
  GimpGeglProcedure *proc = GIMP_GEGL_PROCEDURE (object);

  g_clear_object (&proc->default_settings);

  g_clear_pointer (&proc->operation,  g_free);
  g_clear_pointer (&proc->menu_label, g_free);

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

static gint64
gimp_gegl_procedure_get_memsize (GimpObject *object,
                                 gint64     *gui_size)
{
  GimpGeglProcedure *proc    = GIMP_GEGL_PROCEDURE (object);
  gint64             memsize = 0;

  memsize += gimp_string_get_memsize (proc->operation);
  memsize += gimp_string_get_memsize (proc->menu_label);

  return memsize + GIMP_OBJECT_CLASS (parent_class)->get_memsize (object,
                                                                  gui_size);
}

static gchar *
gimp_gegl_procedure_get_description (GimpViewable  *viewable,
                                     gchar        **tooltip)
{
  GimpProcedure *procedure = GIMP_PROCEDURE (viewable);

  if (tooltip)
    *tooltip = g_strdup (gimp_procedure_get_blurb (procedure));

  return g_strdup (gimp_procedure_get_label (procedure));
}

static const gchar *
gimp_gegl_procedure_get_help_id (GimpProcedure *procedure)
{
  GimpGeglProcedure *proc = GIMP_GEGL_PROCEDURE (procedure);
  GList             *managers;
  GimpActionGroup   *group;
  const gchar       *help_id = NULL;

  managers = gimp_ui_managers_from_name ("<Image>");
  group    = gimp_ui_manager_get_action_group (managers->data, "filters");

  if (procedure->help_id)
    {
      return procedure->help_id;
    }
  else if (group)
    {
      GList *actions;
      GList *iter;

      actions = gimp_action_group_list_actions (group);
      for (iter = actions; iter; iter = iter->next)
        if (GIMP_IS_STRING_ACTION (iter->data) &&
            g_strcmp0 (GIMP_STRING_ACTION (iter->data)->value, proc->operation) == 0)
          {
            help_id = gimp_action_get_help_id (iter->data);
            break;
          }

      g_list_free (actions);
    }

  return help_id == NULL ? GIMP_HELP_TOOL_GEGL : help_id;
}

static const gchar *
gimp_gegl_procedure_get_menu_label (GimpProcedure *procedure)
{
  GimpGeglProcedure *proc = GIMP_GEGL_PROCEDURE (procedure);

  if (proc->menu_label)
    return proc->menu_label;

  return GIMP_PROCEDURE_CLASS (parent_class)->get_menu_label (procedure);
}

static gboolean
gimp_gegl_procedure_get_sensitive (GimpProcedure  *procedure,
                                   GimpObject     *object,
                                   const gchar   **reason)
{
  GimpImage *image     = GIMP_IMAGE (object);
  GList     *drawables = NULL;
  gboolean   sensitive = FALSE;

  if (image)
    drawables = gimp_image_get_selected_drawables (image);

  if (g_list_length (drawables) == 1)
    {
      GimpDrawable *drawable  = drawables->data;
      GimpItem     *item;

      if (GIMP_IS_LAYER_MASK (drawable))
        item = GIMP_ITEM (gimp_layer_mask_get_layer (GIMP_LAYER_MASK (drawable)));
      else
        item = GIMP_ITEM (drawable);

      sensitive = ! gimp_item_is_content_locked (item, NULL);

      if (gimp_viewable_get_children (GIMP_VIEWABLE (drawable)))
        sensitive = FALSE;
    }

  g_list_free (drawables);

  return sensitive;
}

static GimpValueArray *
gimp_gegl_procedure_execute (GimpProcedure   *procedure,
                             Gimp            *gimp,
                             GimpContext     *context,
                             GimpProgress    *progress,
                             GimpValueArray  *args,
                             GError         **error)
{
  GimpImage  *image;
  gint        n_drawables;
  GObject   **drawables;
  GObject    *config;

  image       = g_value_get_object (gimp_value_array_index (args, 1));
  drawables   = g_value_get_boxed  (gimp_value_array_index (args, 2));
  config      = g_value_get_object (gimp_value_array_index (args, 3));

  n_drawables = gimp_core_object_array_get_length ((GObject **) drawables);

  if (n_drawables == 1)
    {
      GeglNode *node;

      node = gegl_node_new_child (NULL,
                                  "operation",
                                  GIMP_GEGL_PROCEDURE (procedure)->operation,
                                  NULL);

      gimp_drawable_apply_operation_with_config (GIMP_DRAWABLE (drawables[0]),
                                                 progress,
                                                 gimp_procedure_get_label (procedure),
                                                 node, config);

      g_object_unref (node);

      gimp_image_flush (image);

      return gimp_procedure_get_return_values (procedure, TRUE, NULL);
    }

  return gimp_procedure_get_return_values (procedure, FALSE, NULL);
}

static void
gimp_gegl_procedure_execute_async (GimpProcedure  *procedure,
                                   Gimp           *gimp,
                                   GimpContext    *context,
                                   GimpProgress   *progress,
                                   GimpValueArray *args,
                                   GimpDisplay    *display)
{
  GimpGeglProcedure *gegl_procedure = GIMP_GEGL_PROCEDURE (procedure);
  GimpRunMode        run_mode;
  GimpObject        *settings;
  GimpTool          *active_tool;
  const gchar       *tool_name;

  run_mode = g_value_get_enum   (gimp_value_array_index (args, 0));
  settings = g_value_get_object (gimp_value_array_index (args, 3));

  if (! settings &&
      (run_mode != GIMP_RUN_INTERACTIVE ||
       GIMP_GUI_CONFIG (gimp->config)->filter_tool_use_last_settings))
    {
      /*  if we didn't get settings passed, get the last used settings  */

      GType          config_type;
      GimpContainer *container;

      config_type = G_VALUE_TYPE (gimp_value_array_index (args, 3));

      container = gimp_operation_config_get_container (gimp, config_type,
                                                       (GCompareFunc)
                                                       gimp_settings_compare);

      settings = gimp_container_get_child_by_index (container, 0);

      /*  only use the settings if they are automatically created
       *  "last used" values, not if they were saved explicitly and
       *  have a zero timestamp; and if they are not a separator.
       */
      if (settings &&
          (GIMP_SETTINGS (settings)->time == 0 ||
           ! gimp_object_get_name (settings)))
        {
          settings = NULL;
        }
    }

  if (run_mode == GIMP_RUN_NONINTERACTIVE ||
      run_mode == GIMP_RUN_WITH_LAST_VALS)
    {
      if (settings || run_mode == GIMP_RUN_NONINTERACTIVE)
        {
          GimpValueArray *return_vals;

          g_value_set_object (gimp_value_array_index (args, 3), settings);
          return_vals = gimp_procedure_execute (procedure, gimp, context, progress,
                                                args, NULL);
          gimp_value_array_unref (return_vals);
          return;
        }

      gimp_message (gimp,
                    G_OBJECT (progress), GIMP_MESSAGE_WARNING,
                    _("There are no last settings for '%s', "
                      "showing the filter dialog instead."),
                    gimp_procedure_get_label (procedure));
    }

  if (! strcmp (gegl_procedure->operation, "gimp:brightness-contrast"))
    {
      tool_name = "gimp-brightness-contrast-tool";
    }
  else if (! strcmp (gegl_procedure->operation, "gimp:curves"))
    {
      tool_name = "gimp-curves-tool";
    }
  else if (! strcmp (gegl_procedure->operation, "gimp:levels"))
    {
      tool_name = "gimp-levels-tool";
    }
  else if (! strcmp (gegl_procedure->operation, "gimp:threshold"))
    {
      tool_name = "gimp-threshold-tool";
    }
  else if (! strcmp (gegl_procedure->operation, "gimp:offset"))
    {
      tool_name = "gimp-offset-tool";
    }
  else
    {
      tool_name = "gimp-operation-tool";
    }

  active_tool = tool_manager_get_active (gimp);

  /*  do not use the passed context because we need to set the active
   *  tool on the global user context
   */
  context = gimp_get_user_context (gimp);

  if (strcmp (gimp_object_get_name (active_tool->tool_info), tool_name))
    {
      GimpToolInfo *tool_info = gimp_get_tool_info (gimp, tool_name);

      if (GIMP_IS_TOOL_INFO (tool_info))
        gimp_context_set_tool (context, tool_info);
    }
  else
    {
      gimp_context_tool_changed (context);
    }

  active_tool = tool_manager_get_active (gimp);

  if (! strcmp (gimp_object_get_name (active_tool->tool_info), tool_name))
    {
      /*  Remember the procedure that created this tool, because
       *  we can't just switch to an operation tool using
       *  gimp_context_set_tool(), we also have to go through the
       *  initialization code below, otherwise we end up with a
       *  dummy tool that does nothing. See bug #776370.
       */
      g_object_set_data_full (G_OBJECT (active_tool), "gimp-gegl-procedure",
                              g_object_ref (procedure),
                              (GDestroyNotify) g_object_unref);

      if (! strcmp (tool_name, "gimp-operation-tool"))
        {
          gimp_operation_tool_set_operation (GIMP_OPERATION_TOOL (active_tool),
                                             gegl_procedure->filter,
                                             gegl_procedure->operation,
                                             gimp_procedure_get_label (procedure),
                                             gimp_procedure_get_label (procedure),
                                             gimp_procedure_get_label (procedure),
                                             gimp_viewable_get_icon_name (GIMP_VIEWABLE (procedure)),
                                             gimp_procedure_get_help_id (procedure));
        }

      /* For custom GIMP operations, we need to set the existing filter to edit
       * before initializing so it doesn't create a separate filter. */
      if (gegl_procedure->filter)
        GIMP_FILTER_TOOL (active_tool)->existing_filter = gegl_procedure->filter;

      tool_manager_initialize_active (gimp, display);

      /* For GIMP-specific GEGL operations, we need to copy over the
       * config object stored in the GeglNode */
      if (gegl_procedure->filter)
        {
          GeglNode *node;

          gimp_filter_set_active (GIMP_FILTER (gegl_procedure->filter), FALSE);

          node = gimp_drawable_filter_get_operation (gegl_procedure->filter);

          if (gimp_operation_config_is_custom (gimp, gegl_procedure->operation))
            {
              gegl_node_get (node,
                             "config", &settings,
                             NULL);
            }
          else
            {
              GParamSpec       **pspecs;
              guint              n_pspecs;
              gdouble            opacity;
              GimpLayerMode      paint_mode;
              GimpFilterRegion   region;

              opacity    = gimp_drawable_filter_get_opacity (gegl_procedure->filter);
              paint_mode = gimp_drawable_filter_get_paint_mode (gegl_procedure->filter);
              region     = gimp_drawable_filter_get_region (gegl_procedure->filter);

              settings =
                g_object_new (gimp_operation_config_get_type (active_tool->tool_info->gimp,
                                                              gegl_procedure->operation,
                                                              gimp_tool_get_icon_name (active_tool),
                                                              GIMP_TYPE_OPERATION_SETTINGS),
                              NULL);

              pspecs = gegl_operation_list_properties (gegl_procedure->operation, &n_pspecs);

              for (gint i = 0; i < n_pspecs; i++)
                {
                  GValue      value      = G_VALUE_INIT;
                  GParamSpec *pspec      = pspecs[i];
                  GParamSpec *gimp_pspec = g_object_class_find_property (G_OBJECT_GET_CLASS (settings),
                                                                         pspec->name);

                  g_value_init (&value, pspec->value_type);
                  gegl_node_get_property (node, pspec->name,
                                          &value);

                  if (gimp_pspec)
                    g_object_set_property (G_OBJECT (settings), gimp_pspec->name,
                                           &value);
                  else
                    g_critical ("%s: property '%s' of operation '%s' doesn't exist in config %s",
                                G_STRFUNC, pspec->name, gegl_procedure->operation,
                                g_type_name (G_TYPE_FROM_INSTANCE (settings)));
                  g_value_unset (&value);
                }
              g_free (pspecs);

              g_object_set (settings,
                            "gimp-opacity", opacity,
                            "gimp-mode",    paint_mode,
                            "gimp-region",  region,
                            NULL);
            }
        }

      if (settings)
        gimp_filter_tool_set_config (GIMP_FILTER_TOOL (active_tool),
                                     GIMP_CONFIG (settings));
    }
}


/*  public functions  */

GimpProcedure *
gimp_gegl_procedure_new (Gimp               *gimp,
                         GimpDrawableFilter *filter_to_edit,
                         GimpRunMode         default_run_mode,
                         GimpObject         *default_settings,
                         const gchar        *operation,
                         const gchar        *name,
                         const gchar        *menu_label,
                         const gchar        *tooltip,
                         const gchar        *icon_name,
                         const gchar        *help_id)
{
  GimpProcedure     *procedure;
  GimpGeglProcedure *gegl_procedure;
  GType              config_type;

  g_return_val_if_fail (GIMP_IS_GIMP (gimp), NULL);
  g_return_val_if_fail (operation != NULL, NULL);
  g_return_val_if_fail (name != NULL, NULL);
  g_return_val_if_fail (menu_label != NULL, NULL);

  config_type = gimp_operation_config_get_type (gimp, operation, icon_name,
                                                GIMP_TYPE_OPERATION_SETTINGS);

  procedure = g_object_new (GIMP_TYPE_GEGL_PROCEDURE, NULL);

  gegl_procedure = GIMP_GEGL_PROCEDURE (procedure);

  gegl_procedure->filter           = filter_to_edit;
  gegl_procedure->operation        = g_strdup (operation);
  gegl_procedure->default_run_mode = default_run_mode;
  gegl_procedure->menu_label       = g_strdup (menu_label);

  if (default_settings)
    gegl_procedure->default_settings = g_object_ref (default_settings);

  gimp_object_set_name (GIMP_OBJECT (procedure), name);
  gimp_viewable_set_icon_name (GIMP_VIEWABLE (procedure), icon_name);
  gimp_procedure_set_help (procedure,
                           tooltip,
                           tooltip,
                           help_id);
  gimp_procedure_set_static_attribution (procedure,
                                         "author", "copyright", "date");

  gimp_procedure_add_argument (procedure,
                               gimp_param_spec_enum ("run-mode",
                                                     "Run mode",
                                                     "Run mode",
                                                     GIMP_TYPE_RUN_MODE,
                                                     GIMP_RUN_INTERACTIVE,
                                                     GIMP_PARAM_READWRITE));
  gimp_procedure_add_argument (procedure,
                               gimp_param_spec_image ("image",
                                                      "Image",
                                                      "Input image",
                                                      FALSE,
                                                      GIMP_PARAM_READWRITE));
  gimp_procedure_add_argument (procedure,
                               gimp_param_spec_core_object_array ("drawables",
                                                                  "Drawables",
                                                                  "Input drawables",
                                                                  GIMP_TYPE_DRAWABLE,
                                                                  GIMP_PARAM_READWRITE));
  gimp_procedure_add_argument (procedure,
                               g_param_spec_object ("settings",
                                                    "Settings",
                                                    "Settings",
                                                    config_type,
                                                    GIMP_PARAM_READWRITE));

  return procedure;
}

gboolean
gimp_gegl_procedure_is_editing_filter (GimpGeglProcedure *procedure)
{
  g_return_val_if_fail (GIMP_IS_GEGL_PROCEDURE (procedure), FALSE);

  return (procedure->filter != NULL);
}
