/* GIMP - The GNU Image Manipulation Program
 * Copyright (C) 1995 Spencer Kimball and Peter Mattis
 *
 * gimpbrush-mipmap.c
 * Copyright (C) 2020 Ell
 *
 * 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 <gdk-pixbuf/gdk-pixbuf.h>
#include <gegl.h>

#include "libgimpmath/gimpmath.h"

extern "C"
{

#include "core-types.h"

#include "gimpbrush.h"
#include "gimpbrush-mipmap.h"
#include "gimpbrush-private.h"
#include "gimptempbuf.h"

} /* extern "C" */


#define PIXELS_PER_THREAD \
  (/* each thread costs as much as */ 64.0 * 64.0 /* pixels */)

#define GIMP_BRUSH_MIPMAP(brush, mipmaps, x, y) \
  ((*(mipmaps))[(y) * (brush)->priv->n_horz_mipmaps + (x)])


/*  local function prototypes  */

static void                gimp_brush_mipmap_clear          (GimpBrush           *brush,
                                                             GimpTempBuf       ***mipmaps);

static const GimpTempBuf * gimp_brush_mipmap_get            (GimpBrush           *brush,
                                                             const GimpTempBuf   *source,
                                                             GimpTempBuf       ***mipmaps,
                                                             gdouble             *scale_x,
                                                             gdouble             *scale_y);

static GimpTempBuf       * gimp_brush_mipmap_downscale      (const GimpTempBuf   *source);
static GimpTempBuf       * gimp_brush_mipmap_downscale_horz (const GimpTempBuf   *source);
static GimpTempBuf       * gimp_brush_mipmap_downscale_vert (const GimpTempBuf   *source);


/*  private functions  */

static void
gimp_brush_mipmap_clear (GimpBrush     *brush,
                         GimpTempBuf ***mipmaps)
{
  if (*mipmaps)
    {
      gint i;

      for (i = 0;
           i < brush->priv->n_horz_mipmaps * brush->priv->n_vert_mipmaps;
           i++)
        {
          g_clear_pointer (&(*mipmaps)[i], gimp_temp_buf_unref);
        }

      g_clear_pointer (mipmaps, g_free);
    }
}

static const GimpTempBuf *
gimp_brush_mipmap_get (GimpBrush           *brush,
                       const GimpTempBuf   *source,
                       GimpTempBuf       ***mipmaps,
                       gdouble             *scale_x,
                       gdouble             *scale_y)
{
  gint x;
  gint y;
  gint i;

  if (! source)
    return NULL;

  if (! *mipmaps)
    {
      gint width  = gimp_temp_buf_get_width  (source);
      gint height = gimp_temp_buf_get_height (source);

      brush->priv->n_horz_mipmaps = floor (log (width)  / M_LN2) + 1;
      brush->priv->n_vert_mipmaps = floor (log (height) / M_LN2) + 1;

      *mipmaps = g_new0 (GimpTempBuf *, brush->priv->n_horz_mipmaps *
                                        brush->priv->n_vert_mipmaps);

      GIMP_BRUSH_MIPMAP (brush, mipmaps, 0, 0) = gimp_temp_buf_ref (source);
    }

  x = floor (SAFE_CLAMP (log (1.0 / MAX (*scale_x, 0.0)) / M_LN2,
                         0, brush->priv->n_horz_mipmaps - 1));
  y = floor (SAFE_CLAMP (log (1.0 / MAX (*scale_y, 0.0)) / M_LN2,
                         0, brush->priv->n_vert_mipmaps - 1));

  *scale_x *= pow (2.0, x);
  *scale_y *= pow (2.0, y);

  if (GIMP_BRUSH_MIPMAP (brush, mipmaps, x, y))
    return GIMP_BRUSH_MIPMAP (brush, mipmaps, x, y);

  g_return_val_if_fail (x >= 0 || y >= 0, NULL);

  for (i = 1; i <= x + y; i++)
    {
      gint u = x - i;
      gint v = y;

      if (u < 0)
        {
          v += u;
          u  = 0;
        }

      while (u <= x && v >= 0)
        {
          if (GIMP_BRUSH_MIPMAP (brush, mipmaps, u, v))
            {
              for (; x - u > y - v; u++)
                {
                  GIMP_BRUSH_MIPMAP (brush, mipmaps, u + 1, v) =
                    gimp_brush_mipmap_downscale_horz (
                      GIMP_BRUSH_MIPMAP (brush, mipmaps, u, v));
                }

              for (; y - v > x - u; v++)
                {
                  GIMP_BRUSH_MIPMAP (brush, mipmaps, u, v + 1) =
                    gimp_brush_mipmap_downscale_vert (
                      GIMP_BRUSH_MIPMAP (brush, mipmaps, u, v));
                }

              for (; u < x; u++, v++)
                {
                  GIMP_BRUSH_MIPMAP (brush, mipmaps, u + 1, v + 1) =
                    gimp_brush_mipmap_downscale (
                      GIMP_BRUSH_MIPMAP (brush, mipmaps, u, v));
                }

              return GIMP_BRUSH_MIPMAP (brush, mipmaps, u, v);
            }

          u++;
          v--;
        }
    }

  g_return_val_if_reached (NULL);
}

