/*
 * 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.desktop.swing;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JToolBar;
import javax.swing.JViewport;
import javax.swing.ListSelectionModel;

import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.BundleEvent;
import org.osgi.framework.Constants;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceReference;

public class TimeLineDisplayer extends DefaultSwingBundleDisplayer {

  ListSelectionModel model;

  Desktop            desktop;

  // Long (time) -> BundleEvent
  SortedMap bundleEvents = new TreeMap();

  // Bundle -> (Long (time) -> ServiceEvent)
  Map bundleToServiceEvents = new HashMap();
  
  public TimeLineDisplayer(BundleContext bc) {
    super(bc, "Time line", "Time line of bundle events", false);

    desktop = Activator.desktop;
  }


  public void bundleChanged(BundleEvent ev) {
    super.bundleChanged(ev);

    Bundle b = ev.getBundle();
    
    FWEvent fwe = new FWEvent(b, ev.getType());
    bundleEvents.put(new Long(fwe.getId()), fwe);
    
    repaintComponents();
  }

  public void serviceChanged(ServiceEvent ev) {
    ServiceReference sr = ev.getServiceReference();
    
    FWEvent fwe = new FWEvent(sr, ev.getType());

    Map serviceEvents = (Map)bundleToServiceEvents.get(sr.getBundle());
    if(serviceEvents == null) {
      serviceEvents = new TreeMap(); 
      bundleToServiceEvents.put(sr.getBundle(), serviceEvents);
    }

    serviceEvents.put(new Long(fwe.getId()), fwe);

    repaintComponents();
  }



  void clear() {
    bundleEvents.clear();
    bundleToServiceEvents.clear();
  }

  public JComponent newJComponent() {
    JTimeLine tl = new JTimeLine();
    tl.reloadAll(false);

    return tl;
  }


  JScrollPane  scroll;

  class JTimeLine extends JPanel {

    JBundleGraph graph;  
    
    public JTimeLine() {
      setLayout(new BorderLayout());
      
      setBackground(Color.white);
      
      graph  = new JBundleGraph();      

      scroll = new JScrollPane(graph);

      add(scroll, BorderLayout.CENTER);
      
      JToolBar cmdPanel = new JToolBar(JToolBar.VERTICAL);
      cmdPanel.setFloatable(false);

      cmdPanel.add(new JButton() { {
	setIcon(desktop.reloadIcon);
	setToolTipText("Reload events");
	addActionListener(new ActionListener() {
	    public void actionPerformed(ActionEvent ev) {
	      reloadAll(true);
	    }
	  });
      }});
      cmdPanel.add(new JToolBar.Separator());

      
      cmdPanel.add(new JButton() { {
	setIcon(desktop.magPlusIcon);
	setToolTipText("Zoom in");
	addActionListener(new ActionListener() {
	    public void actionPerformed(ActionEvent ev) {
	      Point p = scroll.getViewport().getViewPosition();
	      graph.doZoom((int)(p.x * zoomK),
			       (int)(p.y * zoomK),
			   zoomK, zoomK);
	    }
	  });
      }});
      
      cmdPanel.add(new JButton() { {
	setIcon(desktop.magMinusIcon);
	setToolTipText("Zoom out");
	addActionListener(new ActionListener() {
	    public void actionPerformed(ActionEvent ev) {
	      Point p = scroll.getViewport().getViewPosition();
	      graph.doZoom((int)(p.x / zoomK),
			   (int)(p.y / zoomK),
			   1/zoomK, 1/zoomK);
	    }
	  });
      }});
      
      cmdPanel.add(new JButton() { {
	setIcon(desktop.magFitIcon);
	setToolTipText("Zoom out all");
	addActionListener(new ActionListener() {
	    public void actionPerformed(ActionEvent ev) {
	      graph.doZoomOutAll();
	    }
	  });
      }});



      
      add(cmdPanel, BorderLayout.WEST);      
    }

    void reloadAll(boolean bAsk) {
      int n = 0;
      if(bAsk) {
	Object[] options = {Strings.get("yes"), 
			    Strings.get("cancel")};
	
	
	n =JOptionPane
	  .showOptionDialog(Activator.desktop.frame,
			    Strings.get("This will clear all old events and reload with current framework status"),
			    Strings.get("Reload all events?"),
			    JOptionPane.YES_NO_OPTION,
			    JOptionPane.QUESTION_MESSAGE,
			    null,
			    options,
			    options[1]);
      }

      if(n == 0) {
	clear();
	getAllBundles();
	getAllServices();
	invalidate();
      }

    }

    double zoomK = 1.1;
    
    // set any currently hilited bundle by in paintBundles()
    Bundle hiliteBundle = null;
    int    hiliteBundleIx = -1;
    
    
    // The component actually drawing the time line
    class JBundleGraph extends JPanel  {
      
      double zoomFac = 1.0;
      
      int mouseX = 0;
      int mouseY = 0;
      
      int mouseDragX = 0;
      int mouseDragY = 0;
      boolean bIsDragging = false;
      
      public JBundleGraph() {
	
	addMouseMotionListener(new MouseMotionListener() {
	    public void mouseMoved(MouseEvent ev) {
	      saveMousePos(ev);
	      repaint();
	    }
	    public void mouseDragged(MouseEvent ev) {
	      bIsDragging = true;
	      mouseDragX = ev.getX();
	      mouseDragY = ev.getY();
	      repaint();
	    }
	  });
	
	
	addMouseListener(new MouseAdapter() {
	    public void mousePressed(MouseEvent ev) {
	      saveMousePos(ev);
	    }
	    
	    public void mouseReleased(MouseEvent ev) {
	      bIsDragging = false;
	      mouseDragX = ev.getX();
	      mouseDragY = ev.getY();

	      zoomTo(mouseX, mouseY, mouseDragX, mouseDragY);
	      repaint();
	    }
	    
	    
	    public void mouseClicked(MouseEvent ev) {
	      saveMousePos(ev);
	      double k = zoomK;
	      if((ev.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) {
		bundleSelModel.clearSelection();
		if(hiliteBundle != null) {
		  bundleSelModel.setSelected(hiliteBundle.getBundleId(), true);
		}
	      } else {
		double kx = 1.0;
		double ky = 1.0;
		if((ev.getModifiers() & MouseEvent.ALT_MASK) != 0) {
		  kx = k;
		}
		if((ev.getModifiers() & MouseEvent.CTRL_MASK) != 0) {
		  ky = k;
		}
		if((ev.getModifiers() & MouseEvent.SHIFT_MASK) != 0) {
		  kx = 1/ kx;
		  ky = 1/ ky;
		}
		doZoom(ev.getX(), ev.getY(), kx, ky);
	      }
	    }
	  });
      }
      
      void saveMousePos(MouseEvent ev) {
	mouseX = ev.getX();
	mouseY = ev.getY();
      }
      
      void doZoom(int left, int top, double kx, double ky) {
	
	JViewport viewPort = scroll.getViewport();
	
	Dimension size         = getSize();
	Dimension viewSize     = viewPort.getExtentSize();
	
	double w = Math.min(10000, size.width * kx);
	double h = Math.min(10000, size.height * ky);
	Dimension newSize  = new Dimension((int)w, (int)h);

	Rectangle rect = new Rectangle(left, top,
				       viewSize.width,
				       viewSize.height);

	setPreferredSize(newSize);
	revalidate();

	scrollRectToVisible(rect);
	revalidate();
      }

      void zoomTo(int x1, int y1, int x2, int y2) {
	
	int dx = Math.abs(x2 - x1);
	int dy = Math.abs(y2 - y1);
	
	if(dx <= 15 || dy <= 15) {
	  return;
	}

	Dimension size     = getSize();

	double kx = (double)size.width / dx;
	double ky = (double)size.height / dy;

	doZoom((int)(x1 * kx), (int)(y1 * ky), kx, ky);
      }
      
      void doZoomOutAll() {
	
	JViewport viewPort = scroll.getViewport();
	
	Dimension size     = getSize();
	Dimension newSize  = viewPort.getExtentSize();
	
	setPreferredSize(newSize);
	revalidate();
      }
      
      protected void paintComponent(Graphics g) {
	try {
	  Dimension size = getSize();
	  
	  if(isOpaque()) {
	    g.setColor(bgColor);
	    g.fillRect(0,0,size.width, size.height);
	  }
	  paintAll(g);
	} catch (Exception e) {
	  Activator.log.error("Failed to paint", e);
	}
      }
      
      public void paintAll(Graphics _g) {
	try { 
	  Graphics2D g = (Graphics2D)_g;
	  
	  
	  Dimension size = getSize();
	  
	  
	  g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
			     RenderingHints.VALUE_ANTIALIAS_ON);
	  
	  paintBundles(g);
	  paintNearest(g);
	  
	  g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
			     RenderingHints.VALUE_ANTIALIAS_OFF);
	  
	  paintDrag(g);
	} catch (Exception e) {
	  e.printStackTrace();
	}
      }
      
      void paintDrag(Graphics2D g) {
	if(!bIsDragging) {
	  return;
	}
	
	g.setColor(Color.black);
	int x = mouseX;
	int y = mouseY;
	
	int w = mouseDragX - mouseX;
	int h = mouseDragY - mouseY;
	
	if(w < 0) { x += w; w = -w; }
	if(h < 0) { y += h; h = -h; }
	
	g.drawRect(x, y, w, h); 
      }
      
      Color bundleLineColor           = new Color(200,200,200);
      Color bundleSelectedLineColor   = new Color(0,0,0);
      Color bundleHiliteColor         = new Color(230,230,230);
      Color bundleNameColor           = new Color(100,100,100);
      Color bundleEventColor          = new Color(0,  0,  0);
      Color bundleTextColor           = new Color(0,0,0);
      Color bgColor                   = new Color(255,255,255);
      
      int hiliteHeight        = 12;
      

      void paintNearest(Graphics g) {
	FWEvent fwe = getNearest(mouseX, mouseY);

	if(fwe != null) {
	  setToolTipText(fwe.toString());
	  g.setColor(Color.black);
	  g.drawArc(fwe.x - 10, fwe.y - 11,
		    16, 16, 
		    0, 360);
	} else {
	  setToolTipText(null);
	}
      }

      
      FWEvent getNearest(int x, int y) {
	double   bestDist  = Double.MAX_VALUE;
	FWEvent  bestEvent = null;

	for(Iterator itb = bundleToServiceEvents.keySet().iterator(); itb.hasNext();) {
	  Bundle b = (Bundle)itb.next();
	  Map serviceEvents = (Map)bundleToServiceEvents.get(b);

	  for(Iterator it = serviceEvents.keySet().iterator(); it.hasNext(); ) {
	    Long        key  = (Long)it.next();
	    FWEvent     fwe  = (FWEvent)serviceEvents.get(key);
	    
	    double dx    = x - fwe.x;
	    double dy    = y - fwe.y;
	    double dist2 = dx * dx + dy * dy;
	    
	    if(dist2 <= bestDist) {
	      bestDist  = dist2;
	      bestEvent = fwe;
	    }
	  }
	}

	for(Iterator it = bundleEvents.keySet().iterator(); it.hasNext(); ) {
	  Long        key  = (Long)it.next();
	  FWEvent     fwe  = (FWEvent)bundleEvents.get(key);

	  int dx = x - fwe.x;
	  int dy = y - fwe.y;
	  int dist2 = dx * dx + dy * dy;
	  

	  if(dist2 <= bestDist) {
	    bestDist  = dist2;
	    bestEvent = fwe;
	  }
	}

	return bestEvent;
      }
	    

      void paintServices(Graphics g, Bundle b, 
			 long first, 
			 int offsetX, 
			 int offsetY,
			 double kx, double ky) {
	Map serviceEvents = (Map)bundleToServiceEvents.get(b);

	if(serviceEvents == null) {
	  return;
	}

	for(Iterator it = serviceEvents.keySet().iterator(); it.hasNext(); ) {
	  Long        key  = (Long)it.next();
	  FWEvent     fwe  = (FWEvent)serviceEvents.get(key);
	  long        time = fwe.getTime();
	 
	  ServiceReference sr = fwe.getServiceReference();
	  Bundle           b2 = sr.getBundle();
	  if(b2 != null && b.getBundleId() == b2.getBundleId()) {
	    int y = (int)(b.getBundleId() * ky) + offsetY;
	    int x = (int)((time - first) * kx) + offsetX;

	    g.setColor(Color.black);

	    fwe.setPos(x, y + 3);
	    
	    g.drawLine(fwe.x, fwe.y, fwe.x, fwe.y - 3);

	  }
	}
      }

      void paintBundles(Graphics g) {

	Bundle[] bundles = getBundleArray();
	
	if(bundleEvents.size() == 0) {
	  return;
	}
	
	Dimension size = getSize();
	
	int offsetY = 10;
	int offsetX = 10;
	
	
	long first = ((FWEvent)bundleEvents.get(bundleEvents.firstKey())).getTime();
	long last = ((FWEvent)bundleEvents.get(bundleEvents.lastKey())).getTime();
	
	if(last == first) {
	  last = first + 1;
	}
	
	
	long diff = last - first;
	
	double ky = (size.height - offsetY * 2) / (double)bundles.length;
	double kx = (size.width - offsetX * 2) / (double)diff;
	
	int bundleH = (int)((size.height - offsetY/2) / bundles.length);

	Point viewPoint = scroll.getViewport().getViewPosition();

	
	int detailLevel = 0;
	if(bundleH < 12) {
	  detailLevel = 0;
	} else if(bundleH < 20) {
	  detailLevel = 1;
	} else {
	  detailLevel = 2;
	}
	
	int imageSize = 12;
	
	Font font = getSizedFont((bundleH - 10) / 40.0);
	
	
	hiliteBundle   = null;
	hiliteBundleIx = -1;
	
	for(int i = 0; i < bundles.length; i++) {
	  Bundle b = bundles[i];
	  
	  int y = (int)(b.getBundleId() * ky + offsetY);
	  
	  if(mouseY > y - bundleH/2 && mouseY < y + bundleH/2) {
	    hiliteBundle = b;
	    hiliteBundleIx = i;
	    
	    g.setColor(bundleHiliteColor);
	    g.fillRect(offsetX, 
		       y,
		       size.width - offsetX,
		       hiliteHeight);
	  }
	  
	  if(bundleSelModel.isSelected(b.getBundleId())) {
	    g.setColor(bundleSelectedLineColor);
	  } else {
	    g.setColor(bundleLineColor);
	  }
	  g.drawLine(offsetX, 
		     y,
		     size.width - offsetX,
		     y);
	  
	  g.setFont(font);
	  g.setColor(bundleNameColor);
	  
	  g.drawString(Util.getBundleName(b),
		       offsetX,
		       y + font.getSize() + 1);

	  paintServices(g, b, first, offsetX, offsetY, kx, ky);
	}
	
	
	for(Iterator it = bundleEvents.keySet().iterator(); it.hasNext(); ) {
	  Long        key  = (Long)it.next();
	  FWEvent     fwe  = (FWEvent)bundleEvents.get(key);
	  long        time = fwe.getTime();
	  
	  int y = (int)(fwe.getBundle().getBundleId() * ky) + offsetY;
	  int x = (int)((time - first) * kx) + offsetX;
	  
	  fwe.setPos(x, y);

	  g.setColor(bundleEventColor);
	  
	  ImageIcon icon = desktop.getBundleEventIcon(fwe.getType());
	  
	  if(icon != null) {
	    if(imageSize == -1) {
	      g.drawImage(icon.getImage(), x, y, null);
	    } else {
	      g.drawImage(icon.getImage(), 
			  x-10, y-11, 16, 16, null);
	    }
	  } else {
	    g.drawLine(x, y, x, y-3);
	    String s = Util.bundleEventName(fwe.getType());
	    g.drawString(s, x, y);
	  }
	}
	
      }
    }
  }

  static int idCount = 0;
  
  class FWEvent implements Comparable {
    
    long time;
    int  id;
    
    BundleEvent      bundleEvent;
    Bundle           bundle;
    ServiceReference sr;
    int              eventType;
    int              x = -1;
    int              y = -1;
      
    FWEvent          ref;
    
    
    public FWEvent(Bundle b, FWEvent ref) {
      this.id        = idCount++;
      this.time      = System.currentTimeMillis();
      this.bundle    = b; 
      this.ref       = ref;
    }
    
    public FWEvent(Bundle b, int eventType) {
      this.id        = idCount++;
      this.time      = System.currentTimeMillis();
      this.bundle    = b;
      this.eventType = eventType;
    }
    
    public FWEvent(ServiceReference sr, int eventType) {
      this.id        = idCount++;
      this.time      = System.currentTimeMillis();
      this.sr        = sr;
      this.eventType = eventType;
    }
    
    public void setPos(int x, int y) {
      this.x = x;
      this.y = y;
    }

    public Bundle getBundle() {
      return bundle;
    }
    
    public ServiceReference getServiceReference() {
      return sr;
    }
    
    public int getType() {
      return eventType;
    }
    
    public boolean isBundle() {
      return bundle != null && ref == null;
    }
    
    public boolean isSR() {
      return sr != null;
    }
    
    public boolean isRef() {
      return ref != null;
    }
    
    public long getTime() {
      return time;
    }
    
    public int getId() {
      return id;
    }
    
    public int compareTo(Object other) {
      FWEvent e = (FWEvent)other;
      
      return (int)(id - e.id);
    }
    
    public void paint(Graphics2D g, int x, int y) {
      
    }
    
    public String toString() {
      return toString(false);
    }

    public String toString(boolean bCoord) {
      StringBuffer sb = new StringBuffer();
      

      
      if(isBundle()) {
	sb.append(Util.getBundleName(getBundle()));
	sb.append(" ");
	sb.append(Util.bundleEventName(getType()));
      } else if(isSR()) {
	sb.append("#" + getServiceReference().getProperty(Constants.SERVICE_ID));
	String[] sl = (String[])getServiceReference().getProperty(Constants.OBJECTCLASS);
	sb.append(" ");
	for(int i = 0; i < sl.length; i++) {
	  sb.append(sl[i]);
	  if(i < sl.length - 1) {
	    sb.append("/");
	  }
	}
	sb.append(" ");
	sb.append(Util.serviceEventName(getType()));
      }

      sb.append(" " + (new Date(time)).toString());
      if(bCoord) {
	sb.append(" (" + x + ", " + y + ")");
      }
      return sb.toString();
    }
  } 

  
  Font[] fonts = null;
  
  Font getSizedFont(double size) {
    int min = 5; 
    int max = 19;
      
    if(fonts == null) {
      fonts = new Font[10];
      
      for(int i = 0; i < fonts.length; i++) {
	double k = (double)i / fonts.length;
	fonts[i]  = new Font("dialog", 
			     Font.PLAIN, 
			     (int)(min + k * (max - min)));  
      }
    }
    
    int n = (int)(size * (fonts.length - 1));
    n = Math.max(0, Math.min(n, fonts.length - 1));
    
    return fonts[n];
    
  }
}

