/*
 * 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.sling.launchpad.base.impl;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Dictionary;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.felix.framework.Logger;
import org.apache.sling.launchpad.api.StartupHandler;
import org.apache.sling.launchpad.api.StartupListener;
import org.apache.sling.launchpad.api.StartupMode;
import org.apache.sling.launchpad.api.StartupService;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.BundleListener;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.ServiceReference;
import org.osgi.service.startlevel.StartLevel;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;

/**
 * The installation listener is listening for
 * - framework events
 * - events from other services
 *
 * It notifies listeners about the finish of the startup and
 * about an approximate progress.
 *
 * @see StartupHandler
 * @see StartupListener
 * @since 2.4.0
 */
@SuppressWarnings("deprecation")
public class DefaultStartupHandler
    implements StartupHandler, BundleListener, FrameworkListener, Runnable {

    /** Logger. */
    private final Logger logger;

    /** Marker for finished. */
    private final AtomicBoolean finished = new AtomicBoolean(false);

    /** Marker if startup should wait */
    private final AtomicInteger startupShouldWait = new AtomicInteger(0);

    /** The queue for increasing the start level. */
    private final BlockingQueue<Boolean> queue = new LinkedBlockingQueue<>();

    /** The start level service. */
    private final StartLevel startLevelService;

    /** The target start level. */
    private final long targetStartLevel;

    /** The startup mode. */
    private final StartupMode startupMode;

    /** Service tracker for startup listeners. */
    private final ServiceTracker<StartupListener, StartupListener> listenerTracker;

    /** Expected bundle counts. */
    private final int expectedBundlesCount;

    /** Active bundle set. */
    private final Set<String> activeBundles = new HashSet<>();

    /** Bundle Context. */
    private final BundleContext bundleContext;

    /** Use incremental start level handling. */
    private final boolean useIncremental;

    /** MBean startup listener. */
    private final StartupListener mbeanStartupListener;

    /** The started time. */
    private final long startedAt;

    /** The startup manager. */
    private final StartupManager startupManager;

    private volatile Object[] logService;

    /**
     * Constructor.
     * @param context Bundle context
     * @param logger  Logger
     * @param manager The startup manager
     */
    public DefaultStartupHandler(final BundleContext context,
            final Logger logger,
            final StartupManager manager,
            final long startedAt) {
        this.logger = logger;
        this.bundleContext = context;
        this.startedAt = startedAt;
        this.startupMode = manager.getMode();
        this.targetStartLevel = manager.getTargetStartLevel();
        this.startupManager = manager;

        StartupListener listener = null;
        try {
            listener = new MBeanStartupListener();
        } catch ( final Exception ignore ) {
            // ignore
        }
        this.mbeanStartupListener = listener;
        this.listenerTracker = new ServiceTracker<>(context, StartupListener.class,
                new ServiceTrackerCustomizer<StartupListener, StartupListener>() {

                    @Override
                    public void removedService(final ServiceReference<StartupListener> reference, final StartupListener service) {
                        context.ungetService(reference);
                    }

                    @Override
                    public void modifiedService(final ServiceReference<StartupListener> reference, final StartupListener service) {
                        // nothing to do
                    }

                    @Override
                    public StartupListener addingService(final ServiceReference<StartupListener> reference) {
                        final StartupListener listener = context.getService(reference);
                        if (listener != null) {
                            try {
                                listener.inform(startupMode, finished.get());
                            } catch (final Throwable t) {
                                logger.log(Logger.LOG_ERROR, "Error calling StartupListener " + listener, t);
                            }
                        }
                        return listener;
                    }
                });
        this.listenerTracker.open();
        this.startLevelService = (StartLevel)context.getService(context.getServiceReference(StartLevel.class.getName()));
        context.addFrameworkListener(this);

        this.useIncremental = this.startupMode != StartupMode.RESTART && manager.isIncrementalStartupEnabled();

        if ( !this.useIncremental ) {
            final Bundle[] bundles = context.getBundles();
            this.expectedBundlesCount = (bundles != null && bundles.length > 0 ? bundles.length : 10);

            context.addBundleListener(this);
        } else {
            this.expectedBundlesCount = 10;
        }

        this.bundleContext.registerService(StartupHandler.class.getName(), this, null);
        this.log(Logger.LOG_INFO, "Started startup handler with target start level="
               + String.valueOf(this.targetStartLevel) + ", and expected bundle count=" + String.valueOf(this.expectedBundlesCount));
        final Thread t = new Thread(this);
        t.start();
    }

    /**
     * @see org.apache.sling.launchpad.api.StartupHandler#getMode()
     */
    @Override
    public StartupMode getMode() {
        return this.startupMode;
    }

    /**
     * @see org.apache.sling.launchpad.api.StartupHandler#isFinished()
     */
    @Override
    public boolean isFinished() {
        return this.finished.get();
    }

    /**
     * @see java.lang.Runnable#run()
     */
    @Override
    public void run() {
        while ( !this.finished.get() ) {
            Boolean doInc = null;
            try {
                doInc = this.queue.take();
            } catch (final InterruptedException e) {
                // ignore
            }
            if ( doInc != null && doInc ) {
                // if the installer is idle we first wait
                // we have to do this to give the installer or plugins for the installer,
                // time to start after the start level has changed
                if ( this.startupShouldWait.get() == 0 ) {
                    this.sleep(2000L);
                }
                // now we wait until the installer is idle
                while ( this.startupShouldWait.get() != 0 ) {
                    this.sleep(50L);
                }
                this.incStartLevel();
            }
        }
    }

    /**
     * Increment the current start level by one
     */
    private void incStartLevel() {
        final int newLevel = this.startLevelService.getStartLevel() + 1;
        this.log(Logger.LOG_DEBUG, "Increasing start level to " + String.valueOf(newLevel));
        this.startLevelService.setStartLevel(newLevel);
    }

    /**
     * @see org.apache.sling.launchpad.api.StartupHandler#waitWithStartup(boolean)
     */
    @Override
    public void waitWithStartup(final boolean flag) {
        this.log(Logger.LOG_DEBUG, "Wait with startup " + flag);
        if ( flag ) {
            this.startupShouldWait.incrementAndGet();
        } else {
            this.startupShouldWait.decrementAndGet();
        }
    }

    /**
     * Sleep a little bit
     * @param time Sleeping time
     */
    private void sleep(final long time) {
        try {
            Thread.sleep(time);
        } catch ( final InterruptedException e) {
            // ignore
        }
    }

    /**
     * Put a task in the queue
     * @param info Add new boolean information to queue
     */
    private void enqueue(final boolean info) {
        try {
            this.queue.put(info);
        } catch (final InterruptedException e) {
            // ignore
        }
    }

    /**
     * @see org.osgi.framework.FrameworkListener#frameworkEvent(org.osgi.framework.FrameworkEvent)
     */
    @Override
    public void frameworkEvent(final FrameworkEvent event) {
        if ( finished.get() ) {
            return;
        }
        this.log(Logger.LOG_DEBUG, "Received framework event " + event);

        if ( !this.useIncremental ) {
            // restart
            if ( event.getType() == FrameworkEvent.STARTED ) {
                this.startupFinished();
            }

        } else {
            // first startup or update
            if ( event.getType() == FrameworkEvent.STARTED ) {
                this.enqueue(true);

            } else if ( event.getType() == FrameworkEvent.STARTLEVEL_CHANGED ) {
                if ( this.startLevelService.getStartLevel() >= this.targetStartLevel ) {
                    this.startupFinished();
                } else {
                    this.enqueue(true);
                    final int startLevel = this.startLevelService.getStartLevel();
                    this.log(Logger.LOG_DEBUG, "Startup progress " + String.valueOf(startLevel) + '/' + String.valueOf(targetStartLevel));
                    final float ratio = (float) startLevel / (float) targetStartLevel;
                    this.startupProgress(ratio);
                }
            }
        }
    }

    private void log(final int level, final String msg) {
        log(null, level, msg, null);
    }

    private void log(final int level, final String msg, final Throwable t) {
        log(null, level, msg, t);
    }

    private void log(final ServiceReference<?> sRef, final int level, final String msg, final Throwable t) {
        boolean loggedWithService = false;
        if ( this.logService == null ) {
            final ServiceReference<?> ref = this.bundleContext.getServiceReference("org.osgi.service.log.LogService");
            if ( ref != null ) {
                final Object ls = this.bundleContext.getService(ref);
                if ( ls != null ) {
                    final Class<?>[] formalParams = {
                            ServiceReference.class,
                            Integer.TYPE,
                            String.class,
                            Throwable.class
                        };

                    try {
                        final Method logMethod = ls.getClass().getMethod("log", formalParams);
                        logMethod.setAccessible(true);
                        logService = new Object[] { ls, logMethod };
                    } catch (final NoSuchMethodException ex) {
                        // no need to log
                    }
                }
            }
        }
        if ( this.logService != null ) {
            final Object[] params = {sRef, new Integer(level), msg, t};
            try {
                ((Method) this.logService[1]).invoke(this.logService[0], params);
                loggedWithService = true;
            } catch (final InvocationTargetException ex) {
                // no need to log
            } catch (final IllegalAccessException ex) {
                // no need to log
            }
        }
        if ( !loggedWithService ) {
            logger.log(level, msg);
        }
    }

    /**
     * Notify finished startup
     */
    private void startupFinished() {
        this.log(Logger.LOG_INFO, "Startup finished in " + String.valueOf(System.currentTimeMillis() - this.startedAt) + "ms");
        this.finished.set(true);

        for (final StartupListener listener : this.listenerTracker.getServices(new StartupListener[0])) {
            try {
                listener.startupFinished(this.startupMode);
            } catch (Throwable t) {
                this.log(Logger.LOG_ERROR, "Error calling StartupListener " + listener, t);
            }
        }
        if ( this.mbeanStartupListener != null ) {
            this.mbeanStartupListener.startupFinished(this.startupMode);
        }

        // stop the queue
        this.enqueue(false);

        // clear bundle set
        this.activeBundles.clear();

        // unregister listeners
        if ( !this.useIncremental ) {
            this.bundleContext.removeBundleListener(this);
        }
        this.bundleContext.removeFrameworkListener(this);

        // register startup service
        final Dictionary<String, Object> serviceProps = new Hashtable<>();
        serviceProps.put(StartupMode.class.getName(), this.startupMode.name());
        serviceProps.put(Constants.SERVICE_DESCRIPTION, "Apache Sling Startup Service");
        serviceProps.put(Constants.SERVICE_VENDOR, "The Apache Software Foundation");
        this.bundleContext.registerService(StartupService.class, new StartupService() {

            @Override
            public StartupMode getStartupMode() {
                return startupMode;
            }

            }, serviceProps);

        // update timestamp
        this.startupManager.markInstalled();
    }

    /**
     * Notify startup progress
     * @param ratio ratio
     */
    private void startupProgress(final float ratio) {
        for (final StartupListener listener : this.listenerTracker.getServices(new StartupListener[0])) {
            try {
                listener.startupProgress(ratio);
            } catch (final Throwable t) {
                this.log(Logger.LOG_ERROR, "Error calling StartupListener " + listener, t);
            }
        }
        if ( this.mbeanStartupListener != null ) {
            this.mbeanStartupListener.startupProgress(ratio);
        }
    }

    /**
     * @see org.osgi.framework.BundleListener#bundleChanged(org.osgi.framework.BundleEvent)
     */
    @Override
    public void bundleChanged(final BundleEvent event) {
        if (!finished.get()) {
            this.log(Logger.LOG_DEBUG, "Received bundle event " + event);

            if (event.getType() == BundleEvent.RESOLVED || event.getType() == BundleEvent.STARTED) {
                // Add (if not existing) bundle to active bundles and refresh progress bar
                activeBundles.add(event.getBundle().getSymbolicName());

                this.log(Logger.LOG_DEBUG, "Startup progress " + String.valueOf(activeBundles.size()) + '/' + String.valueOf(expectedBundlesCount));
                final float ratio = (float) activeBundles.size() / (float) expectedBundlesCount;
                this.startupProgress(ratio);
            } else if (event.getType() == BundleEvent.STOPPED) {
                // Only remove bundle from active bundles,
                // but do not refresh progress bar, to prevent progress bar from going back
                activeBundles.remove(event.getBundle().getSymbolicName());
            }
        }
    }
}
