#include <stdio.h>
#include <math.h>
#include "Go.h"
#include "GoGlutInterface.h"

#define PI 3.14159265358979324


class Function : public GoGlutInterface
{
  public:

    //
    // The Function.
    //

    double fxy(double x, double y)
    {
        return 1.0 - cos(x*x + y*y) / (x*x + y*y);
    }

    
    // View variables.
    double   eyeX, eyeY, eyeZ;
    double   centerX, centerY, centerZ;
    double   upX, upY, upZ;
    double   radius;
    boolean  perspective;
    
    // Function data
    int triangleStripNum;
    int lineStripNum;
    GoVertex **triangleStripData;
    GoVertex **lineStripData;
    double normal_[3];
    double zClampMin, zClampMax;
    int drawMode;

    // Box data
    GoVertex *top;
    GoVertex *bottom;
    GoVertex *sides;

    // Rotate vector
    double rX, rY, rZ;
    int rotateAbout;
    int rotateAs;

    // Values for rotateAbout
    enum {
        X_AXIS,
        Y_AXIS,
        Z_AXIS
    };

    // Values for rotateAs
    enum {
        SOLID,
        WIRE,
        BOX
    };

    // Class temporary variables
    GoMatrix modelview;
    GoMatrix saveModelview;
    int xStart, yStart;
    double angleStart;


    //
    // Constructor.
    //
    Function(double xMin, double xMax,
             double yMin, double yMax,
             double zMin, double zMax,
             int xDiv, int yDiv,
             double eyeX, double eyeY, double eyeZ,
             double upX, double upY, double upZ,
             boolean perspective)
    {
        //
        // Create the function and bounding box.
        //

        createFunction(xMin, xMax, yMin, yMax, zMin, zMax, xDiv, yDiv);
        createBox(xMin, yMin, zMin, xMax, yMax, zMax);

        //
        // Default drawing mode.
        //

        drawMode = SOLID;
        rotateAbout = Y_AXIS;
        rotateAs = WIRE;

        //
        // Setup a light.
        //
        
        go->light(Go::LIGHT_0, true);
        go->light(Go::LIGHT_0, Go::DIRECTIONAL, -1.3, 2.4, 1.0);
        
        //
        // Define the initial view.
        // NOTE: View variables are used in the resized() method.
        //
        
        this->eyeX  = eyeX;
        this->eyeY  = eyeY;
        this->eyeZ  = eyeZ;
        centerX     = (xMin + xMax) / 2.0;
        centerY     = (yMin + yMax) / 2.0;
        centerZ     = (zMin + zMax) / 2.0;
        this->upX   = upX;
        this->upY   = upY;
        this->upZ   = upZ;
        radius      = distance(centerX, centerY, centerZ,
                               xMax, yMax, zMax);
        this->perspective = perspective;
        
        //
        // Initialize the modelview matrix.
        //
        
        go->lookAt(eyeX, eyeY, eyeZ,
                   centerX, centerY, centerZ,
                   upX, upY, upZ);
    }

    //
    // Interface render method.
    //
    void render(void)
    {
        int i;

        go->clear(Go::IMAGE);

        //
        // Render function.
        //

        switch(drawMode) {

        case SOLID:
            go->color(1, 1, 1);  // white
            for(i = 0; i < triangleStripNum; i++) {
                go->render(triangleStripData[i]);
            }
            
            go->color(1, 0, 0);  // red
            for(i = 0; i < lineStripNum; i++) {
                go->render(lineStripData[i]);
            }
            break;

        case WIRE:
            go->color(1, 0, 0);  // red
            for(i = 0; i < lineStripNum; i++) {
                go->render(lineStripData[i]);
            }
            break;
        }

        //
        // Render box.
        //

        go->color(0, 0, 0);  // black
        go->render(top);
        go->render(bottom);
        go->render(sides);

        swap();
    }

