/*
  <APPLET NAME = "Grapher" CODEBASE = "." CODE = "Grapher.class" 
  WIDTH = 400 HEIGHT = 400>
  </APPLET>

 */

import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.util.*;

public class Grapher extends Applet
{
    // UI Elements
    private String NSPLINE_NAME = "Natural Spline";
    private String HERMITE_NAME = "Hermite Curve";
    private String BEZIER_NAME = "Bezier Curve";
    private String selected = NSPLINE_NAME;

    private CheckboxGroup curveType = new CheckboxGroup (  );
    private Checkbox nSpline = new Checkbox(NSPLINE_NAME, curveType, true);
    private Checkbox hermite = new Checkbox(HERMITE_NAME, curveType, false);
    private Checkbox bezier = new Checkbox(BEZIER_NAME, curveType, false);
    
    static final int CIRCLE_WIDTH = 4;
    int width;
    int height;

    // Define Gx, Gy -- The geometry vectors
    int[] gx = new int[4];
    int[] gy = new int[4];

    int draggingNode = -1;

    public void init()
    {
        setBackground(new Color(255, 255, 255));

        // Add our UI elements
        add(nSpline);
        add(hermite);
        add(bezier);

        width = getSize().width;
        height = getSize().height;

        // Fake some important control points to start.
        // ie: Controllable in all spline types
        gx[0] = 210; gx[1] = 215; gx[2] = 240; gx[3] = 242;
        gy[0] = 147; gy[1] = 165; gy[2] = 165; gy[3] = 148;

        // Add the action listeners
        addMouseListener(new ClickClass());
        addMouseMotionListener(new DragClass());
        nSpline.addItemListener(new RadioClass());
        hermite.addItemListener(new RadioClass());
        bezier.addItemListener(new RadioClass());
    }

    public void paint(Graphics g)
    {
        // Display the geometry vectors
        writeG(g);

        // Draw the curve and basis functions
        if(curveType.getSelectedCheckbox() == nSpline)
        {
            drawNSpline(g);
            drawNaturalBasisFunctions(g);
        }
        else if(curveType.getSelectedCheckbox() == hermite)
        {
            drawHermite(g);
            drawHermiteBasisFunctions(g);
        }
        else if(curveType.getSelectedCheckbox() == bezier)
        {
            drawBezier(g);
            drawBezierBasisFunctions(g);
        }
    }

