/* SPDX-FileCopyrightText: 2020 Blender Authors
 *
 * SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup edgpencil
 */

#include <cstdio>
#include <cstdlib>
#include <cstring>

#include "MEM_guardedalloc.h"

#include "BLI_blenlib.h"
#include "BLI_math_vector.h"

#include "BKE_gpencil_geom_legacy.h"
#include "BKE_gpencil_legacy.h"

#include "DNA_gpencil_legacy_types.h"
#include "DNA_image_types.h"
#include "DNA_material_types.h"
#include "DNA_object_types.h"

#include "IMB_imbuf_types.hh"

#include "gpencil_trace.h"

void ED_gpencil_trace_bitmap_print(FILE *f, const potrace_bitmap_t *bm)
{
  int32_t x, y;
  int32_t xx, yy;
  int32_t d;
  int32_t sw, sh;

  sw = bm->w < 79 ? bm->w : 79;
  sh = bm->w < 79 ? bm->h : bm->h * sw * 44 / (79 * bm->w);

  for (yy = sh - 1; yy >= 0; yy--) {
    for (xx = 0; xx < sw; xx++) {
      d = 0;
      for (x = xx * bm->w / sw; x < (xx + 1) * bm->w / sw; x++) {
        for (y = yy * bm->h / sh; y < (yy + 1) * bm->h / sh; y++) {
          if (BM_GET(bm, x, y)) {
            d++;
          }
        }
      }
      fputc(d ? '*' : ' ', f);
    }
    fputc('\n', f);
  }
}

potrace_bitmap_t *ED_gpencil_trace_bitmap_new(int32_t w, int32_t h)
{
  potrace_bitmap_t *bm;
  int32_t dy = (w + BM_WORDBITS - 1) / BM_WORDBITS;

  bm = (potrace_bitmap_t *)MEM_mallocN(sizeof(potrace_bitmap_t), __func__);
  if (!bm) {
    return nullptr;
  }
  bm->w = w;
  bm->h = h;
  bm->dy = dy;
  bm->map = (potrace_word *)calloc(h, dy * BM_WORDSIZE);
  if (!bm->map) {
    free(bm);
    return nullptr;
  }

  return bm;
}

void ED_gpencil_trace_bitmap_free(const potrace_bitmap_t *bm)
{
  if (bm != nullptr) {
    free(bm->map);
  }
  MEM_SAFE_FREE(bm);
}

void ED_gpencil_trace_bitmap_invert(const potrace_bitmap_t *bm)
{
  int32_t dy = bm->dy;
  int32_t y;
  int32_t i;
  potrace_word *p;

  if (dy < 0) {
    dy = -dy;
  }

  for (y = 0; y < bm->h; y++) {
    p = bm_scanline(bm, y);
    for (i = 0; i < dy; i++) {
      p[i] ^= BM_ALLBITS;
    }
  }
}

/**
 * Return pixel data (rgba) at index
 * \param ibuf: ImBuf of the image
 * \param idx: Index of the pixel
 * \return RGBA value
 */
static void pixel_at_index(const ImBuf *ibuf, const int32_t idx, float r_col[4])
{
  BLI_assert(idx < (ibuf->x * ibuf->y));

  if (ibuf->float_buffer.data) {
    const float *frgba = &ibuf->float_buffer.data[idx * 4];
    copy_v4_v4(r_col, frgba);
  }
  else {
    uchar *cp = ibuf->byte_buffer.data + 4 * idx;
    r_col[0] = float(cp[0]) / 255.0f;
    r_col[1] = float(cp[1]) / 255.0f;
    r_col[2] = float(cp[2]) / 255.0f;
    r_col[3] = float(cp[3]) / 255.0f;
  }
}

void ED_gpencil_trace_image_to_bitmap(ImBuf *ibuf,
                                      const potrace_bitmap_t *bm,
                                      const float threshold)
{
  float rgba[4];
  int32_t pixel = 0;

  for (uint32_t y = 0; y < ibuf->y; y++) {
    for (uint32_t x = 0; x < ibuf->x; x++) {
      pixel = (ibuf->x * y) + x;
      pixel_at_index(ibuf, pixel, rgba);
      /* Get a BW color. */
      mul_v3_fl(rgba, rgba[3]);
      float color = (rgba[0] + rgba[1] + rgba[2]) / 3.0f;
      int32_t bw = (color > threshold) ? 0 : 1;
      BM_PUT(bm, x, y, bw);
    }
  }
}

/* Helper to add point to stroke. */
static void add_point(bGPDstroke *gps, float scale, const int32_t offset[2], float x, float y)
{
  int32_t idx = gps->totpoints;
  if (gps->totpoints == 0) {
    gps->points = static_cast<bGPDspoint *>(MEM_callocN(sizeof(bGPDspoint), "gp_stroke_points"));
  }
  else {
    gps->points = static_cast<bGPDspoint *>(
        MEM_recallocN(gps->points, sizeof(bGPDspoint) * (gps->totpoints + 1)));
  }
  bGPDspoint *pt = &gps->points[idx];
  pt->x = (x - offset[0]) * scale;
  pt->y = 0;
  pt->z = (y - offset[1]) * scale;
  pt->pressure = 1.0f;
  pt->strength = 1.0f;

  gps->totpoints++;
}

/* helper to generate all points of curve. */
static void add_bezier(bGPDstroke *gps,
                       float scale,
                       int32_t offset[2],
                       int32_t resolution,
                       float bcp1[2],
                       float bcp2[2],
                       float bcp3[2],
                       float bcp4[2],
                       const bool skip)
{
  const float step = 1.0f / float(resolution - 1);
  float a = 0.0f;

  for (int32_t i = 0; i < resolution; i++) {
    if ((!skip) || (i > 0)) {
      float fpt[3];
      interp_v2_v2v2v2v2_cubic(fpt, bcp1, bcp2, bcp3, bcp4, a);
      add_point(gps, scale, offset, fpt[0], fpt[1]);
    }
    a += step;
  }
}

