/*
 * Copyright (c) 2004, 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.osgi.bundle.bundlerepository.desktop;

import org.osgi.framework.*;
import org.osgi.util.tracker.*;
import java.util.*;
import org.knopflerfish.service.desktop.*;
import org.ungoverned.osgi.service.bundlerepository.*;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.net.*;
import java.io.*;

import java.lang.reflect.Array;


/**
 * Desktop plugin for the BundleRepositoryService
 * with functionality for install/start and detail information.
 */
public class OBRDisplayer extends DefaultSwingBundleDisplayer {

  static ServiceTracker obrTracker;
  

  // Shared by all instances of JOBRAdmin
  static ImageIcon startIcon;
  static ImageIcon sortIcon;
  static ImageIcon installIcon;
  static ImageIcon updateIcon;
  static ImageIcon bundleIcon;
  static ImageIcon reloadIcon;

  static final int SORT_NONE        = 0;
  static final int SORT_HOST        = 1;
  static final int SORT_CATEGORY    = 2;
  static final int SORT_VENDOR      = 3;
  static final int SORT_APIVENDOR   = 4;
  static final int SORT_STATUS      = 5;
  
  static int[] SORT_ARRAY = new int[] {
    SORT_NONE,
    SORT_HOST,
    SORT_CATEGORY,
    SORT_VENDOR,
    SORT_STATUS,
  };
  
  static String[] SORT_NAMES = new String[] {
    "All",
    "Host",
    "Category",
    "Vendor",
    "Install status",
  };
  

  // Error message for all instances.
  // since the OBR service is shared, the err message
  // can be shared too
  String obrErr = "";

  // Message while loading repo URLs
  static String STR_LOADING   = "Loading...";

  // Name of top node
  static String  STR_TOPNAME  = "Bundle Repository";

  public OBRDisplayer(BundleContext bc) {
    super(bc, "Bundle Repository", "View and install bundles from Bundle Repository", true);
    
    try {
      // share icon instances between instances
      if(startIcon == null) {
	startIcon   = new ImageIcon(getClass().getResource("/player_play.png"));
	installIcon = new ImageIcon(getClass().getResource("/player_install.png"));
	updateIcon  = new ImageIcon(getClass().getResource("/update.png"));
	sortIcon    = new ImageIcon(getClass().getResource("/sort_select.png"));
	bundleIcon  = new ImageIcon(getClass().getResource("/lib16x16.png"));
	reloadIcon     = new ImageIcon(getClass().getResource("/reload_green.png"));
	
      }
    } catch (Exception e) {
      
      System.err.println("icon load failed: " + e);
    }

    obrTracker = new ServiceTracker(bc, 
				    BundleRepositoryService.class.getName(), 
				    null);
    obrTracker.open();
  }


  BundleRepositoryService getOBR() {
    return (BundleRepositoryService)OBRDisplayer.obrTracker.getService();
  }

  public JComponent newJComponent() {
    return new JOBRAdmin();
  }

  public void  disposeJComponent(JComponent comp) {
    JOBRAdmin obrAdmin = (JOBRAdmin)comp;
    obrAdmin.stop();

    super.disposeJComponent(comp);
  }

  void closeComponent(JComponent comp) {
    JOBRAdmin obrAdmin = (JOBRAdmin)comp;
    obrAdmin.stop();    
  }

  public void valueChanged(final long bid) {
    super.valueChanged(bid);

    for(Iterator it = components.iterator(); it.hasNext(); ) {
      JOBRAdmin obrAdmin = (JOBRAdmin)(JComponent)it.next();
      obrAdmin.valueChanged(bid);
    }
  }
  
  public Icon getSmallIcon() {
    return null;
  }


  /**
   * The actual component returned by newJComponent
   */
  class JOBRAdmin extends JPanel {

    DefaultTreeModel treeModel;
    JTree       recordTree;
    JPanel      recordPanel;
    JButton     installButton;
    JButton     refreshButton;
    JButton     startButton;
    JButton     updateButton;
    JTextPane   html;
    JScrollPane htmlScroll;


    TopNode    rootNode;

    // currently selected node
    OBRNode    brSelected = null;

    JMenuItem  contextItem;
    JPopupMenu contextPopupMenu;    

    // Category used for grouping bubdle records
    int sortCategory = SORT_CATEGORY;

    // Map of all bundles in OBR tree
    // String (location) -> OBRNode
    Map locationMap = new HashMap();

