/*
 * 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 2
 * 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, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

/** \file
 * \ingroup edgizmolib
 *
 * \name Move Gizmo
 *
 * 3D Gizmo, also works in 2D views.
 *
 * \brief Simple gizmo to move and translate.
 *
 * - `matrix[0]` is derived from Y and Z.
 * - `matrix[1]` currently not used.
 * - `matrix[2]` is the widget direction (for all gizmos).
 */

#include "MEM_guardedalloc.h"

#include "BLI_math.h"

#include "BKE_context.h"

#include "GPU_immediate.h"
#include "GPU_immediate_util.h"
#include "GPU_matrix.h"
#include "GPU_select.h"
#include "GPU_state.h"

#include "RNA_access.h"
#include "RNA_define.h"

#include "WM_api.h"
#include "WM_types.h"

#include "ED_gizmo_library.h"
#include "ED_screen.h"
#include "ED_transform_snap_object_context.h"
#include "ED_view3d.h"

/* own includes */
#include "../gizmo_geometry.h"
#include "../gizmo_library_intern.h"

#define MVAL_MAX_PX_DIST 12.0f

typedef struct MoveGizmo3D {
  wmGizmo gizmo;
  /* Added to 'matrix_basis' when calculating the matrix. */
  float prop_co[3];
} MoveGizmo3D;

static void gizmo_move_matrix_basis_get(const wmGizmo *gz, float r_matrix[4][4])
{
  MoveGizmo3D *move = (MoveGizmo3D *)gz;

  copy_m4_m4(r_matrix, move->gizmo.matrix_basis);
  add_v3_v3(r_matrix[3], move->prop_co);
}

static int gizmo_move_modal(bContext *C,
                            wmGizmo *gz,
                            const wmEvent *event,
                            eWM_GizmoFlagTweak tweak_flag);

typedef struct MoveInteraction {
  struct {
    float mval[2];
    /* Only for when using properties. */
    float prop_co[3];
    float matrix_final[4][4];
  } init;
  struct {
    eWM_GizmoFlagTweak tweak_flag;
  } prev;

  /* We could have other snap contexts, for now only support 3D view. */
  struct SnapObjectContext *snap_context_v3d;

} MoveInteraction;

#define DIAL_RESOLUTION 32

/* -------------------------------------------------------------------- */

