package de.neemann.digital.gui;

import de.neemann.digital.analyse.AnalyseException;
import de.neemann.digital.analyse.ModelAnalyser;
import de.neemann.digital.analyse.TruthTable;
import de.neemann.digital.analyse.expression.format.FormatToExpression;
import de.neemann.digital.core.*;
import de.neemann.digital.core.element.ElementAttributes;
import de.neemann.digital.core.element.Key;
import de.neemann.digital.core.element.Keys;
import de.neemann.digital.core.io.In;
import de.neemann.digital.core.io.Out;
import de.neemann.digital.core.memory.ROM;
import de.neemann.digital.core.wiring.Clock;
import de.neemann.digital.draw.elements.*;
import de.neemann.digital.draw.graphics.*;
import de.neemann.digital.draw.library.CustomElement;
import de.neemann.digital.draw.library.ElementLibrary;
import de.neemann.digital.draw.library.ElementNotFoundException;
import de.neemann.digital.draw.model.ModelCreator;
import de.neemann.digital.draw.model.RealTimeClock;
import de.neemann.digital.draw.shapes.ShapeFactory;
import de.neemann.digital.gui.components.*;
import de.neemann.digital.gui.components.data.DataSetDialog;
import de.neemann.digital.gui.components.expression.ExpressionDialog;
import de.neemann.digital.gui.components.table.TableDialog;
import de.neemann.digital.gui.components.testing.TestResultDialog;
import de.neemann.digital.gui.components.tree.LibraryTreeModel;
import de.neemann.digital.gui.components.tree.SelectTree;
import de.neemann.digital.gui.remote.DigitalHandler;
import de.neemann.digital.gui.remote.RemoteException;
import de.neemann.digital.gui.remote.RemoteSever;
import de.neemann.digital.gui.state.State;
import de.neemann.digital.gui.state.StateManager;
import de.neemann.digital.gui.sync.LockSync;
import de.neemann.digital.gui.sync.NoSync;
import de.neemann.digital.gui.sync.Sync;
import de.neemann.digital.lang.Lang;
import de.neemann.digital.testing.TestCaseElement;
import de.neemann.digital.testing.TestingDataException;
import de.neemann.gui.*;
import de.neemann.gui.language.Language;

import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.ActionEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ScheduledThreadPoolExecutor;

/**
 * The main frame of the Digital Simulator
 *
 * @author hneemann
 */
public class Main extends JFrame implements ClosingWindowListener.ConfirmSave, ErrorStopper, FileHistory.OpenInterface, DigitalRemoteInterface, StatusInterface {
    private static final ArrayList<Key> ATTR_LIST = new ArrayList<>();
    private static boolean experimental;

    private static File lastExportDirectory;

    /**
     * @return true if experimental features are enabled
     */
    public static boolean enableExperimental() {
        return experimental;
    }

    static {
        ATTR_LIST.add(Keys.SHOW_DATA_TABLE);
        ATTR_LIST.add(Keys.SHOW_DATA_GRAPH);
        ATTR_LIST.add(Keys.SHOW_DATA_GRAPH_MICRO);
    }

    private static final String MESSAGE = Lang.get("message");
    private static final Icon ICON_RUN = IconCreator.create("media-playback-start.png");
    private static final Icon ICON_MICRO = IconCreator.create("media-playback-start-2.png");
    private static final Icon ICON_TEST = IconCreator.create("media-playback-start-T.png");
    private static final Icon ICON_STEP = IconCreator.create("media-seek-forward.png");
    private static final Icon ICON_STOP = IconCreator.create("media-playback-stop.png");
    private static final Icon ICON_NEW = IconCreator.create("document-new.png");
    private static final Icon ICON_NEW_SUB = IconCreator.create("document-new-sub.png");
    private static final Icon ICON_OPEN = IconCreator.create("document-open.png");
    private static final Icon ICON_OPEN_WIN = IconCreator.create("document-open-new.png");
    private static final Icon ICON_SAVE = IconCreator.create("document-save.png");
    private static final Icon ICON_SAVE_AS = IconCreator.create("document-save-as.png");
    private static final Icon ICON_FAST = IconCreator.create("media-skip-forward.png");
    private static final Icon ICON_EXPAND = IconCreator.create("View-zoom-fit.png");
    private static final Icon ICON_ZOOMIN = IconCreator.create("View-zoom-in.png");
    private static final Icon ICON_ZOOMOUT = IconCreator.create("View-zoom-out.png");
    private static final Icon ICON_HELP = IconCreator.create("help.png");
    private final CircuitComponent circuitComponent;
    private final ToolTipAction save;
    private final ElementLibrary library;
    private final ShapeFactory shapeFactory;
    private final JLabel statusLabel;
    private final StateManager stateManager = new StateManager();
    private final ElementAttributes settings = new ElementAttributes();
    private final ScheduledThreadPoolExecutor timerExecuter = new ScheduledThreadPoolExecutor(1);
    private final WindowPosManager windowPosManager = new WindowPosManager();
    private final InsertHistory insertHistory;

    private ToolTipAction doStep;
    private ToolTipAction runToBreakAction;

    private File lastFilename;
    private File filename;
    private FileHistory fileHistory;

    private Sync modelSync;
    private Model model;

    private ModelCreator modelCreator;
    private boolean realtimeClockRunning;

    private State stoppedState;
    private RunModelState runModelState;
    private State runModelMicroState;
    private JComponent componentOnPane;
    private LibraryTreeModel treeModel;

    private Main() {
        this(null, null, null, null);
    }

