package vermilion;

import JSSPF.PluginClassDef;
import java.awt.Graphics;
import java.util.ArrayList;
import java.awt.Polygon;
import java.awt.event.*;
import java.awt.Point;
import java.awt.Cursor;
import java.awt.Robot;
import java.awt.Color;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import relative.*;


/**
 * An extension of the JPanel class that shows machines and their
 * connections (wires) on the screen. Allows the user to manipulate
 * the machines and connections.
 *
 * This includes a custom drawn control that shows a 'dot' the user can
 * drag around to change the volume and panning of a wire.
 */
public class JWirePanel extends JPanel
{
    // Stores the list of machine representations we are showing on
    // the screen. This will be a reflection of what is in the router.
    private ArrayList<JWirePanelMachineBox> machineBoxes = new ArrayList<JWirePanelMachineBox>();
    private ArrayList<WireControl> machWires = new ArrayList<WireControl>();

    // This is our reference to the router itself.
    private SystemRouter sysRouter;

    // Variables we set when we grab a wire.
    private WireControl selectedWire = null;
    private int ctrlY, grabY = 0;
    private int ctrlX, grabX = 0;
    private float grabAmpL = 1.0f;
    private float grabAmpR = 1.0f;

    /**
     * The width at which we draw machines, shared with JMachineBox.
     */
    public static final int MACHINE_WIDTH = 120;

    /**
     * The height at which we draw machines, shared with JMachineBox.
     */
    public static final int MACHINE_HEIGHT = 50;

    // Work out 120 degrees as a value in radians.
    private static final double DEG120 = Math.PI * 2.0 / 3.0;


    // Two popup menus, one for empty space and one for wire nodes.
    private JPopupMenu bgMenu;
    private JWirePanelAddListener bgMenuListen;
    private JPopupMenu wcMenu;

    // Variable we set when we show a popup menu for a wire.
    private WireControl menuWire = null;

    // Variable that is set when we ctrl+drag from a machine, adding a wire.
    private JWirePanelMachineBox newWireMachine = null;
    private int newWireLocX = 0;
    private int newWireLocY = 0;

    // Colours for shading the wires depending on channel.
    private Color[] wireColors = new Color[] { new Color(0.6f, 0.6f, 0.9f), new Color(0.6f, 0.9f, 0.6f), new Color(0.9f, 0.6f, 0.6f) };
    
    // Color for drawing 'the dot' and the dimensions of 'the panel'
    // which is calculated on redraw.
    private Color dotColor = new Color(0.6f, 0.6f, 0.9f);
    private int dotPanelSize = 50;

    private RelativeLayout theLayout;

