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

/** \file
 * \ingroup bke
 */

#include "BKE_main.hh"

#include "BLI_function_ref.hh"
#include "BLI_map.hh"
#include "BLI_utility_mixins.hh"

#include <string>

struct bContext;
struct BlendFileData;
struct BlendFileReadParams;
struct BlendFileReadReport;
struct BlendFileReadWMSetupData;
struct ID;
struct IDNameLib_Map;
struct Library;
struct LibraryIDLinkCallbackData;
struct MemFile;
struct ReportList;
struct UserDef;
struct WorkspaceConfigFileData;

/**
 * The suffix used for blend-files managed by the asset system.
 */
#define BLENDER_ASSET_FILE_SUFFIX ".asset.blend"

/**
 * Check whether given path ends with a blend file compatible extension
 * (`.blend`, `.ble` or `.blend.gz`).
 *
 * \param str: The path to check.
 * \return true is this path ends with a blender file extension.
 */
bool BKE_blendfile_extension_check(const char *str);
/**
 * Try to explode given path into its 'library components'
 * (i.e. a .blend file, id type/group, and data-block itself).
 *
 * \param path: the full path to explode.
 * \param r_dir: the string that'll contain path up to blend file itself ('library' path).
 * WARNING! Must be at least #FILE_MAX_LIBEXTRA long (it also stores group and name strings)!
 * \param r_group: a pointer within `r_dir` to the 'group' part of the path, if any ('\0'
 * terminated). May be NULL.
 * \param r_name: a pointer within `r_dir` to the data-block name, if any ('\0' terminated). May be
 * NULL.
 * \return true if path contains a blend file.
 */
bool BKE_blendfile_library_path_explode(const char *path,
                                        char *r_dir,
                                        char **r_group,
                                        char **r_name);

/**
 * Check whether a given path is actually a Blender-readable, valid .blend file.
 *
 * \note Currently does attempt to open and read (part of) the given file.
 */
bool BKE_blendfile_is_readable(const char *path, ReportList *reports);

/**
 * Shared setup function that makes the data from `bfd` into the current blend file,
 * replacing the contents of #G.main.
 * This uses the bfd returned by #BKE_blendfile_read and similarly named functions.
 *
 * This is done in a separate step so the caller may perform actions after it is known the file
 * loaded correctly but before the file replaces the existing blend file contents.
 */
void BKE_blendfile_read_setup_readfile(bContext *C,
                                       BlendFileData *bfd,
                                       const BlendFileReadParams *params,
                                       BlendFileReadWMSetupData *wm_setup_data,
                                       BlendFileReadReport *reports,
                                       bool startup_update_defaults,
                                       const char *startup_app_template);

/**
 * Simpler version of #BKE_blendfile_read_setup_readfile used when reading undo steps from
 * memfile.
 */
void BKE_blendfile_read_setup_undo(bContext *C,
                                   BlendFileData *bfd,
                                   const BlendFileReadParams *params,
                                   BlendFileReadReport *reports);

/**
 * \return Blend file data, this must be passed to
 * #BKE_blendfile_read_setup_readfile/#BKE_blendfile_read_setup_undo when non-NULL.
 */
BlendFileData *BKE_blendfile_read(const char *filepath,
                                  const BlendFileReadParams *params,
                                  BlendFileReadReport *reports);

/**
 * \return Blend file data, this must be passed to
 * #BKE_blendfile_read_setup_readfile/#BKE_blendfile_read_setup_undo when non-NULL.
 */
BlendFileData *BKE_blendfile_read_from_memory(const void *file_buf,
                                              int file_buf_size,
                                              const BlendFileReadParams *params,
                                              ReportList *reports);

/**
 * \return Blend file data, this must be passed to
 * #BKE_blendfile_read_setup_readfile/#BKE_blendfile_read_setup_undo when non-NULL.
 *
 * \note `memfile` is the undo buffer.
 */
BlendFileData *BKE_blendfile_read_from_memfile(Main *bmain,
                                               MemFile *memfile,
                                               const BlendFileReadParams *params,
                                               ReportList *reports);
/**
 * Utility to make a file 'empty' used for startup to optionally give an empty file.
 * Handy for tests.
 */
void BKE_blendfile_read_make_empty(bContext *C);

