/* 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
 *
 * 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 <cairo.h>
#include <gegl.h>
#include <gdk-pixbuf/gdk-pixbuf.h>

#include "libpikacolor/pikacolor.h"
#include "libpikamath/pikamath.h"
#include "libpikabase/pikabase.h"

#include "paint-types.h"

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

#include "core/pika.h"
#include "core/pika-palettes.h"
#include "core/pikabrush.h"
#include "core/pikadrawable.h"
#include "core/pikadynamics.h"
#include "core/pikagradient.h"
#include "core/pikaimage.h"
#include "core/pikapickable.h"
#include "core/pikasymmetry.h"
#include "core/pikatempbuf.h"

#include "pikapaintbrush.h"
#include "pikapaintoptions.h"

#include "pika-intl.h"


static void       pika_paintbrush_paint                        (PikaPaintCore             *paint_core,
                                                                GList                     *drawables,
                                                                PikaPaintOptions          *paint_options,
                                                                PikaSymmetry              *sym,
                                                                PikaPaintState             paint_state,
                                                                guint32                    time);

static gboolean   pika_paintbrush_real_get_color_history_color (PikaPaintbrush            *paintbrush,
                                                                PikaDrawable              *drawable,
                                                                PikaPaintOptions          *paint_options,
                                                                PikaRGB                   *color);
static void       pika_paintbrush_real_get_paint_params        (PikaPaintbrush            *paintbrush,
                                                                PikaDrawable              *drawable,
                                                                PikaPaintOptions          *paint_options,
                                                                PikaSymmetry              *sym,
                                                                gdouble                    grad_point,
                                                                PikaLayerMode             *paint_mode,
                                                                PikaPaintApplicationMode  *paint_appl_mode,
                                                                const PikaTempBuf        **paint_pixmap,
                                                                PikaRGB                   *paint_color);


G_DEFINE_TYPE (PikaPaintbrush, pika_paintbrush, PIKA_TYPE_BRUSH_CORE)


void
pika_paintbrush_register (Pika                      *pika,
                          PikaPaintRegisterCallback  callback)
{
  (* callback) (pika,
                PIKA_TYPE_PAINTBRUSH,
                PIKA_TYPE_PAINT_OPTIONS,
                "pika-paintbrush",
                _("Paintbrush"),
                "pika-tool-paintbrush");
}

static void
pika_paintbrush_class_init (PikaPaintbrushClass *klass)
{
  PikaPaintCoreClass *paint_core_class = PIKA_PAINT_CORE_CLASS (klass);
  PikaBrushCoreClass *brush_core_class = PIKA_BRUSH_CORE_CLASS (klass);

  paint_core_class->paint                  = pika_paintbrush_paint;

  brush_core_class->handles_changing_brush = TRUE;

  klass->get_color_history_color           = pika_paintbrush_real_get_color_history_color;
  klass->get_paint_params                  = pika_paintbrush_real_get_paint_params;
}

static void
pika_paintbrush_init (PikaPaintbrush *paintbrush)
{
}

static void
pika_paintbrush_paint (PikaPaintCore    *paint_core,
                       GList            *drawables,
                       PikaPaintOptions *paint_options,
                       PikaSymmetry     *sym,
                       PikaPaintState    paint_state,
                       guint32           time)
{
  PikaPaintbrush *paintbrush = PIKA_PAINTBRUSH (paint_core);

  g_return_if_fail (g_list_length (drawables) == 1);

  switch (paint_state)
    {
    case PIKA_PAINT_STATE_INIT:
      {
        PikaRGB color;

        for (GList *iter = drawables; iter; iter = iter->next)
          if (PIKA_PAINTBRUSH_GET_CLASS (paintbrush)->get_color_history_color &&
              PIKA_PAINTBRUSH_GET_CLASS (paintbrush)->get_color_history_color (paintbrush,
                                                                               iter->data,
                                                                               paint_options,
                                                                               &color))
            {
              PikaContext *context = PIKA_CONTEXT (paint_options);

              pika_palettes_add_color_history (context->pika, &color);
            }
      }
      break;

    case PIKA_PAINT_STATE_MOTION:
      for (GList *iter = drawables; iter; iter = iter->next)
        _pika_paintbrush_motion (paint_core, iter->data, paint_options,
                                 sym, PIKA_OPACITY_OPAQUE);
      break;

    case PIKA_PAINT_STATE_FINISH:
      g_clear_weak_pointer (&paintbrush->paint_buffer);
      g_clear_pointer (&paintbrush->paint_pixmap, pika_temp_buf_unref);
      break;
    }
}

static gboolean
pika_paintbrush_real_get_color_history_color (PikaPaintbrush   *paintbrush,
                                              PikaDrawable     *drawable,
                                              PikaPaintOptions *paint_options,
                                              PikaRGB          *color)
{
  PikaContext   *context    = PIKA_CONTEXT (paint_options);
  PikaBrushCore *brush_core = PIKA_BRUSH_CORE (paintbrush);
  PikaDynamics  *dynamics   = pika_context_get_dynamics (context);

  /* We don't save gradient color history and pixmap brushes
   * have no color to save.
   */
  if (pika_dynamics_is_output_enabled (dynamics, PIKA_DYNAMICS_OUTPUT_COLOR) ||
      (brush_core->brush && pika_brush_get_pixmap (brush_core->brush)))
    {
      return FALSE;
    }

  pika_context_get_foreground (context, color);

  return TRUE;
}

