/*╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
 * database/ObjectStoreWrapper.h is part of Brewtarget, and is copyright the following authors 2021-2026:
 *   • Matt Young <mfsy@yahoo.com>
 *
 * Brewtarget 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 3 of the License, or (at your option) any later
 * version.
 *
 * Brewtarget 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, see
 * <http://www.gnu.org/licenses/>.
 ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌*/
#ifndef DATABASE_OBJECTSTOREWRAPPER_H
#define DATABASE_OBJECTSTOREWRAPPER_H
#pragma once
#include "database/ObjectStoreTyped.h"

/**
 * \brief Namespace containing convenience functions for accessing member functions of appropriate ObjectStoreTyped
 *        instances via template argument deduction
 *
 *        Where functions below are not commented, the documentation is in the corresponding function declaration in
 *        ObjectStoreTyped.h
 */
namespace ObjectStoreWrapper {

   /**
    * \brief Log diagnostics about the ObjectStore (for this type of object).  Usually only needed for debugging double-
    *        free issues...
    */
   template<class NE> void logDiagnostics() {
      ObjectStoreTyped<NE>::getInstance().logDiagnostics();
      return;
   }

   /**
    * \brief Determines whether an object of the specified ID exists in the ObjectStore (for this type of object)
    */
   template<class NE> bool contains(int id) {
      return ObjectStoreTyped<NE>::getInstance().contains(id);
   }

   /**
    * \brief Get a shared pointer to an object from its database ID.
    *        Note that it is a coding error to call this for an ID that is not stored in the ObjectStore.  If it is not
    *        certain that an ID is present, then \c contains() must be called first, to check.
    *
    * \return A shared pointer copied from the ObjectStore's "master" shared pointer for this object
    */
   template<class NE> std::shared_ptr<NE> getById(int id) {
      return ObjectStoreTyped<NE>::getInstance().getById(id);
   }

   /**
    * \brief Raw pointer version of \c getById
    */
   template<class NE> NE * getByIdRaw(int id) {
      return ObjectStoreTyped<NE>::getInstance().getById(id).get();
   }

   /**
    * \brief Similar to \c getById but returns a list of cached objects matching a supplied list of IDs
    */
   template<class NE> QList<std::shared_ptr<NE> > getByIds(QVector<int> const & listOfIds) {
      return ObjectStoreTyped<NE>::getInstance().getByIds(listOfIds);
   }

   template<class NE> QList<std::shared_ptr<NE> > getAll() {
      return ObjectStoreTyped<NE>::getInstance().getAll();
   }

   //! \brief Raw pointer version
   template<class NE> QList<NE *> getAllRaw() {
      return ObjectStoreTyped<NE>::getInstance().getAllRaw();
   }

   /**
    * \brief Gets only those objects which are not marked deleted and are not "subsidiary" items (ie, for Recipes, are
    *        not prior versions -- aka ancestors -- of a \c Recipe).
    */
   template<class NE> QList<std::shared_ptr<NE>> getAllDisplayable() {
      return ObjectStoreTyped<NE>::getInstance().findAllMatching(
         [](NE const & ne) { return !ne.deleted() && !ne.subsidiary(); }
      );
   }

   //! \brief Raw pointer version
   template<class NE> QList<NE *> getAllDisplayableRaw() {
      return ObjectStoreTyped<NE>::getInstance().findAllMatching(
         [](NE const * ne) { return !ne->deleted() && !ne->subsidiary(); }
      );
   }