/**
 * Only read the #UserDef from a .blend.
 */
UserDef *BKE_blendfile_userdef_read(const char *filepath, ReportList *reports);
UserDef *BKE_blendfile_userdef_read_from_memory(const void *file_buf,
                                                int file_buf_size,
                                                ReportList *reports);
UserDef *BKE_blendfile_userdef_from_defaults();

/**
 * Only write the #UserDef in a `.blend`.
 * \return success.
 */
bool BKE_blendfile_userdef_write(const char *filepath, ReportList *reports);
/**
 * Only write the #UserDef in a `.blend`, merging with the existing blend file.
 * \return success.
 *
 * \note In the future we should re-evaluate user preferences,
 * possibly splitting out system/hardware specific preferences.
 */
bool BKE_blendfile_userdef_write_app_template(const char *filepath, ReportList *reports);

bool BKE_blendfile_userdef_write_all(ReportList *reports);

WorkspaceConfigFileData *BKE_blendfile_workspace_config_read(const char *filepath,
                                                             const void *file_buf,
                                                             int file_buf_size,
                                                             ReportList *reports);
void BKE_blendfile_workspace_config_data_free(WorkspaceConfigFileData *workspace_config);

namespace blender::bke::blendfile {

/**
 * Partial blendfile writing.
 *
 * This wrapper around the Main struct is designed to have a very short life span, during which it
 * will contain independent copies of the IDs that are added to it.
 *
 * In general, the #G_MAIN data should not change while such a context exists, otherwise mapping
 * info between the context content and the G_MAIN content cannot be kept up-to-date.
 *
 * The context can then be written to disk, and destroyed.
 *
 * It also has advanced ways to handle ID dependencies (and libraries for linked IDs), by allowing
 * specific handling for each dependency individually. By using the `dependencies_filter_cb`
 * optional parameter of #id_add, it is possible to skip (ignore) certain dependencies, or make
 * linked ones local in the context, etc.
 *
 * Design task: #122061
 */
class PartialWriteContext : NonCopyable, NonMovable {
 public:
  /** The temp Main itself, storing all IDs copied into this partial write context. */
  Main bmain = {};

 private:
  /**
   * The filepath that should be used as root for IDs _added_ to the context, when handling
   * remapping of their relative filepaths.
   *
   * Typically, the current G_MAIN's filepath.
   *
   * \note Currently always also copied into the temp `bmain.filepath`,
   * as this simplifies remapping of relative file-paths.
   * This may change in the future, if context can be loaded from external blend-files.
   */
  std::string reference_root_filepath_;
  /**
   * This mapping only contains entries for IDs in the context which have a known matching ID in
   * current G_MAIN.
   *
   * It is used to avoid adding several time a same ID (e.g. as a dependency of several other added
   * IDs).
   */
  IDNameLib_Map *matching_uid_map_;

  /** A mapping from the absolute library paths to the #Library IDs in the context. */
  blender::Map<std::string, Library *> libraries_map_;

 public:
  /* Passing a reference root filepath is mandatory, for remapping of relative paths to work as
   * expected. */
  PartialWriteContext() = delete;
  PartialWriteContext(StringRefNull reference_root_filepath);
  ~PartialWriteContext();

  /**
   * Control how to handle IDs and their dependencies when they are added to this context.
   *
   * \note For linked IDs, if #MAKE_LOCAL is not used, the library ID pointer is _not_ considered
   * nor handled as a regular dependency. Instead, the library is _always_ added to the context
   * data, and never duplicated. Also, library matching always happens based on absolute filepath.
   *
   * \warning Heterogeneous usages of these operations flags during a same PartialWriteContext
   * session may not generate expected results. Typically, once an ID has been added to the context
   * as 'matching' counterpart of the source Main (i.e. sharing the same session UID), it will not
   * be re-processed further if found again as dependency of another ID, or added explicitly as
   * root ID.
   * So e.g. if an ID is added (explicitly or implicitly) but none of its dependencies are (using
   * `CLEAR_DEPENDENCIES`), re-adding the same ID (explicitly or implicitly) with e.g.
   * `ADD_DEPENDENCIES` set will __not__ add its dependencies.
   * This is not expected to be an issue in current use-cases.
   */
  enum IDAddOperations {
    NOP = 0,
    /**
     * Do not keep linked info (library and/or liboverride references).
     *
     * \warning By default, when #ADD_DEPENDENCIES is defined, this will also apply to all
     * dependencies as well.
     *
     * \note Often required when only a small subset of the ID dependencies are also added to the
     * context (i.e. many of the added data's ID pointers are set to `nullptr`). Otherwise, some
     * areas not expecting nullptr (like LibOverride data) may assert or error on load of the
     * partial written blendfile.
     */
    MAKE_LOCAL = 1 << 0,
    /**
     * Set the 'fake user' flag to the added ID. Ensures that it is never auto-removed from the
     * context, and always written to disk.
     */
    SET_FAKE_USER = 1 << 1,
    /**
     * Set the 'clipboard' flag to the added ID. Ensures that it is treated as potential source
     * data for a 'paste ID' operation.
     */
    SET_CLIPBOARD_MARK = 1 << 4,