    public JOBRAdmin() {
      setLayout(new BorderLayout());


      recordTree = new JTree(new TopNode("[not loaded]"));
      recordTree.setRootVisible(true);

      // Must be registered for renderer tooltips to work
      ToolTipManager.sharedInstance().registerComponent(recordTree);

      // Load leaf icon for the tree cell renderer.
      TreeCellRenderer renderer = new DefaultTreeCellRenderer() {
	  public Component getTreeCellRendererComponent(JTree tree,
							Object value,
							boolean sel,
							boolean expanded,
							boolean leaf,
							int row,
							boolean hasFocus) {
	    
	    Component c = 
	      super.getTreeCellRendererComponent(tree, value, sel,
						 expanded, leaf, row,
						 hasFocus);
	    
	    TreePath tp = tree.getPathForRow(row);
	    
	    try {
	      Object node = tp.getLastPathComponent();
	      String tt = null;
	      if(node instanceof OBRNode) {
		OBRNode obrNode = (OBRNode)node;
		setIcon(obrNode.bBusy ? reloadIcon : bundleIcon);
		String loc = (String)obrNode.getBundleRecord().getAttribute(BundleRecord.BUNDLE_UPDATELOCATION);
		tt = obrNode.bBusy ? "busy..." : loc;

		boolean bInstalled = isInstalled(obrNode.getBundleRecord());
		obrNode.setInstalled(bInstalled);
		if(bInstalled) {
		  setForeground(Color.gray);
		}
	      } else if(node instanceof TopNode) {
		TopNode topNode = (TopNode)node;
		if(STR_LOADING.equals(topNode.name)) {
		  setIcon(reloadIcon);
		}
	      } else {
		//		setIcon(null);
	      }
	      setToolTipText(tt);
	    } catch (Exception ignored ) {
	    }
	    return this;
	  }
	};

      recordTree.setCellRenderer(renderer);

      
      // call setSelected() when user selects nodes/leafs in the tree
      recordTree.addTreeSelectionListener(new TreeSelectionListener() {
	  public void valueChanged(TreeSelectionEvent e) {
	    TreePath[] sel = recordTree.getSelectionPaths();
	    if(sel != null && sel.length == 1) {
	      setSelected((TreeNode)sel[0].getLastPathComponent());
	    } else {
	      setSelected(null);
	    }
	  }
	});
      
      // Create the HTML text pane for detail view.
      // The node's getTitle()/toHTML() methods
      // will be called whenever a node is HTMLAble
      html = new JTextPane();
      html.setText("");
      html.setContentType("text/html");
      
      html.setEditable(false);
      
      html.addHyperlinkListener(new HyperlinkListener() {
	  public void hyperlinkUpdate(HyperlinkEvent ev) {
	    if (ev.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
	      URL url = ev.getURL();
	      try {
		Util.openExternalURL(url);
	      } catch (Exception e) {
		// Activator.log.warn("Failed to open external url=" + url, e);
	      }
	    }
	  }
	});
      
      htmlScroll = 
	new JScrollPane(html, 
			JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
			JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
      
      htmlScroll.setPreferredSize(new Dimension(300, 300));
      
      JScrollPane treeScroll = 
	new JScrollPane(recordTree, 
			JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
			JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);

      treeScroll.setPreferredSize(new Dimension(200, 300));

      JButton repoButton = new JButton("URLs");
      repoButton.setToolTipText("Show/set repository URLs");
      repoButton.addActionListener(new ActionListener() {
	  public void actionPerformed(ActionEvent ev) {
	    askRepoURls();
	  }
	});
      
      installButton = new JButton(installIcon);
      installButton.setToolTipText("Install from OBR");
      ActionListener installAction;
      installButton.addActionListener(installAction = new ActionListener() {
	  public void actionPerformed(ActionEvent ev) {
	    installOrStart(brSelected, false);
	  }
	});
      installButton.setEnabled(false);

      startButton = new JButton(startIcon);
      startButton.setToolTipText("Install and start from OBR");
      ActionListener startAction;
      startButton.addActionListener(startAction = new ActionListener() {
	  public void actionPerformed(ActionEvent ev) {
	    installOrStart(brSelected, true);
	  }
	});
      startButton.setEnabled(false);

      refreshButton = new JButton(reloadIcon);
      refreshButton.setToolTipText("Refresh OBR list");
      refreshButton.addActionListener(new ActionListener() {
	  public void actionPerformed(ActionEvent ev) {
	    refreshList(true);
	  }
	});

      recordPanel = new JPanel(new BorderLayout());
      recordPanel.add(htmlScroll, BorderLayout.CENTER);
     
      JPanel left = new JPanel(new BorderLayout());
      
      left.add(treeScroll, BorderLayout.CENTER);

      JToolBar leftTools = new JToolBar();
      leftTools.add(makeSortSelectionButton());
      leftTools.add(refreshButton);

      leftTools.add(installButton);
      leftTools.add(startButton);
      leftTools.add(repoButton);

      left.add(leftTools, BorderLayout.SOUTH);

      // create a context menu which copies the names
      // and actions from the start and stop buttons.
      contextPopupMenu = new JPopupMenu();
      contextItem = new JMenuItem("------------");
      contextItem.setEnabled(false);

      contextPopupMenu.add(contextItem);
      contextPopupMenu.add(new JPopupMenu.Separator());

      JMenuItem item;

      item = new JMenuItem(startButton.getToolTipText(),
			   startButton.getIcon());
      item.addActionListener(startAction);
      contextPopupMenu.add(item);
      
      item = new JMenuItem(installButton.getToolTipText(),
			   installButton.getIcon());
      item.addActionListener(installAction);
      contextPopupMenu.add(item);
      

      // add listener for tree context menu, which selects the
      // item belo the mouse and pops up the context menu.
      recordTree.addMouseListener(new MouseAdapter() {
	  public void mousePressed(MouseEvent e) {
	    maybeShowPopup(e);
	  }
	  public void mouseReleased(MouseEvent e) {
	    maybeShowPopup(e);
	  }
	  private void maybeShowPopup(MouseEvent e) {
	    int mod = e.getModifiers();
	    if(contextPopupMenu != null && 
	       (e.isPopupTrigger() || 
		((e.getModifiers() & InputEvent.BUTTON2_MASK) != 0))) {
	      TreePath tp = recordTree.getPathForLocation(e.getX(), e.getY());
	      if(tp != null) {
		TreeNode node = (TreeNode)tp.getLastPathComponent();
		if(node instanceof OBRNode) {
		  contextItem.setText(((OBRNode)node).name);
		  recordTree.setSelectionPath(tp);
		  setSelected(node);
		  Component comp = e.getComponent();
		  contextPopupMenu.show(comp, e.getX(), e.getY());
		}
	      }
	    }
	  }
	});


      JSplitPane panel = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
					left,
					recordPanel);
      
      panel.setDividerLocation(200);
      
      
      add(panel, BorderLayout.CENTER);

      refreshList(true);
    }

