/*
 * Copyright (c) 2004-2020 The YAWL Foundation. All rights reserved.
 * The YAWL Foundation is a collaboration of individuals and
 * organisations who are committed to improving workflow technology.
 *
 * This file is part of YAWL. YAWL is free software: you can
 * redistribute it and/or modify it under the terms of the GNU Lesser
 * General Public License as published by the Free Software Foundation.
 *
 * YAWL 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 Lesser General
 * Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with YAWL. If not, see <http://www.gnu.org/licenses/>.
 */

package org.yawlfoundation.yawl.engine;


import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.hibernate.*;
import org.hibernate.c3p0.internal.C3P0ConnectionProvider;
import org.hibernate.cfg.Configuration;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.internal.SessionFactoryImpl;
import org.hibernate.tool.hbm2ddl.SchemaUpdate;
import org.yawlfoundation.yawl.authentication.YExternalClient;
import org.yawlfoundation.yawl.elements.YAWLServiceReference;
import org.yawlfoundation.yawl.elements.YSpecification;
import org.yawlfoundation.yawl.elements.state.YIdentifier;
import org.yawlfoundation.yawl.engine.time.YLaunchDelayer;
import org.yawlfoundation.yawl.engine.time.YWorkItemTimer;
import org.yawlfoundation.yawl.exceptions.Problem;
import org.yawlfoundation.yawl.exceptions.YPersistenceException;
import org.yawlfoundation.yawl.util.HibernateStatistics;

import java.util.Iterator;
import java.util.List;


/**
 * This class acts as a handler for transactional persistence within the engine.
 *
 * @author Andrew Hastie (M2 Investments)
 *         Date: 21/06/2005
 *         Time: 13:46:54
 * @author Michael Adams - updated for v2.1 11/2009
 */
public class YPersistenceManager {

    // persistence actions
    public static final int DB_UPDATE = 0;
    public static final int DB_DELETE = 1;
    public static final int DB_INSERT = 2;

    private static Class[] persistedClasses = {
            YSpecification.class, YNetRunner.class, YWorkItem.class, YIdentifier.class,
            YNetData.class, YAWLServiceReference.class, YExternalClient.class,
            YWorkItemTimer.class, YLaunchDelayer.class, YCaseNbrStore.class, Problem.class
    };

    private static final boolean INSERT = false;
    private static final boolean UPDATE = true;
    private static Logger logger = null;

    protected static SessionFactory factory = null;
    private boolean restoring = false;
    private boolean enabled = false;

    /**
     * Constructor
     */
    public YPersistenceManager() {
        logger = LogManager.getLogger(YPersistenceManager.class);
    }


    protected SessionFactory initialise(boolean journalising) throws YPersistenceException {
        Configuration cfg;

        // Create the Hibernate config, check and create database if required,
        // and generally set things up .....
        if (journalising) {
            try {
                cfg = new Configuration();
                for (Class persistedClass : persistedClasses) {
                    cfg.addClass(persistedClass);
                }

                factory = cfg.buildSessionFactory();
                new SchemaUpdate(cfg).execute(false, true);
                setEnabled(true);
            } catch (Exception e) {
                e.printStackTrace();
                logger.fatal("Failure initialising persistence layer", e);
                throw new YPersistenceException("Failure initialising persistence layer", e);
            }
        }
        return factory;
    }


    public void setEnabled(boolean enable) { enabled = enable; }

    public boolean isEnabled() { return enabled && (factory != null); }


    public SessionFactory getFactory() {
        return factory;
    }


    public boolean isRestoring() {
        return restoring;
    }

    protected void setRestoring(boolean restoring) {
        this.restoring = restoring;
    }

    public Session getSession() {
        return (factory != null) ? factory.getCurrentSession() : null;
    }

    public Transaction getTransaction() {
        Session session = getSession();
        return (session != null) ? session.getTransaction() : null;
    }

    public void closeSession() {
        if (isEnabled()) {
            try {
                Session session = getSession();
                if ((session != null) && (session.isOpen())) {
                    session.close();
                }
            } catch (HibernateException e) {
                logger.error("Failure to close Hibernate session", e);
            }
        }
    }


