/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package usda.weru.gis.gui;

import com.vividsolutions.jts.geom.Coordinate;
import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;

import org.geotools.data.memory.MemoryDataStore;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.map.MapContext;
import org.geotools.map.MapLayer;
import org.geotools.map.event.MapLayerListEvent;

import org.geotools.map.event.MapLayerListListener;
import org.geotools.renderer.GTRenderer;
import org.geotools.renderer.label.LabelCacheImpl;
import org.geotools.renderer.lite.LabelCache;
import org.geotools.renderer.lite.RendererUtilities;
import org.geotools.renderer.lite.StreamingRenderer;
import org.opengis.feature.Feature;


/**
 * * @author Joseph A. Levin <joelevin@weru.ksu.edu>
 */
public class MapRenderLayer extends JComponent implements Runnable, MapController, MapLayerListListener{

    public static final String PROP_ZOOMFACTOR = "zoomFactor";
    public static final String PROP_PANOFFSET = "panOffset";
    private MapContext c_map;
    private GTRenderer c_renderer;
    private LabelCache c_labelCache;
    private Coordinate c_center;
    private double c_zoomFactor;
    private BufferedImage c_bufferImage;
    private double c_bufferFactor = .3d;
    private double c_bufferZoomFactor;
    private Coordinate c_bufferCenter;
    private static final Object RENDER_LOCK = new Object();
    private static final Object BUFFER_LOCK = new Object();
    private ExecutorService c_renderService;
    private Future c_renderTask;
    private MemoryDataStore c_highlight;
    private MapLayer c_highlightLayer;


    //used by internal actions
    private boolean c_panning;
    private double c_zoomAdjustment = 0.5d;

    public MapRenderLayer() {
        addComponentListener(new InternalComponentListener());

        InternalPanAction panAction = new InternalPanAction();
        addMouseListener(panAction);
        addMouseMotionListener(panAction);

        InternalZoomAction zoomAction = new InternalZoomAction();
        addMouseWheelListener(zoomAction);

        c_renderService = Executors.newSingleThreadExecutor();

        c_center = new Coordinate(0, 0);

        setPreferredSize(new Dimension(500, 600));
        
        StreamingRenderer renderer = new StreamingRenderer();
//        ShapefileRenderer renderer = new ShapefileRenderer();
       Map hints = new HashMap();
        //hints.put(StreamingRenderer.SCALE_COMPUTATION_METHOD_KEY, StreamingRenderer.SCALE_OGC);
        hints.put(StreamingRenderer.TEXT_RENDERING_KEY, StreamingRenderer.TEXT_RENDERING_STRING);
        hints.put(StreamingRenderer.OPTIMIZED_DATA_LOADING_KEY, true);
        hints.put(StreamingRenderer.ADVANCED_PROJECTION_HANDLING_KEY, true);
        hints.put("renderingBuffer", 20);
        c_labelCache = new LabelCacheImpl();
        hints.put(StreamingRenderer.LABEL_CACHE_KEY, c_labelCache);

        renderer.setRendererHints(hints);
        c_renderer = renderer;






        
        setZoomFactor(1.0d);

    }

    public void setMapContext(MapContext map) throws IOException {
        c_map = map;
        c_renderer.setContext(c_map);
        c_map.addMapLayerListListener(this);
        enqueueRender();
    }

    public MapContext getMapContext() {
        return c_map;

    }

    public void setCenter(Coordinate center) {
        c_center = center;
        repaint();
    }

    public Coordinate getCenter() {
        return c_center;
    }

    public void setZoomFactor(double zoom) {
        double old = c_zoomFactor;
        c_zoomFactor = zoom;
        repaint();
        firePropertyChange(PROP_ZOOMFACTOR, old, c_zoomFactor);
    }

    public double getZoomFactor() {
        return c_zoomFactor;
    }

    public Dimension getViewSize() {
        return getSize();
    }

    public void setBufferFactor(double factor) {
        if (factor < 0) {
            throw new IllegalArgumentException("Buffer factor must be equal or greater than 0.");
        }
        c_bufferFactor = factor;
        repaint();
    }

    public Dimension calculateBufferImageSize() {
        Dimension view = getViewSize();

        int width = (int) (view.width * (1 + c_bufferFactor));
        int height = (int) (view.height * (1 + c_bufferFactor));

        return new Dimension(width, height);
    }

    private void flipBuffer(BufferedImage buffer, double zoom, Coordinate center) {
        synchronized (BUFFER_LOCK) {
            //todo: fire property change events if needed
            c_bufferImage = buffer;
            c_bufferZoomFactor = zoom;
            c_bufferCenter = center;
        }
        setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
        repaint();
    }