    /**
     * Called when BundleSelectionListener distributes events.
     *
     * <p>
     * If possible, select and show the bundle with id <tt>bid</tt>
     * in the tree.
     * </p>
     */
    void valueChanged(long bid) {
      try {
	if(bid >= 0) {
	  Bundle b = bc.getBundle(bid);
	  if(b != null) {
	    OBRNode obrNode = getOBRNode(b);
	    if(obrNode != null && obrNode != brSelected) {
	      TreePath tp = new TreePath(obrNode.getPath());
	      showPath(tp, null);
	    }
	  }
	}
      } catch (Exception e) {
      }
    }
    
    /**
     * Get the OBRNode tree node which matches a bundle.
     *
     * @return OBRNode for the specified bundle if it exists,
     *         <tt>null</tt> otherwise.
     */
    OBRNode getOBRNode(Bundle b) {
      OBRNode node = (OBRNode)locationMap.get(b.getLocation());
      
      if(node != null) {
	return node;
      }

      for(Iterator it = locationMap.keySet().iterator(); it.hasNext(); ) {
	String  loc  = (String)it.next();
	node = (OBRNode)locationMap.get(loc);
	if(Util.bundleEqual(b, node.getBundleRecord())) {
	  return node;
	}
      }
      return null;
    }


    /**
     * Create the sort selection button, with items
     * defined in SORT_ARRAY/SORT_NAMES
     */
    JButton makeSortSelectionButton() {
      JButton sortButton = new JButton(sortIcon);
      sortButton.setToolTipText("Select sorting");

      final JPopupMenu  sortPopupMenu = new JPopupMenu();
      ButtonGroup       group         = new ButtonGroup();

      for(int i = 0; i < SORT_ARRAY.length; i++) {
	JCheckBoxMenuItem item = new JCheckBoxMenuItem(SORT_NAMES[i]);
	final int cat = SORT_ARRAY[i];
	item.setState(cat == sortCategory);
	item.addActionListener(new ActionListener() {
	    public void actionPerformed(ActionEvent ev) {
	      sortCategory = cat;
	      refreshList(false);
	    }
	  });
	sortPopupMenu.add(item);
	group.add(item);
      }
      
      sortButton.addMouseListener(new MouseAdapter() {
	  public void mousePressed(MouseEvent e) {
	    showPopup(e);
	  }	  
	  public void mouseReleased(MouseEvent e) {
	    showPopup(e);
	  }
	  private void showPopup(MouseEvent e) {
	    Component comp = e.getComponent();
	    sortPopupMenu.show(comp, 0, comp.getSize().height);
	  }
	});
      
      return sortButton;
    }

    
    boolean bBusy = false;