static void
pika_paintbrush_real_get_paint_params (PikaPaintbrush            *paintbrush,
                                       PikaDrawable              *drawable,
                                       PikaPaintOptions          *paint_options,
                                       PikaSymmetry              *sym,
                                       gdouble                    grad_point,
                                       PikaLayerMode             *paint_mode,
                                       PikaPaintApplicationMode  *paint_appl_mode,
                                       const PikaTempBuf        **paint_pixmap,
                                       PikaRGB                   *paint_color)
{
  PikaPaintCore *paint_core = PIKA_PAINT_CORE (paintbrush);
  PikaBrushCore *brush_core = PIKA_BRUSH_CORE (paintbrush);
  PikaContext   *context    = PIKA_CONTEXT (paint_options);
  PikaImage     *image      = pika_item_get_image (PIKA_ITEM (drawable));

  *paint_mode = pika_context_get_paint_mode (context);

  if (pika_paint_options_get_gradient_color (paint_options, image,
                                             grad_point,
                                             paint_core->pixel_dist,
                                             paint_color))
    {
      /* optionally take the color from the current gradient */
      pika_pickable_srgb_to_image_color (PIKA_PICKABLE (drawable),
                                         paint_color, paint_color);

      *paint_appl_mode = PIKA_PAINT_INCREMENTAL;
    }
  else if (brush_core->brush && pika_brush_get_pixmap (brush_core->brush))
    {
      /* otherwise check if the brush has a pixmap and use that to
       * color the area
       */
      *paint_pixmap = pika_brush_core_get_brush_pixmap (brush_core);

      *paint_appl_mode = PIKA_PAINT_INCREMENTAL;
    }
  else
    {
      /* otherwise fill the area with the foreground color */
      pika_context_get_foreground (context, paint_color);

      pika_pickable_srgb_to_image_color (PIKA_PICKABLE (drawable),
                                         paint_color, paint_color);
    }
}

