/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2011, Red Hat, Inc., and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.jboss.as.jpa.hibernate4.management;

import java.util.Locale;

import javax.persistence.Cache;

import org.jboss.as.controller.AbstractRuntimeOnlyHandler;
import org.jboss.as.controller.OperationContext;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.controller.OperationStepHandler;
import org.jboss.as.controller.PathAddress;
import org.jboss.as.controller.PathElement;
import org.jboss.as.controller.descriptions.DescriptionProvider;
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
import org.jboss.as.controller.registry.AttributeAccess;
import org.jboss.as.controller.registry.ManagementResourceRegistration;
import org.jboss.as.controller.registry.Resource;
import org.jboss.as.jpa.spi.ManagementAdaptor;
import org.jboss.as.jpa.spi.PersistenceUnitServiceRegistry;
import org.jboss.dmr.ModelNode;

/**
 * Contains management support for Hibernate
 *
 * @author Scott Marlow
 */
public class HibernateManagementAdaptor implements ManagementAdaptor {

    // shared instance for all Hibernate 4 JPA deployments
    private static final HibernateManagementAdaptor INSTANCE = new HibernateManagementAdaptor();

    private static final String PROVIDER_LABEL = "hibernate-persistence-unit";
    public static final String OPERATION_CLEAR = "clear";
    public static final String OPERATION_EVICTALL = "evict-all";
    public static final String OPERATION_SUMMARY = "summary";
    public static final String OPERATION_STATISTICS_ENABLED = "enabled";
    public static final String OPERATION_ENTITY_DELETE_COUNT = "entity-delete-count";
    public static final String OPERATION_ENTITY_INSERT_COUNT = "entity-insert-count";
    public static final String OPERATION_ENTITY_LOAD_COUNT = "entity-load-count";
    public static final String OPERATION_ENTITY_FETCH_COUNT = "entity-fetch-count";
    public static final String OPERATION_ENTITY_UPDATE_COUNT = "entity-update-count";
    public static final String ATTRIBUTE_QUERY_NAME = "query-name";
    public static final String OPERATION_QUERY_EXECUTION_COUNT = "query-execution-count";
    public static final String OPERATION_QUERY_EXECUTION_ROW_COUNT = "query-execution-row-count";
    public static final String OPERATION_QUERY_EXECUTION_AVG_TIME = "query-execution-average-time";
    public static final String OPERATION_QUERY_EXECUTION_MAX_TIME = "query-execution-max-time";
    public static final String OPERATION_QUERY_EXECUTION_MIN_TIME = "query-execution-min-time";
    public static final String OPERATION_QUERY_EXECUTION_MAX_TIME_QUERY_STRING = "query-execution-max-time-query-string";
    public static final String OPERATION_QUERY_CACHE_HIT_COUNT = "query-cache-hit-count";
    public static final String OPERATION_QUERY_CACHE_MISS_COUNT = "query-cache-miss-count";
    public static final String OPERATION_QUERY_CACHE_PUT_COUNT = "query-cache-put-count";
    public static final String OPERATION_FLUSH_COUNT = "flush-count";
    public static final String OPERATION_CONNECT_COUNT = "connect-count";
    public static final String OPERATION_SECOND_LEVEL_CACHE_HIT_COUNT = "second-level-cache-hit-count";
    public static final String OPERATION_SECOND_LEVEL_CACHE_MISS_COUNT = "second-level-cache-miss-count";
    public static final String OPERATION_SECOND_LEVEL_CACHE_PUT_COUNT = "second-level-cache-put-count";
    public static final String OPERATION_SESSION_CLOSE_COUNT = "session-close-count";
    public static final String OPERATION_SESSION_OPEN_COUNT = "session-open-count";
    public static final String OPERATION_COLLECTION_LOAD_COUNT = "collection-load-count";
    public static final String OPERATION_COLLECTION_FETCH_COUNT = "collection-fetch-count";
    public static final String OPERATION_COLLECTION_UPDATE_COUNT = "collection-update-count";
    public static final String OPERATION_COLLECTION_REMOVE_COUNT = "collection-remove-count";
    public static final String OPERATION_COLLECTION_RECREATED_COUNT = "collection-recreated-count";
    public static final String OPERATION_SUCCESSFUL_TRANSACTION_COUNT = "successful-transaction-count";
    public static final String OPERATION_COMPLETED_TRANSACTION_COUNT = "completed-transaction-count";
    public static final String OPERATION_PREPARED_STATEMENT_COUNT = "prepared-statement-count";
    public static final String OPERATION_CLOSE_STATEMENT_COUNT = "close-statement-count";
    public static final String OPERATION_OPTIMISTIC_FAILURE_COUNT = "optimistic-failure-count";