    /**
     * The constructor simply sets the layout manager, event handlers and menus.
     */
    public JWirePanel(SystemRouter router)
    {
        sysRouter = router;

        theLayout = new RelativeLayout(null);
        setLayout(theLayout);

        // Add a mouse click listener that handles popup menus and shows wire controls.
        this.addMouseListener(new MouseListener() {
            @Override
            public void mouseExited(MouseEvent e){}
            @Override
            public void mouseEntered(MouseEvent e){}
            @Override
            public void mouseReleased(MouseEvent e)
            {
                // When the mouse is released we clear the selected wire
                // var, reset the mouse cursor and repaint sans control.
                selectedWire = null;
                setCursor(Cursor.getDefaultCursor());
                repaint();
            }
            @Override
            public void mousePressed(MouseEvent e)
            {
                // Check if the mouse was pressed in the zone of one of
                // the wires.
                boolean handled = false;
                for (int x = 0; x < machWires.size(); x++)
                {
                    WireControl wc = machWires.get(x);
                    if (wc.poly.contains(e.getPoint()))
                    {
                        // Detect if left or right click.
                        if (e.getButton() == MouseEvent.BUTTON1)
                        {
                            // If we've left clicked a wire, grab the wire and
                            // the point where we clicked, both for calculation
                            // and for drawing.
                            selectedWire = wc;
                            ctrlY = grabY = e.getY();
                            ctrlX = grabX = e.getX();

                            // Get the initial volumes for the wire.
                            grabAmpL = wc.wire.amplitudeL;
                            grabAmpR = wc.wire.amplitudeR;

                            // Find out where 'the dot' is going to be and
                            // adjust for the mouse being moved there.
                            Point dotPos = getDotPos(grabAmpL, grabAmpR);
                            grabX += dotPos.x;
                            grabY += dotPos.y;

                            // Move the cursor over the dot and set it to the crosshair one.
                            setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
                            try
                            {
                                new Robot().mouseMove(e.getXOnScreen() + dotPos.x, e.getYOnScreen() + dotPos.y);
                            }
                            catch (Exception ex) {}

                            // Repaint with the volume/panning control on top.
                            repaint();
                        }
                        else // Right click
                        {
                            // Show menu for changing wire's channel or deleting.
                            wcMenu.show(e.getComponent(), e.getX(), e.getY());
                            menuWire = wc;
                        }

                        handled = true;
                    }
                }

                // If we right clicked on empty space, show the machine menu.
                if (!handled && e.getButton() == MouseEvent.BUTTON3)
                {
                    bgMenu.show(e.getComponent(), e.getX(), e.getY());
                    bgMenuListen.setLocation(e.getX(), e.getY());
                }
            }
            @Override
            public void mouseClicked(MouseEvent e){}
        } );

        // Add a mouse motion listener to handle dragging 'the dot'.
        this.addMouseMotionListener(new MouseMotionListener() {
            @Override
            public void mouseDragged(MouseEvent me)
            {
                if (selectedWire != null)
                {
                    // Get the amount we've moved from where the dot was
                    // initially.
                    int volDelta = me.getY() - grabY;
                    int panDelta = me.getX() - grabX;

                    // Add the (control relative) initial dot position.
                    Point dotPos = getDotPos(grabAmpL, grabAmpR);
                    volDelta += dotPos.y;
                    panDelta += dotPos.x;

                    // Work out where we now want the dot to be.
                    if (volDelta > dotPanelSize) volDelta = dotPanelSize;
                    if (volDelta < -dotPanelSize) volDelta = -dotPanelSize;
                    if (panDelta > dotPanelSize) panDelta = dotPanelSize;
                    if (panDelta < -dotPanelSize) panDelta = -dotPanelSize;

                    // Calculate the volume and panning amounts.
                    float volAmount = -volDelta / (dotPanelSize / 2.0f);
                    float panAmount = panDelta / (float) dotPanelSize;

                    // Figure out the actual volumes we want to set.
                    float biggest = (float) Math.pow(10.0, volAmount);
                    float smallest = biggest - Math.abs(panAmount) * biggest;

                    // Set the volume on the wire.
                    if (panAmount >= 0)
                    {
                        selectedWire.wire.amplitudeR = biggest;
                        selectedWire.wire.amplitudeL = smallest;
                    }
                    else
                    {
                        selectedWire.wire.amplitudeR = smallest;
                        selectedWire.wire.amplitudeL = biggest;
                    }

                    // Finally we repaint, and if we've done the maths
                    // correctly, we should have put the dot under the mouse.
                    repaint();
                }
            }
            @Override
            public void mouseMoved(MouseEvent me) { }
        } );

        // Set up the add machine menu.
        bgMenu = new JPopupMenu();
        JMenu addItem = new JMenu("Add");
        theLayout.registerForFontSizeChanges(addItem);

        bgMenuListen = new JWirePanelAddListener(this);

        // Add a menu item for each control.
        ArrayList<PluginClassDef> machineList = Main.plugins.getPluginList();
        for (int x = 0; x < machineList.size(); x++)
        {
            PluginClassDef dc = machineList.get(x);
            if (dc.classType.equals("EFFECT") || dc.classType.equals("SOURCE"))
            {
                JMenuItem machItem = new JMenuItem(dc.className);
                theLayout.registerForFontSizeChanges(machItem);
                machItem.addActionListener(bgMenuListen);
                addItem.add(machItem);
            }
        }

        bgMenu.add(addItem);

        // Set up the wire control menu.
        wcMenu = new JPopupMenu();
        JMenuItem tItem = new JMenuItem("Channel A");
        theLayout.registerForFontSizeChanges(tItem);
        tItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e){
                menuWire.wire.channel = 0;
                repaint();
                }
            });
        wcMenu.add(tItem);
        
        tItem = new JMenuItem("Channel B");
        theLayout.registerForFontSizeChanges(tItem);
        tItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e){
                menuWire.wire.channel = 1;
                repaint();
                }
            });
        wcMenu.add(tItem);
        
        tItem = new JMenuItem("Channel C");
        theLayout.registerForFontSizeChanges(tItem);
        tItem.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e){
                menuWire.wire.channel = 2;
                repaint();
                }
            });
        wcMenu.add(tItem);

        tItem = new JMenuItem("- Delete -");
        theLayout.registerForFontSizeChanges(tItem);
        tItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                sysRouter.removeWire(menuWire.wire);
                repaint();
            }
        });
        wcMenu.add(tItem);
    }

    /**
     * Called from the system router to pass the list of machines
     * and draw them on the screen. This method will be called
     * whenever a plugin is added or removed from the wiring.
     * @param machines The list of machines currently in the
     * wiring for the composition.
     */
    public void setupMachineBoxes(ArrayList<MachineBase> machines, ArrayList<SimpleWire> wires)
    {
        // Set up an interface here by adding a JComponent
        // to the panel for each machine. We draw the wires
        // over the top ourselves.

        machineBoxes.clear();
        removeAll();

        for (int x = 0; x < machines.size(); x++)
        {
            MachineBase machine = machines.get(x);
            JWirePanelMachineBox mbox = new JWirePanelMachineBox(machine, this);
            theLayout.registerForFontSizeChanges(mbox);

            add(mbox, new RelativePlacement(machine.xLoc, machine.yLoc, MACHINE_WIDTH, MACHINE_HEIGHT));
            machineBoxes.add(mbox);
        }

        machWires.clear();

        for (int x = 0; x < wires.size(); x++)
        {
            WireControl wc = new WireControl();
            wc.wire = wires.get(x);
            machWires.add(wc);
        }

        revalidate();
        repaint();
    }

    /**
     * Paints the component by drawing the wires and then making the machine
     * boxes paint themselves over the top of them.
     * @param g The graphics class used for drawing.
     */
    @Override
    public void paintComponent(Graphics g)
    {
        // Paint the background, clearing the screen.
        g.setColor(getBackground());
        g.fillRect(0, 0, getWidth(), getHeight());

        // Work out a radius for wire arrows and size for 'dot panel'.
        double dim = Math.max(getWidth(), getHeight());
        double arrowRadius = dim / 80;
        int dotRadius = (int) (dim / 200);
        dotPanelSize = (int) (dim / 20);

        // Paint wires onto the screen.
        for (int x = 0; x < machWires.size(); x++)
        {
            WireControl aWire = machWires.get(x);

            JWirePanelMachineBox b1 = findBox(aWire.wire.input);
            JWirePanelMachineBox b2 = findBox(aWire.wire.output);

            if (b1 != null && b2 != null)
            {
                // Get the center of both boxes.
                int x1 = b1.getX() + b1.getWidth() / 2;
                int y1 = b1.getY() + b1.getHeight() / 2;
                int x2 = b2.getX() + b2.getWidth() / 2;
                int y2 = b2.getY() + b2.getHeight() / 2;

                // Draw the line between the boxes.
                g.setColor(getForeground());
                g.drawLine(x1, y1, x2, y2);

                // Find the centre of the line.
                int cx = (x1+x2)/2;
                int cy = (y1+y2)/2;

                // Get the angle the line runs at.
                int dx = x2-x1;
                int dy = y2-y1;
                double angle = 0.0;
                angle = Math.atan2(dy, dx);

                // Find the three corners of our arrow.
                int px1 = (int) (cx + Math.cos(angle) * arrowRadius);
                int py1 = (int) (cy + Math.sin(angle) * arrowRadius);

                int px2 = (int) (cx + Math.cos(angle + DEG120) * arrowRadius);
                int py2 = (int) (cy + Math.sin(angle + DEG120) * arrowRadius);

                int px3 = (int) (cx + Math.cos(angle - DEG120) * arrowRadius);
                int py3 = (int) (cy + Math.sin(angle - DEG120) * arrowRadius);

                Polygon p = new Polygon(new int[] { px1, px2, px3 }, new int[] { py1, py2, py3 }, 3);

                // Draw different colour depending on channel.
                g.setColor(wireColors[aWire.wire.channel]);
                g.fillPolygon(p);
                aWire.poly = p;
            }
        }

        // Paint boxes over the top.
        super.paintChildren(g);

        // Paint interaction with wires.
        if (selectedWire != null)
        {
            // Paint a clear area for the panel.
            g.setColor(getBackground());
            g.fillRect(ctrlX - dotPanelSize, ctrlY - dotPanelSize, dotPanelSize*2, dotPanelSize*2);

            // Put a box around and midway lines.
            g.setColor(getForeground());
            g.drawRect(ctrlX - dotPanelSize - 1, ctrlY - dotPanelSize - 1, dotPanelSize * 2 + 2, dotPanelSize * 2 + 2);
            g.drawLine(ctrlX - dotPanelSize, ctrlY, ctrlX + dotPanelSize, ctrlY);
            g.drawLine(ctrlX, ctrlY - dotPanelSize, ctrlX, ctrlY + dotPanelSize);

            // Using left and right amplitude find the volume and panning
            // amounts on the wire.
            Point dotPos = getDotPos(selectedWire.wire.amplitudeL, selectedWire.wire.amplitudeR);

            // Draw oval for the dot.
            g.setColor(dotColor);
            g.fillOval(ctrlX + dotPos.x - dotRadius, ctrlY + dotPos.y - dotRadius, dotRadius*2, dotRadius*2);
        }

        if (newWireMachine != null)
        {
            int x1 = newWireMachine.getX() + newWireMachine.getWidth() / 2;
            int y1 = newWireMachine.getY() + newWireMachine.getHeight() / 2;
            int x2 = newWireLocX - this.getLocationOnScreen().x;
            int y2 = newWireLocY - this.getLocationOnScreen().y;

            g.setColor(getForeground());
            g.drawLine(x1, y1, x2, y2);
        }
    }

    // Calculate the position of 'the dot' given volumes for left and right channels.
    // This is for drawing the machine connection manipulation control.
    private Point getDotPos(float ampL, float ampR)
    {
        float biggest = Math.max(ampL, ampR);
        float smallest = Math.min(ampL, ampR);

        // Figure out amplitudes in DB and panning from -1 to 1;
        float dbAmount = (float) Math.log10(biggest);
        float panAmount = (biggest-smallest)/biggest;
        if (ampL > ampR) panAmount = 0.0f - panAmount;

        // Calculate position of 'the dot'.
        int posY = (int) (-dbAmount * dotPanelSize / 2.0f); // range of 20db
        int posX = (int) (panAmount * dotPanelSize);

        return new Point(posX, posY);
    }

    /**
     * We override paintChildren and do nothing since we will
     * call the superclass to do this when it suits us.
     * @param g The (unused) Graphics object used for drawing.
     */
    @Override
    public void paintChildren(Graphics g)
    {
        // Do nothing, so we don't automatically paint
        // child controls over this one.
    }

    // Finds a machinebox component given the machine in question.
    private JWirePanelMachineBox findBox(MachineBase input)
    {
        for (int x = 0; x < machineBoxes.size(); x++)
        {
            JWirePanelMachineBox t = machineBoxes.get(x);
            if (t.machine == input) return t;
        }

        return null;
    }

    /**
     * Called from JMachineBox to delete a machine from the composition.
     * @param machine The machine to be removed.
     */
    public void removeMachine(MachineBase machine)
    {
        sysRouter.removeMachine(machine);
    }

    /**
     * Called from the menu listener to add a machine to the composition.
     * @param newMachName
     * @param xLoc
     * @param yLoc
     */
    public void addMachine(String newMachName, int xLoc, int yLoc)
    {
        sysRouter.addMachine(newMachName, null, true, xLoc, yLoc, false);
    }

    /**
     * Called from JMachineBox when the user releases the mouse button
     * after having started drawing a new wire. If the mouse is released
     * over a different machine, the two machines will be connected.
     */
    public void unsetNewWire()
    {
        if (newWireMachine == null) return;

        for (int x = 0; x < machineBoxes.size(); x++)
        {
            JWirePanelMachineBox jmb = machineBoxes.get(x);

            int x1 = jmb.getLocationOnScreen().x;
            int y1 = jmb.getLocationOnScreen().y;

            int x2 = x1 + jmb.getWidth();
            int y2 = y1 + jmb.getHeight();

            if (newWireLocX >= x1 && newWireLocX <= x2
                && newWireLocY >= y1 && newWireLocY <= y2)
            {
                if (jmb != newWireMachine) sysRouter.addWire(newWireMachine.machine, jmb.machine, 1.0f, 1.0f, 0, true);
            }
        }

        newWireMachine = null;
        repaint();
    }

    /**
     * Called from JMachineBox when the box is clicked and the mouse is dragged
     * while the user holds Ctrl. This causes a line to be drawn from the box
     * to the mouse indicating the creation of a new wire.
     * @param mb The machine control where the user started dragging.
     * @param xOnScreen The current screen x location of the mouse.
     * @param yOnScreen The current screen y location of the mouse.
     */
    public void setNewWire(JWirePanelMachineBox mb, int xOnScreen, int yOnScreen)
    {
        newWireMachine = mb;
        newWireLocX = xOnScreen;
        newWireLocY = yOnScreen;
        repaint();
    }
}

