/* PIKA - Photo and Image Kooker Application
 * a rebranding of The GNU Image Manipulation Program (created with heckimp)
 * A derived work which may be trivial. However, any changes may be (C)2023 by Aldercone Studio
 *
 * Original copyright, applying to most contents (license remains unchanged): 
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * pikapdbdialog.c
 * Copyright (C) 2004 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 "libpikabase/pikabase.h"
#include "libpikawidgets/pikawidgets.h"

#include "widgets-types.h"

#include "core/pika.h"
#include "core/pikacontext.h"
#include "core/pikaresource.h"

#include "pdb/pikapdb.h"

#include "pikamenufactory.h"
#include "pikapdbdialog.h"
#include "pikawidgets-utils.h"

#include "pika-intl.h"


enum
{
  PROP_0,
  PROP_PDB,
  PROP_CONTEXT,
  PROP_SELECT_TYPE,
  PROP_INITIAL_OBJECT,
  PROP_CALLBACK_NAME,
  PROP_MENU_FACTORY
};


static void     pika_pdb_dialog_constructed       (GObject            *object);
static void     pika_pdb_dialog_dispose           (GObject            *object);
static void     pika_pdb_dialog_set_property      (GObject            *object,
                                                   guint               property_id,
                                                   const GValue       *value,
                                                   GParamSpec         *pspec);

static void     pika_pdb_dialog_response          (GtkDialog          *dialog,
                                                   gint                response_id);

static void     pika_pdb_dialog_context_changed   (PikaContext        *context,
                                                   PikaObject         *object,
                                                   PikaPdbDialog      *dialog);
static void     pika_pdb_dialog_plug_in_closed    (PikaPlugInManager  *manager,
                                                   PikaPlugIn         *plug_in,
                                                   PikaPdbDialog      *dialog);

static gboolean pika_pdb_dialog_run_callback_idle (PikaPdbDialog      *dialog);


G_DEFINE_ABSTRACT_TYPE (PikaPdbDialog, pika_pdb_dialog, PIKA_TYPE_DIALOG)

#define parent_class pika_pdb_dialog_parent_class


static void
pika_pdb_dialog_class_init (PikaPdbDialogClass *klass)
{
  GObjectClass   *object_class = G_OBJECT_CLASS (klass);
  GtkDialogClass *dialog_class = GTK_DIALOG_CLASS (klass);

  parent_class = g_type_class_peek_parent (klass);

  object_class->constructed  = pika_pdb_dialog_constructed;
  object_class->dispose      = pika_pdb_dialog_dispose;
  object_class->set_property = pika_pdb_dialog_set_property;

  dialog_class->response     = pika_pdb_dialog_response;

  klass->run_callback        = NULL;
  klass->get_object          = NULL;
  klass->set_object          = NULL;

  g_object_class_install_property (object_class, PROP_CONTEXT,
                                   g_param_spec_object ("context", NULL, NULL,
                                                        PIKA_TYPE_CONTEXT,
                                                        PIKA_PARAM_WRITABLE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_PDB,
                                   g_param_spec_object ("pdb", NULL, NULL,
                                                        PIKA_TYPE_PDB,
                                                        PIKA_PARAM_WRITABLE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_SELECT_TYPE,
                                   g_param_spec_gtype ("select-type",
                                                       NULL, NULL,
                                                       PIKA_TYPE_OBJECT,
                                                       PIKA_PARAM_WRITABLE |
                                                       G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_INITIAL_OBJECT,
                                   g_param_spec_object ("initial-object",
                                                        NULL, NULL,
                                                        PIKA_TYPE_OBJECT,
                                                        PIKA_PARAM_WRITABLE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_CALLBACK_NAME,
                                   g_param_spec_string ("callback-name",
                                                        NULL, NULL,
                                                        NULL,
                                                        PIKA_PARAM_WRITABLE |
                                                        G_PARAM_CONSTRUCT_ONLY));

  g_object_class_install_property (object_class, PROP_MENU_FACTORY,
                                   g_param_spec_object ("menu-factory",
                                                        NULL, NULL,
                                                        PIKA_TYPE_MENU_FACTORY,
                                                        PIKA_PARAM_WRITABLE |
                                                        G_PARAM_CONSTRUCT_ONLY));
}

static void
pika_pdb_dialog_init (PikaPdbDialog *dialog)
{
  gtk_dialog_add_button (GTK_DIALOG (dialog),
                         _("_OK"), GTK_RESPONSE_OK);
  gtk_dialog_add_button (GTK_DIALOG (dialog),
                         _("_Cancel"), GTK_RESPONSE_CANCEL);
}

static void
pika_pdb_dialog_constructed (GObject *object)
{
  PikaPdbDialog      *dialog = PIKA_PDB_DIALOG (object);
  PikaPdbDialogClass *klass  = PIKA_PDB_DIALOG_GET_CLASS (object);
  const gchar        *signal_name;

  G_OBJECT_CLASS (parent_class)->constructed (object);

  klass->dialogs = g_list_prepend (klass->dialogs, dialog);

  pika_assert (PIKA_IS_PDB (dialog->pdb));
  pika_assert (PIKA_IS_CONTEXT (dialog->caller_context));
  pika_assert (g_type_is_a (dialog->select_type, PIKA_TYPE_OBJECT));

  dialog->context = pika_context_new (dialog->caller_context->pika,
                                      G_OBJECT_TYPE_NAME (object),
                                      NULL);

  if (g_type_is_a (dialog->select_type, PIKA_TYPE_RESOURCE))
    {
      pika_context_set_by_type (dialog->context, dialog->select_type,
                                dialog->initial_object);

      signal_name = pika_context_type_to_signal_name (dialog->select_type);

      g_signal_connect_object (dialog->context, signal_name,
                               G_CALLBACK (pika_pdb_dialog_context_changed),
                               dialog, 0);
    }

  g_signal_connect_object (dialog->caller_context->pika->plug_in_manager,
                           "plug-in-closed",
                           G_CALLBACK (pika_pdb_dialog_plug_in_closed),
                           dialog, 0);
}

static void
pika_pdb_dialog_dispose (GObject *object)
{
  PikaPdbDialog      *dialog = PIKA_PDB_DIALOG (object);
  PikaPdbDialogClass *klass  = PIKA_PDB_DIALOG_GET_CLASS (object);

  klass->dialogs = g_list_remove (klass->dialogs, object);

  g_clear_object (&dialog->pdb);
  g_clear_object (&dialog->caller_context);
  g_clear_object (&dialog->context);
  g_clear_object (&dialog->initial_object);

  g_clear_pointer (&dialog->callback_name, g_free);

  g_clear_object (&dialog->menu_factory);

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

static void
pika_pdb_dialog_set_property (GObject      *object,
                              guint         property_id,
                              const GValue *value,
                              GParamSpec   *pspec)
{
  PikaPdbDialog *dialog = PIKA_PDB_DIALOG (object);

  switch (property_id)
    {
    case PROP_PDB:
      dialog->pdb = g_value_dup_object (value);
      break;

    case PROP_CONTEXT:
      dialog->caller_context = g_value_dup_object (value);
      break;

    case PROP_SELECT_TYPE:
      dialog->select_type = g_value_get_gtype (value);
      break;

    case PROP_INITIAL_OBJECT:
      dialog->initial_object = g_value_dup_object (value);
      break;

    case PROP_CALLBACK_NAME:
      if (dialog->callback_name)
        g_free (dialog->callback_name);
      dialog->callback_name = g_value_dup_string (value);
      break;

    case PROP_MENU_FACTORY:
      dialog->menu_factory = g_value_dup_object (value);
      break;

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

static void
pika_pdb_dialog_response (GtkDialog *gtk_dialog,
                          gint       response_id)
{
  PikaPdbDialog *dialog = PIKA_PDB_DIALOG (gtk_dialog);

  if (response_id != GTK_RESPONSE_OK)
    {
      if (g_type_is_a (dialog->select_type, PIKA_TYPE_RESOURCE))
        {
          pika_context_set_by_type (dialog->context, dialog->select_type,
                                    dialog->initial_object);
        }
      else
        {
          PikaPdbDialogClass *klass = PIKA_PDB_DIALOG_GET_CLASS (dialog);

          g_return_if_fail (klass->set_object != NULL);
          klass->set_object (dialog, dialog->initial_object);
        }
    }

  pika_pdb_dialog_run_callback (&dialog, TRUE);
  gtk_widget_destroy (GTK_WIDGET (dialog));
}

void
pika_pdb_dialog_run_callback (PikaPdbDialog **dialog,
                              gboolean        closing)
{
  PikaPdbDialogClass *klass = PIKA_PDB_DIALOG_GET_CLASS (*dialog);
  PikaObject         *object;

  g_object_add_weak_pointer (G_OBJECT (*dialog), (gpointer) dialog);

  if (g_type_is_a ((*dialog)->select_type, PIKA_TYPE_RESOURCE))
    {
      object = pika_context_get_by_type ((*dialog)->context, (*dialog)->select_type);
    }
  else
    {
      g_return_if_fail (klass->get_object != NULL);
      object = klass->get_object (*dialog);
    }

  if (*dialog                  &&
      klass->run_callback      &&
      (*dialog)->callback_name &&
      ! (*dialog)->callback_busy)
    {
      (*dialog)->callback_busy = TRUE;

      if (pika_pdb_lookup_procedure ((*dialog)->pdb, (*dialog)->callback_name) &&
          (object == NULL || g_type_is_a (G_TYPE_FROM_INSTANCE (object), (*dialog)->select_type)))
        {
          PikaValueArray *return_vals;
          GError         *error = NULL;

          return_vals = klass->run_callback (*dialog, object, closing, &error);

          if (*dialog &&
              g_value_get_enum (pika_value_array_index (return_vals, 0)) !=
              PIKA_PDB_SUCCESS)
            {
              const gchar *message;

              if (error && error->message)
                message = error->message;
              else
                message = _("The corresponding plug-in may have crashed.");

              pika_message ((*dialog)->caller_context->pika, G_OBJECT (*dialog),
                            PIKA_MESSAGE_ERROR,
                            _("Unable to run %s callback.\n%s"),
                            g_type_name (G_TYPE_FROM_INSTANCE (*dialog)),
                            message);
            }
          else if (*dialog && error)
            {
              pika_message_literal ((*dialog)->caller_context->pika, G_OBJECT (*dialog),
                                    PIKA_MESSAGE_ERROR,
                                    error->message);
              g_error_free (error);
            }

          pika_value_array_unref (return_vals);
        }

      if (*dialog)
        (*dialog)->callback_busy = FALSE;
    }

  if (*dialog)
    g_object_remove_weak_pointer (G_OBJECT (*dialog), (gpointer) dialog);
}

PikaPdbDialog *
pika_pdb_dialog_get_by_callback (PikaPdbDialogClass *klass,
                                 const gchar        *callback_name)
{
  GList *list;

  g_return_val_if_fail (PIKA_IS_PDB_DIALOG_CLASS (klass), NULL);
  g_return_val_if_fail (callback_name != NULL, NULL);

  for (list = klass->dialogs; list; list = g_list_next (list))
    {
      PikaPdbDialog *dialog = list->data;

      if (dialog->callback_name &&
          ! strcmp (callback_name, dialog->callback_name))
        return dialog;
    }

  return NULL;
}


/*  private functions  */