    private PersistenceUnitServiceRegistry persistenceUnitRegistry;

    /**
     * The management statistics are shared across all Hibernate 4 JPA deployments
     * @return shared instance for all Hibernate 4 JPA deployments
     */
    public static HibernateManagementAdaptor getInstance() {
        return INSTANCE;
    }

    @Override
    public void register(final ManagementResourceRegistration jpaSubsystemDeployments, PersistenceUnitServiceRegistry persistenceUnitRegistry) {

        this.persistenceUnitRegistry = persistenceUnitRegistry;

        // setup top level statistics
        DescriptionProvider topLevelDescriptions = new DescriptionProvider() {

            @Override
            public ModelNode getModelDescription(Locale locale) {
                // get description/type for each top level Hibernate statistic
                return HibernateDescriptions.describeTopLevelAttributes(locale);
            }
        };

        final ManagementResourceRegistration jpaHibernateRegistration =
            jpaSubsystemDeployments.registerSubModel(PathElement.pathElement(getIdentificationLabel()), topLevelDescriptions);

        registerStatisticAttributes(jpaHibernateRegistration);

        registerStatisticOperations(jpaHibernateRegistration);

        jpaHibernateRegistration.registerSubModel(new SecondLevelCacheResourceDefinition(persistenceUnitRegistry));
        jpaHibernateRegistration.registerSubModel(new QueryResourceDefinition(persistenceUnitRegistry));
        jpaHibernateRegistration.registerSubModel(new EntityResourceDefinition(persistenceUnitRegistry));
        jpaHibernateRegistration.registerSubModel(new CollectionResourceDefinition(persistenceUnitRegistry));

//
// TODO:  handle other stats

    }

    @Override
    public Resource createPersistenceUnitResource(final String persistenceUnitName, final String providerLabel) {
        return new HibernateStatisticsResource(persistenceUnitName, persistenceUnitRegistry, providerLabel);
    }