   /**
    * \brief Given a raw pointer to an object, gets either a copy of the ObjectStore's "master" shared pointer for that
    *        object (if the object is stored in an ObjectStore), or a new shared pointer (if the object is not stored in
    *        an ObjectStore)
    *
    * \param obj  Pointer to the object, which (a) must not be null and (b) must already be stored in the ObjectStore
    */
   template<class NE> std::shared_ptr<NE> getSharedFromRaw(NE * const ne) {
      int const id = ne->key();
      if (ObjectStoreTyped<NE> & objectStore = ObjectStoreTyped<NE>::getInstance();
          id > 0 && objectStore.contains(id)) {
         return objectStore.getById(id);
      }
      // If the object isn't stored in the DB then we can create a shared pointer for it, but this is dangerous as there
      // might already be another shared pointer to it.  At minimum, we should log a warning.  In the long run we should
      // Q_ASSERT(false) here.
      qWarning() <<
         Q_FUNC_INFO << "Creating new shared_ptr for unstored" << ne->metaObject()->className() << "#" << id << " :" <<
         ne->name() << ".  This may be a bug - eg if a shared_ptr already exists for this object!";
      return std::shared_ptr<NE>{ne};
   }

   template<class NE> std::shared_ptr<NE> getShared(NE const & ne) {
      ObjectStoreTyped<NE> & objectStore = ObjectStoreTyped<NE>::getInstance();
      return objectStore.getById(ne.key());
   }

   /**
    * \brief Makes a \b new \b unsaved object that is a copy of the supplied one.  NB: Use \c insertCopyOf to make a
    *        \c saved copy of an existing object.
    *
    * \return Shared pointer to the new object
    */
   template<class NE> std::shared_ptr<NE> unstoredCopy(NE const & ne) {
      return std::make_shared<NE>(ne);
   }

   /**
    * \brief Preferred way of inserting a new object in a store.
    *
    *        Caller creates a new object in a shared_ptr and, if and when the caller decides we want to keep (rather
    *        than discard) this new object, calls this function, which will store data in DB and make a copy of the
    *        shared_ptr inside the ObjectStore.   This means the object will be properly destroyed, without
    *        additional coding, in all cases: (i) object intentionally discarded, (ii) object stored, (iii) ghastly
    *        error happens causing exceptions etc.
    *
    * \return The ID of what was inserted
    */
   template<class NE> int insert(std::shared_ptr<NE> & ne) {
      return ObjectStoreTyped<NE>::getInstance().insert(ne);
   }

   /**
    * \brief We don't want people to insert without having a shared_ptr, because it creates numerous problems:
    *          - If the caller's object goes out of scope and gets destructed, the object we have in the ObjectStore is
    *            no longer valid.
    *          - If a caller _does_ have a shared_ptr to the object but inadvertently passes us the reference to the
    *            object itself then the new shared_ptr we create will not know about the caller's shared_ptr, so the
    *            object's destructor will get called twice and, sooner or later, we'll get a segfault.
    */
   template<class NE> int insert(NE & ne) = delete;
   template<class NE> int insert(NE * ne) = delete;

   template<class NE> std::shared_ptr<NE> insertCopyOf(NE const & ne) {
      return ObjectStoreTyped<NE>::getInstance().insertCopyOf(ne);
   }

   template<class NE> void update(NE & ne) {
      ObjectStoreTyped<NE>::getInstance().update(static_cast<QObject &>(ne));
      return;
   }

   template<class NE> int insertOrUpdate(NE & ne) {
      qWarning() << Q_FUNC_INFO << "Deprecated function";
      return ObjectStoreTyped<NE>::getInstance().insertOrUpdate(static_cast<QObject &>(ne));
   }

   template<class NE> void updateProperty(NE const & ne, BtStringConst const & propertyName) {
      ObjectStoreTyped<NE>::getInstance().updateProperty(ne, propertyName);
      return;
   }

   template<class NE> std::shared_ptr<NE> softDelete(NE const & ne) requires (std::is_base_of_v<NamedEntity, NE>) {
      return ObjectStoreTyped<NE>::getInstance().softDelete(ne.key());
   }

   template<class NE> QList<std::shared_ptr<NE>> softDelete(QList<int> const & ids) {
      return ObjectStoreTyped<NE>::getInstance().softDelete(ids);
   }

   template<class NE> std::shared_ptr<NE> hardDelete(int id) {
      return ObjectStoreTyped<NE>::getInstance().hardDelete(id);
   }

