/****************************************************************************
**
** https://www.qxorm.com/
** Copyright (C) 2013 XDL Team (ic-east.com)
**
** This file is part of the QxOrm library
**
** This software is provided 'as-is', without any express or implied
** warranty. In no event will the authors be held liable for any
** damages arising from the use of this software
**
** Commercial Usage
** Licensees holding valid commercial QxOrm licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and XDL Team
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3.0 as published by the Free Software
** Foundation and appearing in the file 'license.gpl3.txt' included in the
** packaging of this file. Please review the following information to
** ensure the GNU General Public License version 3.0 requirements will be
** met : http://www.gnu.org/copyleft/gpl.html
**
** If you are unsure which license is appropriate for your use, or
** if you have questions regarding the use of this file, please contact :
** ic-east.com
**
****************************************************************************/

#include <QxPrecompiled.h>

#include <QtCore/qpair.h>
#include <QtCore/qthread.h>

#include <QxDao/QxSession.h>
#include <QxDao/QxSqlDatabase.h>

#include <QxMemLeak/mem_leak.h>

namespace qx
{

   struct Q_DECL_HIDDEN QxSession::QxSessionImpl
   {

      QSqlDatabase m_database;                //!< Database connexion of current session
      QList<QSqlError> m_lstSqlError;         //!< List of SQL errors
      bool m_bTransaction;                    //!< Flag to know if a transaction is opened or not
      bool m_bThrowable;                      //!< When a SQL error is appended, an exception of type qx::dao::sql_error is thrown
      bool m_bThrowInEvent;                   //!< An exception of type qx::dao::sql_error is throwing
      bool m_bAutoOpenClose;                  //!< Open and close automatically connection to database
      bool m_bIgnoreSoftDelete;               //!< Ignore soft delete behavior even if it has been defined for registered classes
      QStringList m_lstIgnoreSoftDelete;      //!< List of registered classes to ignore soft delete behavior (if empty and m_bIgnoreSoftDelete is true, then all classes are ignored)
      bool m_bAutoRollbackWhenDestroyed;      //!< If true, then database rollback is called when this session instance destructor (or close() method) is invoked
      QPair<Qt::HANDLE, QString> m_sessionId; //!< Session identifier based on connection name and current thread

      static QMutex m_mutex;                                                         //!< Mutex => qx::QxSession is thread-safe
      static QHash<QPair<Qt::HANDLE, QString>, qx::QxSession *> m_lstActiveSessions; //!< List of active sessions by database connection name

      QxSessionImpl() : m_bTransaction(false), m_bThrowable(false), m_bThrowInEvent(false), m_bAutoOpenClose(false), m_bIgnoreSoftDelete(false), m_bAutoRollbackWhenDestroyed(false) { ; }
      QxSessionImpl(const QSqlDatabase &database) : m_database(database), m_bTransaction(false), m_bThrowable(false), m_bThrowInEvent(false), m_bAutoOpenClose(false), m_bIgnoreSoftDelete(false), m_bAutoRollbackWhenDestroyed(false) { ; }
      QxSessionImpl(const QSqlDatabase &database, bool bThrowable, bool bAutoRollbackWhenDestroyed) : m_database(database), m_bTransaction(false), m_bThrowable(bThrowable), m_bThrowInEvent(false), m_bAutoOpenClose(false), m_bIgnoreSoftDelete(false), m_bAutoRollbackWhenDestroyed(bAutoRollbackWhenDestroyed) { ; }
      ~QxSessionImpl() { ; }

      void appendSqlError(const QSqlError &err)
      {
         if (!err.isValid())
         {
            return;
         }
         m_lstSqlError.append(err);
         if (m_bThrowInEvent)
         {
            return;
         }
         if (m_bThrowable)
         {
            QString msg = err.text();
            qDebug("[QxOrm] qx::QxSession throw 'qx::dao::sql_error' exception : '%s'", qPrintable(msg));
         }
         if (m_bThrowable)
         {
            m_bThrowInEvent = true;
            throw qx::dao::sql_error(err);
         }
         m_bThrowInEvent = false;
      }

      void clear()
      {
         m_lstSqlError.clear();
         m_bTransaction = false;
      }

      static QPair<Qt::HANDLE, QString> getSessionIdentifier(QSqlDatabase &db, qx::QxSession *pSessionToFill, qx::QxSession *pSessionToFetch)
      {
         if (pSessionToFetch && pSessionToFetch->m_pImpl)
         {
            return pSessionToFetch->m_pImpl->m_sessionId;
         }
         QString connectionName = db.connectionName();
         Qt::HANDLE hCurrThreadId = QThread::currentThreadId();
         QPair<Qt::HANDLE, QString> sessionId = qMakePair(hCurrThreadId, connectionName);
         if (pSessionToFill && pSessionToFill->m_pImpl)
         {
            pSessionToFill->m_pImpl->m_sessionId = sessionId;
         }
         return sessionId;
      }
   };

