package com.srbenoit.modeling.grid;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import com.srbenoit.log.LoggedPanel;
import com.srbenoit.render.LineSegment2D;

/**
 * A panel that renders the state of a grid.
 */
public class GridPanel2D extends LoggedPanel implements MouseListener, MouseMotionListener {

    /** version number for serialization */
    private static final long serialVersionUID = 1940457897064190922L;

    /** flag to control whether grid is drawn */
    private static final boolean DRAW_GRID = false;

    /** flag to control whether circles are drawn on linked list elements */
    private static final boolean CIRCLES_ON_LINKED = false;

    /** flag to control whether antialiasing is performed */
    private static final boolean ANTIALIAS = false;

    /** the grid this panel renders */
    private final Grid2D grid;

    /** an iterator that traverses the grid's member objects */
    private final GridIterator2D iter;

    /** the pixels per grid cell */
    private int pixPerCell;

    /** the offscreen image */
    private final transient BufferedImage offscreen;

    /** the <code>Graphics2D</code> object for the offscreen image */
    private final transient Graphics2D graphics;

    /** a stroke to use to render lines */
    private final transient BasicStroke stroke;

    /** the grid member being dragged */
    private GridMember2Int dragging;

    /** the scale at which to draw force vectors (0 for no vectors) */
    private double vectorScale;

    /** the color in which to draw the grid lines */
    private final Color gridColor;

    /**
     * Constructs a new <code>GridPanel</code>.
     *
     * @param  width    the width of the window
     * @param  height   the height of the window
     * @param  theGrid  the grid this panel will render
     */
    public GridPanel2D(final int width, final int height, final Grid2D theGrid) {

        super();

        int pixPerCellX;
        int pixPerCellY;
        int actWidth;
        int actHeight;

        this.grid = theGrid;
        this.iter = new GridIterator2D(this.grid);
        this.dragging = null;
        this.vectorScale = 0;

        // Determine the axis with the largest number of blocks
        pixPerCellX = width / theGrid.getGridWidth();
        pixPerCellY = height / theGrid.getGridHeight();

        // Make the grid cells square - the smaller pixel size from above
        this.pixPerCell = (pixPerCellX < pixPerCellY) ? pixPerCellX : pixPerCellY;

        if (this.pixPerCell == 0) {
            this.pixPerCell = 1;
        }

        actWidth = (theGrid.getGridWidth() * this.pixPerCell) + 1;
        actHeight = (theGrid.getGridHeight() * this.pixPerCell) + 1;
        setPreferredSize(new Dimension(actWidth, actHeight));

        this.offscreen = new BufferedImage(actWidth, actHeight, BufferedImage.TYPE_INT_RGB);
        this.graphics = (Graphics2D) (this.offscreen.getGraphics());
        this.graphics.setColor(Color.black);
        this.graphics.fillRect(0, 0, actWidth, actHeight);

        if (ANTIALIAS) {
            this.graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);
        }

        this.stroke = new BasicStroke(2);
        this.gridColor = new Color(220, 220, 255);

