
package uk.co.wingpath.modbusgui;

import java.io.*;
import java.util.*;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.datatransfer.*;
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 ByteTablePanel
    extends JPanel
    implements TableVerifier
{
    private final JTable table;
    private final Model model;
    private final StatusBar statusBar;
    private final JLabel titleLabel;
    private static final int COLUMNS = 10;
    private final boolean editable;
    private final boolean mayBeEmpty;
    private Numeric.Value [] data;
    private Numeric.Value [] compareData;
    private boolean [] unapplied;
    private final ValueEventSource listeners;
    private final boolean isEditor;
    private boolean highlight;

    public ByteTablePanel (String title, int size, boolean isEditor,
        final boolean editable, final boolean mayBeEmpty,
        final StatusBar statusBar, int visibleRows)
    {
        Event.checkIsEventDispatchThread ();
        this.isEditor = isEditor;
        this.editable = editable;
        this.mayBeEmpty = mayBeEmpty;
        this.statusBar = statusBar;
        listeners = new ValueEventSource ();

        TransferHandler transferHandler =
            new TransferHandler ("DataString")
            {
                @Override
                public boolean canImport (JComponent c,
                    DataFlavor [] transferFlavors)
                {
                    if (!editable)
                        return false;

                    for (DataFlavor df : transferFlavors)
                    {
                        if (df.equals (DataFlavor.stringFlavor))
                            return true;
                    }

                    return false;
                }

                @Override
                public int getSourceActions (JComponent c)
                {
                    return COPY;
                }

                @Override
                protected Transferable createTransferable (JComponent c)
                {
                    return new StringSelection (
                        Bytes.toHexString (Numeric.toByteArray (data)));
                }

                @Override
                public boolean importData (JComponent c, Transferable t)
                {
                    if (!editable)
                        return false;
                    DataFlavor [] flavors = t.getTransferDataFlavors ();
                    try
                    {
                        String str = (String)
                            t.getTransferData (DataFlavor.stringFlavor);
                        try
                        {
                            Numeric.Value [] d = Numeric.fromByteArray (
                                Bytes.fromHexString (str));
                            setData (d);
                            listeners.fireValueChanged (ByteTablePanel.this);
                        }
                        catch (ValueException e)
                        {
                            statusBar.showError (
                                "Must be hex numbers in the range 00..ff");
                            return false;
                        }
                    }
                    catch (UnsupportedFlavorException e)
                    {
                        return false;
                    }
                    catch (IOException e)
                    {
                        return false;
                    }
                    statusBar.clear ();
                    return true;
                }
            };

        setTransferHandler (transferHandler);

        data = mayBeEmpty ? Numeric.Type.uint8.createUndefArray (size) :
            Numeric.Type.uint8.createZeroArray (size);
        compareData = null;
        unapplied = new boolean [size];
        model = new Model ();
        table = new WTable (model);
        table.setSelectionMode (ListSelectionModel.SINGLE_SELECTION);
        table.setRowSelectionAllowed (false);

        // Have to replace the table's transfer handler, otherwise it
        // swallows paste actions. Replacing the transfer handler doesn't
        // seem to affect pasting into table cells - that still works.
        // User has to select a non-editable cell in order to paste whole
        // array.
        table.setTransferHandler (transferHandler);

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

        int width = setupColumns ();
        width += new JScrollBar ().getWidth ();

        setLayout (new BorderLayout ());
        if (title != null)
        {
            titleLabel = new JLabel (title + ":");
            add (titleLabel, BorderLayout.NORTH);
        }
        else
        {
            titleLabel = null;
        }
        JScrollPane scrollPane = new JScrollPane (table,
                JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
            JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.setPreferredSize (
            new Dimension (width, visibleRows * table.getRowHeight () + 3));

        add (scrollPane, BorderLayout.CENTER);
    }

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

        for (int i = 0 ; i < data.length ; ++i)
        {
            boolean ch = i >= values.length || !values [i].equals (data [i]);
            if (ch)
                changed = true;
            unapplied [i] = ch;
        }

        model.fireTableRowsUpdated (0, model.getRowCount () - 1);
        return changed;
    }

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

    private int setupColumns ()
    {
        int columns = model.getColumnCount ();

        TableCellRenderer renderer = new DefaultTableCellRenderer ()
            {
                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 (
                        highlight && model.isError (row, col) ?
                            Gui.COLOUR_BACKGROUND_ERROR :
                        (col == 0 || row * COLUMNS + col - 1 >= data.length) ?
                            Gui.COLOUR_BACKGROUND_INACTIVE :
                        model.isUnapplied (row, col) ?
                            Gui.COLOUR_BACKGROUND_UNAPPLIED :
                        isSelected ? Gui.COLOUR_BACKGROUND_SELECTED :
                        Gui.COLOUR_BACKGROUND_NORMAL);
                    ((JLabel) component).setHorizontalAlignment (
                        col == 0 ? SwingConstants.RIGHT :
                            SwingConstants.CENTER);
                    return component;
                }
            };

        int totalWidth = 0;

        for (int col = 0 ; col < columns ; col++)
        {
            TableColumn column = table.getColumnModel ().getColumn (col);
            column.setCellRenderer (renderer);

            if (col == 0)
            {
                int width = Gui.getTextWidth (6);
                column.setMaxWidth (width);
                column.setPreferredWidth (width);
                totalWidth += width;
            }
            else if (col != 0)
            {
                int width = Gui.getTextWidth (2);
                column.setMaxWidth (width);
                column.setPreferredWidth (width);
                totalWidth += width;
                if (editable)
                {
                    WCellEditor cellEditor = new WCellEditor (listeners);
                    cellEditor.setVerifier (this);
                    column.setCellEditor (cellEditor);
                }
            }
        }

        return totalWidth;
    }

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

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

    public Numeric.Value [] getData ()
    {
        return data.clone ();
    }

    public void reset ()
    {
        Event.checkIsEventDispatchThread ();
        data = mayBeEmpty ? Numeric.Type.uint8.createUndefArray (data.length) :
            Numeric.Type.uint8.createZeroArray (data.length);
        modelChanged ();
    }

    public void setData (Numeric.Value [] d)
    {
        Event.checkIsEventDispatchThread ();
        data = d.clone ();
        unapplied = new boolean [d.length];
        modelChanged ();
    }

    public void setCompareData (Numeric.Value [] d)
    {
        Event.checkIsEventDispatchThread ();
        compareData = d == null ? null : d.clone ();
        modelChanged ();
    }

    public void setSize (int size)
    {
        Event.checkIsEventDispatchThread ();
        if (size != data.length)
        {
            Numeric.Value d [] =
                mayBeEmpty ? Numeric.Type.uint8.createUndefArray (size) :
                    Numeric.Type.uint8.createZeroArray (size);
            boolean [] u = new boolean [d.length];

            for (int i = 0 ; i < size && i < data.length ; i++)
            {
                d [i] = data [i];
                u [i] = unapplied [i];
            }

            data = d;
            unapplied = u;
            modelChanged ();
        }
    }

    public void setToolTipText (String text)
    {
        table.setToolTipText (text);
    }

    public void setEnabled (boolean enabled)
    {
        table.setEnabled (enabled);
        if (titleLabel != null)
            titleLabel.setEnabled (enabled);
    }

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

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

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

    private class Model
        extends AbstractTableModel
    {
        public int getColumnCount ()
        {
            return COLUMNS + 1;
        }

        public int getRowCount ()
        {
            if (data.length == 0)
                return 1;
            return (data.length - 1) / COLUMNS + 1;
        }

        public String getValueAt (int row, int col)
        {
            if (col == 0)
            {
                if (row == 0)
                    return "";
                return Integer.toString (row * COLUMNS);
            }
            int index = row * COLUMNS + col - 1;
            if (index >= data.length)
                return "";
            if (!data [index].isDefined ())
                return "";
            return String.format ("%02x", data [index].getLongValue () & 0xff);
        }

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

        String fromString (String str, int row, int col)
            throws ValueException
        {
            if (col == 0)
                return str;
            int index = row * COLUMNS + col - 1;
            try
            {
                str = str.trim ();
                if (str.equals (""))
                {
                    if (!mayBeEmpty)
                        throw new ValueException ("Value missing");
                    data [index] = Numeric.Type.uint8.undef;
                    return "";
                }
                else
                {
                    int n = Integer.parseInt (str.trim (), 16);
                    if (n < 0 || n > 255)
                        throw new NumberFormatException ();
                    data [index] = Numeric.Type.uint8.createValue (n);
                    return String.format ("%02x", n);
                }
            }
            catch (NumberFormatException e)
            {
                throw new ValueException (
                    "Value must be hex number in range 00..ff");
            }
        }

        @Override
        public String getColumnName (int col)
        {
            if (col == 0)
                return "";
            return Integer.toString (col - 1);
        }

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

        @Override
        public boolean isCellEditable (int row, int col)
        {
            if (col == 0)
                return false;
            int index = row * COLUMNS + col - 1;
            return editable && index < data.length;
        }

        private boolean isError (int row, int col)
        {
            if (compareData == null)
                return false;
            if (col == 0)
                return false;
            int index = row * COLUMNS + col - 1;
            if (index >= data.length && index >= compareData.length)
                return false;
            if (index >= data.length || index >= compareData.length)
                return true;
            if (!data [index].matches (compareData [index]))
                return true;
            return false;
        }

        private boolean isUnapplied (int row, int col)
        {
            if (!isEditor)
                return false;
            if (col == 0)
                return false;
            int index = row * COLUMNS + col - 1;
            if (index >= unapplied.length)
                return false;
            return unapplied [index];
        }
    }
}

