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

/** \file
 * \ingroup asset_system
 */

#pragma once

#include <map>
#include <memory>
#include <set>
#include <string>

#include "BLI_function_ref.hh"
#include "BLI_map.hh"
#include "BLI_set.hh"
#include "BLI_string_ref.hh"
#include "BLI_uuid.h"
#include "BLI_vector.hh"

#include "AS_asset_catalog_path.hh"

namespace blender::asset_system {

class AssetCatalog;
class AssetCatalogCollection;
class AssetCatalogDefinitionFile;
class AssetCatalogFilter;
class AssetCatalogTree;

using CatalogID = bUUID;
using CatalogPathComponent = std::string;
/* Would be nice to be able to use `std::filesystem::path` for this, but it's currently not
 * available on the minimum macOS target version. */
using CatalogFilePath = std::string;
using OwningAssetCatalogMap = Map<CatalogID, std::unique_ptr<AssetCatalog>>;

/* Manages the asset catalogs of a single asset library (i.e. of catalogs defined in a single
 * directory hierarchy). */
class AssetCatalogService {
  std::unique_ptr<AssetCatalogCollection> catalog_collection_;
  std::unique_ptr<AssetCatalogTree> catalog_tree_;
  CatalogFilePath asset_library_root_;

  Vector<std::unique_ptr<AssetCatalogCollection>> undo_snapshots_;
  Vector<std::unique_ptr<AssetCatalogCollection>> redo_snapshots_;

  const bool is_read_only_ = false;

 public:
  static const CatalogFilePath DEFAULT_CATALOG_FILENAME;

  struct read_only_tag {
  };

 public:
  AssetCatalogService();
  explicit AssetCatalogService(read_only_tag);
  explicit AssetCatalogService(const CatalogFilePath &asset_library_root);

  /**
   * Set tag indicating that some catalog modifications are unsaved, which could
   * get lost on exit. This tag is not set by internal catalog code, the catalog
   * service user is responsible for it. It is cleared by #write_to_disk().
   *
   * This "dirty" state is tracked per catalog, so that it's possible to gracefully load changes
   * from disk. Any catalog with unsaved changes will not be overwritten by on-disk changes. */
  void tag_has_unsaved_changes(AssetCatalog *edited_catalog);
  bool has_unsaved_changes() const;

  /**
   * Check if this is a read-only service meaning the user shouldn't be able to do edits. This is
   * not enforced by internal catalog code, the catalog service user is responsible for it. For
   * example the UI should disallow edits.
   */
  bool is_read_only() const;

  /** Load asset catalog definitions from the files found in the asset library. */
  void load_from_disk();
  /** Load asset catalog definitions from the given file or directory. */
  void load_from_disk(const CatalogFilePath &file_or_directory_path);

  /**
   * Duplicate the catalogs from \a other_service into this one. Does not rebuild the tree, this
   * needs to be done by the caller (call #rebuild_tree()!).
   */
  void add_from_existing(const AssetCatalogService &other_service);

  /**
   * Write the catalog definitions to disk.
   *
   * The location where the catalogs are saved is variable, and depends on the location of the
   * blend file. The first matching rule wins:
   *
   * - Already loaded a CDF from disk?
   *    -> Always write to that file.
   * - The directory containing the blend file has a blender_assets.cats.txt file?
   *    -> Merge with & write to that file.
   * - The directory containing the blend file is part of an asset library, as per
   *   the user's preferences?
   *    -> Merge with & write to ${ASSET_LIBRARY_ROOT}/blender_assets.cats.txt
   * - Create a new file blender_assets.cats.txt next to the blend file.
   *
   * Return true on success, which either means there were no in-memory categories to save,
   * or the save was successful. */
  bool write_to_disk(const CatalogFilePath &blend_file_path);

  /**
   * Ensure that the next call to #on_blend_save_post() will choose a new location for the CDF
   * suitable for the location of the blend file (regardless of where the current catalogs come
   * from), and that catalogs will be merged with already-existing ones in that location.
   *
   * Use this for a "Save as..." that has to write the catalogs to the new blend file location,
   * instead of updating the previously read CDF. */
  void prepare_to_merge_on_write();

  /**
   * Merge on-disk changes into the in-memory asset catalogs.
   * This should be called before writing the asset catalogs to disk.
   *
   * - New on-disk catalogs are loaded into memory.
   * - Already-known on-disk catalogs are ignored (so will be overwritten with our in-memory
   *   data). This includes in-memory marked-as-deleted catalogs.
   */
  void reload_catalogs();

