/*
 * 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.felix.framework;

import java.util.ArrayList;
import java.util.List;

import org.osgi.framework.AdminPermission;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.startlevel.BundleStartLevel;
import org.osgi.framework.startlevel.FrameworkStartLevel;
import org.osgi.service.startlevel.StartLevel;

class FrameworkStartLevelImpl implements FrameworkStartLevel, Runnable
{
    static final String THREAD_NAME = "FelixStartLevel";

    private static final int BUNDLE_IDX = 0;
    private static final int STARTLEVEL_IDX = 1;

    private final Felix m_felix;
    private final ServiceRegistry m_registry;
    private final List m_requests = new ArrayList();
    private final List<FrameworkListener[]> m_requestListeners
        = new ArrayList<FrameworkListener[]>();
    private ServiceRegistration<StartLevel> m_slReg;
    private Thread m_thread = null;

    FrameworkStartLevelImpl(Felix felix, ServiceRegistry registry)
    {
        m_felix = felix;
        m_registry = registry;
    }

    @SuppressWarnings("unchecked")
    void start()
    {
        m_slReg = (ServiceRegistration<StartLevel>) m_registry.registerService(m_felix._getBundleContext(),
                new String[] { StartLevel.class.getName() },
                new StartLevelImpl(m_felix),
                null);
    }

    // Should only be called hold requestList lock.
    private void startThread()
    {
        // Start a thread to perform asynchronous package refreshes.
        if (m_thread == null)
        {
            m_thread = new Thread(this, THREAD_NAME);
            m_thread.setDaemon(true);
            m_thread.start();
        }
    }

    /**
     * Stops the FelixStartLevel thread on system shutdown. Shutting down the
     * thread explicitly is required in the embedded case, where Felix may be
     * stopped without the Java VM being stopped. In this case the
     * FelixStartLevel thread must be stopped explicitly.
     * <p>
     * This method is called by the
     * {@link StartLevelActivator#stop(BundleContext)} method.
     */
    void stop()
    {
        synchronized (m_requests)
        {
            if (m_thread != null)
            {
                // Null thread variable to signal to the thread that
                // we want it to exit.
                m_thread = null;

                // Wake up the thread, if it is currently in the wait() state
                // for more work.
                m_requests.notifyAll();
            }
        }
    }

    public Bundle getBundle()
    {
        return m_felix;
    }

    public int getStartLevel()
    {
        return m_felix.getActiveStartLevel();
    }

    public void setStartLevel(int startlevel, FrameworkListener... listeners)
    {
        Object sm = System.getSecurityManager();

        if (sm != null)
        {
            ((SecurityManager) sm).checkPermission(
                new AdminPermission(m_felix, AdminPermission.STARTLEVEL));
        }

        if (startlevel <= 0)
        {
            throw new IllegalArgumentException(
                "Start level must be greater than zero.");
        }

        synchronized (m_requests)
        {
            if (m_thread == null)
            {
                throw new IllegalStateException("No inital startlevel yet");
            }
            // Queue request.
            m_requestListeners.add(listeners);
            m_requests.add(new Integer(startlevel));
            m_requests.notifyAll();
        }
    }

    /**
     * This method is currently only called by the by the thread that calls
     * the Felix.start() method and the shutdown thread when the
     * framework is shutting down.
     * @param startlevel
    **/
    /* package */ void setStartLevelAndWait(int startlevel)
    {
        Object request = new Integer(startlevel);
        synchronized (request)
        {
            synchronized (m_requests)
            {
                // Start thread if necessary.
                startThread();
                // Queue request.
                m_requestListeners.add(null);
                m_requests.add(request);
                m_requests.notifyAll();
            }

            try
            {
                request.wait();
            }
            catch (InterruptedException ex)
            {
                // Log it and ignore since it won't cause much of an issue.
                m_felix.getLogger().log(
                    Logger.LOG_WARNING,
                    "Wait for start level change during shutdown interrupted.",
                    ex);
            }
        }
    }

    public int getInitialBundleStartLevel()
    {
        return m_felix.getInitialBundleStartLevel();
    }

    public void setInitialBundleStartLevel(int startlevel)
    {
        Object sm = System.getSecurityManager();

        if (sm != null)
        {
            ((SecurityManager) sm).checkPermission(
                new AdminPermission(m_felix, AdminPermission.STARTLEVEL));
        }
        m_felix.setInitialBundleStartLevel(startlevel);
    }

    BundleStartLevel createBundleStartLevel(BundleImpl bundle)
    {
        return new BundleStartLevelImpl(bundle);
    }

    class BundleStartLevelImpl implements BundleStartLevel
    {
        private BundleImpl m_bundle;

        private BundleStartLevelImpl(BundleImpl bundle)
        {
            m_bundle = bundle;
        }

        public Bundle getBundle()
        {
            return m_bundle;
        }

        public int getStartLevel()
        {
            return m_felix.getBundleStartLevel(m_bundle);
        }

        public void setStartLevel(int startlevel)
        {
            Object sm = System.getSecurityManager();

            if (sm != null)
            {
                ((SecurityManager) sm).checkPermission(
                    new AdminPermission(m_bundle, AdminPermission.EXECUTE));
            }

            if (m_bundle.getBundleId() == 0)
            {
                throw new IllegalArgumentException(
                    "Cannot change system bundle start level.");
            }
            else if (startlevel <= 0)
            {
                throw new IllegalArgumentException(
                    "Start level must be greater than zero.");
            }
            synchronized (m_requests)
            {
                // Start thread if necessary.
                startThread();
                // Synchronously persists the start level.
                m_bundle.setStartLevel(startlevel);
                // Queue request.
                m_requestListeners.add(null);
                m_requests.add(new Object[] { m_bundle, new Integer(startlevel) });
                m_requests.notifyAll();
            }
        }

        public boolean isPersistentlyStarted()
        {
            return m_felix.isBundlePersistentlyStarted(m_bundle);
        }

        public boolean isActivationPolicyUsed()
        {
            return m_felix.isBundleActivationPolicyUsed(m_bundle);
        }
    }

    public void run()
    {
        // This thread loops forever, thus it should
        // be a daemon thread.
        Object previousRequest = null;
        while (true)
        {
            Object request = null;
            FrameworkListener[] listeners = null;
            synchronized (m_requests)
            {
                // Wait for a request.
                while (m_requests.isEmpty())
                {
                    // Terminate the thread if requested to do so (see stop()).
                    if (m_thread == null)
                    {
                        return;
                    }

                    try
                    {
                        m_requests.wait();
                    }
                    catch (InterruptedException ex)
                    {
                        // Ignore.
                    }
                }

                // Get the requested start level.
                request = m_requests.remove(0);
                listeners = m_requestListeners.remove(0);
            }

            // If the request object is an Integer, then the request
            // is to set the framework start level. If the request is
            // an Object array, then the request is to set the start
            // level for a bundle.
            // NOTE: We don't catch any exceptions here, because
            // the invoked methods shield us from exceptions by
            // catching Throwables when they invoke callbacks.
            if (request instanceof Integer)
            {
                // Set the new framework start level.
                try
                {
                    m_felix.setActiveStartLevel(((Integer) request).intValue(), listeners);
                }
                catch (IllegalStateException ise)
                {
                    // Thrown if global lock cannot be acquired, in which case
                    // just retry (unless we already did)
                    if (previousRequest == request)
                    {
                        m_felix.getLogger().log(Logger.LOG_ERROR,
                            "Unexpected problem setting active start level to " + request, ise);
                    }
                    else
                    {
                        synchronized (m_requests)
                        {
                            m_requests.add(0, request);
                            previousRequest = request;
                        }
                    }
                }
                catch (Exception ex)
                {
                    m_felix.getLogger().log(Logger.LOG_ERROR,
                        "Unexpected problem setting active start level to " + request, ex);
                }
            }
            else
            {
                Bundle bundle = (Bundle) ((Object[]) request)[BUNDLE_IDX];
                int startlevel = ((Integer) ((Object[]) request)[STARTLEVEL_IDX]).intValue();
                m_felix.setBundleStartLevel(bundle, startlevel);
            }

            // Notify any waiting thread that this request is done.
            synchronized (request)
            {
                request.notifyAll();
            }
        }
    }
}