/*
 * Copyright (c) 2003-2016, KNOPFLERFISH project
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following
 * conditions are met:
 *
 * - Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistributions in binary form must reproduce the above
 *   copyright notice, this list of conditions and the following
 *   disclaimer in the documentation and/or other materials
 *   provided with the distribution.
 *
 * - Neither the name of the KNOPFLERFISH project nor the names of its
 *   contributors may be used to endorse or promote products derived
 *   from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package org.knopflerfish.framework;

import java.io.File;
import java.util.Vector;

import org.osgi.framework.Bundle;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkEvent;
import org.osgi.framework.FrameworkListener;
import org.osgi.framework.ServiceFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.framework.startlevel.BundleStartLevel;
import org.osgi.framework.startlevel.FrameworkStartLevel;
import org.osgi.framework.startlevel.dto.BundleStartLevelDTO;
import org.osgi.framework.startlevel.dto.FrameworkStartLevelDTO;
import org.osgi.service.startlevel.StartLevel;


/**
 * StartLevel service implementation.
 *
 */
@SuppressWarnings("deprecation")
public class StartLevelController
  implements Runnable, ServiceFactory<StartLevel>
{
  // The version of the StartLevel service API
  public static final String SPEC_VERSION = "1.1";

  // The version of the StartLevel API
  public static final String API_SPEC_VERSION = "1.0";

  final static int START_MIN = 0;
  final static int START_MAX = Integer.MAX_VALUE;

  final static String INITIAL_LEVEL_FILE = "initiallevel";

  Thread        wc;
  boolean       bRun     = false;
  Queue<Runnable> jobQueue = new Queue<Runnable>(0);

  int currentLevel     = 0;
  int initStartLevel   = 1;
  int targetStartLevel = currentLevel;

  final FrameworkContext fwCtx;

  FileTree storage;

  // Set to true indicates startlevel compatibility mode.
  // all bundles and current start level will be 1
  final boolean  bCompat /*= false*/;

  final private boolean readOnly;


  StartLevelController(FrameworkContext fwCtx)
  {
    this.fwCtx = fwCtx;
    bCompat = fwCtx.props.getBooleanProperty(FWProps.STARTLEVEL_COMPAT_PROP);

    readOnly = fwCtx.props.getBooleanProperty(FWProps.READ_ONLY_PROP);
    storage = Util.getFileStorage(fwCtx, "startlevel", !readOnly);

    // Create first job to goto beginning start-level
    setStartLevel0(-1, false);
 
    // restoreState just reads from persistent storage
    // open() needs to be called to actually do the work
    // This is done after framework has been launched.
    restoreState();
  }

  void open() {
    if (fwCtx.debug.startlevel) {
      fwCtx.debug.println("startlevel: open");
    }

    final Runnable beginJob = jobQueue.firstElement();
    wc = new Thread(fwCtx.threadGroup, this, "startlevel job");
    synchronized (beginJob) {
      bRun = true;
      wc.start();
      // Wait for the last of the jobs scheduled before starting the
      // framework to complete before return
      try {
        beginJob.wait();
      } catch (final InterruptedException _ignore) { }
    }
  }

  /**
   * Load persistent state from storage and set up all actions
   * necessary to bump bundle states. If no persistent state was found,
   * try to set the target start level from the beginning start level
   * framework property.
   *
   * <p>Note that {@link open()} needs to be called for any work to
   * be done.</p>
   */
  private void restoreState() {
    if (fwCtx.debug.startlevel) {
      fwCtx.debug.println("startlevel: restoreState");
    }

    // Restore the initial bundle start level
    if (storage != null) {
      try {
        final String s = Util.getContent(new File(storage, INITIAL_LEVEL_FILE));
        if (s != null) {
          setInitialBundleStartLevel0(Integer.parseInt(s), false);
        }
      } catch (final Exception _ignored) { }
    }
  }


  void shutdown() {
    synchronized (wc) {
      setStartLevel0(0, false);
      while (bRun) {
        try {
          wc.join();
        } catch (InterruptedException _ignore) { }
      }
    }
    if (fwCtx.debug.startlevel) {
      fwCtx.debug.println("*** closed startlevel service");
    }
  }

  public void run() {
    while(bRun) {
      try {
        final Runnable job = jobQueue.remove();
        if (job != null) {
          job.run();
          synchronized (job) {
            if (currentLevel == 0) {
              bRun = false;
              jobQueue.close();
            }
            job.notifyAll();
          }
        }
      } catch (final Exception ignored) {
        ignored.printStackTrace();
      }
    }
  }


  int getStartLevel() {
    return currentLevel;
  }

  void setStartLevel(final int startLevel)
  {
    setStartLevel(startLevel, (FrameworkListener[]) null);
  }

  void setStartLevel(final int startLevel, final FrameworkListener... listeners)
  {
    if (!bRun) {
      throw new IllegalStateException("StartLevelService isn't running yet");
    }
    fwCtx.perm.checkStartLevelAdminPerm();
    if (startLevel <= 0) {
      throw new IllegalArgumentException("Initial start level must be > 0, is "
                                         + startLevel);
    }
    // No start-level changed events if called before open() or after close().
    setStartLevel0(startLevel, true, listeners);
  }

  private void setStartLevel0(final int startLevel,
                              final boolean notifyFw,
                              final FrameworkListener... listeners)
  {
    if (fwCtx.debug.startlevel) {
      fwCtx.debug.println("startlevel: setStartLevel " + startLevel);
    }

    jobQueue.insert(new Runnable() {
      public void run() {
        final int sl = bCompat ? 1 : startLevel;

        // Set the beginning start level to go to when open() is called.
        if (sl == -1) {
          final String sBeginningLevel
            = fwCtx.props.getProperty(Constants.FRAMEWORK_BEGINNING_STARTLEVEL);
          try {
            targetStartLevel = Integer.parseInt(sBeginningLevel);
          } catch (final NumberFormatException nfe) {
            fwCtx.debug.printStackTrace("Invalid number '" + sBeginningLevel +
                                        "' in value of property named '"
                                        + Constants.FRAMEWORK_BEGINNING_STARTLEVEL
                                        + "'.", nfe);
            targetStartLevel = 1;
          }
          if (fwCtx.debug.startlevel) {
            fwCtx.debug.println("startlevel: beginning level " + targetStartLevel);
          }
        } else {
          targetStartLevel = sl;
        }

        while (targetStartLevel > currentLevel) {
          increaseStartLevel();
        }

        while (targetStartLevel < currentLevel) {
          decreaseStartLevel();
        }

        if (notifyFw) {
          final FrameworkEvent event
            = new FrameworkEvent(FrameworkEvent.STARTLEVEL_CHANGED,
                                 fwCtx.systemBundle, null);
          // Send event to all registered framework listeners
          fwCtx.listeners.frameworkEvent(event, listeners);
        }
      }
    });
  }


  Object lock = new Object();


  void increaseStartLevel() {
    synchronized (lock) {

      currentLevel++;

      if (fwCtx.debug.startlevel) {
        fwCtx.debug.println("startlevel: increaseStartLevel currentLevel="
                            + currentLevel);
      }
      final Vector<BundleImpl> set = new Vector<BundleImpl>();

      for (final BundleImpl bs : fwCtx.bundles.getBundles()) {
        BundleArchive archive = bs.current().archive;
        if (archive != null) {
          if (bs.getStartLevel() == currentLevel) {
            if (bs.current().archive.getAutostartSetting()!=-1) {
              set.addElement(bs);
            }
          }
        }
      }

      Util.sort(set, BSComparator, false);

      for (int i = 0; i < set.size(); i++) {
        final BundleImpl bs = set.elementAt(i);
        BundleArchive archive = bs.current().archive;
        if (archive != null) {
          try {
            if (bs.current().archive.getAutostartSetting()!=-1) {
              if (fwCtx.debug.startlevel) {
                fwCtx.debug.println("startlevel: start " + bs);
              }
              int startOptions = Bundle.START_TRANSIENT;
              if (isBundleActivationPolicyUsed(bs.current().archive)) {
                startOptions |= Bundle.START_ACTIVATION_POLICY;
              }
              bs.start(startOptions);
            }
          } catch (final IllegalStateException ignore) {
            // Tried to start an uninstalled bundle, skip
          } catch (final Exception e) {
            fwCtx.frameworkError(bs, e);
          }
        }
      }
    }
  }


  void decreaseStartLevel() {
    synchronized (lock) {
      currentLevel--;

      final Vector<BundleImpl> set = new Vector<BundleImpl>();

      for (final BundleImpl bs : fwCtx.bundles.getBundles()) {
        if (bs.getState() == Bundle.ACTIVE ||
            (bs.getState() == Bundle.STARTING && bs.current().lazyActivation)) {
          if (bs.getStartLevel() == currentLevel + 1) {
            set.addElement(bs);
          }
        }
      }

      Util.sort(set, BSComparator, true);

      synchronized (fwCtx.resolver) {
        for (int i = 0; i < set.size(); i++) {
          final BundleImpl bs = set.elementAt(i);
          if (bs.getState() == Bundle.ACTIVE ||
              (bs.getState() == Bundle.STARTING && bs.current().lazyActivation)) {
            if (fwCtx.debug.startlevel) {
              fwCtx.debug.println("startlevel: stop " + bs);
            }

            try {
              bs.stop(Bundle.STOP_TRANSIENT);
            } catch (final Throwable t) {
              fwCtx.frameworkError(bs, t);
            }
          }
        }
      }
    }
  }


  boolean canStart(BundleImpl b) {
    return b.getState() != Bundle.UNINSTALLED;
  }


  static final Util.Comparator<BundleImpl,BundleImpl>
    BSComparator = new Util.Comparator<BundleImpl,BundleImpl>() {
    public int compare(BundleImpl b1, BundleImpl b2)
    {
      int res = b1.getStartLevel() - b2.getStartLevel();
      if (res == 0) {
        res = (int) (b1.getBundleId() - b2.getBundleId());
      }
      return res;
    }
  };

  int getBundleStartLevel(BundleImpl bundle) {
    if (bundle.getBundleId() == 0) {
      return 0;
    }
    return bundle.getStartLevel();
  }


  void setBundleStartLevel(final BundleImpl bundle, final int startLevel) {
    fwCtx.perm.checkExecuteAdminPerm(bundle);

    if (startLevel <= 0) {
      throw new IllegalArgumentException("Initial start level must be > 0, is " + startLevel);
    }

    if (bundle.getBundleId() == 0) {
      throw new IllegalArgumentException("System bundle start level cannot be changed");
    }

    fwCtx.perm.callSetStartLevel(bundle, bCompat ? 1 : startLevel);

    if (bRun) {
      jobQueue.insert(new Runnable() {
        public void run() {
          syncStartLevel(bundle);
        }
      });
    }
  }


  void syncStartLevel(BundleImpl bs) {
    try {
      if (fwCtx.debug.startlevel) {
        fwCtx.debug.println("syncstartlevel: " + bs);
      }
      synchronized (lock) {
        synchronized (fwCtx.resolver) {
          if (bs.getStartLevel() <= currentLevel) {
            final BundleGeneration current = bs.current();
            if ((bs.getState() & (Bundle.INSTALLED|Bundle.RESOLVED|Bundle.STOPPING)) != 0
                && current.archive.getAutostartSetting()!=-1) {
              if (fwCtx.debug.startlevel) {
                fwCtx.debug.println("startlevel: start " + bs);
              }
              int startOptions = Bundle.START_TRANSIENT;
              if (isBundleActivationPolicyUsed(current.archive)) {
                startOptions |= Bundle.START_ACTIVATION_POLICY;
              }
              bs.start(startOptions);
            }
          } else {
            if ((bs.getState() & (Bundle.ACTIVE|Bundle.STARTING)) != 0) {
              if (fwCtx.debug.startlevel) {
                fwCtx.debug.println("startlevel: stop " + bs);
              }
              bs.stop(Bundle.STOP_TRANSIENT);
            }
          }
        }
      }
    } catch (final Throwable t) {
      fwCtx.frameworkError(bs, t);
    }
  }


  int getInitialBundleStartLevel() {
    return initStartLevel;
  }


  void setInitialBundleStartLevel(int startLevel) {
    fwCtx.perm.checkStartLevelAdminPerm();
    fwCtx.perm.callSetInitialBundleStartLevel0(this, startLevel);
  }


  void setInitialBundleStartLevel0(int startLevel, boolean save) {
    if(startLevel <= 0) {
      throw new IllegalArgumentException("Initial start level must be > 0, is " + startLevel);
    }
    initStartLevel = bCompat ? 1 : startLevel;
    if (storage != null && !readOnly && save) {
      try {
        Util.putContent(new File(storage, INITIAL_LEVEL_FILE),
                        Integer.toString(initStartLevel));
      } catch (final Exception e) {
        e.printStackTrace();
      }
    }
  }


  boolean isBundlePersistentlyStarted(BundleArchive archive) {
    return archive == null || archive.getAutostartSetting() != -1;
  }


  boolean isBundleActivationPolicyUsed(BundleArchive archive) {
    return archive != null && archive.getAutostartSetting() == Bundle.START_ACTIVATION_POLICY;
  }


  public StartLevel getService(Bundle bundle,
                               ServiceRegistration<StartLevel> registration)
  {
    return new StartLevelImpl(this);
  }

  public void ungetService(Bundle bundle,
                           ServiceRegistration<StartLevel> registration,
                           StartLevel service)
  {
  }

  public static class StartLevelImpl
    implements StartLevel
  {
    private final StartLevelController st;

    StartLevelImpl(StartLevelController st) {
      this.st = st;
    }

    public int getBundleStartLevel(Bundle bundle) {
      return st.getBundleStartLevel(checkBundle(bundle));
    }

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

    public int getStartLevel() {
      return st.getStartLevel();
    }

    public boolean isBundleActivationPolicyUsed(Bundle bundle) {
      return st.isBundleActivationPolicyUsed(getBundleArchive(bundle));
    }

    public boolean isBundlePersistentlyStarted(Bundle bundle) {
      return st.isBundlePersistentlyStarted(getBundleArchive(bundle));
    }

    public void setBundleStartLevel(Bundle bundle, int startlevel) {
      st.setBundleStartLevel(checkBundle(bundle), startlevel);
    }

    public void setInitialBundleStartLevel(int startlevel) {
      st.setInitialBundleStartLevel(startlevel);
    }

    public void setStartLevel(int startlevel) {
      st.setStartLevel(startlevel);
    }

    private BundleImpl checkBundle(Bundle b) {
      if (b instanceof BundleImpl) {
        final BundleImpl res = (BundleImpl)b;
        if (res.fwCtx == st.fwCtx) {
          if (res.state != Bundle.UNINSTALLED) {
            return res;
          }
          throw new IllegalArgumentException("Bundle is in UNINSTALLED state");
        }
      }
      throw new IllegalArgumentException("Bundle doesn't belong to the same framework as the StartLevel service");
    }

    private BundleArchive getBundleArchive(Bundle b) {
      final BundleImpl bi = checkBundle(b);
      final BundleArchive res = bi.current().archive;
      if (res == null && bi.id != 0) {
        throw new IllegalArgumentException("Bundle is in UNINSTALLED state");
      }
      return res;
    }

  }

  BundleStartLevelImpl bundleStartLevel(final BundleImpl bi) {
    return new BundleStartLevelImpl(this, bi);
  }

  static class BundleStartLevelImpl
    implements BundleStartLevel
  {
    final StartLevelController st;
    final BundleImpl bi;

    BundleStartLevelImpl(final StartLevelController st, final BundleImpl bi) {
      this.st = st;
      this.bi = bi;
    }

    public Bundle getBundle()
    {
      return bi;
    }

    public int getStartLevel()
    {
      return st.getBundleStartLevel(bi);
    }

    public void setStartLevel(int startlevel)
    {
      st.setBundleStartLevel(bi, startlevel);
    }

    public boolean isPersistentlyStarted()
    {
      return st.isBundlePersistentlyStarted(getBundleArchive());
    }

    public boolean isActivationPolicyUsed()
    {
      return st.isBundleActivationPolicyUsed(getBundleArchive());
    }

    BundleStartLevelDTO getDTO()
    {
      BundleStartLevelDTO res = new BundleStartLevelDTO();
      res.bundle = bi.id;
      res.startLevel = getStartLevel();
      res.activationPolicyUsed = isActivationPolicyUsed();
      res.persistentlyStarted = isPersistentlyStarted();
      return res;
    }

    private BundleArchive getBundleArchive() {
      final BundleArchive res = bi.current().archive;
      if (res == null && bi.id != 0) {
        throw new IllegalArgumentException("Bundle is in UNINSTALLED state");
      }
      return res;
    }

  }

  FrameworkStartLevelImpl frameworkStartLevel()
  {
    return new FrameworkStartLevelImpl(this, fwCtx.systemBundle);
  }

  static class FrameworkStartLevelImpl
    implements FrameworkStartLevel
  {
    final StartLevelController st;
    final BundleImpl bi;

    public FrameworkStartLevelImpl(StartLevelController startLevelController,
        BundleImpl bi)
    {
      this.st = startLevelController;
      this.bi = bi;
    }

    public Bundle getBundle()
    {
      return bi;
    }

    public int getStartLevel()
    {
      return st.getStartLevel();
    }

    public void setStartLevel(int startlevel, FrameworkListener... listeners)
    {
      st.setStartLevel(startlevel, listeners);
    }

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

    public void setInitialBundleStartLevel(int startlevel)
    {
      st.setInitialBundleStartLevel(startlevel);
    }

    FrameworkStartLevelDTO getDTO()
    {
      FrameworkStartLevelDTO res = new FrameworkStartLevelDTO();
      res.startLevel = getStartLevel();
      res.initialBundleStartLevel = getInitialBundleStartLevel();
      return res;
    }
  }
}