    /**
     * Install or install+start a bundle specified by an OBRNode
     * using the BundleRepositoryService. Run an a new thread to 
     * avoid blocking the swing thread for potentially long operations.
     *
     * @param obrNode Bundle to install/start
     * @param bStart if <tt>true</t>, start, otherewise just install
     */
    void installOrStart(final OBRNode obrNode, final boolean bStart) {
      if(bBusy) {
	return;
      }
      new Thread() {
	{ start(); }
	public void run() {
	  try {
	    bBusy = obrNode.bBusy = true;
	    setSelected(obrNode);
	    installOrStartSync(obrNode, bStart);
	  } catch (Exception e) {
	    obrNode.appendLog("" + e + "\n");
	  } finally {
	    bBusy = obrNode.bBusy = false;
	    setSelected(obrNode);
	  }
	}
      };
    }

    void installOrStartSync(OBRNode obrNode, boolean bStart) {
      BundleRecord br = obrNode.getBundleRecord();
      if(br == null) {
	return;
      }
      
      String updateURL = 
	(String)br.getAttribute(BundleRecord.BUNDLE_UPDATELOCATION);
      
      // Check if bundle already is installed. If so, 
      // ask user if it should be updated, installed again or
      // if the operation should be cancelled.
      Bundle b = getBundle(br);
      if(b != null) {
	
	boolean bIsRepoBundle = Util.isInRepo(b, updateURL);
	
	String[] options = bIsRepoBundle
	  ? (new String[] { "Update", "Cancel",	  })
	  : (new String[] { "Update", "Install again", "Cancel",  });
	
	String msg = bIsRepoBundle
	  ?  
	  "The selected bundle is already installed\n" + 
	  "from the repository.\n" + 
	  "\n" + 
	  "It can be updated from the repository."
	  : 
	  "The selected bundle is already installed.\n" + 
	  "\n" + 
	  "It can be updated from the repository, or\n" + 
	  "a new instance can be installed from the\n" + 
	  "repository";
	
	int n = JOptionPane
	  .showOptionDialog(recordTree, 
			    msg,
			    "Bundle is installed", // title
			    JOptionPane.YES_NO_CANCEL_OPTION,
			    JOptionPane.QUESTION_MESSAGE,
			    null, // icon
			    options, 
			    options[0]); 
	
	if(bIsRepoBundle) {
	  if(n == 0) {        // update
	    obrNode.appendLog("update.\n");
	  } else if(n == 1) { // Cancel
	    return; 
	  }
	} else {
	  if(n == 0) { // Update
	    InputStream in = null;
	    try {
	      URL url = new URL(updateURL);
	      in = new BufferedInputStream(url.openStream());
	      b.update(in);
	      obrNode.appendLog("Updated from " + url + "\n"); 
	    } catch (Exception e) {
	      obrNode.appendLog("Update failed: " + e + "\n");
	    } finally {
	      try { in.close(); } catch (Exception ignored) { }
	    }
	    return;
	  } else if(n == 1) { // install new
	    obrNode.appendLog("Install new instance.\n");
	  } else if(n == 2) { // cancel
	    return;
	  }
	}
      }
      
      // Install the budle from repo
      
      BundleRepositoryService obr = getOBR();
      
      // deployBundle writes all info to streams, so we have to 
      // catch that info. The text will be displayed by
      // the node's toHTML method
      ByteArrayOutputStream bout = new ByteArrayOutputStream();
      PrintStream outStream = new PrintStream(bout, true);
      
      // We always resolve.
      boolean bResolve = true;
      
      try {
	boolean bOK = obr.deployBundle(outStream, // Output stream.
				       outStream, // Error stream.
				       updateURL,
				       bResolve, 
				       bStart);
	if(bOK) {
	  if(sortCategory == SORT_STATUS) {
	    refreshList(false);
	  }
	}
      } catch (Exception e) {
	e.printStackTrace(outStream);
      } finally {
	try {  outStream.close();} catch (Exception ignored) {	}
	String s = new String(bout.toByteArray());
	obrNode.appendLog(s);
      }
      
      setSelected(obrNode);
    }

