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

/** \file
 * \ingroup bke
 *
 * Handle curve object data bevel options,
 * both extruding
 */

#include <cstring>

#include "BLI_listbase.h"
#include "BLI_math_base.h" /* Needed with MSVC for M_PI & M_PI_2. */

#include "MEM_guardedalloc.h"

#include "DNA_curve_types.h"
#include "DNA_curveprofile_types.h"
#include "DNA_object_types.h"

#include "BKE_curve.hh"
#include "BKE_displist.h"
#include "BKE_object_types.hh"

enum CurveBevelFillType {
  BACK = 0,
  FRONT,
  HALF,
  FULL,
};

static CurveBevelFillType curve_bevel_get_fill_type(const Curve *curve)
{
  if (!(curve->flag & (CU_FRONT | CU_BACK))) {
    return FULL;
  }
  if ((curve->flag & CU_FRONT) && (curve->flag & CU_BACK)) {
    return HALF;
  }

  return (curve->flag & CU_FRONT) ? FRONT : BACK;
}

static void bevel_quarter_fill(const Curve *curve,
                               float *quarter_coords_x,
                               float *quarter_coords_y)
{
  if (curve->bevel_mode == CU_BEV_MODE_ROUND) {
    float angle = 0.0f;
    const float dangle = float(M_PI_2) / (curve->bevresol + 1);
    for (int i = 0; i < curve->bevresol + 1; i++) {
      quarter_coords_x[i] = float(cosf(angle) * (curve->bevel_radius));
      quarter_coords_y[i] = float(sinf(angle) * (curve->bevel_radius));
      angle += dangle;
    }
  }
  else {
    /* The curve profile evaluation should be done when the resolution is set. */
    BLI_assert(curve->bevel_profile->segments != nullptr);
    BLI_assert(curve->bevel_profile->segments_len == curve->bevresol + 1);

    /* If there aren't enough samples, the curveprofile won't
     * sample the start vertex, so set it manually instead. */
    quarter_coords_x[0] = curve->bevel_radius;
    quarter_coords_y[0] = 0.0f;
    for (int i = 1; i < curve->bevresol + 1; i++) {
      quarter_coords_x[i] = float(curve->bevel_profile->segments[i].x * (curve->bevel_radius));
      quarter_coords_y[i] = float(curve->bevel_profile->segments[i].y * (curve->bevel_radius));
    }
  }
}

static void curve_bevel_make_extrude_and_fill(const Curve *cu,
                                              ListBase *disp,
                                              const bool use_extrude,
                                              const CurveBevelFillType fill_type)
{
  DispList *dl = MEM_callocN<DispList>(__func__);

  /* Calculate the profile of the bevel once to reuse it for each quarter. We will need
   * to flip around the indices for every other section in order to build around the circle
   * in a consistent direction.
   *
   * These should be small enough for stack allocations because the current limit
   * for #Curve.bevresol is 32. */
  float *quarter_coords_x = static_cast<float *>(alloca(sizeof(float) * (cu->bevresol + 1)));
  float *quarter_coords_y = static_cast<float *>(alloca(sizeof(float) * (cu->bevresol + 1)));
  bevel_quarter_fill(cu, quarter_coords_x, quarter_coords_y);

  int nr;
  if (fill_type == FULL) {
    /* The full loop. */
    nr = 4 * cu->bevresol + (use_extrude ? 6 : 4);
    dl->flag = DL_FRONT_CURVE | DL_BACK_CURVE;
  }
  else if (fill_type == HALF) {
    /* Half the loop. */
    nr = 2 * (cu->bevresol + 1) + (use_extrude ? 2 : 1);
    dl->flag = DL_FRONT_CURVE | DL_BACK_CURVE;
  }
  else {
    /* One quarter of the loop (just front or back). */
    nr = use_extrude ? cu->bevresol + 3 : cu->bevresol + 2;
    dl->flag = (fill_type == FRONT) ? DL_FRONT_CURVE : DL_BACK_CURVE;
  }

  dl->verts = MEM_malloc_arrayN<float>(3 * size_t(nr), __func__);
  BLI_addtail(disp, dl);
  /* Use a different type depending on whether the loop is complete or not. */
  dl->type = (fill_type == FULL) ? DL_POLY : DL_SEGM;
  dl->parts = 1;
  dl->nr = nr;

  float *fp = dl->verts;

  /* Build the back section. */
  if (ELEM(fill_type, BACK, HALF, FULL)) {
    /* Add the bottom vertex. */
    fp[0] = 0.0f;
    fp[1] = 0.0f;
    fp[2] = -cu->extrude - cu->bevel_radius;
    fp += 3;

    for (int i = cu->bevresol; i >= 0; i--) {
      fp[0] = 0.0f;
      fp[1] = quarter_coords_x[i];
      fp[2] = -quarter_coords_y[i] - cu->extrude;
      fp += 3;
    }
  }

  /* Add the extrusion if we're only building either the back or the front. */
  if (use_extrude && ELEM(fill_type, FRONT, BACK)) {
    fp[0] = 0.0f;
    fp[1] = cu->bevel_radius;
    fp[2] = (fill_type == FRONT) ? -cu->extrude : cu->extrude;
    fp += 3;
  }

  /* Build the front section. */
  if (ELEM(fill_type, FRONT, HALF, FULL)) {
    /* Don't duplicate the last back vertex. */
    const int front_start = (!use_extrude && ELEM(fill_type, HALF, FULL)) ? 1 : 0;
    for (int i = front_start; i < cu->bevresol + 1; i++) {
      fp[0] = 0.0f;
      fp[1] = quarter_coords_x[i];
      fp[2] = quarter_coords_y[i] + cu->extrude;
      fp += 3;
    }
    /* Add the top vertex. */
    fp[0] = 0.0f;
    fp[1] = 0.0f;
    fp[2] = cu->extrude + cu->bevel_radius;
    fp += 3;
  }

  /* Build the other half only if we're building the full loop. */
  if (fill_type == FULL) {
    for (int i = cu->bevresol; i > 0; i--) {
      fp[0] = 0.0f;
      fp[1] = -quarter_coords_x[i];
      fp[2] = quarter_coords_y[i] + cu->extrude;
      fp += 3;
    }

    if (use_extrude) {
      /* Add the extrusion. */
      fp[0] = 0.0f;
      fp[1] = -cu->bevel_radius;
      fp[2] = cu->extrude;
      fp += 3;
    }

    for (int i = 0; i < cu->bevresol + 1; i++) {
      fp[0] = 0.0f;
      fp[1] = -quarter_coords_x[i];
      fp[2] = -quarter_coords_y[i] - cu->extrude;
      fp += 3;
    }
  }
}

