/*
 * 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) 2007 by Janne Karhu.
 * All rights reserved.
 */

/** \file
 * \ingroup edphys
 */

#include <stdlib.h>
#include <math.h>
#include <string.h>
#include <assert.h>

#include "MEM_guardedalloc.h"

#include "DNA_scene_types.h"
#include "DNA_meshdata_types.h"
#include "DNA_windowmanager_types.h"

#include "BLI_listbase.h"
#include "BLI_utildefines.h"

#include "BKE_context.h"
#include "BKE_particle.h"
#include "BKE_pointcache.h"
#include "BKE_undo_system.h"

#include "DEG_depsgraph.h"

#include "ED_object.h"
#include "ED_particle.h"
#include "ED_physics.h"

#include "particle_edit_utildefines.h"

#include "physics_intern.h"

/* -------------------------------------------------------------------- */
/** \name Undo Conversion
 * \{ */

static void undoptcache_from_editcache(PTCacheUndo *undo, PTCacheEdit *edit)
{
  PTCacheEditPoint *point;
  int i;

  size_t mem_used_prev = MEM_get_memory_in_use();

  undo->totpoint = edit->totpoint;

  if (edit->psys) {
    ParticleData *pa;

    pa = undo->particles = MEM_dupallocN(edit->psys->particles);

    for (i = 0; i < edit->totpoint; i++, pa++) {
      pa->hair = MEM_dupallocN(pa->hair);
    }

    undo->psys_flag = edit->psys->flag;
  }
  else {
    PTCacheMem *pm;

    BLI_duplicatelist(&undo->mem_cache, &edit->pid.cache->mem_cache);
    pm = undo->mem_cache.first;

    for (; pm; pm = pm->next) {
      for (i = 0; i < BPHYS_TOT_DATA; i++) {
        pm->data[i] = MEM_dupallocN(pm->data[i]);
      }
    }
  }

  point = undo->points = MEM_dupallocN(edit->points);
  undo->totpoint = edit->totpoint;

  for (i = 0; i < edit->totpoint; i++, point++) {
    point->keys = MEM_dupallocN(point->keys);
    /* no need to update edit key->co & key->time pointers here */
  }

  size_t mem_used_curr = MEM_get_memory_in_use();

  undo->undo_size = mem_used_prev < mem_used_curr ? mem_used_curr - mem_used_prev :
                                                    sizeof(PTCacheUndo);
}

static void undoptcache_to_editcache(PTCacheUndo *undo, PTCacheEdit *edit)
{
  ParticleSystem *psys = edit->psys;
  ParticleData *pa;
  HairKey *hkey;
  POINT_P;
  KEY_K;

  LOOP_POINTS
  {
    if (psys && psys->particles[p].hair) {
      MEM_freeN(psys->particles[p].hair);
    }

    if (point->keys) {
      MEM_freeN(point->keys);
    }
  }
  if (psys && psys->particles) {
    MEM_freeN(psys->particles);
  }
  if (edit->points) {
    MEM_freeN(edit->points);
  }
  if (edit->mirror_cache) {
    MEM_freeN(edit->mirror_cache);
    edit->mirror_cache = NULL;
  }

  edit->points = MEM_dupallocN(undo->points);
  edit->totpoint = undo->totpoint;

  LOOP_POINTS
  {
    point->keys = MEM_dupallocN(point->keys);
  }

  if (psys) {
    psys->particles = MEM_dupallocN(undo->particles);

    psys->totpart = undo->totpoint;

    LOOP_POINTS
    {
      pa = psys->particles + p;
      hkey = pa->hair = MEM_dupallocN(pa->hair);

      LOOP_KEYS
      {
        key->co = hkey->co;
        key->time = &hkey->time;
        hkey++;
      }
    }

    psys->flag = undo->psys_flag;
  }
  else {
    PTCacheMem *pm;
    int i;

    BKE_ptcache_free_mem(&edit->pid.cache->mem_cache);

    BLI_duplicatelist(&edit->pid.cache->mem_cache, &undo->mem_cache);

    pm = edit->pid.cache->mem_cache.first;

    for (; pm; pm = pm->next) {
      for (i = 0; i < BPHYS_TOT_DATA; i++) {
        pm->data[i] = MEM_dupallocN(pm->data[i]);
      }
      BKE_ptcache_mem_pointers_init(pm);

      LOOP_POINTS
      {
        LOOP_KEYS
        {
          if ((int)key->ftime == (int)pm->frame) {
            key->co = pm->cur[BPHYS_DATA_LOCATION];
            key->vel = pm->cur[BPHYS_DATA_VELOCITY];
            key->rot = pm->cur[BPHYS_DATA_ROTATION];
            key->time = &key->ftime;
          }
        }
        BKE_ptcache_mem_pointers_incr(pm);
      }
    }
  }
}

