
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 DeviceIdPanel
    extends JPanel
    implements TableVerifier
{
    private final JTable table;
    private final Model model;
    private final StatusBar statusBar;
    private final MirrorField mirror;
    private final JLabel label;
    private static final int COLUMN_ID = 0;
    private static final int COLUMN_NAME = 1;
    private static final int COLUMN_VALUE = 2;
    private static final int COLUMNS = 3;
    private final boolean isEditor;
    private final boolean editable;
    private DeviceId deviceId;
    private final ValueEventSource listeners;
    private boolean isChanging;
    private int selectedId;
    private boolean enabled;

    public DeviceIdPanel (String title, DeviceId deviceId, boolean isEditor,
        boolean editable, final StatusBar statusBar,
        MirrorField mirror, int visibleRows)
    {
        this.deviceId = deviceId;
        this.isEditor = isEditor;
        this.editable = editable;
        this.statusBar = statusBar;
        this.mirror = mirror;
        listeners = new ValueEventSource ();

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

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

        int totalWidth = setupColumns ();

        table.setPreferredScrollableViewportSize (
            new Dimension (totalWidth, visibleRows * table.getRowHeight ()));
        setLayout (new BorderLayout ());
        if (title != null)
        {
            label = new JLabel (title + ":");
            add (label, BorderLayout.NORTH);
            label.setLabelFor (table);
        }
        else
        {
            label = null;
        }
        JScrollPane scrollPane = new JScrollPane (table,
            JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED,
            JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        add (scrollPane, BorderLayout.CENTER);

        table.getSelectionModel ().addListSelectionListener (
            new ListSelectionListener ()
            {
                public void valueChanged (ListSelectionEvent e)
                {
                    if (e.getValueIsAdjusting ())
                        return;
                    int row = table.getSelectedRow ();
                    ArrayList<Integer> ids =
                        DeviceIdPanel.this.deviceId.getIds ();
                    int id = row < 0 ? -1 : ids.get (row);
                    if (id != selectedId)
                    {
                        selectedId = id;
                        listeners.fireValueChanged (new ValueEvent (this));
                    }
                }
            });
    }

    public boolean haveValuesChanged (DeviceId id)
    {
        boolean changed = false;
        TableCellEditor cellEditor = table.getCellEditor ();
        if (cellEditor instanceof WCellEditor &&
            ((WCellEditor) cellEditor).hasValueChanged ())
        {
            changed = true;
        }

        if (model.haveValuesChanged (id))
            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 int setupColumns ()
    {
        TableColumn column = table.getColumnModel ().getColumn (COLUMN_ID);
        WTableCellRenderer renderer = new WTableCellRenderer ();
        renderer.setHorizontalAlignment (SwingConstants.CENTER);
        column.setCellRenderer (renderer);
        int width = model.getColumnWidth (COLUMN_ID);
        column.setMaxWidth (width);
        int totalWidth = width;

        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.LEFT);

        for (int col = 1 ; col < COLUMNS ; col++)
        {
            width = model.getColumnWidth (col);
            totalWidth += width;
            column = table.getColumnModel ().getColumn (col);
            column.setPreferredWidth (width);
            column.setCellRenderer (renderer);
            if (editable)
            {
                WCellEditor cellEditor = new WCellEditor (listeners);
                cellEditor.setVerifier (this);
                cellEditor.setMirror (mirror);
                column.setCellEditor (cellEditor);
            }
        }

        return totalWidth;
    }

    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 ();
    }

    DeviceId getDeviceId ()
    {
        return deviceId;
    }

    void setDeviceId (final DeviceId deviceId)
    {
        Event.checkIsEventDispatchThread ();
        cancelEditing ();
        DeviceIdPanel.this.deviceId = deviceId;
        selectedId = -1;
        model.fireTableDataChanged ();
        setSelectedId (0);
    }

    void addId (int id)
    {
        assert id >= 0;
        deviceId.put (id, "");
        model.fireTableDataChanged ();
    }

    void removeId (int id)
    {
        assert id >= 0;
        deviceId.put (id, null);
        model.fireTableDataChanged ();
    }

    public int getSelectedId ()
    {
        return selectedId;
    }

    public void setSelectedId (int id)
    {
        if (id == selectedId)
            return;
        ArrayList<Integer> ids = deviceId.getIds ();
        int row = ids.indexOf (id);
        if (row < 0)
        {
            selectedId = -1;
            row = table.getSelectedRow ();
            table.getSelectionModel ().removeSelectionInterval (row, row);
        }
        else
        {
            selectedId = id;
            table.getSelectionModel ().setSelectionInterval (row, row);
            table.scrollRectToVisible (table.getCellRect (row, 0, true));
        }
    }

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

    @Override
    public void setEnabled (boolean enabled)
    {
        this.enabled = enabled;
        super.setEnabled (enabled);
        if (label != null)
            label.setEnabled (enabled);
    }

    public static String idToString (int id)
    {
        if (id < 0)
            return "";
        return String.format ("%02x", id);
    }

    class Model
        extends AbstractTableModel
        implements HasCellLabels
    {
        private DeviceId savedId = null;

        public int getColumnCount ()
        {
            return COLUMNS;
        }

        public int getRowCount ()
        {
            return deviceId.getIds ().size ();
        }

        public String getValueAt (int row, int col)
        {
            ArrayList<Integer> ids = deviceId.getIds ();
            int id = ids.get (row);
            switch (col)
            {
            case COLUMN_ID:
                return idToString (id);
            case COLUMN_NAME:
                return deviceId.getName (id);
            case COLUMN_VALUE:
                return deviceId.get (id);
            default:
                throw new AssertionError ("Unreachable");
            }
        }

        @Override
        public void setValueAt (Object value, int row, int col)
        {
            try
            {
                fromString ((String) value, row, col);
            }
            catch (ValueException e)
            {
                throw new IllegalStateException (e);
            }
        }

        void fromString (String value, int row, int col)
            throws ValueException
        {
            ArrayList<Integer> ids = deviceId.getIds ();
            if (row >= ids.size ())
                return;
            int id = ids.get (row);
            switch (col)
            {
            case COLUMN_NAME:
                {
                    String oldValue = deviceId.getName (id);
                    if (!value.equals (oldValue))
                    {
                        deviceId.setName (id, value);
                        listeners.fireValueChanged (new ValueEvent (this));
                    }
                }
                break;
            case COLUMN_VALUE:
                {
                    String oldValue = deviceId.get (id);
                    if (value.equals (oldValue))
                        return;
                    try
                    {
                        byte [] data = value.getBytes ("UTF-8");
                        if (data.length > 255)
                        {
                            throw new ValueException (
                                "Value too long: must be < 256 characters");
                        }
                    }
                    catch (UnsupportedEncodingException e)
                    {
                        // Shouldn't happen - UTF-8 should be supported.
                        throw new AssertionError ("Unreachable");
                    }
                    deviceId.put (id, value);
                    listeners.fireValueChanged (new ValueEvent (this));
                }
                break;
            }
        }

        @Override
        public String getColumnName (int col)
        {
            switch (col)
            {
            case COLUMN_ID:
                return "ID";
            case COLUMN_NAME:
                return "Name";
            case COLUMN_VALUE:
                return "Value";
            default:
                return "";
            }
        }

        @Override
        public String getCellLabel (int row, int col)
        {
            ArrayList<Integer> ids = deviceId.getIds ();
            if (row >= ids.size ())
                return "";
            return idToString (ids.get (row)) + " " + getColumnName (col);
        }

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

        @Override
        public boolean isCellEditable (int row, int col)
        {
            return enabled && editable && col != 0;
        }

        private int getColumnWidth (int col)
        {
            return Gui.getTextWidth (col == COLUMN_ID ? 4 : 20);
        }

        private boolean isUnapplied (int row, int col)
        {
            if (!isEditor || savedId == null)
                return false;
            ArrayList<Integer> ids = deviceId.getIds ();
            if (row >= ids.size ())
                return false;
            int id = ids.get (row);
            String obj = savedId.get (id);
            if (obj == null)
            {
                // New object.
                return true;
            }
            switch (col)
            {
            case COLUMN_NAME:
                return !deviceId.getName (id).equals (savedId.getName (id));
            case COLUMN_VALUE:
                return !deviceId.get (id).equals (obj);
            }
            return false;
        }

        private boolean haveValuesChanged (DeviceId id)
        {
            savedId = id;
            return !id.equals (deviceId);
        }
    }

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

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