    /**
     * Clear all dependency IDs that are not in the partial write context. Mutually exclusive with
     * #ADD_DEPENDENCIES.
     *
     * WARNING: This also means that dependencies like obdata, shape-keys or actions are not
     * duplicated either.
     *
     * NOTE: Either #CLEAR_DEPENDENCIES or #ADD_DEPENDENCIES must be specified in the final
     * operation flags for all ID dependencies. This can be achieved by
     */
    CLEAR_DEPENDENCIES = 1 << 8,
    /**
     * Also add (or reuse if already there) dependency IDs into the partial write context. Mutually
     * exclusive with #CLEAR_DEPENDENCIES.
     */
    ADD_DEPENDENCIES = 1 << 9,
    /**
     * For each explicitly added IDs (i.e. these with a fake user), ensure all of their
     * dependencies are independent copies, instead of being shared with other explicitly added
     * IDs. Only relevant with #ADD_DEPENDENCIES.
     *
     * \warning Implies that the `session_uid` of these duplicated dependencies will be different
     * than their source data.
     */
    DUPLICATE_DEPENDENCIES = 1 << 10,

    /**
     * Operation flags that are (by default) inherited by all dependencies.
     *
     * \note This will be (partially) superseded by masked-out values from #MASK_PER_ID_USAGES
     * below.
     */
    MASK_INHERITED = (MAKE_LOCAL | CLEAR_DEPENDENCIES | ADD_DEPENDENCIES | DUPLICATE_DEPENDENCIES),
    /**
     * Operation flags that are defined by the #dependencies_filter_cb callback, if given.
     *
     * \note This mask is applied on top of the filter from #MASK_INHERITED, for ID dependencies
     * of explicitly added data.
     */
    MASK_PER_ID_USAGE = (MAKE_LOCAL | SET_FAKE_USER | SET_CLIPBOARD_MARK | CLEAR_DEPENDENCIES |
                         ADD_DEPENDENCIES),
  };
  /**
   * Options passed to the #id_add method.
   */
  struct IDAddOptions {
    IDAddOperations operations;
  };
  /**
   * Add a copy of the given ID to the partial write context.
   *
   * \note The duplicated ID will have the same session_uid as its source. In case a matching ID
   * already exists in the context, it is returned instead of duplicating it again.
   *
   * \param options: Control how the added ID (and its dependencies) are handled. See
   *        #IDAddOptions and #IDAddOperations above for details.
   *        If no #dependencies_filter_cb callback is specified, #options.operations must contain
   *        either #CLEAR_DEPENDENCIES or #ADD_DEPENDENCIES.
   * \param dependencies_filter_cb: Optional, a callback called for each ID usages, which returns
   *        specific operations flags for each ID usage.
   *        Currently, only accepted return values are the ones included in #MASK_PER_ID_USAGE.
   *        Returned flags must always contain either #CLEAR_DEPENDENCIES or #ADD_DEPENDENCIES.
   *
   * \return The pointer to the duplicated ID in the partial write context.
   */
  ID *id_add(const ID *id,
             IDAddOptions options,
             blender::FunctionRef<IDAddOperations(LibraryIDLinkCallbackData *cb_data,
                                                  IDAddOptions options)> dependencies_filter_cb =
                 nullptr);