    //
    // Interface resized method.
    //
    void resized(int width, int height)
    {
        double dx = eyeX - centerX;
        double dy = eyeY - centerY;
        double dz = eyeZ - centerZ;
        double d = sqrt(dx * dx + dy * dy + dz * dz);
    
        double fovy = 2.0 * 180.0/PI * atan(radius / d);
        double near = d - radius;
        double far  = d + radius;
    
        double xUnits, yUnits;
    
        if(width >= height) {
            xUnits = 2 * radius * width / height;
            yUnits = 2 * radius;
        }
        else {
            xUnits = 2 * radius;
            yUnits = 2 * radius * height / width;
        }
    
        // setup projection matrix
        go->matrixMode(Go::PROJECTION);
        go->identity();
        if(perspective) {
            if(width >= height) {
                go->perspective(fovy, xUnits / yUnits, near, far);
            }
            else {
                double radiusModified = radius * yUnits / xUnits;
                double fovyModified = atan(radiusModified / d) * 180.0 / PI * 2.0;
                go->perspective(fovyModified, xUnits / yUnits, near, far);
            }
        }
        else {
            go->ortho(-xUnits / 2.0, xUnits / 2.0,
                      -yUnits / 2.0, yUnits / 2.0,
                      near, far);
        }
        go->matrixMode(Go::MODELVIEW);
    }

    //
    // Interface keyTyped method.
    //
    void keyTyped(char key)
    {
        if(key == 's') {
            rotateAs = SOLID;
        }
        else if(key == 'w') {
            rotateAs = WIRE;
        }
        else if(key == 'b') {
            rotateAs = BOX;
        }
        else if(key == '1') {
            rotateAbout = X_AXIS;
        }
        else if(key == '2') {
            rotateAbout = Y_AXIS;
        }
        else if(key == '3') {
            rotateAbout = Z_AXIS;
        }
    }

    //
    // Interface mousePressed method.
    //
    void mousePressed(int x, int y)
    {
        int width = go->width();
        int height = go->height();

        //
        // Init mouse info.
        // 
        
        xStart = x;
        yStart = y;
        angleStart = atan2(height / 2.0 - yStart,
                           xStart - width / 2.0);
        
        //
        // Save the current modelview matrix so that
        // it can be restored as the mouse is moved.
        //
        
        go->getModelview(&saveModelview);

        //
        // Redisplay in rotateAs mode.
        //
        
        switch(rotateAs) {
            
        case SOLID:
            drawMode = SOLID;
            break;
            
        case WIRE:
            drawMode = WIRE;
            break;
            
        case BOX:
            drawMode = BOX;
            break;
        }
        
        rerender();

        //
        // Calc the rotation vector <rX, rY, rZ>
        //
        
        double x0 = 0.0;
        double y0 = 0.0;
        double z0 = 0.0;
        double x1;
        double y1;
        double z1;
        
        switch(rotateAbout) {
            
        case X_AXIS:
            x1 = 1.0;
            y1 = 0.0;
            z1 = 0.0;
            break;
            
        default:
        case Y_AXIS:
            x1 = 0.0;
            y1 = 1.0;
            z1 = 0.0;
            break;
            
        case Z_AXIS:
            x1 = 0.0;
            y1 = 0.0;
            z1 = 1.0;
            break;
        }

        go->push(Go::MODELVIEW);
        go->inverse();
        go->getModelview(&modelview);
        go->pop(Go::MODELVIEW);
        
        double *m = modelview.m;
        
        double w = m[3] * x0 + m[7] * y0 + m[11] * z0 + m[15];
        double x0N = (m[0] * x0 + m[4] * y0 + m[8]  * z0 + m[12]) / w;
        double y0N = (m[1] * x0 + m[5] * y0 + m[9]  * z0 + m[13]) / w;
        double z0N = (m[2] * x0 + m[6] * y0 + m[10] * z0 + m[14]) / w;
        
        w = m[3] * x1 + m[7] * y1 + m[11] * z1 + m[15];
        double x1N = (m[0] * x1 + m[4] * y1 + m[8]  * z1 + m[12]) / w;
        double y1N = (m[1] * x1 + m[5] * y1 + m[9]  * z1 + m[13]) / w;
        double z1N = (m[2] * x1 + m[6] * y1 + m[10] * z1 + m[14]) / w;
        
        rX = x1N - x0N;
        rY = y1N - y0N;
        rZ = z1N - z0N;
    }

