/*
    This file is part of darktable,
    Copyright (C) 2014-2025 darktable developers.

    darktable 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.

    darktable 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 darktable.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "common/dbus.h"
#include "control/progress.h"
#include "control/control.h"

#ifdef HAVE_UNITY
#include <unity/unity/unity.h>
#endif
#ifdef MAC_INTEGRATION
#include <gtkosxapplication.h>
#endif

#ifdef _WIN32
#include <gdk/gdkwin32.h>
#ifndef ITaskbarList3_SetProgressValue
  #define ITaskbarList3_SetProgressValue(This,hwnd,ullCompleted,ullTotal) (This)->lpVtbl->SetProgressValue(This,hwnd,ullCompleted,ullTotal)
#endif
#ifndef ITaskbarList3_SetProgressState
  #define ITaskbarList3_SetProgressState(This,hwnd,tbpFlags) (This)->lpVtbl->SetProgressState(This,hwnd,tbpFlags)
#endif
#ifndef ITaskbarList3_HrInit
  #define ITaskbarList3_HrInit(This) (This)->lpVtbl->HrInit(This)
#endif
#endif


typedef struct _dt_progress_t
{
  double progress;
  gchar *message;
  gboolean has_progress_bar;
  dt_pthread_mutex_t mutex;
  void *gui_data;

  // cancel callback and its data
  dt_progress_cancel_callback_t cancel;
  void *cancel_data;

#ifdef HAVE_UNITY
  UnityLauncherEntry *darktable_launcher;
#endif

} _dt_progress_t;

static void _global_progress_start(dt_control_t *control, dt_progress_t *progress)
{
  control->progress_system.n_progress_bar++;

#ifndef _WIN32

#ifdef HAVE_UNITY

  progress->darktable_launcher = unity_launcher_entry_get_for_desktop_id("org.darktable.darktable.desktop");
  unity_launcher_entry_set_progress(progress->darktable_launcher, 0.0);
  unity_launcher_entry_set_progress_visible(progress->darktable_launcher, TRUE);

#else

  // this should work for unity as well as kde
  // https://wiki.ubuntu.com/Unity/LauncherAPI#Low_level_DBus_API:_com.canonical.Unity.LauncherEntry
  if(darktable.dbus && darktable.dbus->dbus_connection)
  {
    GError *error = NULL;
    g_object_ref(G_OBJECT(darktable.dbus->dbus_connection));

    GVariantBuilder builder;
    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
    g_variant_builder_add(&builder, "{sv}", "progress", g_variant_new_double(control->progress_system.global_progress));
    g_variant_builder_add(&builder, "{sv}", "progress-visible", g_variant_new_boolean(TRUE));
    GVariant *params = g_variant_new("(sa{sv})", "application://org.darktable.darktable.desktop", &builder);

    g_dbus_connection_emit_signal(darktable.dbus->dbus_connection,
                                  "com.canonical.Unity",
                                  "/darktable",
                                  "com.canonical.Unity.LauncherEntry",
                                  "Update",
                                  params,
                                  &error);
    if(error)
    {
      dt_print(DT_DEBUG_ALWAYS, "[progress_create] dbus error: %s", error->message);
      g_error_free(error);
    }
  }

#endif // HAVE_UNITY

#else // _WIN32
/* Commented out for 5.4 because this code could cause a crash when importing
   a folder from the command line and there was not enough time before the planned
   release to figure out how to properly fix the code.
   Should be uncommented back and investigated how to properly fix this when time permits.
  // we can't init this in dt_control_progress_init as it's run too early :/
  if(!control->progress_system.taskbarlist && darktable.gui)
  {
    void *taskbarlist;
    if(CoCreateInstance(&CLSID_TaskbarList, NULL, CLSCTX_INPROC_SERVER, &IID_ITaskbarList3, (void **)&taskbarlist) == S_OK)
      if(ITaskbarList3_HrInit((ITaskbarList3 *)taskbarlist) == S_OK)
        control->progress_system.taskbarlist = taskbarlist;
  }

  if(control->progress_system.taskbarlist)
  {
    HWND hwnd = GDK_WINDOW_HWND(gtk_widget_get_window(dt_ui_main_window(darktable.gui->ui)));
    if(ITaskbarList3_SetProgressState(control->progress_system.taskbarlist, hwnd, TBPF_NORMAL) != S_OK)
      dt_print(DT_DEBUG_ALWAYS, "[progress_create] SetProgressState failed");
    if(ITaskbarList3_SetProgressValue(control->progress_system.taskbarlist, hwnd, control->progress_system.global_progress * 100, 100) != S_OK)
      dt_print(DT_DEBUG_ALWAYS, "[progress_create] SetProgressValue failed");
  }
*/
#endif
}

