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

/** \file
 * \ingroup modifiers
 */

#include "BLI_hash.h"
#include "BLI_rand.h"

#include "BLT_translation.hh"

#include "BLO_read_write.hh"

#include "DNA_defaults.h"
#include "DNA_gpencil_modifier_types.h"
#include "DNA_node_types.h" /* For `GeometryNodeCurveSampleMode` */
#include "DNA_object_types.h"

#include "BKE_curves.hh"
#include "BKE_geometry_set.hh"
#include "BKE_grease_pencil.hh"
#include "BKE_modifier.hh"

#include "UI_interface.hh"
#include "UI_resources.hh"

#include "MOD_grease_pencil_util.hh"
#include "MOD_modifiertypes.hh"
#include "MOD_ui_common.hh"

#include "RNA_access.hh"
#include "RNA_prototypes.hh"

#include "GEO_extend_curves.hh"
#include "GEO_trim_curves.hh"

namespace blender {

static void init_data(ModifierData *md)
{
  GreasePencilLengthModifierData *gpmd = reinterpret_cast<GreasePencilLengthModifierData *>(md);

  BLI_assert(MEMCMP_STRUCT_AFTER_IS_ZERO(gpmd, modifier));

  MEMCPY_STRUCT_AFTER(gpmd, DNA_struct_default_get(GreasePencilLengthModifierData), modifier);
  modifier::greasepencil::init_influence_data(&gpmd->influence, false);
}

static void copy_data(const ModifierData *md, ModifierData *target, int flags)
{
  const auto *omd = reinterpret_cast<const GreasePencilLengthModifierData *>(md);
  auto *tomd = reinterpret_cast<GreasePencilLengthModifierData *>(target);

  modifier::greasepencil::free_influence_data(&tomd->influence);

  BKE_modifier_copydata_generic(md, target, flags);
  modifier::greasepencil::copy_influence_data(&omd->influence, &tomd->influence, flags);
}

static void free_data(ModifierData *md)
{
  auto *omd = reinterpret_cast<GreasePencilLengthModifierData *>(md);
  modifier::greasepencil::free_influence_data(&omd->influence);
}

static void foreach_ID_link(ModifierData *md, Object *ob, IDWalkFunc walk, void *user_data)
{
  auto *omd = reinterpret_cast<GreasePencilLengthModifierData *>(md);
  modifier::greasepencil::foreach_influence_ID_link(&omd->influence, ob, walk, user_data);
}

static void blend_write(BlendWriter *writer, const ID * /*id_owner*/, const ModifierData *md)
{
  const GreasePencilLengthModifierData *mmd =
      reinterpret_cast<const GreasePencilLengthModifierData *>(md);

  BLO_write_struct(writer, GreasePencilLengthModifierData, mmd);
  modifier::greasepencil::write_influence_data(writer, &mmd->influence);
}

static void blend_read(BlendDataReader *reader, ModifierData *md)
{
  GreasePencilLengthModifierData *mmd = reinterpret_cast<GreasePencilLengthModifierData *>(md);

  modifier::greasepencil::read_influence_data(reader, &mmd->influence);
}

static Array<float> noise_table(int len, int offset, int seed)
{
  Array<float> table(len);
  for (const int i : table.index_range()) {
    table[i] = BLI_hash_int_01(BLI_hash_int_2d(seed, i + offset + 1));
  }
  return table;
}

static float table_sample(MutableSpan<float> table, float x)
{
  return math::interpolate(table[int(math::ceil(x))], table[int(math::floor(x))], math::fract(x));
}

static void deform_drawing(const ModifierData &md,
                           const Object &ob,
                           bke::greasepencil::Drawing &drawing,
                           const int current_time)
{
  const GreasePencilLengthModifierData &mmd =
      reinterpret_cast<const GreasePencilLengthModifierData &>(md);
  modifier::greasepencil::ensure_no_bezier_curves(drawing);
  bke::CurvesGeometry &curves = drawing.strokes_for_write();

  if (curves.is_empty()) {
    return;
  }

  IndexMaskMemory memory;
  const IndexMask selection = modifier::greasepencil::get_filtered_stroke_mask(
      &ob, curves, mmd.influence, memory);

  const int curves_num = curves.curves_num();

  /* Variable for tagging shrinking when values are adjusted after random. */
  std::atomic<bool> needs_additional_shrinking = false;

  VArray<float> use_starts = VArray<float>::ForSingle(mmd.start_fac, curves_num);
  VArray<float> use_ends = VArray<float>::ForSingle(mmd.end_fac, curves_num);

  Array<float> modified_starts;
  Array<float> modified_ends;
  if (mmd.rand_start_fac != 0.0 || mmd.rand_end_fac != 0.0) {
    modified_starts = Array<float>(curves.curves_num(), mmd.start_fac);
    modified_ends = Array<float>(curves.curves_num(), mmd.end_fac);

    /* Use random to modify start/end factors. Put the modified values outside the
     * branch so it could be accessed in later stretching/shrinking stages. */
    use_starts = VArray<float>::ForSpan(modified_starts.as_mutable_span());
    use_ends = VArray<float>::ForSpan(modified_ends.as_mutable_span());

    int seed = mmd.seed;

    /* Make sure different modifiers get different seeds. */
    seed += BLI_hash_string(ob.id.name + 2);
    seed += BLI_hash_string(md.name);

    if (mmd.flag & GP_LENGTH_USE_RANDOM) {
      seed += current_time / mmd.step;
    }

    float rand_offset = BLI_hash_int_01(seed);

    Array<float> noise_table_length = noise_table(
        4 + curves_num, int(math::floor(mmd.rand_offset)), seed + 2);

    threading::parallel_for(IndexRange(curves_num), 512, [&](const IndexRange parallel_range) {
      for (const int i : parallel_range) {
        /* To ensure a nice distribution, we use halton sequence and offset using the seed. */
        double r[2];
        const uint primes[2] = {2, 3};
        double offset[2] = {0.0f, 0.0f};
        BLI_halton_2d(primes, offset, i, r);

        float rand[2] = {0.0f, 0.0f};
        for (int j = 0; j < 2; j++) {
          float noise = table_sample(noise_table_length, i + j * 2 + math::fract(mmd.rand_offset));

          rand[j] = math::mod(float(r[j] + rand_offset), 1.0f);
          rand[j] = math::abs(
              math::mod(sin(rand[j] * 12.9898f + j * 78.233f) * 43758.5453f, 1.0f) + noise);
        }

        modified_starts[i] = modified_starts[i] + rand[0] * mmd.rand_start_fac;
        modified_ends[i] = modified_ends[i] + rand[1] * mmd.rand_end_fac;

        if (modified_starts[i] <= 0.0f || modified_ends[i] <= 0.0f) {
          needs_additional_shrinking.store(true, std::memory_order_relaxed);
        }
      }
    });
  }

  curves = geometry::extend_curves(curves,
                                   selection,
                                   use_starts,
                                   use_ends,
                                   mmd.overshoot_fac,
                                   (mmd.flag & GP_LENGTH_USE_CURVATURE) != 0,
                                   mmd.point_density,
                                   mmd.segment_influence,
                                   mmd.max_angle,
                                   (mmd.flag & GP_LENGTH_INVERT_CURVATURE) != 0,
                                   ((mmd.mode & GP_LENGTH_ABSOLUTE) != 0) ?
                                       GEO_NODE_CURVE_SAMPLE_LENGTH :
                                       GEO_NODE_CURVE_SAMPLE_FACTOR,
                                   {});

  /* Always do the stretching first since it might depend on points which could be deleted by the
   * shrink. */
  if (mmd.start_fac < 0.0f || mmd.end_fac < 0.0f || needs_additional_shrinking) {
    /* #trim_curves() accepts the `end` values if it's sampling from the beginning of the
     * curve, so we need to get the lengths of the curves and subtract it from the back when the
     * modifier is in Absolute mode. For convenience, we always call #trim_curves() in LENGTH
     * mode since the function itself will need length to be sampled anyway. */
    Array<float> starts(curves.curves_num());
    Array<float> ends(curves.curves_num());
    Array<bool> needs_removal(curves.curves_num());
    needs_removal.fill(false);

    curves.ensure_evaluated_lengths();

    threading::parallel_for(curves.curves_range(), 512, [&](const IndexRange parallel_range) {
      for (const int curve : parallel_range) {
        float length = curves.evaluated_length_total_for_curve(curve, false);
        if (mmd.mode & GP_LENGTH_ABSOLUTE) {
          starts[curve] = -math::min(use_starts[curve], 0.0f);
          ends[curve] = length - -math::min(use_ends[curve], 0.0f);
        }
        else {
          starts[curve] = -math::min(use_starts[curve], 0.0f) * length;
          ends[curve] = (1 + math::min(use_ends[curve], 0.0f)) * length;
        }
        if (starts[curve] > ends[curve]) {
          needs_removal[curve] = true;
        }
      }
    });
    curves = geometry::trim_curves(curves,
                                   selection,
                                   VArray<float>::ForSpan(starts.as_span()),
                                   VArray<float>::ForSpan(ends.as_span()),
                                   GEO_NODE_CURVE_SAMPLE_LENGTH,
                                   {});

    /* #trim_curves() will leave the last segment there when trimmed length is greater than
     * curve original length, thus we need to remove those curves afterwards. */
    IndexMaskMemory memory_remove;
    const IndexMask to_remove = IndexMask::from_bools(needs_removal.as_span(), memory_remove);
    if (!to_remove.is_empty()) {
      curves.remove_curves(to_remove, {});
    }
  }

  drawing.tag_topology_changed();
}

static void modify_geometry_set(ModifierData *md,
                                const ModifierEvalContext *ctx,
                                blender::bke::GeometrySet *geometry_set)
{
  GreasePencilLengthModifierData *mmd = reinterpret_cast<GreasePencilLengthModifierData *>(md);

  if (!geometry_set->has_grease_pencil()) {
    return;
  }

  GreasePencil &grease_pencil = *geometry_set->get_grease_pencil_for_write();

  IndexMaskMemory mask_memory;
  const IndexMask layer_mask = modifier::greasepencil::get_filtered_layer_mask(
      grease_pencil, mmd->influence, mask_memory);
  const Vector<bke::greasepencil::Drawing *> drawings =
      modifier::greasepencil::get_drawings_for_write(
          grease_pencil, layer_mask, grease_pencil.runtime->eval_frame);

  threading::parallel_for_each(drawings, [&](bke::greasepencil::Drawing *drawing) {
    deform_drawing(*md, *ctx->object, *drawing, grease_pencil.runtime->eval_frame);
  });
}

static void panel_draw(const bContext *C, Panel *panel)
{
  uiLayout *layout = panel->layout;

  PointerRNA *ptr = modifier_panel_get_property_pointers(panel, nullptr);

  uiLayoutSetPropSep(layout, true);
  layout->prop(ptr, "mode", UI_ITEM_NONE, std::nullopt, ICON_NONE);

  uiLayout *col = &layout->column(true);

  if (RNA_enum_get(ptr, "mode") == GP_LENGTH_RELATIVE) {
    col->prop(ptr, "start_factor", UI_ITEM_NONE, IFACE_("Start"), ICON_NONE);
    col->prop(ptr, "end_factor", UI_ITEM_NONE, IFACE_("End"), ICON_NONE);
  }
  else {
    col->prop(ptr, "start_length", UI_ITEM_NONE, IFACE_("Start"), ICON_NONE);
    col->prop(ptr, "end_length", UI_ITEM_NONE, IFACE_("End"), ICON_NONE);
  }

  layout->prop(ptr, "overshoot_factor", UI_ITEM_R_SLIDER, IFACE_("Used Length"), ICON_NONE);
  PanelLayout random_panel_layout = layout->panel_prop_with_bool_header(
      C, ptr, "open_random_panel", ptr, "use_random", IFACE_("Randomize"));
  if (uiLayout *random_layout = random_panel_layout.body) {
    uiLayout *subcol = &random_layout->column(false);
    uiLayoutSetPropSep(subcol, true);
    uiLayoutSetActive(subcol, RNA_boolean_get(ptr, "use_random"));

    subcol->prop(ptr, "step", UI_ITEM_NONE, std::nullopt, ICON_NONE);

    subcol->prop(ptr, "random_start_factor", UI_ITEM_NONE, IFACE_("Offset Start"), ICON_NONE);
    subcol->prop(ptr, "random_end_factor", UI_ITEM_NONE, IFACE_("End"), ICON_NONE);
    subcol->prop(ptr, "random_offset", UI_ITEM_NONE, IFACE_("Noise Offset"), ICON_NONE);
    subcol->prop(ptr, "seed", UI_ITEM_NONE, std::nullopt, ICON_NONE);
  }
  PanelLayout curvature_panel_layout = layout->panel_prop_with_bool_header(
      C, ptr, "open_curvature_panel", ptr, "use_curvature", IFACE_("Curvature"));
  if (uiLayout *curvature_layout = curvature_panel_layout.body) {
    uiLayout *subcol = &curvature_layout->column(false);
    uiLayoutSetPropSep(subcol, true);
    uiLayoutSetActive(subcol, RNA_boolean_get(ptr, "use_curvature"));

    subcol->prop(ptr, "point_density", UI_ITEM_NONE, std::nullopt, ICON_NONE);
    subcol->prop(ptr, "segment_influence", UI_ITEM_NONE, std::nullopt, ICON_NONE);
    subcol->prop(ptr, "max_angle", UI_ITEM_NONE, std::nullopt, ICON_NONE);
    subcol->prop(ptr, "invert_curvature", UI_ITEM_NONE, IFACE_("Invert"), ICON_NONE);
  }

  if (uiLayout *influence_panel = layout->panel_prop(
          C, ptr, "open_influence_panel", IFACE_("Influence")))
  {
    modifier::greasepencil::draw_layer_filter_settings(C, influence_panel, ptr);
    modifier::greasepencil::draw_material_filter_settings(C, influence_panel, ptr);
  }

  modifier_error_message_draw(layout, ptr);
}

static void panel_register(ARegionType *region_type)
{
  modifier_panel_register(region_type, eModifierType_GreasePencilLength, panel_draw);
}

}  // namespace blender

