/*
 * Data Hub Service (DHuS) - For Space data distribution.
 * Copyright (C) 2013,2014,2015,2017 GAEL Systems
 *
 * This file is part of DHuS software sources.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program 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 Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package fr.gael.dhus.database.dao.interfaces;

import fr.gael.dhus.olingo.v1.SQLVisitor;

import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.sql.SQLException;
import java.util.Date;
import java.util.List;

import javax.swing.event.EventListenerList;

import org.hibernate.Criteria;
import org.hibernate.HibernateException;
import org.hibernate.Query;
import org.hibernate.SQLQuery;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Projections;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.support.DataAccessUtils;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

/**
 * Hibernate DAO Implementation, containing minimal CRUD operations.
 * 
 * @param <T> Object concerned by this DAO
 * @param <PK> Primary Key of this Object.
 */
public class HibernateDao<T, PK extends Serializable> extends
   HibernateDaoSupport implements GenericDao<T, PK>, Pageable<T>
{
   private static final Integer MAX_FETCH_SIZE = Integer.getInteger("dhus.db.fetch.size", 100);

   protected Class<T> entityClass;
   private final EventListenerList listeners = new EventListenerList ();

   @SuppressWarnings ("unchecked")
   public HibernateDao ()
   {
      ParameterizedType genericSuperclass =
         (ParameterizedType) getClass ().getGenericSuperclass ();
      this.entityClass =
         (Class<T>) genericSuperclass.getActualTypeArguments ()[0];
   }

   @SuppressWarnings ("unchecked")
   @Override
   public T create (T t)
   {
      T sent = t;
      long start = new Date ().getTime ();
      PK id = (PK) getHibernateTemplate ().save (t);
      t = getHibernateTemplate ().get ((Class<T>)t.getClass (), id);
      long end = new Date ().getTime ();
      logger.info("Create/save " + entityClass.getSimpleName () + "("+ id 
         +") spent " + (end-start) + "ms" );

      fireCreatedEvent (new DaoEvent<T> (sent));
      return t;
   }

   @Override
   public T read (PK id)
   {
      long start = new Date ().getTime ();
      T ret = getHibernateTemplate ().get (entityClass, id);
      long end = new Date ().getTime ();
      logger.debug("Read " + entityClass.getSimpleName ()
            + "(" + id + ") spent " + (end-start) + "ms" );
      return ret;
   }

   @Override
   public void update (T t)
   {
      long start = new Date ().getTime ();
      getHibernateTemplate ().update (t);
      long end = new Date ().getTime ();
      logger.info("Update " + entityClass.getSimpleName () + " spent "
            + (end-start) + "ms" );

      fireUpdatedEvent (new DaoEvent<T> (t));
   }

   /**
    * Merge the provided object into the current session.
    * This could be useful when one session handle the same object twice.
    * @param t the entity to merge.
    */
   public void merge (T t)
   {
      long start = new Date ().getTime ();
      getHibernateTemplate().getSessionFactory().getCurrentSession().merge(t);
      long end = new Date ().getTime ();
      logger.info("Merge " + entityClass.getSimpleName () + " spent "
            + (end-start) + "ms" );
   }

   @Override
   public void delete (T t)
   {
      long start = new Date ().getTime ();
      getHibernateTemplate ().delete (t);
      long end = new Date ().getTime ();
      logger.info("Delete " + entityClass.getSimpleName () + " spent "
            + (end-start) + "ms" );

      fireDeletedEvent (new DaoEvent<T> (t));
   }

   /**
    * Remove all the element from the db og this <T> instance.
    */
   public void deleteAll ()
   {
      for (T entity : readAll ())
         delete (entity);

//      String hql = "DELETE FROM " + entityClass.getName ();
//      getHibernateTemplate ().bulkUpdate (hql);
   }

   /**
    * <p>Returns a List of <b>T</b> entities, where HQL clauses can be
    * specified.</p>
    * 
    * Note: This method is useful in read only. It can be use to delete or 
    * create <b>T</b> entities, but caution with <code>top</code> and 
    * <code>skip</code> arguments.
    * 
    * @param clauses query clauses (WHERE, ORDER BY, GROUP BY), if null no
    * clauses are apply.
    * @param skip number of entities to skip.
    * @param n  number of entities max returned.
    * @return a list of <b>T</b> entities.
    * @deprecated use of {@link #listCriteria(DetachedCriteria, int, int)}
    */
   @Deprecated
   @SuppressWarnings ("unchecked")
   public List<T> scroll (final String clauses, final int skip, final int n)
   {
      StringBuilder hql = new StringBuilder ();
      hql.append ("FROM ").append (entityClass.getName ());
      if (clauses != null)
         hql.append (" ").append (clauses);

      Session session;
      boolean newSession = false;
      try
      {
         session = getSessionFactory ().getCurrentSession ();
      }
      catch (HibernateException e)
      {
         session = getSessionFactory ().openSession ();
         newSession = true;
      }

      Query query = session.createQuery (hql.toString ());
      if (skip > 0) query.setFirstResult (skip);
      if (n > 0) 
      {
         query.setMaxResults (n);
         query.setFetchSize (n);
      }
      
      logger.info("Execution of HQL: " + hql.toString ());
      long start = System.currentTimeMillis ();

      List<T> result = (List<T>) query.list ();
      logger.info("HQL executed in " + 
         (System.currentTimeMillis() -start) + "ms.");

      if (newSession)
      {
         session.disconnect ();
      }

      return result;
   }

   /**
    * Retrieve the first element of the results.
    * 
    * @param query_string
    * @return
    */
   public T first (String query_string)
   {
      @SuppressWarnings ("unchecked")
      List<T> result = (List<T>) getHibernateTemplate ().find (query_string);
      return (result.isEmpty ()) ? null : result.get (0);
   }

   /**
    * Returns all Objects in a List.
    * 
    * @return List containing all Objects.
    */
   @SuppressWarnings ("unchecked")
   @Override
   public List<T> readAll ()
   {
      return find ("FROM " + entityClass.getName ());
   }

   /**
    * Count objects in table.
    * 
    * @return Objects count.
    */
   public int count ()
   {
      int count = 0;
      @SuppressWarnings ("unchecked")
      List<Long>counts = find (
         "select count(*) FROM " + entityClass.getName ());
      if (counts != null) for (Long c:counts) count +=c;
      return count;
   }
   
   @SuppressWarnings ("rawtypes")
   public List find(String query_string) throws DataAccessException
   {
      long start = new Date ().getTime ();
      List ret= getHibernateTemplate ().find (query_string);
      
      long end = new Date ().getTime ();
      logger.debug("Query \"" + 
         query_string.replaceAll("(\\r|\\n)", " ").trim () +
         "\" spent " + (end-start) + "ms" );
      return ret;
   }

   public void addListener (DaoListener<T> listener)
   {
      listeners.add (DaoListener.class, listener);
   }

   public void removeListener (DaoListener<T> listener)
   {
      listeners.remove (DaoListener.class, listener);
   }

   @SuppressWarnings ("unchecked")
   public DaoListener<T>[] getListeners ()
   {
      return listeners.getListeners (DaoListener.class);
   }
   
   protected void fireCreatedEvent (DaoEvent<T> e)
   {
      for (DaoListener<T> listener : getListeners ())
      {
         listener.created (e);
      }
   }

   protected void fireUpdatedEvent (DaoEvent<T> e)
   {
      for (DaoListener<T> listener : getListeners ())
      {
         listener.updated (e);
      }
   }

   protected void fireDeletedEvent (DaoEvent<T> e)
   {
      for (DaoListener<T> listener : getListeners ())
      {
         listener.deleted (e);
      }
   }
   
   @Autowired
   public void init (SessionFactory session_factory)
   {
      setSessionFactory (session_factory);
   }
   
   public void printCurrentSessions ()
   {
      int num_session = countOpenSessions ();
      logger.info(countOpenSessions () + " open sessions:");
      int index = 0;
      while (index<num_session)
      {
         logger.info("   SESSION_ID       "+ getSystemByName("SESSION_ID", index));
         logger.info("   CONNECTED        "+ getSystemByName("CONNECTED", index));
         logger.info("   SCHEMA           "+ getSystemByName("SCHEMA", index));
         //logger.info(
         //   "TRANSACTION      "+ getSystemByName("TRANSACTION", index));
         logger.info("   WAITING_FOR_THIS "+ getSystemByName("WAITING_FOR_THIS", index));
         logger.info("   THIS_WAITING_FOR "+ getSystemByName("THIS_WAITING_FOR", index));
         logger.info("   LATCH_COUNT      "+ getSystemByName("LATCH_COUNT", index));
         logger.info("   STATEMENT        "+ getSystemByName("CURRENT_STATEMENT",index));
         logger.info("");
         index++;
      }
   }
   
   @SuppressWarnings ("rawtypes")
   private int countOpenSessions ()
   {
      return DataAccessUtils.intResult (getHibernateTemplate ().execute (
         new HibernateCallback<List>()
         {
            @Override
            public List doInHibernate(Session session) 
               throws HibernateException, SQLException
            {
               String sql = 
                  "SELECT count (*) FROM INFORMATION_SCHEMA.SYSTEM_SESSIONS";
               SQLQuery query = session.createSQLQuery (sql);
               return query.list ();
            }
         }));
   }
   
   @SuppressWarnings ({ "unchecked", "rawtypes" })
   private String getSystemByName (final String name, final int index)
   {
      return DataAccessUtils.uniqueResult (getHibernateTemplate ().execute (
         new HibernateCallback<List>()
         {
            @Override
            public List doInHibernate(Session session) 
               throws HibernateException, SQLException
            {
               String sql = 
                  "SELECT " + name +
                  " FROM INFORMATION_SCHEMA.SYSTEM_SESSIONS" +
                  " LIMIT  1 OFFSET " + index;
               SQLQuery query = session.createSQLQuery (sql);
               return query.list ();
            }
         })).toString ();
   }

   /**
    * Returns a paged list of database entities.
    * 
    * @param query the passed query to retrieve the list.
    * @param skip the number of elements to skip in the list (0=no skip).
    * @param top number of element to be retained in the list.
    * @throws ClassCastException if query does not returns entity list of type T.
    * @see org.hibernate.Query
    */
   @Override
   public List<T> getPage (final String query, final int skip, final int top)
   {
      return getHibernateTemplate ().execute (new HibernateCallback<List<T>> ()
      {
         // List must be instance of List<T> otherwise ClassCast
         @SuppressWarnings ("unchecked")
         @Override
         public List<T> doInHibernate (Session session) throws
               HibernateException, SQLException
         {
            Query hql_query = session.createQuery (query);
            hql_query.setFirstResult (skip);
            hql_query.setMaxResults (top);
            return hql_query.list ();
         }
      });
   }

   @SuppressWarnings ("unchecked")
   public List<T> listCriteria (DetachedCriteria detached, int skip, int top)
   {
      SessionFactory factory = getSessionFactory ();
      org.hibernate.classic.Session session = factory.getCurrentSession ();

      Criteria criteria = detached.getExecutableCriteria (session);

      if (skip > 0)
         criteria.setFirstResult (skip);
      if (top > 0)
         criteria.setMaxResults (top);
      return criteria.list ();
   }

   @SuppressWarnings ("unchecked")
   public T uniqueResult (DetachedCriteria criteria)
   {
      Criteria excrit = criteria.getExecutableCriteria(getSessionFactory().getCurrentSession());
      excrit.setMaxResults(1);
      List<?> res = excrit.list();
      return res.isEmpty() ? null : (T) res.get(0);
   }

   public int count (DetachedCriteria detached)
   {
      Session session = getSessionFactory ().getCurrentSession ();
      Criteria criteria = detached.getExecutableCriteria (session);
      criteria.setProjection(Projections.rowCount());
      Object result = criteria.uniqueResult ();
      return ((Number) result).intValue ();
   }

   @SuppressWarnings("unchecked")
   public List<T> executeHQLQuery(final String hql,
         final List<SQLVisitor.SQLVisitorParameter> parameters,
         final int skip, final int top)
   {
      Session session = getSessionFactory().getCurrentSession();
      Query query = session.createQuery(hql);
      for (int i = 0; i < parameters.size(); i++)
      {
         SQLVisitor.SQLVisitorParameter param = parameters.get(i);
         query.setParameter(i, param.getValue(), param.getType());
      }

      if (skip > -1)
      {
         query.setFirstResult(skip);
      }
      if (top > -1)
      {
         query.setMaxResults(top);
      }
      query.setReadOnly(true);
      return (List<T>) query.list();
   }

   public int countHQLQuery(String hql,
         List<SQLVisitor.SQLVisitorParameter> parameters)
   {
      String hqlQuery = "SELECT COUNT(*) " + hql;
      Session session = getSessionFactory().getCurrentSession();
      Query query = session.createQuery(hqlQuery);
      for (int i = 0; i < parameters.size(); i++)
      {
         SQLVisitor.SQLVisitorParameter param = parameters.get(i);
         query.setParameter(i, param.getValue(), param.getType());
      }
      query.setReadOnly(true);

      Object obj = query.uniqueResult();
      int result;
      if (obj instanceof Long)
      {
         result = ((Long) obj).intValue();
      }
      else
      {
         result = (int) obj;
      }
      return result;
   }

}