        setBackground(Color.WHITE);
    }

    /**
     * Sets the scale at which to draw force vectors
     *
     * @param  scale  the new scale
     */
    public void setVectorScale(final double scale) {

        this.vectorScale = scale;
    }

    /**
     * Renders the component.
     *
     * @param  grx  The <code>Graphics</code> to which to draw.
     */
    @Override public void paintComponent(final Graphics grx) {

        synchronized (this.offscreen) {
            grx.drawImage(this.offscreen, 0, 0, null);
        }
    }

    /**
     * Renders the offscreen bitmap
     *
     * @param  framesPerSec  an optional frame rate to display on the panel (if zero, frame rate
     *                       will not be displayed)
     */
    public void update(final double framesPerSec) {

        int width;
        int height;
        double rad;
        GridMember2Int member;
        LinkedListGridMember2D linked;
        DynamicGridMember2D dynamic;
        double minX;
        double minY;
        double cellSize;
        int xPix;
        int yPix;
        int xPix2;
        int yPix2;
        Ellipse2D circle;
        StringBuilder str;
        LineSegment2D seg;
        int count;
        Stroke orig;

        this.graphics.setColor(Color.WHITE);
        this.graphics.fillRect(0, 0, getWidth(), getHeight());

        width = this.grid.getGridWidth();
        height = this.grid.getGridHeight();

        minX = this.grid.getMinX();
        minY = this.grid.getMinY();
        cellSize = this.grid.getGridCellSize();

        if (DRAW_GRID) {
            this.graphics.setColor(this.gridColor);
            xPix = 0;
            yPix = (height * this.pixPerCell);

            for (int i = 0; i <= width; i++) {
                this.graphics.drawLine(xPix, 0, xPix, yPix);
                xPix += this.pixPerCell;
            }

            yPix = 0;
            xPix = (width * this.pixPerCell);

            for (int i = 0; i <= height; i++) {
                this.graphics.drawLine(0, yPix, xPix, yPix);
                yPix += this.pixPerCell;
            }
        }

        // Draw the items
        circle = new Ellipse2D.Double(0, 0, 1, 1);

        this.iter.reset();
        count = 0;

        while (this.iter.hasNext()) {
            member = this.iter.next();

            count++;

            this.graphics.setColor(member.getColor());
            xPix = (int) ((member.getPosX() - minX) / cellSize * this.pixPerCell);
            yPix = (height * this.pixPerCell)
                - (int) ((member.getPosY() - minY) / cellSize * this.pixPerCell);

            if (member instanceof LinkedListGridMember2D) {
                linked = (LinkedListGridMember2D) member;

                if (linked.getNext() != null) {
                    xPix2 = (int) ((linked.getNext().getPosX() - minX) / cellSize
                            * this.pixPerCell);
                    yPix2 = (height * this.pixPerCell)
                        - (int) ((linked.getNext().getPosY() - minY) / cellSize * this.pixPerCell);
                    orig = this.graphics.getStroke();
                    this.graphics.setStroke(this.stroke);
                    this.graphics.drawLine(xPix, yPix, xPix2, yPix2);
                    this.graphics.setStroke(orig);
                }

                if (CIRCLES_ON_LINKED) {
                    rad = member.getRadius() / this.grid.getGridCellSize() * this.pixPerCell;

                    if (rad < 0.75) {
                        this.graphics.drawLine(xPix, yPix, xPix, yPix);
                    } else {
                        circle.setFrame(xPix - rad, yPix - rad, 2 * rad, 2 * rad);

                        if (member.fillColor() != null) {
                            this.graphics.setColor(member.fillColor());
                            this.graphics.fill(circle);
                            this.graphics.setColor(member.getColor());
                        }

                        this.graphics.draw(circle);
                    }
                }
            } else {
                rad = member.getRadius() / this.grid.getGridCellSize() * this.pixPerCell;

                if (rad < 0.75) {
                    this.graphics.drawLine(xPix, yPix, xPix, yPix);
                } else {
                    circle.setFrame(xPix - rad, yPix - rad, 2 * rad, 2 * rad);

                    if (member.fillColor() != null) {
                        this.graphics.setColor(member.fillColor());
                        this.graphics.fill(circle);
                        this.graphics.setColor(member.getColor());
                    }

                    this.graphics.draw(circle);
                }
            }

            if ((this.vectorScale > 0) && (member instanceof DynamicGridMember2D)) {

                dynamic = (DynamicGridMember2D) member;

                xPix2 = (int) ((member.getPosX() + (dynamic.getXForce() * this.vectorScale) - minX)
                        / cellSize * this.pixPerCell);
                yPix2 = (height * this.pixPerCell)
                    - (int) ((member.getPosY() + (dynamic.getYForce() * this.vectorScale) - minY)
                        / cellSize * this.pixPerCell);
                seg = new LineSegment2D(xPix, yPix, xPix2, yPix2);
                seg.clip(0, 0, getWidth(), getHeight());
                xPix = (int) seg.getX1();
                yPix = (int) seg.getY1();
                xPix2 = (int) seg.getX2();
                yPix2 = (int) seg.getY2();
                this.graphics.drawLine(xPix, yPix, xPix2, yPix2);
            }
        }

        if (framesPerSec != 0) {
            str = new StringBuilder(24);
            str.append("Frames/Sec: ");
            str.append(Float.toString((float) framesPerSec));
            str.append(" (");
            str.append(count);
            str.append(" model elements)");
            this.graphics.setColor(Color.GRAY);
            this.graphics.drawString(str.toString(), 10, this.offscreen.getHeight() - 10);
        }

        repaint();
    }

    /**
     * Exports a single frame of animation to a JPEG file.
     *
     * @param  frameNum  The integer frame number
     */
    public void exportFrame(final int frameNum) {

        File file;
        ImageWriter writer = null;
        Iterator<?> writers;
        ImageOutputStream ios;

        file = new File("/imp/frames/frame_" + Integer.toString(frameNum / 1000)
                + Integer.toString((frameNum / 100) % 10) + Integer.toString((frameNum / 10) % 10)
                + Integer.toString(frameNum % 10) + ".png");

        synchronized (this) {
            writers = ImageIO.getImageWritersByFormatName("PNG");

            if (!writers.hasNext()) {
                LOG.warning("No PNG Writers Available");

                return;
            }

            writer = (ImageWriter) writers.next();

            if (file.exists()) {
                file.delete();
            }

            try {
                ios = ImageIO.createImageOutputStream(file);
                writer.setOutput(ios);
                writer.write(this.offscreen);
                ios.flush();
                writer.dispose();
                ios.close();
            } catch (IOException e) {
                LOG.log(Level.WARNING, "Exception writing frame", e);
            }
        }
    }

    /**
     * Handles mouse click events.
     *
     * @param  evt  the event
     */
    public void mouseClicked(final MouseEvent evt) {

        // No action
    }

    /**
     * Handles mouse press events.
     *
     * @param  evt  the event
     */
    public void mousePressed(final MouseEvent evt) {

        double xPos;
        double yPos;
        double radius;
        GridIterator2D iterator;
        GridMember2Int mem;

        // Get (X,Y) coordinates in object space of the mouse position
        xPos = ((double) evt.getX() / this.pixPerCell * this.grid.gridCellSize)
            + this.grid.getMinX();
        yPos = ((double) (getHeight() - evt.getY()) / this.pixPerCell * this.grid.gridCellSize)
            + this.grid.getMinY();
        radius = 3 * (this.grid.getMaxX() - this.grid.getMinX()) / getWidth();

        // See if there is a grid object within a couple of pixels of the press, and if so, set
        // that object as the one being dragged (NOTE: can't use global iterator since we're in
        // the AWT thread, and a non-AWT thread might be calling update)
        iterator = new GridIterator2D(this.grid);

        while (iterator.hasNext()) {
            mem = iterator.next();

            if ((mem.getPosX() >= (xPos - radius)) && (mem.getPosX() <= (xPos + radius))
                    && (mem.getPosY() >= (yPos - radius)) && (mem.getPosY() <= (yPos + radius))) {
                this.dragging = mem;

                break;
            }
        }
    }

    /**
     * Handles mouse release events.
     *
     * @param  evt  the event
     */
    public void mouseReleased(final MouseEvent evt) {

        this.dragging = null;
    }

    /**
     * Handles mouse enter events.
     *
     * @param  evt  the event
     */
    public void mouseEntered(final MouseEvent evt) {

        // No action
    }

    /**
     * Handles mouse exit events.
     *
     * @param  evt  the event
     */
    public void mouseExited(final MouseEvent evt) {

        // No action
    }

    /**
     * Handles mouse drag events.
     *
     * @param  evt  the event
     */
    public void mouseDragged(final MouseEvent evt) {

        double xPos;
        double yPos;

        if (this.dragging != null) {

            // Get (X,Y) coordinates in object space of the mouse position
            xPos = ((double) evt.getX() / this.pixPerCell * this.grid.gridCellSize)
                + this.grid.getMinX();
            yPos = ((double) (getHeight() - evt.getY()) / this.pixPerCell * this.grid.gridCellSize)
                + this.grid.getMinY();

            this.dragging.setPos(xPos, yPos);
        }
    }

    /**
     * Handles mouse move events.
     *
     * @param  evt  the event
     */
    public void mouseMoved(final MouseEvent evt) {

        // No action
    }
}
