/*
 * Copyright The WildFly Authors
 * SPDX-License-Identifier: Apache-2.0
 */
package org.jboss.system;

import java.util.concurrent.atomic.AtomicLong;

import javax.management.AttributeChangeNotification;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.NotificationBroadcasterSupport;
import javax.management.ObjectName;

import org.jboss.logging.Logger;
import org.jboss.system.logging.ServiceMBeanLogger;

/**
 * An abstract base class JBoss services can subclass to implement a service that conforms to the ServiceMBean interface.
 * Subclasses must override {@link #getName} method and should override {@link #startService}, and {@link #stopService} as
 * approriate.
 *
 * @see ServiceMBean
 *
 * @author <a href="mailto:rickard.oberg@telkel.com">Rickard Öberg</a>
 * @author Scott.Stark@jboss.org
 * @author <a href="mailto:andreas@jboss.org">Andreas Schaefer</a>
 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
 * @author Eduardo Martins (AS7)
 */
public class ServiceMBeanSupport extends NotificationBroadcasterSupport implements ServiceMBean, MBeanRegistration {

    protected Logger log;

    /** The MBeanServer which we have been register with. */
    protected MBeanServer server;

    /** The object name which we are registered under. */
    protected ObjectName serviceName;

    /** The current state this service is in. */
    private int state = UNREGISTERED;

    /** Sequence number for jmx notifications we send out */
    private final AtomicLong sequenceNumber = new AtomicLong(0);

    // on AS7 MBean lifecycle is CREATED -> STARTED -> REGISTERED -> UNREGISTERED -> STOP -> DESTROY,
    // on previous AS versions is REGISTERED -> CREATED -> STARTED -> STOP -> DESTROYED -> UNREGISTERED
    // to maintain compatibility with old ServiceMBeanSupport, we ignore some state changes, but redo
    // these when proper state change happens
    // the flags below are used to mark ignored lifecycle methods invocations
    private boolean createIgnored = false;
    private boolean startIgnored = false;
    private boolean stopIgnored = false;
    private boolean destroyIgnored = false;
    private boolean unregisterIgnored = false;

    /**
     * Construct a <t>ServiceMBeanSupport</tt>.
     *
     * <p>
     * Sets up logging.
     */
    public ServiceMBeanSupport() {
        // can not call this(Class) because we need to call getClass()
        this.log = Logger.getLogger(getClass().getName());
        log.trace("Constructing");
    }

    /**
     * Construct a <t>ServiceMBeanSupport</tt>.
     *
     * <p>
     * Sets up logging.
     *
     * @param type The class type to determine category name from.
     */
    public ServiceMBeanSupport(final Class<?> type) {
        this(type.getName());
    }

    /**
     * Construct a <t>ServiceMBeanSupport</tt>.
     *
     * <p>
     * Sets up logging.
     *
     * @param category The logger category name.
     */
    public ServiceMBeanSupport(final String category) {
        this(Logger.getLogger(category));
    }

    /**
     * Construct a <t>ServiceMBeanSupport</tt>.
     *
     * @param log The logger to use.
     */
    public ServiceMBeanSupport(final Logger log) {
        this.log = log;
        log.trace("Constructing");
    }

    /**
     * Use the short class name as the default for the service name.
     *
     * @return a description of the mbean
     */
    public String getName() {
        final String s = log.getName();
        final int i = s.lastIndexOf(".");
        return i != -1 ? s.substring(i + 1, s.length()) : s;
    }

    public ObjectName getServiceName() {
        return serviceName;
    }

    public MBeanServer getServer() {
        return server;
    }

    public int getState() {
        return state;
    }

    public String getStateString() {
        return states[state];
    }

    public Logger getLog() {
        return log;
    }

    // /////////////////////////////////////////////////////////////////////////
    // State Mutators //
    // /////////////////////////////////////////////////////////////////////////

    public void create() throws Exception {
        jbossInternalCreate();
    }

    public void start() throws Exception {
        jbossInternalStart();
    }

    public void stop() {
        try {
            jbossInternalStop();
        } catch (Throwable t) {
            log.warn(ServiceMBeanLogger.ROOT_LOGGER.errorInStop(jbossInternalDescription()), t);
        }
    }

    public void destroy() {
        try {
            jbossInternalDestroy();
        } catch (Throwable t) {
            log.warn(ServiceMBeanLogger.ROOT_LOGGER.errorInDestroy(jbossInternalDescription()), t);
        }
    }

