/*
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 com.sun.opengl.util.j2d.*;
import com.sun.opengl.util.texture.*;
import gov.nasa.worldwind.geom.*;
import gov.nasa.worldwind.geom.Point;

import javax.media.opengl.*;
import java.awt.*;
import java.util.*;

/**
 * @author tag
 * @version $Id: IconRenderer.java 1784 2007-05-08 06:33:06Z tgaskins $
 */
public class IconRenderer implements Disposable
{
    // TODO: Periodically clean unused textures from the texture map.
    private java.util.HashMap<String, Texture> iconTextures = new java.util.HashMap<String, Texture>();
    private Pedestal pedestal;
    private PickSupport pickSupport = new PickSupport();
    private HashMap<Font, ToolTipRenderer> toolTipRenderers = new HashMap<Font, ToolTipRenderer>();

    public IconRenderer()
    {
    }

    public void dispose()
    {
        for (Texture iconTexture : this.iconTextures.values())
        {
            if (iconTexture != null)
                iconTexture.dispose();
        }
    }

    public Pedestal getPedestal()
    {
        return pedestal;
    }

    public void setPedestal(Pedestal pedestal)
    {
        this.pedestal = pedestal;
    }

    private static boolean isIconValid(WWIcon icon, boolean checkPosition)
    {
        if (icon == null || icon.getPath() == null)
            return false;

        //noinspection RedundantIfStatement
        if (checkPosition && icon.getPosition() == null)
            return false;

        return true;
    }

    public void pick(DrawContext dc, Iterator<WWIcon> icons, java.awt.Point pickPoint, Layer layer)
    {
        this.drawMany(dc, icons);
    }

    public void pick(DrawContext dc, WWIcon icon, Point iconPoint, java.awt.Point pickPoint, Layer layer)
    {
        if (!isIconValid(icon, false))
            return;

        this.drawOne(dc, icon, iconPoint);
    }

    public void render(DrawContext dc, Iterator<WWIcon> icons)
    {
        this.drawMany(dc, icons);
    }

    public void render(DrawContext dc, WWIcon icon, Point iconPoint)
    {
        if (!isIconValid(icon, false))
            return;

        this.drawOne(dc, icon, iconPoint);
    }