    private void registerStatisticOperations(ManagementResourceRegistration jpaHibernateRegistration) {
        /**
         * reset all statistics
         */
        DescriptionProvider clear = new DescriptionProvider() {
            @Override
            public ModelNode getModelDescription(Locale locale) {
                return HibernateDescriptions.clear(locale);
            }
        };

        OperationStepHandler clearHandler = new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                stats.getStatistics().clear();
            }
        };
        jpaHibernateRegistration.registerOperationHandler(OPERATION_CLEAR, clearHandler, clear);

        /**
         * evict all second level cache entries
         */
        DescriptionProvider evictAll = new DescriptionProvider() {
            @Override
            public ModelNode getModelDescription(Locale locale) {
                return HibernateDescriptions.evictall(locale);
            }
        };

        OperationStepHandler evictAllHandler = new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                Cache secondLevelCache = stats.getEntityManagerFactory().getCache();
                if (secondLevelCache != null) {
                    secondLevelCache.evictAll();
                }
            }
        };
        jpaHibernateRegistration.registerOperationHandler(OPERATION_EVICTALL, evictAllHandler, evictAll);

        /**
         * log statistics at INFO level
         */
        DescriptionProvider summary = new DescriptionProvider() {
            @Override
            public ModelNode getModelDescription(Locale locale) {
                return HibernateDescriptions.summary(locale);
            }
        };

        OperationStepHandler summaryHandler = new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                stats.getStatistics().logSummary();
            }
        };
        jpaHibernateRegistration.registerOperationHandler(OPERATION_SUMMARY, summaryHandler, summary);
    }

    private void registerStatisticAttributes(ManagementResourceRegistration jpaHibernateRegistration) {

        /**
         * Get global number of entity deletes
         * @return entity deletion count
         */
        jpaHibernateRegistration.registerMetric(OPERATION_ENTITY_DELETE_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getEntityDeleteCount());
            }
        });

        /**
         * Get global number of entity inserts
         * @return entity insertion count
         */
        jpaHibernateRegistration.registerMetric(OPERATION_ENTITY_INSERT_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getEntityInsertCount());
            }
        });


        /**
         * Get global number of entity loads
         * @return entity load (from DB)
         */
        jpaHibernateRegistration.registerMetric(OPERATION_ENTITY_LOAD_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getEntityLoadCount());
            }
        });

        /**
         * Get global number of entity fetches
         * @return entity fetch (from DB)
         */
        jpaHibernateRegistration.registerMetric(OPERATION_ENTITY_FETCH_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getEntityFetchCount());
            }
        });


        /**
         * Get global number of entity updates
         * @return entity update
         */
        jpaHibernateRegistration.registerMetric(OPERATION_ENTITY_UPDATE_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getEntityUpdateCount());
            }
        });

        /**
         * Get global number of executed queries
         * @return query execution count
         */
        jpaHibernateRegistration.registerMetric(OPERATION_QUERY_EXECUTION_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getQueryExecutionCount());
            }
        });

        /**
         * Get the time in milliseconds of the slowest query.
         */
        jpaHibernateRegistration.registerMetric(OPERATION_QUERY_EXECUTION_MAX_TIME, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getQueryExecutionMaxTime());
            }
        });

        /**
         * Get the query string for the slowest query.
         */
        jpaHibernateRegistration.registerMetric(OPERATION_QUERY_EXECUTION_MAX_TIME_QUERY_STRING, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                String sql = stats.getStatistics().getQueryExecutionMaxTimeQueryString();
                if (sql != null) {
                    response.set(sql);
                } else {
                    context.getResult();        // result will be undefined
                }
            }
        });

        /**
         * Get the global number of cached queries successfully retrieved from cache
         */
        jpaHibernateRegistration.registerMetric(OPERATION_QUERY_CACHE_HIT_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getQueryCacheHitCount());
            }
        });

        /**
         * Get the global number of cached queries *not* found in cache
         */
        jpaHibernateRegistration.registerMetric(OPERATION_QUERY_CACHE_MISS_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getQueryCacheMissCount());
            }
        });

        /**
         * Get the global number of cacheable queries put in cache
         */
        jpaHibernateRegistration.registerMetric(OPERATION_QUERY_CACHE_PUT_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getQueryCachePutCount());
            }
        });

        /**
         * Get the global number of flush executed by sessions (either implicit or explicit)
         */
        jpaHibernateRegistration.registerMetric(OPERATION_FLUSH_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getFlushCount());
            }
        });

        /**
         * Get the global number of connections asked by the sessions
         * (the actual number of connections used may be much smaller depending
         * whether you use a connection pool or not)
         */
        jpaHibernateRegistration.registerMetric(OPERATION_CONNECT_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getConnectCount());
            }
        });

        /**
         * Global number of cacheable entities/collections successfully retrieved from the cache
         */
        jpaHibernateRegistration.registerMetric(OPERATION_SECOND_LEVEL_CACHE_HIT_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getSecondLevelCacheHitCount());
            }
        });

        /**
         * Global number of cacheable entities/collections not found in the cache and loaded from the database.
         */
        jpaHibernateRegistration.registerMetric(OPERATION_SECOND_LEVEL_CACHE_MISS_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getSecondLevelCacheMissCount());
            }
        });

        /**
         * Global number of cacheable entities/collections put in the cache
         */
        jpaHibernateRegistration.registerMetric(OPERATION_SECOND_LEVEL_CACHE_PUT_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getSecondLevelCachePutCount());
            }
        });

        /**
         * Global number of sessions closed
         */
        jpaHibernateRegistration.registerMetric(OPERATION_SESSION_CLOSE_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getSessionCloseCount());
            }
        });

        /**
         * Global number of sessions opened
         */
        jpaHibernateRegistration.registerMetric(OPERATION_SESSION_OPEN_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getSessionOpenCount());
            }
        });

        /**
         * Global number of collections loaded
         */
        jpaHibernateRegistration.registerMetric(OPERATION_COLLECTION_LOAD_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getCollectionLoadCount());
            }
        });

        /**
         * Global number of collections fetched
         */
        jpaHibernateRegistration.registerMetric(OPERATION_COLLECTION_FETCH_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getCollectionFetchCount());
            }
        });

        /**
         * Global number of collections updated
         */
        jpaHibernateRegistration.registerMetric(OPERATION_COLLECTION_UPDATE_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getCollectionUpdateCount());
            }
        });


        /**
         * Global number of collections removed
         */
        //even on inverse="true"
        jpaHibernateRegistration.registerMetric(OPERATION_COLLECTION_REMOVE_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getCollectionRemoveCount());
            }
        });

        /**
         * Global number of collections recreated
         */
        jpaHibernateRegistration.registerMetric(OPERATION_COLLECTION_RECREATED_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getCollectionRecreateCount());
            }
        });

        /**
         * The number of transactions we know to have been successful
         */
        jpaHibernateRegistration.registerMetric(OPERATION_SUCCESSFUL_TRANSACTION_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getSuccessfulTransactionCount());
            }
        });

        /**
         * The number of transactions we know to have completed
         */
        jpaHibernateRegistration.registerMetric(OPERATION_COMPLETED_TRANSACTION_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getTransactionCount());
            }
        });

        /**
         * The number of prepared statements that were acquired
         */
        jpaHibernateRegistration.registerMetric(OPERATION_PREPARED_STATEMENT_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getPrepareStatementCount());
            }
        });

        /**
         * The number of prepared statements that were released
         */
        jpaHibernateRegistration.registerMetric(OPERATION_CLOSE_STATEMENT_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getCloseStatementCount());
            }
        });

        /**
         * The number of <tt>StaleObjectStateException</tt>s
         * that occurred
         */
        jpaHibernateRegistration.registerMetric(OPERATION_OPTIMISTIC_FAILURE_COUNT, new AbstractMetricsHandler() {
            @Override
            void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                response.set(stats.getStatistics().getOptimisticFailureCount());
            }
        });

        /**
         * enable/disable statistics attribute
         */
        // void registerReadWriteAttribute(String attributeName, OperationStepHandler readHandler, OperationStepHandler writeHandler, AttributeAccess.Storage storage);
        jpaHibernateRegistration.registerReadWriteAttribute(OPERATION_STATISTICS_ENABLED,
            new AbstractMetricsHandler() {  // readHandler
                @Override
                void handle(final ModelNode response, final String name, ManagementLookup stats, OperationContext context) {
                    response.set(stats.getStatistics().isStatisticsEnabled());
                }
            },
            new StatisticsEnabledWriteHandler(persistenceUnitRegistry),
            AttributeAccess.Storage.RUNTIME
        );

    }


    @Override
    public String getIdentificationLabel() {
        return PROVIDER_LABEL;
    }

    abstract class AbstractMetricsHandler extends AbstractRuntimeOnlyHandler {

        abstract void handle(ModelNode response, String name, ManagementLookup stats, OperationContext context);

        @Override
        protected void executeRuntimeStep(final OperationContext context, final ModelNode operation) throws
            OperationFailedException {
            final PathAddress address = PathAddress.pathAddress(operation.get(ModelDescriptionConstants.OP_ADDR));
            final String puResourceName = address.getLastElement().getValue();
            ManagementLookup stats = ManagementLookup.create(persistenceUnitRegistry, puResourceName);
            if (stats != null) {
                handle(context.getResult(), address.getLastElement().getValue(), stats, context);
            }
            context.stepCompleted();
        }
    }


}
