
package uk.co.wingpath.modbusgui;

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

public class FileGroupPanel
    extends GridBagPanel
    implements TableVerifier
{
    private final WTable table;
    private final Model model;
    private final StatusBar statusBar;
    private final MirrorField mirror;
    private static final int COLUMN_FILE = 0;
    private static final int COLUMN_ADDRESS = 1;
    private static final int COLUMN_NVALUES = 2;
    private static final int COLUMNS = 3;
    private static final int VISIBLE_ROWS = 4;
    private final boolean isEditor;
    private final ArrayList<FileGroup> groups;
    private final ValueEventSource listeners;
    private boolean isChanging;
    private boolean enabled;
    private JButton addButton;
    private JButton deleteButton;

    public FileGroupPanel (boolean isEditor, final StatusBar statusBar,
        MirrorField mirror)
    {
        this.isEditor = isEditor;
        this.statusBar = statusBar;
        this.mirror = mirror;
        groups = new ArrayList<FileGroup> ();
        listeners = new ValueEventSource ();

        model = new Model ();
        table = new WTable (model);
        table.setSelectionMode (ListSelectionModel.SINGLE_SELECTION);
        enabled = true;

        InputMap inputMap = table.getInputMap (
            JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
        inputMap.put (KeyStroke.getKeyStroke (KeyEvent.VK_ENTER, 0, false),
            "selectNextColumn");

        setupColumns ();

        table.setPreferredScrollableViewportSize (
            new Dimension (200, VISIBLE_ROWS * table.getRowHeight ()));

        JLabel label = new JLabel ("File Register Groups:");
        GridBagConstraints constraints = createConstraints (0, 0);
        add (label, constraints);

        JScrollPane scrollPane = new JScrollPane (table,
            JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
            JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        constraints = createConstraints (0, 1);
        add (scrollPane, constraints);

        ButtonPanel buttonPanel = ButtonPanel.createVertical ();
        constraints = createConstraints (1, 1);
        add (buttonPanel, constraints);

        addButton = buttonPanel.addButton ("Add", null,
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    if (!stopEditing ())
                        return;
                    groups.add (new FileGroup (1, 0, 0));
                    model.fireTableDataChanged ();
                    setSelectedRow (groups.size () - 1);
                    fireValueChanged ();
                    enableButtons ();
                }
            });
        addButton.setMnemonic (KeyEvent.VK_A);
        addButton.setToolTipText ("Add a register group");

        deleteButton = buttonPanel.addButton ("Delete", null,
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    int row = table.getSelectedRow ();
                    if (row < 0)
                        return;
                    cancelEditing ();
                    groups.remove (row);
                    model.fireTableDataChanged ();
                    fireValueChanged ();
                    enableButtons ();
                }
            });
        deleteButton.setMnemonic (KeyEvent.VK_D);
        deleteButton.setToolTipText ("Delete selected register group");
    }

    private void fireValueChanged ()
    {
        listeners.fireValueChanged (new ValueEvent (this));
    }

    public boolean haveValuesChanged (FileGroup [] oldGroups)
    {
        boolean changed = false;
        TableCellEditor cellEditor = table.getCellEditor ();
        if (cellEditor instanceof WCellEditor &&
            ((WCellEditor) cellEditor).hasValueChanged ())
        {
            changed = true;
        }

        if (model.haveValuesChanged (oldGroups))
            changed = true;
        model.fireTableRowsUpdated (0, model.getRowCount () - 1);
        return changed;
    }

    @Override
    public String verify (String str, int row, int col, boolean isChanging)
    {
        if (isChanging)
        {
            statusBar.clear ();
            return str;
        }
        try
        {
            model.fromString (str, row, col);
            statusBar.clear ();
            return str;
        }
        catch (ValueException e)
        {
            statusBar.showException (e);
            return null;
        }
    }

    private void setupColumns ()
    {
        WTableCellRenderer renderer =
            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.isUnapplied (row, col) ?
                            Gui.COLOUR_BACKGROUND_UNAPPLIED :
                        isSelected ? Gui.COLOUR_BACKGROUND_SELECTED :
                            Gui.COLOUR_BACKGROUND_NORMAL);
                    return component;
                }
            };
        ((JLabel) renderer).setHorizontalAlignment (SwingConstants.RIGHT);

        for (int col = 0 ; col < COLUMNS ; col++)
        {
            TableColumn column = table.getColumnModel ().getColumn (col);
            column.setCellRenderer (renderer);
            WCellEditor cellEditor = new WCellEditor (listeners);
            cellEditor.setVerifier (this);
            cellEditor.setMirror (mirror);
            column.setCellEditor (cellEditor);
            column.setPreferredWidth (Gui.getTextWidth (8));
        }
    }

    public boolean stopEditing ()
    {
        TableCellEditor cellEditor = table.getCellEditor ();
        if (cellEditor != null)
            return cellEditor.stopCellEditing ();
        return true;
    }

    private void cancelEditing ()
    {
        TableCellEditor cellEditor = table.getCellEditor ();
        if (cellEditor != null)
            cellEditor.cancelCellEditing ();
    }

    FileGroup [] getGroups ()
    {
        FileGroup [] result = new FileGroup [groups.size ()];
        return groups.toArray (result);
    }

    private void setSelectedRow (int row)
    {
        table.getSelectionModel ().setSelectionInterval (row, row);
        table.scrollRectToVisible (table.getCellRect (row, 0, true));
    }

    void setGroups (FileGroup [] groups)
    {
        Event.checkIsEventDispatchThread ();
        cancelEditing ();
        this.groups.clear ();
        Collections.addAll (this.groups, groups);
        model.fireTableDataChanged ();
        if (groups.length != 0)
            setSelectedRow (0);
        enableButtons ();
    }

    public boolean checkValues ()
    {
        return stopEditing ();
    }

    @Override
    public void setEnabled (boolean enabled)
    {
        this.enabled = enabled;
        super.setEnabled (enabled);
        enableButtons ();
    }

    private void enableButtons ()
    {
        addButton.setEnabled (enabled);
        deleteButton.setEnabled (enabled && table.getSelectedRow () >= 0);
    }

    class Model
        extends AbstractTableModel
        implements HasCellLabels
    {
        private FileGroup [] savedGroups = null;

        public int getColumnCount ()
        {
            return COLUMNS;
        }

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

        public String getValueAt (int row, int col)
        {
            if (row >= groups.size ())
                return "";
            FileGroup group = groups.get (row);
            int value = 0;
            switch (col)
            {
            case COLUMN_FILE:
                value = group.fileNum;
                break;
            case COLUMN_ADDRESS:
                value = group.address;
                break;
            case COLUMN_NVALUES:
                value = group.nvalues;
                break;
            default:
                throw new AssertionError ("Unreachable");
            }
            return "" + value;
        }

        @Override
        public void setValueAt (Object value, int row, int col)
        {
            try
            {
                fromString ((String) value, row, col);
            }
            catch (ValueException e)
            {
                // Shouldn't happen - we have verifiers on all columns.
                throw new IllegalStateException (e);
            }
        }

        void fromString (String str, int row, int col)
            throws ValueException
        {
            if (row >= groups.size ())
                return;
            FileGroup group = groups.get (row);
            int minVal = col == COLUMN_FILE ? 1 : 0;
            FileGroup newGroup = null;
            try
            {
                int value = Integer.parseInt (str);
                if (value < minVal || value > 65535)
                {
                    throw new ValueException (
                        "Must be in the range " + minVal + " to 65535");
                }
                switch (col)
                {
                case COLUMN_FILE:
                    newGroup =
                        new FileGroup (value, group.address, group.nvalues);
                    break;
                case COLUMN_ADDRESS:
                    newGroup =
                        new FileGroup (group.fileNum, value, group.nvalues);
                    break;
                case COLUMN_NVALUES:
                    newGroup =
                        new FileGroup (group.fileNum, group.address, value);
                    break;
                }
            }
            catch (NumberFormatException e)
            {
                throw new ValueException ("Must be an integer");
            }
            if (!newGroup.equals (group))
            {
                groups.set (row, newGroup);
                fireValueChanged ();
            }
        }

        @Override
        public String getColumnName (int col)
        {
            switch (col)
            {
            case COLUMN_FILE:
                return "File";
            case COLUMN_ADDRESS:
                return "Address";
            case COLUMN_NVALUES:
                return "Count";
            default:
                return "";
            }
        }

        @Override
        public String getCellLabel (int row, int col)
        {
            return row + " " + getColumnName (col);
        }

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

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

        private boolean isUnapplied (int row, int col)
        {
            if (!isEditor || savedGroups == null)
                return false;
            if (row >= groups.size ())
                return false;
            if (row >= savedGroups.length)
                return true;
            FileGroup group = groups.get (row);
            FileGroup savedGroup = savedGroups [row];
            switch (col)
            {
            case COLUMN_FILE:
                return group.fileNum != savedGroup.fileNum;
            case COLUMN_ADDRESS:
                return group.address != savedGroup.address;
            case COLUMN_NVALUES:
                return group.nvalues != savedGroup.nvalues;
            }
            return false;
        }

        private boolean haveValuesChanged (FileGroup [] oldGroups)
        {
            savedGroups = oldGroups;
            return !Arrays.equals (oldGroups, getGroups ());
        }
    }

    public void addValueListener (ValueListener l)
    {
        listeners.addListener (l);
    }

    public void removeValueListener (ValueListener l)
    {
        listeners.removeListener (l);
    }
}

