/*
 * 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.
 *
 * The Original Code is Copyright (C) 2008, Blender Foundation
 * This is a new part of Blender
 * Operator for converting Grease Pencil data to geometry
 */

/** \file
 * \ingroup edgpencil
 */

#include "MEM_guardedalloc.h"

#include "BLI_blenlib.h"
#include "BLI_math.h"

#include "DNA_gpencil_types.h"
#include "DNA_scene_types.h"
#include "DNA_screen_types.h"

#include "BKE_context.h"
#include "BKE_duplilist.h"
#include "BKE_global.h"
#include "BKE_gpencil.h"
#include "BKE_gpencil_geom.h"
#include "BKE_main.h"
#include "BKE_material.h"
#include "BKE_object.h"
#include "BKE_report.h"
#include "BKE_scene.h"

#include "DEG_depsgraph.h"
#include "DEG_depsgraph_query.h"

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

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

#include "ED_gpencil.h"
#include "ED_transform_snap_object_context.h"

#include "gpencil_intern.h"

/* Check frame_end is always > start frame! */
static void gpencil_bake_set_frame_end(struct Main *UNUSED(main),
                                       struct Scene *UNUSED(scene),
                                       struct PointerRNA *ptr)
{
  int frame_start = RNA_int_get(ptr, "frame_start");
  int frame_end = RNA_int_get(ptr, "frame_end");

  if (frame_end <= frame_start) {
    RNA_int_set(ptr, "frame_end", frame_start + 1);
  }
}

/* Extract mesh animation to Grease Pencil. */
static bool gpencil_bake_mesh_animation_poll(bContext *C)
{
  if (CTX_data_mode_enum(C) != CTX_MODE_OBJECT) {
    return false;
  }

  /* Only if the current view is 3D View. */
  ScrArea *area = CTX_wm_area(C);
  return (area && area->spacetype);
}

typedef struct GpBakeOb {
  struct GpBakeOb *next, *prev;
  Object *ob;
} GpBakeOb;

static void gpencil_bake_duplilist(Depsgraph *depsgraph, Scene *scene, Object *ob, ListBase *list)
{
  GpBakeOb *elem = NULL;
  ListBase *lb;
  DupliObject *dob;
  lb = object_duplilist(depsgraph, scene, ob);
  for (dob = lb->first; dob; dob = dob->next) {
    if (dob->ob->type != OB_MESH) {
      continue;
    }
    elem = MEM_callocN(sizeof(GpBakeOb), __func__);
    elem->ob = dob->ob;
    BLI_addtail(list, elem);
  }

  free_object_duplilist(lb);
}

static bool gpencil_bake_ob_list(bContext *C, Depsgraph *depsgraph, Scene *scene, ListBase *list)
{
  GpBakeOb *elem = NULL;
  bool simple_material = false;

  /* Add active object. In some files this could not be in selected array. */
  Object *obact = CTX_data_active_object(C);

  if (obact->type == OB_MESH) {
    elem = MEM_callocN(sizeof(GpBakeOb), __func__);
    elem->ob = obact;
    BLI_addtail(list, elem);
  }
  /* Add duplilist. */
  else if (obact->type == OB_EMPTY) {
    gpencil_bake_duplilist(depsgraph, scene, obact, list);
    simple_material |= true;
  }

  /* Add other selected objects. */
  CTX_DATA_BEGIN (C, Object *, ob, selected_objects) {
    if (ob == obact) {
      continue;
    }
    /* Add selected meshes.*/
    if (ob->type == OB_MESH) {
      elem = MEM_callocN(sizeof(GpBakeOb), __func__);
      elem->ob = ob;
      BLI_addtail(list, elem);
    }

    /* Add duplilist. */
    if (ob->type == OB_EMPTY) {
      gpencil_bake_duplilist(depsgraph, scene, obact, list);
    }
  }
  CTX_DATA_END;

  return simple_material;
}

static void gpencil_bake_free_ob_list(ListBase *list)
{
  LISTBASE_FOREACH_MUTABLE (GpBakeOb *, elem, list) {
    MEM_SAFE_FREE(elem);
  }
}