static void _global_progress_set(dt_control_t *control, dt_progress_t *progress, double value)
{
  control->progress_system.global_progress = MAX(control->progress_system.global_progress, value);

#ifndef _WIN32

#ifdef HAVE_UNITY

  unity_launcher_entry_set_progress(progress->darktable_launcher, value);

#else

  if(darktable.dbus && darktable.dbus->dbus_connection)
  {
    GError *error = NULL;

    GVariantBuilder builder;
    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
    g_variant_builder_add(&builder, "{sv}", "progress", g_variant_new_double(control->progress_system.global_progress));
    GVariant *params = g_variant_new("(sa{sv})", "application://org.darktable.darktable.desktop", &builder);

    g_dbus_connection_emit_signal(darktable.dbus->dbus_connection,
                                  "com.canonical.Unity",
                                  "/darktable",
                                  "com.canonical.Unity.LauncherEntry",
                                  "Update",
                                  params,
                                  &error);
    if(error)
    {
      dt_print(DT_DEBUG_ALWAYS, "[progress_set] dbus error: %s", error->message);
      g_error_free(error);
    }
  }

#endif // HAVE_UNITY

#else // _WIN32

  if(control->progress_system.taskbarlist)
  {
    HWND hwnd = GDK_WINDOW_HWND(gtk_widget_get_window(dt_ui_main_window(darktable.gui->ui)));
    if(ITaskbarList3_SetProgressValue(control->progress_system.taskbarlist, hwnd, control->progress_system.global_progress * 100, 100) != S_OK)
      dt_print(DT_DEBUG_ALWAYS, "[progress_create] SetProgressValue failed");
  }

#endif
}

static void _global_progress_end(dt_control_t *control, dt_progress_t *progress)
{
  control->progress_system.n_progress_bar--;

  // find the biggest progress value among the remaining progress bars
  control->progress_system.global_progress = 0.0;
  for(GList *iter = control->progress_system.list; iter; iter = g_list_next(iter))
  {
    // this is called after the current progress got removed from the list!
    dt_progress_t *p = iter->data;
    const double value = dt_control_progress_get_progress(p);
    control->progress_system.global_progress = MAX(control->progress_system.global_progress, value);
  }

#ifndef _WIN32

#ifdef HAVE_UNITY

  unity_launcher_entry_set_progress(progress->darktable_launcher, 1.0);
  unity_launcher_entry_set_progress_visible(progress->darktable_launcher, FALSE);

#else

  if(darktable.dbus && darktable.dbus->dbus_connection)
  {
    GError *error = NULL;

    GVariantBuilder builder;
    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
    if(control->progress_system.n_progress_bar == 0)
      g_variant_builder_add(&builder, "{sv}", "progress-visible", g_variant_new_boolean(FALSE));
    g_variant_builder_add(&builder, "{sv}", "progress", g_variant_new_double(control->progress_system.global_progress));
    GVariant *params = g_variant_new("(sa{sv})", "application://org.darktable.darktable.desktop", &builder);

    g_dbus_connection_emit_signal(darktable.dbus->dbus_connection,
                                  "com.canonical.Unity",
                                  "/darktable",
                                  "com.canonical.Unity.LauncherEntry",
                                  "Update",
                                  params,
                                  &error);
    if(error)
    {
      dt_print(DT_DEBUG_ALWAYS, "[progress_destroy] dbus error: %s", error->message);
      g_error_free(error);
    }

    g_object_unref(G_OBJECT(darktable.dbus->dbus_connection));
    darktable.dbus->dbus_connection = NULL;
  }

#endif // HAVE_UNITY

#else // _WIN32

  if(control->progress_system.taskbarlist)
  {
    HWND hwnd = GDK_WINDOW_HWND(gtk_widget_get_window(dt_ui_main_window(darktable.gui->ui)));
    if(control->progress_system.n_progress_bar == 0)
    {
      if(ITaskbarList3_SetProgressState(control->progress_system.taskbarlist, hwnd, TBPF_NOPROGRESS) != S_OK)
        dt_print(DT_DEBUG_ALWAYS, "[progress_create] SetProgressState failed");
    }
    else
    {
      if(ITaskbarList3_SetProgressValue(control->progress_system.taskbarlist, hwnd,
                                        control->progress_system.global_progress * 100, 100) != S_OK)
        dt_print(DT_DEBUG_ALWAYS, "[progress_create] SetProgressValue failed");
    }
  }

#endif
}