    //
    // Interface mouseReleased method.
    //
    void mouseReleased(int x, int y)
    {
        drawMode = SOLID;
        rerender();
    }

    //
    // Interface mouseDragged method.
    //
    void mouseDragged(int x, int y)
    {
        int xEnd = x;
        int yEnd = y;

        int width = go->width();
        int height = go->height();
                    
        go->load(&saveModelview);
    
        double delta;
    
        switch(rotateAbout) {
        
        case X_AXIS:
            delta = (yEnd - yStart) / 2.0;
            break;
        
        default:
        case Y_AXIS:
            delta = (xEnd - xStart) / 2.0;
            break;
        
        case Z_AXIS:
            double angleEnd = atan2(height / 2.0 - yEnd,
                                    xEnd - width / 2.0);
            delta = (angleEnd - angleStart) * 180.0 / PI;
            break;
        }
    
        go->translate(centerX, centerY, centerZ);
        go->rotate(delta, rX, rY, rZ);
        go->translate(-centerX, -centerY, -centerZ);
    
        rerender();
    }

    //
    // Clamp function to Z range.
    //
    double fxyClamp(double x, double y)
    {
        double zVal = fxy(x, y);

        //
        // Chop off anything less than zMin or greater than zMax.
        //

        if(zVal < zClampMin) {
            return zClampMin;
        }
        else if(zVal > zClampMax) {
            return zClampMax;
        }
        else {
            return zVal;
        }
    }

    //
    // Derivative of function with respect to x.  (used to calc normals)
    //
    double dfxy_dx(double x, double y)
    {
        double delta = 0.0000001;
        
        return 1.0/(2.0 * delta) * (fxy(x + delta, y) - fxy(x - delta, y));
    }

    //
    // Derivative of function with respect to y.  (used to calc normals)
    //
    double dfxy_dy(double x, double y)
    {
        double delta = 0.0000001;
        
        return 1.0/(2.0 * delta) * (fxy(x, y + delta) - fxy(x, y - delta));
    }

    //
    // Normal vector at f(x, y)
    //
    void normal(double x, double y, double *n)
    {
        double dx = dfxy_dx(x, y);
        double dy = dfxy_dy(x, y);
        double den = sqrt(1.0 + dx * dx + dy * dy);

        n[0] = dx / den;
        n[1] = dy / den;
        n[2] = 1.0 / den;
    }

    //
    // Function creation.
    //
    void createFunction(double xMin, double xMax,
                        double yMin, double yMax,
                        double zMin, double zMax,
                        int xDiv, int yDiv)
    {
        int     i, j, count;
        double  x, y;
        double  xInc = (xMax - xMin) / xDiv;
        double  yInc = (yMax - yMin) / yDiv;

        triangleStripNum = xDiv;
        lineStripNum     = xDiv + yDiv + 2;
        triangleStripData = new GoVertex * [triangleStripNum];
        lineStripData = new GoVertex * [lineStripNum];


        //
        // Save Z clamp values for use in fxyClamp().
        //

        zClampMin = zMin;
        zClampMax = zMax;

        //
        // Calc lineStripData.
        //

        count = 0;

        for(i = 0, x = xMin;
            i <= xDiv;
            i++, x += xInc, count++) {
            
            lineStripData[count] = new GoLineStrip(yDiv + 1);
            
            for(j = 0, y = yMin;
                j < yDiv + 1;
                j++, y += yInc) {
                
                lineStripData[count]->xyz(j, x, y, fxyClamp(x, y));
            }
        }

        for(i = 0, y = yMin;
            i <= yDiv;
            i++, y += yInc, count++) {
            
            lineStripData[count] = new GoLineStrip(xDiv + 1);
            
            for(j = 0, x = xMin;
                j < xDiv + 1;
                j++, x += xInc) {
                
                lineStripData[count]->xyz(j, x, y, fxyClamp(x, y));
            }
        }

        //
        // Calc triangleStripData.
        //

        count = 0;

        for(i = 0, x = xMin;
            i < xDiv;
            i++, x += xInc, count++) {
            
            triangleStripData[count] = new GoTriangleStrip(yDiv * 2 + 2, Go::NORMAL);

            triangleStripData[count]->xyz(0, x, yMin, fxyClamp(x, yMin));
            normal(x, yMin, normal_);
            triangleStripData[count]->ijk(0, normal_[0], normal_[1], normal_[2]);

            triangleStripData[count]->xyz(1, x + xInc, yMin, fxyClamp(x + xInc, yMin));
            normal(x + xInc, yMin, normal_);
            triangleStripData[count]->ijk(1, normal_[0], normal_[1], normal_[2]);
            
            for(j = 1, y = yMin + yInc;
                j <= yDiv;
                j++, y += yInc) {
                
                triangleStripData[count]->xyz(j * 2, x, y, fxyClamp(x, y));
                normal(x, y, normal_);
                triangleStripData[count]->ijk(j * 2, normal_[0], normal_[1], normal_[2]);
                
                triangleStripData[count]->xyz(j * 2 + 1, x + xInc, y, fxyClamp(x + xInc, y));
                normal(x + xInc, y, normal_);
                triangleStripData[count]->ijk(j * 2 + 1, normal_[0], normal_[1], normal_[2]);
            }
        }
    }