   template<class NE> std::shared_ptr<NE> hardDelete(NE const & ne) requires (std::is_base_of_v<NamedEntity, NE>) {
      return ObjectStoreTyped<NE>::getInstance().hardDelete(ne.key());
   }

   template<class NE> std::shared_ptr<NE> hardDelete(std::shared_ptr<NE> ne) requires (std::is_base_of_v<NamedEntity, NE>) {
      return ObjectStoreTyped<NE>::getInstance().hardDelete(ne->key());
   }

   /**
    * \brief Search the set of all cached objects with a lambda.
    *
    * \param matchFunction Takes a pointer to an object and returns \c true if the object is a match or \c false otherwise.
    *
    * \return Shared pointer to the first object that gives a \c true result to \c matchFunction, or \c nullptr if none
    *         does.
    */
   template<class NE> std::shared_ptr<NE> findFirstMatching(
      std::function<bool(std::shared_ptr<NE>)> const & matchFunction
   ) {
      return ObjectStoreTyped<NE>::getInstance().findFirstMatching(matchFunction);
   }

   /**
    * \brief Alternate version of \c findFirstMatching that uses raw pointers
    *
    * \return Pointer to the first object that gives a \c true result to \c matchFunction, or \c nullptr if
    *         none does
    */
   template<class NE> NE * findFirstMatching(
      std::function<bool(NE *)> const & matchFunction
   ) {
      return ObjectStoreTyped<NE>::getInstance().findFirstMatching(matchFunction);
   }

   template<class NE> QList<std::shared_ptr<NE> > findAllMatching(
      std::function<bool(std::shared_ptr<NE>)> const & matchFunction
   ) {
      return ObjectStoreTyped<NE>::getInstance().findAllMatching(matchFunction);
   }

   template<class NE> QList<NE *> findAllMatching(
      std::function<bool(NE *)> const & matchFunction
   ) {
      return ObjectStoreTyped<NE>::getInstance().findAllMatching(matchFunction);
   }

   template<class NE>
   QVector<int> idsOfAllMatching(std::function<bool(NE const *)> const & matchFunction) {
      return ObjectStoreTyped<NE>::getInstance().idsOfAllMatching(matchFunction);
   }

   /**
    * \brief Similar to \c idsOfAllMatching but returns a count of the number of cached objects that "match" according
    *        to the lambda function.
    */
   template<class NE> int numMatching(std::function<bool(NE const *)> const & matchFunction) {
      return ObjectStoreTyped<NE>::getInstance().numMatching(matchFunction);
   }

   /**
    * \brief Given two IDs of some subclass of \c NamedEntity, return \c true if the corresponding objects are equal (or
    *        if both IDs are invalid), and \c false otherwise
    */
   template<class NE>
   bool compareById(int lhsId, int rhsId) {
      if (lhsId <= 0 && rhsId <= 0) {
         // Both are invalid IDs
         return true;
      }
      NE const * lhs = ObjectStoreWrapper::getByIdRaw<NE>(lhsId);
      NE const * rhs = ObjectStoreWrapper::getByIdRaw<NE>(rhsId);
      if (nullptr == lhs && nullptr == rhs) {
         // Neither ID was found in the ObjectStore
         return true;
      }
      if (nullptr == lhs || nullptr == rhs) {
         // Only one of the IDs was found in the ObjectStore
         return false;
      }
      // Both IDs found in the ObjectStore, so we can compare the corresponding object directly
      return *lhs == *rhs;
   }

   /**
    * \brief Given two lists of IDs of some subclass of \c NamedEntity, return \c true if all the corresponding objects
    *        are equal, and \c false otherwise
    */
   template<class NE>
   bool compareListByIds(QVector<int> & lhsIds, QVector<int> & rhsIds) {
      if (lhsIds.size() != rhsIds.size()) {
         return false;
      }

      // .:TBD:. For the moment we're assuming everything is always in the same order and order matters
      for (int ii = 0; ii < lhsIds.size(); ++ii) {
         if (!compareById<NE>(lhsIds.at(ii), rhsIds.at(ii))) {
            return false;
         }
      }

      return true;
   }

