#include "glib-object.h"
#include "glib.h"
#include "glibconfig.h"
#include <barrier>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <gtk/gtk.h>
#include <libgimp/gimp.h>
#include <libgimp/gimpenums.h>
#include <libgimp/gimpui.h>
#include <libgimpbase/gimpbaseenums.h>
#include <thread>
#include <vector>

#define PLUG_IN_PROC "plug-in-seam-carving"
#define PLUG_IN_BINARY "seam_carving"

struct _Plugin {
  GimpPlugIn parent_instance;
};

#define PLUGIN_TYPE (plugin_get_type())
G_DECLARE_FINAL_TYPE(Plugin, plugin, plugin, , GimpPlugIn)

static GList *plugin_query_procedures(GimpPlugIn *plug_in);
static GimpProcedure *plugin_create_procedure(GimpPlugIn *plug_in,
                                              const gchar *name);

static GimpValueArray *plugin_run(GimpProcedure *procedure,
                                  GimpRunMode run_mode, GimpImage *image,
                                  GimpDrawable **drawables,
                                  GimpProcedureConfig *config,
                                  gpointer run_data);

G_DEFINE_TYPE(Plugin, plugin, GIMP_TYPE_PLUG_IN)

static void plugin_class_init(PluginClass *klass) {
  GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS(klass);

  plug_in_class->query_procedures = plugin_query_procedures;
  plug_in_class->create_procedure = plugin_create_procedure;
}

static void plugin_init(Plugin *plugin) {}

static GList *plugin_query_procedures(GimpPlugIn *plug_in) {
  return g_list_append(NULL, g_strdup(PLUG_IN_PROC));
}

static GimpProcedure *plugin_create_procedure(GimpPlugIn *plug_in,
                                              const gchar *name) {
  GimpProcedure *procedure = NULL;

  if (g_strcmp0(name, PLUG_IN_PROC) == 0) {
    procedure = gimp_image_procedure_new(
        plug_in, name, GIMP_PDB_PROC_TYPE_PLUGIN, plugin_run, NULL, NULL);
    gimp_procedure_set_sensitivity_mask(procedure,
                                        GIMP_PROCEDURE_SENSITIVE_DRAWABLE);
    gimp_procedure_set_menu_label(procedure, "_Seam carving");
    gimp_procedure_add_menu_path(procedure, "<Image>/Filters/");
    gimp_procedure_set_documentation(procedure, "Intelligent image resizing",
                                     NULL, NULL);
    gimp_procedure_set_attribution(procedure, "Vlad Litvinov <vlad@sek1.ro>",
                                   "LGPL-3.0", "2025");
    gimp_procedure_add_int_argument(procedure, "blur-factor", "Blur factor",
                                    NULL, 0, 100, 3, G_PARAM_READWRITE);
    gimp_procedure_add_int_argument(procedure, "horizontal-resize",
                                    "Horizontal resize", NULL, 0, 100, 50,
                                    G_PARAM_READWRITE);
    gimp_procedure_add_boolean_argument(procedure, "show-path", "Show path",
                                        NULL, false, G_PARAM_READWRITE);
    gimp_procedure_add_boolean_argument(procedure, "show-energy", "Show energy",
                                        NULL, false, G_PARAM_READWRITE);
  }

  return procedure;
}

typedef struct {
  guint8 r, g, b, a;
} RGBA;

#define PATH_VOID -2
#define PATH_END -1
#define PATH_LEFT 0
#define PATH_BOTTOM 1
#define PATH_RIGHT 2

typedef struct {
  gint8 direction;
  guint64 energy;
} PathCell;

static void carve_seam(RGBA *energy, RGBA *output, PathCell *path, gint *width,
                       gint height, gint rowstride);
static void print_seam(RGBA *output, PathCell *path, gint width, gint height,
                       gint rowstride);
static void update_void_path(RGBA *energy, PathCell *path, gint width,
                             gint height, gint rowstride);