static void curve_bevel_make_full_circle(const Curve *cu, ListBase *disp)
{
  const int nr = 4 + 2 * cu->bevresol;

  DispList *dl = MEM_callocN<DispList>(__func__);
  dl->verts = MEM_malloc_arrayN<float>(3 * size_t(nr), __func__);
  BLI_addtail(disp, dl);
  dl->type = DL_POLY;
  dl->parts = 1;
  dl->flag = DL_BACK_CURVE;
  dl->nr = nr;

  float *fp = dl->verts;
  const float dangle = (2.0f * float(M_PI) / (nr));
  float angle = -(nr - 1) * dangle;

  for (int i = 0; i < nr; i++) {
    fp[0] = 0.0;
    fp[1] = (cosf(angle) * (cu->bevel_radius));
    fp[2] = (sinf(angle) * (cu->bevel_radius)) - cu->extrude;
    angle += dangle;
    fp += 3;
  }
}

static void curve_bevel_make_only_extrude(const Curve *cu, ListBase *disp)
{
  DispList *dl = MEM_callocN<DispList>(__func__);
  dl->verts = MEM_malloc_arrayN<float>(3 * 2, __func__);
  BLI_addtail(disp, dl);
  dl->type = DL_SEGM;
  dl->parts = 1;
  dl->flag = DL_FRONT_CURVE | DL_BACK_CURVE;
  dl->nr = 2;

  float *fp = dl->verts;
  fp[0] = fp[1] = 0.0;
  fp[2] = -cu->extrude;
  fp[3] = fp[4] = 0.0;
  fp[5] = cu->extrude;
}

static void curve_bevel_make_from_object(const Curve *cu, ListBase *disp)
{
  if (cu->bevobj == nullptr) {
    return;
  }
  if (cu->bevobj->type != OB_CURVES_LEGACY) {
    return;
  }

  Curve *bevcu = static_cast<Curve *>(cu->bevobj->data);
  if (bevcu->extrude == 0.0f && bevcu->bevel_radius == 0.0f) {
    ListBase bevdisp = {nullptr, nullptr};
    float facx = cu->bevobj->scale[0];
    float facy = cu->bevobj->scale[1];

    DispList *dl;
    if (cu->bevobj->runtime->curve_cache) {
      dl = static_cast<DispList *>(cu->bevobj->runtime->curve_cache->disp.first);
    }
    else {
      BLI_assert(cu->bevobj->runtime->curve_cache != nullptr);
      dl = nullptr;
    }

    while (dl) {
      if (ELEM(dl->type, DL_POLY, DL_SEGM)) {
        DispList *dlnew = MEM_mallocN<DispList>(__func__);
        *dlnew = *dl;
        dlnew->verts = MEM_malloc_arrayN<float>(3 * size_t(dl->parts) * size_t(dl->nr), __func__);
        memcpy(dlnew->verts, dl->verts, sizeof(float[3]) * dl->parts * dl->nr);

        if (dlnew->type == DL_SEGM) {
          dlnew->flag |= (DL_FRONT_CURVE | DL_BACK_CURVE);
        }

        BLI_addtail(disp, dlnew);
        float *fp = dlnew->verts;
        int nr = dlnew->parts * dlnew->nr;
        while (nr--) {
          fp[2] = fp[1] * facy;
          fp[1] = -fp[0] * facx;
          fp[0] = 0.0;
          fp += 3;
        }
      }
      dl = dl->next;
    }

    BKE_displist_free(&bevdisp);
  }
}

ListBase BKE_curve_bevel_make(const Curve *curve)
{
  ListBase bevel_shape = {nullptr, nullptr};

  if (curve->bevel_mode == CU_BEV_MODE_OBJECT) {
    if (curve->bevobj != nullptr) {
      curve_bevel_make_from_object(curve, &bevel_shape);
    }
  }
  else {
    const bool use_extrude = curve->extrude != 0.0f;
    const bool use_bevel = curve->bevel_radius != 0.0f;
    /* Pass. */
    if (use_extrude && !use_bevel) {
      curve_bevel_make_only_extrude(curve, &bevel_shape);
    }
    else if (use_extrude || use_bevel) {
      CurveBevelFillType fill_type = curve_bevel_get_fill_type(curve);

      if (!use_extrude && fill_type == FULL && curve->bevel_mode == CU_BEV_MODE_ROUND) {
        curve_bevel_make_full_circle(curve, &bevel_shape);
      }
      else {
        /* The general case for nonzero extrusion or an incomplete loop. */
        curve_bevel_make_extrude_and_fill(curve, &bevel_shape, use_extrude, fill_type);
      }
    }
  }

  return bevel_shape;
}
