/*
Copyright (C) 2001, 2006 United States Government
as represented by the Administrator of the
National Aeronautics and Space Administration.
All Rights Reserved.
*/
package gov.nasa.worldwind;

import gov.nasa.worldwind.geom.*;

import javax.media.opengl.*;
import javax.media.opengl.glu.*;

/**
 * @author tag
 * @version $Id$
 */
public class TrackRenderer implements Disposable
{
    private int lowerLimit = 0;
    private int upperLimit = Integer.MAX_VALUE;
    private double markerPixels = 10d; // TODO: these should all be configurable
    private double minMarkerSize = 5d;
    private double markerElevation = 10d;
    private boolean overrideMarkerElevation = false;
    private Material material = Material.WHITE;
    private String iconFilePath;
    private final TrackRenderer.Shape SPHERE = new TrackRenderer.Sphere();
    private final TrackRenderer.Shape CONE = new TrackRenderer.Cone();
    private final TrackRenderer.Shape CYLINDER = new TrackRenderer.Cylinder();
    private TrackRenderer.Shape shape = SPHERE;

    public TrackRenderer()
    {
    }

    public void dispose()
    {
        this.CONE.dispose();
        this.CYLINDER.dispose();
        this.SPHERE.dispose();
    }

    public double getMarkerPixels()
    {
        return markerPixels;
    }

    public void setMarkerPixels(double markerPixels)
    {
        this.markerPixels = markerPixels;
    }

    public double getMinMarkerSize()
    {
        return minMarkerSize;
    }

    public void setMinMarkerSize(double minMarkerSize)
    {
        this.minMarkerSize = minMarkerSize;
    }

    public double getMarkerElevation()
    {
        return markerElevation;
    }

    public void setMarkerElevation(double markerElevation)
    {
        this.markerElevation = markerElevation;
    }

    public boolean isOverrideMarkerElevation()
    {
        return overrideMarkerElevation;
    }

    public void setOverrideMarkerElevation(boolean overrideMarkerElevation)
    {
        this.overrideMarkerElevation = overrideMarkerElevation;
    }

    public Material getMaterial()
    {
        return material;
    }