template <class T>
struct MipmapTraits;

template <>
struct MipmapTraits<guint8>
{
  static guint8
  mix (guint8 a,
       guint8 b)
  {
    return ((guint32) a + (guint32) b + 1) / 2;
  }

  static guint8
  mix (guint8 a,
       guint8 b,
       guint8 c,
       guint8 d)
  {
    return ((guint32) a + (guint32) b + (guint32) c + (guint32) d + 2) / 4;
  }
};

template <>
struct MipmapTraits<gfloat>
{
  static gfloat
  mix (gfloat a,
       gfloat b)
  {
    return (a + b) / 2.0;
  }

  static gfloat
  mix (gfloat a,
       gfloat b,
       gfloat c,
       gfloat d)
  {
    return (a + b + c + d) / 4.0;
  }
};

template <class T,
          gint  N>
struct MipmapAlgorithms
{
  static GimpTempBuf *
  downscale (const GimpTempBuf *source)
  {
    GimpTempBuf *destination;
    gint         width  = gimp_temp_buf_get_width  (source);
    gint         height = gimp_temp_buf_get_height (source);

    width  /= 2;
    height /= 2;

    destination = gimp_temp_buf_new (width, height,
                                     gimp_temp_buf_get_format (source));

    gegl_parallel_distribute_area (
      GEGL_RECTANGLE (0, 0, width, height), PIXELS_PER_THREAD,
      [=] (const GeglRectangle *area)
      {
        const T *src0        = (const T *) gimp_temp_buf_get_data (source);
        T       *dest0       = (T       *) gimp_temp_buf_get_data (destination);
        gint     src_stride  = N * gimp_temp_buf_get_width (source);
        gint     dest_stride = N * gimp_temp_buf_get_width (destination);
        gint     y;

        src0  += 2 * (area->y * src_stride  + N * area->x);
        dest0 +=      area->y * dest_stride + N * area->x;

        for (y = 0; y < area->height; y++)
          {
            const T *src  = src0;
            T       *dest = dest0;
            gint     x;

            for (x = 0; x < area->width; x++)
              {
                gint c;

                for (c = 0; c < N; c++)
                  {
                    dest[c] = MipmapTraits<T>::mix (src[c],
                                                    src[N + c],
                                                    src[src_stride + c],
                                                    src[src_stride + N + c]);
                  }

                src  += 2 * N;
                dest += N;
              }

            src0  += 2 * src_stride;
            dest0 += dest_stride;
          }
      });

    return destination;
  }

  static GimpTempBuf *
  downscale_horz (const GimpTempBuf *source)
  {
    GimpTempBuf *destination;
    gint         width  = gimp_temp_buf_get_width  (source);
    gint         height = gimp_temp_buf_get_height (source);

    width /= 2;

    destination = gimp_temp_buf_new (width, height,
                                     gimp_temp_buf_get_format (source));

    gegl_parallel_distribute_range (
      height, PIXELS_PER_THREAD / width,
      [=] (gint offset,
           gint size)
      {
        const T *src0        = (const T *) gimp_temp_buf_get_data (source);
        T       *dest0       = (T       *) gimp_temp_buf_get_data (destination);
        gint     src_stride  = N * gimp_temp_buf_get_width (source);
        gint     dest_stride = N * gimp_temp_buf_get_width (destination);
        gint     y;

        src0  += offset * src_stride;
        dest0 += offset * dest_stride;

        for (y = 0; y < size; y++)
          {
            const T *src  = src0;
            T       *dest = dest0;
            gint     x;

            for (x = 0; x < width; x++)
              {
                gint c;

                for (c = 0; c < N; c++)
                  dest[c] = MipmapTraits<T>::mix (src[c], src[N + c]);

                src  += 2 * N;
                dest += N;
              }

            src0  += src_stride;
            dest0 += dest_stride;
          }
      });

    return destination;
  }