   QMutex qx::QxSession::QxSessionImpl::m_mutex;
   QHash<QPair<Qt::HANDLE, QString>, qx::QxSession *> qx::QxSession::QxSessionImpl::m_lstActiveSessions;

   QxSession::QxSession() : m_pImpl(new QxSessionImpl())
   {
      m_pImpl->m_database = qx::QxSqlDatabase::getDatabaseCloned();
      m_pImpl->m_bThrowable = qx::QxSqlDatabase::getSingleton()->getSessionThrowable();
      if (qx::QxSqlDatabase::getSingleton()->getSessionAutoTransaction())
      {
         open();
      }
      QMutexLocker locker(&QxSessionImpl::m_mutex);
      QPair<Qt::HANDLE, QString> sessionId = QxSessionImpl::getSessionIdentifier(m_pImpl->m_database, this, NULL);
      if (!sessionId.second.isEmpty())
      {
         QxSessionImpl::m_lstActiveSessions.insert(sessionId, this);
      }
   }

   QxSession::QxSession(const QSqlDatabase &database) : m_pImpl(new QxSessionImpl(database))
   {
      m_pImpl->m_bThrowable = qx::QxSqlDatabase::getSingleton()->getSessionThrowable();
      if (qx::QxSqlDatabase::getSingleton()->getSessionAutoTransaction())
      {
         open();
      }
      QMutexLocker locker(&QxSessionImpl::m_mutex);
      QPair<Qt::HANDLE, QString> sessionId = QxSessionImpl::getSessionIdentifier(m_pImpl->m_database, this, NULL);
      if (!sessionId.second.isEmpty())
      {
         QxSessionImpl::m_lstActiveSessions.insert(sessionId, this);
      }
   }

   QxSession::QxSession(const QSqlDatabase &database, bool bOpenTransaction) : m_pImpl(new QxSessionImpl(database))
   {
      m_pImpl->m_bThrowable = qx::QxSqlDatabase::getSingleton()->getSessionThrowable();
      if (bOpenTransaction)
      {
         open();
      }
      QMutexLocker locker(&QxSessionImpl::m_mutex);
      QPair<Qt::HANDLE, QString> sessionId = QxSessionImpl::getSessionIdentifier(m_pImpl->m_database, this, NULL);
      if (!sessionId.second.isEmpty())
      {
         QxSessionImpl::m_lstActiveSessions.insert(sessionId, this);
      }
   }

   QxSession::QxSession(const QSqlDatabase &database, bool bOpenTransaction, bool bThrowable, bool bAutoRollbackWhenDestroyed /* = false */) : m_pImpl(new QxSessionImpl(database, bThrowable, bAutoRollbackWhenDestroyed))
   {
      if (bOpenTransaction)
      {
         open();
      }
      QMutexLocker locker(&QxSessionImpl::m_mutex);
      QPair<Qt::HANDLE, QString> sessionId = QxSessionImpl::getSessionIdentifier(m_pImpl->m_database, this, NULL);
      if (!sessionId.second.isEmpty())
      {
         QxSessionImpl::m_lstActiveSessions.insert(sessionId, this);
      }
   }

   QxSession::~QxSession()
   {
      close();
      QMutexLocker locker(&QxSessionImpl::m_mutex);
      QPair<Qt::HANDLE, QString> sessionId = QxSessionImpl::getSessionIdentifier(m_pImpl->m_database, NULL, this);
      if (!sessionId.second.isEmpty())
      {
         QxSessionImpl::m_lstActiveSessions.remove(sessionId);
      }
   }

   QxSession *QxSession::getActiveSession(QSqlDatabase *db)
   {
      if (!db)
      {
         return NULL;
      }
      QMutexLocker locker(&QxSessionImpl::m_mutex);
      QPair<Qt::HANDLE, QString> sessionId = QxSessionImpl::getSessionIdentifier((*db), NULL, NULL);
      if (sessionId.second.isEmpty())
      {
         return NULL;
      }
      return (QxSessionImpl::m_lstActiveSessions.contains(sessionId) ? QxSessionImpl::m_lstActiveSessions.value(sessionId) : NULL);
   }

   bool QxSession::isThrowable() const { return m_pImpl->m_bThrowable; }

   bool QxSession::isOpened() const { return m_pImpl->m_bTransaction; }

   bool QxSession::isValid() const { return (m_pImpl->m_lstSqlError.count() <= 0); }

   bool QxSession::isAutoRollbackWhenDestroyed() const { return m_pImpl->m_bAutoRollbackWhenDestroyed; }

