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

#include <sstream>

#include "BKE_bake_geometry_nodes_modifier.hh"
#include "BKE_collection.h"
#include "BKE_curves.hh"
#include "BKE_main.h"

#include "DNA_modifier_types.h"
#include "DNA_node_types.h"
#include "DNA_pointcloud_types.h"

#include "BLI_binary_search.hh"
#include "BLI_fileops.hh"
#include "BLI_hash_md5.h"
#include "BLI_path_util.h"
#include "BLI_string.h"
#include "BLI_string_utils.h"

#include "MOD_nodes.hh"

namespace blender::bke::bake {

void NodeCache::reset()
{
  std::destroy_at(this);
  new (this) NodeCache();
}
void scene_simulation_states_reset(Scene &scene)
{
  FOREACH_SCENE_OBJECT_BEGIN (&scene, ob) {
    LISTBASE_FOREACH (ModifierData *, md, &ob->modifiers) {
      if (md->type != eModifierType_Nodes) {
        continue;
      }
      NodesModifierData *nmd = reinterpret_cast<NodesModifierData *>(md);
      if (!nmd->runtime->cache) {
        continue;
      }
      for (auto item : nmd->runtime->cache->cache_by_id.items()) {
        item.value->reset();
      }
    }
  }
  FOREACH_SCENE_OBJECT_END;
}

std::optional<std::string> get_modifier_bake_path(const Main &bmain,
                                                  const Object &object,
                                                  const NodesModifierData &nmd)
{
  const StringRefNull bmain_path = BKE_main_blendfile_path(&bmain);
  if (bmain_path.is_empty()) {
    return std::nullopt;
  }
  if (StringRef(nmd.simulation_bake_directory).is_empty()) {
    return std::nullopt;
  }
  const char *base_path = ID_BLEND_PATH(&bmain, &object.id);
  char absolute_bake_dir[FILE_MAX];
  STRNCPY(absolute_bake_dir, nmd.simulation_bake_directory);
  BLI_path_abs(absolute_bake_dir, base_path);
  return absolute_bake_dir;
}

std::optional<bake::BakePath> get_node_bake_path(const Main &bmain,
                                                 const Object &object,
                                                 const NodesModifierData &nmd,
                                                 int node_id)
{
  const NodesModifierBake *bake = nmd.find_bake(node_id);
  if (bake == nullptr) {
    return std::nullopt;
  }
  if (bake->flag & NODES_MODIFIER_BAKE_CUSTOM_PATH) {
    if (StringRef(bake->directory).is_empty()) {
      return std::nullopt;
    }
    const char *base_path = ID_BLEND_PATH(&bmain, &object.id);
    char absolute_bake_dir[FILE_MAX];
    STRNCPY(absolute_bake_dir, bake->directory);
    BLI_path_abs(absolute_bake_dir, base_path);
    return bake::BakePath::from_single_root(absolute_bake_dir);
  }
  const std::optional<std::string> modifier_bake_path = get_modifier_bake_path(bmain, object, nmd);
  if (!modifier_bake_path) {
    return std::nullopt;
  }
  char bake_dir[FILE_MAX];
  BLI_path_join(
      bake_dir, sizeof(bake_dir), modifier_bake_path->c_str(), std::to_string(node_id).c_str());
  return bake::BakePath::from_single_root(bake_dir);
}

static IndexRange fix_frame_range(const int start, const int end)
{
  const int num_frames = std::max(1, end - start + 1);
  return IndexRange(start, num_frames);
}

std::optional<IndexRange> get_node_bake_frame_range(const Scene &scene,
                                                    const Object & /*object*/,
                                                    const NodesModifierData &nmd,
                                                    int node_id)
{
  const NodesModifierBake *bake = nmd.find_bake(node_id);
  if (bake == nullptr) {
    return std::nullopt;
  }
  if (bake->flag & NODES_MODIFIER_BAKE_CUSTOM_SIMULATION_FRAME_RANGE) {
    return fix_frame_range(bake->frame_start, bake->frame_end);
  }
  if (scene.flag & SCE_CUSTOM_SIMULATION_RANGE) {
    return fix_frame_range(scene.simulation_frame_start, scene.simulation_frame_end);
  }
  return fix_frame_range(scene.r.sfra, scene.r.efra);
}

/**
 * Turn the name into something that can be used as file name. It does not necessarily have to be
 * human readable, but it can help if it is at least partially readable.
 */
static std::string escape_name(const StringRef name)
{
  std::stringstream ss;
  for (const char c : name) {
    /* Only some letters allowed. Digits are not because they could lead to name collisions. */
    if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z')) {
      ss << c;
    }
    else {
      ss << int(c);
    }
  }
  return ss.str();
}

static std::string get_blend_file_name(const Main &bmain)
{
  const StringRefNull blend_file_path = BKE_main_blendfile_path(&bmain);
  char blend_name[FILE_MAX];

  BLI_path_split_file_part(blend_file_path.c_str(), blend_name, sizeof(blend_name));
  const int64_t type_start_index = StringRef(blend_name).rfind(".");
  if (type_start_index == StringRef::not_found) {
    return "";
  }
  blend_name[type_start_index] = '\0';
  return "blendcache_" + StringRef(blend_name);
}

static std::string get_modifier_directory_name(const Object &object, const ModifierData &md)
{
  const std::string object_name_escaped = escape_name(object.id.name + 2);
  const std::string modifier_name_escaped = escape_name(md.name);
  return object_name_escaped + "_" + modifier_name_escaped;
}

std::string get_default_modifier_bake_directory(const Main &bmain,
                                                const Object &object,
                                                const NodesModifierData &nmd)
{
  char dir[FILE_MAX];
  /* Make path that's relative to the .blend file. */
  BLI_path_join(dir,
                sizeof(dir),
                "//",
                get_blend_file_name(bmain).c_str(),
                get_modifier_directory_name(object, nmd.modifier).c_str());
  return dir;
}

}  // namespace blender::bke::bake
