package com.srbenoit.modeling.cell;

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.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import com.srbenoit.log.LoggedPanel;

/**
 * A panel that can render cells.
 */
public class CellPanel extends LoggedPanel implements ComponentListener {

    /** version number for serialization */
    private static final long serialVersionUID = 5666568382362507929L;

    /** object on which to synchronize access to member variables */
    private final Object synch;

    /** the bounding rectangle after adjustment to match window's aspect ratio */
    private final Rectangle2D adjBounds;

    /** the current width of the offscreen images */
    private int width;

    /** the current height of the offscreen images */
    private int height;

    /** the most recently drawn image */
    private BufferedImage active;

    /** an image to which the next render should be directed */
    private BufferedImage passive;

    /** temporary storage for an incorrectly sized image rendered while size was changing */
    private BufferedImage temp;

    /** a color for actin */
    private final static Color actinColor = new Color(0, 120, 0);

    /**
     * Constructs a new <code>CellPanel</code>.
     *
     * @param  width   the preferred width of the window
     * @param  height  the preferred height of the window
     */
    public CellPanel(final int width, final int height) {

        super();

        this.synch = new Object();

        setBackground(Color.WHITE);
        setPreferredSize(new Dimension(width, height));
        setSize(width, height);
        this.adjBounds = new Rectangle2D.Double();

        this.active = null;
        this.passive = null;
        this.temp = null;
        this.width = 0;
        this.height = 0;
        updateOffscreenImages();

        addComponentListener(this);
    }

    /**
     * Tests the current size of the panel against the offscreen image sizes, and if needed,
     * creates new offscreen images. This is done in a synchronized block so a completing render
     * cannot stomp an image we create, and this method will not stomp the output of a render.
     */
    private void updateOffscreenImages() {

        int actWidth;
        int actHeight;
        Graphics grx;

        actWidth = getWidth();
        actHeight = getHeight();

        synchronized (this.synch) {

            if ((actWidth != this.width) || (actHeight != this.height)) {
                this.active = new BufferedImage(actWidth, actHeight, BufferedImage.TYPE_INT_RGB);
                this.passive = new BufferedImage(actWidth, actHeight, BufferedImage.TYPE_INT_RGB);

                this.width = actWidth;
                this.height = actHeight;

                grx = this.active.getGraphics();
                grx.setColor(Color.WHITE);
                grx.fillRect(0, 0, this.width, this.height);

                grx = this.passive.getGraphics();
                grx.setColor(Color.WHITE);
                grx.fillRect(0, 0, this.width, this.height);
            }
        }
    }

    /**
     * Draws the panel contents using the most up-to-date rendered image available.
     *
     * @param  grx  the <code>graphics</code> to which to draw.
     */
    @Override public void paintComponent(final Graphics grx) {

        int actWidth;
        int actHeight;
        double xScale;
        double yScale;
        double scale;
        int scaledWidth;
        int scaledHeight;

        synchronized (this.synch) {

            if (this.temp == null) {

                if (this.active != null) {
                    grx.drawImage(this.active, 0, 0, null);
                }

            } else {
                actWidth = getWidth();
                actHeight = getHeight();
                xScale = actWidth / this.temp.getWidth();
                yScale = actHeight / this.temp.getHeight();
                scale = (xScale > yScale) ? xScale : yScale;
                scaledWidth = (int) ((this.temp.getWidth() * scale) + 0.5); // at least actWidth
                scaledHeight = (int) ((this.temp.getHeight() * scale) + 0.5); // at least actHeight
                grx.drawImage(this.temp, (actWidth - scaledWidth) / 2,
                    (actHeight - scaledHeight) / 2, scaledWidth, scaledHeight, null);
            }
        }
    }

    /**
     * Handles panel resize events, which require allocation of new offscreen images.
     *
     * @param  evt  the component event
     */
    public void componentResized(final ComponentEvent evt) {

        updateOffscreenImages();
        repaint();
    }

    /**
     * Handles panel move events, which we ignore.
     *
     * @param  evt  the component event
     */
    public void componentMoved(final ComponentEvent evt) {
        // No action
    }

