/*
 * Copyright (c) 2003, 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.bundle.device;

import org.osgi.framework.*;
import org.osgi.service.log.*;
import org.osgi.service.device.*;

import java.io.*;
import java.util.*;

public class Activator
  extends Thread
  implements BundleActivator, ServiceListener,
	     BundleListener, FrameworkListener {

  // NB: These constants should be configurable

  private static final long LONG_LIFE      = 15*60*1000;
  private static final long SHORT_LIFE     =  5*60*1000;
  private static final long REAP_INTERVAL  =  1*60*1000;

  static final String DYNAMIC_DRIVER_TAG   = "__DD_";

  private static final String LOG_FILTER=
    "(objectClass="+LogService.class.getName()+")";
  private static final String DEVICE_FILTER=
    "(|(objectClass="+Device.class.getName()+")(DEVICE_CATEGORY=*))";
  private static final String DRIVER_FILTER=
    "(objectClass="+Driver.class.getName()+")";
  private static final String SELECTOR_FILTER=
    "(objectClass="+DriverSelector.class.getName()+")";
  private static final String LOCATOR_FILTER=
    "(objectClass="+DriverLocator.class.getName()+")";

  private static final String GLOBAL_FILTER = 
    "(|" + LOG_FILTER + DEVICE_FILTER + DRIVER_FILTER + SELECTOR_FILTER + LOCATOR_FILTER + ")";
  
  BundleContext bc;
  private boolean active;
  private boolean quit;

  private Filter isLog;
  private Filter isDevice;
  private Filter isDriver;
  private Filter isSelector;
  private Filter isLocator;

  private Vector /*DriverRef*/ drivers=new Vector();

  private ServiceReference[] locatorRefs;
  DriverLocator[] locators;

  private ServiceReference selectorRef;
  private DriverSelector selector;

  private ServiceReference logRef;
  private LogService log;

  private Hashtable /*Integer->MatchValue*/ cache=new Hashtable();

  private Hashtable /*ServiceReference->ServiceReference*/ newDevices=
    new Hashtable(20);

  private Hashtable /*Bundle->Long*/ tempDrivers=new Hashtable(10);
  private long reapTime;

  public Activator() {
    super("DeviceManager");
  }

  public void start(BundleContext bc) throws Exception {
    this.bc=bc;

    isLog=bc.createFilter(LOG_FILTER);
    isDevice=bc.createFilter(DEVICE_FILTER);
    isDriver=bc.createFilter(DRIVER_FILTER);
    isSelector=bc.createFilter(SELECTOR_FILTER);
    isLocator=bc.createFilter(LOCATOR_FILTER);

    startService(LOG_FILTER);

    start();

    bc.addFrameworkListener(this);
    Bundle b=bc.getBundle(0);
    if(b.getState()==Bundle.ACTIVE) activate();
    else info("Passive start");
  }

  public void stop(BundleContext bc) {
    info("Stopping");
    quit=true;
    synchronized(this) { notifyAll(); }
  }

  private synchronized void activate() throws Exception {

    if(active) return;
    active=true;
    info("Activating");

    bc.addBundleListener(this);

    // Thanks to TID for combining the (buggy)
    // separate listeners into one
    startService(GLOBAL_FILTER);
  }

  public void frameworkEvent(FrameworkEvent e) {
    try {
      if(e.getType()==FrameworkEvent.STARTED) activate();
    } catch(Exception e1) {}
  }

  private void startService(String filter) throws Exception {
    bc.addServiceListener(this,filter);
    ServiceReference[] sra=bc.getServiceReferences(null,filter);
    if(sra!=null) {
      for(int i=0;i<sra.length;i++) {
	try {
	  serviceChanged(new ServiceEvent(ServiceEvent.REGISTERED,sra[i]));
	} catch(Exception e) {}
      }
    }
  }

  public void serviceChanged(ServiceEvent e) {
    ServiceReference sr=e.getServiceReference();
    if(isDevice.match(sr)) {
      switch(e.getType()) {
      case ServiceEvent.REGISTERED:
	info("device found, "+showDevice(sr));
	touchDevice(sr);
	break;
      case ServiceEvent.MODIFIED:
	// We should most likely not do anything here
	//removeSRCachedMatch(sr);
	//touchDevice(sr);
	break;
      case ServiceEvent.UNREGISTERING:
	info("device lost, "+showDevice(sr));
	removeSRCachedMatch(sr);
	break;
      }
    }
    if(isDriver.match(sr)) {
      try {
	switch(e.getType()) {
	case ServiceEvent.REGISTERED:
	  info("driver found, "+showDriver(sr));
	  driverAppeared(sr);
	  touchAllDevices();
	  break;
	case ServiceEvent.MODIFIED:
	  // We should probably not do anything here
	  //driverGone(sr);
	  //driverAppeared(sr);
	  break;
	case ServiceEvent.UNREGISTERING:
	  info("driver lost, "+showDriver(sr));
	  driverGone(sr);
	  touchAllDevices();
	  break;
	}
      } catch(Exception e1) {}
    }
    if(isSelector.match(sr)) {
      try { bc.ungetService(selectorRef); } catch(Exception e1) {}
      selector = null;
      try {
	selectorRef=bc.getServiceReference(DriverSelector.class.getName());
	selector=(DriverSelector)bc.getService(selectorRef);
      } catch(Exception e1) {}
    }
    if(isLocator.match(sr)) {
      try {
	for(int i=0;i<locatorRefs.length;i++) {
	  bc.ungetService(locatorRefs[i]);
	}
      } catch(Exception e1) {}
      locatorRefs=null;
      locators=null;
      try {
	locatorRefs=bc.getServiceReferences(DriverLocator.class.getName(),null);
	locators=new DriverLocator[locatorRefs.length];
	for(int i=0;i<locatorRefs.length;i++) {
	  locators[i]=(DriverLocator)bc.getService(locatorRefs[i]);
	}
      } catch(Exception e1) {}
    }
    if(isLog.match(sr)) {
      try { bc.ungetService(logRef); } catch(Exception e1) {}
      try {
	logRef=bc.getServiceReference(LogService.class.getName());
	log=(LogService)bc.getService(logRef);
      } catch(Exception e1) {}
    }
  }

  private void driverAppeared(ServiceReference sr) {
    try {
      for(int i=0;i<drivers.size();i++) {
	DriverRef dr=(DriverRef)drivers.elementAt(i);
	if(dr.sr==sr) return;
      }
      DriverRef dr=new DriverRef();
      try {
	dr.ranking=((Integer)sr.
		    getProperty(org.osgi.framework.Constants.SERVICE_RANKING)).
	  intValue();
      } catch(Exception e) {}
      dr.servid=((Long)sr.getProperty(org.osgi.framework.Constants.SERVICE_ID)).
	longValue();
      dr.id=(String)sr.getProperty(org.osgi.service.device.Constants.DRIVER_ID);
      dr.sr=sr;
      if(dr.id!=null) dr.drv=(Driver)bc.getService(sr);
      else error("ignoring driver without id "+showDriver(sr));
      if(dr.drv!=null) drivers.addElement(dr);
    } catch(Exception e) {}
  }

  private void driverGone(ServiceReference sr) {
    try {
      for(int i=0;i<drivers.size();i++) {
	DriverRef dr=(DriverRef)drivers.elementAt(i);
	if(dr.sr==sr) {
	  drivers.removeElementAt(i);
	  return;
	}
      }
    } catch(Exception e) {}
  }

  public void bundleChanged(BundleEvent e) {
    if(e.getType()==BundleEvent.UNINSTALLED) {
      tempDrivers.remove(e.getBundle());
    }
  }

  private void info(String msg) {
    try {
      log.log(LogService.LOG_INFO,msg);
    } catch(Exception e) {
      System.err.println("[Device Manager] info: "+msg);
    }
  }

  private void error(String msg) {
    try {
      log.log(LogService.LOG_ERROR,msg);
    } catch(Exception e) {
      System.err.println("[Device Manager] ERROR: "+msg);
    }
  }

  public void run() {
    while(!quit) {
      boolean sleep=true;
      try {
	ServiceReference dev=
	  (ServiceReference)newDevices.keys().nextElement();
	newDevices.remove(dev);
	sleep=false;
	handleDevice(dev);
      } catch(Exception e) {}

      if(!sleep) continue;

      long now=System.currentTimeMillis();
      if(now>=reapTime) {
	reapDrivers();
	reapTime=now+REAP_INTERVAL;

	// NB: Should also clean out old matches every now
	//     and then based on some kind of LRU scheme
      }
      synchronized(this) {
	if(!quit) try { wait(reapTime-now); } catch(Exception e) {}
      }
    }
  }

  private void handleDevice(ServiceReference dev) {
    if(isUsed(dev)) return;

    Dictionary props=collectProperties(dev);
    
    Vector /*MatchImpl*/ matches=new Vector();

    // Populate matches with driver locator recommendations
    DriverLocator[] dla=locators;
    if(dla!=null) {
      for(int i=0;i<dla.length;i++) {
	try {
	  DriverLocator dl=dla[i];
	  String[] dria=dl.findDrivers(props);
	  for(int j=0;j<dria.length;j++) {
	    String dri=dria[j];
	    MatchImpl m=null;
	    for(int k=0;k<matches.size();k++) {
	      m=(MatchImpl)matches.elementAt(k);
	      if(m.equals(dri)) break;
	      m=null;
	    }
	    if(m==null) {
	      m=new MatchImpl(this,dev,dri);
	      matches.addElement(m);
	    }
	    m.addDriverLocator(dl);
	  }
	} catch(Exception e) {}
      }
    }

    for(;;) {
      // Add current drivers to matches
      for(int i=0;i<drivers.size();i++) {
	DriverRef dr=(DriverRef)drivers.elementAt(i);
	MatchImpl m=null;
	for(int k=0;k<matches.size();k++) {
	  m=(MatchImpl)matches.elementAt(k);
	  if(m.connect(dr)) break;
	  m=null;
	}
	if(m==null) {
	  m=new MatchImpl(this,dev,dr);
	  matches.addElement(m);
	}
      }

      int n=0;
      boolean loading=false;

      // Count good matches and trigger loading
      for(int i=0;i<matches.size();i++) {
	MatchImpl m=(MatchImpl)matches.elementAt(i);
	int match=m.getMatchValue();
	if(match==MatchImpl.UNKNOWN) loading=true;
	else if(match>Device.MATCH_NONE) n++;
      }

      // If not finished loading continue
      if(loading) continue;

      // If nothing matches we're done
      if(n==0) {
	tellNotFound(dev);
	return;
      }

      // Filter out good matches and select default best match
      MatchImpl best=null;
      Match[] sel=new Match[n];
      n=0;
      for(int i=0;i<matches.size();i++) {
	MatchImpl m=(MatchImpl)matches.elementAt(i);
	if(m.getMatchValue()>Device.MATCH_NONE) {
	  sel[n++]=m;
	  int cmp=0;
	  if(best==null || best.compare(m)<0) best=m;
	}
      }

      // Maybe use driver selector instead
      DriverSelector ds=selector;
      if(ds!=null) {
	int ix=ds.select(dev,sel);
	if(ix==DriverSelector.SELECT_NONE) {
	  tellNotFound(dev);
	  return;
	}
	try {
	  best=(MatchImpl)sel[ix];
	} catch(Exception e) {}
      }

      // If attach succeeds we're done
      String ref=null;
      try {
	ref=best.attach();
      } catch(Exception e) {
	error("failed attach "+showDriver(best.getDriver())+" -> "+showDevice(dev));
	continue;
      }
      
      if(best.getMatchValue()>Device.MATCH_NONE) {
	// Just loaded, go around and pick up the driver ref
	continue;
      }

      if(ref==null) {
	info("attached "+showDriver(best.getDriver())+" -> "+showDevice(dev));
	Bundle b=best.getBundle();
	if(b!=null) updateLife(b,LONG_LIFE);
	return;
      }

      info(showDriver(best.getDriver())+" refers to "+ref);

      // Append the referred match
      MatchImpl m=null;
      for(int i=0;i<matches.size();i++) {
	m=(MatchImpl)matches.elementAt(i);
	if(m.equals(ref)) break;
	m=null;
      }
      if(m==null) {
	m=new MatchImpl(this,dev,ref);
	matches.addElement(m);
      }
    }
  }

  private void updateLife(Bundle b, long t) {
    tempDrivers.put(b,new Long(System.currentTimeMillis()+t));
  }

  private boolean isUsed(ServiceReference sr) {
    Bundle[] ba=sr.getUsingBundles();
    if(ba!=null) {
      for(int i=0;i<ba.length;i++) {
	Bundle b=ba[i];
	try {
	  for(int j=0;j<drivers.size();j++) {
	    DriverRef dr=(DriverRef)drivers.elementAt(j);
	    if(dr.sr.getBundle()==b) return true;
	  }
	} catch(Exception e) {
	  return true;
	}
      }
    }
    return false;
  }

  private void reapDrivers() {
    long now=System.currentTimeMillis();
    Bundle[] ba=bc.getBundles();
    if(ba!=null) {
      for(int i=0;i<ba.length;i++) {
	try {
	  Bundle b=ba[i];
	  if(b.getLocation().startsWith(DYNAMIC_DRIVER_TAG)) {
	    Long expire=(Long)tempDrivers.get(b);
	    boolean inUse=false;

	    ServiceReference[] sra=b.getServicesInUse();
	    if(sra!=null) {
	      for(int j=0;j<sra.length;j++) {
		if(isDevice.match(sra[j])) {
		  inUse=true;
		  break;
		}
	      }
	    }

	    if(inUse) {
	      updateLife(b,LONG_LIFE);
	    } else if(expire==null) {
	      updateLife(b,SHORT_LIFE);
	    } else if(expire.longValue()<now) {
	      info("uninstalling "+b.getLocation());
	      b.uninstall();
	    }
	  }
	} catch(Exception e) {}
      }
    }
  }

  private void touchDevice(ServiceReference dev) {
    if(newDevices.put(dev,dev)==null) {
      synchronized(this) { notifyAll(); }
    }
  }

  private void touchAllDevices() {
    boolean added=false;
    try {
      ServiceReference[] sra=bc.getServiceReferences(null,DEVICE_FILTER);
      if(sra!=null) {
	for(int i=0;i<sra.length;i++) {
	  ServiceReference dev=sra[i];
	  if(newDevices.put(dev,dev)==null) added=true;
	}
      }
    } catch(Exception e) {}
    if(added) {
      synchronized(this) { notifyAll(); }
    }
  }

  private void tellNotFound(ServiceReference dev) {
    // NB: Should we avoid repeating the call to the same device?

    info("no driver for "+showDevice(dev));
    Object d=null;
    try {
      d=bc.getService(dev);
      ((Device)d).noDriverFound();
    } catch(Exception e) {
    } finally {
      try { bc.ungetService(dev); } catch(Exception e1) {}
    }
  }

  private Dictionary /*String->Object*/ collectProperties(ServiceReference sr) {
    Dictionary props=new Hashtable();
    String[] keys=sr.getPropertyKeys();
    if(keys!=null) {
      for(int i=0;i<keys.length;i++) {
        String key=keys[i];
        props.put(key,sr.getProperty(key));
      }
    }
    return props;
  }

  Bundle installBundle(String name, InputStream is) {
    Bundle b=null;
    try {
      info("installing "+name);
      b=bc.installBundle(name,is);
      b.start();
      updateLife(b,SHORT_LIFE);
      return b;
    } catch(Exception e) {
      error("failed to install "+name);
      try { b.uninstall(); } catch(Exception e1) {}
      return null;
    } finally {
      try { is.close(); } catch(Exception e1) {}
    }
  }

  void removeSRCachedMatch(ServiceReference sr) {

    // NB: Index to speed up this process?

    String pid=null;
    try {
      pid=(String)sr.getProperty(org.osgi.framework.Constants.SERVICE_PID);
    } catch(Exception e) {}
    if(pid!=null) return;
    for(Enumeration e=cache.keys();e.hasMoreElements();) {
      Integer k=(Integer)e.nextElement();
      MatchValue mv0=(MatchValue)cache.get(k);
      MatchValue mv=mv0;
      while(mv!=null) {
	if(mv.dev!=sr) {
	  // Keep it
	  mv0=mv;
	  mv=mv.next;
	} else if(mv==mv0) {
	  // Update hash table entry
	  mv0=mv=mv.next;
	  if(mv!=null) cache.put(k,mv);
	  else cache.remove(k);
	} else {
	  // Link past this one
	  mv=mv.next;
	  mv0.next=mv;
	}
      }
    }
  }

  int getCachedMatch(String drvid, ServiceReference dev) {
    MatchValue mv=findMatch(drvid,dev,false);
    return mv!=null ? mv.match : MatchImpl.UNKNOWN;
  }

  void putCachedMatch(String drvid, ServiceReference dev, int match) {
    MatchValue mv=findMatch(drvid,dev,true);
    mv.match=match;
  }

  private MatchValue findMatch(String drvid, ServiceReference dev,
			       boolean create) {
    String pid=null;
    try {
      pid=(String)dev.getProperty(org.osgi.framework.Constants.SERVICE_PID);
    } catch(Exception e) {}
    int k1=pid!=null ? pid.hashCode() : dev.hashCode();
    int k2=drvid.hashCode();
    Integer key=new Integer(k1+k2);
    MatchValue mv0=(MatchValue)cache.get(key);
    MatchValue mv=mv0;
    while(mv!=null) {
      if(drvid.equals(mv.drvid) &&
	 pid!=null ? pid.equals(mv.pid) : dev==mv.dev) return mv;
      mv=mv.next;
    }

    if(!create) return null;

    mv=new MatchValue();
    mv.next=mv0;
    mv.key=key;
    mv.drvid=drvid;
    mv.pid=pid;
    if(pid==null) mv.dev=dev;
    cache.put(key,mv);
    return mv;
  }

  private String showDevice(ServiceReference sr) {
    StringBuffer sb=new StringBuffer();
    Object o=sr.getProperty(org.osgi.service.device.Constants.DEVICE_CATEGORY);
    if(o instanceof String) {
      sb.append(o);
    } else if(o instanceof String[]) {
      String[] dca=(String[])o;
      for(int i=0;i<dca.length;i++) {
	if(i>0) sb.append('_');
	sb.append(dca[i]);
      }
    }
    o=sr.getProperty(org.osgi.framework.Constants.SERVICE_ID);
    if(o!=null) sb.append(o);
    Bundle b=sr.getBundle();
    if(b!=null) {
      sb.append('/');
      sb.append(b.getBundleId());
    }
    return sb.toString();
  }

  private String showDriver(ServiceReference sr) {
    StringBuffer sb=new StringBuffer();
    String s=(String)sr.getProperty(org.osgi.service.device.Constants.DRIVER_ID);
    sb.append(s!=null ? s : "driver");
    Bundle b=sr.getBundle();
    if(b!=null) {
      sb.append('/');
      sb.append(b.getBundleId());
    }
    return sb.toString();
  }
}