static void undoptcache_free_data(PTCacheUndo *undo)
{
  PTCacheEditPoint *point;
  int i;

  for (i = 0, point = undo->points; i < undo->totpoint; i++, point++) {
    if (undo->particles && (undo->particles + i)->hair) {
      MEM_freeN((undo->particles + i)->hair);
    }
    if (point->keys) {
      MEM_freeN(point->keys);
    }
  }
  if (undo->points) {
    MEM_freeN(undo->points);
  }
  if (undo->particles) {
    MEM_freeN(undo->particles);
  }
  BKE_ptcache_free_mem(&undo->mem_cache);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Implements ED Undo System
 * \{ */

typedef struct ParticleUndoStep {
  UndoStep step;
  UndoRefID_Scene scene_ref;
  UndoRefID_Object object_ref;
  PTCacheUndo data;
} ParticleUndoStep;

static bool particle_undosys_poll(struct bContext *C)
{
  Scene *scene = CTX_data_scene(C);
  ViewLayer *view_layer = CTX_data_view_layer(C);
  Object *ob = OBACT(view_layer);
  PTCacheEdit *edit = PE_get_current(scene, ob);

  return (edit != NULL);
}

static bool particle_undosys_step_encode(struct bContext *C,
                                         struct Main *UNUSED(bmain),
                                         UndoStep *us_p)
{
  ParticleUndoStep *us = (ParticleUndoStep *)us_p;
  ViewLayer *view_layer = CTX_data_view_layer(C);
  us->scene_ref.ptr = CTX_data_scene(C);
  us->object_ref.ptr = OBACT(view_layer);
  PTCacheEdit *edit = PE_get_current(us->scene_ref.ptr, us->object_ref.ptr);
  undoptcache_from_editcache(&us->data, edit);
  return true;
}

static void particle_undosys_step_decode(struct bContext *C,
                                         struct Main *UNUSED(bmain),
                                         UndoStep *us_p,
                                         int UNUSED(dir),
                                         bool UNUSED(is_final))
{
  /* TODO(campbell): undo_system: use low-level API to set mode. */
  ED_object_mode_set(C, OB_MODE_PARTICLE_EDIT);
  BLI_assert(particle_undosys_poll(C));

  ParticleUndoStep *us = (ParticleUndoStep *)us_p;
  Scene *scene = us->scene_ref.ptr;
  Object *ob = us->object_ref.ptr;
  PTCacheEdit *edit = PE_get_current(scene, ob);
  if (edit) {
    undoptcache_to_editcache(&us->data, edit);
    DEG_id_tag_update(&ob->id, ID_RECALC_GEOMETRY);
  }
  else {
    BLI_assert(0);
  }
}

static void particle_undosys_step_free(UndoStep *us_p)
{
  ParticleUndoStep *us = (ParticleUndoStep *)us_p;
  undoptcache_free_data(&us->data);
}

static void particle_undosys_foreach_ID_ref(UndoStep *us_p,
                                            UndoTypeForEachIDRefFn foreach_ID_ref_fn,
                                            void *user_data)
{
  ParticleUndoStep *us = (ParticleUndoStep *)us_p;
  foreach_ID_ref_fn(user_data, ((UndoRefID *)&us->scene_ref));
  foreach_ID_ref_fn(user_data, ((UndoRefID *)&us->object_ref));
}

/* Export for ED_undo_sys. */
void ED_particle_undosys_type(UndoType *ut)
{
  ut->name = "Edit Particle";
  ut->poll = particle_undosys_poll;
  ut->step_encode = particle_undosys_step_encode;
  ut->step_decode = particle_undosys_step_decode;
  ut->step_free = particle_undosys_step_free;

  ut->step_foreach_ID_ref = particle_undosys_foreach_ID_ref;

  ut->use_context = true;

  ut->step_size = sizeof(ParticleUndoStep);
}

/** \} */