    @Override
    protected void paintComponent(Graphics g) {
        if (c_bufferImage == null) {
            enqueueRender();
            return;
        }

        Graphics2D g2d = (Graphics2D) g;

        if (c_zoomFactor != c_bufferZoomFactor || (!c_panning && !c_center.equals2D(c_bufferCenter))) {
            enqueueRender();
        }

        //make the temp zoom look pretty
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF);
        g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_SPEED);
        synchronized (BUFFER_LOCK) {
            //Scale the image while we wait for the new image to be rendered
            double zoom = c_zoomFactor / c_bufferZoomFactor;

            //draw the buffer image zoomed and panned
            int panX = (int) ((c_bufferCenter.x - c_center.x) * c_zoomFactor);
            int panY = (int) ((c_center.y - c_bufferCenter.y) * c_zoomFactor);           

            int width = (int) (c_bufferImage.getWidth() * zoom);
            int height = (int) (c_bufferImage.getHeight() * zoom);

            g2d.drawImage(c_bufferImage, (getWidth() / 2) - (width / 2) + panX, (getHeight() / 2) - (height / 2) + panY, width, height, this);
        }
    }

    
    private void enqueueRender() {
        if (c_renderTask != null && !c_renderTask.isDone()) {
            //c_renderer.stopRendering();
            c_renderTask.cancel(false);
        }        
        c_renderTask = c_renderService.submit(this);
        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

    }

    public void run() {
        try {
            render();
        }
        catch (Exception e) {
        }
    }

    private void render() {
        assert !EventQueue.isDispatchThread() : "Do not call on EventQueue.  Call enqueueRender() instead.";
        if (c_bufferImage != null) {
            c_renderer.stopRendering();
        }
        Dimension imageSize = calculateBufferImageSize();
        final BufferedImage image = new BufferedImage(imageSize.width, imageSize.height, BufferedImage.TYPE_INT_ARGB);

        Graphics2D g = image.createGraphics();
//        g.setColor(getBackground());
//        g.fillRect(0, 0, image.getWidth(), image.getHeight());

        try {

            final double zoom = getZoomFactor();

            final double x = c_center.x;
            final double y = c_center.y;

            //create the transform
            AffineTransform transform = new AffineTransform();
            // Translate to the center of the buffer image.
            transform.translate((image.getWidth() / 2), (image.getHeight() / 2));

            // Scale with negative y factor to correct the orientation.
            transform.scale(zoom, -zoom);

            // Translate to the center of the feature collection.
            transform.translate(-x, -y);


            if(c_map != null){
                synchronized (RENDER_LOCK) {
                    c_labelCache.clear();
                    //c_renderer.paint(g, new Rectangle(imageSize), transform);

                    Rectangle paintSize = new Rectangle(imageSize);
                    ReferencedEnvelope envelope = RendererUtilities.createMapEnvelope(paintSize, transform, c_map.getCoordinateReferenceSystem());
                    c_renderer.paint(g, paintSize, envelope, transform);                    

                }
            }
            flipBuffer(image, zoom, new Coordinate(x, y));

        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    //some map actions
    public void zoomIn(double adjustment) {
        setZoomFactor(c_zoomFactor * adjustment);

    }

    public void zoomOut(double adjustment) {
        setZoomFactor(c_zoomFactor / adjustment);
    }

    public void pan(int x, int y) {
        Coordinate center = getCenter();
        double zoom = getZoomFactor();
        double pX = x / zoom;
        double pY = y / zoom;
        Coordinate newCenter = new Coordinate(center.x - pX, center.y + pY);
        setCenter(newCenter);
    }

    public boolean isPanning() {
        return c_panning;
    }

    public void setPanning(boolean panning) {
        c_panning = panning;
    }


    //Handles setting the cursor high enough in the component tree so that every layer displays cursors.
        public void setCursor(Cursor cursor){
            Container parent = SwingUtilities.getAncestorOfClass(MapPane.class, MapRenderLayer.this);
            if(parent == null){
                parent = MapRenderLayer.this;
            }
            parent.setCursor(cursor);

        }




    public void highlight(Feature... features){
        if(c_map == null){
            throw new IllegalStateException("MapContext is null");
        }
        if(c_map.indexOf(c_highlightLayer) >= 0){
            c_map.removeLayer(c_highlightLayer);
        }
        c_highlight = new MemoryDataStore();


        //c_highlight.addFeature(features);


    }

    public void layerAdded(MapLayerListEvent event) {
        enqueueRender();
    }

    public void layerRemoved(MapLayerListEvent event) {
        enqueueRender();
    }

    public void layerChanged(MapLayerListEvent event) {
        enqueueRender();
    }

    public void layerMoved(MapLayerListEvent event) {
        enqueueRender();
    }


    private class InternalPanAction extends MouseAdapter {

        private int c_ix,  c_iy;

        @Override
        public void mousePressed(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                c_panning = true;
                c_ix = e.getX();
                c_iy = e.getY();                
            }

        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                setCursor(Cursor.getDefaultCursor());
                c_panning = false;
                repaint();
            }
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            if (c_panning) {
                setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
                int x = e.getX();
                int y = e.getY();
                int dx = x - c_ix;
                int dy = y - c_iy;
                pan(dx, dy);
                c_ix = x;
                c_iy = y;                
            }
        }

        

    }


    private class InternalZoomAction extends MouseAdapter {       

        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {
            //we do in fact have a map pane
            int units = e.getUnitsToScroll();
            double adjustment = Math.abs(c_zoomAdjustment * units);
            if (units < 0) {
                zoomIn(adjustment);
            }
            else if (units > 0) {
                zoomOut(adjustment);
            }

        }
    }

    private class InternalComponentListener extends ComponentAdapter {

        @Override
        public void componentResized(ComponentEvent e) {
            enqueueRender();
        }
    }
}