    // Draw a natural spline curve, as defined by the geometry vectors gx[], 
    // gy[], and the basis functions f1(), f2(), f3(), and f4()
    void drawNSpline(Graphics g)
    {
        int newX = 0;
        int newY = 0;
        int oldX = 0;
        int oldY = 0;

        // Draw the control points
        g.setColor(new Color(255, 0, 0));

        g.drawOval(gx[0], (height - gy[0]), CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("0", gx[0], (height - gy[0]));
        g.drawOval(gx[1], (height - gy[1]), CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("1", gx[1], (height - gy[1]));
        g.drawOval(gx[2], (height - gy[2]), CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("2", gx[2], (height - gy[2]));
        g.drawOval(gx[3], (height - gy[3]), CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("3", gx[3], (height - gy[3]));

        // Draw the curve
        g.setColor(new Color(0, 0, 0));
        for(double t = 0; t <= 1.02; t += 0.02)
        {
            // Find the X coordinate, as influenced by the
            // basis functions
            newX = (int) ((f1(t, gx) * gx[0])
                          + (f2(t, gx) * gx[1])
                          + (f3(t, gx) * gx[2])
                          + (f4(t, gx) * gx[3]));

            // Find the Y coordinate, as influenced by the 
            // basis functions
            newY = (int) ((f1(t, gy) * gy[0])
                          + (f2(t, gy) * gy[1])
                          + (f3(t, gy) * gy[2])
                          + (f4(t, gy) * gy[3]));

            if(t != 0)
            {
                // drawLine gives you a much smoother curve than a bunch
                // of points
                g.drawLine(oldX, (height -oldY), newX, (height -newY));
            }

            oldX = newX;
            oldY = newY;
        }
    }

    // Draw a Hermite curve, as defined by the geometry vectors gx[], gy[],
    // and the basis functions f1(), f2(), f3(), and f4()
    void drawHermite(Graphics g)
    {
        int newX = 0;
        int newY = 0;
        int oldX = 0;
        int oldY = 0;

        // Draw the control points
        g.setColor(new Color(255, 0, 0));

        g.drawOval(gx[0], (height - gy[0]), CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("0", gx[0], (height - gy[0]));
        g.drawOval(gx[1], (height - gy[1]), CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("1", gx[1], (height - gy[1]));
        g.drawOval((gx[0] + gx[2]), (height - (gy[0] + gy[2])), 
                   CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("2", (gx[0] + gx[2]), (height - (gy[0] + gy[2])));
        g.drawOval((gx[1] + gx[3]), (height - (gy[1] + gy[3])), 
                   CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("3", (gx[1] + gx[3]), (height - (gy[1] + gy[3])));

        // Draw the lines for the tangent vector
        g.setColor(new Color(200, 200, 200));
        g.drawLine(gx[0], (height - gy[0]), (gx[0] + gx[2]), 
                   (height - (gy[0] + gy[2])));
        g.drawLine(gx[1], (height - gy[1]), (gx[1] + gx[3]), 
                   (height - (gy[1] + gy[3])));

        // Draw the curve
        g.setColor(new Color(0, 0, 0));
        for(double t = 0; t <= 1.02; t += 0.02)
        {
            // For these next computations, we control the 2nd and 3rd
            // Geometry entries (tangents at P0 and P4) to be the 
            // vector between (P0, P2) and (P1, P3), respectivey.

            // Find the X coordinate, as influenced by the
            // basis functions
            newX = (int) ((fh1(t, gx) * gx[0])
                          + (fh2(t, gx) * gx[1])
                          + (fh3(t, gx) * gx[2])
                          + (fh4(t, gx) * gx[3]));

            // Find the Y coordinate, as influenced by the 
            // basis functions
            newY = (int) ((fh1(t, gy) * gy[0])
                          + (fh2(t, gy) * gy[1])
                          + (fh3(t, gy) * gy[2])
                          + (fh4(t, gy) * gy[3]));

            if(t != 0)
            {
                // drawLine gives you a much smoother curve than a bunch
                // of points
                g.drawLine(oldX, (height -oldY), newX, (height -newY));
            }

            oldX = newX;
            oldY = newY;
        }
    }

    // Draw a Bezier curve, as defined by the geometry vectors gx[], gy[],
    // and the basis functions f1(), f2(), f3(), and f4()
    void drawBezier(Graphics g)
    {
        int newX = 0;
        int newY = 0;
        int oldX = 0;
        int oldY = 0;

        // Draw the control points
        g.setColor(new Color(255, 0, 0));

        g.drawOval(gx[0], (height - gy[0]), CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("0", gx[0], (height - gy[0]));
        g.drawOval(gx[1], (height - gy[1]), CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("1", gx[1], (height - gy[1]));
        g.drawOval(gx[2], (height - gy[2]), CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("2", gx[2], (height - gy[2]));
        g.drawOval(gx[3], (height - gy[3]), CIRCLE_WIDTH, CIRCLE_WIDTH);
        g.drawString("3", gx[3], (height - gy[3]));

        // Draw the lines for the control points
        g.setColor(new Color(200, 200, 200));
        g.drawLine(gx[0], (height - gy[0]), gx[1], (height - gy[1]));
        g.drawLine(gx[1], (height - gy[1]), gx[2], (height - gy[2]));
        g.drawLine(gx[2], (height - gy[2]), gx[3], (height - gy[3]));

        // Draw the curve
        g.setColor(new Color(0, 0, 0));
        for(double t = 0; t <= 1.02; t += 0.02)
        {
            // For these next computations, we control the 2nd and 3rd
            // Geometry entries (tangents at P0 and P4) to be the 
            // vector between (P0, P2) and (P1, P3), respectivey.

            // Find the X coordinate, as influenced by the
            // basis functions
            newX = (int) ((fb1(t, gx) * gx[0])
                          + (fb2(t, gx) * gx[1])
                          + (fb3(t, gx) * gx[2])
                          + (fb4(t, gx) * gx[3]));

            // Find the Y coordinate, as influenced by the 
            // basis functions
            newY = (int) ((fb1(t, gy) * gy[0])
                          + (fb2(t, gy) * gy[1])
                          + (fb3(t, gy) * gy[2])
                          + (fb4(t, gy) * gy[3]));

            if(t != 0)
            {
                // drawLine gives you a much smoother curve than a bunch
                // of points
                g.drawLine(oldX, (height -oldY), newX, (height -newY));
            }

            oldX = newX;
            oldY = newY;
        }
    }


    void drawNaturalBasisFunctions(Graphics g)
    {
        int newX = 0;
        int[] newY = {0, 0, 0, 0};
        int oldX = 0;
        int[] oldY = {0, 0, 0, 0};

        // Draw the axis
        g.drawLine(1, (height / 2), width, (height / 2));

        for(double t = 0; t <= 1.05; t += 0.05)
        {
            newX = (int) (width * t);

            newY[0] = (int) ((f1(t, gx) * (height / 2)) + (height / 2));
            newY[1] = (int) ((f2(t, gx) * (height / 2)) + (height / 2));
            newY[2] = (int) ((f3(t, gx) * (height / 2)) + (height / 2));
            newY[3] = (int) ((f4(t, gx) * (height / 2)) + (height / 2));

            if(t != 0)
            {
                Color c = null;

                for(int x = 0; x < 4; x++)
                {
                    switch(x)
                    {
                        case 0: c = new Color(128, 128, 0); break;
                        case 1: c = new Color(255, 0, 0); break;
                        case 2: c = new Color(0, 255, 0); break;
                        case 3: c = new Color(0, 0, 255); break;
                    }
                    
                    g.setColor(c);
                    g.drawLine(oldX, (height - oldY[x]), newX, 
                               (height - newY[x]));
                }
            }

            oldX = newX;

            for(int x = 0; x < 4; x++)
                oldY[x] = newY[x];
        }
    }

    // Draw Hermite Functions
    void drawHermiteBasisFunctions(Graphics g)
    {
        int newX = 0;
        int[] newY = {0, 0, 0, 0};
        int oldX = 0;
        int[] oldY = {0, 0, 0, 0};

        // Draw the axis
        g.drawLine(1, (height / 2), width, (height / 2));

        for(double t = 0; t <= 1.05; t += 0.05)
        {
            newX = (int) (width * t);

            newY[0] = (int) ((fh1(t, gx) * (height / 2)) + (height / 2));
            newY[1] = (int) ((fh2(t, gx) * (height / 2)) + (height / 2));
            newY[2] = (int) ((fh3(t, gx) * (height / 2)) + (height / 2));
            newY[3] = (int) ((fh4(t, gx) * (height / 2)) + (height / 2));

            if(t != 0)
            {
                Color c = null;

                for(int x = 0; x < 4; x++)
                {
                    switch(x)
                    {
                        case 0: c = new Color(128, 128, 0); break;
                        case 1: c = new Color(255, 0, 0); break;
                        case 2: c = new Color(0, 255, 0); break;
                        case 3: c = new Color(0, 0, 255); break;
                    }
                    
                    g.setColor(c);
                    g.drawLine(oldX, (height - oldY[x]), newX, 
                               (height - newY[x]));
                }
            }

            oldX = newX;

            for(int x = 0; x < 4; x++)
                oldY[x] = newY[x];
        }
    }

    void drawBezierBasisFunctions(Graphics g)
    {
        int newX = 0;
        int[] newY = {0, 0, 0, 0};
        int oldX = 0;
        int[] oldY = {0, 0, 0, 0};

        // Draw the axis
        g.drawLine(1, (height / 2), width, (height / 2));

        for(double t = 0; t <= 1.05; t += 0.05)
        {
            newX = (int) (width * t);

            newY[0] = (int) ((fb1(t, gx) * (height / 2)) + (height / 2));
            newY[1] = (int) ((fb2(t, gx) * (height / 2)) + (height / 2));
            newY[2] = (int) ((fb3(t, gx) * (height / 2)) + (height / 2));
            newY[3] = (int) ((fb4(t, gx) * (height / 2)) + (height / 2));

            if(t != 0)
            {
                Color c = null;

                for(int x = 0; x < 4; x++)
                {
                    switch(x)
                    {
                        case 0: c = new Color(128, 128, 0); break;
                        case 1: c = new Color(255, 0, 0); break;
                        case 2: c = new Color(0, 255, 0); break;
                        case 3: c = new Color(0, 0, 255); break;
                    }
                    
                    g.setColor(c);
                    g.drawLine(oldX, (height - oldY[x]), newX, 
                               (height - newY[x]));
                }
            }

            oldX = newX;

            for(int x = 0; x < 4; x++)
                oldY[x] = newY[x];
        }
    }

    ////////////////////////////////
    // Natural Spline basis functions
    ////////////////////////////////
    double f1(double t, int[] g)
    {
        return (-9.0/2.0) * Math.pow(t, 3)
            + (9.0/1.0) * Math.pow(t,2)
            + (-11.0/2.0) * Math.pow(t,1)
            + 1;
    }

    double f2(double t, int[] g)
    {
        return (27.0/2.0) * Math.pow(t, 3)
            + (-45.0/2.0) * Math.pow(t,2)
            + (9.0/1.0) * Math.pow(t,1);
    }

    double f3(double t, int[] g)
    {
        return (-27.0/2.0) * Math.pow(t, 3)
            + (18.0/1.0) * Math.pow(t,2)
            + (-9.0/2.0) * Math.pow(t,1);
    }

    double f4(double t, int[] g)
    {
        return (9.0/2.0) * Math.pow(t, 3)
            + (-9.0/2.0) * Math.pow(t,2)
            + (1.0/1.0) * Math.pow(t,1);
    }

    ////////////////////////////////
    // Hermite basis functions
    ////////////////////////////////
    double fh1(double t, int[] g)
    {
        return (2.0) * Math.pow(t, 3)
            + (-3.0) * Math.pow(t,2)
            + (0) * Math.pow(t,1)
            + 1;
    }

    double fh2(double t, int[] g)
    {
        return (-2.0) * Math.pow(t, 3)
            + (3.0) * Math.pow(t,2)
            + (0) * Math.pow(t,1)
            + 0;
    }

    double fh3(double t, int[] g)
    {
        return (1.0) * Math.pow(t, 3)
            + (-2.0) * Math.pow(t,2)
            + (1) * Math.pow(t,1)
            + 0;
    }

    double fh4(double t, int[] g)
    {
        return (1.0) * Math.pow(t, 3)
            + (-1.0) * Math.pow(t,2)
            + (0) * Math.pow(t,1)
            + 0;
    }
    
    ////////////////////////////////
    // Bezier Spline basis functions
    ////////////////////////////////
    double fb1(double t, int[] g)
    {
        return Math.pow((1.0 - t), 3);
    }

    double fb2(double t, int[] g)
    {
        return (3.0 * t * Math.pow((1.0 - t), 2));
    }

    double fb3(double t, int[] g)
    {
        return (3.0 * Math.pow(t, 2) * (1.0 - t));
    }

    double fb4(double t, int[] g)
    {
        return Math.pow(t, 3);
    }


    // Our "Mouse Click" listener
    class ClickClass implements MouseListener
    {
        // The mouse enters the component (do nothing)
        public void mouseEntered(MouseEvent e) {}

        // The mouse exits the component (do nothing)
        public void mouseExited(MouseEvent e) {}

        // When the user clicks the mouse (do nothing)
        public void mouseClicked(MouseEvent e) {}

        // When the user holds the mouse
        public void mousePressed(MouseEvent e)
        {
            // This set of crazy IF statements check that we've clicked 
            // within the bounding box around the circle.  However, it gets 
            // more complicated then neccessary because the applet draws
            // in "graph coordinates" (ie: 0,0 at bottom, left) instead of 
            // monitor coordinates.  That's why all of the y coordinates get
            // replaced with (height -e.getY())

            // Hold fake testing points -- especially for the hermite
            // tangent controls
            int testX = e.getX();
            int testY = e.getY();

            for(int x = 0; x < 4; x++)
            {
                draggingNode = -1;
            
                // If we're in "Hermite" mode, update testX and testY
                // if they're dragging a tangent control
                if(selected == HERMITE_NAME)
                {
                    // Check that they're dragging the first control
                    int tempX = (gx[0] + gx[2]);
                    int tempY = (gy[0] + gy[2]);

                    if( (testX >= (tempX - CIRCLE_WIDTH)) && 
                        (testX <= (tempX + CIRCLE_WIDTH)) &&
                        (testY >= ((height -tempY) - CIRCLE_WIDTH)) &&
                        (testY <= ((height -tempY) + CIRCLE_WIDTH)))
                    {
                        testX = gx[2];
                        testY = gx[2];
                        draggingNode = 2;
                        break;
                    }

                    tempX = (gx[1] + gx[3]);
                    tempY = (gy[1] + gy[3]);

                    // Check that they're dragging the second control
                    if( (testX >= (tempX - CIRCLE_WIDTH)) && 
                        (testX <= (tempX + CIRCLE_WIDTH)) &&
                        (testY >= ((height -tempY) - CIRCLE_WIDTH)) &&
                        (testY <= ((height -tempY) + CIRCLE_WIDTH)))
                    {
                        testX = gx[3];
                        testY = gx[3];
                        draggingNode = 3;
                        break;
                    }
                }

                if(draggingNode < 0)
                {
                    if( (gx[x] >= (testX - CIRCLE_WIDTH)) && 
                        (gx[x] <= (testX + CIRCLE_WIDTH)) &&
                        (gy[x] >= ((height -testY) - CIRCLE_WIDTH)) &&
                        (gy[x] <= ((height -testY) + CIRCLE_WIDTH)))
                    {
                        draggingNode = x;
                        break;
                    }
                }
            }
        }
        
        // When they release it
        public void mouseReleased(MouseEvent e)
        {
            draggingNode = -1;
        }
    }

    // Our "Mouse Drag" listener
    class DragClass implements MouseMotionListener
    {
        // A mouse move ... do nothing
        public void mouseMoved(MouseEvent e) {}

        // A mouse drag
        public void mouseDragged(MouseEvent e)
        {
            // If we're dragging a node, then update its coordinates
            if(draggingNode >= 0)
            {
                // If they're dragging a tangent point
                if((selected == HERMITE_NAME) && (draggingNode == 2))
                {
                    gx[draggingNode] = e.getX() - gx[0];
                    gy[draggingNode] = (height -e.getY()) - gy[0];
                }
                else if((selected == HERMITE_NAME) && (draggingNode == 3))
                {
                    gx[draggingNode] = e.getX() - gx[1];
                    gy[draggingNode] = (height -e.getY()) - gy[1];
                }
                else
                {
                    gx[draggingNode] = e.getX();
                    gy[draggingNode] = (height -e.getY());
                }
                repaint();
            }
        }
    }

    // Our "Radio Button Change" listener
    class RadioClass implements ItemListener
    {
        // An item state change
        public void itemStateChanged(ItemEvent e)
        {
            // Convert whatever curve type to the "natural" form.
            if(selected == HERMITE_NAME)
            {
                gx = convertHermToNatural(gx);
                gy = convertHermToNatural(gy);
            }
            else if(selected == BEZIER_NAME)
            {
                gx = convertBezierToNatural(gx);
                gy = convertBezierToNatural(gy);
            }

            // Convert the "natural geometry" to the
            // specified one.
            if(e.getItem() == NSPLINE_NAME)
            {
                selected = NSPLINE_NAME;
            }

            else if(e.getItem() == HERMITE_NAME)
            {
                selected = HERMITE_NAME;
                
                gx = convertNaturalToHerm(gx);
                gy = convertNaturalToHerm(gy);
            }

            else if(e.getItem() == BEZIER_NAME)
            {
                selected = BEZIER_NAME;

                gx = convertNaturalToBezier(gx);
                gy = convertNaturalToBezier(gy);
            }

            repaint();
        }
    }

    void writeG(Graphics g)
    {
        g.drawString("X Geometry Vector: [" + gx[0] + ","  + gx[1] 
                     + ","  + gx[2] + ","  + gx[3] + "]", 1, height - 30);
        g.drawString("Y Geometry Vector: [" + gy[0] + ","  + gy[1] 
                     + ","  + gy[2] + ","  + gy[3] + "]", 1, height - 15);
    }

    // The following "conversion functions" are simply unrolled matrix 
    // multiplications.  Not done for efficiency, but to save us from 
    // matrix multiplication

    ////////////////////
    // Hermite Functions
    ////////////////////
    // Convert natural geometry to hermite
    int[] convertNaturalToHerm(int[] g)
    {
        int[] newg = new int[4];

        newg[0] = g[0];
        newg[1] = g[3];
        newg[2] = (int) ((-11.0/2.0 * g[0]) + (9.0 * g[1]) 
                         + (-9.0/2.0 * g[2]) + (1 * g[3]));
        newg[3] = (int) ((-1.0 * g[0]) + (9.0/2.0 * g[1]) 
                         + (-9.0 * g[2]) + (11.0/2.0 * g[3]));

        return newg;
    }

    // Convert hermite geometry to natural
    int[] convertHermToNatural(int[] g)
    {
        int[] newg = new int[4];

        newg[0] = (int) (1 * g[0]);
        newg[1] = (int) ((20.0/27.0 * g[0]) + (7.0/27.0 * g[1]) 
                         + (4.0/27.0 * g[2]) + (-2.0/27.0 * g[3]));
        newg[2] = (int) ((7.0/27.0 * g[0]) + (20.0/27.0 * g[1]) 
                         + (2.0/27.0 * g[2]) + (-4.0/27.0 * g[3]));
        newg[3] = (int) (1 * g[1]);

        return newg;
    }

    ////////////////////
    // Bezier Functions
    ////////////////////
    // Convert natural geometry to bezier
    int[] convertNaturalToBezier(int[] g)
    {
        int[] newg = new int[4];

        newg[0] = g[0];
        newg[1] = (int) ((-5.0/6.0 * g[0]) + (3.0 * g[1]) 
                         + (-3.0/2.0 * g[2]) + (1.0/3.0 * g[3]));
        newg[2] = (int) ((1.0/3.0 * g[0]) + (-3.0/2.0 * g[1]) 
                         + (3.0 * g[2]) + (-5.0/6.0 * g[3]));
        newg[3] = g[3];

        return newg;
    }

    // Convert bezier geometry to natural
    int[] convertBezierToNatural(int[] g)
    {
        int[] newg = new int[4];

        newg[0] = g[0];
        newg[1] = (int) ((8.0/27.0 * g[0]) + (4.0/9.0 * g[1]) 
                         + (2.0/9.0 * g[2]) + (1.0/27.0 * g[3]));
        newg[2] = (int) ((1.0/27.0 * g[0]) + (2.0/9.0 * g[1]) 
                         + (4.0/9.0 * g[2]) + (8.0/27.0 * g[3]));
        newg[3] = g[3];

        return newg;
    }
}