  static GimpTempBuf *
  downscale_vert (const GimpTempBuf *source)
  {
    GimpTempBuf *destination;
    gint         width  = gimp_temp_buf_get_width  (source);
    gint         height = gimp_temp_buf_get_height (source);

    height /= 2;

    destination = gimp_temp_buf_new (width, height,
                                     gimp_temp_buf_get_format (source));

    gegl_parallel_distribute_range (
      width, PIXELS_PER_THREAD / height,
      [=] (gint offset,
           gint size)
      {
        const T *src0        = (const T *) gimp_temp_buf_get_data (source);
        T       *dest0       = (T       *) gimp_temp_buf_get_data (destination);
        gint     src_stride  = N * gimp_temp_buf_get_width (source);
        gint     dest_stride = N * gimp_temp_buf_get_width (destination);
        gint     x;

        src0  += offset * N;
        dest0 += offset * N;

        for (x = 0; x < size; x++)
          {
            const T *src  = src0;
            T       *dest = dest0;
            gint     y;

            for (y = 0; y < height; y++)
              {
                gint c;

                for (c = 0; c < N; c++)
                  dest[c] = MipmapTraits<T>::mix (src[c], src[src_stride + c]);

                src  += 2 * src_stride;
                dest += dest_stride;
              }

            src0  += N;
            dest0 += N;
          }
      });

    return destination;
  }
};

template <class Func>
static GimpTempBuf *
gimp_brush_mipmap_dispatch (const GimpTempBuf *source,
                            Func               func)
{
  const Babl *format = gimp_temp_buf_get_format (source);
  const Babl *type;
  gint        n_components;

  type         = babl_format_get_type (format, 0);
  n_components = babl_format_get_n_components (format);

  if (type == babl_type ("u8"))
    {
      switch (n_components)
        {
        case 1:
          return func (MipmapAlgorithms<guint8, 1> ());

        case 3:
          return func (MipmapAlgorithms<guint8, 3> ());
        }
    }
  else if (type == babl_type ("float"))
    {
      switch (n_components)
        {
        case 1:
          return func (MipmapAlgorithms<gfloat, 1> ());

        case 3:
          return func (MipmapAlgorithms<gfloat, 3> ());
        }
    }

  g_return_val_if_reached (NULL);
}

static GimpTempBuf *
gimp_brush_mipmap_downscale (const GimpTempBuf *source)
{
  return gimp_brush_mipmap_dispatch (
    source,
    [&] (auto algorithms)
    {
      return algorithms.downscale (source);
    });
}

static GimpTempBuf *
gimp_brush_mipmap_downscale_horz (const GimpTempBuf *source)
{
  return gimp_brush_mipmap_dispatch (
    source,
    [&] (auto algorithms)
    {
      return algorithms.downscale_horz (source);
    });
}

static GimpTempBuf *
gimp_brush_mipmap_downscale_vert (const GimpTempBuf *source)
{
  return gimp_brush_mipmap_dispatch (
    source,
    [&] (auto algorithms)
    {
      return algorithms.downscale_vert (source);
    });
}


/*  public functions  */

void
gimp_brush_mipmap_clear (GimpBrush *brush)
{
  gimp_brush_mipmap_clear (brush, &brush->priv->mask_mipmaps);
  gimp_brush_mipmap_clear (brush, &brush->priv->pixmap_mipmaps);
}

const GimpTempBuf *
gimp_brush_mipmap_get_mask (GimpBrush *brush,
                            gdouble   *scale_x,
                            gdouble   *scale_y)
{
  return gimp_brush_mipmap_get (brush,
                                brush->priv->mask,
                                &brush->priv->mask_mipmaps,
                                scale_x, scale_y);
}

const GimpTempBuf *
gimp_brush_mipmap_get_pixmap (GimpBrush *brush,
                              gdouble   *scale_x,
                              gdouble   *scale_y)
{
  return gimp_brush_mipmap_get (brush,
                                brush->priv->pixmap,
                                &brush->priv->pixmap_mipmaps,
                                scale_x, scale_y);
}

gsize
gimp_brush_mipmap_get_memsize (GimpBrush *brush)
{
  gsize memsize = 0;

  if (brush->priv->mask_mipmaps)
    {
      gint i;

      for (i = 1;
           i < brush->priv->n_horz_mipmaps * brush->priv->n_vert_mipmaps;
           i++)
        {
          memsize += gimp_temp_buf_get_memsize (brush->priv->mask_mipmaps[i]);
        }
    }

  if (brush->priv->pixmap_mipmaps)
    {
      gint i;

      for (i = 1;
           i < brush->priv->n_horz_mipmaps * brush->priv->n_vert_mipmaps;
           i++)
        {
          memsize += gimp_temp_buf_get_memsize (brush->priv->pixmap_mipmaps[i]);
        }
    }

  return memsize;
}
