
package uk.co.wingpath.modbusgui;

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

public class DeviceIdSettingsPanel
    implements TreeCard
{
    private final Frontend frontend;
    private final DeviceIdSettings deviceIdSettings;
    private WEditableComboBox objectIdField;
    private JButton applyButton;
    private JButton resetButton;
    private JButton addButton;
    private JButton deleteButton;
    private JPanel outerPanel;
    private final DeviceIdPanel deviceIdPanel;
    private final WComponent<Integer> countField;
    private final ByteTablePanel dataPanel;
    private final WTextField dataField;
    private int selectedId;
    private final StatusBar statusBar;
    private final MirrorField mirror;
    private final ValueEventSource statusListeners;

    public DeviceIdSettingsPanel (Frontend frontend, Settings settings)
    {
        this.frontend = frontend;
        statusListeners = new ValueEventSource ();
        deviceIdSettings = settings.getDeviceId ();

        selectedId = -1;

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

        mirror = new MirrorField ();

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

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

        GridBagPanel panel = new GridBagPanel ();

        mainPanel.add (panel);
        mainPanel.add (Box.createVerticalGlue ());
        mainPanel.add (buttonPanel);
        mainPanel.add (mirror.getComponent ());
        mainPanel.add (statusBar);

        objectIdField = new WEditableComboBox (statusBar, "Object ID");
        objectIdField.setToolTipText ("Hex object ID");
        objectIdField.setMnemonic (KeyEvent.VK_O);
        GridBagConstraints constraints = panel.createConstraints ();
        panel.add (objectIdField.getLabel (), constraints);
        constraints.gridx = 1;
        constraints.gridwidth = 2;
        panel.add (objectIdField.getComponent (), constraints);
        objectIdField.setVerifier (
            new Verifier ()
            {
                public String verify (String value, boolean isChanging)
                {
                    if (isChanging)
                    {
                        statusBar.clear ("DI1");
                        return value;
                    }
                    try
                    {
                        int id = Integer.parseInt (value, 16);
                        if (id >= 0 && id <= 255)
                        {
                            statusBar.clear ("DI2");
                            return deviceIdPanel.idToString (id);
                        }
                    }
                    catch (NumberFormatException e)
                    {
                    }
                    statusBar.showError (
                        "ID must be hex integer in range 00..ff");
                    return null;
                }
            });

        ButtonPanel editButtonPanel = ButtonPanel.createVertical ();
        constraints.gridx = 3;
        constraints.gridwidth = 1;
        panel.add (editButtonPanel, constraints);

        deviceIdPanel =
            new DeviceIdPanel (null, deviceIdSettings.getDeviceId ().clone (),
            true, true, statusBar, mirror, 10);
        deviceIdPanel.setToolTipText ("Data to send - as hex bytes");
        constraints = panel.createConstraints ();
        constraints.gridwidth = 4;
        constraints.fill = GridBagConstraints.HORIZONTAL;
        constraints.weightx = 1.0;
        panel.add (deviceIdPanel, constraints);

        JLabel cmd17Label = new JLabel ("Data for Report Slave ID:");
        cmd17Label.setToolTipText (
            "Data to send in response to Command 17 - Report Slave ID command");
        constraints = panel.createConstraints ();
        constraints.gridwidth = 4;
        constraints.fill = GridBagConstraints.HORIZONTAL;
        constraints.weightx = 1.0;
        panel.add (cmd17Label, constraints);

        countField = new WIntegerField (statusBar, "Count", 0, 255, 0);
        countField.setToolTipText ("Number of data bytes to send");
        countField.setWidthChars (6);
        countField.setMnemonic (KeyEvent.VK_C);
        countField.setMirror (mirror);
        constraints = panel.createConstraints ();
        panel.add (countField.getLabel (), constraints);
        constraints.gridx = 1;
        panel.add (countField.getComponent (), constraints);

        dataPanel = new ByteTablePanel (null, 0, true, true, false,
            statusBar, 5);
        constraints.gridx = 2;
        constraints.gridwidth = 2;
        constraints.fill = GridBagConstraints.HORIZONTAL;
        constraints.weightx = 1.0;
        panel.add (dataPanel, constraints);

        dataField = new WTextField (statusBar, "Text");
        dataField.setWidthChars (30);
        dataField.setToolTipText ("Data to send - as text");
        dataField.setMnemonic (KeyEvent.VK_T);
        dataField.setMirror (mirror);
        constraints = panel.createConstraints ();
        panel.add (dataField.getLabel (), constraints);
        constraints.gridx = 1;
        constraints.gridwidth = 4;
        constraints.fill = GridBagConstraints.HORIZONTAL;
        constraints.weightx = 1.0;
        panel.add (dataField.getComponent (), constraints);

        dataField.setVerifier (
            new Verifier ()
            {
                public String verify (String str, boolean isChanging)
                {
                    try
                    {
                        byte [] data = str.getBytes ("UTF-8");
                        if (data.length > 255)
                        {
                            statusBar.showError (
                                "Value too long: must be < 256 characters");
                            return null;
                        }
                        dataPanel.setData (Numeric.fromByteArray (data));
                        countField.setValue (data.length);
                        return isChanging ? str : new String (data, "UTF-8");
                    }
                    catch (UnsupportedEncodingException e)
                    {
                        // Shouldn't happen - UTF-8 should be supported.
                        throw new AssertionError ("Unreachable");
                    }
                }
            });

        applyButton = buttonPanel.addButton ("Apply", null,
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    if (checkValues ())
                        putValuesToModel ();
                }
            });

        resetButton = buttonPanel.addButton ("Reset", null,
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    reset ();
                }
            });
        resetButton.setMnemonic (KeyEvent.VK_R);

        buttonPanel.addButton (getHelpAction ());

        addButton = editButtonPanel.addButton ("Add", null,
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    if (!objectIdField.checkValue ())
                        return;
                    int id = deviceIdPanel.getSelectedId ();
                    if (id < 0)
                    {
                        deviceIdPanel.addId (selectedId);
                        deviceIdPanel.setSelectedId (selectedId);
                        setComboItems ();
                        enableApplyResetButtons (false);
                        enableAddRemoveButtons (false);
                    }
                }
            });
        addButton.setMnemonic (KeyEvent.VK_A);
        addButton.setToolTipText ("Add object definition");

        deleteButton = editButtonPanel.addButton ("Delete", null,
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    if (!objectIdField.checkValue ())
                        return;
                    int id = deviceIdPanel.getSelectedId ();
                    if (id >= 0)
                    {
                        deviceIdPanel.removeId (id);
                        setComboItems ();
                        enableApplyResetButtons (false);
                        enableAddRemoveButtons (false);
                    }
                }
            });
        deleteButton.setMnemonic (KeyEvent.VK_D);
        deleteButton.setToolTipText ("Delete object definition");

        frontend.addBackendStateListener (new BackendState.Listener ()
            {
                public void stateChanged (BackendState state)
                {
                    boolean en = state.isSlave && !state.isMaster &&
                        state.isStopped;
                    objectIdField.setEnabled (en);
                    deviceIdPanel.setEnabled (en);
                    countField.setEnabled (en);
                    dataPanel.setEnabled (en);
                    dataField.setEnabled (en);
                    enableApplyResetButtons (false);
                    enableAddRemoveButtons (false);
                }
            });

        objectIdField.addValueListener (new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    if (!e.isChanging ())
                    {
                        String value = objectIdField.getValue ();
                        int id = value.equals ("") ? -1 :
                            Integer.parseInt (value.trim (), 16);
                        if (id != selectedId)
                        {
                            selectedId = id;
                            deviceIdPanel.setSelectedId (id);
                        }
                    }
                    else
                    {
                        statusBar.clear ("DI3");
                    }
                    enableApplyResetButtons (false);
                    enableAddRemoveButtons (e.isChanging ());
                }
            });

        deviceIdPanel.addValueListener (new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    if (e.isChanging ())
                        statusBar.clear ("DI4");
                    enableApplyResetButtons (e.isChanging ());
                    enableAddRemoveButtons (false);
                    int id = deviceIdPanel.getSelectedId ();
                    if (id >= 0 && id != selectedId)
                    {
                        selectedId = id;
                        objectIdField.setValue (deviceIdPanel.idToString (id));
                    }
                }
            });

        countField.addValueListener (new ValueListener ()
        {
            public void valueChanged (ValueEvent e)
            {
                if (!e.isChanging ())
                {
                    dataPanel.setSize (countField.getValue ());
                    updateDataField (dataPanel.getData ());
                    enableApplyResetButtons (false);
                }
            }
        });

        dataPanel.addValueListener (new ValueListener ()
        {
            public void valueChanged (ValueEvent e)
            {
                Numeric.Value [] d = dataPanel.getData ();
                countField.setValue (d.length);
                updateDataField (d);
                enableApplyResetButtons (e.isChanging ());
            }
        });

        dataField.addValueListener (new ValueListener ()
        {
            public void valueChanged (ValueEvent e)
            {
                try
                {
                    Numeric.Value [] data = Numeric.fromByteArray (
                        dataField.getValue ().getBytes ("UTF-8"));
                    dataPanel.setData (data);
                    countField.setValue (data.length);
                    enableApplyResetButtons (e.isChanging ());
                }
                catch (UnsupportedEncodingException uee)
                {
                    // Shouldn't happen - UTF-8 should be supported.
                    throw new AssertionError ("Unreachable");
                }
            }
        });

        deviceIdSettings.addValueListener (
            new ValueListener ()
            {
                public void valueChanged (ValueEvent e)
                {
                    getValuesFromModel ();
                }
            });

        getValuesFromModel ();
    }

    private void setComboItems ()
    {
        List<String> ids = new ArrayList<String> ();

        for (int id : deviceIdPanel.getDeviceId ().getIds ())
            ids.add (deviceIdPanel.idToString (id));

        objectIdField.setItems (ids);
        objectIdField.setValue (deviceIdPanel.idToString (selectedId));
    }

    private void enableAddRemoveButtons (boolean isChanging)
    {
        boolean enabled = frontend.isSlave () &&
            !frontend.isMaster () && frontend.isStopped ();
        int id = deviceIdPanel.getSelectedId ();
        addButton.setEnabled (enabled && (isChanging || id < 0));
        deleteButton.setEnabled (enabled && (isChanging || id >= 0));
    }

    private void enableApplyResetButtons (boolean isChanging)
    {
        boolean enabled = frontend.isSlave () &&
            !frontend.isMaster () && frontend.isStopped () &&
            (isChanging || hasUnappliedChanges ());
        Gui.setEnabled (applyButton, enabled);
        Gui.setEnabled (resetButton, enabled);
        fireStatusChanged ();
    }

    public JPanel getPanel ()
    {
        return outerPanel;
    }

    public String getTag ()
    {
        return "deviceid";
    }

    public String getName ()
    {
        return "Device Identification";
    }

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

    public JButton getDefaultButton ()
    {
        return applyButton;
    }

    public String getToolTipText ()
    {
        return null;
    }

    @Override
    public void selected ()
    {
    }

    @Override
    public void reset ()
    {
        getValuesFromModel ();
    }

    public boolean hasUnappliedChanges ()
    {
        boolean changed = false;
        DeviceId deviceId = deviceIdSettings.getDeviceId ();
        if (deviceIdPanel.haveValuesChanged (deviceId))
            changed = true;

        Numeric.Value [] data = deviceIdSettings.getCmd17Data ();
        if (countField.hasValueChanged (data.length))
            changed = true;
        if (dataPanel.haveValuesChanged (data))
            changed = true;
        try
        {
            if (dataField.hasValueChanged (
                new String (Numeric.toByteArray (data), "UTF-8")))
            {
                changed = true;
            }
        }
        catch (UnsupportedEncodingException e)
        {
            // Shouldn't happen - UTF-8 should be supported.
            throw new AssertionError ("Unreachable");
        }

        return changed;
    }

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

    public boolean checkValues ()
    {
        if (!countField.checkValue ())
            return false;
        if (!dataField.checkValue ())
            return false;
        if (!dataPanel.stopEditing ())
            return false;
        return deviceIdPanel.checkValues ();
    }

    private void updateDataField (Numeric.Value [] data)
    {
        try
        {
            dataField.setValue (
                new String (Numeric.toByteArray (data), "UTF-8"));
        }
        catch (UnsupportedEncodingException e)
        {
            // Shouldn't happen - UTF-8 should be supported.
            throw new AssertionError ("Unreachable");
        }
    }

    public void getValuesFromModel ()
    {
        Event.checkIsEventDispatchThread ();
        statusBar.clear ("DI5");
        deviceIdPanel.setDeviceId (deviceIdSettings.getDeviceId ().clone ());
        Numeric.Value [] data = deviceIdSettings.getCmd17Data ();
        countField.setValue (data.length);
        dataPanel.setData (data);
        updateDataField (data);
        selectedId = 0;
        setComboItems ();
        deviceIdPanel.setSelectedId (selectedId);
        enableApplyResetButtons (false);
        enableAddRemoveButtons (false);
    }

    public void putValuesToModel ()
    {
        deviceIdSettings.setDeviceId (deviceIdPanel.getDeviceId ().clone ());
        deviceIdSettings.setCmd17Data (dataPanel.getData ());
        enableApplyResetButtons (false);
    }

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

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

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