static guint64 update_path(RGBA *energy, PathCell *path, gint width,
                           gint height, gint rowstride);
static void print_state(RGBA *energy, PathCell *path, gint width, gint height,
                        gint rowstride) {
  for (gint row = 0; row < height; row++) {
    for (gint col = 0; col < width; col++) {
      printf("%4hhu ", energy[row * rowstride + col].r);
    }
    printf("\n");
  }

  printf("----\n");
  for (gint row = 0; row < height; row++) {
    for (gint col = 0; col < width; col++) {
      printf("%4lu ", path[row * rowstride + col].energy);
    }
    printf("\n");
  }

  printf("----\n");
  for (gint row = 0; row < height; row++) {
    for (gint col = 0; col < width; col++) {
      switch (path[row * rowstride + col].direction) {
      case PATH_LEFT:
        printf("/ ");
        break;

      case PATH_BOTTOM:
        printf("| ");
        break;

      case PATH_RIGHT:
        printf("\\ ");
        break;
      }
    }
    printf("\n");
  }
}
static GimpValueArray *plugin_run(GimpProcedure *procedure,
                                  GimpRunMode run_mode, GimpImage *image,
                                  GimpDrawable **drawables,
                                  GimpProcedureConfig *config,
                                  gpointer run_data) {

  gint blur_factor, horizontal_resize;
  gboolean show_path, show_energy;
  g_object_get(config, "blur-factor", &blur_factor, "horizontal-resize",
               &horizontal_resize, "show-path", &show_path, "show-energy",
               &show_energy, NULL);

  if (run_mode == GIMP_RUN_INTERACTIVE) {
    GtkWidget *dialog, *area, *label_bf, *label_hr, *scale_bf, *scale_hr,
        *toggle_sp, *toggle_se;

    gimp_ui_init(PLUG_IN_BINARY);

    dialog = gtk_dialog_new_with_buttons("Seam carving", NULL, GTK_DIALOG_MODAL,
                                         "_Ok", GTK_RESPONSE_OK, "_Cancel",
                                         GTK_RESPONSE_CANCEL, NULL);
    gtk_window_set_resizable(GTK_WINDOW(dialog), false);

    area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
    gtk_widget_set_margin_start(area, 12);
    gtk_widget_set_margin_end(area, 12);
    gtk_widget_set_margin_top(area, 12);

    scale_bf = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 0, 100, 1);
    scale_hr = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, 0, 100, 1);
    toggle_sp = gtk_toggle_button_new_with_label("Show path");
    toggle_se = gtk_toggle_button_new_with_label("Show energy");

    gtk_range_set_value(GTK_RANGE(scale_bf), 3);
    gtk_range_set_value(GTK_RANGE(scale_hr), 50);

    label_bf = gtk_label_new("Blur factor");
    label_hr = gtk_label_new("Horizontal resize");

    gtk_box_pack_end(GTK_BOX(area), toggle_se, FALSE, FALSE, 0);
    gtk_box_pack_end(GTK_BOX(area), toggle_sp, FALSE, FALSE, 0);
    gtk_box_pack_end(GTK_BOX(area), scale_hr, FALSE, FALSE, 0);
    gtk_box_pack_end(GTK_BOX(area), label_hr, FALSE, FALSE, 0);
    gtk_box_pack_end(GTK_BOX(area), scale_bf, FALSE, FALSE, 0);
    gtk_box_pack_end(GTK_BOX(area), label_bf, FALSE, FALSE, 0);

    gtk_widget_show_all(dialog);

    gint response = gtk_dialog_run(GTK_DIALOG(dialog));

    switch (response) {
    case GTK_RESPONSE_OK:
      blur_factor = gtk_range_get_value(GTK_RANGE(scale_bf));
      horizontal_resize = gtk_range_get_value(GTK_RANGE(scale_hr));
      show_path = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle_sp));
      show_energy = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle_se));
      break;

    default:
      gtk_widget_destroy(dialog);
      return gimp_procedure_new_return_values(procedure, GIMP_PDB_CANCEL, NULL);
    }

    gtk_widget_destroy(dialog);
  }

  const Babl *rgba_u8 = babl_format("RGBA u8");

  for (int i = 0; i < gimp_core_object_array_get_length((GObject **)drawables);
       i++) {
    gint width = gimp_drawable_get_width(drawables[i]),
         height = gimp_drawable_get_height(drawables[i]),
         target_width = width * horizontal_resize / 100, rowstride = width;
    GimpLayer *output_layer = gimp_layer_new_from_drawable(drawables[i], image);
    GimpLayer *energy_layer = gimp_layer_new_from_drawable(drawables[i], image);

    // https://gegl.org/operations/gegl-saturation.html
    // https://gegl.org/operations/gegl-gaussian-blur.html
    // https://gegl.org/operations/gegl-edge-sobel.html
    gimp_drawable_merge_new_filter(
        GIMP_DRAWABLE(energy_layer), "gegl:saturation", "Gray",
        GIMP_LAYER_MODE_REPLACE, 1.0, "scale", 0.0, NULL);
    gimp_drawable_merge_new_filter(
        GIMP_DRAWABLE(energy_layer), "gegl:gaussian-blur", "Blur",
        GIMP_LAYER_MODE_REPLACE, 1.0, "std-dev-x", blur_factor * 0.15,
        "std-dev-y", blur_factor * 0.15, NULL);
    gimp_drawable_merge_new_filter(GIMP_DRAWABLE(energy_layer),
                                   "gegl:edge-sobel", "Edge",
                                   GIMP_LAYER_MODE_REPLACE, 1.0, NULL);

    GeglBuffer *output_buffer =
        gimp_drawable_get_buffer(GIMP_DRAWABLE(output_layer));
    GeglBuffer *energy_buffer =
        gimp_drawable_get_buffer(GIMP_DRAWABLE(energy_layer));

    auto *output = (RGBA *)g_malloc(width * height * sizeof(RGBA));
    auto *energy = (RGBA *)g_malloc(width * height * sizeof(RGBA));
    auto *path = (PathCell *)g_malloc(width * height * sizeof(PathCell));
    GeglRectangle rect = {0, 0, width, height};
    gegl_buffer_get(output_buffer, &rect, 1.0, rgba_u8, output,
                    GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_BLACK);
    gegl_buffer_get(energy_buffer, &rect, 1.0, rgba_u8, energy,
                    GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_BLACK);

    guint64 max_energy = update_path(energy, path, width, height, rowstride);
    if (width < 40) {
      print_state(energy, path, width, height, rowstride);
    }

    if (show_energy) {
      for (gint row = 0; row < height; row++) {
        for (gint col = 0; col < width; col++) {
          guint8 e =
              (float)(path[row * rowstride + col].energy) / max_energy * 255;
          output[row * rowstride + col].r = e;
          output[row * rowstride + col].g = e;
          output[row * rowstride + col].b = e;
          output[row * rowstride + col].a = 255;
        }
      }
    } else {
      if (show_path) {
        for (gint i = 0; i < width - target_width; i++) {
          print_seam(output, path, width, height, rowstride);
          update_void_path(energy, path, width, height, rowstride);
          if (width < 40) {
            print_state(energy, path, width, height, rowstride);
          }
        }
      } else {
        while (width > target_width) {
          carve_seam(energy, output, path, &width, height, rowstride);
          update_path(energy, path, width, height, rowstride);
          if (width < 40) {
            print_state(energy, path, width, height, rowstride);
          }
        }
      }
    }

    gegl_buffer_set(output_buffer, &rect, 0, rgba_u8, output,
                    GEGL_AUTO_ROWSTRIDE);
    gegl_buffer_flush(output_buffer);
    gimp_image_insert_layer(
        image, output_layer, NULL,
        gimp_image_get_item_position(image, GIMP_ITEM(drawables[i])) + 1);

    if (!show_energy && !show_path) {
      gimp_layer_resize(output_layer, width, height, 0, 0);
    }

    if (GIMP_IS_LAYER(drawables[i])) {
      gimp_image_remove_layer(image, GIMP_LAYER(drawables[i]));
    } else if (GIMP_IS_CHANNEL(drawables[i])) {
      gimp_image_remove_channel(image, GIMP_CHANNEL(drawables[i]));
    }

    g_free(path);
    g_free(energy);
    g_object_unref(energy_layer);
  }

  babl_exit();

  return gimp_procedure_new_return_values(procedure, GIMP_PDB_SUCCESS, NULL);
}

