package com.srbenoit.font;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.Iterator;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.FileImageOutputStream;
import com.srbenoit.log.LoggedPanel;

/**
 * A panel that renders the glyphs of a font.
 */
public class GlyphPanel extends LoggedPanel {

    /** version number for serialization */
    private static final long serialVersionUID = -3511066856984634391L;

    /** the owning frame */
    private final transient ViewerInt ownerFrame;

    /** the font to be rendered */
    private transient Font font = null;

    /** an offscreen image that is rendered to */
    private transient BufferedImage offscreen = null;

    /** the width of the box for each glyph */
    private transient int boxWidth;

    /** the height of the box for each glyph */
    private transient int boxHeight;

    /** flag to control whether bounds are drawn around characters */
    private transient boolean drawBoxes = false;

    /** flag indicating offscreen image needs to be repainted */
    private transient boolean dirty = false;

    /**
     * Constructs a new <code>GlyphPanel</code>.
     *
     * @param  owner  the viewer that owns this panel
     */
    public GlyphPanel(final ViewerInt owner) {

        super();

        this.ownerFrame = owner;

        setPreferredSize(new Dimension(640, 480));
        setBackground(Color.white);
    }

    /**
     * Sets the font that the panel will render.
     *
     * @param  theFont  the font
     */
    public void setTheFont(final Font theFont) {

        this.font = theFont;
        this.dirty = true;
    }

    /**
     * Sets the flag controlling whether or not bounds are drawn.
     *
     * @param  drawBoundsBoxes  <code>true</code> to draw bounds boxes; <code>false</code>
     *                          otherwise
     */
    public void setDrawBoundsBoxes(final boolean drawBoundsBoxes) {

        this.drawBoxes = drawBoundsBoxes;
        this.dirty = true;
    }

    /**
     * Gets the state of the flag controlling whether or not bounds are drawn.
     *
     * @return  <code>true</code> if bounding boxes are being drawn; <code>false</code> otherwise
     */
    public boolean isDrawingBoundsBoxes() {

        return this.drawBoxes;
    }

    /**
     * Redraws the panel. If this is the first time paint has been called since the font was
     * changed, the offscreen glyph image is created.
     *
     * @param  grx  the <code>Graphics</code> object to render to
     */
    @Override public void paint(final Graphics grx) {

        grx.setColor(Color.white);
        grx.fillRect(0, 0, getWidth(), getHeight());

        if (this.dirty) {

            if (this.offscreen != null) {
                this.offscreen.flush();
            }

            buildOffscreen((Graphics2D) grx);
            this.dirty = false;
        }

        grx.drawImage(this.offscreen, 0, 0, this);
    }

    /**
     * Creates an offscreen image with the glyph renderings.
     *
     * @param  grx  the <code>Graphics2D</code> object to render to
     */
    private void buildOffscreen(final Graphics2D grx) {

        char[] chars;
        Font labelFont;
        FontMetrics fmLabel;
        FontMetrics fmFont;
        int columns;
        int imgW;
        int imgH;
        int rows;
        Graphics2D g2d;

        // Get the characters the font supports
        chars = getFontSupportedChars(this.font);

        // Create a font to be used for labeling
        labelFont = new Font("Dialog", Font.PLAIN, 9);
        fmLabel = grx.getFontMetrics(labelFont);
        fmFont = grx.getFontMetrics(this.font);

        // Find box width based on max size of label or glyph
        this.boxWidth = fmLabel.stringWidth("9999");

        if (this.boxWidth < (int) (fmFont.getMaxCharBounds(grx).getWidth() + 0.9)) {
            this.boxWidth = (int) (fmFont.getMaxCharBounds(grx).getWidth() + 0.9);
        }

        this.boxWidth++; // add a pixel per box for left border

        // Using box width, and assuming right border, find boxes per row
        columns = 639 / this.boxWidth;

        // Compute total width of image, including borders
        imgW = (this.boxWidth * columns) + 1; // add a pixel for right border

        // Determine the number of rows
        rows = chars.length / columns; // full rows

        if ((rows * columns) < chars.length) {
            rows++; // partial row
        }

        // Compute box height. Note we don't need descent on label since
        // digits don't extend below baseline, but we add 1 pixel below,
        // along with 1 pixel for an interior border line.
        this.boxHeight = fmFont.getHeight() + fmLabel.getAscent();
        this.boxHeight += 2; // Add top border and border between glyph and

        // label
        imgH = (this.boxHeight * rows) + 1; // add a pixel for bottom border

        // Add space for a line for the font name
        imgH += fmFont.getHeight() + fmFont.getLeading();

        g2d = createOffscreen(imgW, imgH);
        drawGrid(g2d, fmFont);
        g2d.setFont(labelFont);
        drawLabels(g2d, chars, fmFont, fmLabel);

        makeImage(g2d, fmFont, chars);

        setPreferredSize(new Dimension(imgW, imgH));
        this.ownerFrame.updateScroller(this.boxHeight);
    }