    protected String jbossInternalDescription() {
        if (serviceName != null)
            return serviceName.toString();
        else
            return getName();
    }

    public void jbossInternalLifecycle(String method) throws Exception {
        if (method == null)
            throw ServiceMBeanLogger.ROOT_LOGGER.nullMethodName();

        if (method.equals("create"))
            jbossInternalCreate();
        else if (method.equals("start"))
            jbossInternalStart();
        else if (method.equals("stop"))
            jbossInternalStop();
        else if (method.equals("destroy"))
            jbossInternalDestroy();
        else
            throw ServiceMBeanLogger.ROOT_LOGGER.unknownLifecycleMethod(method);
    }

    protected void jbossInternalCreate() throws Exception {
        // if (state == CREATED || state == STARTING || state == STARTED
        // || state == STOPPING || state == STOPPED)
        if (state != REGISTERED) {
            createIgnored = true;
            if (log.isDebugEnabled()) {
                log.debug("Ignoring create call; current state is " + getStateString());
            }
            return;
        }

        createIgnored = false;
        if (log.isDebugEnabled()) {
            log.debug("Creating " + jbossInternalDescription());
        }

        try {
            createService();
            state = CREATED;
        } catch (Exception e) {
            log.warn(ServiceMBeanLogger.ROOT_LOGGER.initializationFailed(jbossInternalDescription()), e);
            throw e;
        }

        if (log.isDebugEnabled()) {
            log.debug("Created " + jbossInternalDescription());
        }

        if (startIgnored) {
            start();
        }
    }

    protected void jbossInternalStart() throws Exception {
        if (state != CREATED && state != STOPPED) {
            startIgnored = true;
            if (log.isDebugEnabled()) {
                log.debug("Ignoring start call; current state is " + getStateString());
            }
            return;
        }

        startIgnored = false;

        state = STARTING;
        sendStateChangeNotification(STOPPED, STARTING, getName() + " starting", null);
        if (log.isDebugEnabled()) {
            log.debug("Starting " + jbossInternalDescription());
        }

        try {
            startService();
        } catch (Exception e) {
            state = FAILED;
            sendStateChangeNotification(STARTING, FAILED, getName() + " failed", e);
            log.warn(ServiceMBeanLogger.ROOT_LOGGER.startingFailed(jbossInternalDescription()), e);
            throw e;
        }

        state = STARTED;
        sendStateChangeNotification(STARTING, STARTED, getName() + " started", null);
        if (log.isDebugEnabled()) {
            log.debug("Started " + jbossInternalDescription());
        }

        if (stopIgnored) {
            stop();
        }
    }

    protected void jbossInternalStop() {
        if (state != STARTED) {
            stopIgnored = true;
            if (log.isDebugEnabled()) {
                log.debug("Ignoring stop call; current state is " + getStateString());
            }
            return;
        }

        stopIgnored = false;
        state = STOPPING;
        sendStateChangeNotification(STARTED, STOPPING, getName() + " stopping", null);
        if (log.isDebugEnabled()) {
            log.debug("Stopping " + jbossInternalDescription());
        }

        try {
            stopService();
        } catch (Throwable e) {
            state = FAILED;
            sendStateChangeNotification(STOPPING, FAILED, getName() + " failed", e);
            log.warn(ServiceMBeanLogger.ROOT_LOGGER.stoppingFailed(jbossInternalDescription()), e);
            return;
        }

        state = STOPPED;
        sendStateChangeNotification(STOPPING, STOPPED, getName() + " stopped", null);
        if (log.isDebugEnabled()) {
            log.debug("Stopped " + jbossInternalDescription());
        }

        if (destroyIgnored) {
            destroy();
        }
    }

    protected void jbossInternalDestroy() {
        if (state != STOPPED) {
            destroyIgnored = true;
            if (log.isDebugEnabled()) {
                log.debug("Ignoring destroy call; current state is " + getStateString());
            }
            return;
        }

        destroyIgnored = false;
        if (log.isDebugEnabled()) {
            log.debug("Destroying " + jbossInternalDescription());
        }

        try {
            destroyService();
        } catch (Throwable t) {
            log.warn(ServiceMBeanLogger.ROOT_LOGGER.destroyingFailed(jbossInternalDescription()), t);
        }
        state = DESTROYED;
        if (log.isDebugEnabled()) {
            log.debug("Destroyed " + jbossInternalDescription());
        }

        if (unregisterIgnored) {
            postDeregister();
        }
    }