static int gpencil_bake_mesh_animation_exec(bContext *C, wmOperator *op)
{
  Main *bmain = CTX_data_main(C);
  Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
  Scene *scene = CTX_data_scene(C);
  ARegion *region = CTX_wm_region(C);
  View3D *v3d = CTX_wm_view3d(C);
  Object *ob_gpencil = NULL;

  ListBase list = {NULL, NULL};
  const bool simple_material = gpencil_bake_ob_list(C, depsgraph, scene, &list);

  /* Cannot check this in poll because the active object changes. */
  if (list.first == NULL) {
    BKE_report(op->reports, RPT_INFO, "No valid object selected");
    gpencil_bake_free_ob_list(&list);
    return OPERATOR_CANCELLED;
  }

  /* Grab all relevant settings. */
  const int step = RNA_int_get(op->ptr, "step");

  const int frame_start = (scene->r.sfra > RNA_int_get(op->ptr, "frame_start")) ?
                              scene->r.sfra :
                              RNA_int_get(op->ptr, "frame_start");

  const int frame_end = (scene->r.efra < RNA_int_get(op->ptr, "frame_end")) ?
                            scene->r.efra :
                            RNA_int_get(op->ptr, "frame_end");

  const float angle = RNA_float_get(op->ptr, "angle");
  const int thickness = RNA_int_get(op->ptr, "thickness");
  const bool use_seams = RNA_boolean_get(op->ptr, "seams");
  const bool use_faces = RNA_boolean_get(op->ptr, "faces");
  const float offset = RNA_float_get(op->ptr, "offset");
  const int frame_offset = RNA_int_get(op->ptr, "frame_target") - frame_start;
  char target[64];
  RNA_string_get(op->ptr, "target", target);
  const int project_type = RNA_enum_get(op->ptr, "project_type");

  /* Create a new grease pencil object in origin. */
  bool newob = false;
  if (STREQ(target, "*NEW")) {
    ushort local_view_bits = (v3d && v3d->localvd) ? v3d->local_view_uuid : 0;
    float loc[3] = {0.0f, 0.0f, 0.0f};
    ob_gpencil = ED_gpencil_add_object(C, loc, local_view_bits);
    newob = true;
  }
  else {
    ob_gpencil = BLI_findstring(&bmain->objects, target, offsetof(ID, name) + 2);
  }

  if ((ob_gpencil == NULL) || (ob_gpencil->type != OB_GPENCIL)) {
    BKE_report(op->reports, RPT_ERROR, "Target grease pencil object not valid");
    gpencil_bake_free_ob_list(&list);
    return OPERATOR_CANCELLED;
  }

  bGPdata *gpd = (bGPdata *)ob_gpencil->data;
  gpd->draw_mode = (project_type == GP_REPROJECT_KEEP) ? GP_DRAWMODE_3D : GP_DRAWMODE_2D;

  /* Set cursor to indicate working. */
  WM_cursor_wait(1);

  GP_SpaceConversion gsc = {NULL};
  SnapObjectContext *sctx = NULL;
  if (project_type != GP_REPROJECT_KEEP) {
    /* Init space conversion stuff. */
    gpencil_point_conversion_init(C, &gsc);
    /* Init snap context for geometry projection. */
    sctx = ED_transform_snap_object_context_create_view3d(scene, 0, region, CTX_wm_view3d(C));

    /* Tag all existing strokes to avoid reprojections. */
    LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) {
      LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) {
        LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
          gps->flag |= GP_STROKE_TAG;
        }
      }
    }
  }

  /* Loop all frame range. */
  int oldframe = (int)DEG_get_ctime(depsgraph);
  int key = -1;
  for (int i = frame_start; i < frame_end + 1; i++) {
    key++;
    /* Jump if not step limit but include last frame always. */
    if ((key % step != 0) && (i != frame_end)) {
      continue;
    }

    /* Move scene to new frame. */
    CFRA = i;
    BKE_scene_graph_update_for_newframe(depsgraph, bmain);

    /* Loop all objects in the list. */
    LISTBASE_FOREACH (GpBakeOb *, elem, &list) {
      Object *ob_eval = (Object *)DEG_get_evaluated_object(depsgraph, elem->ob);

      /* Generate strokes. */
      BKE_gpencil_convert_mesh(bmain,
                               depsgraph,
                               scene,
                               ob_gpencil,
                               elem->ob,
                               angle,
                               thickness,
                               offset,
                               ob_eval->obmat,
                               frame_offset,
                               use_seams,
                               use_faces,
                               simple_material);

      /* Reproject all untaged created strokes. */
      if (project_type != GP_REPROJECT_KEEP) {
        LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) {
          bGPDframe *gpf = gpl->actframe;
          if (gpf != NULL) {
            LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
              if ((gps->flag & GP_STROKE_TAG) == 0) {
                ED_gpencil_stroke_reproject(
                    depsgraph, &gsc, sctx, gpl, gpf, gps, project_type, false);
                gps->flag |= GP_STROKE_TAG;
              }
            }
          }
        }
      }
    }
  }

  /* Return scene frame state and DB to original state. */
  CFRA = oldframe;
  BKE_scene_graph_update_for_newframe(depsgraph, bmain);

  /* Remove unused materials. */
  int actcol = ob_gpencil->actcol;
  for (int slot = 1; slot <= ob_gpencil->totcol; slot++) {
    while (slot <= ob_gpencil->totcol && !BKE_object_material_slot_used(ob_gpencil->data, slot)) {
      ob_gpencil->actcol = slot;
      BKE_object_material_slot_remove(CTX_data_main(C), ob_gpencil);

      if (actcol >= slot) {
        actcol--;
      }
    }
  }
  ob_gpencil->actcol = actcol;

  /* Untag all strokes. */
  if (project_type != GP_REPROJECT_KEEP) {
    LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) {
      LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) {
        LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
          gps->flag &= ~GP_STROKE_TAG;
        }
      }
    }
  }

  /* Free memory. */
  gpencil_bake_free_ob_list(&list);
  if (sctx != NULL) {
    ED_transform_snap_object_context_destroy(sctx);
  }

  /* notifiers */
  if (newob) {
    DEG_relations_tag_update(bmain);
  }
  DEG_id_tag_update(&scene->id, ID_RECALC_SELECT);
  WM_event_add_notifier(C, NC_OBJECT | NA_ADDED, NULL);
  WM_event_add_notifier(C, NC_SCENE | ND_OB_ACTIVE, scene);

  /* Reset cursor. */
  WM_cursor_wait(0);

  /* done */
  return OPERATOR_FINISHED;
}