    /**
     * Creates the offscreen image and build its graphics object.
     *
     * @param   imgW  the image width
     * @param   imgH  the image height
     * @return  the <code>Graphics2D</code> for the image
     */
    private Graphics2D createOffscreen(final int imgW, final int imgH) {

        Graphics2D g2d;

        this.offscreen = new BufferedImage(imgW, imgH, BufferedImage.TYPE_INT_RGB);

        g2d = (Graphics2D) (this.offscreen.getGraphics());
        g2d.setBackground(Color.white);
        g2d.clearRect(0, 0, imgW, imgH);

        return g2d;
    }

    /**
     * Draws the grid on the image.
     *
     * @param  g2d     the <code>Graphics2D</code> to which to draw
     * @param  fmFont  the metrics of the font being rendered
     */
    private void drawGrid(final Graphics2D g2d, final FontMetrics fmFont) {

        int inx;
        int yPos;
        int rows;
        int columns;

        rows = this.offscreen.getHeight() / this.boxHeight;
        columns = this.offscreen.getWidth() / this.boxWidth;

        yPos = fmFont.getHeight();

        // Draw grid
        for (inx = 0; inx <= rows; inx++) {
            g2d.setColor(Color.lightGray);
            g2d.fillRect(0, yPos + (inx * this.boxHeight) + fmFont.getHeight(),
                this.offscreen.getWidth(), this.boxHeight - fmFont.getHeight());
            g2d.setColor(Color.black);
            g2d.drawLine(0, yPos + (inx * this.boxHeight), this.offscreen.getWidth(),
                yPos + (inx * this.boxHeight));
            g2d.setColor(Color.gray);
            g2d.drawLine(0, yPos + (inx * this.boxHeight) + fmFont.getHeight(),
                this.offscreen.getWidth(), yPos + (inx * this.boxHeight) + fmFont.getHeight());
        }

        g2d.setColor(Color.BLACK);

        for (inx = 0; inx <= columns; inx++) {
            g2d.drawLine(inx * this.boxWidth, yPos, inx * this.boxWidth,
                yPos + this.offscreen.getHeight() - fmFont.getHeight() - fmFont.getLeading() - 1);
        }
    }

    /**
     * Draws the labels on the image.
     *
     * @param  g2d      the <code>Graphics2D</code> to which to draw
     * @param  chars    the characters supported by the font
     * @param  fmFont   the metrics of the font being rendered
     * @param  fmLabel  the metrics of the label font
     */
    private void drawLabels(final Graphics2D g2d, final char[] chars, final FontMetrics fmFont,
        final FontMetrics fmLabel) {

        int xPos;
        int yPos;
        int inx;
        String str;

        yPos = fmFont.getHeight();
        xPos = 0;

        for (inx = 0; inx < chars.length; inx++) {
            str = Integer.toHexString(chars[inx]);
            g2d.drawString(str, xPos + 1 + ((this.boxWidth - fmLabel.stringWidth(str)) / 2),
                yPos + fmFont.getHeight() + fmLabel.getAscent());

            xPos += this.boxWidth;

            if (xPos >= (this.offscreen.getWidth() - 1)) {
                xPos = 0;
                yPos += this.boxHeight;
            }
        }
    }