   /**
    * \brief Getting the \c Boil, \c Fermentation, etc for a \c Recipe, or the \c BrewLog for a \c StockUse subclass is
    *        pretty much the same logic, so we template it
    *
    * \param getter  The object making the request (eg the \c Recipe that wants the \c Boil)
    * \param idToGet The primary key of the \c Boil, \c Fermentation, etc being requested.
    *
    * NOTE:  In BeerJSON, each of mash, boil and fermentation is optional.  I guess no fermentation is for recipes for
    *        making hop water etc.  Equally, there are people making beer without boiling -- eg see
    *        https://byo.com/article/raw-ale/.  In both cases, our current support for "no boil" and/or "no ferment" is
    *        somewhat limited and untested for now.
    *
    * NOTE:  The order of template parameters here matters!  We typically want \c Getter to be deduced automatically
    *        from the first parameter (whereas \c NE always needs to be specified by the caller).  This means \c Getter
    *        needs to be the second template parameter.
    */
   template<class NE, class Getter>
   std::shared_ptr<NE> getRelational(Getter const & getter, int const idToGet) {
      // Normally leave the next line commented out otherwise it generates too much logging
//      qDebug() << Q_FUNC_INFO << getter << "request for" << NE::staticMetaObject.className() << "#" << idToGet;
      if (idToGet < 0) {
         // Negative ID just means there isn't one -- because this is how we store "NULL" for a foreign key
         // Normally leave the next line commented out otherwise it generates too much logging
//         qDebug() << Q_FUNC_INFO << "No" << NE::staticMetaObject.className() << "on" << getter;
         return nullptr;
      }
      auto retVal = ObjectStoreWrapper::getById<NE>(idToGet);
      if (!retVal) {
         // I would think it's a coding error to have a seemingly valid boil/etc ID that's not in the database, but we
         // try to recover as best we can.
         qCritical() <<
            Q_FUNC_INFO << "Invalid" << NE::staticMetaObject.className() << "ID (" << idToGet << ") on" << getter;
         return nullptr;
      }
      return retVal;
   }


   /**
    * \brief Called, eg, by \c Recipe, to set \c Mash, \c Boil, \c Fermentation, \c Style, \c Equipment, etc -- ie
    *        things that the \c Recipe has at most one of.
    *
    * \param setter  The object making the request (eg the \c Recipe that wants the \c Boil)
    * \param val     The Mash/Boil/Fermentation/Style/Equipment to set
    * \param idVar   Reference to the setter's member variable storing the ID of Mash/Boil/Fermentation/Style/Equipment
    *
    * \return \c true if something was changed; \c false otherwise
    */
   template<class NE, class Setter>
   bool setRelational(Setter & setter, std::shared_ptr<NE> val, int & idVar) {
      if (!val && idVar < 0) {
         // No change (from "not set" to "not set")
         return false;
      }

      if (idVar > 0) {
         // This comparison is only valid if we have a valid ID, because val might not yet be stored in the DB.
         if (val && val->key() == idVar) {
            // No change (same object as we already have)
            return false;
         }

         //
         // We can disconnect signals from the old object here because we don't need to know what they are (or even
         // whether there were any).  However, it is the caller's responsibility to make any new connections.
         //
         std::shared_ptr<NE> oldVal = ObjectStoreWrapper::getById<NE>(idVar);
         setter.disconnect(oldVal.get(), nullptr, &setter, nullptr);
      }

      if (!val) {
         //
         // Setter changed from having an object to not having one
         //
         idVar = -1;
         return true;
      }

      if (val->key() < 0) {
         idVar = ObjectStoreWrapper::insert<NE>(val);
      } else {
         idVar = val->key();
      }

      return true;
   }

}

#endif