    public void closeFactory() {                    // shutdown persistence engine
        if (factory != null) {
            if (factory instanceof SessionFactoryImpl) {
               SessionFactoryImpl sf = (SessionFactoryImpl) factory;
               ConnectionProvider conn = sf.getConnectionProvider();
               if (conn instanceof C3P0ConnectionProvider) {
                 ((C3P0ConnectionProvider)conn).stop();
               }
            }

            factory.close();
        }
    }


    public String getStatistics() {
        if (factory != null) {
            HibernateStatistics stats = new HibernateStatistics(factory);
            return stats.toXML();
        }
        return null;
    }

    public void setStatisticsEnabled(boolean enabled) {
        if (factory != null) factory.getStatistics().setStatisticsEnabled(enabled);
    }

    public boolean isStatisticsEnabled() {
        return (factory != null) && factory.getStatistics().isStatisticsEnabled();
    }


    /**
     * Start a new Hibernate transaction.
     *
     * @return true if a transaction is started successfully, false if the session
     *         already has an active transaction
     * @throws YPersistenceException if there's a problem starting a transaction
     */
    public boolean startTransaction() throws YPersistenceException {
        if ((!isEnabled()) || isActiveTransaction()) return false;
        logger.debug("---> start Transaction");
        try {
            getSession().beginTransaction();
        } catch (HibernateException e) {
            logger.fatal("Failure to start transactional session", e);
            throw new YPersistenceException("Failure to start transactional session", e);
        }
        logger.debug("<--- start Transaction");
        return true;
    }


    /**
     * Persists an object.
     *
     * @param obj The object to be persisted
     */
    protected void storeObject(Object obj) throws YPersistenceException {
        if ((!restoring) && isEnabled()) {
            logger.debug("Adding to insert cache: Type={}", obj.getClass().getName());
            doPersistAction(obj, INSERT);
        }
    }


    /**
     * Causes the supplied object to be updated when the current transaction is committed.
     *
     * @param obj The object to be persisted
     */
    protected void updateObject(Object obj) throws YPersistenceException {
        if ((!restoring) && isEnabled()) {
            logger.debug("Adding to update cache: Type={}", obj.getClass().getName());
            doPersistAction(obj, UPDATE);
        }
    }


    /**
     * Causes the supplied object to be removed from the persistence cache when the
     * current transaction is committed.
     *
     * @param obj The object to be persisted
     * @throws YPersistenceException
     */
    protected void deleteObject(Object obj) throws YPersistenceException {
        if (!isEnabled()) return;

        logger.debug("--> delete: Object={}: {}", obj.getClass().getName(), obj.toString());

        try {
            getSession().delete(obj);
            getSession().flush();
        } catch (HibernateException e) {
            logger.error("Failed to delete - " + e.getMessage());
        }
        try {
            getSession().evict(obj);
        } catch (HibernateException he) {
            // nothing to do
        }
        logger.debug("<-- delete");
    }


    private void updateOrMerge(Object obj) {
        try {
            getSession().saveOrUpdate(obj);
        } catch (Exception e) {
            logger.error("Persistence update failed, trying merge. Object: {}",
                    obj.toString());
            getSession().merge(obj);
        }
    }


    private boolean isActiveTransaction() {
        Transaction transaction = getTransaction();
        return (transaction != null) && transaction.isActive();
    }


    /**
     * Causes the supplied object to be persisted when the current transaction is committed.
     * This method simply calls {@link #storeObject(Object)} but is public in scope.
     *
     * @param obj The object to be persisted
     */
    public void storeObjectFromExternal(Object obj) throws YPersistenceException {
        storeObject(obj);
    }

    /**
     * Causes the supplied object to be updated within the persistence cache when the
     * current transaction is committed.
     * This method simply calls {@link #updateObject(Object)} but is public in scope.
     *
     * @param obj The object to be persisted
     */
    public void updateObjectExternal(Object obj) throws YPersistenceException {
        updateObject(obj);
    }


    /**
     * Causes the supplied object to be unpersisted when the current transaction is committed.
     * This method simply calls {@link #deleteObject(Object)} but is public in scope.
     *
     * @param obj The object to be unpersisted
     */
    public void deleteObjectFromExternal(Object obj) throws YPersistenceException {
        deleteObject(obj);
    }