    // /////////////////////////////////////////////////////////////////////////
    // JMX Hooks //
    // /////////////////////////////////////////////////////////////////////////

    /**
     * Callback method of {@link javax.management.MBeanRegistration} before the MBean is registered at the JMX Agent.
     *
     * <p>
     * <b>Attention</b>: Always call this method when you overwrite it in a subclass because it saves the Object Name of the
     * MBean.
     *
     * @param server Reference to the JMX Agent this MBean is registered on
     * @param name Name specified by the creator of the MBean. Note that you can overwrite it when the given ObjectName is null
     *        otherwise the change is discarded (maybe a bug in JMX-RI).
     * @return the ObjectName
     * @throws Exception for any error
     */
    public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception {
        this.server = server;

        serviceName = getObjectName(server, name);

        return serviceName;
    }

    public void postRegister(Boolean registrationDone) {
        if (!registrationDone.booleanValue()) {
            log.debug("Registration is not done -> stop");
            stop();
        } else {
            state = REGISTERED;
            if (createIgnored) {
                try {
                    create();
                } catch (Exception e) {
                    log.error(ServiceMBeanLogger.ROOT_LOGGER.postRegisterInitializationFailed(), e);
                }
            }
        }
    }

    public void preDeregister() throws Exception {}

    public void postDeregister() {
        if (state != DESTROYED) {
            unregisterIgnored = true;
            if (log.isDebugEnabled()) {
                log.debug("Ignoring postDeregister call; current state is " + getStateString());
            }
            return;
        }
        unregisterIgnored = false;
        server = null;
        serviceName = null;
        state = UNREGISTERED;
    }

    // /////////////////////////////////////////////////////////////////////////
    // Concrete Service Overrides //
    // /////////////////////////////////////////////////////////////////////////

    /**
     * Sub-classes should override this method if they only need to set their object name during MBean pre-registration.
     *
     * @param server the mbeanserver
     * @param name the suggested name, maybe null
     * @return the object name
     * @throws javax.management.MalformedObjectNameException for a bad object name
     */
    protected ObjectName getObjectName(MBeanServer server, ObjectName name) throws MalformedObjectNameException {
        return name;
    }

    /**
     * Sub-classes should override this method to provide custum 'create' logic.
     *
     * <p>
     * This method is empty, and is provided for convenience when concrete service classes do not need to perform anything
     * specific for this state change.
     *
     * @throws Exception for any error
     */
    protected void createService() throws Exception {}

    /**
     * Sub-classes should override this method to provide custum 'start' logic.
     *
     * <p>
     * This method is empty, and is provided for convenience when concrete service classes do not need to perform anything
     * specific for this state change.
     *
     * @throws Exception for any error
     */
    protected void startService() throws Exception {}

    /**
     * Sub-classes should override this method to provide custum 'stop' logic.
     *
     * <p>
     * This method is empty, and is provided for convenience when concrete service classes do not need to perform anything
     * specific for this state change.
     *
     * @throws Exception for any error
     */
    protected void stopService() throws Exception {}

    /**
     * Sub-classes should override this method to provide custum 'destroy' logic.
     *
     * <p>
     * This method is empty, and is provided for convenience when concrete service classes do not need to perform anything
     * specific for this state change.
     *
     * @throws Exception for any error
     */
    protected void destroyService() throws Exception {}

    /**
     * The <code>nextNotificationSequenceNumber</code> method returns the next sequence number for use in notifications.
     *
     * @return a <code>long</code> value
     */
    public long nextNotificationSequenceNumber() {
        return sequenceNumber.incrementAndGet();
    }

    /**
     * The <code>getNextNotificationSequenceNumber</code> method returns the next sequence number for use in notifications.
     *
     * @return a <code>long</code> value
     */
    protected long getNextNotificationSequenceNumber() {
        return nextNotificationSequenceNumber();
    }

    /**
     * Helper for sending out state change notifications
     */
    private void sendStateChangeNotification(int oldState, int newState, String msg, Throwable t) {
        long now = System.currentTimeMillis();
        AttributeChangeNotification stateChangeNotification = new AttributeChangeNotification(this,
                getNextNotificationSequenceNumber(), now, msg, "State", "java.lang.Integer", new Integer(oldState),
                new Integer(newState));
        stateChangeNotification.setUserData(t);
        sendNotification(stateChangeNotification);
    }
}
