package com.atlassian.stash.internal.hibernate;

import org.hibernate.*;
import org.hibernate.cache.spi.QueryCache;
import org.hibernate.cache.spi.Region;
import org.hibernate.cache.spi.UpdateTimestampsCache;
import org.hibernate.cfg.Settings;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;
import org.hibernate.dialect.Dialect;
import org.hibernate.dialect.function.SQLFunctionRegistry;
import org.hibernate.engine.ResultSetMappingDefinition;
import org.hibernate.engine.jdbc.spi.JdbcServices;
import org.hibernate.engine.jdbc.spi.SqlExceptionHelper;
import org.hibernate.engine.profile.FetchProfile;
import org.hibernate.engine.query.spi.QueryPlanCache;
import org.hibernate.engine.spi.*;
import org.hibernate.exception.spi.SQLExceptionConverter;
import org.hibernate.id.IdentifierGenerator;
import org.hibernate.id.factory.IdentifierGeneratorFactory;
import org.hibernate.internal.NamedQueryRepository;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.metadata.CollectionMetadata;
import org.hibernate.persister.collection.CollectionPersister;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.service.spi.ServiceRegistryImplementor;
import org.hibernate.stat.Statistics;
import org.hibernate.stat.spi.StatisticsImplementor;
import org.hibernate.type.Type;
import org.hibernate.type.TypeResolver;
import org.springframework.core.InfrastructureProxy;