    private synchronized void doPersistAction(Object obj, boolean update)
            throws YPersistenceException {

            logger.debug("--> doPersistAction: Mode={}; Object = {}:{}; Object identity = {}",
                    (update ? "Update " : "Create "),
                    obj.getClass().getName(), obj.toString(),
                    System.identityHashCode(obj));

        try {
            if (update) {
                updateOrMerge(obj);
            } else {
                getSession().save(obj);
            }
   //         getSession().flush();
        } catch (Exception e) {
            logger.error("Failure detected whilst persisting instance of " +
                    obj.getClass().getName(), e);
            try {
                getTransaction().rollback();
            } catch (Exception e2) {
                throw new YPersistenceException("Failure to rollback transactional session", e2);
            }
            throw new YPersistenceException("Failure detected whilst persisting instance of " +
                    obj.getClass().getName(), e);
        }

//        try {
//            getSession().evict(obj);
//        } catch (HibernateException e) {
//            logger.warn("Failure whilst evicting object from Hibernate session cache", e);
//        }
        logger.debug("<-- doPersistAction");
    }


    public void commit() throws YPersistenceException {
        logger.debug("--> start commit");
        try {
            if (isEnabled() && isActiveTransaction()) getTransaction().commit();
        } catch (Exception e1) {
            logger.fatal("Failure to commit transactional session - Rolling Back Transaction", e1);
            rollbackTransaction();
            throw new YPersistenceException("Failure to commit transactional session", e1);
        }
        logger.debug("<-- end commit");
    }


    /**
     * Forces a rollback of the current transaction,<P>
     */
    protected void rollbackTransaction() throws YPersistenceException {
        logger.debug("--> rollback Transaction");
        if (isEnabled() && isActiveTransaction()) {
            try {
                getTransaction().rollback();
            } catch (HibernateException e) {
                throw new YPersistenceException("Failure to rollback transaction", e);
            } finally {
                closeSession();
            }
        }
        logger.debug("<-- rollback Transaction");
    }


    public Query createQuery(String queryString) throws YPersistenceException {
        if (isEnabled()) {
            try {
                return getSession().createQuery(queryString);
            } catch (HibernateException e) {
                throw new YPersistenceException("Failure to create Hibernate query object", e);
            }
        }
        return null;
    }


    public List execQuery(String queryString) throws YPersistenceException {
        return execQuery(createQuery(queryString));
    }


    /**
     * executes a Query object based on the sql string passed
     *
     * @param query - the sql query to execute
     * @return the List of objects returned
     * @throws YPersistenceException if there's a problem reading the db
     */
    public List execQuery(Query query) throws YPersistenceException {
        try {
            return (query != null) ? query.list() : null;
        } catch (HibernateException he) {
            throw new YPersistenceException("Error executing query: " + query.getQueryString(), he);
        }
    }


    /**
     * returns all the instances currently persisted for the class passed
     *
     * @param className - the name of the class to retrieve instances of
     * @return a List of the instances retrieved
     * @throws YPersistenceException if there's a problem reading the db
     */
    public List getObjectsForClass(String className) throws YPersistenceException {
        return execQuery("from " + className);
    }


    /**
     * returns all the instances currently persisted for the class passed that
     * match the condition specified in the where clause
     *
     * @param className   the name of the class to retrieve instances of
     * @param whereClause the condition (without the 'where' part) e.g. "age=21"
     * @return a List of the instances retrieved
     * @throws YPersistenceException if there's a problem reading the db
     */
    public List getObjectsForClassWhere(String className, String whereClause)
            throws YPersistenceException {
        try {
            String qry = String.format("from %s as tbl where tbl.%s",
                    className, whereClause);
            Query query = createQuery(qry);
            return (query != null) ? query.list() : null;
        } catch (HibernateException he) {
            throw new YPersistenceException("Error reading data for class: " + className, he);
        }
    }


    /**
     * gets a scalar value (as an object) based on the values passed
     *
     * @param className - the type of object to select
     * @param field     - the column name which contains the queried value
     * @param value     - the value to find in the 'field' column
     * @return the first (or only) object matching 'where [field] = [value]'
     */
    public Object selectScalar(String className, String field, String value)
            throws YPersistenceException {
        String qryStr = String.format("select distinct t from %s as t where t.%s=%s",
                className, field, value);
        Iterator itr = createQuery(qryStr).iterate();
        if (itr.hasNext()) return itr.next();
        else return null;
    }


    /**
     * same as above but takes a long value instead
     */
    public Object selectScalar(String className, String field, long value)
            throws YPersistenceException {
        return selectScalar(className, field, String.valueOf(value));
    }

}