void GPENCIL_OT_bake_mesh_animation(wmOperatorType *ot)
{
  static const EnumPropertyItem reproject_type[] = {
      {GP_REPROJECT_KEEP, "KEEP", 0, "No Reproject", ""},
      {GP_REPROJECT_FRONT, "FRONT", 0, "Front", "Reproject the strokes using the X-Z plane"},
      {GP_REPROJECT_SIDE, "SIDE", 0, "Side", "Reproject the strokes using the Y-Z plane"},
      {GP_REPROJECT_TOP, "TOP", 0, "Top", "Reproject the strokes using the X-Y plane"},
      {GP_REPROJECT_VIEW,
       "VIEW",
       0,
       "View",
       "Reproject the strokes to end up on the same plane, as if drawn from the current viewpoint "
       "using 'Cursor' Stroke Placement"},
      {GP_REPROJECT_CURSOR,
       "CURSOR",
       0,
       "Cursor",
       "Reproject the strokes using the orientation of 3D cursor"},
      {0, NULL, 0, NULL, NULL},
  };

  PropertyRNA *prop;

  /* identifiers */
  ot->name = "Bake Mesh Animation to Grease Pencil";
  ot->idname = "GPENCIL_OT_bake_mesh_animation";
  ot->description = "Bake Mesh Animation to Grease Pencil strokes";

  /* callbacks */
  ot->exec = gpencil_bake_mesh_animation_exec;
  ot->poll = gpencil_bake_mesh_animation_poll;

  /* flags */
  ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;

  /* properties */
  ot->prop = RNA_def_int(
      ot->srna, "frame_start", 1, 1, 100000, "Start Frame", "The start frame", 1, 100000);

  prop = RNA_def_int(
      ot->srna, "frame_end", 250, 1, 100000, "End Frame", "The end frame of animation", 1, 100000);
  RNA_def_property_update_runtime(prop, gpencil_bake_set_frame_end);

  prop = RNA_def_int(ot->srna, "step", 1, 1, 100, "Step", "Step between generated frames", 1, 100);

  prop = RNA_def_float_rotation(ot->srna,
                                "angle",
                                0,
                                NULL,
                                DEG2RADF(0.0f),
                                DEG2RADF(180.0f),
                                "Threshold Angle",
                                "Threshold to determine ends of the strokes",
                                DEG2RADF(0.0f),
                                DEG2RADF(180.0f));
  RNA_def_property_float_default(prop, DEG2RADF(70.0f));

  RNA_def_int(ot->srna, "thickness", 1, 1, 100, "Thickness", "", 1, 100);
  RNA_def_boolean(ot->srna, "seams", 0, "Only Seam Edges", "Convert only seam edges");
  RNA_def_boolean(ot->srna, "faces", 1, "Export Faces", "Export faces as filled strokes");
  RNA_def_float_distance(
      ot->srna, "offset", 0.001f, 0.0, 100.0, "Offset", "Offset strokes from fill", 0.0, 100.00);
  RNA_def_int(ot->srna, "frame_target", 1, 1, 100000, "Frame Target", "", 1, 100000);
  RNA_def_string(ot->srna,
                 "target",
                 "*NEW",
                 64,
                 "Target Object",
                 "Target grease pencil object name. Leave empty for new object");

  RNA_def_enum(ot->srna, "project_type", reproject_type, GP_REPROJECT_VIEW, "Projection Type", "");
}