  /**
   * Add and return a new ID into the partial write context.
   *
   * NOTE: Since this ID is _created_ in the partial write buffer, by definition it has no matching
   * counterpart in the current G_MAIN. Therefore, there is no need to add it to
   * #matching_uid_map_, and its `session_uid` is not guaranteed to be constant (as it may be
   * preempted later by another ID added from the current G_MAIN).
   *
   * \param options: Control how the created ID is handled. See #IDAddOptions and #IDAddOperations
   *        above for details, note that the only relevant operation flags currently are the
   *        #SET_FAKE_USER and #SET_CLIPBOARD_MARK ones.
   */
  ID *id_create(short id_type, StringRefNull id_name, Library *library, IDAddOptions options);

  /**
   * Delete the copy of the given ID from the partial write context.
   *
   * \note The search is based on the #ID.session_uid of the given ID. This means that if
   * `duplicate_depencies` option was used when adding the ID, these independent dependencies
   * duplicates cannot be removed directly from the context. Use #remove_unused for this.
   *
   * \note No dependencies will be removed. Use #remove_unused to remove all unused IDs from the
   * current context.
   */
  void id_delete(const ID *id);

  /**
   * Remove all unused IDs from the current context.
   *
   * \param clear_extra_user: If `true`, the runtime tag ensuring that IDs are written on disk will
   *        be cleared. In other words, only IDs flagged with 'fake user' and their dependencies
   *        will be kept. Allows to also remove IDs that were added to this context during the same
   *        editing session, and were not flagged as 'fake user'.
   */
  void remove_unused(bool clear_extra_user = false);

  /**
   * Fully empty the partial write context.
   */
  void clear();

  /**
   * Debug: Check if the current partial write context is fully valid.
   *
   * Currently, check if any ID in the context still has relations to IDs not in the context.
   *
   * \return false if the context is invalid.
   */
  bool is_valid();

  /**
   * Write the content of the current context as a blendfile on disk.
   *
   * \return `true` on success.
   */
  bool write(const char *write_filepath, int write_flags, int remap_mode, ReportList &reports);
  bool write(const char *write_filepath, ReportList &reports);

  /* TODO: To allow editing an existing external blendfile:
   *   - API to load a context from a blendfile.
   *   - API to 'match' a context's content with another Main database's content (based on ID
   *     names and libraries).
   *   - API to replace the matching context IDs by a 'new version' (similar to 'add_id', but
   *     ensuring that the context ID, if it already exists, is a pristine copy of the given source
   *     one).
   *   - Rework the remapping of relative filepaths, since data already existing in the
   *     loaded-from-disk temp context will have different root-path than the data from current
   *     G_MAIN.
   */

 private:
  /**
   * In case an explicitly added ID has the same session_uid as an existing one in current
   * context, the added one should be able to 'steal' that session_uid in the context, and
   * re-assign a new one to the other ID.
   */
  void preempt_session_uid(ID *ctx_id, unsigned int session_uid);
  /**
   * Ensures that given ID will be written on disk (within current context), by either setting the
   * 'fake user' flag, or the (runtime-only, cleared on next file load) 'extra user' tag, depending
   * on whether #SET_FAKE_USER is set or not.
   *
   * Also handles the setting of the #ID_FLAG_CLIPBOARD_MARK flag if #SET_CLIPBOARD_MARK is set.
   */
  void process_added_id(ID *ctx_id, const IDAddOperations operations);
  /**
   * Utils for #PartialWriteContext::id_add, only adds (duplicate) the given source ID into
   * current context.
   */
  ID *id_add_copy(const ID *id, bool regenerate_session_uid);
  /** Make given context ID local to the context. */
  void make_local(ID *ctx_id, int make_local_flags);
  /**
   * Ensure that the given ID's library has a matching Library ID in the context, copying the
   * current `ctx_id->lib` one if needed.
   */
  Library *ensure_library(ID *ctx_id);
  /**
   * Ensure that the given library path has a matching Library ID in the context, creating a new
   * one if needed.
   */
  Library *ensure_library(StringRefNull library_absolute_path);
};

ENUM_OPERATORS(PartialWriteContext::IDAddOperations,
               PartialWriteContext::IDAddOperations::MASK_INHERITED);

}  // namespace blender::bke::blendfile