void dt_control_progress_init(void)
{
#ifndef _WIN32

#ifdef HAVE_UNITY

  UnityLauncherEntry *darktable_launcher = unity_launcher_entry_get_for_desktop_id("org.darktable.darktable.desktop");
  unity_launcher_entry_set_progress_visible(darktable_launcher, FALSE);

#else

  if(darktable.dbus && darktable.dbus->dbus_connection)
  {
    GError *error = NULL;

    GVariantBuilder builder;
    g_variant_builder_init(&builder, G_VARIANT_TYPE("a{sv}"));
    g_variant_builder_add(&builder, "{sv}", "progress-visible", g_variant_new_boolean(FALSE));
    GVariant *params = g_variant_new("(sa{sv})", "application://org.darktable.darktable.desktop", &builder);

    g_dbus_connection_emit_signal(darktable.dbus->dbus_connection,
                                  "com.canonical.Unity",
                                  "/darktable",
                                  "com.canonical.Unity.LauncherEntry",
                                  "Update",
                                  params,
                                  &error);
    if(error)
    {
      dt_print(DT_DEBUG_ALWAYS, "[progress_init] dbus error: %s", error->message);
      g_error_free(error);
    }

    g_object_unref(G_OBJECT(darktable.dbus->dbus_connection));
    darktable.dbus->dbus_connection = NULL;
  }

#endif // HAVE_UNITY

#else // _WIN32

  // initializing control->progress_system.taskbarlist in here doesn't work,
  // it seems to only succeed after dt_gui_gtk_init

#endif // _WIN32
}

dt_progress_t *dt_control_progress_create(const gboolean has_progress_bar,
                                          const gchar *message)
{
  // create the object
  dt_control_t *control = darktable.control;
  if(!control) return NULL;

  dt_progress_t *progress = calloc(1, sizeof(dt_progress_t));
  dt_pthread_mutex_init(&progress->mutex, NULL);

  // fill it with values
  progress->message = g_strdup(message);
  progress->has_progress_bar = has_progress_bar;

  dt_pthread_mutex_lock(&control->progress_system.mutex);

  // add it to the global list
  control->progress_system.list = g_list_append(control->progress_system.list, progress);
  control->progress_system.list_length++;
  if(has_progress_bar) _global_progress_start(control, progress);

  // tell the gui
  if(control->progress_system.proxy.module != NULL)
    progress->gui_data = control->progress_system.proxy.added(control->progress_system.proxy.module,
                                                              has_progress_bar, message);

  dt_pthread_mutex_unlock(&control->progress_system.mutex);

  return progress;
}

void dt_control_progress_destroy(dt_progress_t *progress)
{
  dt_control_t *control = darktable.control;
  if(!control || !progress) return;
  dt_pthread_mutex_lock(&control->progress_system.mutex);

  // tell the gui
  if(control->progress_system.proxy.module != NULL)
    control->progress_system.proxy.destroyed(control->progress_system.proxy.module, progress->gui_data);

  // remove the object from the global list
  control->progress_system.list = g_list_remove(control->progress_system.list, progress);
  control->progress_system.list_length--;
  if(progress->has_progress_bar) _global_progress_end(control, progress);

  dt_pthread_mutex_unlock(&control->progress_system.mutex);

  // free the object
  dt_pthread_mutex_destroy(&progress->mutex);
  g_free(progress->message);
  free(progress);
}

void dt_control_progress_make_cancellable(dt_progress_t *progress,
                                          dt_progress_cancel_callback_t cancel,
                                          void *data)
{
  dt_control_t *control = darktable.control;
  if(!control || !progress) return;
  // set the value
  dt_pthread_mutex_lock(&progress->mutex);
  progress->cancel = cancel;
  progress->cancel_data = data;
  dt_pthread_mutex_unlock(&progress->mutex);

  // tell the gui
  dt_pthread_mutex_lock(&control->progress_system.mutex);
  if(control->progress_system.proxy.module != NULL)
    control->progress_system.proxy.cancellable(control->progress_system.proxy.module, progress->gui_data,
                                               progress);
  dt_pthread_mutex_unlock(&control->progress_system.mutex);
}

static void _control_progress_cancel_callback(dt_progress_t *progress, void *data)
{
  // cancelling a job always includes destroying the progress structure including the mutex
  dt_control_job_cancel((dt_job_t *)data);
}

void dt_control_progress_attach_job(dt_progress_t *progress, dt_job_t *job)
{
  if(progress && job)
    dt_control_progress_make_cancellable(progress, &_control_progress_cancel_callback, job);
}

