
package uk.co.wingpath.modbusgui;

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

public class CommandEdit
    implements TreeCard
{
    private final Frontend frontend;
    private final boolean isTester;
    private final Backend backend;
    private final Settings settings;
    private final Reporter reporterNSB;
    private final CommandDefine commandDefine;
    private final StatusBar statusBar;
    private final MirrorField mirror;
    private final JPanel outerPanel;
    private final JPanel cardPanel;
    private final WTextField nameField;
    private final WTextArea descriptionField;
    private final JButton sendButton;
    private final JButton saveButton;
    private final JButton resetButton;
    private final JButton copyButton;
    private final JButton expectActualButton;
    private final List<CommandPanel> commandPanels;
    private final ExceptionSelector expectedExceptionSelector;
    private final ExceptionField actualExceptionField;
    private final CommandList commands;
    private final CommandList.Selector commandSelector;
    private final Map<String,CommandPanel> panelMap;
    private CommandPanel commandPanel;
    private Command command;
    private final ValueEventSource statusListeners;

    public CommandEdit (final Frontend frontend, final Backend backend,
        Settings settings, Reporter reporterNSB,
        final CommandDefine commandDefine)
    {
        Event.checkIsEventDispatchThread ();
        this.frontend = frontend;
        this.backend = backend;
        this.settings = settings;
        this.reporterNSB = reporterNSB;
        this.commandDefine = commandDefine;
        isTester = frontend.getProduct ().isTester ();
        statusListeners = new ValueEventSource ();

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

        mirror = new MirrorField ();

        commands = settings.getCommands ();

        commandPanels = new ArrayList<CommandPanel> ();
        commandPanels.add (
            new CommandCustomPanel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new CommandRawPanel (frontend, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command1Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command2Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command3Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command4Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command5Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command6Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command7Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command8Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command11Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command12Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command15Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command16Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command17Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command20Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command21Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command22Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command23Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command24Panel (frontend, settings, statusBar, mirror, isTester, true));
        commandPanels.add (
            new Command43Panel (frontend, settings, statusBar, mirror, isTester, true));

        commandPanel = commandPanels.get (0);
        commandPanel.setEnabled (false, false);

        panelMap = new HashMap<String,CommandPanel> ();

        for (CommandPanel cp : commandPanels)
            panelMap.put (cp.getTag (), cp);

        command = null;

        outerPanel = new JPanel ();
        outerPanel.setLayout (new BorderLayout ());
        JLabel heading = Gui.createDialogHeading ("View/Edit Command");
        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));

        GridBagPanel topPanel = new GridBagPanel ();
        mainPanel.add (topPanel);

        commandSelector = commands.createSelector ();
        topPanel.addComponent (commandSelector);
        commandSelector.setToolTipText (
            "Select Modbus command to be viewed/edited");
        commandSelector.setEnabled (false);

        commandSelector.addValueListener (new ValueListener ()
        {
            public void valueChanged (ValueEvent e)
            {
                selectCommand ();
            }
        });

        nameField = new WTextField (statusBar, "Name");
        topPanel.addComponent (nameField);
        nameField.setToolTipText ("Name for saved command");
        nameField.setMnemonic (KeyEvent.VK_N);
        nameField.setWidthChars (20);
        nameField.setMirror (mirror);
        nameField.setVerifier (new Verifier ()
        {
            public String verify (String name, boolean isChanging)
            {
                setEnabled (false, name);
                if (isChanging)
                {
                    statusBar.clear ();
                    return name;
                }
                if (name.equals (""))
                {
                    statusBar.showError ("Name missing");
                    return null;
                }
                Command cmd = commands.getCommand (name);
                if (cmd != null && cmd != command)
                {
                    statusBar.showError ("Command '" + name +
                        "' is already defined");
                    return null;
                }
                return name;
            }
        });

        descriptionField = new WTextArea (statusBar, "Description");
        descriptionField.setToolTipText ("Description");
        descriptionField.setMnemonic (KeyEvent.VK_T);
        descriptionField.setRows (3);
        descriptionField.setWidthChars (40);
        topPanel.addComponent (descriptionField);

        cardPanel = new JPanel ();
        cardPanel.setLayout (new CardLayout ());
        mainPanel.add (cardPanel);

        for (CommandPanel cp : commandPanels)
            cardPanel.add (cp.getPanel (), cp.getTag ());

        if (isTester)
        {
            GridBagPanel bottomPanel = new GridBagPanel ();
            mainPanel.add (bottomPanel);

            expectedExceptionSelector = new ExceptionSelector (
                "Expected Outcome");
            GridBagConstraints constraints = bottomPanel.createConstraints ();
            bottomPanel.add (expectedExceptionSelector.getLabel (),
                constraints);
            constraints.gridx = 1;
            constraints.weightx = 1.0;
            constraints.fill = GridBagConstraints.HORIZONTAL;
            bottomPanel.add (expectedExceptionSelector.getComponent (),
                constraints);

            actualExceptionField = new ExceptionField (statusBar,
                "Actual Outcome", frontend.getHelpViewer ());
            actualExceptionField.setWidthChars (20);
            constraints = bottomPanel.createConstraints ();
            bottomPanel.add (actualExceptionField.getLabel (), constraints);
            constraints.gridx = 1;
            constraints.weightx = 1.0;
            constraints.fill = GridBagConstraints.HORIZONTAL;
            bottomPanel.add (actualExceptionField.getComponent (), constraints);
        }
        else
        {
            expectedExceptionSelector = null;
            actualExceptionField = null;
        }

        ButtonPanel buttonPanel = new ButtonPanel ();
        mainPanel.add (buttonPanel);
        mainPanel.add (mirror.getComponent ());
        mainPanel.add (statusBar);

        saveButton = buttonPanel.addButton ("Apply", null,
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    Event.checkIsEventDispatchThread ();
                    if (command == null)        // defensive - shouldn't happen
                        return;
                    if (!commandPanel.checkFields ())
                        return;
                    if (!nameField.checkValue ())
                        return;
                    if (!descriptionField.checkValue ())
                        return;
                    String name = nameField.getValue ();
                    if (name.equals (""))
                    {
                        statusBar.showError ("Name missing");
                        return;
                    }
                    Command cmd = commands.getCommand (name);
                    if (cmd != null && cmd != command)
                    {
                        statusBar.showError ("Command '" + name +
                            "' is already defined");
                        return;
                    }
                    ModbusException exception = null;
                    if (isTester)
                    {
                        exception = expectedExceptionSelector.getValue ();
                        if (exception != null &&
                            exception.getErrorCode () == Modbus.ERROR_NONE)
                        {
                            exception = null;
                        }
                    }
                    Command newCommand = commandPanel.createCommand (name,
                        descriptionField.getValue (), exception);
                    commands.replaceCommand (command, newCommand);
                    command = newCommand;
                    statusBar.clear ();
                    setEnabled ();
                }
            });
        saveButton.setMnemonic (KeyEvent.VK_V);
        saveButton.setToolTipText ("Save changes to command definition");

        copyButton = buttonPanel.addButton ("Copy", null,
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    if (command == null)        // defensive - shouldn't happen
                        return;
                    commandDefine.setCommand (command);
                    frontend.selectPanel ("command.define");
                }
            });
        copyButton.setMnemonic (KeyEvent.VK_P);
        copyButton.setToolTipText (
            "Create new command definition by copying this one");

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

        sendButton = buttonPanel.addButton (isTester ? "Test" : "Send", null,
            new ActionListener ()
            {
                public void actionPerformed (ActionEvent e)
                {
                    Event.checkIsEventDispatchThread ();
                    if (!commandPanel.checkFields ())
                        return;
                    Command cmd = commandPanel.createCommand (null, "",
                        isTester ? expectedExceptionSelector.getValue () :
                        null);
                    sendCommand (cmd);
                }
            });
        sendButton.setMnemonic (isTester? KeyEvent.VK_T : KeyEvent.VK_S);
        sendButton.setToolTipText ("Send command to slave");

        if (isTester)
        {
            expectActualButton = buttonPanel.addButton ("Expect actual", null,
                new ActionListener ()
                {
                    public void actionPerformed (ActionEvent e)
                    {
                        statusBar.clear ();
                        ModbusException exception =
                            actualExceptionField.getValue ();
                        expectedExceptionSelector.setValue (exception);
                        actualExceptionField.setValue (exception, false);
                        commandPanel.expectActual ();
                        setEnabled ();
                    }
                });
            expectActualButton.setMnemonic (KeyEvent.VK_E);
            expectActualButton.setToolTipText ("Use actual values as expected");
            Gui.setEnabled (expectActualButton, false);
        }
        else
        {
            expectActualButton = null;
        }

        buttonPanel.addButton (getHelpAction ());

        commands.addValueListener (new ValueListener ()
        {
            public void valueChanged (ValueEvent e)
            {
                update (command);
            }
        });

        frontend.addBackendStateListener (new BackendState.Listener ()
        {
            public void stateChanged (BackendState state)
            {
                setEnabled ();
            }
        });

        ValueListener guiListener = new ValueListener ()
        {
            public void valueChanged (ValueEvent e)
            {
                statusBar.clear ();
                setEnabled (e.isChanging (), nameField.getValue ());
            }
        };

        descriptionField.addValueListener (guiListener);

        for (CommandPanel cp : commandPanels)
            cp.addValueListener (guiListener);

        if (isTester)
        {
            expectedExceptionSelector.addValueListener (guiListener);
        }

        settings.getCommands ().addListListener (
            new ListDataListener ()
            {
                public void intervalAdded (ListDataEvent e)
                {
                }

                public void intervalRemoved (ListDataEvent e)
                {
                }

                public void contentsChanged (ListDataEvent e)
                {
                    int index0 = e.getIndex0 ();
                    int index1 = e.getIndex1 ();
                    if (index0 < 0 && index1 == 0)
                    {
                        // Whole table updated (i.e. loaded from file)
                        reset ();
                    }
                }

            });
    }

    private void selectCommand ()
    {
        Command command = commandSelector.getValue ();
        if (command != this.command)
        {
            this.command = command;
            if (command != null)
            {
                String tag = command.getTag ();
                commandPanel = panelMap.get (tag);
                CardLayout layout = (CardLayout) cardPanel.getLayout ();
                layout.show (cardPanel, tag);
            }
            commandPanel.setCommand (command);
            nameField.setValue (command == null ? "" : command.toString ());
            descriptionField.setValue (
                command == null ? "" : command.getDescription ());
            update (command);
        }
    }

    private void update (Command command)
    {
        statusBar.clear ();
        if (command != null)
        {
            Command.Result actual = command.getActualResult ();
            commandPanel.setActualResult (actual);
            if (isTester)
            {
                Command.Result expected = command.getExpectedResult ();
                ModbusException expectedException =
                    expected == null ? null : expected.getException ();
                ModbusException actualException =
                    actual == null ? null : actual.getException ();
                expectedExceptionSelector.setValue (expectedException);
                boolean exceptionError =
                    actual == null ? false :
                    expectedException == null ? actualException != null :
                    !expectedException.matches (actualException);
                actualExceptionField.setValue (
                    actual == null ? null :
                        actualException == null ? ModbusException.NO_ERROR :
                        actualException,
                    exceptionError);
                statusBar.clear ();
                if (actual != null && expected != null)
                {
                    if (expected.matches (actual))
                        statusBar.showMessage ("Test passed");
                    else
                        statusBar.showError ("Test failed");
                }
            }
        }
        else
        {
            if (isTester)
            {
                expectedExceptionSelector.setValue (null);
                actualExceptionField.setValue (null);
            }
        }
        setEnabled ();
    }

    public JComponent getPanel ()
    {
        return outerPanel;
    }

    public String getTag ()
    {
        return "command.edit";
    }

    public String getName ()
    {
        return "View/Edit Command";
    }

    public JButton getDefaultButton ()
    {
        return saveButton;
    }

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

    public String getToolTipText ()
    {
        return null;
    }

    @Override
    public void selected ()
    {
        selectCommand ();
    }

    @Override
    public void reset ()
    {
        if (command == null)
            return;
        selectCommand ();
        commandPanel.setCommand (command);
        nameField.setValue (command.toString ());
        descriptionField.setValue (command.getDescription ());
        update (command);
    }

    public boolean hasUnappliedChanges ()
    {
        if (command == null)
            return false;
        boolean changed = false;
        if (nameField.hasValueChanged (command.toString ()))
            changed = true;
        if (descriptionField.hasValueChanged (command.getDescription ()))
            changed = true;
        if (isTester && expectedExceptionSelector.hasValueChanged (
            command.getExpectedResult ().getException ()))
        {
            changed = true;
        }
        if (commandPanel.haveValuesChanged (command))
            changed = true;
        return changed;
    }

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

    private boolean checkFields ()
    {
        Event.checkIsEventDispatchThread ();
        statusBar.clear ();
        return commandPanel.checkFields ();
    }

    private boolean nameOk (String name)
    {
        if (name.equals (""))
            return false;
        Command cmd = commands.getCommand (name);
        if (cmd != null && cmd != command)
            return false;
        return true;
    }

    private void setEnabled (boolean isChanging, String name)
    {
        boolean haveCommand = command != null;
        boolean changed = haveCommand && (isChanging || hasUnappliedChanges ());
        commandSelector.setEnabled (haveCommand);
        nameField.setEnabled (haveCommand);
        descriptionField.setEnabled (haveCommand);
        Gui.setEnabled (saveButton, changed && nameOk (name));
        Gui.setEnabled (resetButton, changed);
        Gui.setEnabled (copyButton, haveCommand && !changed);
        Gui.setEnabled (sendButton, haveCommand &&
            frontend.isRunning () && frontend.isMaster ());
        if (isTester)
        {
            boolean haveActual = actualExceptionField.getValue () != null;
            Gui.setEnabled (expectActualButton, haveActual);
            commandPanel.highlightErrors (haveActual);
            boolean expectOK = expectedExceptionSelector.getValue () == null;
            commandPanel.setEnabled (haveCommand, expectOK);
            expectedExceptionSelector.setEnabled (haveCommand);
            actualExceptionField.setEnabled (haveCommand);
        }
        else
        {
            commandPanel.setEnabled (haveCommand, false);
        }
        fireStatusChanged ();
    }

    private void setEnabled ()
    {
        setEnabled (false, nameField.getValue ());
    }

    private void sendCommand (final Command command)
    {
        final ModbusClient cl = backend.getClient ();
        if (cl == null)
        {
            statusBar.showError ("I100", "Connection closed");
            return;
        }

        statusBar.clear ();
        statusBar.showMessage ("Sending command ...");

        backend.execute (new Backend.Task ()
        {
            public void run ()
                throws InterruptedException, IOException, ValueException
            {
                command.send (cl, settings);
            }

            public void done ()
            {
                update (command);
                Command.Result actual = command.getActualResult ();
                ModbusException actualException =
                    actual == null ? null : actual.getException ();
                if (actualException != null)
                {
                    reporterNSB.error (Exceptions.getHelpId (actualException),
                        Exceptions.getMessage (actualException));
                }
                if (!isTester)
                {
                    if (actual != null)
                    {
                        statusBar.showMessage ("OK - response time " +
                            Metric.formatNanoTime (actual.getResponseTime ()));
                    }
                }
            }

            public void exceptionOccurred (Exception ex)
            {
                update (command);
                statusBar.showException (ex);
                reporterNSB.error (Exceptions.getHelpId (ex),
                    Exceptions.getMessage (ex));
                if (ex instanceof IOException)
                    backend.stop ();
            }
        });
    }

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

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

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

    public void printSizes ()
    {
        String tallestMin = "";
        String widestMin = "";
        int maxMinWidth = 0;
        int maxMinHeight = 0;
        String tallestPref = "";
        String widestPref = "";
        int maxPrefWidth = 0;
        int maxPrefHeight = 0;

        System.out.println ();
        System.out.println ("Command Test width " +
            outerPanel.getWidth () + " height " +
            outerPanel.getHeight () + ":");

        for (CommandPanel cp : commandPanels)
        {
            JComponent jcomp = cp.getPanel ();
            int mw = (int) jcomp.getMinimumSize ().getWidth ();
            int mh = (int) jcomp.getMinimumSize ().getHeight ();
            int pw = (int) jcomp.getPreferredSize ().getWidth ();
            int ph = (int) jcomp.getPreferredSize ().getHeight ();
            System.out.println (cp.getTag () + " " +
                mw + " " + mh + ", " + pw + " " + ph);
            if (mw > maxMinWidth)
            {
                maxMinWidth = mw;
                widestMin = cp.getTag ();
            }
            if (mh > maxMinHeight)
            {
                maxMinHeight = mh;
                tallestMin = cp.getTag ();
            }
            if (pw > maxPrefWidth)
            {
                maxPrefWidth = pw;
                widestPref = cp.getTag ();
            }
            if (ph > maxPrefHeight)
            {
                maxPrefHeight = ph;
                tallestPref = cp.getTag ();
            }
        }

        System.out.println ("Widest min: " + widestMin + " " + maxMinWidth);
        System.out.println ("Tallest min: " + tallestMin + " " + maxMinHeight);
        System.out.println ("Widest pref: " + widestPref + " " + maxPrefWidth);
        System.out.println ("Tallest pref: " + tallestPref + " " +
            maxPrefHeight);
    }
}