    /**
     * Creates a new instance.
     * Used to open a nested circuit.
     *
     * @param parent     the parent component
     * @param fileToOpen a file to open
     * @param library    the library to use, if not null a nested circuit is opened
     */
    public Main(Component parent, File fileToOpen, ElementLibrary library) {
        this(parent, fileToOpen, library, null);
    }

    /**
     * Creates a new instance.
     * Used to show a generated circuit.
     *
     * @param parent  the parent component
     * @param circuit circuit to show
     */
    public Main(Component parent, ElementLibrary parentsLibrary, Circuit circuit) {
        this(parent, null, parentsLibrary, circuit);
    }

    /**
     * Creates a new instance
     *
     * @param parent           the parent component
     * @param nestedFileToOpen a file to open
     * @param parentsLibrary   the parents library. If not null means opening a nested circuit
     * @param circuit          circuit to show
     */
    private Main(Component parent, File nestedFileToOpen, ElementLibrary parentsLibrary, Circuit circuit) {
        super(Lang.get("digital"));
        setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
        setIconImages(IconCreator.createImages("icon32.png", "icon64.png", "icon128.png"));

        if (parentsLibrary != null) library = parentsLibrary;
        else library = new ElementLibrary();

        shapeFactory = new ShapeFactory(library, Settings.getInstance().get(Keys.SETTINGS_IEEE_SHAPES));

        fileHistory = new FileHistory(this);

        circuitComponent = new CircuitComponent(this, library, shapeFactory);
        if (circuit != null) {
            SwingUtilities.invokeLater(() -> circuitComponent.setCircuit(circuit));
            setFilename(nestedFileToOpen, false);
        } else {
            if (nestedFileToOpen != null) {
                SwingUtilities.invokeLater(() -> loadFile(nestedFileToOpen, false));
            } else {
                File name = fileHistory.getMostRecent();
                if (name != null) {
                    SwingUtilities.invokeLater(() -> loadFile(name, true));
                }
            }
        }

        library.addListener(circuitComponent);

        getContentPane().add(circuitComponent);
        componentOnPane = circuitComponent;

        statusLabel = new JLabel(" ");
        getContentPane().add(statusLabel, BorderLayout.SOUTH);

        setupStates();

        JMenuBar menuBar = new JMenuBar();
        JToolBar toolBar = new JToolBar();

        save = createFileMenu(menuBar, toolBar, nestedFileToOpen == null);
        toolBar.addSeparator();

        createViewMenu(menuBar, toolBar);

        toolBar.addSeparator();

        createEditMenu(menuBar);

        toolBar.add(circuitComponent.getDeleteAction().createJButtonNoText());
        toolBar.addSeparator();

        createStartMenu(menuBar, toolBar);

        createAnalyseMenu(menuBar);

        toolBar.addSeparator();

        insertHistory = new InsertHistory(toolBar, library);
        library.addListener(insertHistory);
        final LibrarySelector librarySelector = new LibrarySelector(library, shapeFactory);
        library.addListener(librarySelector);
        menuBar.add(librarySelector.buildMenu(insertHistory, circuitComponent));

        addWindowListener(new ClosingWindowListener(this, this));
        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosed(WindowEvent e) {
                clearModelDescription(); // stop model timer if running
                timerExecuter.shutdown();
                library.removeListener(librarySelector);
                library.removeListener(insertHistory);
                library.removeListener(circuitComponent);
                if (treeModel != null)
                    library.removeListener(treeModel);
            }
        });

        getContentPane().add(toolBar, BorderLayout.NORTH);

        setJMenuBar(menuBar);
        JMenu help = InfoDialog.getInstance().addToFrame(this, MESSAGE);
        help.add(new ToolTipAction(Lang.get("menu_help_elements")) {
            @Override
            public void actionPerformed(ActionEvent actionEvent) {
                try {
                    new ElementHelpDialog(Main.this, library, shapeFactory).setVisible(true);
                } catch (NodeException | PinException e) {
                    new ErrorMessage(Lang.get("msg_creatingHelp")).addCause(e).show(Main.this);
                }
            }
        }.setToolTip(Lang.get("menu_help_elements_tt")).createJMenuItem());

        setPreferredSize(new Dimension(1024, 768));
        pack();
        setLocationRelativeTo(parent);
    }

    private void createViewMenu(JMenuBar menuBar, JToolBar toolBar) {
        ToolTipAction maximize = new ToolTipAction(Lang.get("menu_maximize"), ICON_EXPAND) {
            @Override
            public void actionPerformed(ActionEvent e) {
                circuitComponent.fitCircuit();
            }
        };
        ToolTipAction zoomIn = new ToolTipAction(Lang.get("menu_zoomIn"), ICON_ZOOMIN) {
            @Override
            public void actionPerformed(ActionEvent e) {
                circuitComponent.scaleCircuit(1.25);
            }
        };
        ToolTipAction zoomOut = new ToolTipAction(Lang.get("menu_zoomOut"), ICON_ZOOMOUT) {
            @Override
            public void actionPerformed(ActionEvent e) {
                circuitComponent.scaleCircuit(0.8);
            }
        };

        ToolTipAction viewHelp = new ToolTipAction(Lang.get("menu_viewHelp"), ICON_HELP) {
            @Override
            public void actionPerformed(ActionEvent e) {
                final Circuit circuit = circuitComponent.getCircuit();
                final String name = Lang.get("msg_actualCircuit");
                File file = filename;
                if (file == null)
                    file = new File(name);
                try {
                    ElementLibrary.ElementTypeDescriptionCustom description =
                            new ElementLibrary.ElementTypeDescriptionCustom(file,
                                    attributes -> new CustomElement(circuit, library, filename),
                                    circuit.getAttributes(), circuit.getInputNames());
                    description.setShortName(name);
                    description.setDescription(circuit.getAttributes().get(Keys.DESCRIPTION));
                    new ElementHelpDialog(Main.this, description, circuit.getAttributes()).setVisible(true);
                } catch (PinException | NodeException e1) {
                    new ErrorMessage(Lang.get("msg_creatingHelp")).addCause(e1).show(Main.this);
                }
            }
        }.setToolTip(Lang.get("menu_viewHelp_tt"));

        JCheckBoxMenuItem treeCheckBox = new JCheckBoxMenuItem(Lang.get("menu_treeSelect"));
        treeCheckBox.setToolTipText(Lang.get("menu_treeSelect_tt"));
        treeCheckBox.addActionListener(actionEvent -> {
            getContentPane().remove(componentOnPane);
            if (treeCheckBox.isSelected()) {
                JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
                treeModel = new LibraryTreeModel(library);
                split.setLeftComponent(new JScrollPane(new SelectTree(treeModel, circuitComponent, shapeFactory, insertHistory)));
                split.setRightComponent(circuitComponent);
                getContentPane().add(split);
                componentOnPane = split;
            } else {
                if (treeModel != null) {
                    library.removeListener(treeModel);
                    treeModel = null;
                }
                getContentPane().add(circuitComponent);
                componentOnPane = circuitComponent;
            }
            revalidate();
        });

        toolBar.add(viewHelp.createJButtonNoText());
        toolBar.add(zoomIn.createJButtonNoText());
        toolBar.add(zoomOut.createJButtonNoText());
        toolBar.add(maximize.createJButtonNoText());

        JMenu view = new JMenu(Lang.get("menu_view"));
        menuBar.add(view);
        view.add(maximize.createJMenuItem());
        view.add(zoomOut.createJMenuItem());
        view.add(zoomIn.createJMenuItem());
        view.addSeparator();
        view.add(treeCheckBox);
        view.addSeparator();
        view.add(viewHelp.createJMenuItem());
    }

    /**
     * Creates the file menu and adds it to menu and toolbar
     *
     * @param menuBar the menuBar
     * @param toolBar the toolBar
     * @return the save action
     */
    private ToolTipAction createFileMenu(JMenuBar menuBar, JToolBar toolBar, boolean allowAll) {
        ToolTipAction newFile = new ToolTipAction(Lang.get("menu_new"), ICON_NEW) {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (ClosingWindowListener.checkForSave(Main.this, Main.this)) {
                    setFilename(null, true);
                    circuitComponent.setCircuit(new Circuit());
                    windowPosManager.closeAll();
                    try {
                        library.setRootFilePath(null);
                    } catch (IOException e1) {
                        // can not happen, no folder is scanned
                    }
                }
            }
        }.setToolTip(Lang.get("menu_new_tt")).setActive(allowAll);

        ToolTipAction newSubFile = new ToolTipAction(Lang.get("menu_newSub"), ICON_NEW_SUB) {
            @Override
            public void actionPerformed(ActionEvent e) {
                new Main(Main.this, library, new Circuit()).setVisible(true);
            }
        }.setToolTip(Lang.get("menu_newSub_tt"));

        ToolTipAction open = new ToolTipAction(Lang.get("menu_open"), ICON_OPEN) {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (ClosingWindowListener.checkForSave(Main.this, Main.this)) {
                    JFileChooser fc = getJFileChooser(lastFilename);
                    if (fc.showOpenDialog(Main.this) == JFileChooser.APPROVE_OPTION) {
                        loadFile(fc.getSelectedFile(), true);
                    }
                }
            }
        }.setActive(allowAll);

        ToolTipAction openWin = new ToolTipAction(Lang.get("menu_openWin"), ICON_OPEN_WIN) {
            @Override
            public void actionPerformed(ActionEvent e) {
                JFileChooser fc = getJFileChooser(lastFilename);
                if (fc.showOpenDialog(Main.this) == JFileChooser.APPROVE_OPTION) {
                    Main m = new Main(Main.this, fc.getSelectedFile(), null);
                    m.setLocationRelativeTo(Main.this);
                    m.setVisible(true);
                }
            }
        }.setToolTip(Lang.get("menu_openWin_tt")).setActive(allowAll);

        JMenu openRecent = new JMenu(Lang.get("menu_openRecent"));
        fileHistory.setMenu(openRecent);
        openRecent.setEnabled(allowAll);

        ToolTipAction saveas = new ToolTipAction(Lang.get("menu_saveAs"), ICON_SAVE_AS) {
            @Override
            public void actionPerformed(ActionEvent e) {
                JFileChooser fc = getJFileChooser(lastFilename);
                if (lastFilename == null && library.getRootFilePath() != null)
                    fc.setCurrentDirectory(library.getRootFilePath());

                boolean repeat;
                do {
                    repeat = false;
                    if (fc.showSaveDialog(Main.this) == JFileChooser.APPROVE_OPTION) {
                        final File selectedFile = fc.getSelectedFile();
                        if (library.isFileAccessible(selectedFile))
                            saveFile(selectedFile, false);
                        else {
                            Object[] options = {Lang.get("btn_saveAnyway"), Lang.get("btn_newName"), Lang.get("cancel")};
                            int res = JOptionPane.showOptionDialog(Main.this,
                                    Lang.get("msg_fileNotAccessible"),
                                    Lang.get("msg_warning"),
                                    JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE,
                                    null, options, options[0]);
                            switch (res) {
                                case 0:
                                    saveFile(selectedFile, true);
                                    break;
                                case 1:
                                    repeat = true;
                                    break;
                            }
                        }
                    }
                } while (repeat);
            }
        }.setActive(allowAll);

        ToolTipAction save = new ToolTipAction(Lang.get("menu_save"), ICON_SAVE) {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (filename == null)
                    saveas.actionPerformed(e);
                else
                    saveFile(filename, false);
            }
        };

        JMenu export = new JMenu(Lang.get("menu_export"));
        export.add(new ExportAction(Lang.get("menu_exportSVG"), "svg", GraphicSVGIndex::new));
        export.add(new ExportAction(Lang.get("menu_exportSVGLaTex"), "svg", GraphicSVGLaTeX::new));
        export.add(new ExportAction(Lang.get("menu_exportPNGSmall"), "png", (out, min, max) -> GraphicsImage.create(out, min, max, "PNG", 1)));
        export.add(new ExportAction(Lang.get("menu_exportPNGLarge"), "png", (out, min, max) -> GraphicsImage.create(out, min, max, "PNG", 2)));

        JMenu file = new JMenu(Lang.get("menu_file"));
        menuBar.add(file);
        file.add(newFile.createJMenuItem());
        file.add(newSubFile.createJMenuItem());
        file.add(openRecent);
        file.add(open.createJMenuItem());
        file.add(openWin.createJMenuItem());
        file.add(save.createJMenuItem());
        file.add(saveas.createJMenuItem());
        file.add(export);

        toolBar.add(newFile.createJButtonNoText());
        toolBar.add(open.createJButtonNoText());
        toolBar.add(save.createJButtonNoText());

        return save;
    }

    /**
     * Creates the edit menu
     *
     * @param menuBar the menu bar
     */
    private void createEditMenu(JMenuBar menuBar) {
        JMenu edit = new JMenu(Lang.get("menu_edit"));
        menuBar.add(edit);

        ToolTipAction orderInputs = new ToolTipAction(Lang.get("menu_orderInputs")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                ElementOrder o = new ElementOrder(circuitComponent.getCircuit(),
                        element -> element.equalsDescription(In.DESCRIPTION)
                                || element.equalsDescription(Clock.DESCRIPTION));
                new ElementOrderer<>(Main.this, Lang.get("menu_orderInputs"), o).showDialog();
            }
        }.setToolTip(Lang.get("menu_orderInputs_tt"));

        ToolTipAction orderOutputs = new ToolTipAction(Lang.get("menu_orderOutputs")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                ElementOrder o = new ElementOrder(circuitComponent.getCircuit(),
                        element -> element.equalsDescription(Out.DESCRIPTION)
                                || element.equalsDescription(Out.LEDDESCRIPTION));
                new ElementOrderer<>(Main.this, Lang.get("menu_orderOutputs"), o).showDialog();
            }
        }.setToolTip(Lang.get("menu_orderOutputs_tt"));

        ToolTipAction orderMeasurements = new ToolTipAction(Lang.get("menu_orderMeasurements")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                orderMeasurements();
            }
        }.setToolTip(Lang.get("menu_orderMeasurements_tt"));


        ToolTipAction editAttributes = new ToolTipAction(Lang.get("menu_editAttributes")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                circuitComponent.getCircuit().editAttributes(Main.this);
            }
        }.setToolTip(Lang.get("menu_editAttributes_tt"));

        ToolTipAction editSettings = new ToolTipAction(Lang.get("menu_editSettings")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                Language oldLang = Settings.getInstance().get(Keys.SETTINGS_LANGUAGE);
                if (new AttributeDialog(Main.this, Settings.SETTINGS_KEYS, Settings.getInstance().getAttributes()).showDialog()) {
                    FormatToExpression.setDefaultFormat(Settings.getInstance().get(Keys.SETTINGS_EXPRESSION_FORMAT));
                    final Language newLang = Settings.getInstance().getAttributes().get(Keys.SETTINGS_LANGUAGE);
                    if (!newLang.equals(oldLang)) {
                        Lang.setLanguage(newLang);
                        JOptionPane.showMessageDialog(Main.this, Lang.get("msg_restartNeeded"));
                    }
                }
            }
        }.setToolTip(Lang.get("menu_editSettings_tt"));


        ToolTipAction actualToDefault = new ToolTipAction(Lang.get("menu_actualToDefault")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                circuitComponent.getCircuit().actualToDefault();
                stoppedState.enter();
            }
        }.setToolTip(Lang.get("menu_actualToDefault_tt"));

        ToolTipAction unprogramAllFuses = new ToolTipAction(Lang.get("menu_unprogramAllFuses")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                circuitComponent.getCircuit().unprogramAllFuses();
                stoppedState.enter();
            }
        }.setToolTip(Lang.get("menu_unprogramAllFuses_tt"));

        ToolTipAction insertAsNew = new ToolTipAction(Lang.get("menu_insertAsNew")) {
            @Override
            public void actionPerformed(ActionEvent actionEvent) {
                Clipboard clpbrd = Toolkit.getDefaultToolkit().getSystemClipboard();
                try {
                    Object data = clpbrd.getData(DataFlavor.stringFlavor);
                    if (data instanceof String) {
                        ArrayList<Moveable> elements = CircuitTransferable.createList(data, shapeFactory, new Vector(0, 0));
                        Circuit circuit = new Circuit();
                        for (Moveable m : elements) {
                            if (m instanceof Wire)
                                circuit.add((Wire) m);
                            if (m instanceof VisualElement)
                                circuit.add((VisualElement) m);
                        }

                        Main m = new Main(Main.this, library, circuit);
                        m.lastFilename = filename;
                        m.setLocationRelativeTo(Main.this);
                        m.setVisible(true);
                    }
                } catch (Exception e1) {
                    e1.printStackTrace();
                    SwingUtilities.invokeLater(new ErrorMessage(Lang.get("msg_clipboardContainsNoImportableData")).setComponent(Main.this));
                }
            }
        }.setToolTip(Lang.get("menu_insertAsNew_tt"));

        edit.add(editAttributes.createJMenuItem());
        edit.add(actualToDefault.createJMenuItem());
        edit.add(unprogramAllFuses.createJMenuItem());
        edit.addSeparator();
        edit.add(orderInputs.createJMenuItem());
        edit.add(orderOutputs.createJMenuItem());
        edit.add(orderMeasurements.createJMenuItem());
        edit.addSeparator();

        JMenuItem copyItem = new JMenuItem(circuitComponent.getCopyAction());
        copyItem.setAccelerator(KeyStroke.getKeyStroke('C', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        edit.add(copyItem);
        JMenuItem pasteItem = new JMenuItem(circuitComponent.getPasteAction());
        pasteItem.setAccelerator(KeyStroke.getKeyStroke('V', Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
        edit.add(pasteItem);
        JMenuItem rotateItem = new JMenuItem(circuitComponent.getRotateAction());
        rotateItem.setAccelerator(KeyStroke.getKeyStroke('R'));
        edit.add(rotateItem);
        edit.add(insertAsNew.createJMenuItem());
        edit.addSeparator();
        edit.add(editSettings.createJMenuItem());
    }

    /**
     * Creates the start menu
     *
     * @param menuBar the menu bar
     * @param toolBar the tool bar
     */
    private void createStartMenu(JMenuBar menuBar, JToolBar toolBar) {
        doStep = new ToolTipAction(Lang.get("menu_step"), ICON_STEP) {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    model.doMicroStep(false);
                    circuitComponent.removeHighLighted();
                    modelCreator.addNodeElementsTo(model.nodesToUpdate(), circuitComponent.getHighLighted());
                    circuitComponent.hasChanged();
                    doStep.setEnabled(model.needsUpdate());
                } catch (Exception e1) {
                    SwingUtilities.invokeLater(
                            new ErrorMessage(Lang.get("msg_errorCalculatingStep")).addCause(e1).setComponent(Main.this)
                    );
                }
            }
        }.setToolTip(Lang.get("menu_step_tt"));

        ToolTipAction runModelAction = runModelState.createToolTipAction(Lang.get("menu_run"), ICON_RUN)
                .setToolTip(Lang.get("menu_run_tt"));
        ToolTipAction runModelMicroAction = runModelMicroState.createToolTipAction(Lang.get("menu_micro"), ICON_MICRO)
                .setToolTip(Lang.get("menu_micro_tt"));
        runToBreakAction = new ToolTipAction(Lang.get("menu_fast"), ICON_FAST) {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    int i = model.runToBreak();
                    circuitComponent.hasChanged();
                    statusLabel.setText(Lang.get("stat_clocks", i));
                } catch (NodeException e1) {
                    stoppedState.enter();
                    new ErrorMessage(Lang.get("msg_fastRunError")).addCause(e1).show(Main.this);
                }
            }
        }.setToolTip(Lang.get("menu_fast_tt")).setActive(false);

        ToolTipAction stoppedStateAction = stoppedState.createToolTipAction(Lang.get("menu_element"), ICON_STOP).setToolTip(Lang.get("menu_element_tt"));

        ToolTipAction runTests = new ToolTipAction(Lang.get("menu_runTests"), ICON_TEST) {
            @Override
            public void actionPerformed(ActionEvent e) {
                startTests();
            }
        }.setToolTip(Lang.get("menu_runTests_tt"));

        ToolTipAction speedTest = new ToolTipAction(Lang.get("menu_speedTest")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    Model model = new ModelCreator(circuitComponent.getCircuit(), library).createModel(false);

                    SpeedTest speedTest = new SpeedTest(model);
                    double frequency = speedTest.calculate();
                    circuitComponent.getCircuit().clearState();
                    JOptionPane.showMessageDialog(Main.this, Lang.get("msg_frequency_N", frequency));
                } catch (Exception e1) {
                    new ErrorMessage("SpeedTestError").addCause(e1).show();
                }
            }
        }.setToolTip(Lang.get("menu_speedTest_tt"));

        ToolTipAction editRunAttributes = new ToolTipAction(Lang.get("menu_editRunAttributes")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                new AttributeDialog(Main.this, ATTR_LIST, settings).showDialog();
            }
        }.setToolTip(Lang.get("menu_editRunAttributes_tt"));

        JMenu run = new JMenu(Lang.get("menu_run"));
        menuBar.add(run);
        run.add(editRunAttributes.createJMenuItem());
        run.addSeparator();
        run.add(runModelAction.createJMenuItem());
        run.add(runModelMicroAction.createJMenuItem());
        run.add(doStep.createJMenuItem());
        run.add(runToBreakAction.createJMenuItem());
        run.add(stoppedStateAction.createJMenuItem());
        run.add(runTests.createJMenuItem());
        run.addSeparator();
        run.add(speedTest.createJMenuItem());
        doStep.setEnabled(false);

        toolBar.add(runModelState.setIndicator(runModelAction.createJButtonNoText()));
        toolBar.add(runToBreakAction.createJButtonNoText());
        toolBar.add(stoppedStateAction.createJButtonNoText());
        toolBar.addSeparator();
        toolBar.add(runModelMicroState.setIndicator(runModelMicroAction.createJButtonNoText()));
        toolBar.add(doStep.createJButtonNoText());
        toolBar.addSeparator();
        toolBar.add(runTests.createJButtonNoText());
    }

    /**
     * starts the tests
     */
    public void startTests() {
        try {
            ArrayList<TestResultDialog.TestSet> tsl = new ArrayList<>();
            for (VisualElement el : circuitComponent.getCircuit().getElements())
                if (el.equalsDescription(TestCaseElement.TESTCASEDESCRIPTION))
                    tsl.add(new TestResultDialog.TestSet(
                            el.getElementAttributes().get(TestCaseElement.TESTDATA),
                            el.getElementAttributes().getCleanLabel()));

            if (tsl.isEmpty())
                throw new TestingDataException(Lang.get("err_noTestData"));

            windowPosManager.register("testresult", new TestResultDialog(Main.this, tsl, circuitComponent.getCircuit(), library)).setVisible(true);

            stoppedState.enter();
        } catch (NodeException | ElementNotFoundException | PinException | TestingDataException | RuntimeException e1) {
            showErrorAndStopModel(Lang.get("msg_runningTestError"), e1);
        }
    }

    /**
     * Creates the analyse menu
     *
     * @param menuBar the menu bar
     */
    private void createAnalyseMenu(JMenuBar menuBar) {
        JMenu analyse = new JMenu(Lang.get("menu_analyse"));
        menuBar.add(analyse);
        analyse.add(new ToolTipAction(Lang.get("menu_analyse")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                try {
                    Model model = new ModelCreator(circuitComponent.getCircuit(), library).createModel(false);
                    new TableDialog(Main.this, new ModelAnalyser(model).analyse(), library, shapeFactory, filename)
                            .setVisible(true);
                    stoppedState.enter();
                } catch (PinException | NodeException | AnalyseException | ElementNotFoundException | RuntimeException e1) {
                    showErrorAndStopModel(Lang.get("msg_annalyseErr"), e1);
                }
            }
        }
                .setToolTip(Lang.get("menu_analyse_tt"))
                .createJMenuItem());

        analyse.add(new ToolTipAction(Lang.get("menu_synthesise")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                TruthTable tt = new TruthTable(3).addResult();
                new TableDialog(Main.this, tt, library, shapeFactory, null).setVisible(true);
                stoppedState.enter();
            }
        }
                .setToolTip(Lang.get("menu_synthesise_tt"))
                .createJMenuItem());

        analyse.add(new ToolTipAction(Lang.get("menu_expression")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                new ExpressionDialog(Main.this, library, shapeFactory).setVisible(true);
            }
        }
                .setToolTip(Lang.get("menu_expression_tt"))
                .createJMenuItem());

    }

    private void orderMeasurements() {
        try {
            Model m = new ModelCreator(circuitComponent.getCircuit(), library).createModel(false);
            stoppedState.enter();
            ArrayList<String> names = new ArrayList<>();
            for (Signal s : m.getSignals())
                names.add(s.getName());
            new OrderMerger<String, String>(circuitComponent.getCircuit().getMeasurementOrdering()).order(names);
            ElementOrderer.ListOrder<String> o = new ElementOrderer.ListOrder<>(names);
            if (new ElementOrderer<>(Main.this, Lang.get("menu_orderMeasurements"), o)
                    .addOkButton()
                    .showDialog()) {
                circuitComponent.getCircuit().setMeasurementOrdering(names);
            }
        } catch (NodeException | PinException | ElementNotFoundException | RuntimeException e) {
            showErrorAndStopModel(Lang.get("msg_errorCreatingModel"), e);
        }
    }

    private void setupStates() {
        stoppedState = stateManager.register(new State() {
            @Override
            public void enter() {
                super.enter();
                clearModelDescription();
                circuitComponent.setModeAndReset(false, NoSync.INST);
                doStep.setEnabled(false);
                stoppedState.getAction().setEnabled(false);
                runToBreakAction.setEnabled(false);
            }

        });
        runModelState = stateManager.register(new RunModelState());
        runModelMicroState = stateManager.register(new State() {
            @Override
            public void enter() {
                super.enter();
                stoppedState.getAction().setEnabled(true);
                if (createAndStartModel(false, ModelEvent.MICROSTEP, null))
                    circuitComponent.setManualChangeObserver(new MicroStepObserver(model));
            }
        });
    }

    private void clearModelDescription() {
        circuitComponent.removeHighLighted();
        if (model != null)
            model.close();

        modelCreator = null;
        model = null;
    }

    private boolean createAndStartModel(boolean globalRunClock, ModelEvent updateEvent, ModelModifier modelModifier) {
        try {
            circuitComponent.removeHighLighted();

            modelCreator = new ModelCreator(circuitComponent.getCircuit(), library);

            if (model != null)
                model.close();

            model = modelCreator.createModel(true);

            statusLabel.setText(Lang.get("msg_N_nodes", model.size()));

            realtimeClockRunning = false;
            modelSync = null;
            if (globalRunClock)
                for (Clock c : model.getClocks())
                    if (c.getFrequency() > 0) {
                        if (modelSync == null)
                            modelSync = new LockSync();
                        model.addObserver(new RealTimeClock(model, c, timerExecuter, this, modelSync, this));
                        realtimeClockRunning = true;
                    }
            if (modelSync == null)
                modelSync = NoSync.INST;

            circuitComponent.setModeAndReset(true, modelSync);

            if (realtimeClockRunning) {
                // if clock is running, enable automatic update of gui
                GuiModelObserver gmo = new GuiModelObserver(circuitComponent, updateEvent);
                modelCreator.connectToGui(gmo);
                model.addObserver(gmo);
            } else
                // all repainting is initiated by user actions!
                modelCreator.connectToGui(null);

            doStep.setEnabled(false);
            runToBreakAction.setEnabled(!realtimeClockRunning && model.isFastRunModel());

            List<String> ordering = circuitComponent.getCircuit().getMeasurementOrdering();
            if (settings.get(Keys.SHOW_DATA_TABLE))
                windowPosManager.register("probe", new ProbeDialog(this, model, updateEvent, ordering, modelSync)).setVisible(true);

            if (settings.get(Keys.SHOW_DATA_GRAPH))
                windowPosManager.register("dataset", new DataSetDialog(this, model, updateEvent == ModelEvent.MICROSTEP, ordering, modelSync)).setVisible(true);
            if (settings.get(Keys.SHOW_DATA_GRAPH_MICRO))
                windowPosManager.register("datasetMicro", new DataSetDialog(this, model, true, ordering, modelSync)).setVisible(true);

            if (modelModifier != null)
                modelModifier.preInit(model);

            model.init();

            return true;
        } catch (NodeException | PinException | RuntimeException | ElementNotFoundException e) {
            showErrorAndStopModel(Lang.get("msg_errorCreatingModel"), e);
        }
        return false;
    }

    @Override
    public void showErrorAndStopModel(String message, Exception cause) {
        SwingUtilities.invokeLater(() -> {
            if (cause instanceof NodeException) {
                NodeException e = (NodeException) cause;
                circuitComponent.addHighLightedWires(e.getValues());
                if (modelCreator != null)
                    modelCreator.addNodeElementsTo(e.getNodes(), circuitComponent.getHighLighted());
            } else if (cause instanceof PinException) {
                PinException e = (PinException) cause;
                circuitComponent.addHighLighted(e.getVisualElement());
                if (e.getNet() != null)
                    circuitComponent.addHighLighted(e.getNet().getWires());
            } else if (cause instanceof BurnException) {
                BurnException e = (BurnException) cause;
                circuitComponent.addHighLightedWires(e.getValues());
            }
            circuitComponent.hasChanged();
            new ErrorMessage(message).addCause(cause).show(Main.this);
            stoppedState.enter();
        });
    }

    /**
     * stops the model
     */
    public void stopModel() {
        stoppedState.enter();
    }


    private static JFileChooser getJFileChooser(File filename) {
        File folder = null;
        if (filename != null)
            folder = filename.getParentFile();

        JFileChooser fileChooser = new JFileChooser(folder);
        fileChooser.setFileFilter(new FileNameExtensionFilter("Circuit", "dig"));
        return fileChooser;
    }

    @Override
    public boolean isStateChanged() {
        return circuitComponent.getCircuit().isModified();
    }

    @Override
    public void saveChanges() {
        save.actionPerformed(null);
    }

    @Override
    public void open(File file) {
        if (ClosingWindowListener.checkForSave(Main.this, Main.this)) {
            loadFile(file, true);
        }
    }

    private void loadFile(File filename, boolean setLibraryRoot) {
        try {
            setFilename(filename, setLibraryRoot);
            if (setLibraryRoot) library.setRootFilePath(filename.getParentFile());
            Circuit circ = Circuit.loadCircuit(filename, shapeFactory);
            circuitComponent.setCircuit(circ);
            stoppedState.enter();
            windowPosManager.closeAll();
            statusLabel.setText(" ");
        } catch (Exception e) {
            circuitComponent.setCircuit(new Circuit());
            setFilename(null, false);
            new ErrorMessage(Lang.get("msg_errorReadingFile")).addCause(e).show(this);
        }
    }

    private void saveFile(File filename, boolean toPrefs) {
        filename = checkSuffix(filename, "dig");
        try {
            circuitComponent.getCircuit().save(filename);
            stoppedState.enter();
            setFilename(filename, toPrefs);

            library.invalidateElement(filename);

            if (library.getRootFilePath() == null)
                library.setRootFilePath(filename.getParentFile());
        } catch (IOException e) {
            new ErrorMessage(Lang.get("msg_errorWritingFile")).addCause(e).show();
        }
    }

    private void setFilename(File filename, boolean toPrefs) {
        this.filename = filename;
        if (filename != null) {
            this.lastFilename = filename;
            if (toPrefs)
                fileHistory.add(filename);
            setTitle(filename + " - " + Lang.get("digital"));
        } else {
            setTitle(Lang.get("digital"));
        }
    }

    /**
     * Adds the given suffix to the file
     *
     * @param filename filename
     * @param suffix   suffix
     * @return the file name with the given suffix
     */
    public static File checkSuffix(File filename, String suffix) {
        String name = filename.getName();
        int p = name.lastIndexOf('.');
        if (p >= 0)
            name = name.substring(0, p);
        return new File(filename.getParentFile(), name + "." + suffix);
    }

    /**
     * @return the window position manager
     */
    public WindowPosManager getWindowPosManager() {
        return windowPosManager;
    }

    @Override
    public void setStatus(String message) {
        SwingUtilities.invokeLater(() -> statusLabel.setText(message));
    }

    private class FullStepObserver implements Observer {
        private final Model model;

        FullStepObserver(Model model) {
            this.model = model;
        }

        @Override
        public void hasChanged() {
            try {
                modelSync.accessNEx(() -> {
                    model.fireManualChangeEvent();
                    model.doStep();
                });
                circuitComponent.hasChanged();
            } catch (NodeException | RuntimeException e) {
                showErrorAndStopModel(Lang.get("msg_errorCalculatingStep"), e);
            }
        }
    }

    private class MicroStepObserver implements Observer {
        private final Model model;

        MicroStepObserver(Model model) {
            this.model = model;
        }

        @Override
        public void hasChanged() {
            modelCreator.addNodeElementsTo(model.nodesToUpdate(), circuitComponent.getHighLighted());
            model.fireManualChangeEvent();
            circuitComponent.hasChanged();
            doStep.setEnabled(model.needsUpdate());
        }
    }

    private class ExportAction extends ToolTipAction {
        private final String name;
        private final String suffix;
        private final ExportFactory exportFactory;

        ExportAction(String name, String suffix, ExportFactory exportFactory) {
            super(name);
            this.name = name;
            this.suffix = suffix;
            this.exportFactory = exportFactory;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            JFileChooser fc = new JFileChooser();
            if (filename != null)
                fc.setSelectedFile(checkSuffix(filename, suffix));

            if (lastExportDirectory != null)
                fc.setCurrentDirectory(lastExportDirectory);

            fc.addChoosableFileFilter(new FileNameExtensionFilter(name, suffix));
            if (fc.showSaveDialog(Main.this) == JFileChooser.APPROVE_OPTION) {

                lastExportDirectory = fc.getSelectedFile().getParentFile();

                try (OutputStream out = new FileOutputStream(checkSuffix(fc.getSelectedFile(), suffix))) {
                    new Export(circuitComponent.getCircuit(), exportFactory).export(out);

                } catch (IOException e1) {
                    new ErrorMessage(Lang.get("msg_errorWritingFile")).addCause(e1).show(Main.this);
                }
            }
        }
    }

    private class RunModelState extends State {
        @Override
        public void enter() {
            enter(true, null);
        }

        void enter(boolean runRealTime, ModelModifier modelModifier) {
            super.enter();
            stoppedState.getAction().setEnabled(true);
            if (createAndStartModel(runRealTime, ModelEvent.STEP, modelModifier))
                circuitComponent.setManualChangeObserver(new FullStepObserver(model));
        }
    }

    //***********************
    // remote interface start
    //***********************

    private static class AddressPicker {
        private long addr;

        private void getProgRomAddr(Model model) {
            List<ROM> roms = model.findNode(ROM.class, ROM::isProgramMemory);
            if (roms.size() == 1)
                addr = roms.get(0).getRomAddress();
            else
                addr = -1;
        }

        String getAddrString() {
            if (addr < 0)
                return null;
            else
                return Long.toHexString(addr);
        }
    }

    private void setDebug(boolean debug) {
        settings.set(Keys.SHOW_DATA_TABLE, debug);
    }

    @Override
    public void start(File romHex) throws RemoteException {
        SwingUtilities.invokeLater(() -> {
            setDebug(false);
            windowPosManager.closeAll();
            runModelState.enter(true, new RomLoader(romHex));
            circuitComponent.hasChanged();
        });
    }

    @Override
    public void debug(File romHex) throws RemoteException {
        SwingUtilities.invokeLater(() -> {
            setDebug(true);
            runModelState.enter(false, new RomLoader(romHex));
            circuitComponent.hasChanged();
        });
    }

    @Override
    public String doSingleStep() throws RemoteException {
        if (model != null && !realtimeClockRunning) {
            try {
                AddressPicker addressPicker = new AddressPicker();
                SwingUtilities.invokeAndWait(() -> {
                    ArrayList<Clock> cl = model.getClocks();
                    if (cl.size() == 1) {
                        ObservableValue clkVal = cl.get(0).getClockOutput();
                        clkVal.setBool(!clkVal.getBool());
                        try {
                            model.doStep();
                            if (clkVal.getBool()) {
                                clkVal.setBool(!clkVal.getBool());
                                model.doStep();
                            }
                            circuitComponent.hasChanged();
                            addressPicker.getProgRomAddr(model);
                        } catch (NodeException | RuntimeException e) {
                            showErrorAndStopModel(Lang.get("err_remoteExecution"), e);
                        }
                    }
                });
                return addressPicker.getAddrString();
            } catch (InterruptedException | InvocationTargetException e) {
                throw new RemoteException("error performing a single step " + e.getMessage());
            }
        }
        return null;
    }

    @Override
    public String runToBreak() throws RemoteException {
        try {
            AddressPicker addressPicker = new AddressPicker();
            SwingUtilities.invokeAndWait(() -> {
                if (model != null && model.isFastRunModel() && !realtimeClockRunning)
                    runToBreakAction.actionPerformed(null);
                addressPicker.getProgRomAddr(model);
            });
            return addressPicker.getAddrString();
        } catch (InterruptedException | InvocationTargetException e) {
            throw new RemoteException("error performing a run to break " + e.getMessage());
        }
    }

    @Override
    public void stop() {
        SwingUtilities.invokeLater(() -> {
            stoppedState.enter();
            circuitComponent.hasChanged();
        });
    }
    //**********************
    // remote interface end
    //**********************

    /**
     * Starts the main app
     *
     * @param args the arguments
     */
    public static void main(String[] args) {
        ToolTipManager.sharedInstance().setDismissDelay(10000);
        URL.setURLStreamHandlerFactory(ElementHelpDialog.createURLStreamHandlerFactory());
        experimental = args.length == 1 && args[0].equals("experimental");
        FormatToExpression.setDefaultFormat(Settings.getInstance().get(Keys.SETTINGS_EXPRESSION_FORMAT));
        SwingUtilities.invokeLater(() -> {
            Main main = new Main();
            try {
                new RemoteSever(new DigitalHandler(main)).start(41114);
            } catch (IOException e) {
                SwingUtilities.invokeLater(() -> main.statusLabel.setText(Lang.get("err_portIsInUse")));
            }
            main.setVisible(true);
        });
    }

}