void ED_gpencil_trace_data_to_strokes(Main *bmain,
                                      potrace_state_t *st,
                                      Object *ob,
                                      bGPDframe *gpf,
                                      int32_t offset[2],
                                      const float scale,
                                      const float sample,
                                      const int32_t resolution,
                                      const int32_t thickness)
{
#define MAX_LENGTH 100.0f
  /* Find materials and create them if not found. */
  int32_t mat_fill_idx = BKE_gpencil_material_find_index_by_name_prefix(ob, "Stroke");
  int32_t mat_mask_idx = BKE_gpencil_material_find_index_by_name_prefix(ob, "Holdout");

  const float default_color[4] = {0.0f, 0.0f, 0.0f, 1.0f};
  /* Stroke and Fill material. */
  if (mat_fill_idx == -1) {
    int32_t new_idx;
    Material *mat_gp = BKE_gpencil_object_material_new(bmain, ob, "Stroke", &new_idx);
    MaterialGPencilStyle *gp_style = mat_gp->gp_style;

    copy_v4_v4(gp_style->stroke_rgba, default_color);
    gp_style->flag |= GP_MATERIAL_STROKE_SHOW;
    gp_style->flag |= GP_MATERIAL_FILL_SHOW;
    mat_fill_idx = ob->totcol - 1;
  }
  /* Holdout material. */
  if (mat_mask_idx == -1) {
    int32_t new_idx;
    Material *mat_gp = BKE_gpencil_object_material_new(bmain, ob, "Holdout", &new_idx);
    MaterialGPencilStyle *gp_style = mat_gp->gp_style;

    copy_v4_v4(gp_style->stroke_rgba, default_color);
    copy_v4_v4(gp_style->fill_rgba, default_color);
    gp_style->flag |= GP_MATERIAL_STROKE_SHOW;
    gp_style->flag |= GP_MATERIAL_FILL_SHOW;
    gp_style->flag |= GP_MATERIAL_IS_STROKE_HOLDOUT;
    gp_style->flag |= GP_MATERIAL_IS_FILL_HOLDOUT;
    mat_mask_idx = ob->totcol - 1;
  }

  int n, *tag;
  potrace_dpoint_t(*c)[3];

  /* There isn't any rule here, only the result of lots of testing to get a value that gets
   * good results using the Potrace data. */
  const float scalef = 0.008f * scale;
  /* Draw each curve. */
  potrace_path_t *path = st->plist;
  while (path != nullptr) {
    n = path->curve.n;
    tag = path->curve.tag;
    c = path->curve.c;
    int mat_idx = path->sign == '+' ? mat_fill_idx : mat_mask_idx;
    /* Create a new stroke. */
    bGPDstroke *gps = BKE_gpencil_stroke_add(gpf, mat_idx, 0, thickness, false);
    /* Last point that is equals to start point. */
    float start_point[2], last[2];
    start_point[0] = c[n - 1][2].x;
    start_point[1] = c[n - 1][2].y;
    zero_v2(last);

    for (int32_t i = 0; i < n; i++) {
      switch (tag[i]) {
        case POTRACE_CORNER: {
          if (gps->totpoints == 0) {
            add_point(gps, scalef, offset, c[n - 1][2].x, c[n - 1][2].y);
          }
          else {
            add_point(gps, scalef, offset, last[0], last[1]);
          }

          add_point(gps, scalef, offset, c[i][1].x, c[i][1].y);

          add_point(gps, scalef, offset, c[i][2].x, c[i][2].y);

          last[0] = c[i][2].x;
          last[1] = c[i][2].y;
          break;
        }
        case POTRACE_CURVETO: {
          float cp1[2], cp2[2], cp3[2], cp4[2];
          if (gps->totpoints == 0) {
            cp1[0] = start_point[0];
            cp1[1] = start_point[1];
          }
          else {
            copy_v2_v2(cp1, last);
          }

          cp2[0] = c[i][0].x;
          cp2[1] = c[i][0].y;

          cp3[0] = c[i][1].x;
          cp3[1] = c[i][1].y;

          cp4[0] = c[i][2].x;
          cp4[1] = c[i][2].y;

          add_bezier(gps,
                     scalef,
                     offset,
                     resolution,
                     cp1,
                     cp2,
                     cp3,
                     cp4,
                     (gps->totpoints == 0) ? false : true);
          copy_v2_v2(last, cp4);
          break;
        }
        default:
          break;
      }
    }
    /* In some situations, Potrace can produce a wrong data and generate a very
     * long stroke. Here the length is checked and removed if the length is too big. */
    float length = BKE_gpencil_stroke_length(gps, true);
    if (length <= MAX_LENGTH) {
      bGPdata *gpd = static_cast<bGPdata *>(ob->data);
      if (sample > 0.0f) {
        /* Resample stroke. Don't need to call to BKE_gpencil_stroke_geometry_update() because
         * the sample function already call that. */
        BKE_gpencil_stroke_sample(gpd, gps, sample, false, 0);
      }
      else {
        BKE_gpencil_stroke_geometry_update(gpd, gps);
      }
    }
    else {
      /* Remove too long strokes. */
      BLI_remlink(&gpf->strokes, gps);
      BKE_gpencil_free_stroke(gps);
    }

    path = path->next;
  }
#undef MAX_LENGTH
}