    /**
     * Refresh the OBR tree and try to reshow/reselect
     * any previously selected bundle node.
     *
     * @param bReload if <tt>true</tt>, reload the bundle repository
     *                XML files, otherewise, just rebuild the tree.
     */
    synchronized void refreshList(final boolean bReload) { 
      Thread t = new Thread() {
	  public void run() {
	    locationMap.clear();
	    BundleRecord brOld = brSelected != null ? brSelected.getBundleRecord() : null;

	    setRootText(STR_LOADING);
	    
	    rootNode = new TopNode(STR_TOPNAME);
	    treeModel = new DefaultTreeModel(rootNode);

	    BundleRepositoryService obr = getOBR();
	    
	    if(obr != null) {
	      if(bReload) {
		obrErr = "";
		try {
		  assertRepoURLs(obr.getRepositoryURLs());
		  obr.setRepositoryURLs(obr.getRepositoryURLs());
		} catch (Exception e) {
		  obrErr = 
		    "<b>" + e.getClass().getName() + "</b>"+ 
		    "<pre>\n" + 
		    e.getMessage() + 
		    "</pre>";
		}
	      }
	      int count = obr.getBundleRecordCount();

	      // String (category) -> Set (BundleRecord)
	      Map categories = new TreeMap(new Comparator() {
		  public int compare(Object o1, Object o2) {
		    return o1.toString().compareToIgnoreCase(o2.toString());
		  }
		});
	      
	      // move all bundle records into a sorted
	      // category map of sets
	      for(int i = 0; i < count; i++) {
		BundleRecord br = obr.getBundleRecord(i);

		String loc = (String)br.getAttribute(BundleRecord.BUNDLE_UPDATELOCATION);

		String category = "other";
		if(sortCategory == SORT_CATEGORY) {
		  category = Util.getAttribute(br, 
					  BundleRecord.BUNDLE_CATEGORY,
					  "[no category]");
		} else if(sortCategory == SORT_VENDOR) {
		  category = Util.getAttribute(br,
					  BundleRecord.BUNDLE_VENDOR,
					  "[no vendor]");
		} else if(sortCategory == SORT_STATUS) {
		  if(isInstalled(br)) {
		    category = "Installed";
		  } else {
		    category = "Not installed";
		  }
		} else if(sortCategory == SORT_NONE) {
		  category = SORT_NAMES[SORT_NONE];
		} else {
		  int ix = loc.indexOf(":");
		  if(ix != -1) {
		    category = loc.substring(0, ix);
		    if(loc.startsWith("http://")) {
		      ix = loc.indexOf("/", 8);
		      if(ix != -1) {
			category = loc.substring(0, ix);
		      }
		    } else {
		      ix = loc.indexOf("/", ix + 1);
		      if(ix != -1) {
			category = loc.substring(0, ix);
		      } else {
			ix = loc.indexOf("\\", ix + 1);
			if(ix != -1) {
			  category = loc.substring(0, ix);
			}
		      }
		    }
		  }
		}
		Set set = (Set)categories.get(category);
		if(set == null) {
		  set = new TreeSet(new BRComparator());
		  categories.put(category, set);
		}
		set.add(br);
	      }
	      

	      int i = 0;
	      DefaultMutableTreeNode selNode = null;

	      for(Iterator it = categories.keySet().iterator(); it.hasNext();) {
		String category = (String)it.next();
		Set    set      = (Set)categories.get(category);
		
		final DefaultMutableTreeNode categoryNode = 
		  new CategoryNode(category);
		
		for(Iterator it2 = set.iterator(); it2.hasNext();) {
		  BundleRecord br = (BundleRecord)it2.next();
		  
		  DefaultMutableTreeNode brNode =  new OBRNode(br);
		  categoryNode.add(brNode);
		  i++;

		  String loc = (String)br.getAttribute(BundleRecord.BUNDLE_UPDATELOCATION);

		  locationMap.put(loc, brNode);
		  if(brOld != null && loc.equals(brOld.getAttribute(BundleRecord.BUNDLE_UPDATELOCATION))) {
		    selNode = brNode;
		    
		  }
												 
		}
		
		rootNode.add(categoryNode);
	      }

	      final TreePath selPath = 
		new TreePath(selNode != null 
			     ? selNode.getPath()
			     : rootNode.getPath());

	      showPath(selPath, treeModel);
	    }
	  }
	  
	};
      t.start();
    }