import javax.annotation.Nonnull;
import javax.annotation.PreDestroy;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.io.Serializable;
import java.sql.Connection;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * An implementation of {@link SwappableSessionFactory} which delegates all methods to another
 * {@code SessionFactoryImplementor} instance. That instance may be swapped out at runtime, allowing
 * Hibernate to be "updated" by tasks such as database migration without requiring the new session
 * factory to be re-injected throughout the system.
 * <p>
 * Additionally, this class adds a {@code PreDestroy} annotation to the {@link #close()} method, which is used to close
 * the delegate {@code SessionFactoryImplementor} when the Spring context is shutdown.
 */
@SuppressWarnings("deprecation")
public class DelegatingSwappableSessionFactory implements SwappableSessionFactory {

    private volatile SessionFactoryImplementor delegate;

    public DelegatingSwappableSessionFactory(SessionFactoryImplementor delegate) {
        this.delegate = delegate;
    }

    @Override
    public void addObserver(SessionFactoryObserver observer) {
        delegate.addObserver(observer);
    }

    @Override
    @PreDestroy
    public void close() throws HibernateException {
        delegate.close();
    }

    @Override
    public boolean containsFetchProfileDefinition(String name) {
        return delegate.containsFetchProfileDefinition(name);
    }

    @Deprecated
    @Override
    public void evict(Class persistentClass) throws HibernateException {
        delegate.evict(persistentClass);
    }

    @Deprecated
    @Override
    public void evict(Class persistentClass, Serializable id) throws HibernateException {
        delegate.evict(persistentClass, id);
    }

    @Deprecated
    @Override
    public void evictCollection(String roleName) throws HibernateException {
        delegate.evictCollection(roleName);
    }

    @Deprecated
    @Override
    public void evictCollection(String roleName, Serializable id) throws HibernateException {
        delegate.evictCollection(roleName, id);
    }

    @Deprecated
    @Override
    public void evictEntity(String entityName) throws HibernateException {
        delegate.evictEntity(entityName);
    }

    @Deprecated
    @Override
    public void evictEntity(String entityName, Serializable id) throws HibernateException {
        delegate.evictEntity(entityName, id);
    }

    @Deprecated
    @Override
    public void evictQueries() throws HibernateException {
        delegate.evictQueries();
    }

    @Deprecated
    @Override
    public void evictQueries(String regionName) throws HibernateException {
        delegate.evictQueries(regionName);
    }

    @Override
    public Map<String, ClassMetadata> getAllClassMetadata() throws HibernateException {
        return delegate.getAllClassMetadata();
    }

    @Override
    public Map getAllCollectionMetadata() throws HibernateException {
        return delegate.getAllCollectionMetadata();
    }

    @Override
    public Map getAllSecondLevelCacheRegions() {
        return delegate.getAllSecondLevelCacheRegions();
    }

    @Override
    public Cache getCache() {
        return delegate.getCache();
    }

    @Override
    public ClassMetadata getClassMetadata(Class persistentClass) throws HibernateException {
        return delegate.getClassMetadata(persistentClass);
    }

    @Override
    public ClassMetadata getClassMetadata(String entityName) throws HibernateException {
        return delegate.getClassMetadata(entityName);
    }

    @Override
    public CollectionMetadata getCollectionMetadata(String roleName) throws HibernateException {
        return delegate.getCollectionMetadata(roleName);
    }

    @Override
    public CollectionPersister getCollectionPersister(String role) throws MappingException {
        return delegate.getCollectionPersister(role);
    }

    @Override
    public Map<String, CollectionPersister> getCollectionPersisters() {
        return delegate.getCollectionPersisters();
    }

    @Override
    public Set<String> getCollectionRolesByEntityParticipant(String entityName) {
        return delegate.getCollectionRolesByEntityParticipant(entityName);
    }

    @Deprecated
    @Override
    public ConnectionProvider getConnectionProvider() {
        return delegate.getConnectionProvider();
    }

    @Override
    public Session getCurrentSession() throws HibernateException {
        return delegate.getCurrentSession();
    }

    @Override
    public CurrentTenantIdentifierResolver getCurrentTenantIdentifierResolver() {
        return delegate.getCurrentTenantIdentifierResolver();
    }

    @Override
    public CustomEntityDirtinessStrategy getCustomEntityDirtinessStrategy() {
        return delegate.getCustomEntityDirtinessStrategy();
    }

    @Override
    public Set getDefinedFilterNames() {
        return delegate.getDefinedFilterNames();
    }

    @Override
    public Dialect getDialect() {
        return delegate.getDialect();
    }

    @Override
    public EntityNotFoundDelegate getEntityNotFoundDelegate() {
        return delegate.getEntityNotFoundDelegate();
    }

    @Override
    public EntityPersister getEntityPersister(String entityName) throws MappingException {
        return delegate.getEntityPersister(entityName);
    }

    @Override
    public Map<String, EntityPersister> getEntityPersisters() {
        return delegate.getEntityPersisters();
    }

    @Override
    public FetchProfile getFetchProfile(String name) {
        return delegate.getFetchProfile(name);
    }

    @Override
    public FilterDefinition getFilterDefinition(String filterName) throws HibernateException {
        return delegate.getFilterDefinition(filterName);
    }

    @Override
    public IdentifierGenerator getIdentifierGenerator(String rootEntityName) {
        return delegate.getIdentifierGenerator(rootEntityName);
    }

    @Deprecated
    @Override
    public IdentifierGeneratorFactory getIdentifierGeneratorFactory() {
        return delegate.getIdentifierGeneratorFactory();
    }

    @Override
    public String getIdentifierPropertyName(String className) throws MappingException {
        return delegate.getIdentifierPropertyName(className);
    }

    @Override
    public Type getIdentifierType(String className) throws MappingException {
        return delegate.getIdentifierType(className);
    }

    @Override
    public String[] getImplementors(String className) throws MappingException {
        return delegate.getImplementors(className);
    }

    @Override
    public String getImportedClassName(String className) {
        return delegate.getImportedClassName(className);
    }

    @Override
    public Interceptor getInterceptor() {
        return delegate.getInterceptor();
    }

    @Override
    public JdbcServices getJdbcServices() {
        return delegate.getJdbcServices();
    }

    @Override
    public NamedQueryDefinition getNamedQuery(String queryName) {
        return delegate.getNamedQuery(queryName);
    }

    @Override
    public NamedQueryRepository getNamedQueryRepository() {
        return delegate.getNamedQueryRepository();
    }

    @Override
    public NamedSQLQueryDefinition getNamedSQLQuery(String queryName) {
        return delegate.getNamedSQLQuery(queryName);
    }

    @Override
    public Region getNaturalIdCacheRegion(String regionName) {
        return delegate.getNaturalIdCacheRegion(regionName);
    }

    @Override
    public Properties getProperties() {
        return delegate.getProperties();
    }

    @Override
    public QueryCache getQueryCache() {
        return delegate.getQueryCache();
    }

    @Override
    public QueryCache getQueryCache(String regionName) throws HibernateException {
        return delegate.getQueryCache(regionName);
    }

    @Override
    public QueryPlanCache getQueryPlanCache() {
        return delegate.getQueryPlanCache();
    }

    @Override
    public Reference getReference() throws NamingException {
        return delegate.getReference();
    }

    @Override
    public Type getReferencedPropertyType(String className, String propertyName) throws MappingException {
        return delegate.getReferencedPropertyType(className, propertyName);
    }

    @Override
    public ResultSetMappingDefinition getResultSetMapping(String resultSetName) {
        return delegate.getResultSetMapping(resultSetName);
    }

    @Override
    public String[] getReturnAliases(String queryString) throws HibernateException {
        return delegate.getReturnAliases(queryString);
    }

    @Override
    public Type[] getReturnTypes(String queryString) throws HibernateException {
        return delegate.getReturnTypes(queryString);
    }

    @Override
    public SQLExceptionConverter getSQLExceptionConverter() {
        return delegate.getSQLExceptionConverter();
    }

    @Override
    public SqlExceptionHelper getSQLExceptionHelper() {
        return delegate.getSQLExceptionHelper();
    }

    @Override
    public Region getSecondLevelCacheRegion(String regionName) {
        return delegate.getSecondLevelCacheRegion(regionName);
    }

    @Override
    public ServiceRegistryImplementor getServiceRegistry() {
        return delegate.getServiceRegistry();
    }

    @Override
    public SessionFactoryOptions getSessionFactoryOptions() {
        return delegate.getSessionFactoryOptions();
    }

    @Override
    public Settings getSettings() {
        return delegate.getSettings();
    }

    @Override
    public SQLFunctionRegistry getSqlFunctionRegistry() {
        return delegate.getSqlFunctionRegistry();
    }

    @Override
    public Statistics getStatistics() {
        return delegate.getStatistics();
    }

    @Override
    public StatisticsImplementor getStatisticsImplementor() {
        return delegate.getStatisticsImplementor();
    }

    @Override
    public TypeHelper getTypeHelper() {
        return delegate.getTypeHelper();
    }

    @Override
    public TypeResolver getTypeResolver() {
        return delegate.getTypeResolver();
    }

    @Override
    public UpdateTimestampsCache getUpdateTimestampsCache() {
        return delegate.getUpdateTimestampsCache();
    }

    @Override
    public SessionFactoryImplementor getWrappedObject() {
        // cache the delegate field to a local variable to guard against the delegate changing between the instanceof
        // check and the rest of the ternary
        SessionFactoryImplementor delegate = this.delegate;

        return delegate instanceof InfrastructureProxy ?
                (SessionFactoryImplementor) ((InfrastructureProxy) delegate).getWrappedObject() :
                delegate;
    }

    @Override
    public boolean isClosed() {
        return delegate.isClosed();
    }

    @Override
    public Iterable<EntityNameResolver> iterateEntityNameResolvers() {
        return delegate.iterateEntityNameResolvers();
    }

    @Override
    public Session openSession() throws HibernateException {
        return delegate.openSession();
    }

    @Override
    public StatelessSession openStatelessSession() {
        return delegate.openStatelessSession();
    }

    @Override
    public StatelessSession openStatelessSession(Connection connection) {
        return delegate.openStatelessSession(connection);
    }

    @Override
    public Session openTemporarySession() throws HibernateException {
        return delegate.openTemporarySession();
    }

    @Override
    public void registerNamedQueryDefinition(String name, NamedQueryDefinition definition) {
        delegate.registerNamedQueryDefinition(name, definition);
    }

    @Override
    public void registerNamedSQLQueryDefinition(String name, NamedSQLQueryDefinition definition) {
        delegate.registerNamedSQLQueryDefinition(name, definition);
    }

    @Nonnull
    @Override
    public SessionFactoryImplementor swap(@Nonnull SessionFactoryImplementor target) {
        SessionFactoryImplementor old = delegate;
        delegate = target;

        return old;
    }

    @Override
    public SessionBuilderImplementor withOptions() {
        return delegate.withOptions();
    }

    @Override
    public StatelessSessionBuilder withStatelessOptions() {
        return delegate.withStatelessOptions();
    }
}