    /**
     * Handles panel shown events, which causes a test for actual size against current size of the
     * offscreen images, and may result in allocation of new offscreen images.
     *
     * <p>NOTE: Rendering is done outside the AWT event loop, but writes to the offscreen images.
     * At the end of a render process, if the rendered image is not the size specified in <code>
     * width</code> and <code>height</code>, we store the image in the <code>temp</code> member
     * variable. Otherwise, the end of the render cycle stores the result as the active image, and
     * clears the <code>temp</code> member. When a repaint is requested, if the <code>temp</code>
     * object has an image, that image is drawn, but scaled to fit the window.
     *
     * @param  evt  the component event
     */
    public void componentShown(final ComponentEvent evt) {

        updateOffscreenImages();
    }

    /**
     * Handles panel hidden events, which we ignore.
     *
     * @param  evt  the component event
     */
    public void componentHidden(final ComponentEvent evt) {
        // No action
    }

    /**
     * Updates the offscreen bitmap.
     *
     * @param  bounds    the bounds of model space to render
     * @param  cells     the list of cells to draw
     * @param  emitters  the list of emitters to draw
     * @param  walls     the list of walls to draw
     */
    public void render(final Rectangle2D bounds, final List<Cell> cells,
        final List<Emitter> emitters, final List<Wall> walls) {

        BufferedImage target;

        synchronized (this.synch) {
            target = this.passive;
        }

        if (target != null) {

            renderScene(bounds, target, cells, emitters, walls);

            synchronized (this.synch) {

                if (this.passive == target) {

                    // Images have not been reallocated, so just flip buffers
                    this.passive = this.active;
                    this.active = target;
                    this.temp = null;
                } else {

                    // A resize has resulted in new images, so place our results in "temp"
                    this.temp = target;
                }
            }
        }

        repaint();
    }

    /**
     * Draws the cells on the target image.
     *
     * @param  bounds    the bounds of model space to render
     * @param  target    the <code>BufferedImage</code> on which to draw.
     * @param  cells     the list of cells to render
     * @param  emitters  the list of emitters to render
     * @param  walls     the list of walls to render
     */
    private void renderScene(final Rectangle2D bounds, final BufferedImage target,
        final List<Cell> cells, final List<Emitter> emitters, final List<Wall> walls) {

        Graphics2D grx;
        int imgWidth;
        int imgHeight;
        double pixelScale;

        // Get the target size and adjust the bounds for the correct aspect ratio
        imgWidth = target.getWidth();
        imgHeight = target.getHeight();
        adjustBounds(imgWidth, imgHeight, bounds);
        pixelScale = imgWidth / adjBounds.getWidth();

        // Clear the image for rendering
        grx = (Graphics2D) target.getGraphics();
        grx.setColor(Color.WHITE);
        grx.fillRect(0, 0, imgWidth, imgHeight);
        grx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        grx.setStroke(new BasicStroke(0.4f));

        renderCells(imgWidth, imgHeight, grx, pixelScale, cells);
        renderEmitters(imgWidth, imgHeight, grx, pixelScale, emitters);
        renderWalls(imgWidth, imgHeight, grx, pixelScale, walls);
    }

    /**
     * Adjust the bound rectangle to have the proper target aspect ratio. The result is stored in
     * <code>adjBounds</code>.
     *
     * @param  imgWidth   the image width
     * @param  imgHeight  the image height
     * @param  bounds     the bounding rectangle
     */
    private void adjustBounds(final int imgWidth, final int imgHeight, final Rectangle2D bounds) {

        double aspect;
        double rectAspect;
        double newSize;
        double delta;

        aspect = (double) imgWidth / (double) imgHeight;
        rectAspect = bounds.getWidth() / bounds.getHeight();

        if (aspect > rectAspect) {

            // Make bounds wider to match aspect ratio
            newSize = bounds.getWidth() * (aspect / rectAspect);
            delta = (newSize - bounds.getWidth()) / 2;
            this.adjBounds.setFrame(bounds.getX() - delta, bounds.getY(), newSize,
                bounds.getHeight());
        } else {

            // Make bounds taller to match aspect ratio
            newSize = bounds.getHeight() * (rectAspect / aspect);
            delta = (newSize - bounds.getHeight()) / 2;
            this.adjBounds.setFrame(bounds.getX(), bounds.getY() - delta, bounds.getWidth(),
                newSize);
        }
    }