static void update_void_path(RGBA *energy, PathCell *path, gint width,
                             gint height, gint rowstride) {
  auto threads_count = std::thread::hardware_concurrency();
  std::barrier sync_point(threads_count);
  auto worker = [&](int id) {
    for (gint row = height - 2; row >= 0; row--) {
      gint start = id * width / threads_count;
      gint end = (id + 1) * width / threads_count;

      for (gint col = start; col < end; col++) {
        PathCell *cell = &path[row * rowstride + col];
        if (cell->direction == PATH_VOID) {
          continue;
        }

        guint64 left = 0, bottom, right = 0;
        if (col > 0) {
          gint i = col - 1;
          for (;
               i >= 0 && path[(row + 1) * rowstride + i].direction == PATH_VOID;
               i--) {
          }
          left = path[(row + 1) * rowstride + i].energy;
        }
        {
          gint i = col;
          for (;
               i >= 0 && path[(row + 1) * rowstride + i].direction == PATH_VOID;
               i--) {
          }
          for (; i < width &&
                 path[(row + 1) * rowstride + i].direction == PATH_VOID;
               i++) {
          }
          bottom = path[(row + 1) * rowstride + col].energy;
        }
        if (col < width - 1) {
          gint i = col + 1;
          for (; i < width &&
                 path[(row + 1) * rowstride + i].direction == PATH_VOID;
               i++) {
          }
          right = path[(row + 1) * rowstride + i].energy;
        }

        cell->energy = energy[row * rowstride + col].r;

        if (col > 0 && left < bottom && left < right) {
          cell->direction = PATH_LEFT;
          cell->energy += left;
        } else if (col < (width - 1) && right < bottom && right < left) {
          cell->direction = PATH_RIGHT;
          cell->energy += right;
        } else {
          cell->direction = PATH_BOTTOM;
          cell->energy += bottom;
        }
      }

      sync_point.arrive_and_wait();
    }
  };

  std::vector<std::thread> threads;
  for (gint i = 0; i < threads_count; i++) {
    threads.emplace_back(worker, i);
  }

  for (auto &t : threads) {
    t.join();
  }
}

