package de.neemann.digital.gui.components;

import de.neemann.digital.core.NodeException;
import de.neemann.digital.core.ObservableValue;
import de.neemann.digital.core.Observer;
import de.neemann.digital.core.element.ElementTypeDescription;
import de.neemann.digital.core.element.ImmutableList;
import de.neemann.digital.core.element.Key;
import de.neemann.digital.core.element.Keys;
import de.neemann.digital.draw.elements.*;
import de.neemann.digital.draw.graphics.*;
import de.neemann.digital.draw.library.ElementLibrary;
import de.neemann.digital.draw.library.ElementNotFoundException;
import de.neemann.digital.draw.library.LibraryListener;
import de.neemann.digital.draw.library.LibraryNode;
import de.neemann.digital.draw.shapes.Drawable;
import de.neemann.digital.draw.shapes.ShapeFactory;
import de.neemann.digital.gui.Main;
import de.neemann.digital.gui.sync.NoSync;
import de.neemann.digital.gui.sync.Sync;
import de.neemann.digital.lang.Lang;
import de.neemann.gui.*;

import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;

import static de.neemann.digital.draw.shapes.GenericShape.SIZE;
import static de.neemann.digital.draw.shapes.GenericShape.SIZE2;
import static java.awt.event.InputEvent.CTRL_DOWN_MASK;

/**
 * Component which shows the circuit.
 * ToDo: refactoring of repaint logic. Its to complex now.
 *
 * @author hneemann
 */
public class CircuitComponent extends JComponent implements Circuit.ChangedListener, LibraryListener {
    /**
     * The delete icon, also used from {@link de.neemann.digital.gui.components.terminal.TerminalDialog}
     */
    public static final Icon ICON_DELETE = IconCreator.create("delete.png");
    private static final String DEL_ACTION = "myDelAction";
    private static final String ESC_ACTION = "myEscAction";
    private static final int MOUSE_BORDER_SMALL = 10;
    private static final int MOUSE_BORDER_LARGE = 50;

    private static final int DRAG_DISTANCE = (int) (SIZE2 * Screen.getInstance().getScaling());

    private final Main parent;
    private final ElementLibrary library;
    private final HashSet<Drawable> highLighted;
    private final ToolTipAction deleteAction;

    private final MouseController mouseNormal;
    private final MouseControllerInsertElement mouseInsertElement;
    private final MouseControllerMoveElement mouseMoveElement;
    private final MouseControllerMoveWire mouseMoveWire;
    private final MouseControllerWire mouseWire;
    private final MouseControllerSelect mouseSelect;
    private final MouseControllerMoveSelected mouseMoveSelected;
    private final MouseController mouseRun;
    private final MouseControllerInsertCopied mouseInsertList;
    private final Cursor moveCursor;
    private final ToolTipAction copyAction;
    private final ToolTipAction pasteAction;
    private final ToolTipAction rotateAction;

    private Circuit circuit;
    private MouseController activeMouseController;
    private AffineTransform transform = new AffineTransform();
    private Observer manualChangeObserver;
    private Vector lastMousePos;
    private Sync modelSync;
    private boolean isManualScale;
    private boolean hasChanged = true;
    private boolean focusWasLost = false;
    private boolean lockMessageShown = false;
    private boolean antiAlias = true;


    /**
     * Creates a new instance
     *
     * @param parent       the parent window
     * @param library      the library used to edit the attributes of the elements
     * @param shapeFactory the shapeFactory used for copied elements
     */
    public CircuitComponent(Main parent, ElementLibrary library, ShapeFactory shapeFactory) {
        this.parent = parent;
        this.library = library;
        highLighted = new HashSet<>();

        rotateAction = new ToolTipAction(Lang.get("menu_rotate")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                activeMouseController.rotate();
            }
        }.setActive(false).setAccelerator(KeyStroke.getKeyStroke("R")).enableAcceleratorIn(this);