    /**
     * Renders the cells.
     *
     * @param  imgWidth    the width of the image to which to render
     * @param  imgHeight   the height of the image to which to render
     * @param  grx         the <code>Graphics2D</code> to which to draw
     * @param  pixelScale  the pixel scale (pixels per unit in model space)
     * @param  cells       the list of cells to render
     */
    private void renderCells(final int imgWidth, final int imgHeight, final Graphics2D grx,
        final double pixelScale, final List<Cell> cells) {

        MembraneElement mem;
        MembraneElement next;
        double xPix;
        double yPix;
        double xPix2;
        double yPix2;
        double xPos;
        double yPos;
        Ellipse2D ellipse;
        Line2D line;
        int count;
        ActinFilament actin;
        double rad;

        ellipse = new Ellipse2D.Double();
        line = new Line2D.Double();

        // Render the cells
        for (Cell cell : cells) {

            mem = cell.getMembrane();
            next = mem.getNext();

            for (;;) {
                xPos = mem.getPosX();
                yPos = mem.getPosY();

                xPix = (xPos - adjBounds.getX()) * pixelScale;
                yPix = imgHeight - ((yPos - adjBounds.getY()) * pixelScale);
                xPix2 = (next.getPosX() - adjBounds.getX()) * pixelScale;
                yPix2 = imgHeight - ((next.getPosY() - adjBounds.getY()) * pixelScale);

                // Draw the membrane element point
                grx.setColor(Color.BLUE);
                ellipse.setFrame(xPix - 1, yPix - 1, 2, 2);
                grx.fill(ellipse);
                line.setLine(xPix, yPix, xPix2, yPix2);
                grx.draw(line);

                // Draw the actin filament
                count = mem.getNumActin();

                for (int i = 0; i < count; i++) {
                    actin = mem.getActin(i);

                    while (actin != null) {

                        xPos = actin.getPosX();
                        yPos = actin.getPosY();
                        xPix2 = (xPos - adjBounds.getX()) * pixelScale;
                        yPix2 = imgHeight - ((yPos - adjBounds.getY()) * pixelScale);
                        rad = actin.getRadius() * pixelScale;

                        grx.setColor(actinColor);
                        ellipse.setFrame(xPix2 - rad, yPix2 - rad, 2 * rad, 2 * rad);
                        grx.draw(ellipse);
                        line.setLine(xPix, yPix, xPix2, yPix2);
                        grx.draw(line);

                        xPix = xPix2;
                        yPix = yPix2;

                        actin = actin.getDownstream();
                    }
                }

                mem = mem.getNext();
                next = mem.getNext();

                if (mem == cell.getMembrane()) {
                    break;
                }
            }
        }
    }