  /**
   * Make sure the tree is updated to the latest collection of catalogs stored in this service.
   * Does not depend on a CDF file being available so this can be called on a service that stores
   * catalogs that are not stored in a CDF.
   * Most API functions that modify catalog data will trigger this, unless otherwise specified (for
   * batch operations).
   */
  void rebuild_tree();

  /** Return catalog with the given ID. Return nullptr if not found. */
  AssetCatalog *find_catalog(CatalogID catalog_id) const;

  /**
   * Return first catalog with the given path. Return nullptr if not found. This is not an
   * efficient call as it's just a linear search over the catalogs.
   *
   * If there are multiple catalogs with the same path, return the first-loaded one. If there is
   * none marked as "first loaded", return the one with the lowest UUID. */
  AssetCatalog *find_catalog_by_path(const AssetCatalogPath &path) const;

  /**
   * Return true only if this catalog is known.
   * This treats deleted catalogs as "unknown". */
  bool is_catalog_known(CatalogID catalog_id) const;

  /**
   * Create a filter object that can be used to determine whether an asset belongs to the given
   * catalog, or any of the catalogs in the sub-tree rooted at the given catalog.
   *
   * \see #AssetCatalogFilter
   */
  AssetCatalogFilter create_catalog_filter(CatalogID active_catalog_id) const;

  /** Create a catalog with some sensible auto-generated catalog ID.
   * The catalog will be saved to the default catalog file. */
  AssetCatalog *create_catalog(const AssetCatalogPath &catalog_path);

  /**
   * Delete all catalogs with the given path, and their children.
   */
  void prune_catalogs_by_path(const AssetCatalogPath &path);

  /**
   * Delete all catalogs with the same path as the identified catalog, and their children.
   * This call is the same as calling `prune_catalogs_by_path(find_catalog(catalog_id)->path)`.
   */
  void prune_catalogs_by_id(CatalogID catalog_id);

  /**
   * Update the catalog path, also updating the catalog path of all sub-catalogs.
   */
  void update_catalog_path(CatalogID catalog_id, const AssetCatalogPath &new_catalog_path);

  AssetCatalogTree *get_catalog_tree();

  /** Return true only if there are no catalogs known. */
  bool is_empty() const;

  /**
   * Store the current catalogs in the undo stack.
   * This snapshots everything in the #AssetCatalogCollection. */
  void undo_push();
  /**
   * Restore the last-saved undo snapshot, pushing the current state onto the redo stack.
   * The caller is responsible for first checking that undoing is possible.
   */
  void undo();
  bool is_undo_possbile() const;
  /**
   * Restore the last-saved redo snapshot, pushing the current state onto the undo stack.
   * The caller is responsible for first checking that undoing is possible. */
  void redo();
  bool is_redo_possbile() const;

 protected:
  void load_directory_recursive(const CatalogFilePath &directory_path);
  void load_single_file(const CatalogFilePath &catalog_definition_file_path);

  /** Implementation of #write_to_disk() that doesn't clear the "has unsaved changes" tag. */
  bool write_to_disk_ex(const CatalogFilePath &blend_file_path);
  void untag_has_unsaved_changes();
  bool is_catalog_known_with_unsaved_changes(CatalogID catalog_id) const;

  /**
   * Delete catalogs, only keeping them when they are either listed in
   * \a catalogs_to_keep or have unsaved changes.
   *
   * \note Deleted catalogs are hard-deleted, i.e. they just vanish instead of
   * remembering them as "deleted".
   */
  void purge_catalogs_not_listed(const Set<CatalogID> &catalogs_to_keep);

  /**
   * Delete a catalog, without deleting any of its children and without rebuilding the catalog
   * tree. The deletion in "Soft", in the sense that the catalog pointer is moved from `catalogs_`
   * to `deleted_catalogs_`; the AssetCatalog instance itself is kept in memory. As a result, it
   * will be removed from a CDF when saved to disk.
   *
   * This is a lower-level function than #prune_catalogs_by_path.
   */
  void delete_catalog_by_id_soft(CatalogID catalog_id);

  /**
   * Hard delete a catalog. This simply removes the catalog from existence. The deletion will not
   * be remembered, and reloading the CDF will bring it back. */
  void delete_catalog_by_id_hard(CatalogID catalog_id);

  std::unique_ptr<AssetCatalogDefinitionFile> parse_catalog_file(
      const CatalogFilePath &catalog_definition_file_path);

  /**
   * Construct an in-memory catalog definition file (CDF) from the currently known catalogs.
   * This object can then be processed further before saving to disk. */
  std::unique_ptr<AssetCatalogDefinitionFile> construct_cdf_in_memory(
      const CatalogFilePath &file_path);

