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.geom.NoninvertibleTransformException;
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.Layer;
import org.geotools.map.MapContent;
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;

/**
 * handles user's changes to the map, such as zooming and panning
 * * @author Joseph A. Levin <joelevin@weru.ksu.edu>
 */
public class MapRenderLayer extends JComponent implements Runnable, MapController, MapLayerListListener {

    private static final long serialVersionUID = 1L;

    /**
     *
     */
    public static final String PROP_ZOOMFACTOR = "zoomFactor";

    /**
     *
     */
    public static final String PROP_PANOFFSET = "panOffset";
    private MapContent c_map;
    private final GTRenderer c_renderer;
    private final 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 final ExecutorService c_renderService;
    private Future<?> c_renderTask;
    private MemoryDataStore c_highlight;
    private Layer c_highlightLayer;

    //used by internal actions
    private boolean c_panning;
    private final 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<String, Object> hints = new HashMap<>();
        //hints.put(StreamingRenderer.SCALE_COMPUTATION_METHOD_KEY, StreamingRenderer.SCALE_OGC);
        hints.put(StreamingRenderer.TEXT_RENDERING_KEY, StreamingRenderer.TEXT_RENDERING_STRING);
        // it looks like the 'optimized data loading' option no longer exists -max
        //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);
    }

    /**
     *
     * @param map
     * @throws IOException
     */
    public void setMapContent(MapContent map) throws IOException {
        c_map = map;
        
        c_renderer.setMapContent(c_map);
        c_map.addMapLayerListListener(this);
        enqueueRender();
    }

    /**
     *
     * @return
     */
    public MapContent getMapContent() {
        return c_map;
    }

    /**
     *
     * @param center
     */
    @Override
    public void setCenter(Coordinate center) {
        c_center = center;
        repaint();
    }

    /**
     *
     * @return
     */
    @Override
    public Coordinate getCenter() {
        return c_center;
    }

    /**
     *
     * @param zoom
     */
    @Override
    public void setZoomFactor(double zoom) {
        double old = c_zoomFactor;
        c_zoomFactor = zoom;
        repaint();
        firePropertyChange(PROP_ZOOMFACTOR, old, c_zoomFactor);
    }

    /**
     *
     * @return
     */
    @Override
    public double getZoomFactor() {
        return c_zoomFactor;
    }

    /**
     *
     * @return
     */
    public Dimension getViewSize() {
        return getSize();
    }

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

    /**
     *
     * @return
     */
    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();
    }

    /**
     *
     * @param g
     */
    @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_renderTask.cancel(false);
        }
        c_renderTask = c_renderService.submit(this);
        setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));

    }

    /**
     *
     */
    @Override
    public void run() {
        try {
            render();
        } catch (Exception e) {
        }
    }

    private void render() {
/**
                 * Note:  Assertions are not enabled.  These will be useless items
                 * unless assertions are enabled.  Thus, they will be commented out unless
                 * the user wishes to enable specific assertions (feed the virtual machine 
                 * the -ea argument).
                 */
//        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();

        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();

                    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 (NoninvertibleTransformException e) {
            e.printStackTrace();
        }
    }

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

    /**
     *
     * @param adjustment
     */
    @Override
    public void zoomOut(double adjustment) {
        setZoomFactor(c_zoomFactor / adjustment);
    }

    /**
     *
     * @param x
     * @param y
     */
    @Override
    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);
    }

    /**
     *
     * @return
     */
    @Override
    public boolean isPanning() {
        return c_panning;
    }

    /**
     *
     * @param panning
     */
    @Override
    public void setPanning(boolean panning) {
        c_panning = panning;
    }

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

    /**
     *
     * @param features
     */
    public void highlight(Feature... features) {
        if (c_map == null) {
            throw new IllegalStateException("MapContent is null");
        }
        c_map.removeLayer(c_highlightLayer);
        c_highlight = new MemoryDataStore();
    }

    /**
     *
     * @param event
     */
    @Override
    public void layerAdded(MapLayerListEvent event) {
        enqueueRender();
    }

    /**
     *
     * @param event
     */
    @Override
    public void layerRemoved(MapLayerListEvent event) {
        enqueueRender();
    }

    /**
     *
     * @param event
     */
    @Override
    public void layerChanged(MapLayerListEvent event) {
        enqueueRender();
    }

    /**
     *
     * @param event
     */
    @Override
    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();
        }
    }

    @Override
    public void layerPreDispose(MapLayerListEvent arg0) {

    }
}
