
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 DeviceIdTestPanel
    extends JPanel
    implements TableVerifier
{
    private enum Error { NONE, MISSING, EXTRA, WRONG }

    private static class Row
    {
        final int id;
        final String name;
        final String expectedValue;
        final String actualValue;
        final Error error;
        boolean nameUnapplied;
        boolean valueUnapplied;

        Row (int id, String name, String expectedValue, String actualValue,
            Error error)
        {
            this.id = id;
            this.name = name;
            this.expectedValue = expectedValue;
            this.actualValue = actualValue;
            this.error = error;
            nameUnapplied = false;
            valueUnapplied = false;
        }
    }

    private final JTable table;
    private final StatusBar statusBar;
    private final MirrorField mirror;
    private final JLabel label;
    private final boolean isEditor;
    private final Model model;
    private static final int COLUMN_ID = 0;
    private static final int COLUMN_NAME = 1;
    private static final int COLUMN_EXPECTED = 2;
    private static final int COLUMN_ACTUAL = 3;
    private static final int COLUMN_ERROR = 4;
    private static final int COLUMNS = 5;
    private DeviceId expectedDeviceId;
    private DeviceId actualDeviceId;
    private ArrayList<Row> rows;
    private final ValueEventSource listeners;
    private int selectedId;
    private boolean highlight;
    private boolean enabled;

    public DeviceIdTestPanel (String title, DeviceId expectedDeviceId,
        DeviceId actualDeviceId, final StatusBar statusBar,
        final MirrorField mirror, boolean isEditor, int visibleRows)
    {
        this.expectedDeviceId = expectedDeviceId.clone ();
        this.actualDeviceId = actualDeviceId;
        this.statusBar = statusBar;
        this.mirror = mirror;
        this.isEditor = isEditor;
        selectedId = -1;
        highlight = true;
        listeners = new ValueEventSource ();
        buildComparison ();

        model = new Model ();
        table = new WTable (model);
        table.setSelectionMode (ListSelectionModel.SINGLE_SELECTION);
        enabled = true;
        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 ();
                    if (row < 0)
                    {
                        // The table seems to lose its selection for no
                        // apparent reason. Attempt to restore it, rather than
                        // propagate the loss.
                        selectRow (getRowForId (selectedId));
                    }
                    else
                    {
                        Row r = rows.get (row);
                        int id = r.id;
                        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;
        }

        for (int row = 0 ; row < rows.size () ; ++row)
        {
            Row r = rows.get (row);
            if (r.error == Error.EXTRA)
                continue;
            String name = expectedDeviceId.getName (r.id);
            String oldName = id.getName (r.id);
            r.nameUnapplied = !name.equals (oldName);
            if (r.nameUnapplied)
                changed = true;
            String value = expectedDeviceId.get (r.id);
            String oldValue = id.get (r.id);
            r.valueUnapplied = !value.equals (oldValue);
            if (r.valueUnapplied)
                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 ()
    {
        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);
                    assert (JLabel) component == this;
                    int col = table.convertColumnIndexToModel (column);
                    switch (col)
                    {
                    case COLUMN_ID:
                    case COLUMN_ERROR:
                        setHorizontalAlignment (SwingConstants.CENTER);
                        break;
                    default:
                        setHorizontalAlignment (SwingConstants.LEFT);
                        break;
                    }
                    Row r = rows.get (row);
                    boolean isUnapplied =
                        col == COLUMN_NAME && r.nameUnapplied ||
                        col == COLUMN_EXPECTED && r.valueUnapplied;
                    boolean isError = false;
                    switch (r.error)
                    {
                    case NONE:
                        break;
                    case MISSING:
                        isError = col == COLUMN_ERROR;
                        break;
                    case EXTRA:
                        isError = true;
                        break;
                    case WRONG:
                        isError = col == COLUMN_ACTUAL || col == COLUMN_ERROR;
                        break;
                    }
                    component.setBackground (
                        isError ? Gui.COLOUR_BACKGROUND_ERROR :
                        isUnapplied ? Gui.COLOUR_BACKGROUND_UNAPPLIED :
                        isSelected ? Gui.COLOUR_BACKGROUND_SELECTED :
                            Gui.COLOUR_BACKGROUND_NORMAL);
                    return component;
                }
            };

        table.setDefaultRenderer (String.class, renderer);
        int totalWidth = 0;

        for (int col = 0 ; col < COLUMNS ; col++)
        {
            TableColumn column = table.getColumnModel ().getColumn (col);
            // column.setCellRenderer (renderer);
            int width = 20;
            switch (col)
            {
            case COLUMN_ID:
                width = Gui.getTextWidth (4);
                column.setMaxWidth (width);
                break;
            case COLUMN_ERROR:
                width = Gui.getTextWidth (6);
                column.setMaxWidth (width);
                break;
            case COLUMN_NAME:
            case COLUMN_EXPECTED:
                width = Gui.getTextWidth (15);
                WCellEditor cellEditor = new WCellEditor (listeners);
                cellEditor.setVerifier (this);
                cellEditor.setMirror (mirror);
                column.setCellEditor (cellEditor);
                break;
            case COLUMN_ACTUAL:
                width = Gui.getTextWidth (15);
                break;
            }
            column.setPreferredWidth (width);
            totalWidth += width;
        }

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

    private void buildComparison ()
    {
        rows = new ArrayList<Row> ();

        ArrayList<Integer> expectedIds = expectedDeviceId.getIds ();
        ArrayList<Integer> actualIds = actualDeviceId.getIds ();

        int e = 0;
        int a = 0;
        final int END_ID = 1000;    // max ID is 255

        while (e < expectedIds.size () || a < actualIds.size ())
        {
            int eid = e < expectedIds.size () ? expectedIds.get (e) : END_ID;
            int aid = a < actualIds.size () ? actualIds.get (a) : END_ID;
            assert eid != END_ID || aid != END_ID : eid + " " + aid;
            if (eid == aid)
            {
                String expected = expectedDeviceId.get (eid);
                String actual = actualDeviceId.get (eid);
                rows.add (new Row (
                    eid,
                    expectedDeviceId.getName (eid),
                    expected,
                    actual,
                    (expected.equals ("") || expected.equals (actual)) ?
                        Error.NONE : Error.WRONG));
                e++;
                a++;
            }
            else if (eid < aid)
            {
                assert eid != END_ID;
                rows.add (new Row (
                    eid,
                    expectedDeviceId.getName (eid),
                    expectedDeviceId.get (eid),
                    "",
                    highlight ? Error.MISSING : Error.NONE));
                e++;
            }
            else
            {
                assert aid != END_ID;
                rows.add (new Row (
                    aid,
                    actualDeviceId.getName (aid),
                    "",
                    actualDeviceId.get (aid),
                    Error.EXTRA));
                a++;
            }
        }
    }

    public ArrayList<Integer> getIds ()
    {
        ArrayList<Integer> ids = new ArrayList<Integer> ();

        for (int row = 0 ; row < rows.size () ; ++row)
        {
            Row r = rows.get (row);
            ids.add (r.id);
        }

        return ids;
    }

    void setActualDeviceId (final DeviceId actualDeviceId)
    {
        this.actualDeviceId = actualDeviceId;
        buildComparison ();
        model.fireTableDataChanged ();
    }

    void setExpectedDeviceId (final DeviceId expectedDeviceId)
    {
        cancelEditing ();
        this.expectedDeviceId = expectedDeviceId.clone ();
        buildComparison ();
        selectedId = -1;
        model.fireTableDataChanged ();
        setSelectedId (0);
        listeners.fireValueChanged (new ValueEvent (this));
    }

    DeviceId getActualDeviceId ()
    {
        return actualDeviceId;
    }

    DeviceId getExpectedDeviceId ()
    {
        return expectedDeviceId.clone ();
    }

    boolean isExpected (int id)
    {
        return id >= 0 && expectedDeviceId.get (id) != null;
    }

    void addId (int id)
    {
        if (!isExpected (id))
        {
            expectedDeviceId.put (id, "");
            buildComparison ();
            model.fireTableDataChanged ();
            listeners.fireValueChanged (new ValueEvent (this));
        }
    }

    void removeId (int id)
    {
        if (isExpected (id))
        {
            expectedDeviceId.put (id, null);
            buildComparison ();
            model.fireTableDataChanged ();
            listeners.fireValueChanged (new ValueEvent (this));
        }
    }

    public int getSelectedId ()
    {
        return selectedId;
    }

    private int getRowForId (int id)
    {
        for (int row = 0 ; row < rows.size () ; ++row)
        {
            Row r = rows.get (row);
            if (r.id == id)
                return row;
        }

        return -1;
    }

    private void selectRow (int row)
    {
        int oldRow = table.getSelectedRow ();
        if (row == oldRow)
            return;
        if (row >= 0)
        {
            table.getSelectionModel ().setSelectionInterval (row, row);
            table.scrollRectToVisible (table.getCellRect (row, 0, true));
        }
        else
        {
            table.getSelectionModel ().clearSelection ();
        }
    }

    public void setSelectedId (int id)
    {
        if (id == selectedId)
            return;
        int row = getRowForId (id);
        selectRow (row);
        if (row >= 0)
            selectedId = id;
    }

    public void setEnabled (boolean enabled, boolean enExpected)
    {
        this.enabled = enabled && enExpected;
        super.setEnabled (enabled);
        if (label != null)
            label.setEnabled (enabled);
    }

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

    public void highlightErrors (boolean highlight)
    {
        this.highlight = highlight;
        buildComparison ();
        model.fireTableRowsUpdated (0, model.getRowCount () - 1);
    }

    class Model
        extends AbstractTableModel
        implements HasCellLabels
    {
        public int getColumnCount ()
        {
            return COLUMNS;
        }

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

        public String getValueAt (int row, int col)
        {
            Row r = rows.get (row);
            switch (col)
            {
            case COLUMN_ID:
                return idToString (r.id);
            case COLUMN_NAME:
                return r.name;
            case COLUMN_EXPECTED:
                return r.expectedValue;
            case COLUMN_ACTUAL:
                return r.actualValue;
            case COLUMN_ERROR:
                switch (r.error)
                {
                case NONE:
                    return "";
                case EXTRA:
                    return "Extra";
                case MISSING:
                    return "Missing";
                case WRONG:
                    return "Wrong";
                }
                break;
            }
            return "";
        }

        @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
        {
            Row r = rows.get (row);
            switch (col)
            {
            case COLUMN_NAME:
                {
                    String oldValue = expectedDeviceId.getName (r.id);
                    if (!value.equals (oldValue))
                    {
                        expectedDeviceId.setName (r.id, value);
                        buildComparison ();
                        model.fireTableRowsUpdated (row, row);
                        listeners.fireValueChanged (new ValueEvent (this));
                    }
                }
                break;
            case COLUMN_EXPECTED:
                {
                    String oldValue = expectedDeviceId.get (r.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");
                    }
                    expectedDeviceId.put (r.id, value);
                    buildComparison ();
                    model.fireTableRowsUpdated (row, row);
                    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_EXPECTED:
                return "Expected Value";
            case COLUMN_ACTUAL:
                return "Actual Value";
            case COLUMN_ERROR:
                return "Error";
            default:
                return "";
            }
        }

        @Override
        public String getCellLabel (int row, int col)
        {
            if (row >= rows.size ())
                return "";
            Row r = rows.get (row);
            return idToString (r.id) + " " + getColumnName (col);
        }

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

        @Override
        public boolean isCellEditable (int row, int col)
        {
            if (!enabled)
                return false;
            Row r = rows.get (row);
            return r.error != Error.EXTRA &&
                (col == COLUMN_NAME || col == COLUMN_EXPECTED);
        }
    }

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

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