   void QxSession::setAutoRollbackWhenDestroyed(bool b) { m_pImpl->m_bAutoRollbackWhenDestroyed = b; }

   QSqlError QxSession::firstError() const { return ((m_pImpl->m_lstSqlError.count() > 0) ? m_pImpl->m_lstSqlError.first() : QSqlError()); }

   QSqlError QxSession::lastError() const { return ((m_pImpl->m_lstSqlError.count() > 0) ? m_pImpl->m_lstSqlError.last() : QSqlError()); }

   QList<QSqlError> QxSession::allErrors() const { return m_pImpl->m_lstSqlError; }

   const QSqlDatabase *QxSession::database() const { return (&m_pImpl->m_database); }

   QSqlDatabase *QxSession::database() { return (&m_pImpl->m_database); }

   bool QxSession::open()
   {
      if (m_pImpl->m_bTransaction)
      {
         return true;
      }
      if (!m_pImpl->m_database.isOpen())
      {
         m_pImpl->m_bAutoOpenClose = m_pImpl->m_database.open();
         if (!m_pImpl->m_bAutoOpenClose)
         {
            m_pImpl->appendSqlError(m_pImpl->m_database.lastError());
            return false;
         }
      }
      if (!m_pImpl->m_database.driver() || !m_pImpl->m_database.driver()->hasFeature(QSqlDriver::Transactions))
      {
         return false;
      }
      m_pImpl->m_bTransaction = m_pImpl->m_database.transaction();
      return m_pImpl->m_bTransaction;
   }

   bool QxSession::close()
   {
      bool bCloseOk = true;
      if (m_pImpl->m_bTransaction && m_pImpl->m_bAutoRollbackWhenDestroyed)
      {
         bCloseOk = rollback();
      }
      else if (m_pImpl->m_bTransaction && isValid())
      {
         bCloseOk = commit();
      }
      else if (m_pImpl->m_bTransaction)
      {
         bCloseOk = rollback();
      }
      if (m_pImpl->m_bAutoOpenClose)
      {
         m_pImpl->m_database.close();
         m_pImpl->m_bAutoOpenClose = false;
      }
      return bCloseOk;
   }

   bool QxSession::commit()
   {
      if (m_pImpl->m_bTransaction && !isValid())
      {
         qDebug("[QxOrm] %s", "qx::QxSession is not valid and 'commit()' method is called");
      }
      if (!m_pImpl->m_bTransaction)
      {
         m_pImpl->clear();
         return false;
      }
      bool bCommit = m_pImpl->m_database.commit();
      if (bCommit)
      {
         m_pImpl->clear();
         return true;
      }
      m_pImpl->appendSqlError(m_pImpl->m_database.lastError());
      m_pImpl->m_bTransaction = false;
      return false;
   }

   bool QxSession::rollback()
   {
      if (!m_pImpl->m_bTransaction)
      {
         m_pImpl->clear();
         return false;
      }
      qDebug("[QxOrm] qx::QxSession : '%s'", "rollback transaction");
      bool bRollback = m_pImpl->m_database.rollback();
      if (bRollback)
      {
         m_pImpl->clear();
         return true;
      }
      m_pImpl->appendSqlError(m_pImpl->m_database.lastError());
      m_pImpl->m_bTransaction = false;
      return false;
   }

   QxSession &QxSession::operator+=(const QSqlError &err)
   {
      m_pImpl->appendSqlError(err);
      return (*this);
   }

   void QxSession::ignoreSoftDelete(bool bIgnoreSoftDelete /* = true */, const QStringList &classesToIgnore /* = QStringList() */)
   {
      m_pImpl->m_bIgnoreSoftDelete = bIgnoreSoftDelete;
      m_pImpl->m_lstIgnoreSoftDelete = classesToIgnore;
      m_pImpl->m_lstIgnoreSoftDelete.sort();
   }

   bool QxSession::checkIgnoreSoftDelete(const QString &classKey) const
   {
      if (!m_pImpl->m_bIgnoreSoftDelete)
      {
         return false;
      }
      if (m_pImpl->m_lstIgnoreSoftDelete.isEmpty())
      {
         return true;
      }
      if (m_pImpl->m_lstIgnoreSoftDelete.contains(classKey))
      {
         return true;
      }
      return false;
   }

   QString QxSession::getIgnoreSoftDeleteHash() const
   {
      if (!m_pImpl->m_bIgnoreSoftDelete)
      {
         return "";
      }
      if (m_pImpl->m_lstIgnoreSoftDelete.isEmpty())
      {
         return "[ALL]";
      }
      return m_pImpl->m_lstIgnoreSoftDelete.join("_");
   }

} // namespace qx