    /**
     * Show the specified path and possible model (on the Swing thread)
     *
     * @param selPath path to show
     * @param model   if not <tt>null</tt>, set as tree's model before setting
     *                selected path.
     */
    void showPath(final TreePath selPath, final TreeModel model) {
      SwingUtilities.invokeLater(new Runnable() {
	  public void run() {
	    if(model != null) {
	      recordTree.setModel(model);
	    }

	    recordTree.expandPath(selPath);
	    recordTree.setSelectionPath(selPath);
	    recordTree.scrollPathToVisible(selPath);
	  }
	});
    }



    
    /**
     * Assert that the URLs seem to be valid ULRs.
     *
     * @throws RuntimeException if any of the strings in <tt>urls</tt>
     *                          fails to resolve to valid URL.
     */
    void assertRepoURLs(String[] urls) {
      if(urls == null) {
	throw new RuntimeException("No URLs set");
      }
      
      StringBuffer sb = new StringBuffer();
      int nConnectionErrs = 0;
      
      // for each of the strings, try to create an URL and
      // do an initial connect()
      for(int i = 0; i < urls.length; i++) {
	URLConnection conn = null;
	try {
	  URL url = new URL(urls[i]);
	  conn = url.openConnection();
	  conn.connect();
	} catch (Exception e) {
	  sb.append(" " + urls[i] + ": " + e);
	  sb.append("\n");
	  nConnectionErrs++;
	} finally {
	  // close?
	}
      }
      if(nConnectionErrs > 0) {
	String msg = 
	  "URL connection errors:\n" + 
	  sb.toString();
	throw new RuntimeException(msg);
      }
    }
	
    
    /**
     * Clear the tree and set the text of the tree's root node.
     */
    void setRootText(final String s) {
      try {
	SwingUtilities.invokeAndWait(new Runnable() {
	    public void run() {
	      recordTree.setModel(new DefaultTreeModel(new TopNode(s)));
	    }
	  });
      } catch (Exception e) {
      }
    }
    
    
    /**
     * Query the user to a list of repo URLs
     */
    void askRepoURls() {
      try {
	StringBuffer sb = new StringBuffer();
	String[] urls = getOBR().getRepositoryURLs();
	for(int i = 0; i < urls.length; i++) {
	  sb.append(urls[i]);
	  if(i < urls.length - 1) {
	    sb.append("\n");
	  }
	}
	
	JPanel panel = new JPanel(new BorderLayout());
	
	JTextArea text = new JTextArea(Math.min(3, urls.length), 40);
	text.setText(sb.toString());
	//	text.setPreferredSize(new Dimension(300, 100));
	JScrollPane scroll = 
	  new JScrollPane(text, 
			  JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
			  JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
	
	scroll.setPreferredSize(new Dimension(300, 100));
	
	panel.add(scroll, BorderLayout.CENTER);
	panel.add(new JLabel("Repository URLs."), BorderLayout.NORTH);
	int option = JOptionPane.showConfirmDialog(this,
						   panel,
						   "Repository URLs",
						   JOptionPane.YES_NO_OPTION);
	
	String r2 = text.getText();
	if(option == 0 && !r2.equals(sb.toString())) {
	  StringTokenizer st = new StringTokenizer(r2, "\n");
	  urls = new String[st.countTokens()];
	  int i = 0;
	  while (st.hasMoreTokens()) {
	    urls[i++] = st.nextToken();
	  }
	  try {
	    getOBR().setRepositoryURLs(urls);
	    refreshList(true);
	  } catch (Exception e) {
	    obrErr = "" + e;
	  }
	}
      } catch (Exception e) {
	e.printStackTrace();
      }
    }
    
    
    /**
     * Set the selected bundle by settingt the HTML detail string.
     */
    void setSelected(TreeNode node) {
      if(node != null && (node instanceof OBRNode)) {
	brSelected = (OBRNode)node;
	Bundle b = getBundle(brSelected.getBundleRecord());
	if(b != null) {
	  gotoBid(b.getBundleId());
	} else {
	  getBundleSelectionModel().clearSelection();
	}
      } else {
	brSelected = null;
      }

      installButton.setEnabled(brSelected != null && !bBusy);
      startButton.setEnabled(brSelected != null && !bBusy);

      StringBuffer sb = new StringBuffer();


      
      sb.append("<html>\n");

      sb.append("<table border=\"0\" width=\"100%\">\n");
      sb.append("<tr>");
      


      if(node != null  && (node instanceof HTMLable)) {
	HTMLable htmlNode = (HTMLable)node;
	sb.append("<td valign=\"top\" bgcolor=\"#eeeeee\">");

	Util.startFont(sb, "-1");
	
	sb.append(htmlNode.getTitle());

	sb.append("</font>\n");
	sb.append("</td>\n");

	String   iconURL  = htmlNode.getIconURL();
	if(iconURL != null && !"".equals(iconURL.trim())) {
	  sb.append("<td valign=\"top\" bgcolor=\"#eeeeee\">");
	  sb.append("<img align=\"left\" src=\"" + iconURL + "\">");
	  sb.append("</td>");
	}
       
      } else {
	sb.append("");
      }

      sb.append("</tr>\n");
      sb.append("</table>\n");


      if(node != null  && (node instanceof HTMLable)) {
	HTMLable htmlNode = (HTMLable)node;
	sb.append(htmlNode.toHTML());
      }
      
      sb.append("</html>");
      setHTML(sb.toString());
      recordTree.invalidate();
      recordTree.repaint();
    }

    
    public void setBundle(Bundle b) {
    }
    
    public void stop() {
      if(recordTree != null) {
	ToolTipManager.sharedInstance().registerComponent(recordTree);
      }
    }
  
    
    void setHTML(String s) {
      html.setText(s);
      
      SwingUtilities.invokeLater(new Runnable() {
	  public void run() {
	    try {
	      JViewport vp = htmlScroll.getViewport();
	      if(vp != null) {
	      vp.setViewPosition(new Point(0,0));
	      htmlScroll.setViewport(vp);
	      }  
	    } catch (Exception e) {
	      e.printStackTrace();
	    }
	  }
	});
    }
  }
  