        copyAction = new ToolTipAction(Lang.get("menu_copy")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                ArrayList<Movable> elements = null;
                if (activeMouseController instanceof MouseControllerSelect) {
                    MouseControllerSelect mcs = ((MouseControllerSelect) activeMouseController);
                    elements = circuit.getElementsToCopy(Vector.min(mcs.corner1, mcs.corner2), Vector.max(mcs.corner1, mcs.corner2), shapeFactory);
                } else if (activeMouseController instanceof MouseControllerMoveElement) {
                    MouseControllerMoveElement mcme = ((MouseControllerMoveElement) activeMouseController);
                    elements = new ArrayList<>();
                    elements.add(mcme.visualElement);
                }
                if (elements != null) {
                    Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
                    clipboard.setContents(new CircuitTransferable(elements), null);
                    activeMouseController.escapePressed();
                }
            }
        }.setActive(false).setAcceleratorCTRLplus('C').enableAcceleratorIn(this);


        pasteAction = new ToolTipAction(Lang.get("menu_paste")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!isLocked()) {
                    Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
                    try {
                        Object data = clipboard.getData(DataFlavor.stringFlavor);
                        if (data instanceof String) {
                            Vector posVector = getPosVector(lastMousePos.x, lastMousePos.y);
                            ArrayList<Movable> elements = CircuitTransferable.createList(data, shapeFactory, posVector);
                            if (elements != null) {
                                removeHighLighted();
                                mouseInsertList.activate(elements, posVector);
                            }
                        }
                    } catch (Exception e1) {
                        e1.printStackTrace();
                        SwingUtilities.invokeLater(new ErrorMessage(Lang.get("msg_clipboardContainsNoImportableData")).setComponent(CircuitComponent.this));
                    }
                }
            }
        }.setAcceleratorCTRLplus('P').enableAcceleratorIn(this);

        deleteAction = new ToolTipAction(Lang.get("menu_delete"), ICON_DELETE) {
            @Override
            public void actionPerformed(ActionEvent e) {
                activeMouseController.delete();
            }
        }.setToolTip(Lang.get("menu_delete_tt"));

        new ToolTipAction("Escape") {
            @Override
            public void actionPerformed(ActionEvent e) {
                activeMouseController.escapePressed();
            }
        }.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0)).enableAcceleratorIn(this);

        new ToolTipAction(Lang.get("menu_programDiode")) {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (activeMouseController instanceof MouseControllerNormal) {
                    programElementAt(getPosVector(lastMousePos.x, lastMousePos.y));
                }
            }
        }.setAccelerator(KeyStroke.getKeyStroke("P")).enableAcceleratorIn(this);

        getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), DEL_ACTION);
        getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), DEL_ACTION);
        getActionMap().put(DEL_ACTION, deleteAction);

        setFocusable(true);

        addMouseWheelListener(e -> {
            Vector pos = getPosVector(e);
            double f = Math.pow(0.9, e.getWheelRotation());
            transform.translate(pos.x, pos.y);
            transform.scale(f, f);
            transform.translate(-pos.x, -pos.y);
            isManualScale = true;
            hasChanged();
        });

        addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent componentEvent) {
                if (!isManualScale)
                    fitCircuit();
            }
        });

        addFocusListener(new FocusAdapter() {
            @Override
            public void focusLost(FocusEvent focusEvent) {
                focusWasLost = true;
            }
        });

        Cursor normalCursor = new Cursor(Cursor.DEFAULT_CURSOR);
        moveCursor = new Cursor(Cursor.MOVE_CURSOR);
        mouseNormal = new MouseControllerNormal(normalCursor);
        mouseInsertElement = new MouseControllerInsertElement(normalCursor);
        mouseInsertList = new MouseControllerInsertCopied(normalCursor);
        mouseMoveElement = new MouseControllerMoveElement(normalCursor);
        mouseMoveWire = new MouseControllerMoveWire(normalCursor);
        mouseWire = new MouseControllerWire(normalCursor);
        mouseSelect = new MouseControllerSelect(new Cursor(Cursor.CROSSHAIR_CURSOR));
        mouseMoveSelected = new MouseControllerMoveSelected(moveCursor);
        mouseRun = new MouseControllerRun(normalCursor);

        setCircuit(new Circuit());

        MouseDispatcher dispatcher = new MouseDispatcher();
        addMouseMotionListener(dispatcher);
        addMouseListener(dispatcher);

        setToolTipText("");
    }

    private void programElementAt(Vector pos) {
        VisualElement ve = circuit.getElementAt(pos);
        if (ve != null && library.isProgrammable(ve.getElementName())) {
            boolean blown = ve.getElementAttributes().get(Keys.BLOWN);
            ve.getElementAttributes().set(Keys.BLOWN, !blown);
            circuit.modified();
            hasChanged();
        }
    }

    /**
     * invalidates the image buffer and calls repaint();
     */
    public void hasChanged() {
        hasChanged = true;
        repaint();
    }

    /**
     * @return the main frame
     */
    public Main getMain() {
        return parent;
    }

    @Override
    public String getToolTipText(MouseEvent event) {
        Vector pos = getPosVector(event);
        VisualElement ve = circuit.getElementAt(pos);
        if (ve == null) return null;

        Pin p = circuit.getPinAt(raster(pos), ve);
        if (p != null)
            return createPinToolTip(p);

        try {
            ElementTypeDescription etd = library.getElementType(ve.getElementName());
            String tt = etd.getDescription(ve.getElementAttributes());
            final int pin = ve.getElementAttributes().get(Keys.PINNUMBER);
            if (pin > 0)
                tt += " (" + Lang.get("msg_pin_N", pin) + ")";
            return checkToolTip(tt);
        } catch (ElementNotFoundException e) {
            return null;
        }
    }

    private String createPinToolTip(Pin p) {
        String text = p.getName();
        final String des = p.getDescription();
        if (des != null && des.length() > 0) {
            text += ": " + des;
        }
        if (p.getPinNumber() != 0)
            text += " (" + Lang.get("msg_pin_N", +p.getPinNumber()) + ")";
        return checkToolTip(text);
    }

    private String checkToolTip(String tt) {
        if (tt != null && tt.length() == 0)
            return null;
        else
            return new LineBreaker().toHTML().breakLines(tt);
    }

    /**
     * @return the delete action to put it to the toolbar
     */
    public ToolTipAction getDeleteAction() {
        return deleteAction;
    }

    /**
     * @return the copy action
     */
    public ToolTipAction getCopyAction() {
        return copyAction;
    }

    /**
     * @return the paste action
     */
    public ToolTipAction getPasteAction() {
        return pasteAction;
    }

    /**
     * @return the rotate action
     */
    public ToolTipAction getRotateAction() {
        return rotateAction;
    }

    /**
     * Sets the observer to call if the user is clicking on elements while running.
     *
     * @param callOnManualChange the listener
     */
    public void setManualChangeObserver(Observer callOnManualChange) {
        this.manualChangeObserver = callOnManualChange;
    }

    /**
     * Sets the edit mode and resets the circuit
     *
     * @param runMode   true if running, false if editing
     * @param modelSync used to access the running model
     */
    public void setModeAndReset(boolean runMode, Sync modelSync) {
        this.modelSync = modelSync;
        if (runMode)
            mouseRun.activate();
        else {
            mouseNormal.activate();
            circuit.clearState();
        }
        requestFocusInWindow();
    }

    /**
     * @return the high lighted elements
     */
    public Collection<Drawable> getHighLighted() {
        return highLighted;
    }

    /**
     * Adds a drawable to the highlighted list
     *
     * @param drawable the drawable to add
     * @param <T>      type of drawable
     */
    public <T extends Drawable> void addHighLighted(T drawable) {
        if (drawable != null)
            highLighted.add(drawable);
    }

    /**
     * Add a list of drawables to high light
     *
     * @param drawables the list of drawables
     */
    public void addHighLighted(Collection<? extends Drawable> drawables) {
        if (drawables != null)
            highLighted.addAll(drawables);
    }

    /**
     * Adds all the wires representing the given value to the highlighted list
     *
     * @param values the value
     */
    public void addHighLightedWires(ImmutableList<ObservableValue> values) {
        if (values == null) return;

        HashSet<ObservableValue> ov = new HashSet<>();
        ov.addAll(values);
        for (Wire w : circuit.getWires())
            if (ov.contains(w.getValue()))
                addHighLighted(w);
    }

    /**
     * remove all highlighted elements
     */
    public void removeHighLighted() {
        highLighted.clear();
    }

    /**
     * Adds the given element to insert to the circuit
     *
     * @param element the element to insert
     */
    public void setPartToInsert(VisualElement element) {
        parent.stopModel();
        mouseInsertElement.activate(element);
        Point point = MouseInfo.getPointerInfo().getLocation();
        SwingUtilities.convertPointFromScreen(point, this);
        if (point.x < MOUSE_BORDER_LARGE || point.x > getWidth() - MOUSE_BORDER_SMALL
                || point.y < MOUSE_BORDER_LARGE || point.y > getHeight() - MOUSE_BORDER_SMALL) {

            if (point.x < MOUSE_BORDER_LARGE)
                point.x = MOUSE_BORDER_LARGE;
            else if (point.x > getWidth() - MOUSE_BORDER_SMALL)
                point.x = getWidth() - MOUSE_BORDER_SMALL;

            if (point.y < MOUSE_BORDER_LARGE)
                point.y = MOUSE_BORDER_LARGE;
            else if (point.y > getHeight() - MOUSE_BORDER_SMALL)
                point.y = getHeight() - MOUSE_BORDER_SMALL;

            SwingUtilities.convertPointToScreen(point, this);

            try {
                new Robot().mouseMove(point.x, point.y);
            } catch (AWTException e) {
                e.printStackTrace();
            }
        }
    }


    private BufferedImage buffer;
    private int highlightedPaintedSize;

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);

        boolean needsNewBuffer = buffer == null
                || getWidth() != buffer.getWidth()
                || getHeight() != buffer.getHeight();

        if (needsNewBuffer && !isManualScale)
            fitCircuit();

        if (hasChanged
                || needsNewBuffer
                || highLighted.size() != highlightedPaintedSize) {

            if (needsNewBuffer)
                buffer = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().createCompatibleImage(getWidth(), getHeight());

            Graphics2D gr2 = buffer.createGraphics();
            enableAntiAlias(gr2);
            gr2.setColor(Color.WHITE);
            gr2.fillRect(0, 0, getWidth(), getHeight());
            gr2.transform(transform);

            GraphicSwing gr = new GraphicSwing(gr2, (int) (2 / transform.getScaleX()));

            long time = System.currentTimeMillis();
            circuit.drawTo(gr, highLighted, modelSync);
            time = System.currentTimeMillis() - time;

            if (time > 500) antiAlias = false;
            if (time < 50) antiAlias = true;

//            System.out.println("repaint: " + Long.toString(time) + "ms");

            highlightedPaintedSize = highLighted.size();
            hasChanged = false;
        }

        g.drawImage(buffer, 0, 0, null);

        Graphics2D gr2 = (Graphics2D) g;
        AffineTransform oldTrans = gr2.getTransform();
        gr2.transform(transform);
        enableAntiAlias(gr2);
        GraphicSwing gr = new GraphicSwing(gr2, (int) (2 / transform.getScaleX()));
        activeMouseController.drawTo(gr);
        gr2.setTransform(oldTrans);
    }

    private void enableAntiAlias(Graphics2D gr2) {
        if (antiAlias) {
            gr2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            gr2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
            gr2.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
        }
    }

    @Override
    public void circuitHasChanged() {
        hasChanged = true;
    }

    /**
     * forces a immediately repaint
     * Is called from {@link de.neemann.digital.gui.GuiModelObserver} if the models data has changed.
     * Therefore the double buffer is invalidated.
     */
    public void paintImmediately() {
        hasChanged = true;
        paintImmediately(0, 0, getWidth(), getHeight());
    }


    private Vector getPosVector(MouseEvent e) {
        return getPosVector(e.getX(), e.getY());
    }

    private Vector getPosVector(int x, int y) {
        try {
            Point2D.Double p = new Point2D.Double();
            transform.inverseTransform(new Point(x, y), p);
            return new Vector((int) Math.round(p.getX()), (int) Math.round(p.getY()));
        } catch (NoninvertibleTransformException e1) {
            throw new RuntimeException(e1);
        }
    }

    /**
     * rounds the given vector to the raster
     *
     * @param pos the vector
     * @return pos round to raster
     */
    static Vector raster(Vector pos) {
        return new Vector((int) Math.round((double) pos.x / SIZE) * SIZE,
                (int) Math.round((double) pos.y / SIZE) * SIZE);
    }

    /**
     * @return the circuit shown
     */
    public Circuit getCircuit() {
        return circuit;
    }

    /**
     * Sets a circuit to this component
     *
     * @param circuit the circuit
     */
    public void setCircuit(Circuit circuit) {

        if (this.circuit != null) {
            this.circuit.removeListener(this);
        }

        this.circuit = circuit;

        circuit.addListener(this);

        fitCircuit();
        setModeAndReset(false, NoSync.INST);
    }

    /**
     * maximizes the circuit shown
     */
    public void fitCircuit() {
        GraphicMinMax gr = new GraphicMinMax();
        circuit.drawTo(gr);

        AffineTransform newTrans = new AffineTransform();
        if (gr.getMin() != null && getWidth() != 0 && getHeight() != 0) {
            Vector delta = gr.getMax().sub(gr.getMin());
            double sx = ((double) getWidth()) / (delta.x + Style.NORMAL.getThickness() * 2);
            double sy = ((double) getHeight()) / (delta.y + Style.NORMAL.getThickness() * 2);
            double s = Math.min(sx, sy);


            newTrans.setToScale(s, s);  // set Scaling

            Vector center = gr.getMin().add(gr.getMax()).div(2);
            newTrans.translate(-center.x, -center.y);  // move drawing center to (0,0)

            Vector dif = new Vector(getWidth(), getHeight()).div(2);
            newTrans.translate(dif.x / s, dif.y / s);  // move drawing center to frame center
            isManualScale = false;
        } else {
            isManualScale = true;
        }
        if (!newTrans.equals(transform)) {
            transform = newTrans;
            hasChanged();
        }
    }

    /**
     * scales the circuit
     *
     * @param f factor to scale
     */
    public void scaleCircuit(double f) {
        Vector dif = getPosVector(getWidth() / 2, getHeight() / 2);
        transform.translate(dif.x, dif.y);
        transform.scale(f, f);
        transform.translate(-dif.x, -dif.y);
        isManualScale = true;
        hasChanged();
    }

    private void editAttributes(VisualElement vp, MouseEvent e) {
        String name = vp.getElementName();
        try {
            ElementTypeDescription elementType = library.getElementType(name);
            ArrayList<Key> list = elementType.getAttributeList();
            if (list.size() > 0) {
                Point p = new Point(e.getX(), e.getY());
                SwingUtilities.convertPointToScreen(p, CircuitComponent.this);
                AttributeDialog attributeDialog = new AttributeDialog(this, p, list, vp.getElementAttributes());
                if (elementType instanceof ElementLibrary.ElementTypeDescriptionCustom) {
                    attributeDialog.addButton(Lang.get("attr_openCircuitLabel"), new ToolTipAction(Lang.get("attr_openCircuit")) {
                        @Override
                        public void actionPerformed(ActionEvent e) {
                            attributeDialog.dispose();
                            new Main.MainBuilder()
                                    .setParent(CircuitComponent.this)
                                    .setFileToOpen(((ElementLibrary.ElementTypeDescriptionCustom) elementType).getFile())
                                    .setLibrary(library)
                                    .denyMostFileActions()
                                    .keepPrefMainFile()
                                    .openLater();
                        }
                    }.setToolTip(Lang.get("attr_openCircuit_tt")));
                }
                attributeDialog.addButton(new ToolTipAction(Lang.get("attr_help")) {
                    @Override
                    public void actionPerformed(ActionEvent actionEvent) {
                        try {
                            new ElementHelpDialog(attributeDialog, elementType, vp.getElementAttributes()).setVisible(true);
                        } catch (PinException | NodeException e1) {
                            new ErrorMessage(Lang.get("msg_creatingHelp")).addCause(e1).show(CircuitComponent.this);
                        }
                    }
                }.setToolTip(Lang.get("attr_help_tt")));
                if (attributeDialog.showDialog()) {
                    circuit.modified();
                    hasChanged();
                }
            }
        } catch (ElementNotFoundException ex) {
            // do nothing if element not found!
        }
    }

    @Override
    public void libraryChanged(LibraryNode node) {
        circuit.clearState();
        hasChanged = true;
        repaint();
    }

    /**
     * @return returns true if this circuit is locked
     */
    public boolean isLocked() {
        final boolean locked = circuit.getAttributes().get(Keys.LOCKED_MODE);
        if (locked && !lockMessageShown) {
            String message = Lang.get("msg_isLocked",
                    Lang.get("menu_edit"),
                    Lang.get("menu_editAttributes"),
                    Lang.get("key_lockedMode"));
            SwingUtilities.invokeLater(new ErrorMessage(message).setComponent(this));
            lockMessageShown = true;
        }
        return locked;
    }

    private class MouseDispatcher extends MouseAdapter implements MouseMotionListener {
        private Vector pos;
        private boolean isMoved;

        @Override
        public void mousePressed(MouseEvent e) {
            pos = new Vector(e.getX(), e.getY());
            isMoved = false;
            requestFocusInWindow();
            activeMouseController.pressed(e);
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            activeMouseController.released(e);
            if (!(wasMoved(e) || isMoved))
                activeMouseController.clicked(e);
        }

        private boolean wasMoved(MouseEvent e) {
            Vector d = new Vector(e.getX(), e.getY()).sub(pos);
            return Math.abs(d.x) > DRAG_DISTANCE || Math.abs(d.y) > DRAG_DISTANCE;
        }

        @Override
        public void mouseMoved(MouseEvent e) {
            lastMousePos = new Vector(e.getX(), e.getY());
            activeMouseController.moved(e);
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            lastMousePos = new Vector(e.getX(), e.getY());
            activeMouseController.moved(e);
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            if (wasMoved(e) || isMoved) {
                isMoved = true;
                if (!activeMouseController.dragged(e)) {
                    Vector newPos = new Vector(e.getX(), e.getY());
                    Vector delta = newPos.sub(pos);
                    double s = transform.getScaleX();
                    transform.translate(delta.x / s, delta.y / s);
                    pos = newPos;
                    isManualScale = true;
                    hasChanged();
                }
            }
        }

    }

    //MouseController can not be final because its overridden. Maybe checkstyle has a bug?
    //CHECKSTYLE.OFF: FinalClass
    private class MouseController {
        private final Cursor mouseCursor;

        private MouseController(Cursor mouseCursor) {
            this.mouseCursor = mouseCursor;
        }

        void activate() {
            activeMouseController = this;
            deleteAction.setActive(false);
            copyAction.setEnabled(false);
            rotateAction.setEnabled(false);
            setCursor(mouseCursor);
            hasChanged();
        }

        void clicked(MouseEvent e) {
        }

        void pressed(MouseEvent e) {
        }

        void released(MouseEvent e) {
        }

        void moved(MouseEvent e) {
        }

        boolean dragged(MouseEvent e) {
            return false;
        }

        public void drawTo(Graphic gr) {
        }

        public void delete() {
        }

        public void rotate() {
        }

        public void escapePressed() {
        }
    }


    private VisualElement getVisualElement(Vector pos, boolean includeText) {
        VisualElement vp = null;
        List<VisualElement> list = circuit.getElementListAt(pos, includeText);
        if (list.size() == 1)
            vp = list.get(0);
        else if (list.size() > 1) {
            ItemPicker<VisualElement> picker = new ItemPicker<>(CircuitComponent.this, list);
            vp = picker.select();
        }
        return vp;
    }

    //CHECKSTYLE.ON: FinalClass

    private final class MouseControllerNormal extends MouseController {
        private Vector pos;
        private int downButton;

        private MouseControllerNormal(Cursor cursor) {
            super(cursor);
        }

        @Override
        void clicked(MouseEvent e) {
            Vector pos = getPosVector(e);

            if (e.getButton() == MouseEvent.BUTTON3) {
                if (!isLocked()) {
                    VisualElement vp = getVisualElement(pos, true);
                    if (vp != null)
                        editAttributes(vp, e);
                }
            } else if (e.getButton() == MouseEvent.BUTTON1) {
                VisualElement vp = getVisualElement(pos, false);
                if (vp != null) {
                    if (circuit.isPinPos(raster(pos), vp) && !e.isControlDown()) {
                        if (!isLocked()) mouseWire.activate(pos);
                    } else
                        mouseMoveElement.activate(vp, pos);
                } else if (!isLocked()) {
                    if (e.isControlDown()) {
                        Wire wire = circuit.getWireAt(pos, SIZE2);
                        if (wire != null)
                            mouseMoveWire.activate(wire, pos);
                    } else if (!focusWasLost)
                        mouseWire.activate(pos);
                }
            }
            focusWasLost = false;
        }

        @Override
        void pressed(MouseEvent e) {
            downButton = e.getButton();
            pos = getPosVector(e);
        }

        @Override
        boolean dragged(MouseEvent e) {
            if (downButton == MouseEvent.BUTTON1) {
                mouseSelect.activate(pos, getPosVector(e));
                return true;
            }
            return false;
        }

    }

    private final class MouseControllerInsertElement extends MouseController {
        private VisualElement element;
        private Vector delta;

        private MouseControllerInsertElement(Cursor cursor) {
            super(cursor);
        }

        private void activate(VisualElement element) {
            super.activate();
            this.element = element;
            delta = null;
            deleteAction.setActive(true);
            rotateAction.setEnabled(true);
        }

        @Override
        void moved(MouseEvent e) {
            if (delta == null) {
                GraphicMinMax minMax = element.getMinMax(false);
                delta = element.getPos().sub(minMax.getMax());
            }
            element.setPos(raster(getPosVector(e).add(delta)));
            repaint();
        }

        @Override
        public void delete() {
            mouseNormal.activate();
        }

        @Override
        public void drawTo(Graphic gr) {
            if (delta != null)
                element.drawTo(gr, true);
        }

        @Override
        void clicked(MouseEvent e) {
            if (e.getButton() == MouseEvent.BUTTON1 && !isLocked()) {
                circuit.add(element);
                hasChanged();
            }
            mouseNormal.activate();
            focusWasLost = false;
        }

        @Override
        public void rotate() {
            element.rotate();
            repaint();
        }

        @Override
        public void escapePressed() {
            mouseNormal.activate();
        }
    }

    private final class MouseControllerMoveElement extends MouseController {
        private VisualElement visualElement;
        private Vector delta;
        private Vector initialPos;
        private int initialRot;

        private MouseControllerMoveElement(Cursor cursor) {
            super(cursor);
        }

        private void activate(VisualElement visualElement, Vector pos) {
            super.activate();
            this.visualElement = visualElement;
            initialPos = visualElement.getPos();
            initialRot = visualElement.getRotate();
            delta = initialPos.sub(pos);
            deleteAction.setActive(true);
            rotateAction.setEnabled(true);
            copyAction.setEnabled(true);
            hasChanged();
        }

        @Override
        void clicked(MouseEvent e) {
            if (!isLocked())
                visualElement.setPos(raster(visualElement.getPos()));
            mouseNormal.activate();
        }

        @Override
        void moved(MouseEvent e) {
            if (!isLocked()) {
                Vector pos = getPosVector(e);
                visualElement.setPos(raster(pos.add(delta)));
                circuit.modified();
                hasChanged();
            }
        }

        @Override
        public void drawTo(Graphic gr) {
            visualElement.drawTo(gr, true);
        }

        @Override
        public void delete() {
            if (!isLocked()) {
                circuit.delete(visualElement);
                mouseNormal.activate();
                isManualScale = true;
            }
        }

        @Override
        public void rotate() {
            if (!isLocked()) {
                visualElement.rotate();
                circuit.modified();
                hasChanged();
            }
        }

        @Override
        public void escapePressed() {
            if (!isLocked()) {
                visualElement.setPos(raster(initialPos));
                visualElement.setRotation(initialRot);
            }
            mouseNormal.activate();
        }
    }

    private final class MouseControllerMoveWire extends MouseController {
        private Wire wire;
        private Vector pos;
        private Vector initialPos;

        private MouseControllerMoveWire(Cursor cursor) {
            super(cursor);
        }

        private void activate(Wire wire, Vector pos) {
            super.activate();
            this.wire = wire;
            this.pos = raster(pos);
            this.initialPos = this.pos;
            deleteAction.setActive(true);
            removeHighLighted();
            hasChanged();
        }

        @Override
        void clicked(MouseEvent e) {
            removeHighLighted();
            circuit.elementsMoved();
            mouseNormal.activate();
        }

        @Override
        void moved(MouseEvent e) {
            Vector pos = raster(getPosVector(e));
            final Vector delta = pos.sub(this.pos);
            if (!delta.isZero()) {
                wire.move(delta);
                wire.noDot();
                isManualScale = true;
                circuit.modified();
                hasChanged();
            }
            this.pos = pos;
        }

        @Override
        public void delete() {
            circuit.delete(wire);
            mouseNormal.activate();
            isManualScale = true;
        }

        @Override
        public void drawTo(Graphic gr) {
            // ensure that highlighted wire is visible by drawing it on top of other drawings.
            wire.drawTo(gr, true);
        }

        @Override
        public void escapePressed() {
            wire.move(initialPos.sub(pos));
            removeHighLighted();
            circuit.elementsMoved();
            mouseNormal.activate();
        }
    }


    private final class MouseControllerWire extends MouseController {
        private Wire wire;

        private MouseControllerWire(Cursor cursor) {
            super(cursor);
        }

        private void activate(Vector startPos) {
            super.activate();
            Vector pos = raster(startPos);
            wire = new Wire(pos, pos);
        }

        @Override
        void moved(MouseEvent e) {
            wire.setP2(raster(getPosVector(e)));
            repaint();
        }

        @Override
        void clicked(MouseEvent e) {
            if (e.getButton() == MouseEvent.BUTTON3)
                mouseNormal.activate();
            else {
                circuit.add(wire);
                if (circuit.isPinPos(wire.p2))
                    mouseNormal.activate();
                else
                    wire = new Wire(wire.p2, wire.p2);
            }
        }

        @Override
        public void drawTo(Graphic gr) {
            wire.drawTo(gr, false);
        }

        @Override
        public void escapePressed() {
            mouseNormal.activate();
        }
    }

    private final class MouseControllerSelect extends MouseController {
        private static final int MIN_SIZE = 8;
        private Vector corner1;
        private Vector corner2;
        private boolean wasReleased;

        private MouseControllerSelect(Cursor cursor) {
            super(cursor);
        }

        private void activate(Vector corner1, Vector corner2) {
            super.activate();
            this.corner1 = corner1;
            this.corner2 = corner2;
            deleteAction.setActive(true);
            copyAction.setEnabled(true);
            rotateAction.setEnabled(true);
            wasReleased = false;
        }

        @Override
        void clicked(MouseEvent e) {
            mouseNormal.activate();
            removeHighLighted();
        }

        @Override
        void released(MouseEvent e) {
            wasReleased = true;
            Vector dif = corner1.sub(corner2);
            if (Math.abs(dif.x) > MIN_SIZE && Math.abs(dif.y) > MIN_SIZE)
                setCursor(moveCursor);
            else {
                removeHighLighted();
                mouseNormal.activate();
            }
        }

        @Override
        boolean dragged(MouseEvent e) {
            if (wasReleased) {
                if (!isLocked())
                    mouseMoveSelected.activate(corner1, corner2, getPosVector(e));
            } else {
                corner2 = getPosVector(e);
                if ((e.getModifiersEx() & CTRL_DOWN_MASK) != 0) {
                    Vector dif = corner2.sub(corner1);
                    int dx = dif.x;
                    int dy = dif.y;
                    int absDx = Math.abs(dx);
                    int absDy = Math.abs(dy);
                    if (absDx != absDy) {
                        if (absDx > absDy) {
                            if (dx > absDy) dx = absDy;
                            else dx = -absDy;
                        } else {
                            if (dy > absDx) dy = absDx;
                            else dy = -absDx;
                        }
                    }
                    corner2 = corner1.add(dx, dy);
                }

                ArrayList<Drawable> elements = circuit.getElementsToHighlight(Vector.min(corner1, corner2), Vector.max(corner1, corner2));
                removeHighLighted();
                if (elements != null)
                    addHighLighted(elements);

                repaint();
            }
            return true;
        }

        @Override
        public void drawTo(Graphic gr) {
            Vector p1 = new Vector(corner1.x, corner2.y);
            Vector p2 = new Vector(corner2.x, corner1.y);
            gr.drawLine(corner1, p1, Style.DASH);
            gr.drawLine(p1, corner2, Style.DASH);
            gr.drawLine(p2, corner2, Style.DASH);
            gr.drawLine(corner1, p2, Style.DASH);
        }

        @Override
        public void delete() {
            if (!isLocked()) {
                circuit.delete(Vector.min(corner1, corner2), Vector.max(corner1, corner2));
                mouseNormal.activate();
                isManualScale = true;
            }
        }

        public void rotate() {
            if (!isLocked()) {
                mouseMoveSelected.activate(corner1, corner2, lastMousePos);
                mouseMoveSelected.rotate();
            }
        }

        @Override
        public void escapePressed() {
            removeHighLighted();
            mouseNormal.activate();
        }
    }

    private void rotateElements(ArrayList<Movable> elements, Vector pos) {
        Vector p1 = raster(pos);

        Transform transform = new TransformRotate(p1, 1) {
            @Override
            public Vector transform(Vector v) {
                return super.transform(v.sub(p1));
            }
        };

        for (Movable m : elements) {

            if (m instanceof VisualElement) {
                VisualElement ve = (VisualElement) m;
                ve.rotate();
                ve.setPos(transform.transform(ve.getPos()));
            } else if (m instanceof Wire) {
                Wire w = (Wire) m;
                w.p1 = transform.transform(w.p1);
                w.p2 = transform.transform(w.p2);
            } else {
                Vector p = m.getPos();
                Vector t = transform.transform(p);
                m.move(t.sub(p));
            }

        }

        circuit.modified();
        hasChanged();
    }


    private final class MouseControllerMoveSelected extends MouseController {
        private ArrayList<Movable> elements;
        private Vector lastPos;
        private Vector center;
        private boolean wasMoved;

        private MouseControllerMoveSelected(Cursor cursor) {
            super(cursor);
        }

        private void activate(Vector corner1, Vector corner2, Vector pos) {
            super.activate();
            rotateAction.setEnabled(true);
            lastPos = pos;
            center = corner1.add(corner2).div(2);
            wasMoved = false;
            elements = circuit.getElementsToMove(Vector.min(corner1, corner2), Vector.max(corner1, corner2));
        }

        @Override
        void moved(MouseEvent e) {
            lastPos = getPosVector(e);
        }

        @Override
        boolean dragged(MouseEvent e) {
            if (elements != null) {
                Vector pos = getPosVector(e);
                Vector delta = raster(pos.sub(lastPos));

                if (delta.x != 0 || delta.y != 0) {
                    for (Movable m : elements)
                        m.move(delta);
                    wasMoved = true;

                    hasChanged();
                    lastPos = lastPos.add(delta);
                    center = center.add(delta);
                }
            }
            return true;
        }

        @Override
        void released(MouseEvent e) {
            if (wasMoved)
                circuit.elementsMoved();
            removeHighLighted();
            mouseNormal.activate();
        }

        @Override
        public void rotate() {
            rotateElements(elements, center);
        }
    }

    private final class MouseControllerInsertCopied extends MouseController {
        private ArrayList<Movable> elements;
        private Vector lastPos;

        private MouseControllerInsertCopied(Cursor cursor) {
            super(cursor);
        }

        private void activate(ArrayList<Movable> elements, Vector pos) {
            super.activate();
            this.elements = elements;
            lastPos = pos;
            deleteAction.setActive(true);
            rotateAction.setEnabled(true);
        }

        @Override
        void moved(MouseEvent e) {
            if (elements != null) {
                Vector pos = getPosVector(e);
                Vector delta = raster(pos.sub(lastPos));

                if (delta.x != 0 || delta.y != 0) {
                    for (Movable m : elements)
                        m.move(delta);

                    repaint();
                    lastPos = lastPos.add(delta);
                }
            }
        }

        @Override
        public void drawTo(Graphic gr) {
            if (elements != null)
                for (Movable m : elements)
                    if (m instanceof Drawable)
                        ((Drawable) m).drawTo(gr, true);
        }

        @Override
        public void delete() {
            mouseNormal.activate();
        }

        @Override
        void clicked(MouseEvent e) {
            if (elements != null && e.getButton() == 1) {
                for (Movable m : elements) {
                    if (m instanceof Wire)
                        circuit.add((Wire) m);
                    if (m instanceof VisualElement)
                        circuit.add((VisualElement) m);
                }
            }
            mouseNormal.activate();
            focusWasLost = false;
        }

        @Override
        public void rotate() {
            rotateElements(elements, lastPos);
        }

        @Override
        public void escapePressed() {
            mouseNormal.activate();
        }
    }


    private interface Actor {
        boolean interact(CircuitComponent cc, Point p, Vector posInComponent, Sync modelSync);
    }

    private final class MouseControllerRun extends MouseController {

        private boolean dragHandled;

        private MouseControllerRun(Cursor cursor) {
            super(cursor);
        }

        @Override
        void pressed(MouseEvent e) {
            VisualElement ve = getInteractiveElementAt(e);
            if (ve != null) {
                interact(e, ve::elementPressed);
                dragHandled = true;
            } else
                dragHandled = false;
        }

        private VisualElement getInteractiveElementAt(MouseEvent e) {
            List<VisualElement> elementList = circuit.getElementListAt(getPosVector(e), false);
            for (VisualElement ve : elementList) {
                if (ve.isInteractive())
                    return ve;
            }
            return null;
        }

        @Override
        void released(MouseEvent e) {
            VisualElement ve = getInteractiveElementAt(e);
            if (ve != null)
                interact(e, ve::elementReleased);
        }

        @Override
        void clicked(MouseEvent e) {
            VisualElement ve = getInteractiveElementAt(e);
            if (ve != null)
                interact(e, ve::elementClicked);
        }

        @Override
        boolean dragged(MouseEvent e) {
            VisualElement ve = getInteractiveElementAt(e);
            if (ve != null)
                interact(e, ve::elementDragged);
            return dragHandled;
        }

        private void interact(MouseEvent e, Actor actor) {
            Point p = new Point(e.getX(), e.getY());
            SwingUtilities.convertPointToScreen(p, CircuitComponent.this);
            boolean modelHasChanged = actor.interact(CircuitComponent.this, p, getPosVector(e), modelSync);
            if (modelHasChanged) {
                if (manualChangeObserver != null)
                    manualChangeObserver.hasChanged();
            } else
                hasChanged();
        }
    }

    /**
     * Activate a wizard
     *
     * @param wizardNotification the wizard notification
     */
    public void activateWizard(WizardNotification wizardNotification) {
        mouseNormal.activate();
        circuit.clearState();
        new MouseControllerWizard(wizardNotification).activate();
    }

    /**
     * Deactivate a wizard
     */
    public void deactivateWizard() {
        if (activeMouseController instanceof MouseControllerWizard) {
            MouseControllerWizard mcw = (MouseControllerWizard) activeMouseController;
            mcw.wizardNotification.closed();
        }
        mouseNormal.activate();
    }

    private final class MouseControllerWizard extends MouseController {

        private final WizardNotification wizardNotification;

        private MouseControllerWizard(WizardNotification wizardNotification) {
            super(new Cursor(Cursor.CROSSHAIR_CURSOR));
            this.wizardNotification = wizardNotification;
        }

        @Override
        void clicked(MouseEvent e) {
            Vector pos = getPosVector(e);
            VisualElement vp = getVisualElement(pos, true);
            if (vp != null)
                wizardNotification.notify(vp);
        }

        @Override
        public void escapePressed() {
            wizardNotification.closed();
            mouseNormal.activate();
        }
    }

    /**
     * Interface to interact with wizards
     */
    public interface WizardNotification {
        /**
         * Called if an element is clicked
         *
         * @param clicked the element clicked
         */
        void notify(VisualElement clicked);

        /**
         * Called if the wizard is to close
         */
        void closed();
    }

}