    /**
     * Renders the emitters.
     *
     * @param  imgWidth    the width of the image to which to render
     * @param  imgHeight   the height of the image to which to render
     * @param  grx         the <code>Graphics2D</code> to which to draw
     * @param  pixelScale  the pixel scale (pixels per unit in model space)
     * @param  emitters    the list of emitters to render
     */
    private void renderEmitters(final int imgWidth, final int imgHeight, final Graphics2D grx,
        final double pixelScale, final List<Emitter> emitters) {

        double xPix;
        double yPix;
        double xPos;
        double yPos;
        double xPix2;
        double yPix2;
        double xPos2;
        double yPos2;
        Ellipse2D ellipse;
        Line2D line;
        int count;
        Signal sig;
        FixedElement fixed;
        FixedElement next;

        ellipse = new Ellipse2D.Double();
        line = new Line2D.Double();

        // Render the emitters
        for (Emitter emitter : emitters) {

            xPos = emitter.getPosX();
            yPos = emitter.getPosY();

            xPix = (xPos - adjBounds.getX()) * pixelScale;
            yPix = imgHeight - ((yPos - adjBounds.getY()) * pixelScale);

            // Draw the emitter point
            grx.setColor(Color.MAGENTA);
            ellipse.setFrame(xPix - 2, yPix - 2, 4, 4);
            grx.fill(ellipse);

            // Draw the fixed ring
            grx.setColor(Color.GRAY);
            count = emitter.getNumFixed();

            for (int i = 0; i < count; i++) {
                fixed = emitter.getFixed(i);
                next = emitter.getFixed((i + 1) % count);

                xPos = fixed.getPosX();
                yPos = fixed.getPosY();

                xPix = (xPos - adjBounds.getX()) * pixelScale;
                yPix = imgHeight - ((yPos - adjBounds.getY()) * pixelScale);

                xPos2 = next.getPosX();
                yPos2 = next.getPosY();

                xPix2 = (xPos2 - adjBounds.getX()) * pixelScale;
                yPix2 = imgHeight - ((yPos2 - adjBounds.getY()) * pixelScale);

                ellipse.setFrame(xPix - 1, yPix - 1, 2, 2);
                grx.fill(ellipse);
                line.setLine(xPix, yPix, xPix2, yPix2);
                grx.draw(line);
            }

            // Draw the signals
            grx.setColor(Color.GRAY);
            count = emitter.getNumSignals();

            for (int i = 0; i < count; i++) {
                sig = emitter.getSignal(i);

                xPos = sig.getPosX();
                yPos = sig.getPosY();

                xPix = (xPos - adjBounds.getX()) * pixelScale;
                yPix = imgHeight - ((yPos - adjBounds.getY()) * pixelScale);

                ellipse.setFrame(xPix, yPix, 1, 1);
                grx.fill(ellipse);
            }
        }
    }

    /**
     * Renders the emitters.
     *
     * @param  imgWidth    the width of the image to which to render
     * @param  imgHeight   the height of the image to which to render
     * @param  grx         the <code>Graphics2D</code> to which to draw
     * @param  pixelScale  the pixel scale (pixels per unit in model space)
     * @param  walls       the list of walls to render
     */
    private void renderWalls(final int imgWidth, final int imgHeight, final Graphics2D grx,
        final double pixelScale, final List<Wall> walls) {

        double xPix;
        double yPix;
        double xPos;
        double yPos;
        double xPix2;
        double yPix2;
        double xPos2;
        double yPos2;
        Ellipse2D ellipse;
        Line2D line;
        int count;
        Signal sig;
        FixedElement fixed;
        FixedElement next;

        ellipse = new Ellipse2D.Double();
        line = new Line2D.Double();

        for (Wall wall : walls) {

            grx.setColor(Color.GRAY);
            count = wall.getNumFixed();

            for (int i = 0; i < count; i++) {
                fixed = wall.getFixed(i);

                xPos = fixed.getPosX();
                yPos = fixed.getPosY();
                xPix = (xPos - adjBounds.getX()) * pixelScale;
                yPix = imgHeight - ((yPos - adjBounds.getY()) * pixelScale);

                ellipse.setFrame(xPix - 1, yPix - 1, 2, 2);
                grx.fill(ellipse);

                if ((i + 1) < count) {
                    next = wall.getFixed(i + 1);
                    xPos2 = next.getPosX();
                    yPos2 = next.getPosY();
                    xPix2 = (xPos2 - adjBounds.getX()) * pixelScale;
                    yPix2 = imgHeight - ((yPos2 - adjBounds.getY()) * pixelScale);

                    line.setLine(xPix, yPix, xPix2, yPix2);
                    grx.draw(line);
                }
            }
        }
    }

    /**
     * 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.synch) {
            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.active);
                ios.flush();
                writer.dispose();
                ios.close();
            } catch (IOException e) {
                LOG.log(Level.WARNING, "Exception writing frame", e);
            }
        }
    }
}