  /**
   * Find a suitable path to write a CDF to.
   *
   * This depends on the location of the blend file, and on whether a CDF already exists next to it
   * or whether the blend file is saved inside an asset library.
   */
  static CatalogFilePath find_suitable_cdf_path_for_writing(
      const CatalogFilePath &blend_file_path);

  std::unique_ptr<AssetCatalogTree> read_into_tree();

  /**
   * For every catalog, ensure that its parent path also has a known catalog.
   */
  void create_missing_catalogs();

  /**
   * For every catalog, mark it as "dirty".
   */
  void tag_all_catalogs_as_unsaved_changes();

  /* For access by subclasses, as those will not be marked as friend by #AssetCatalogCollection. */
  AssetCatalogDefinitionFile *get_catalog_definition_file();
  OwningAssetCatalogMap &get_catalogs();
  OwningAssetCatalogMap &get_deleted_catalogs();
};

/**
 * All catalogs that are owned by a single asset library, and managed by a single instance of
 * #AssetCatalogService. The undo system for asset catalog edits contains historical copies of this
 * struct.
 */
class AssetCatalogCollection {
 protected:
  /** All catalogs known, except the known-but-deleted ones. */
  OwningAssetCatalogMap catalogs_;

  /** Catalogs that have been deleted. They are kept around so that the load-merge-save of catalog
   * definition files can actually delete them if they already existed on disk (instead of the
   * merge operation resurrecting them). */
  OwningAssetCatalogMap deleted_catalogs_;

  /* For now only a single catalog definition file is supported.
   * The aim is to support an arbitrary number of such files per asset library in the future. */
  std::unique_ptr<AssetCatalogDefinitionFile> catalog_definition_file_;

  /** Whether any of the catalogs have unsaved changes. */
  bool has_unsaved_changes_ = false;

  friend AssetCatalogService;

 public:
  AssetCatalogCollection() = default;
  AssetCatalogCollection(const AssetCatalogCollection &other) = delete;
  AssetCatalogCollection(AssetCatalogCollection &&other) noexcept = default;

  std::unique_ptr<AssetCatalogCollection> deep_copy() const;
  /**
   * Copy the catalogs from \a other and append them to this collection. Copies no other data
   * otherwise.
   */
  void add_catalogs_from_existing(const AssetCatalogCollection &other);

 protected:
  static OwningAssetCatalogMap copy_catalog_map(const OwningAssetCatalogMap &orig);
};

/** Keeps track of which catalogs are defined in a certain file on disk.
 * Only contains non-owning pointers to the #AssetCatalog instances, so ensure the lifetime of this
 * class is shorter than that of the #`AssetCatalog`s themselves. */
class AssetCatalogDefinitionFile {
 protected:
  /* Catalogs stored in this file. They are mapped by ID to make it possible to query whether a
   * catalog is already known, without having to find the corresponding `AssetCatalog*`. */
  Map<CatalogID, AssetCatalog *> catalogs_;

 public:
  /* For now this is the only version of the catalog definition files that is supported.
   * Later versioning code may be added to handle older files. */
  const static int SUPPORTED_VERSION;
  /* String that's matched in the catalog definition file to know that the line is the version
   * declaration. It has to start with a space to ensure it won't match any hypothetical future
   * field that starts with "VERSION". */
  const static std::string VERSION_MARKER;
  const static std::string HEADER;

  CatalogFilePath file_path;

 public:
  AssetCatalogDefinitionFile() = default;

  /**
   * Write the catalog definitions to the same file they were read from.
   * Return true when the file was written correctly, false when there was a problem.
   */
  bool write_to_disk() const;
  /**
   * Write the catalog definitions to an arbitrary file path.
   *
   * Any existing file is backed up to "filename~". Any previously existing backup is overwritten.
   *
   * Return true when the file was written correctly, false when there was a problem.
   */
  bool write_to_disk(const CatalogFilePath &dest_file_path) const;

  bool contains(CatalogID catalog_id) const;
  /** Add a catalog, overwriting the one with the same catalog ID. */
  void add_overwrite(AssetCatalog *catalog);
  /** Add a new catalog. Undefined behavior if a catalog with the same ID was already added. */
  void add_new(AssetCatalog *catalog);

  /** Remove the catalog from the collection of catalogs stored in this file. */
  void forget(CatalogID catalog_id);

  using AssetCatalogParsedFn = FunctionRef<bool(std::unique_ptr<AssetCatalog>)>;
  void parse_catalog_file(const CatalogFilePath &catalog_definition_file_path,
                          AssetCatalogParsedFn callback);