static guint64 update_path(RGBA *energy, PathCell *path, gint width,
                           gint height, gint rowstride) {
  std::atomic<guint64> max_energy = 0;

  for (gint col = 0; col < width; col++) {
    PathCell *cell = &path[(height - 1) * rowstride + col];
    cell->direction = PATH_END;
    cell->energy = energy[(height - 1) * rowstride + col].r;
    max_energy = MAX(max_energy.load(), cell->energy);
  }

  auto threads_count = std::thread::hardware_concurrency();
  std::barrier sync_point(threads_count);
  auto worker = [&](int id) {
    for (gint row = height - 2; row >= 0; row--) {
      gint start = id * width / threads_count;
      gint end = (id + 1) * width / threads_count;

      for (gint col = start; col < end; col++) {
        guint64 left = 0, bottom, right = 0;
        if (col > 0) {
          left = path[(row + 1) * rowstride + col - 1].energy;
        }
        bottom = path[(row + 1) * rowstride + col].energy;
        if (col < width - 1) {
          right = path[(row + 1) * rowstride + col + 1].energy;
        }

        PathCell *cell = &path[row * rowstride + col];

        cell->energy = energy[row * rowstride + col].r;

        if (col > 0 && left < bottom && left < right) {
          cell->direction = PATH_LEFT;
          cell->energy += left;
        } else if (col < (width - 1) && right < bottom && right < left) {
          cell->direction = PATH_RIGHT;
          cell->energy += right;
        } else {
          cell->direction = PATH_BOTTOM;
          cell->energy += bottom;
        }

        guint64 current_energy = max_energy.load();
        while (
            current_energy < cell->energy &&
            !max_energy.compare_exchange_weak(current_energy, cell->energy)) {
        }
      }

      sync_point.arrive_and_wait();
    }
  };

  std::vector<std::thread> threads;
  for (gint i = 0; i < threads_count; i++) {
    threads.emplace_back(worker, i);
  }

  for (auto &t : threads) {
    t.join();
  }

  return max_energy;
}

