package lcm.spy;

import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;
import java.awt.event.*;

import java.io.*;
import java.util.*;
import lcm.util.*;
import java.lang.reflect.*;

import lcm.lcm.*;

/** Spy main class. **/
public class Spy
{
    LCM          lcm;
    LCMTypeDatabase handlers;
    long startuTime; // time that lcm-spy started

    HashMap<String,  ChannelData> channelMap = new HashMap<String, ChannelData>();
    ArrayList<ChannelData>        channelList = new ArrayList<ChannelData>();

    ChannelTableModel _channelTableModel = new ChannelTableModel();
    TableSorter  channelTableModel = new TableSorter(_channelTableModel);
    JTable channelTable = new JTable(channelTableModel);
    ChartData chartData;

    ArrayList<SpyPlugin> plugins = new ArrayList<SpyPlugin>();

    JButton clearButton = new JButton("Clear");

    public Spy(String lcmurl) throws IOException
    {
        //    sortedChannelTableModel.addMouseListenerToHeaderInTable(channelTable);
        channelTableModel.setTableHeader(channelTable.getTableHeader());
        channelTableModel.setSortingStatus(0, TableSorter.ASCENDING);

        handlers = new LCMTypeDatabase();

        TableColumnModel tcm = channelTable.getColumnModel();
        tcm.getColumn(0).setMinWidth(140);
        tcm.getColumn(1).setMinWidth(140);
        tcm.getColumn(2).setMaxWidth(100);
        tcm.getColumn(3).setMaxWidth(100);
        tcm.getColumn(4).setMaxWidth(100);
        tcm.getColumn(5).setMaxWidth(100);
        tcm.getColumn(6).setMaxWidth(100);

        JFrame jif = new JFrame("LCM Spy");
        jif.setLayout(new BorderLayout());
        jif.add(channelTable.getTableHeader(), BorderLayout.PAGE_START);
        // XXX weird bug, if clearButton is added after JScrollPane, we get an error.
        jif.add(clearButton, BorderLayout.SOUTH);
        jif.add(new JScrollPane(channelTable), BorderLayout.CENTER);

        chartData = new ChartData(utime_now());

        jif.setSize(800,600);
        jif.setLocationByPlatform(true);
        jif.setVisible(true);

        if(null == lcmurl)
            lcm = new LCM();
        else
            lcm = new LCM(lcmurl);
        lcm.subscribeAll(new MySubscriber());

        new HzThread().start();

        clearButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                channelMap.clear();
                channelList.clear();
                channelTableModel.fireTableDataChanged();
            }
        });

        channelTable.addMouseListener(new MouseAdapter()
        {
            public void mouseClicked(MouseEvent e)
            {
                int mods=e.getModifiersEx();

                if (e.getButton()==3)
                {
                    showPopupMenu(e);
                }
                else if (e.getClickCount() == 2)
                {
                    Point p = e.getPoint();
                    int row = rowAtPoint(p);

                    ChannelData cd = channelList.get(row);
                    boolean got_one = false;
                    for (SpyPlugin plugin : plugins)
                    {
                        if (!got_one && plugin.canHandle(cd.fingerprint)) {

                            // start the plugin
                            (new PluginStarter(plugin, cd)).getAction().actionPerformed(null);

                            got_one = true;
                        }
                    }

                    if (!got_one)
                        createViewer(channelList.get(row));
                }
            }
        });

        jif.addWindowListener(new WindowAdapter()
        {
            public void windowClosing(WindowEvent e)
            {
                System.out.println("Spy quitting");
                System.exit(0);
            }
        });

        ClassDiscoverer.findClasses(new PluginClassVisitor());
        System.out.println("Found "+plugins.size()+" plugins");
        for (SpyPlugin plugin : plugins) {
            System.out.println(" "+plugin);
        }

    }

    class PluginStarter
    {

        private SpyPlugin plugin;
        private ChannelData cd;
        private String name;


        public PluginStarter(SpyPlugin pluginIn, ChannelData cdIn)
        {
            plugin = pluginIn;
            cd = cdIn;
            Action thisAction = plugin.getAction(null, null);
            name = (String) thisAction.getValue("Name");
        }

        public Action getAction() { return new PluginStarterAction(); }

        class PluginStarterAction extends AbstractAction
        {
            public PluginStarterAction() {
                super(name);
            }

            @Override
            public void actionPerformed(ActionEvent e) {

                // for historical reasons, plugins expect a JDesktopPane
                // here we create a JFrame, add a JDesktopPane, and start the
                // plugin by calling its actionPerformed method

                JFrame pluginFrame = new JFrame(cd.name);
                pluginFrame.setLayout(new BorderLayout());
                JDesktopPane pluginJdp = new JDesktopPane();
                pluginFrame.add(pluginJdp);
                pluginFrame.setSize(500, 400);
                pluginFrame.setLocationByPlatform(true);
                pluginFrame.setVisible(true);

                plugin.getAction(pluginJdp, cd).actionPerformed(null);

            }
        }
    }

    class PluginClassVisitor implements ClassDiscoverer.ClassVisitor
    {
        public void classFound(String jar, Class cls)
        {
            Class interfaces[] = cls.getInterfaces();
            for (Class iface : interfaces) {
                if (iface.equals(SpyPlugin.class)) {
                    try {
                        Constructor c = cls.getConstructor(new Class[0]);
                        SpyPlugin plugin = (SpyPlugin) c.newInstance(new Object[0]);
                        plugins.add(plugin);
                    } catch (Exception ex) {
                        System.out.println("ex: "+ex);
                    }
                }
            }
        }
    }

    void createViewer(ChannelData cd)
    {

        if (cd.viewerFrame != null && !cd.viewerFrame.isVisible())
        {
            cd.viewerFrame.dispose();
            cd.viewer = null;
        }

        if (cd.viewer == null) {
            cd.viewerFrame = new JFrame(cd.name);

            cd.viewer = new ObjectPanel(cd.name, chartData);
            cd.viewer.setObject(cd.last, cd.last_utime);

            //    cd.viewer = new ObjectViewer(cd.name, cd.cls, null);
            cd.viewerFrame.setLayout(new BorderLayout());

            // default scroll speed is too slow, so increase it
            JScrollPane viewerScrollPane = new JScrollPane(cd.viewer);
            viewerScrollPane.getVerticalScrollBar().setUnitIncrement(16);
            
            // we need to tell the viewer what its viewport is so that it can
            // make smart decisions about which elements are in view of the user
            // so it can avoid drawing items outside the view
            cd.viewer.setViewport(viewerScrollPane.getViewport());

            cd.viewerFrame.add(viewerScrollPane, BorderLayout.CENTER);

            //jdp.add(cd.viewerFrame);

            cd.viewerFrame.setSize(650,400);
            cd.viewerFrame.setLocationByPlatform(true);
            cd.viewerFrame.setVisible(true);
        } else {
            cd.viewerFrame.setVisible(true);
            //cd.viewerFrame.moveToFront();
        }
    }

    static final long utime_now()
    {
        return System.nanoTime()/1000;
    }

    class ChannelTableModel extends AbstractTableModel
    {
        public int getColumnCount()
        {
            return 8;
        }

        public int getRowCount()
        {
            return channelList.size();
        }

        public Object getValueAt(int row, int col)
        {
            ChannelData cd = channelList.get(row);
            if (cd == null)
                return "";

            switch (col)
            {
                case 0:
                    return cd.name;
                case 1:
                    if (cd.cls == null)
                        return String.format("?? %016x", cd.fingerprint);

                    String s = cd.cls.getName();
                    return s.substring(s.lastIndexOf('.')+1);

                case 2:
                    return ""+cd.nreceived;
                case 3:
                    return String.format("%6.2f", cd.hz);
                case 4:
                    return String.format("%6.2f ms",1000.0/cd.hz); // cd.max_interval/1000.0);
                case 5:
                    return String.format("%6.2f ms",(cd.max_interval - cd.min_interval)/1000.0);
                case 6:
                    return String.format("%6.2f KB/s", (cd.bandwidth/1024.0));
                case 7:
                    return ""+cd.nerrors;
            }
            return "???";
        }

        public String getColumnName(int col)
        {
            switch (col)
            {
                case 0:
                    return "Channel";
                case 1:
                    return "Type";
                case 2:
                    return "Num Msgs";
                case 3:
                    return "Hz";
                case 4:
                    return "1/Hz";
                case 5:
                    return "Jitter";
                case 6:
                    return "Bandwidth";
                case 7:
                    return "Undecodable";
            }
            return "???";
        }

    }

    class MySubscriber implements LCMSubscriber
    {
        public void messageReceived(LCM lcm, String channel, LCMDataInputStream dins)
        {
            Object o = null;
            ChannelData cd = channelMap.get(channel);
            int msg_size = 0;

            try {
                msg_size = dins.available();
                long fingerprint = (msg_size >=8) ? dins.readLong() : -1;
                dins.reset();

                Class cls = handlers.getClassByFingerprint(fingerprint);

                if (cd == null) {
                    cd = new ChannelData();
                    cd.name = channel;
                    cd.cls = cls;
                    cd.fingerprint = fingerprint;
                    cd.row = channelList.size();

                    synchronized(channelList) {
                        channelMap.put(channel, cd);
                        channelList.add(cd);
                        _channelTableModel.fireTableDataChanged();
                    }

                } else {
                    if (cls != null && cd.cls != null && !cd.cls.equals(cls)) {
                        System.out.println("WARNING: Class changed for channel "+channel);
                        cd.nerrors++;
                    }
                }

                long utime = utime_now();
                long interval = utime - cd.last_utime;
                cd.hz_min_interval = Math.min(cd.hz_min_interval, interval);
                cd.hz_max_interval = Math.max(cd.hz_max_interval, interval);
                cd.hz_bytes += msg_size;
                cd.last_utime = utime;

                cd.nreceived++;

                o = cd.cls.getConstructor(DataInput.class).newInstance(dins);
                cd.last = o;

                if (cd.viewer != null)
                    cd.viewer.setObject(o, cd.last_utime);

            } catch (NullPointerException ex) {
                cd.nerrors++;
            } catch (IOException ex) {
                cd.nerrors++;
                System.out.println("Spy.messageReceived ex: "+ex);
            } catch (NoSuchMethodException ex) {
                cd.nerrors++;
                System.out.println("Spy.messageReceived ex: "+ex);
            } catch (InstantiationException ex) {
                cd.nerrors++;
                System.out.println("Spy.messageReceived ex: "+ex);
            } catch (IllegalAccessException ex) {
                cd.nerrors++;
                System.out.println("Spy.messageReceived ex: "+ex);
            } catch (InvocationTargetException ex) {
                cd.nerrors++;
                // these are almost always spurious
                //System.out.println("ex: "+ex+"..."+ex.getTargetException());
            }
        }
    }

    class HzThread extends Thread
    {
        public HzThread()
        {
            setDaemon(true);
        }

        public void run()
        {
            while (true)
            {
                long utime = utime_now();

                synchronized(channelList)
                {
                    for (ChannelData cd : channelList)
                    {
                        long diff_recv = cd.nreceived - cd.hz_last_nreceived;
                        cd.hz_last_nreceived = cd.nreceived;
                        long dutime = utime - cd.hz_last_utime;
                        cd.hz_last_utime = utime;

                        cd.hz = diff_recv / (dutime/1000000.0);

                        cd.min_interval = cd.hz_min_interval;
                        cd.max_interval = cd.hz_max_interval;
                        cd.hz_min_interval = 9999;
                        cd.hz_max_interval = 0;
                        cd.bandwidth = cd.hz_bytes / (dutime/1000000.0);
                        cd.hz_bytes = 0;
                    }
                }

                int selrow = channelTable.getSelectedRow();
                channelTableModel.fireTableDataChanged();
                if (selrow >= 0)
                    channelTable.setRowSelectionInterval(selrow, selrow);

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException ex) {
                }
            }
        }

    }

    class DefaultViewer extends AbstractAction
    {
        ChannelData cd;

        public DefaultViewer(ChannelData cd)
        {
            super("Structure Viewer...");
            this.cd = cd;
        }

        public void actionPerformed(ActionEvent e)
        {
            createViewer(cd);
        }
    }

    int rowAtPoint(Point p)
    {
        int physicalRow = channelTable.rowAtPoint(p);

        return channelTableModel.modelIndex(physicalRow);
    }

    public void showPopupMenu(MouseEvent e)
    {
        Point p = e.getPoint();
        int row = rowAtPoint(p);
        ChannelData cd = channelList.get(row);
        JPopupMenu jm = new JPopupMenu("Viewers");

        int prow = channelTable.rowAtPoint(p);
        channelTable.setRowSelectionInterval(prow, prow);

        jm.add(new DefaultViewer(cd));



        if (cd.cls != null)
        {
            for (SpyPlugin plugin : plugins)
            {
                if (plugin.canHandle(cd.fingerprint))
                {
                    jm.add(new PluginStarter(plugin, cd).getAction());

                    //jm.add(plugin.getAction(this_desktop_pane, cd));
                }
            }
        }

        jm.show(channelTable, e.getX(), e.getY());
    }

    public static void usage()
    {
        System.err.println("usage: lcm-spy [options]");
        System.err.println("");
        System.err.println("lcm-spy is the Lightweight Communications and Marshalling traffic ");
        System.err.println("inspection utility.  It is a graphical tool for viewing messages received on ");
        System.err.println("an LCM network, and is analagous to tools like Ethereal/Wireshark and tcpdump");
        System.err.println("in that it is able to inspect all LCM messages received and provide information");
        System.err.println("and statistics on the channels used.");
        System.err.println("");
        System.err.println("When given appropriate LCM type definitions, lcm-spy is able to");
        System.err.println("automatically detect and decode messages, and can display the individual fields");
        System.err.println("of recognized messages.  lcm-spy is limited to displaying statistics for");
        System.err.println("unrecognized messages.");
        System.err.println("");
        System.err.println("Options:");
        System.err.println("  -l, --lcm-url=URL      Use the specified LCM URL");
        System.err.println("  -h, --help             Shows this help text and exits");
        System.err.println("");
        System.exit(1);
    }

    public static void main(String args[])
    {
        // check if the JRE is supplied by gcj, and warn the user if it is.
        if(System.getProperty("java.vendor").indexOf("Free Software Foundation") >= 0) {
            System.err.println("WARNING: Detected gcj. lcm-spy is not known to work well with gcj.");
            System.err.println("         The Sun JRE is recommended.");
        }

        String lcmurl = null;
        for(int optind=0; optind<args.length; optind++) {
            String c = args[optind];
            if(c.equals("-h") || c.equals("--help")) {
                usage();
            } else if(c.equals("-l") || c.equals("--lcm-url") || c.startsWith("--lcm-url=")) {
                String optarg = null;
                if(c.startsWith("--lcm-url=")) {
                    optarg=c.substring(10);
                } else if(optind < args.length) {
                    optind++;
                    optarg = args[optind];
                }
                if(null == optarg) {
                    usage();
                } else {
                    lcmurl = optarg;
                }
            } else {
                usage();
            }
        }

        try {
            new Spy(lcmurl);
        } catch (IOException ex) {
            System.out.println(ex);
        }
    }
}
