
package uk.co.wingpath.modbusgui;

import java.io.*;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import uk.co.wingpath.util.*;
import uk.co.wingpath.gui.*;
import uk.co.wingpath.modbus.*;
import uk.co.wingpath.event.*;
import uk.co.wingpath.event.Event;

public class CommandTable
    implements TreeCard
{
    private static final int NAME_COL = 0;
    private static final int TYPE_COL = 1;
    private static final int RESULT_COL = 2;

    private final Frontend frontend;
    private final boolean isTester;
    private final Backend backend;
    private final Settings settings;
    private final StatusBar statusBar;
    private final JPanel outerPanel;
    private final JButton deleteButton;
    private final JButton deleteAllButton;
    private final JToggleButton sendButton;
    private final JToggleButton sendAllButton;
    private final JTable table;
    private final CmdTableModel model;
    private final JScrollPane scrollPane;
    private final CommandList commands;
    private final Variable<Command> selectedCommand;
    private final int columnCount;

    private final Action cancelAction;
    private boolean deleting;
    private final ValueEventSource statusListeners;

    public CommandTable (final Frontend frontend, final Backend backend,
        Settings settings)
    {
        Event.checkIsEventDispatchThread ();
        this.frontend = frontend;
        this.backend = backend;
        this.settings = settings;
        isTester = frontend.getProduct ().isTester ();
        commands = settings.getCommands ();
        selectedCommand = commands.getSelectedCommand ();
        model = new CmdTableModel ();
        columnCount = isTester ? 3 : 2;
        deleting = false;
        statusListeners = new ValueEventSource ();

        statusBar = new StatusBar ("comtab", frontend.getHelpViewer ());
        statusBar.addStatusListener (
            new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    fireStatusChanged ();
                }
            });

        outerPanel = new JPanel ();
        outerPanel.setLayout (new BorderLayout ());
        JLabel heading = Gui.createDialogHeading ("Command Table");
        outerPanel.add (heading, BorderLayout.NORTH);
        outerPanel.add (statusBar, BorderLayout.SOUTH);

        JPanel mainPanel = new JPanel ();
        outerPanel.add (mainPanel, BorderLayout.CENTER);
        mainPanel.setLayout (new BorderLayout ());
        ButtonPanel buttonPanel = new ButtonPanel ();
        mainPanel.add (buttonPanel, BorderLayout.SOUTH);

        table = new WTable (model);
        TableRowTransferHandler.setHandler (table);
        table.setPreferredScrollableViewportSize (new Dimension (200, 200));
        scrollPane = new JScrollPane (table);
        mainPanel.add (scrollPane, BorderLayout.CENTER);

        for (int col = 0 ; col < columnCount ; col++)
        {
            TableColumn column = table.getColumnModel ().getColumn (col);
            switch (col)
            {
            case NAME_COL:
                break;
            case TYPE_COL:
                break;
            case RESULT_COL:
                column.setPreferredWidth (Gui.getTextWidth (8));
                column.setMaxWidth (Gui.getTextWidth (6));
                break;
            }
        }

        table.setDefaultRenderer (String.class,
            new WTableCellRenderer ()
            {
                public Component getTableCellRendererComponent (
                    JTable table, Object value, boolean isSelected,
                    boolean hasFocus, int row, int column)
                {
                    Component component =
                         super.getTableCellRendererComponent (table, value,
                            isSelected, hasFocus, row, column);
                    int col = table.convertColumnIndexToModel (column);
                    component.setBackground (
                        model.isError (row, col) ?
                            Gui.COLOUR_BACKGROUND_ERROR :
                        isSelected ? Gui.COLOUR_BACKGROUND_SELECTED :
                        Gui.COLOUR_BACKGROUND_NORMAL);
                    return component;
                }
            });

        table.getSelectionModel ().addListSelectionListener (
            new ListSelectionListener ()
            {
                public void valueChanged (ListSelectionEvent e)
                {
                    if (e.getValueIsAdjusting ())
                        return;
                    int row = table.getSelectedRow ();
                    Command command = row < 0 ? null :
                        commands.getCommandAt (row);
                    selectedCommand.setValue (command);
                }
            });

        final Action deleteAction = new AbstractAction ("Delete")
            {
                public void actionPerformed (ActionEvent e)
                {
                    statusBar.clear ();

                    Command [] cmds = getSelectedCommands ();

                    for (int i = 0 ; i < cmds.length ; i++)
                        commands.deleteCommand (cmds [i]);

                    deleting = false;
                    enableButtons ();
                }
            };
        deleteAction.putValue (AbstractAction.MNEMONIC_KEY, KeyEvent.VK_D);

        final Action deleteAllAction = new AbstractAction ("Delete All")
            {
                public void actionPerformed (ActionEvent e)
                {
                    statusBar.clear ();
                    commands.deleteAll ();
                    deleting = false;
                    enableButtons ();
                }
            };
        deleteAllAction.putValue (AbstractAction.MNEMONIC_KEY, KeyEvent.VK_D);

        cancelAction = new AbstractAction ("Cancel")
            {
                public void actionPerformed (ActionEvent e)
                {
                    deleting = false;
                    statusBar.clear ();
                    enableButtons ();
                }
            };
        cancelAction.putValue (
            AbstractAction.MNEMONIC_KEY, KeyEvent.VK_C);

        sendButton = new JToggleButton (isTester ? "Test" : "Send");
        sendButton.addActionListener (
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    sendCommands (getSelectedCommands (), sendButton);
                }
            });
        sendButton.setMnemonic (isTester ? KeyEvent.VK_T : KeyEvent.VK_S);
        sendButton.setToolTipText ("Send selected commands to slave");
        Gui.setEnabled (sendButton, false);
        sendButton.setRequestFocusEnabled (false);
        buttonPanel.add (sendButton);

        sendAllButton = new JToggleButton (isTester ? "Test All" : "Send All");
        sendAllButton.addActionListener (
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    sendAllCommands (sendAllButton);
                }
            });
        sendAllButton.setMnemonic (KeyEvent.VK_A);
        sendAllButton.setToolTipText ("Send all commands to slave");
        Gui.setEnabled (sendAllButton, false);
        sendAllButton.setRequestFocusEnabled (false);
        buttonPanel.add (sendAllButton);

        deleteButton = buttonPanel.addButton ("Delete", null,
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    Command command = selectedCommand.getValue ();
                    if (command != null)
                    {
                        deleting = true;
                        statusBar.clear ();
                        statusBar.showWarning ("Delete selected commands?",
                            deleteAction, cancelAction);
                        enableButtons ();
                    }
                }
            });
        deleteButton.setMnemonic (KeyEvent.VK_D);
        deleteButton.setToolTipText ("Delete selected commands");
        Gui.setEnabled (deleteButton, false);

        deleteAllButton = buttonPanel.addButton ("Delete All", null,
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    if (commands.getSize () != 0)
                    {
                        deleting = true;
                        statusBar.clear ();
                        statusBar.showWarning ("Delete ALL commands?",
                            deleteAllAction, cancelAction);
                        enableButtons ();
                    }
                }
            });
        deleteAllButton.setToolTipText ("Delete all commands");
        Gui.setEnabled (deleteAllButton, false);

        buttonPanel.addButton (getHelpAction ());

        selectedCommand.addValueListener (new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    Command command = selectedCommand.getValue ();
                    int index = command == null ? -1 :
                        commands.getIndex (command);
                    int row = table.getSelectedRow ();
                    if (index != row)
                    {
                        if (index < 0)
                        {
                            table.getSelectionModel ().removeSelectionInterval (
                                row, row);
                        }
                        else
                        {
                            table.getSelectionModel ().setSelectionInterval (
                                index, index);
                            table.scrollRectToVisible (
                                table.getCellRect (index, 0, true));
                        }
                    }
                    // statusBar.clear ();
                    enableButtons ();
                }
            });

        commands.addValueListener (new ValueListener ()
        {
            public void valueChanged (ValueEvent e)
            {
                model.fireTableRowsUpdated (0, commands.getSize () - 1);
            }
        });

        frontend.addBackendStateListener (new BackendState.Listener ()
        {
            public void stateChanged (BackendState state)
            {
                enableButtons ();
            }
        });
    }

    private void enableButtons ()
    {
        Command command = selectedCommand.getValue ();
        Gui.setEnabled (deleteButton, command != null && !deleting);
        Gui.setEnabled (deleteAllButton,
            commands.getSize () != 0 && !deleting);
        Gui.setEnabled (sendButton,
            command != null &&
            !deleting &&
            frontend.isRunning () &&
            frontend.isMaster ());
        Gui.setEnabled (sendAllButton,
            commands.getSize () != 0 &&
            !deleting &&
            frontend.isRunning () &&
            frontend.isMaster ());
    }

    public Command [] getSelectedCommands ()
    {
        int [] rows = table.getSelectedRows ();
        Command [] cmds = new Command [rows.length];

        for (int i = 0 ; i < rows.length ; i++)
            cmds [i] = commands.getCommandAt (rows [i]);

        return cmds;
    }

    private Command [] getCommands ()
    {
        Command [] cmds = new Command [commands.getSize ()];

        for (int i = 0 ; i < cmds.length ; i++)
            cmds [i] = commands.getCommandAt (i);

        return cmds;
    }

    public JComponent getPanel ()
    {
        return outerPanel;
    }

    public String getTag ()
    {
        return "command.table";
    }

    public String getName ()
    {
        return "Command Table";
    }

    public JButton getDefaultButton ()
    {
        return null;
    }

    public Action getHelpAction ()
    {
        return frontend.getHelpAction ("command_table");
    }

    public String getToolTipText ()
    {
        return null;
    }

    @Override
    public void selected ()
    {
    }

    @Override
    public void reset ()
    {
    }

    public boolean hasUnappliedChanges ()
    {
        return false;
    }

    public boolean hasError ()
    {
        return statusBar.hasError ();
    }

    private class CmdTableModel
        extends AbstractTableModel
        implements Reorderable
    {
        private CmdTableModel ()
        {
            commands.addListListener (
                new ListDataListener ()
                {
                    public void intervalAdded (ListDataEvent e)
                    {
                        fireTableRowsInserted (e.getIndex0 (), e.getIndex1 ());
                    }

                    public void intervalRemoved (ListDataEvent e)
                    {
                        fireTableRowsDeleted (e.getIndex0 (), e.getIndex1 ());
                    }

                    public void contentsChanged (ListDataEvent e)
                    {
                        int index0 = e.getIndex0 ();
                        int index1 = e.getIndex1 ();
                        if (index0 < 0 && index1 < 0)
                        {
                            // Combo "selection" event - ignore
                        }
                        else if (index0 < 0 && index1 == 0)
                        {
                            // Whole table updated
                            fireTableDataChanged ();
                        }
                        else
                        {
                            // Normal update
                            fireTableRowsUpdated (index0, index1);
                        }
                    }

                });
        }

        @Override
        public int getColumnCount ()
        {
            return columnCount;
        }

        @Override
        public String getColumnName (int col)
        {
            switch (col)
            {
            case NAME_COL:
                return "Name";
            case TYPE_COL:
                return "Command Type";
            case RESULT_COL:
                return "Result";
            default:
                return "";
            }
        }

        @Override
        public Class getColumnClass (int col)
        {
            return String.class;
        }

        @Override
        public int getRowCount ()
        {
            return commands.getSize ();
        }

        @Override
        public Object getValueAt (int row, int col)
        {
            Command command = commands.getCommandAt (row);
            switch (col)
            {
            case NAME_COL:
                return command.toString ();
            case TYPE_COL:
                return command.getTypeName ();
            case RESULT_COL:
                return isError (row, col) ? "Fail" : "Pass";
            default:
                return "";
            }
        }

        @Override
        public void setValueAt (Object value, int row, int col)
        {
            throw new IllegalStateException ();
        }

        @Override
        public boolean isCellEditable (int row, int col)
        {
            return false;
        }

        private boolean isError (int row, int col)
        {
            if (col != RESULT_COL)
                return false;
            Command command = commands.getCommandAt (row);
            Command.Result expected = command.getExpectedResult ();
            Command.Result actual = command.getActualResult ();
            return actual != null && !expected.matches (actual);
        }

        @Override
        public void reorder (int firstIndex, int lastIndex, int toIndex)
        {
            commands.moveCommands (firstIndex, lastIndex, toIndex);
        }
    }

    private void sendCommand (final Command command)
    {
        final ModbusClient cl = backend.getClient ();
        if (cl == null)
        {
            statusBar.showError ("I100", "Connection closed");
            return;
        }

        statusBar.clear ();
        statusBar.showMessage ("Sending command ...");

        backend.execute (new Backend.Task ()
        {
            public void run ()
                throws InterruptedException, IOException, ValueException
            {
                command.send (cl, settings);
            }

            public void done ()
            {
                commands.fireValueChanged ();
                Command.Result result = command.getActualResult ();
                ModbusException ex = result.getException ();
                if (ex == null)
                {
                    statusBar.showMessage ("OK - response time " +
                        Metric.formatNanoTime (result.getResponseTime ()));
                }
                else
                {
                    statusBar.showException (ex);
                }
            }

            public void exceptionOccurred (Exception ex)
            {
                statusBar.showException (ex);
                if (ex instanceof IOException)
                    backend.stop ();
            }
        });
    }

    private int failCount;

    public void sendAllCommands (JToggleButton button)
    {
        sendCommands (getCommands (), button);
    }

    private void sendCommands (final Command [] cmds,
        final JToggleButton button)
    {
        if (button != null && !button.isSelected ())
            return;
        if (cmds.length == 0)
        {
            if (button != null)
                button.setSelected (false);
            return;
        }
        final ModbusClient cl = backend.getClient ();
        if (cl == null)
        {
            statusBar.showError ("I100", "Connection closed");
            return;
        }

        statusBar.clear ();
        statusBar.showMessage (
            (isTester ? "Testing" : "Sending") + " command" +
            (cmds.length > 1 ? "s" : "") + " ...");

        backend.execute (new Backend.Task ()
        {
            ResponseTimeAverager responseTimer =
                new ResponseTimeAverager ();

            public void run ()
                throws InterruptedException, IOException, ValueException
            {
                int requestDelay =
                    settings.getGeneral ().getRequestDelay ().getValue ();
                failCount = 0;

                for (int i = 0 ; i < cmds.length ; i++)
                {
                    if (button != null && !button.isSelected ())
                        break;
                    if (i != 0 && requestDelay != 0)
                        Thread.sleep (requestDelay);
                    Command command = cmds [i];
                    commands.getSelectedCommand ().setValue (command);
                    command.send (cl, settings);
                    Command.Result actual = command.getActualResult ();
                    if (isTester)
                    {
                        Command.Result expected = command.getExpectedResult ();
                        if (!expected.matches (actual))
                            failCount++;
                    }
                    else
                    {
                        ModbusException ex = actual.getException ();
                        if (ex == null)
                        {
                            responseTimer.inform (actual.getResponseTime ());
                        }
                        else
                        {
                            statusBar.showException (ex);
                            break;
                        }
                    }
                }
            }

            public void done ()
            {
                commands.fireValueChanged ();
                if (isTester)
                {
                    if (button == null && failCount == 0)
                        System.exit (0);
                    if (cmds.length > 1)
                    {
                        statusBar.showMessage (
                            failCount == 0 ? "All tests passed" :
                            failCount == 1 ? "1 test failed" :
                            failCount + " tests failed");
                    }
                    else
                    {
                        statusBar.clear ();
                    }
                }
                else
                {
                    if (cmds.length > 1)
                    {
                        statusBar.showMessage ("Commands sent");
                    }
                    else
                    {
                        long responseTime = responseTimer.getAverage ();
                        statusBar.showMessage ("OK - response time " +
                            Metric.formatNanoTime (responseTime));
                    }
                }
                if (button != null)
                    button.setSelected (false);
            }

            public void exceptionOccurred (Exception ex)
            {
                statusBar.showException (ex);
                if (ex instanceof IOException)
                    backend.stop ();
                if (button != null)
                    button.setSelected (false);
            }
        });
    }

    private void fireStatusChanged ()
    {
        statusListeners.fireValueChanged (this);
    }

    public void addStatusListener (ValueListener l)
    {
        statusListeners.addListener (l);
    }

    public void removeStatusListener (ValueListener l)
    {
        statusListeners.removeListener (l);
    }
}