/**
 * A class that wraps a wire and the polygon on screen that represents it.
 */
class WireControl
{
    public SimpleWire wire;
    public Polygon poly;
}

/**
 * An action listener that adds a machine in the place where the context menu
 * used to create the machine was shown.
 */
class JWirePanelAddListener implements ActionListener
{
    private JWirePanel wirePanel;
    private int locX, locY;

    /**
     * The constructor accepts the panel so we can reference it later.
     * @param panel The JWirePanel this listener is attached to.
     */
    public JWirePanelAddListener(JWirePanel panel)
    {
        wirePanel = panel;
    }

    /**
     * Listens for the menu firing a command to add a machine and adds it.
     * @param e The action event, which indicates which machine was selected.
     */
    @Override
    public void actionPerformed(ActionEvent e)
    {
        String machineName = e.getActionCommand();

        int totalX = wirePanel.getWidth();
        int totalY = wirePanel.getHeight();

        int percentX = locX * 1000 / totalX;
        int percentY = locY * 1000 / totalY;

        if (percentX < 0) percentX = 0;
        if (percentY < 0) percentY = 0;

        if (percentX > 1000-JWirePanel.MACHINE_WIDTH) percentX = 1000-JWirePanel.MACHINE_WIDTH;
        if (percentY > 1000-JWirePanel.MACHINE_HEIGHT) percentY = 1000-JWirePanel.MACHINE_HEIGHT;

        wirePanel.addMachine(machineName, percentX, percentY);
    }

    /**
     * Accepts the location where the menu is about to open, so we can place the
     * machine accordingly.
     * @param x The x location of the menu relative to the panel.
     * @param y The y location of the menu relative to the panel.
     */
    public void setLocation(int x, int y)
    {
        locX = x;
        locY = y;
    }
}