  void gotoBid(long bid) {
    getBundleSelectionModel().clearSelection();
    getBundleSelectionModel().setSelected(bid, true);
  }
  
  /**
   * If possible, get the bundle matchin a bundle record.
   *
   * @param br record to search for
   * @return an installed bundle, if the bbudle record seem to be installed
   */
  Bundle getBundle(BundleRecord br) {
    
    Bundle[] bl = bc.getBundles();
    
    for(int i = 0; bl != null && i < bl.length; i++) {
      if(Util.bundleEqual(bl[i], br)) {
	return bl[i];
      }
    }
    return null;
  }

  
  /**
   * Check if a bundle record seem to be installed.
   */
  boolean isInstalled(BundleRecord br) {
    return getBundle(br) != null;
  }
  
  void appendHelp(StringBuffer sb) {
    String urlPrefix = "bundle://" + bc.getBundle().getBundleId();

    sb.append("<p>" +
	      "Select a bundle from the bundle repository list, then " + 
	      "select the install or start icons." + 
	      "</p>");

    /*
    sb.append("<p>" + 
	      "<img src=\"" + urlPrefix + "/player_play.png\">" + 
	      "Install and start a bundle and its dependencies." + 
	      "</p>" + 
	      "<img src=\"" + urlPrefix + "/player_install.png\">" + 
	      "Install a bundle and its dependencies." + 
	      "</p>" +
	      "<p>" +
	      "<img src=\"" + urlPrefix + "/update.png\">" + 
	      "Reload the bundle repository list." + 
	      "</p>" + 
	      "<p>" + 
	      "<img src=\"" + urlPrefix + "/sort_select.png\"> " + 
	      "Change category sorting." + 
	      "</p>"
	      );
    */
  }
  
  /**
   * Simple interface for things that can produce HTML
   */
  interface HTMLable {
    public String getTitle();
    public String toHTML();
    public String getIconURL();
  }


  /**
   * Comparator class for comparing two BundleRecords
   *
   * <p>
   * Sort first by case-insensite name, tehn by version
   * </p>
   */
  class BRComparator implements Comparator {
    public int compare(Object o1, Object o2) {
      BundleRecord br1 = (BundleRecord)o1;
      BundleRecord br2 = (BundleRecord)o2;
      String s1 = Util.getBRName(br1).toLowerCase();
      String s2 = Util.getBRName(br2).toLowerCase();
      int n = 0;
      
      try {
	n = s1.compareTo(s2);
	if(n == 0) {
	  s1 = (String)br1.getAttribute(BundleRecord.BUNDLE_VERSION);
	  s2 = (String)br2.getAttribute(BundleRecord.BUNDLE_VERSION);
	}
	
	n = s1.compareTo(s2);
      } catch (Exception e) {
      }
      return n;
    }
  }

  /**
   * TreeNode to top of OBR tree.
   */
  class TopNode extends DefaultMutableTreeNode implements HTMLable {
    
    String name;
    
    public TopNode(String name) {
      this.name = name;
    }

    public String getIconURL() {
      return null;
    }
    
    public String toString() {
      return name;
    }
    
    public String getTitle() {
      return toString();
    }
    
    public String toHTML() {
      StringBuffer sb = new StringBuffer();
      
      if(!"".equals(obrErr)) {
	Util.startFont(sb);
	sb.append("<pre>");
	sb.append(obrErr);
	sb.append("</pre>");
	sb.append("</font>");
      } else {
	Util.startFont(sb);
	
	BundleRepositoryService obr = getOBR();
	
	if(obr != null) {
	  sb.append("<p>");
	  sb.append("<b>Repository URLs</b><br>");
	  String[] urls = obr.getRepositoryURLs();
	  for(int i = 0; i < urls.length; i++) {
	    sb.append(urls[i]);
	    if(i < urls.length - 1) {
	      sb.append("<br>");
	    }
	  }
	  sb.append("</p>");
	  sb.append("<p>");
	  sb.append("Total number of bundles: " + obr.getBundleRecordCount());
	  sb.append("</p>");
	}
	
	appendHelp(sb);
	
	sb.append("</font>");
      }
      return sb.toString();
    }


  }
  