static void move_geom_draw(const wmGizmo *gz,
                           const float color[4],
                           const bool select,
                           const int draw_options)
{
#ifdef USE_GIZMO_CUSTOM_DIAL
  UNUSED_VARS(move3d, col, axis_modal_mat);
  wm_gizmo_geometryinfo_draw(&wm_gizmo_geom_data_move3d, select);
#else
  const int draw_style = RNA_enum_get(gz->ptr, "draw_style");
  const bool filled = (draw_style != ED_GIZMO_MOVE_STYLE_CROSS_2D) &&
                      ((draw_options & (select ? (ED_GIZMO_MOVE_DRAW_FLAG_FILL |
                                                  ED_GIZMO_MOVE_DRAW_FLAG_FILL_SELECT) :
                                                 ED_GIZMO_MOVE_DRAW_FLAG_FILL)));

  GPUVertFormat *format = immVertexFormat();
  uint pos = GPU_vertformat_attr_add(format, "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT);

  immBindBuiltinProgram(filled ? GPU_SHADER_3D_UNIFORM_COLOR :
                                 GPU_SHADER_3D_POLYLINE_UNIFORM_COLOR);

  float viewport[4];
  GPU_viewport_size_get_f(viewport);
  immUniform2fv("viewportSize", &viewport[2]);
  immUniform1f("lineWidth", gz->line_width * U.pixelsize);

  immUniformColor4fv(color);

  /* Use the final scale as a radius if it's not already applied to the final matrix. */
  const float radius = (gz->flag & WM_GIZMO_DRAW_NO_SCALE) ? gz->scale_final : 1.0f;

  if (draw_style == ED_GIZMO_MOVE_STYLE_RING_2D) {
    if (filled) {
      imm_draw_circle_fill_2d(pos, 0, 0, radius, DIAL_RESOLUTION);
    }
    else {
      imm_draw_circle_wire_2d(pos, 0, 0, radius, DIAL_RESOLUTION);
    }
  }
  else if (draw_style == ED_GIZMO_MOVE_STYLE_CROSS_2D) {
    const float radius_diag = M_SQRT1_2 * radius;
    immBegin(GPU_PRIM_LINES, 4);
    immVertex2f(pos, radius_diag, radius_diag);
    immVertex2f(pos, -radius_diag, -radius_diag);

    immVertex2f(pos, -radius_diag, radius_diag);
    immVertex2f(pos, radius_diag, -radius_diag);
    immEnd();
  }
  else {
    BLI_assert(0);
  }

  immUnbindProgram();

  UNUSED_VARS(select);
#endif
}

static void move3d_get_translate(const wmGizmo *gz,
                                 const wmEvent *event,
                                 const ARegion *region,
                                 float co_delta[3])
{
  MoveInteraction *inter = gz->interaction_data;
  const float mval_delta[2] = {
      event->mval[0] - inter->init.mval[0],
      event->mval[1] - inter->init.mval[1],
  };

  RegionView3D *rv3d = region->regiondata;
  float co_ref[3];
  mul_v3_mat3_m4v3(co_ref, gz->matrix_space, inter->init.prop_co);
  const float zfac = ED_view3d_calc_zfac(rv3d, co_ref, NULL);

  ED_view3d_win_to_delta(region, mval_delta, co_delta, zfac);

  float matrix_space_inv[3][3];
  copy_m3_m4(matrix_space_inv, gz->matrix_space);
  invert_m3(matrix_space_inv);
  mul_m3_v3(matrix_space_inv, co_delta);
}

static void move3d_draw_intern(const bContext *C,
                               wmGizmo *gz,
                               const bool select,
                               const bool highlight)
{
  MoveInteraction *inter = gz->interaction_data;
  const int draw_options = RNA_enum_get(gz->ptr, "draw_options");
  const bool align_view = (draw_options & ED_GIZMO_MOVE_DRAW_FLAG_ALIGN_VIEW) != 0;
  float color[4];
  float matrix_final[4][4];
  float matrix_align[4][4];

  gizmo_color_get(gz, highlight, color);
  WM_gizmo_calc_matrix_final(gz, matrix_final);

  GPU_matrix_push();
  GPU_matrix_mul(matrix_final);

  if (align_view) {
    float matrix_final_unit[4][4];
    RegionView3D *rv3d = CTX_wm_region_view3d(C);
    normalize_m4_m4(matrix_final_unit, matrix_final);
    mul_m4_m4m4(matrix_align, rv3d->viewmat, matrix_final_unit);
    zero_v3(matrix_align[3]);
    transpose_m4(matrix_align);
    GPU_matrix_mul(matrix_align);
  }

  GPU_blend(GPU_BLEND_ALPHA);
  move_geom_draw(gz, color, select, draw_options);
  GPU_blend(GPU_BLEND_NONE);
  GPU_matrix_pop();

  if (gz->interaction_data) {
    GPU_matrix_push();
    GPU_matrix_mul(inter->init.matrix_final);

    if (align_view) {
      GPU_matrix_mul(matrix_align);
    }

    GPU_blend(GPU_BLEND_ALPHA);
    move_geom_draw(gz, (const float[4]){0.5f, 0.5f, 0.5f, 0.5f}, select, draw_options);
    GPU_blend(GPU_BLEND_NONE);
    GPU_matrix_pop();
  }
}

static void gizmo_move_draw_select(const bContext *C, wmGizmo *gz, int select_id)
{
  GPU_select_load_id(select_id);
  move3d_draw_intern(C, gz, true, false);
}

static void gizmo_move_draw(const bContext *C, wmGizmo *gz)
{
  const bool is_modal = gz->state & WM_GIZMO_STATE_MODAL;
  const bool is_highlight = (gz->state & WM_GIZMO_STATE_HIGHLIGHT) != 0;

  (void)is_modal;

  GPU_blend(GPU_BLEND_ALPHA);
  move3d_draw_intern(C, gz, false, is_highlight);
  GPU_blend(GPU_BLEND_NONE);
}

static int gizmo_move_modal(bContext *C,
                            wmGizmo *gz,
                            const wmEvent *event,
                            eWM_GizmoFlagTweak tweak_flag)
{
  MoveInteraction *inter = gz->interaction_data;
  if ((event->type != MOUSEMOVE) && (inter->prev.tweak_flag == tweak_flag)) {
    return OPERATOR_RUNNING_MODAL;
  }
  MoveGizmo3D *move = (MoveGizmo3D *)gz;
  ARegion *region = CTX_wm_region(C);

  float prop_delta[3];
  if (CTX_wm_area(C)->spacetype == SPACE_VIEW3D) {
    move3d_get_translate(gz, event, region, prop_delta);
  }
  else {
    float mval_proj_init[2], mval_proj_curr[2];
    if ((gizmo_window_project_2d(C, gz, inter->init.mval, 2, false, mval_proj_init) == false) ||
        (gizmo_window_project_2d(
             C, gz, (const float[2]){UNPACK2(event->mval)}, 2, false, mval_proj_curr) == false)) {
      return OPERATOR_RUNNING_MODAL;
    }
    sub_v2_v2v2(prop_delta, mval_proj_curr, mval_proj_init);
    if ((gz->flag & WM_GIZMO_DRAW_NO_SCALE) == 0) {
      mul_v2_fl(prop_delta, gz->scale_final);
    }
    prop_delta[2] = 0.0f;
  }

  if (tweak_flag & WM_GIZMO_TWEAK_PRECISE) {
    mul_v3_fl(prop_delta, 0.1f);
  }

  add_v3_v3v3(move->prop_co, inter->init.prop_co, prop_delta);

  if (tweak_flag & WM_GIZMO_TWEAK_SNAP) {
    if (inter->snap_context_v3d) {
      float dist_px = MVAL_MAX_PX_DIST * U.pixelsize;
      const float mval_fl[2] = {UNPACK2(event->mval)};
      float co[3];
      if (ED_transform_snap_object_project_view3d(
              inter->snap_context_v3d,
              CTX_data_ensure_evaluated_depsgraph(C),
              (SCE_SNAP_MODE_VERTEX | SCE_SNAP_MODE_EDGE | SCE_SNAP_MODE_FACE),
              &(const struct SnapObjectParams){
                  .snap_select = SNAP_ALL,
                  .use_object_edit_cage = true,
                  .use_occlusion_test = true,
              },
              mval_fl,
              NULL,
              &dist_px,
              co,
              NULL)) {
        float matrix_space_inv[4][4];
        invert_m4_m4(matrix_space_inv, gz->matrix_space);
        mul_v3_m4v3(move->prop_co, matrix_space_inv, co);
      }
    }
  }

  /* set the property for the operator and call its modal function */
  wmGizmoProperty *gz_prop = WM_gizmo_target_property_find(gz, "offset");
  if (WM_gizmo_target_property_is_valid(gz_prop)) {
    WM_gizmo_target_property_float_set_array(C, gz, gz_prop, move->prop_co);
  }
  else {
    zero_v3(move->prop_co);
  }

  ED_region_tag_redraw_editor_overlays(region);

  inter->prev.tweak_flag = tweak_flag;

  return OPERATOR_RUNNING_MODAL;
}

static void gizmo_move_exit(bContext *C, wmGizmo *gz, const bool cancel)
{
  MoveInteraction *inter = gz->interaction_data;
  bool use_reset_value = false;
  const float *reset_value = NULL;
  if (cancel) {
    /* Set the property for the operator and call its modal function. */
    wmGizmoProperty *gz_prop = WM_gizmo_target_property_find(gz, "offset");
    if (WM_gizmo_target_property_is_valid(gz_prop)) {
      use_reset_value = true;
      reset_value = inter->init.prop_co;
    }
  }

  if (use_reset_value) {
    wmGizmoProperty *gz_prop = WM_gizmo_target_property_find(gz, "offset");
    if (WM_gizmo_target_property_is_valid(gz_prop)) {
      WM_gizmo_target_property_float_set_array(C, gz, gz_prop, reset_value);
    }
  }

  if (inter->snap_context_v3d) {
    ED_transform_snap_object_context_destroy(inter->snap_context_v3d);
    inter->snap_context_v3d = NULL;
  }

  if (!cancel) {
    wmGizmoProperty *gz_prop = WM_gizmo_target_property_find(gz, "offset");
    if (WM_gizmo_target_property_is_valid(gz_prop)) {
      WM_gizmo_target_property_anim_autokey(C, gz, gz_prop);
    }
  }
}

static int gizmo_move_invoke(bContext *C, wmGizmo *gz, const wmEvent *event)
{
  const bool use_snap = RNA_boolean_get(gz->ptr, "use_snap");

  MoveInteraction *inter = MEM_callocN(sizeof(MoveInteraction), __func__);
  inter->init.mval[0] = event->mval[0];
  inter->init.mval[1] = event->mval[1];

#if 0
  copy_v3_v3(inter->init.prop_co, move->prop_co);
#else
  wmGizmoProperty *gz_prop = WM_gizmo_target_property_find(gz, "offset");
  if (WM_gizmo_target_property_is_valid(gz_prop)) {
    WM_gizmo_target_property_float_get_array(gz, gz_prop, inter->init.prop_co);
  }
#endif

  WM_gizmo_calc_matrix_final(gz, inter->init.matrix_final);

  if (use_snap) {
    ScrArea *area = CTX_wm_area(C);
    if (area) {
      switch (area->spacetype) {
        case SPACE_VIEW3D: {
          inter->snap_context_v3d = ED_transform_snap_object_context_create_view3d(
              CTX_data_scene(C), 0, CTX_wm_region(C), CTX_wm_view3d(C));
          break;
        }
        default:
          /* Not yet supported. */
          BLI_assert(0);
      }
    }
  }

  gz->interaction_data = inter;

  return OPERATOR_RUNNING_MODAL;
}

static int gizmo_move_test_select(bContext *C, wmGizmo *gz, const int mval[2])
{
  float point_local[2];

  if (gizmo_window_project_2d(C, gz, (const float[2]){UNPACK2(mval)}, 2, true, point_local) ==
      false) {
    return -1;
  }

  /* The 'gz->scale_final' is already applied to the projection
   * when #WM_GIZMO_DRAW_NO_SCALE isn't set. */
  const float radius = (gz->flag & WM_GIZMO_DRAW_NO_SCALE) ? gz->scale_final : 1.0f;
  if (len_squared_v2(point_local) < radius) {
    return 0;
  }

  return -1;
}

static void gizmo_move_property_update(wmGizmo *gz, wmGizmoProperty *gz_prop)
{
  MoveGizmo3D *move = (MoveGizmo3D *)gz;
  if (WM_gizmo_target_property_is_valid(gz_prop)) {
    WM_gizmo_target_property_float_get_array(gz, gz_prop, move->prop_co);
  }
  else {
    zero_v3(move->prop_co);
  }
}

static int gizmo_move_cursor_get(wmGizmo *UNUSED(gz))
{
  return WM_CURSOR_NSEW_SCROLL;
}

/* -------------------------------------------------------------------- */
/** \name Move Gizmo API
 * \{ */

static void GIZMO_GT_move_3d(wmGizmoType *gzt)
{
  /* identifiers */
  gzt->idname = "GIZMO_GT_move_3d";

  /* api callbacks */
  gzt->draw = gizmo_move_draw;
  gzt->draw_select = gizmo_move_draw_select;
  gzt->test_select = gizmo_move_test_select;
  gzt->matrix_basis_get = gizmo_move_matrix_basis_get;
  gzt->invoke = gizmo_move_invoke;
  gzt->property_update = gizmo_move_property_update;
  gzt->modal = gizmo_move_modal;
  gzt->exit = gizmo_move_exit;
  gzt->cursor_get = gizmo_move_cursor_get;

  gzt->struct_size = sizeof(MoveGizmo3D);

  /* rna */
  static EnumPropertyItem rna_enum_draw_style[] = {
      {ED_GIZMO_MOVE_STYLE_RING_2D, "RING_2D", 0, "Ring", ""},
      {ED_GIZMO_MOVE_STYLE_CROSS_2D, "CROSS_2D", 0, "Ring", ""},
      {0, NULL, 0, NULL, NULL},
  };
  static EnumPropertyItem rna_enum_draw_options[] = {
      {ED_GIZMO_MOVE_DRAW_FLAG_FILL, "FILL", 0, "Filled", ""},
      {ED_GIZMO_MOVE_DRAW_FLAG_FILL_SELECT, "FILL_SELECT", 0, "Use fill for selection test", ""},
      {ED_GIZMO_MOVE_DRAW_FLAG_ALIGN_VIEW, "ALIGN_VIEW", 0, "Align View", ""},
      {0, NULL, 0, NULL, NULL},
  };

  RNA_def_enum(
      gzt->srna, "draw_style", rna_enum_draw_style, ED_GIZMO_MOVE_STYLE_RING_2D, "Draw Style", "");
  RNA_def_enum_flag(gzt->srna, "draw_options", rna_enum_draw_options, 0, "Draw Options", "");
  RNA_def_boolean(gzt->srna, "use_snap", false, "Use Snap", "");

  WM_gizmotype_target_property_def(gzt, "offset", PROP_FLOAT, 3);
}

void ED_gizmotypes_move_3d(void)
{
  WM_gizmotype_append(GIZMO_GT_move_3d);
}

/** \} */ /* Move Gizmo API */