static gint find_min_col(PathCell *path, gint width) {
  gint min_col = 0;
  guint64 min_energy = UINT64_MAX;
  for (gint col = 0; col < width; col++) {
    if (path[col].direction != PATH_VOID && path[col].energy < min_energy) {
      min_energy = path[col].energy;
      min_col = col;
    }
  }
  return MIN(width - 2, min_col);
}

static void print_seam(RGBA *output, PathCell *path, gint width, gint height,
                       gint rowstride) {
  gint col = find_min_col(path, width), row = 0;
  for (; row < height - 1; row++) {
    RGBA *out = &output[row * rowstride + col];
    out->r = 255;
    out->g = 0;
    out->b = 255;
    out->a = 255;
    gint8 dir = path[row * rowstride + col].direction;
    path[row * rowstride + col].direction = PATH_VOID;

    switch (dir) {
    case PATH_LEFT:
      do {
        col--;
      } while (col > 0 &&
               path[(row + 1) * rowstride + col].direction == PATH_VOID);
      while (col < width - 1 &&
             path[(row + 1) * rowstride + col].direction == PATH_VOID) {
        col++;
      }
      break;

    case PATH_RIGHT:
      do {
        col++;
      } while (col < width - 1 &&
               path[(row + 1) * rowstride + col].direction == PATH_VOID);
      while (col > 0 &&
             path[(row + 1) * rowstride + col].direction == PATH_VOID) {
        col--;
      }
      break;
    case PATH_BOTTOM:
      while (col < width - 1 &&
             path[(row + 1) * rowstride + col].direction == PATH_VOID) {
        col++;
      }
      while (col > 0 &&
             path[(row + 1) * rowstride + col].direction == PATH_VOID) {
        col--;
      }
      break;
    }
  }

  path[row * rowstride + col].direction = PATH_VOID;
}

static void carve_seam(RGBA *energy, RGBA *output, PathCell *path, gint *width,
                       gint height, gint rowstride) {
  gint col = find_min_col(path, *width), row = 0;

  for (; row < height - 1; row++) {
    gint old_col = col;

    switch (path[row * rowstride + col].direction) {
    case PATH_LEFT:
      col = MAX(0, col - 1);
      break;
    case PATH_RIGHT:
      col = MIN(*width - 1, col + 1);
      break;
    }

    memmove(path + row * rowstride + old_col,
            path + row * rowstride + old_col + 1,
            (*width - old_col - 1) * sizeof(PathCell));
    memmove(output + row * rowstride + old_col,
            output + row * rowstride + old_col + 1,
            (*width - old_col - 1) * sizeof(RGBA));
    memmove(energy + row * rowstride + old_col,
            energy + row * rowstride + old_col + 1,
            (*width - old_col - 1) * sizeof(RGBA));
  }

  memmove(path + row * rowstride + col, path + row * rowstride + col + 1,
          (*width - col - 1) * sizeof(PathCell));
  memmove(output + row * rowstride + col, output + row * rowstride + col + 1,
          (*width - col - 1) * sizeof(RGBA));
  memmove(energy + row * rowstride + col, energy + row * rowstride + col + 1,
          (*width - col - 1) * sizeof(RGBA));

  (*width)--;
}

GIMP_MAIN(PLUGIN_TYPE)