    public void setMaterial(Material material)
    {
        if (material == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.MaterialIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        // don't validate material's colors - material does that.

        this.material = material;
    }

    public String getIconFilePath()
    {
        return iconFilePath;
    }

    public void setIconFilePath(String iconFilePath)
    {
        //don't validate - a null iconFilePath cancels icon drawing
        this.iconFilePath = iconFilePath;
    }

    public int getLowerLimit()
    {
        return this.lowerLimit;
    }

    public void setLowerLimit(int lowerLimit)
    {
        this.lowerLimit = lowerLimit;
    }

    public int getUpperLimit()
    {
        return this.upperLimit;
    }

    public void setUpperLimit(int upperLimit)
    {
        this.upperLimit = upperLimit;
    }

    public void setShapeType(String shapeName)
    {
        if (shapeName.equalsIgnoreCase("Cone"))
            this.shape = CONE;
        else if (shapeName.equalsIgnoreCase("Cylinder"))
            this.shape = CYLINDER;
        else
            this.shape = SPHERE;
    }

    public void pick(DrawContext dc, java.util.Iterator<TrackPoint> trackPositions, java.awt.Point pickPoint,
        Layer layer)
    {
        // TODO: picking
    }

    public Point render(DrawContext dc, java.util.Iterator<TrackPoint> trackPositions)
    {
        return this.draw(dc, trackPositions);
    }

    private Point draw(DrawContext dc, java.util.Iterator<TrackPoint> trackPositions)
    {
        if (dc.getVisibleSector() == null)
            return null;

        SectorGeometryList geos = dc.getSurfaceGeometry();
        if (geos == null)
            return null;

        if (!this.shape.isInitialized)
            this.shape.initialize(dc);

        int index = 0;
        java.util.List<Point> points = new java.util.ArrayList<Point>();
        while (trackPositions.hasNext())
        {
            TrackPoint tp = trackPositions.next();
            if (index >= this.lowerLimit && index <= this.upperLimit)
            {
                Point point = this.computeSurfacePoint(dc, tp);
                if (point != null)
                {
                    points.add(point);
                }
            }
            if (++index >= this.upperLimit)
                break;
        }

        if (points.size() < 1)
            return null;

        Point firstPoint = points.get(0);
        Point lastPointDrawn = firstPoint;
        this.begin(dc);
        {
            Point previousDrawnPoint = null;

            double radius = this.computeMarkerRadius(dc, firstPoint);
            this.shape.render(dc, firstPoint, radius);
            for (Point point : points)
            {
                if (previousDrawnPoint == null)
                {
                    previousDrawnPoint = firstPoint;
                    continue; // skip over first point
                }

                // TODO: More sophisticated separation algorithm to gain frame-to-frame consistency
                radius = this.computeMarkerRadius(dc, point);
                double separation = point.distanceTo(previousDrawnPoint);
                double minSeparation = 4d * radius;
                if (separation > minSeparation)
                {
                    if (!dc.isPickingMode())
                        this.shape.render(dc, point, radius);

                    previousDrawnPoint = point;
                    lastPointDrawn = point;
                }
            }
        }
        this.end(dc);

        Point iconPoint = points.get(points.size() - 1);
        return iconPoint != null ? iconPoint : lastPointDrawn;
    }

    private Point computeSurfacePoint(DrawContext dc, TrackPoint pos)
    {
        return dc.getSurfaceGeometry().getSurfacePoint(Angle.fromDegrees(pos.getLatitude()),
            Angle.fromDegrees(pos.getLongitude()),
            this.overrideMarkerElevation ? this.markerElevation : pos.getElevation());
    }

    private double computeMarkerRadius(DrawContext dc, Point point)
    {
        double d = point.distanceTo(dc.getView().getEyePoint());
        double radius = this.markerPixels * dc.getView().computePixelSizeAtDistance(d);
        if (radius < this.minMarkerSize)
            radius = this.minMarkerSize;

        return radius;
    }

    private void begin(DrawContext dc)
    {
        GL gl = dc.getGL();
        Point cameraPosition = dc.getView().getEyePoint();

        gl.glPushAttrib(
            GL.GL_TEXTURE_BIT | GL.GL_ENABLE_BIT | GL.GL_CURRENT_BIT | GL.GL_LIGHTING_BIT | GL.GL_TRANSFORM_BIT);
        gl.glDisable(GL.GL_TEXTURE_2D);

        float[] lightPosition =
            {(float) (cameraPosition.x() * 2), (float) (cameraPosition.y() / 2), (float) (cameraPosition.z()), 0.0f};
        float[] lightDiffuse = {1.0f, 1.0f, 1.0f, 1.0f};
        float[] lightAmbient = {1.0f, 1.0f, 1.0f, 1.0f};
        float[] lightSpecular = {1.0f, 1.0f, 1.0f, 1.0f};

        this.material.apply(gl, GL.GL_FRONT);

        gl.glLightfv(GL.GL_LIGHT1, GL.GL_POSITION, lightPosition, 0);
        gl.glLightfv(GL.GL_LIGHT1, GL.GL_DIFFUSE, lightDiffuse, 0);
        gl.glLightfv(GL.GL_LIGHT1, GL.GL_AMBIENT, lightAmbient, 0);
        gl.glLightfv(GL.GL_LIGHT1, GL.GL_SPECULAR, lightSpecular, 0);

        gl.glDisable(GL.GL_LIGHT0);
        gl.glEnable(GL.GL_LIGHT1);
        gl.glEnable(GL.GL_LIGHTING);
        gl.glEnable(GL.GL_NORMALIZE);

        gl.glMatrixMode(javax.media.opengl.GL.GL_MODELVIEW);
        gl.glPushMatrix();
    }

    private void end(DrawContext dc)
    {
        GL gl = dc.getGL();

        gl.glMatrixMode(javax.media.opengl.GL.GL_MODELVIEW);
        gl.glPopMatrix();

        gl.glDisable(GL.GL_LIGHT1);
        gl.glEnable(GL.GL_LIGHT0);
        gl.glDisable(GL.GL_LIGHTING);
        gl.glDisable(GL.GL_NORMALIZE);
        gl.glPopAttrib();
    }

    private static abstract class Shape
    {
        protected int objectId;
        protected GLUquadric quadric;
        protected boolean isInitialized = false;

        abstract protected void doRender(DrawContext dc, Point point, double radius);

        protected void initialize(DrawContext dc)
        {
            this.objectId = dc.getGL().glGenLists(1);
            this.quadric = dc.getGLU().gluNewQuadric();
            dc.getGLU().gluQuadricDrawStyle(quadric, GLU.GLU_FILL);
            dc.getGLU().gluQuadricNormals(quadric, GLU.GLU_SMOOTH);
            dc.getGLU().gluQuadricOrientation(quadric, GLU.GLU_OUTSIDE);
            dc.getGLU().gluQuadricTexture(quadric, false);
        }

        private void dispose()
        {
            if (this.isInitialized)
            {
                GLU glu = new GLU();
                glu.gluDeleteQuadric(this.quadric);
                // TODO: Determine how to release the opengl object ID.
            }
        }

        private void render(DrawContext dc, Point point, double radius)
        {
            dc.getView().pushReferenceCenter(dc, point);
            this.doRender(dc, point, radius);
            dc.getView().popReferenceCenter(dc);
        }
    }

    private static class Sphere extends Shape
    {
        protected void initialize(DrawContext dc)
        {
            super.initialize(dc);

            double radius = 1;
            int slices = 36;
            int stacks = 18;

            dc.getGL().glNewList(this.objectId, GL.GL_COMPILE);
            dc.getGLU().gluSphere(quadric, radius, slices, stacks);
            dc.getGL().glEndList();

            this.isInitialized = true;
        }

        protected void doRender(DrawContext dc, Point point, double radius)
        {
            dc.getGL().glScaled(radius, radius, radius);
            dc.getGL().glCallList(this.objectId);
        }
    }

    private static class Cone extends Shape
    {
        protected void initialize(DrawContext dc)
        {
            super.initialize(dc);

            int slices = 30;
            int stacks = 30;
            int loops = 2;

            dc.getGL().glNewList(this.objectId, GL.GL_COMPILE);
            dc.getGLU().gluCylinder(quadric, 1d, 0d, 2d, slices, (int) (2 * (Math.sqrt(stacks)) + 1));
            dc.getGLU().gluDisk(quadric, 0d, 1d, slices, loops);
            dc.getGL().glEndList();

            this.isInitialized = true;
        }

        protected void doRender(DrawContext dc, Point point, double size)
        {
            PolarPoint p = PolarPoint.fromCartesian(point);

            dc.getGL().glLoadIdentity();
            dc.getGL().glScaled(size, size, size);
            dc.getGL().glRotated(p.getLongitude().getDegrees(), 0, 1, 0);
            dc.getGL().glRotated(Math.abs(p.getLatitude().getDegrees()), Math.signum(p.getLatitude().getDegrees()) * -1,
                0, 0);
            dc.getGL().glCallList(this.objectId);
        }
    }

    private static class Cylinder extends Shape
    {
        protected void initialize(DrawContext dc)
        {
            super.initialize(dc);

            int slices = 30;
            int stacks = 30;
            int loops = 2;

            dc.getGL().glNewList(this.objectId, GL.GL_COMPILE);
            dc.getGLU().gluCylinder(quadric, 1d, 1d, 2d, slices, (int) (2 * (Math.sqrt(stacks)) + 1));
            dc.getGLU().gluDisk(quadric, 0d, 1d, slices, loops);
            dc.getGL().glTranslated(0, 0, 2);
            dc.getGLU().gluDisk(quadric, 0d, 1d, slices, loops);
            dc.getGL().glTranslated(0, 0, -2);
            dc.getGL().glEndList();

            this.isInitialized = true;
        }

        protected void doRender(DrawContext dc, Point point, double size)
        {
            PolarPoint p = PolarPoint.fromCartesian(point);

            dc.getGL().glLoadIdentity();
            dc.getGL().glScaled(size, size, size);
            dc.getGL().glRotated(p.getLongitude().getDegrees(), 0, 1, 0);
            dc.getGL().glRotated(Math.abs(p.getLatitude().getDegrees()), Math.signum(p.getLatitude().getDegrees()) * -1,
                0, 0);
            dc.getGL().glCallList(this.objectId);
        }
    }
}