    /**
     * Draws the image with a grid with labels and all the glyphs.
     *
     * @param  g2d     the <code>Graphics</code> to which to draw
     * @param  fmFont  the metrics for the font being displayed
     * @param  chars   the characters supported by the font
     */
    private void makeImage(final Graphics2D g2d, final FontMetrics fmFont, final char[] chars) {

        FontRenderContext frc;
        int inx;
        int xPos;
        int yPos;
        String str;
        int pixX;
        int pixY;
        char[] chr;

        try {
            g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                RenderingHints.VALUE_TEXT_ANTIALIAS_LCD_HRGB);
        } catch (NoSuchFieldError e) {
            g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        }

        // Draw the line of example text
        g2d.setFont(this.font);
        g2d.setColor(Color.black);
        str = this.font.getName() + ", " + this.font.getSize() + " point ";
        g2d.drawString(str, 5, fmFont.getAscent() + (fmFont.getLeading() / 2));
        xPos = 5 + fmFont.stringWidth(str);
        g2d.setFont(this.font.deriveFont(Font.BOLD));
        str = " Bold, ";
        g2d.drawString(str, xPos, fmFont.getAscent() + (fmFont.getLeading() / 2));
        xPos += g2d.getFontMetrics().stringWidth(str);
        g2d.setFont(this.font.deriveFont(Font.ITALIC));
        str = " Italic";
        g2d.drawString(str, xPos, fmFont.getAscent() + (fmFont.getLeading() / 2));
        xPos += g2d.getFontMetrics().stringWidth(str);

        g2d.setFont(this.font);

        // Draw glyphs
        xPos = 0;
        yPos = fmFont.getHeight();

        g2d.setFont(this.font);
        chr = new char[1];

        for (inx = 0; inx < chars.length; inx++) {
            chr[0] = chars[inx];

            pixX = xPos + 1 + ((this.boxWidth - fmFont.charWidth(chr[0])) / 2);
            pixY = yPos + fmFont.getAscent();

            // Draw a glyph bounds box around the character, if configured
            if (this.drawBoxes) {
                g2d.setColor(Color.LIGHT_GRAY);
                frc = g2d.getFontRenderContext();
                g2d.draw(this.font.createGlyphVector(frc, chr).getPixelBounds(frc, pixX, pixY));
                g2d.setColor(Color.BLACK);
            }

            // Draw the character
            g2d.drawChars(chr, 0, 1, pixX, pixY);

            xPos += this.boxWidth;

            if (xPos >= (this.offscreen.getWidth() - 1)) {
                xPos = 0;
                yPos += this.boxHeight;
            }
        }
    }

    /**
     * Gets the list of characters that a font supports.
     *
     * @param   fnt  the font
     * @return  the list of supported characters
     */
    private char[] getFontSupportedChars(final Font fnt) {

        StringBuilder builder;
        char[] chars;

        builder = new StringBuilder(200);

        for (char c = 0; c < 0xFFFF; c++) {

            if (fnt.canDisplay(c)) {
                builder.append(c);
            }
        }

        chars = builder.toString().toCharArray();

        return chars;
    }

    /**
     * Exports the image as a JPEG file.
     *
     * @param  target  the file to write to
     */
    public void export(final File target) {

        Iterator<ImageWriter> iter;
        ImageWriter writer;
        FileImageOutputStream fios;

        iter = ImageIO.getImageWritersByFormatName("png");

        if (iter.hasNext()) {
            writer = iter.next();

            try {
                fios = new FileImageOutputStream(target);
                writer.setOutput(fios);
                writer.write(this.offscreen);
                fios.close();
            } catch (Exception exc) {
                LOG.log(Level.WARNING, "Failed to write {0}: {1}",
                    new Object[] { target.getAbsolutePath(), exc.getLocalizedMessage() });
            }
        }
    }
}
