/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.camel.support;

import java.io.InputStream;
import java.util.Properties;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.camel.ServiceStatus;
import org.apache.camel.StatefulService;
import org.apache.camel.util.IOHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A useful base class which ensures that a service is only initialized once and
 * provides some helper methods for enquiring of its status.
 * <p/>
 * Implementations can extend this base class and implement {@link org.apache.camel.SuspendableService}
 * in case they support suspend/resume.
 * <p/>
 * <b>Important: </b> You should override the lifecycle methods that start with <tt>do</tt>, eg {@link #doStart()}},
 * {@link #doStop()}, etc. where you implement your logic. The methods {@link #start()}, {@link #stop()} should
 * <b>NOT</b> be overriden as they are used internally to keep track of the state of this service and properly
 * invoke the operation in a safe manner.
 *
 * @version 
 */
public abstract class ServiceSupport implements StatefulService {
    private static final Logger LOG = LoggerFactory.getLogger(ServiceSupport.class);

    protected final AtomicBoolean started = new AtomicBoolean(false);
    protected final AtomicBoolean starting = new AtomicBoolean(false);
    protected final AtomicBoolean stopping = new AtomicBoolean(false);
    protected final AtomicBoolean stopped = new AtomicBoolean(false);
    protected final AtomicBoolean suspending = new AtomicBoolean(false);
    protected final AtomicBoolean suspended = new AtomicBoolean(false);
    protected final AtomicBoolean shuttingdown = new AtomicBoolean(false);
    protected final AtomicBoolean shutdown = new AtomicBoolean(false);

    private String version;

    /**
     * <b>Important: </b> You should override the lifecycle methods that start with <tt>do</tt>, eg {@link #doStart()},
     * {@link #doStop()}, etc. where you implement your logic. The methods {@link #start()}, {@link #stop()} should
     * <b>NOT</b> be overriden as they are used internally to keep track of the state of this service and properly
     * invoke the operation in a safe manner.
     */
    public void start() throws Exception {
        if (isStarting() || isStarted()) {
            // only start service if not already started
            LOG.trace("Service already started");
            return;
        }
        if (starting.compareAndSet(false, true)) {
            LOG.trace("Starting service");
            try {
                doStart();
                started.set(true);
                starting.set(false);
                stopping.set(false);
                stopped.set(false);
                suspending.set(false);
                suspended.set(false);
                shutdown.set(false);
                shuttingdown.set(false);
            } catch (Exception e) {
                try {
                    stop();
                } catch (Exception e2) {
                    // Ignore exceptions as we want to show the original exception
                } finally {
                    // ensure flags get reset to stopped as we failed during starting
                    stopping.set(false);
                    stopped.set(true);
                    starting.set(false);
                    started.set(false);
                    suspending.set(false);
                    suspended.set(false);
                    shutdown.set(false);
                    shuttingdown.set(false);
                }
                throw e;
            } 
        }
    }

    /**
     * <b>Important: </b> You should override the lifecycle methods that start with <tt>do</tt>, eg {@link #doStart()},
     * {@link #doStop()}, etc. where you implement your logic. The methods {@link #start()}, {@link #stop()} should
     * <b>NOT</b> be overriden as they are used internally to keep track of the state of this service and properly
     * invoke the operation in a safe manner.
     */
    public void stop() throws Exception {
        if (isStopped()) {
            LOG.trace("Service already stopped");
            return;
        }
        if (isStopping()) {
            LOG.trace("Service already stopping");
            return;
        }
        stopping.set(true);
        try {
            doStop();
        } finally {
            stopping.set(false);
            stopped.set(true);
            starting.set(false);
            started.set(false);
            suspending.set(false);
            suspended.set(false);
            shutdown.set(false);
            shuttingdown.set(false);            
        }
    }

    /**
     * <b>Important: </b> You should override the lifecycle methods that start with <tt>do</tt>, eg {@link #doStart()},
     * {@link #doStop()}, etc. where you implement your logic. The methods {@link #start()}, {@link #stop()} should
     * <b>NOT</b> be overriden as they are used internally to keep track of the state of this service and properly
     * invoke the operation in a safe manner.
     */
    @Override
    public void suspend() throws Exception {
        if (!suspended.get()) {
            if (suspending.compareAndSet(false, true)) {
                try {
                    starting.set(false);
                    stopping.set(false);
                    doSuspend();
                } finally {
                    stopped.set(false);
                    stopping.set(false);
                    starting.set(false);
                    started.set(false);
                    suspending.set(false);
                    suspended.set(true);
                    shutdown.set(false);
                    shuttingdown.set(false);
                }
            }
        }
    }

    /**
     * <b>Important: </b> You should override the lifecycle methods that start with <tt>do</tt>, eg {@link #doStart()},
     * {@link #doStop()}, etc. where you implement your logic. The methods {@link #start()}, {@link #stop()} should
     * <b>NOT</b> be overriden as they are used internally to keep track of the state of this service and properly
     * invoke the operation in a safe manner.
     */
    @Override
    public void resume() throws Exception {
        if (suspended.get()) {
            if (starting.compareAndSet(false, true)) {
                try {
                    doResume();
                } finally {
                    started.set(true);
                    starting.set(false);
                    stopping.set(false);
                    stopped.set(false);
                    suspending.set(false);
                    suspended.set(false);
                    shutdown.set(false);
                    shuttingdown.set(false);
                }
            }
        }
    }

    /**
     * <b>Important: </b> You should override the lifecycle methods that start with <tt>do</tt>, eg {@link #doStart()},
     * {@link #doStop()}, etc. where you implement your logic. The methods {@link #start()}, {@link #stop()} should
     * <b>NOT</b> be overriden as they are used internally to keep track of the state of this service and properly
     * invoke the operation in a safe manner.
     */
    @Override
    public void shutdown() throws Exception {
        if (shutdown.get()) {
            LOG.trace("Service already shut down");
            return;
        }
        // ensure we are stopped first
        stop();

        if (shuttingdown.compareAndSet(false, true)) {
            try {
                doShutdown();
            } finally {
                // shutdown is also stopped so only set shutdown flags
                shutdown.set(true);
                shuttingdown.set(false);
            }
        }
    }

    @Override
    public ServiceStatus getStatus() {
        // we should check the ---ing states first, as this indicate the state is in the middle of doing that
        if (isStarting()) {
            return ServiceStatus.Starting;
        }
        if (isStopping()) {
            return ServiceStatus.Stopping;
        }
        if (isSuspending()) {
            return ServiceStatus.Suspending;
        }

        // then check for the regular states
        if (isStarted()) {
            return ServiceStatus.Started;
        }
        if (isStopped()) {
            return ServiceStatus.Stopped;
        }
        if (isSuspended()) {
            return ServiceStatus.Suspended;
        }

        // use stopped as fallback
        return ServiceStatus.Stopped;
    }
    
    @Override
    public boolean isStarted() {
        return started.get();
    }

    @Override
    public boolean isStarting() {
        return starting.get();
    }

    @Override
    public boolean isStopping() {
        return stopping.get();
    }

    @Override
    public boolean isStopped() {
        return stopped.get();
    }

    @Override
    public boolean isSuspending() {
        return suspending.get();
    }

    @Override
    public boolean isSuspended() {
        return suspended.get();
    }

    @Override
    public boolean isRunAllowed() {
        // if we have not yet initialized, then all options is false
        boolean unused1 = !started.get() && !starting.get() && !stopping.get() && !stopped.get();
        boolean unused2 = !suspending.get() && !suspended.get() && !shutdown.get() && !shuttingdown.get();
        if (unused1 && unused2) {
            return false;
        }
        return !isStoppingOrStopped();
    }

    /**
     * Is the service in progress of being stopped or already stopped
     */
    public boolean isStoppingOrStopped() {
        return stopping.get() || stopped.get();
    }

    /**
     * Is the service in progress of being suspended or already suspended
     */
    public boolean isSuspendingOrSuspended() {
        return suspending.get() || suspended.get();
    }

    /**
     * Implementations override this method to support customized start/stop.
     * <p/>
     * <b>Important: </b> See {@link #doStop()} for more details.
     * 
     * @see #doStop()
     */
    protected abstract void doStart() throws Exception;

    /**
     * Implementations override this method to support customized start/stop.
     * <p/>
     * <b>Important:</b> Camel will invoke this {@link #doStop()} method when
     * the service is being stopped. This method will <b>also</b> be invoked
     * if the service is still in <i>uninitialized</i> state (eg has not
     * been started). The method is <b>always</b> called to allow the service
     * to do custom logic when the service is being stopped, such as when
     * {@link org.apache.camel.CamelContext} is shutting down.
     * 
     * @see #doStart() 
     */
    protected abstract void doStop() throws Exception;

    /**
     * Implementations override this method to support customized suspend/resume.
     */
    protected void doSuspend() throws Exception {
        // noop
    }

    /**
     * Implementations override this method to support customized suspend/resume.
     */
    protected void doResume() throws Exception {
        // noop
    }

    /**
     * Implementations override this method to perform customized shutdown.
     */
    protected void doShutdown() throws Exception {
        // noop
    }

    @Override
    public synchronized String getVersion() {
        if (version != null) {
            return version;
        }
        InputStream is = null;
        // try to load from maven properties first
        try {
            Properties p = new Properties();
            is = getClass().getResourceAsStream("/META-INF/maven/org.apache.camel/camel-core/pom.properties");
            if (is != null) {
                p.load(is);
                version = p.getProperty("version", "");
            }
        } catch (Exception e) {
            // ignore
        } finally {
            if (is != null) {
                IOHelper.close(is);
            }
        }

        // fallback to using Java API
        if (version == null) {
            Package aPackage = getClass().getPackage();
            if (aPackage != null) {
                version = aPackage.getImplementationVersion();
                if (version == null) {
                    version = aPackage.getSpecificationVersion();
                }
            }
        }

        if (version == null) {
            // we could not compute the version so use a blank
            version = "";
        }

        return version;
    }
}