ModifierTypeInfo modifierType_GreasePencilLength = {
    /*idname*/ "GreasePencilLengthModifier",
    /*name*/ N_("Length"),
    /*struct_name*/ "GreasePencilLengthModifierData",
    /*struct_size*/ sizeof(GreasePencilLengthModifierData),
    /*srna*/ &RNA_GreasePencilLengthModifier,
    /*type*/ ModifierTypeType::Nonconstructive,
    /*flags*/
    eModifierTypeFlag_AcceptsGreasePencil | eModifierTypeFlag_EnableInEditmode |
        eModifierTypeFlag_SupportsEditmode,
    /*icon*/ ICON_MOD_LENGTH,

    /*copy_data*/ blender::copy_data,

    /*deform_verts*/ nullptr,
    /*deform_matrices*/ nullptr,
    /*deform_verts_EM*/ nullptr,
    /*deform_matrices_EM*/ nullptr,
    /*modify_mesh*/ nullptr,
    /*modify_geometry_set*/ blender::modify_geometry_set,

    /*init_data*/ blender::init_data,
    /*required_data_mask*/ nullptr,
    /*free_data*/ blender::free_data,
    /*is_disabled*/ nullptr,
    /*update_depsgraph*/ nullptr,
    /*depends_on_time*/ nullptr,
    /*depends_on_normals*/ nullptr,
    /*foreach_ID_link*/ blender::foreach_ID_link,
    /*foreach_tex_link*/ nullptr,
    /*free_runtime_data*/ nullptr,
    /*panel_register*/ blender::panel_register,
    /*blend_write*/ blender::blend_write,
    /*blend_read*/ blender::blend_read,
};