    private void drawMany(DrawContext dc, Iterator<WWIcon> icons)
    {
        if (dc == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.DrawContextIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        if (dc.getVisibleSector() == null)
            return;

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

        if (icons == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.IconIterator");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        if (!icons.hasNext())
            return;

        while (icons.hasNext())
        {
            WWIcon icon = icons.next();
            if (!isIconValid(icon, true))
                continue;

            if (!icon.isVisible())
                continue;

            // Determine Cartesian position from the surface geometry if the icon is near the surface,
            // otherwise draw it from the globe.
            Position pos = icon.getPosition();
            Point iconPoint = null;
            if (pos.getElevation() < dc.getGlobe().getMaxElevation())
                iconPoint = dc.getSurfaceGeometry().getSurfacePoint(icon.getPosition());
            if (iconPoint == null)
                iconPoint = dc.getGlobe().computePointFromPosition(icon.getPosition());

            // The icons aren't drawn here, but added to the ordered queue to be drawn back-to-front.
            double eyeDistance = dc.getView().getEyePoint().distanceTo(iconPoint);
            dc.addOrderedRenderable(new OrderedIcon(icon, iconPoint, eyeDistance));

            if (icon.isShowToolTip())
                this.addToolTip(dc, icon, iconPoint);
        }
    }

    private void drawOne(DrawContext dc, WWIcon icon, Point iconPoint)
    {
        if (dc == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.DrawContextIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            throw new IllegalArgumentException(msg);
        }

        if (dc.getVisibleSector() == null)
            return;

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

        if (!icon.isVisible())
            return;

        if (iconPoint == null)
        {
            Angle lat = icon.getPosition().getLatitude();
            Angle lon = icon.getPosition().getLongitude();

            if (!dc.getVisibleSector().contains(lat, lon))
                return;

            iconPoint = dc.getSurfaceGeometry().getSurfacePoint(lat, lon, icon.getPosition().getElevation());
            if (iconPoint == null)
                return;
        }

        if (!dc.getView().getFrustumInModelCoordinates().contains(iconPoint))
            return;

        double horizon = dc.getView().computeHorizonDistance();
        double eyeDistance = dc.getView().getEyePoint().distanceTo(iconPoint);
        if (eyeDistance > horizon)
            return;

        // The icon isn't drawn here, but added to the ordered queue to be drawn back-to-front.
        dc.addOrderedRenderable(new OrderedIcon(icon, iconPoint, eyeDistance));

        if (icon.isShowToolTip())
            this.addToolTip(dc, icon, iconPoint);
    }

    private void addToolTip(DrawContext dc, WWIcon icon, Point iconPoint)
    {
        if (icon.getToolTipFont() == null && icon.getToolTipText() == null)
            return;

        final Point screenPoint = dc.getView().project(iconPoint);
        if (screenPoint == null)
            return;

        OrderedText tip = new OrderedText(icon.getToolTipText(), icon.getToolTipFont(), screenPoint,
            icon.getToolTipTextColor(), 0d);
        dc.addOrderedRenderable(tip);
    }

    private class OrderedText implements OrderedRenderable
    {
        Font font;
        String text;
        Point point;
        double eyeDistance;
        java.awt.Point pickPoint;
        Layer layer;
        java.awt.Color color;

        OrderedText(String text, Font font, Point point, java.awt.Color color, double eyeDistance)
        {
            this.text = text;
            this.font = font;
            this.point = point;
            this.eyeDistance = eyeDistance;
            this.color = color;
        }

        OrderedText(String text, Font font, Point point, java.awt.Point pickPoint, Layer layer, double eyeDistance)
        {
            this.text = text;
            this.font = font;
            this.point = point;
            this.eyeDistance = eyeDistance;
            this.pickPoint = pickPoint;
            this.layer = layer;
        }

        public double getDistanceFromEye()
        {
            return this.eyeDistance;
        }

        public void render(DrawContext dc)
        {
            ToolTipRenderer tr = IconRenderer.this.toolTipRenderers.get(this.font);
            if (tr == null)
            {
                if (this.font != null)
                    tr = new ToolTipRenderer(new TextRenderer(this.font, true, true));
                else
                    tr = new ToolTipRenderer();
                IconRenderer.this.toolTipRenderers.put(this.font, tr);
            }

            Rectangle vp = dc.getView().getViewport();
            tr.setForeground(this.color);
            tr.setUseSystemLookAndFeel(this.color == null);
            tr.beginRendering(vp.width, vp.height, true);
            tr.draw(this.text, (int) point.x(), (int) point.y());
            tr.endRendering();
        }

        public void pick(DrawContext dc, java.awt.Point pickPoint)
        {
        }
    }

    private class OrderedIcon implements OrderedRenderable, Locatable
    {
        WWIcon icon;
        Point point;
        double eyeDistance;
        java.awt.Point pickPoint;
        Layer layer;

        OrderedIcon(WWIcon icon, Point point, double eyeDistance)
        {
            this.icon = icon;
            this.point = point;
            this.eyeDistance = eyeDistance;
        }

        OrderedIcon(WWIcon icon, Point point, java.awt.Point pickPoint, Layer layer, double eyeDistance)
        {
            this.icon = icon;
            this.point = point;
            this.eyeDistance = eyeDistance;
            this.pickPoint = pickPoint;
            this.layer = layer;
        }

        public double getDistanceFromEye()
        {
            return this.eyeDistance;
        }

        public Position getPosition()
        {
            return this.icon.getPosition();
        }

        public void render(DrawContext dc)
        {
            IconRenderer.this.beginDrawIcons(dc);

            try
            {
                IconRenderer.this.drawIcon(dc, this);
                // Draw as many as we can in a batch to save ogl state switching.
                while (dc.getOrderedRenderables().peek() instanceof OrderedIcon)
                {
                    OrderedIcon oi = (OrderedIcon) dc.getOrderedRenderables().poll();
                    IconRenderer.this.drawIcon(dc, oi);
                }
            }
            catch (WWRuntimeException e)
            {
                String msg = WorldWind.retrieveErrMsg("generic.ExceptionWhileRenderingIcon");
                WorldWind.logger().log(java.util.logging.Level.FINE, msg + ":" + e.getMessage());
            }
            catch (Exception e)
            {
                String msg = WorldWind.retrieveErrMsg("generic.ExceptionWhileRenderingIcon");
                WorldWind.logger().log(java.util.logging.Level.FINE, msg, e);
            }
            finally
            {
                IconRenderer.this.endDrawIcons(dc);
            }
        }

        public void pick(DrawContext dc, java.awt.Point pickPoint)
        {
            IconRenderer.this.pickSupport.clearPickList();
            IconRenderer.this.beginDrawIcons(dc);
            try
            {
                IconRenderer.this.drawIcon(dc, this);
                // Draw as many as we can in a batch to save ogl state switching.
                while (dc.getOrderedRenderables().peek() instanceof OrderedIcon)
                {
                    IconRenderer.this.drawIcon(dc, (OrderedIcon) dc.getOrderedRenderables().poll());
                }
            }
            catch (WWRuntimeException e)
            {
                String msg = WorldWind.retrieveErrMsg("generic.ExceptionWhileRenderingIcon");
                WorldWind.logger().log(java.util.logging.Level.FINE, msg + ":" + e.getMessage());
            }
            catch (Exception e)
            {
                String msg = WorldWind.retrieveErrMsg("generic.ExceptionWhilePickingIcon");
                WorldWind.logger().log(java.util.logging.Level.FINE, msg, e);
            }
            finally
            {
                IconRenderer.this.endDrawIcons(dc);
                IconRenderer.this.pickSupport.resolvePick(dc, pickPoint, layer);
                IconRenderer.this.pickSupport.clearPickList(); // to ensure entries can be garbage collected
            }
        }
    }

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

        int attributeMask =
            GL.GL_DEPTH_BUFFER_BIT // for depth test, depth mask and depth func
                | GL.GL_TRANSFORM_BIT // for modelview and perspective
                | GL.GL_VIEWPORT_BIT // for depth range
                | GL.GL_CURRENT_BIT // for current color
                | GL.GL_COLOR_BUFFER_BIT // for alpha test func and ref, and blend
                | GL.GL_TEXTURE_BIT // for texture env
                | GL.GL_DEPTH_BUFFER_BIT // for depth func
                | GL.GL_ENABLE_BIT; // for enable/disable changes
        gl.glPushAttrib(attributeMask);

        // Apply the depth buffer but don't change it.
        gl.glEnable(GL.GL_DEPTH_TEST);
        gl.glDepthMask(false);

        // Suppress any fully transparent image pixels
        gl.glEnable(GL.GL_ALPHA_TEST);
        gl.glAlphaFunc(GL.GL_GREATER, 0.001f);

        // Load a parallel projection with dimensions (viewportWidth, viewportHeight)
        int[] viewport = new int[4];
        gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);
        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glPushMatrix();
        gl.glLoadIdentity();
        gl.glOrtho(0d, viewport[2], 0d, viewport[3], -1d, 1d);

        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glPushMatrix();

        gl.glMatrixMode(GL.GL_TEXTURE);
        gl.glPushMatrix();

        if (dc.isPickingMode())
        {
            this.pickSupport.beginPicking(dc);

            // Set up to replace the non-transparent texture colors with the single pick color.
            gl.glEnable(GL.GL_TEXTURE_2D);
            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_COMBINE);
            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_SRC0_RGB, GL.GL_PREVIOUS);
            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_COMBINE_RGB, GL.GL_REPLACE);
        }
        else
        {
            gl.glEnable(GL.GL_TEXTURE_2D);
            gl.glEnable(GL.GL_BLEND);
            gl.glBlendFunc(GL.GL_ONE, GL.GL_ONE_MINUS_SRC_ALPHA);
        }
    }

    private void endDrawIcons(DrawContext dc)
    {
        if (dc.isPickingMode())
            this.pickSupport.endPicking(dc);

        GL gl = dc.getGL();
        gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glPopMatrix();

        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glPopMatrix();

        gl.glMatrixMode(GL.GL_TEXTURE);
        gl.glPopMatrix();

        gl.glPopAttrib();
    }

    private Point drawIcon(DrawContext dc, OrderedIcon uIcon)
    {
        if (uIcon.point == null)
        {
            String msg = WorldWind.retrieveErrMsg("nullValue.PointIsNull");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg);
            return null;
        }

        WWIcon icon = uIcon.icon;

        final Point screenPoint = dc.getView().project(uIcon.point);
        if (screenPoint == null)
            return null;

        Texture iconTexture = this.iconTextures.get(icon.getPath());
        if (iconTexture == null)
            iconTexture = this.initializeTexture(dc, icon);

        double pedestalScale;
        double pedestalSpacing;
        Texture pedestalTexture = null;
        if (pedestal != null)
        {
            pedestalScale = this.pedestal.getScale();
            pedestalSpacing = pedestal.getSpacingPixels();

            pedestalTexture = this.iconTextures.get(pedestal.getPath());
            if (pedestalTexture == null)
                pedestalTexture = this.initializeTexture(dc, pedestal);
        }
        else
        {
            pedestalScale = 0d;
            pedestalSpacing = 0d;
        }

        javax.media.opengl.GL gl = dc.getGL();

        this.setDepthFunc(dc, screenPoint);

        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glLoadIdentity();

        Dimension size = icon.getSize();
        double width = size != null ? size.getWidth() : iconTexture.getWidth();
        double height = size != null ? size.getHeight() : iconTexture.getHeight();
        gl.glTranslated(screenPoint.x() - width / 2, screenPoint.y() + (pedestalScale * height) + pedestalSpacing, 0d);

        if (icon.isHighlighted())
        {
            double heightDelta = this.pedestal != null ? 0 : height / 2; // expand only above the pedestal
            gl.glTranslated(width / 2, heightDelta, 0);
            gl.glScaled(icon.getHighlightScale(), icon.getHighlightScale(), icon.getHighlightScale());
            gl.glTranslated(-width / 2, -heightDelta, 0);
        }

        if (dc.isPickingMode())
        {
            java.awt.Color color = dc.getUniquePickColor();
            int colorCode = color.getRGB();
            this.pickSupport.addPickableObject(colorCode, icon, uIcon.getPosition(), false);
            gl.glColor3ub((byte) color.getRed(), (byte) color.getGreen(), (byte) color.getBlue());
        }

        iconTexture.bind();
        TextureCoords texCoords = iconTexture.getImageTexCoords();
        gl.glScaled(width, height, 1d);
        dc.drawUnitQuad(texCoords);

        if (pedestalTexture != null)
        {
            gl.glLoadIdentity();
            gl.glTranslated(screenPoint.x() - (pedestalScale * (width / 2)), screenPoint.y(), 0d);
            gl.glScaled(width * pedestalScale, height * pedestalScale, 1d);

            pedestalTexture.bind();
            texCoords = pedestalTexture.getImageTexCoords();
            dc.drawUnitQuad(texCoords);
        }

        return screenPoint;
    }

    private void setDepthFunc(DrawContext dc, Point screenPoint)
    {
        GL gl = dc.getGL();

        if (dc.getView().getAltitude() < dc.getGlobe().getMaxElevation() * dc.getVerticalExaggeration())
        {
            double depth = screenPoint.z() - 8d * 0.00048875809d;
            depth = depth < 0d ? 0d : (depth > 1d ? 1d : depth);
            gl.glDepthFunc(GL.GL_LESS);
            gl.glDepthRange(depth, depth);
        }
        else if (screenPoint.z() >= 1d)
        {
            gl.glDepthFunc(GL.GL_EQUAL);
            gl.glDepthRange(1d, 1d);
        }
        else
        {
            gl.glDepthFunc(GL.GL_ALWAYS);
        }
    }

    private Texture initializeTexture(DrawContext dc, WWIcon icon)
    {
        try
        {
            java.io.InputStream iconStream = this.getClass().getResourceAsStream("/" + icon.getPath());
            if (iconStream == null)
            {
                java.io.File iconFile = new java.io.File(icon.getPath());
                if (iconFile.exists())
                {
                    iconStream = new java.io.FileInputStream(iconFile);
                }
            }

            // Icons with the same path are assumed to be identical textures, so key the texture id off the path.
            Texture iconTexture = TextureIO.newTexture(iconStream, true, null);
            this.iconTextures.put(icon.getPath(), iconTexture);
            iconTexture.bind();

            GL gl = dc.getGL();
            gl.glTexEnvf(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_MODULATE);
            gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR_MIPMAP_LINEAR);
            gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR);
            gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP_TO_EDGE);
            gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP_TO_EDGE);

            return iconTexture;
        }
        catch (java.io.IOException e)
        {
            String msg = WorldWind.retrieveErrMsg("generic.IOExceptionDuringTextureInitialization");
            WorldWind.logger().log(java.util.logging.Level.FINE, msg, e);
            throw new WWRuntimeException(msg, e);
        }
    }

    @Override
    public String toString()
    {
        return WorldWind.retrieveErrMsg("layers.IconLayer.Name");
    }
}