static void
pika_pdb_dialog_context_changed (PikaContext   *context,
                                 PikaObject    *object,
                                 PikaPdbDialog *dialog)
{
  /* XXX Long explanation below because this was annoying to debug!
   *
   * The context might change for 2 reasons: either from code coming from the
   * plug-in or because of direct GUI interaction with the dialog. This second
   * case may happen while the plug-in is doing something else completely and
   * making PDB calls too. If the callback itself triggers the plug-in to run
   * other PDB calls, we may end up in a case where PDB requests made by the
   * plug-in receive the wrong result. Here is an actual case I encountered:
   *
   * 1. The plug-in calls pika-gradient-get-uniform-samples to draw a button.
   * 2. Because you click inside the dialog (using any of the Gimp*Select
   *    subclasses, e.g. PikaBrushSelect), the core dialog run its callback
   *    indicating that a resource changed. This is unrelated to step 1. It just
   *    happens nearly in the same time by a race condition.
   * 3. The plug-in receives the callback as a temp procedure request, processes
   *    it first. In particular, as part of the code in libpika/plug-in, it
   *    verifies the resource is valid calling pika-resource-id-is-valid.
   * 4. Meanwhile, the core returns pika-gradient-get-uniform-samples result.
   * 5. The plug-in takes it as a result for pika-resource-id-is-valid with
   *    invalid return values (size and types).
   * 6. Then again, it receives result of pika-resource-id-is-valid as being the
   *    result of pika-gradient-get-uniform-samples, again invalid.
   *
   * This scenario mostly happens because the callback is not the consequence of
   * the plug-in's requests (if the callback from core were the consequence of a
   * call from the plug-in, the PDB protocol would ensure that every message in
   * both direction get its return values in the proper order). The problem here
   * is really that the callback interrupts unexpectedly PDB requests coming
   * from the plug-in while the plug-in process can't really know that this
   * callback is not a consequence of its own PDB call.
   *
   * Running as idle will make the callback run when the core is not already
   * processing anything, which should also mean that the plug-in is not waiting
   * for an unrelated procedure result.
   * To be fair, I am not 100% sure if a complex race condition may not still
   * happen, but extensive testing were successful so far.
   */
  if (object)
    g_idle_add_full (G_PRIORITY_DEFAULT_IDLE,
                     (GSourceFunc) pika_pdb_dialog_run_callback_idle,
                     g_object_ref (dialog), g_object_unref);
}

static void
pika_pdb_dialog_plug_in_closed (PikaPlugInManager *manager,
                                PikaPlugIn        *plug_in,
                                PikaPdbDialog     *dialog)
{
  if (dialog->caller_context && dialog->callback_name)
    {
      if (! pika_pdb_lookup_procedure (dialog->pdb, dialog->callback_name))
        {
          gtk_dialog_response (GTK_DIALOG (dialog), GTK_RESPONSE_CLOSE);
        }
    }
}

static gboolean
pika_pdb_dialog_run_callback_idle (PikaPdbDialog *dialog)
{
  pika_pdb_dialog_run_callback (&dialog, FALSE);

  return G_SOURCE_REMOVE;
}