  /**
   * Tree node for grouping BundleRecords into categories in OBR tree
   */
  class CategoryNode extends DefaultMutableTreeNode implements HTMLable {
    String category;

    public CategoryNode(String category) {
      this.category = category;
    }

    public String getIconURL() {
      return null;
    }

    public String toString() {
      return category + " (" + getChildCount() + ")";
    }

    public String getTitle() {
      return toString();
    }


    public String toHTML() {
      StringBuffer sb = new StringBuffer();



      Util.startFont(sb);

      BundleRepositoryService obr = getOBR();
      if(obr != null) {
	sb.append("<p>");
	sb.append("Bundles in this category: " + getChildCount());
	sb.append("</p>");
      }
      
      appendHelp(sb);

      sb.append("</font>");

      return sb.toString();
    }
  }

  /**
   * Tree Node for wrapping a BundleRecord in the OBR tree.
   */
  class OBRNode extends DefaultMutableTreeNode implements HTMLable {
    String       name;
    StringBuffer log = new StringBuffer();
    BundleRecord br;
    boolean bInstalled = false;
    boolean     bBusy;

    public OBRNode(BundleRecord br) {
      super(null);
      this.br = br;

      name = Util.getBRName(br);
      //      setIcon(bundleIcon);
    }

    public BundleRecord getBundleRecord() {
      return br;
    }

    public void appendLog(String s) {
      log.append(s);
    }

    public String getLog() {
      return log.toString();
    }

    void setInstalled(boolean bInstalled) {
      this.bInstalled = bInstalled;
    }

    public String toString() {
      StringBuffer sb = new StringBuffer();
      
      sb.append(name);
      if(bInstalled) {
	sb.append(" (installed)");
      }
      return sb.toString();
    }

    public String getIconURL() {
      String iconURL = (String)br.getAttribute("Application-Icon");
      
      if(iconURL != null && !"".equals(iconURL)) {
	StringBuffer sb = new StringBuffer();
	
	if(iconURL.startsWith("!")) {
	  if(!iconURL.startsWith("!/")) {
	    iconURL = "!/" + iconURL.substring(1);
	  }
	  iconURL = "jar:" + 
	    br.getAttribute(BundleRecord.BUNDLE_UPDATELOCATION) +  iconURL;
	} else if(-1 == iconURL.indexOf(":")) {
	  iconURL = "jar:" + 
	    br.getAttribute(BundleRecord.BUNDLE_UPDATELOCATION) + "!/" + iconURL;
	}
	return iconURL;
      }
      
      return null;
    }

    public String toHTML() {
      StringBuffer sb = new StringBuffer();

      String[]     attrs = br.getAttributes();
      
      Map map = new TreeMap();
      for(int i = 0; i < attrs.length; i++) {
	Object obj = br.getAttribute(attrs[i]);
	map.put(attrs[i], Util.toHTML(obj));
      }


      String desc = (String)br.getAttribute(BundleRecord.BUNDLE_DESCRIPTION);
      if(desc != null) {
	Util.startFont(sb);
	sb.append("<p>");
	sb.append(desc);
	sb.append("</p>");
	sb.append("</font>");

	map.remove(BundleRecord.BUNDLE_DESCRIPTION);
	map.remove(BundleRecord.BUNDLE_DESCRIPTION.toLowerCase());
      }



      sb.append("<table border=0>");


      String log = getLog().trim();
      if(log != null && !"".equals(log)) {
	sb.append("<tr>");
	sb.append("<td bgcolor=\"#eeeeee\" colspan=\"2\" valign=\"top\">");
	sb.append("<pre>");
	Util.startFont(sb, "-2");
	sb.append(log);
	sb.append("</font>");
	sb.append("<pre>");
	sb.append("</td>");
	sb.append("</tr>");	
      }

      for(Iterator it = map.keySet().iterator(); it.hasNext();) {
	String key = (String)it.next();
	String val = (String)map.get(key);
	
	sb.append("<tr>");
	sb.append("<td valign=\"top\"><b>");
	Util.startFont(sb);
	sb.append(key);
	sb.append("</b></font>");
	sb.append("</td>");
	
	sb.append("<td valign=\"top\">");
	Util.startFont(sb);
	sb.append(val);
	sb.append("</font>");
	sb.append("</td>");
	
	sb.append("</tr>");
      }
      
      sb.append("</table>\n");

      return sb.toString();
    }

    public String getTitle() {
      return toString();
    }

  }
}