  std::unique_ptr<AssetCatalogDefinitionFile> copy_and_remap(
      const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const;

 protected:
  bool parse_version_line(StringRef line);
  std::unique_ptr<AssetCatalog> parse_catalog_line(StringRef line);

  /**
   * Write the catalog definitions to the given file path.
   * Return true when the file was written correctly, false when there was a problem.
   */
  bool write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const;
  bool ensure_directory_exists(const CatalogFilePath directory_path) const;
};

/**
 * Asset Catalog definition, containing a symbolic ID and a path that points to a node in the
 * catalog hierarchy.
 *
 * \warning The asset system may reload catalogs, invalidating pointers. Thus it's not recommended
 *          to store pointers to asset catalogs. Store the #CatalogID instead and do a lookup when
 *          needed.
 */
class AssetCatalog {
 public:
  CatalogID catalog_id;
  AssetCatalogPath path;
  /**
   * Simple, human-readable name for the asset catalog. This is stored on assets alongside the
   * catalog ID; the catalog ID is a UUID that is not human-readable,
   * so to avoid complete data-loss when the catalog definition file gets lost,
   * we also store a human-readable simple name for the catalog.
   *
   * It should fit in sizeof(AssetMetaData::catalog_simple_name) bytes. */
  std::string simple_name;

  struct Flags {
    /* Treat this catalog as deleted. Keeping deleted catalogs around is necessary to support
     * merging of on-disk changes with in-memory changes. */
    bool is_deleted = false;

    /* Sort this catalog first when there are multiple catalogs with the same catalog path. This
     * ensures that in a situation where missing catalogs were auto-created, and then
     * load-and-merged with a file that also has these catalogs, the first one in that file is
     * always sorted first, regardless of the sort order of its UUID. */
    bool is_first_loaded = false;

    /* Merging on-disk changes into memory will not overwrite this catalog.
     * For example, when a catalog was renamed (i.e. changed path) in this Blender session,
     * reloading the catalog definition file should not overwrite that change.
     *
     * Note that this flag is ignored when is_deleted=true; deleted catalogs that are still in
     * memory are considered "unsaved" by definition. */
    bool has_unsaved_changes = false;
  } flags;

 public:
  AssetCatalog() = default;
  AssetCatalog(CatalogID catalog_id, const AssetCatalogPath &path, const std::string &simple_name);

  /**
   * Create a new Catalog with the given path, auto-generating a sensible catalog simple-name.
   *
   * NOTE: the given path will be cleaned up (trailing spaces removed, etc.), so the returned
   * `AssetCatalog`'s path differ from the given one.
   */
  static std::unique_ptr<AssetCatalog> from_path(const AssetCatalogPath &path);

  /** Make a new simple name for the catalog, based on its path. */
  void simple_name_refresh();

 protected:
  /** Generate a sensible catalog ID for the given path. */
  static std::string sensible_simple_name_for_path(const AssetCatalogPath &path);
};

/** Comparator for asset catalogs, ordering by (path, first_seen, UUID). */
struct AssetCatalogLessThan {
  bool operator()(const AssetCatalog *lhs, const AssetCatalog *rhs) const
  {
    if (lhs->path != rhs->path) {
      return lhs->path < rhs->path;
    }

    if (lhs->flags.is_first_loaded != rhs->flags.is_first_loaded) {
      return lhs->flags.is_first_loaded;
    }

    return lhs->catalog_id < rhs->catalog_id;
  }
};

/**
 * Set that stores catalogs ordered by (path, UUID).
 * Being a set, duplicates are removed. The catalog's simple name is ignored in this. */
using AssetCatalogOrderedSet = std::set<const AssetCatalog *, AssetCatalogLessThan>;
using MutableAssetCatalogOrderedSet = std::set<AssetCatalog *, AssetCatalogLessThan>;

/**
 * Filter that can determine whether an asset should be visible or not, based on its catalog ID.
 *
 * \see AssetCatalogService::create_catalog_filter()
 */
class AssetCatalogFilter {
  const Set<CatalogID> matching_catalog_ids_;
  const Set<CatalogID> known_catalog_ids_;

  friend AssetCatalogService;

 public:
  bool contains(CatalogID asset_catalog_id) const;

  /* So that all unknown catalogs can be shown under "Unassigned". */
  bool is_known(CatalogID asset_catalog_id) const;

 protected:
  explicit AssetCatalogFilter(Set<CatalogID> &&matching_catalog_ids,
                              Set<CatalogID> &&known_catalog_ids);
};

}  // namespace blender::asset_system