    //
    // Box creation.
    //
    void createBox(double xMin, double yMin, double zMin,
                   double xMax, double yMax, double zMax)
    {
        top = new GoLineLoop(4);
        top->xyz(0, xMin, yMax, zMin);
        top->xyz(1, xMin, yMax, zMax);
        top->xyz(2, xMax, yMax, zMax);
        top->xyz(3, xMax, yMax, zMin);

        bottom = new GoLineLoop(4);
        bottom->xyz(0, xMin, yMin, zMin);
        bottom->xyz(1, xMin, yMin, zMax);
        bottom->xyz(2, xMax, yMin, zMax);
        bottom->xyz(3, xMax, yMin, zMin);

        sides = new GoLines(8);
        sides->xyz(0, xMin, yMin, zMin);
        sides->xyz(1, xMin, yMax, zMin);
        sides->xyz(2, xMax, yMin, zMin);
        sides->xyz(3, xMax, yMax, zMin);
        sides->xyz(4, xMin, yMin, zMax);
        sides->xyz(5, xMin, yMax, zMax);
        sides->xyz(6, xMax, yMin, zMax);
        sides->xyz(7, xMax, yMax, zMax);
    }

    //
    // Calculate distance between two points.
    //
    double distance(double x0, double y0, double z0,
                    double x1, double y1, double z1)
    {
        double dx = x1 - x0;
        double dy = y1 - y0;
        double dz = z1 - z0;
        
        return sqrt(dx*dx + dy*dy + dz*dz);
    }
};

void instructions()
{
    printf("\n");
    printf("Rotate about X - press \"1\"\n");
    printf("Rotate about Y - press \"2\"\n");
    printf("Rotate about Z - press \"3\"\n");
    printf("\n");
    printf("Rotate as Solid - press \"s\"\n");
    printf("Rotate as Wire  - press \"w\"\n");
    printf("Rotate as Box   - press \"b\"\n");
}

int main(int argc, char *argv[])
{
    instructions();

    //
    // Glut setup.
    //

    GoGlutInterface::init(&argc, argv);
    GoGlutInterface::windowName("Rotate");
    GoGlutInterface::windowSize(350, 350);

    //
    // Create the function to be displayed.
    //
    
    Function *function = new Function(-3.0, 3.0,      // X range
                                      -3.0, 3.0,      // Y range
                                      -3.5, 1.5,      // Z clamp range
                                      18, 18,         // X-Y fish-net resolution
                                      3.9, -7.2, 6.0, // Eye location
                                      0.0, 0.0, 1.0,  // Up vector
                                      true);          // Perspective

    GoGlutInterface::mainLoop();
    
    return(0);
}