void
_pika_paintbrush_motion (PikaPaintCore    *paint_core,
                         PikaDrawable     *drawable,
                         PikaPaintOptions *paint_options,
                         PikaSymmetry     *sym,
                         gdouble           opacity)
{
  PikaBrushCore    *brush_core = PIKA_BRUSH_CORE (paint_core);
  PikaPaintbrush   *paintbrush = PIKA_PAINTBRUSH (paint_core);
  PikaContext      *context    = PIKA_CONTEXT (paint_options);
  PikaDynamics     *dynamics   = brush_core->dynamics;
  PikaImage        *image      = pika_item_get_image (PIKA_ITEM (drawable));
  gdouble           fade_point;
  gdouble           grad_point;
  gdouble           force;
  PikaCoords        coords;
  gint              n_strokes;
  gint              off_x, off_y;
  gint              i;

  fade_point = pika_paint_options_get_fade (paint_options, image,
                                            paint_core->pixel_dist);

  pika_item_get_offset (PIKA_ITEM (drawable), &off_x, &off_y);

  coords    = *(pika_symmetry_get_origin (sym));
  coords.x -= off_x;
  coords.y -= off_y;
  pika_symmetry_set_origin (sym, drawable, &coords);

  /* Some settings are based on the original stroke. */
  opacity *= pika_dynamics_get_linear_value (dynamics,
                                             PIKA_DYNAMICS_OUTPUT_OPACITY,
                                             &coords,
                                             paint_options,
                                             fade_point);
  if (opacity == 0.0)
    return;

  if (PIKA_BRUSH_CORE_GET_CLASS (brush_core)->handles_transforming_brush)
    {
      pika_brush_core_eval_transform_dynamics (brush_core,
                                               image,
                                               paint_options,
                                               &coords);
    }

  grad_point = pika_dynamics_get_linear_value (dynamics,
                                               PIKA_DYNAMICS_OUTPUT_COLOR,
                                               &coords,
                                               paint_options,
                                               fade_point);

  n_strokes = pika_symmetry_get_size (sym);
  for (i = 0; i < n_strokes; i++)
    {
      PikaLayerMode             paint_mode;
      PikaPaintApplicationMode  paint_appl_mode;
      GeglBuffer               *paint_buffer;
      gint                      paint_buffer_x;
      gint                      paint_buffer_y;
      const PikaTempBuf        *paint_pixmap = NULL;
      PikaRGB                   paint_color;
      gint                      paint_width, paint_height;

      paint_appl_mode = paint_options->application_mode;

      PIKA_PAINTBRUSH_GET_CLASS (paintbrush)->get_paint_params (paintbrush,
                                                                drawable,
                                                                paint_options,
                                                                sym,
                                                                grad_point,
                                                                &paint_mode,
                                                                &paint_appl_mode,
                                                                &paint_pixmap,
                                                                &paint_color);

      coords    = *(pika_symmetry_get_coords (sym, i));

      if (PIKA_BRUSH_CORE_GET_CLASS (brush_core)->handles_transforming_brush)
        pika_brush_core_eval_transform_symmetry (brush_core, sym, i);

      paint_buffer = pika_paint_core_get_paint_buffer (paint_core, drawable,
                                                       paint_options,
                                                       paint_mode,
                                                       &coords,
                                                       &paint_buffer_x,
                                                       &paint_buffer_y,
                                                       &paint_width,
                                                       &paint_height);
      if (! paint_buffer)
        continue;

      if (! paint_pixmap)
        {
          opacity *= paint_color.a;
          pika_rgb_set_alpha (&paint_color, PIKA_OPACITY_OPAQUE);
        }

      /* fill the paint buffer.  we can skip this step when reusing the
       * previous paint buffer, if the paint color/pixmap hasn't changed
       * (unless using an applicator, which currently modifies the paint buffer
       * in-place).
       */
      if (paint_core->applicators                  ||
          paint_buffer != paintbrush->paint_buffer ||
          paint_pixmap != paintbrush->paint_pixmap ||
          (! paint_pixmap && (pika_rgba_distance (&paint_color,
                                                  &paintbrush->paint_color))))
        {
          g_set_weak_pointer (&paintbrush->paint_buffer, paint_buffer);

          if (paint_pixmap != paintbrush->paint_pixmap)
            {
              g_clear_pointer (&paintbrush->paint_pixmap, pika_temp_buf_unref);

              if (paint_pixmap)
                paintbrush->paint_pixmap = pika_temp_buf_ref (paint_pixmap);
            }

          paintbrush->paint_color = paint_color;

          if (paint_pixmap)
            {
              pika_brush_core_color_area_with_pixmap (brush_core, drawable,
                                                      &coords,
                                                      paint_buffer,
                                                      paint_buffer_x,
                                                      paint_buffer_y,
                                                      FALSE);
            }
          else
            {
              GeglColor *color;

              color = pika_gegl_color_new (&paint_color,
                                           pika_drawable_get_space (drawable));

              gegl_buffer_set_color (paint_buffer, NULL, color);

              g_object_unref (color);
            }
        }

      if (pika_dynamics_is_output_enabled (dynamics, PIKA_DYNAMICS_OUTPUT_FORCE))
        force = pika_dynamics_get_linear_value (dynamics,
                                                PIKA_DYNAMICS_OUTPUT_FORCE,
                                                &coords,
                                                paint_options,
                                                fade_point);
      else
        force = paint_options->brush_force;

      /* finally, let the brush core paste the colored area on the canvas */
      pika_brush_core_paste_canvas (brush_core, drawable,
                                    &coords,
                                    MIN (opacity, PIKA_OPACITY_OPAQUE),
                                    pika_context_get_opacity (context),
                                    paint_mode,
                                    pika_paint_options_get_brush_mode (paint_options),
                                    force,
                                    paint_appl_mode);
    }
}