void dt_control_progress_cancel(dt_progress_t *progress)
{
  if(!progress) return;
  dt_pthread_mutex_lock(&progress->mutex);
  const gboolean ignore = progress->cancel == NULL;
  dt_pthread_mutex_unlock(&progress->mutex);
  if(ignore) return;

/*
Calling the progress->cancel callback is somewhat tricky as that might include
destroying the progress structure which also includes destroying the process->mutex.
  _control_progress_cancel_callback() doing in this order
  --> dt_control_job_cancel() --> _control_job_set_state --> dt_control_progress_destroy

So in this case - we used dt_control_progress_attach_job()
  a)  we **must not** call progress->cancel with the mutex locked as that would make the mutex
      destroy failing leading to assert() failures in debug builds and an undefined behaviour in
      release builds.
  b)  We unlock a mutex immediately after that but that location is not existing any more.

Otherwise, do the cancel callback in mutex locked state as the progress struct will not be destroyed.
*/
  const gboolean destroying = progress->cancel == &_control_progress_cancel_callback;
  if(destroying)
    progress->cancel(progress, progress->cancel_data);
  else
  {
    dt_pthread_mutex_lock(&progress->mutex);
    progress->cancel(progress, progress->cancel_data);
    dt_pthread_mutex_unlock(&progress->mutex);
  }

  // the gui doesn't need to know I guess, it wouldn't to anything with that bit of information
}

void dt_control_progress_set_progress(dt_progress_t *progress, double value)
{
  dt_control_t *control = darktable.control;
  if(!control || !progress) return;

  dt_pthread_mutex_lock(&progress->mutex);
  progress->progress = CLAMP(value, 0.0, 1.0);
  dt_pthread_mutex_unlock(&progress->mutex);

  // tell the gui
  dt_pthread_mutex_lock(&control->progress_system.mutex);
  if(control->progress_system.proxy.module != NULL)
    control->progress_system.proxy.updated(control->progress_system.proxy.module, progress->gui_data, value);

  if(progress->has_progress_bar) _global_progress_set(control, progress, value);

  dt_pthread_mutex_unlock(&control->progress_system.mutex);
}

double dt_control_progress_get_progress(dt_progress_t *progress)
{
  if(!progress) return -1.0;
  dt_pthread_mutex_lock(&progress->mutex);
  const double res = progress->progress;
  dt_pthread_mutex_unlock(&progress->mutex);
  return res;
}

const gchar *dt_control_progress_get_message(dt_progress_t *progress)
{
  if(!progress) return "";
  dt_pthread_mutex_lock(&progress->mutex);
  const gchar *res = progress->message;
  dt_pthread_mutex_unlock(&progress->mutex);
  return res;
}

void dt_control_progress_set_message(dt_progress_t *progress, const char *message)
{
  dt_control_t *control = darktable.control;
  if(!control || !progress) return;
  dt_pthread_mutex_lock(&progress->mutex);
  g_free(progress->message);
  progress->message = g_strdup(message);
  dt_pthread_mutex_unlock(&progress->mutex);

  // tell the gui
  dt_pthread_mutex_lock(&control->progress_system.mutex);
  if(control->progress_system.proxy.module != NULL)
    control->progress_system.proxy.message_updated(control->progress_system.proxy.module, progress->gui_data,
                                                   message);
  dt_pthread_mutex_unlock(&control->progress_system.mutex);
}

void dt_control_progress_set_gui_data(dt_progress_t *progress, void *data)
{
  if(!progress) return;
  dt_pthread_mutex_lock(&progress->mutex);
  progress->gui_data = data;
  dt_pthread_mutex_unlock(&progress->mutex);
}

void *dt_control_progress_get_gui_data(dt_progress_t *progress)
{
  if(!progress) return NULL;
  dt_pthread_mutex_lock(&progress->mutex);
  void *res = progress->gui_data;
  dt_pthread_mutex_unlock(&progress->mutex);
  return res;
}

gboolean dt_control_progress_has_progress_bar(dt_progress_t *progress)
{
  if(!progress) return FALSE;
  dt_pthread_mutex_lock(&progress->mutex);
  const gboolean res = progress->has_progress_bar;
  dt_pthread_mutex_unlock(&progress->mutex);
  return res;
}

gboolean dt_control_progress_cancellable(dt_progress_t *progress)
{
  if(!progress) return FALSE;
  dt_pthread_mutex_lock(&progress->mutex);
  const gboolean res = progress->cancel != NULL;
  dt_pthread_mutex_unlock(&progress->mutex);
  return res;
}

// clang-format off
// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
// vim: shiftwidth=2 expandtab tabstop=2 cindent
// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
// clang-format on

