/*
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
*/
package openbook.server;

import java.io.Serializable;
import java.util.concurrent.locks.ReentrantLock;

import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.PersistenceContextType;

/**
 * An abstract utility for JPA based service.
 * This thin wrapper over a {@link EntityManagerFactory Persistence Unit} maintains
 * <LI>per-thread persistence context
 * <LI>relinquishes direct transaction control under a managed environment
 *
 * @see #getEntityManager()
 * @see #newEntityManager()
 *
 * @author Pinaki Poddar
 *
 */
abstract class PersistenceService implements Serializable {
    private static final long serialVersionUID = 1L;
    private final EntityManagerFactory emf;
    private final String unitName;
    private final boolean isManaged;
    private final PersistenceContextType scope;

    private ThreadLocal<EntityManager> thread = new ThreadLocal<>();
    private ReentrantLock lock = new ReentrantLock();

    protected PersistenceService(String unit, EntityManagerFactory emf, boolean managed,
            PersistenceContextType scope) {
        this.unitName = unit;
        this.emf = emf;
        this.isManaged = managed;
        this.scope = scope;
    }

    public final EntityManagerFactory getUnit() {
        return emf;
    }

    public final String getUnitName() {
        return unitName;
    }

    public final boolean isManaged() {
        return isManaged;
    }

    public final PersistenceContextType getContextType() {
        return scope;
    }

    /**
     * Gets an entity manager associated with the current thread. If the
     * current thread is not associated with any entity manager or the
     * associated entity manager has been closed, creates a new entity manager
     * and associates with the current thread.
     *
     * @return an entity manager associated with the current thread.
     */
    protected EntityManager getEntityManager() {
        try {
            lock.lock();
            EntityManager em = thread.get();
            if (em == null || !em.isOpen()) {
                em = emf.createEntityManager();
                thread.set(em);
            }
            return em;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Creates a new entity manager. The entity manager is not associated with
     * the current thread.
     */
    protected EntityManager newEntityManager() {
        return emf.createEntityManager();
    }

    /**
     * Begins a transaction on the current thread. If the thread is associated
     * with a persistence context, then a transaction is started if necessary.
     * If the thread is not associated with a persistence context, then a new
     * context is created, associated with the thread, new transaction is
     * started.
     *
     * @see #getEntityManager()
     */
    protected EntityManager begin() {
        try {
            lock.lock();
            EntityManager em = getEntityManager();
            if (isManaged) {
                em.joinTransaction();
            } else {
                if (!em.getTransaction().isActive()) {
                    em.getTransaction().begin();
                }
            }
            return em;
        } finally {
            lock.unlock();
        }
    }

    /**
     * Commits a transaction on the current thread.
     */
    protected void commit() {
        try {
            lock.lock();
            EntityManager em = getEntityManager();
            if (isManaged) {
                em.flush();
            } else {
                assertActive();
                em.getTransaction().commit();
            }
            if (scope == PersistenceContextType.TRANSACTION) {
                em.clear();
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Rolls back a transaction on the current thread.
     */
    protected void rollback() {
        try {
            lock.lock();
            EntityManager em = getEntityManager();
            if (isManaged) {

            } else {
                em.getTransaction().rollback();
            }
            if (scope == PersistenceContextType.TRANSACTION) {
                em.clear();
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Assert current thread is associated with an active transaction.
     */
    protected void assertActive() {
        EntityManager em = thread.get();
        String thread = Thread.currentThread().getName();
        assertTrue("No persistent context is associated with " + thread, em != null);
        assertTrue("Persistent context " + em + " associated with " + thread + " has been closed", em.isOpen());
        if (!isManaged) {
            assertTrue("Persistent context " + em + " associated with " + thread + " has no active transaction",
                    em.getTransaction().isActive());
        }
    }

    protected void assertTrue(String s, boolean p) {
        if (!p) {
            System.err.println(s);
            throw new RuntimeException(s);
        }
    }
}
