package fonttest;


import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;

import javax.imageio.ImageIO;


public class FontTextureTest {
	public static final int MAX_FONT_STYLES=	256;
	public static final int MAX_TEXTURES=		256;

	public static final int PIXEL_A=0, PIXEL_RGBA=1;
	
	public static final int
		STYLE_REGULAR=		0,
		STYLE_BOLD=			1,
		STYLE_ITALIC=		2,
		STYLE_BOLD_ITALIC=	3;
	
    public static final String STYLE_STRING[]={ "regular", "bold", "italic", "bold-italic" };
    
	public static int
		TEXTURE_SINGLE				=0, // All characters of all styles on a single texture.
		TEXTURE_SINGLE_PER_STYLE	=1, // All characters of a style on a single texture.
		TEXTURE_NEW_PER_STYLE		=2, // Each style has its own set of textures.
		TEXTURE_SHARED				=3; // All styles shared the textures.
	
	public static final int HORIZONTAL_SPACING=1;
	public static final int VERTICAL_SPACING=1;
	
	public static class Parameters {
		public String fontName;
		public int size;
		public int blocks[][];
		public int textureWidth, textureHeight;
		public int textureOption;
		public boolean antialiasFlag;
	}
		
	public static class Style {
		public int ascent, descent, leading, height;
		public int maxAscent, maxDescent, maxAdvance;
		public int maxWidth, maxHeight;
		public int nbCharacters;
		public Glyph characters[];		
	}
	
	public static class Glyph {
		public int codePoint;
		public short tx, ty, tw, th; // Coordinates in texture.
		public short x, y; // Reference point.
		public short advance;
		public short texture;
	}
	
	public static class Texture {
		public int width, height;
		public BufferedImage data;
//		public byte data[];
	}
	
	public boolean debugFlag=false;
	public int pixelFormat=PIXEL_A;
	public int nbStyles;
	public Style styles[]=new Style[MAX_FONT_STYLES];
	public int nbTextures;
	public Texture textures[]=new Texture[MAX_TEXTURES];

	
	//
	// Data used for construction.
	//
		
	// Data for the current font style.
	private Font font;
	private int nbRequestedCharacters;
	private int requestedCharacters[];		
	private BufferedImage image;
	private Graphics2D graphics;
	private FontRenderContext fontRenderContext;
	private FontMetrics fontMetrics;
	private int lastTextureY=0;

    //--------------------------------------------------------------------------------
	// Test.
    //--------------------------------------------------------------------------------
    public static void main(String[] args) {
        try {
        	buildTest("DejaVuLGC_Sans-regular", 20, LGC_BLOCKS, 1024, 1024, true);
        } catch(Exception e) {
            e.printStackTrace();
            return;
        }

    }
	
    // Blocks for Latin-Greek-Cyrillic languages and some symbols.
    private static final int LGC_BLOCKS[][]={
    	{ -1, -1 },
    	{ 0x00000000, 0x0000007F }, // Basic Latin
    	{ 0x00000080, 0x000000FF }, // Latin-1 Supplement
    	{ 0x00000100, 0x0000017F }, // Latin Extended-A
    	{ 0x00000180, 0x0000024F }, // Latin Extended-B
    	{ 0x00000250, 0x000002AF }, // IPA Extensions
    	{ 0x00000370, 0x000003FF }, // Greek and Coptic
    	{ 0x00000400, 0x000004FF }, // Cyrillic
    	{ 0x00000500, 0x0000052F }, // Cyrillic Supplement
    	{ 0x00001E00, 0x00001EFF }, // Latin Extended Additional
    	{ 0x00001F00, 0x00001FFF }, // Greek Extended
    	{ 0x00002000, 0x0000206F }, // General Punctuation
    	{ 0x000020A0, 0x000020CF }, // Currency Symbols
    	{ 0x00002100, 0x0000214F }, // Letterlike Symbols
    	{ 0x00002190, 0x000021FF }, // Arrows
    	{ 0x00002200, 0x000022FF }, // Mathematical Operators
    	{ 0x000025A0, 0x000025FF }, // Geometric Shapes
    	{ 0x00002600, 0x000026FF }, // Miscellaneous Symbols
    	{ 0x00002700, 0x000027BF }, // Dingbats
    	{ 0x00002B00, 0x00002BFF }, // Miscellaneous Symbols and Arrows
    	{ 0x00002C60, 0x00002C7F }, // Latin Extended-C
    };

    private static void buildTest(String fontName, int size, int blocks[][], int textureWidth, int textureHeight, boolean antialiasFlag) {
    	FontTextureTest fontTexture=new FontTextureTest(FontTextureTest.PIXEL_RGBA, true);
    	FontTextureTest.Parameters p=new FontTextureTest.Parameters();
    	p.size=size;
    	p.blocks=blocks;
    	p.textureWidth=textureWidth; p.textureHeight=textureHeight;
    	p.textureOption=FontTextureTest.TEXTURE_SINGLE_PER_STYLE;
    	p.antialiasFlag=antialiasFlag;
   	    p.fontName=fontName+".ttf";
    	fontTexture.addStyle(p);
    	fontTexture.printInfos();
    	saveFontTextureImages(fontTexture, fontName, size);
    }
    
    private static void saveFontTextureImages(FontTextureTest ft, String fontName, int size) {
    	for (int i=0; i<ft.nbTextures; i++) {
	        saveImage(fontName+"-"+size+"-("+i+").png", ft.textures[i].data);
    	}    	
    }
    
    private static boolean saveImage(String filename, BufferedImage img) {
        try {
            File file=new File(filename);
            ImageIO.write(img, "png", file);
        } catch (IOException e) {
            System.out.println("Cannot write destination image.");
            return true;
        }
        return false;
    }

    
	public FontTextureTest(int pixelFormat, boolean debugFlag) {
		this.pixelFormat=pixelFormat;
		this.debugFlag=debugFlag;
	}
	
    //--------------------------------------------------------------------------------
	// Style.
    //--------------------------------------------------------------------------------
    public int addStyle(Parameters p) {
    	if (nbStyles>MAX_FONT_STYLES) {
    		System.out.println("Too many font styles.");
    		return -1;
    	}
    	
    	// Create the list of requested characters.
    	if (createCharacterList(p.blocks)) return -1;
    	
    	// Load the font.
    	if ((font=createFont(p.fontName, p.size))==null) return -1;
    	
        // Create an image.
        if (image==null || p.textureOption==TEXTURE_SINGLE_PER_STYLE || p.textureOption==TEXTURE_NEW_PER_STYLE) {
        	if (createTexture(p)) return -1;
        } else {
        	graphics.setFont(font);
            fontRenderContext=graphics.getFontRenderContext();
            fontMetrics=graphics.getFontMetrics();
        }
        
    	Style fs=new Style();

        // Retrieve font infos.
        {
	        Rectangle2D maxCharBounds=fontMetrics.getMaxCharBounds(graphics);
	        fs.ascent=fontMetrics.getAscent();
	        fs.descent=fontMetrics.getDescent();
	        fs.leading=fontMetrics.getLeading();
	        fs.height=fontMetrics.getHeight();
	        fs.maxAscent=fontMetrics.getMaxAscent();
	        fs.maxDescent=fontMetrics.getMaxDescent();
	        fs.maxAdvance=fontMetrics.getMaxAdvance();
	        fs.maxWidth=(int)Math.ceil(maxCharBounds.getMaxX()-maxCharBounds.getMinX());
	        fs.maxHeight=(int)Math.ceil(maxCharBounds.getMaxY()-maxCharBounds.getMinY());
	        if (fs.height>fs.maxHeight) fs.maxHeight=fs.height;
        }

        // Build the texture and the character array.
        Glyph characters[]=new Glyph[nbRequestedCharacters];
        int nbEffectiveCharacters=0, nbUndisplayableCharacters=0, nbInvalidCharacters=0;
   	 	int characterX=0, characterY=lastTextureY;
        for (int i=0; i<nbRequestedCharacters; i++) {
        	int codePoint=requestedCharacters[i];
        	int characterWidth, characterHeight, characterAdvance;
        	GlyphVector glyph=null;
        	Rectangle characterBounds=null;
        	
        	if (codePoint>=0) {
	        	if (!Character.isDefined(codePoint)) {
//            		System.out.println("Invalid codepoint: "+codePoint);
	        		continue;
	        	}
	        	if (!font.canDisplay(codePoint)) {
//            		System.out.println("Cannot display codepoint: "+codePoint);
	        		nbUndisplayableCharacters++;
	        		continue;
	        	}
	        	glyph=font.createGlyphVector(fontRenderContext, Character.toChars(codePoint));
        	} else {
        		// Missing glyph.
        		int missingGlyph[]={ font.getMissingGlyphCode() };
        		glyph=font.createGlyphVector(fontRenderContext, missingGlyph);
        	}
        	
        	// Get character infos.
       	 	characterBounds=glyph.getGlyphPixelBounds(0, fontRenderContext, 0, fs.maxAscent);
        	characterWidth=characterBounds.width; characterHeight=characterBounds.height; characterAdvance=fontMetrics.charWidth(codePoint);        	
        	// Check if the character is valid.
        	if (characterWidth<0 || characterWidth>fs.maxWidth) {
        		System.out.println("Invalid character width: codePoint="+codePoint+"; width="+characterWidth);
        		nbInvalidCharacters++; continue;
        	}
        	if (characterHeight<0 || characterHeight>fs.maxHeight) {
        		System.out.println("Invalid character height: codePoint="+codePoint+"; height="+characterHeight);
        		nbInvalidCharacters++; continue;
        	}
        	if (characterAdvance<0 || characterAdvance>fs.maxAdvance) {
               	System.out.println("Invalid character advance: codePoint="+codePoint+"; advance="+characterAdvance);
        		nbInvalidCharacters++; continue;
        	}
        	
           	// Create a new character.
        	Glyph gi=new Glyph();
        	characters[nbEffectiveCharacters++]=gi;
        	gi.codePoint=codePoint;
        	gi.advance=(short)characterAdvance;
        	
        	boolean hasGlyph=(characterWidth>0 && characterHeight>0);
        	if (hasGlyph) {
            	// Check if the character can fit on the current line.
            	int nextCharacterX=characterX+HORIZONTAL_SPACING+characterWidth+HORIZONTAL_SPACING;
           		int nextCharacterY=characterY+VERTICAL_SPACING+fs.maxHeight+VERTICAL_SPACING;
               	if (nextCharacterX>=textures[nbTextures-1].width) {
               		if (nextCharacterY>=textures[nbTextures-1].height) {
               			if (p.textureOption==TEXTURE_SINGLE || p.textureOption==TEXTURE_SINGLE_PER_STYLE) {
                   			System.out.println("Cannot store all characters on the same texture.");
               				break;
               			} else if (createTexture(p)) return -1;
               			nextCharacterY=lastTextureY;
               		}
               		characterX=0; nextCharacterX=HORIZONTAL_SPACING+characterWidth+HORIZONTAL_SPACING;
               		characterY=nextCharacterY;
               	}
               	
//            	characterHeight=fs.maxHeight;
            	gi.tx=(short)(characterX+HORIZONTAL_SPACING); gi.ty=(short)(characterY+characterBounds.y+VERTICAL_SPACING);
            	gi.tw=(short)(characterWidth); gi.th=(short)(characterHeight);
            	gi.x=(short)(characterBounds.x); gi.y=(short)(characterBounds.y-fs.maxAscent);
            	gi.texture=(short)(nbTextures-1);
            	
            	// Draw the character.
            	if (debugFlag) {
		        	graphics.setColor(Color.RED);
		           	graphics.drawRect(gi.tx-HORIZONTAL_SPACING, gi.ty-VERTICAL_SPACING, gi.tw-1+2*HORIZONTAL_SPACING, gi.th-1+2*VERTICAL_SPACING);
            	}
	           	if (glyph!=null) {
		            int drawX=characterX+HORIZONTAL_SPACING-characterBounds.x, drawY=characterY+VERTICAL_SPACING+fs.maxAscent;
		        	graphics.setColor(Color.WHITE);
		           	graphics.drawGlyphVector(glyph, drawX, drawY);
	           	}
	           	
	           	characterX=nextCharacterX;
        	} else {
            	gi.tx=0; gi.ty=0;
            	gi.tw=0; gi.th=0;
            	gi.x=0; gi.y=0;
            	gi.texture=(short)(-1);
        	}
        }
        
        fs.nbCharacters=nbEffectiveCharacters;
        fs.characters=characters;
        styles[nbStyles++]=fs;
        
        characterY+=VERTICAL_SPACING+fs.maxHeight+VERTICAL_SPACING;
        lastTextureY=characterY;
        
        System.out.println("Undisplayable: "+nbUndisplayableCharacters+"; Invalid: "+nbInvalidCharacters);
        System.out.println("Total texture height: "+characterY);
        System.out.println();
        
        return nbStyles-1;
    }
    
    private static Font createFont(String fontName, int size) {
    	Font originalFont;
    	try {
	    	File file=new File(fontName);
	    	originalFont=Font.createFont(Font.TRUETYPE_FONT, file);
    	} catch (Exception e) {
    		System.out.println("Cannot open file: "+fontName);
    		return null;
    	}    	
    	Font font=originalFont.deriveFont((float)size);
    	System.out.println("Building: "+fontName);
    	System.out.println("Family: "+font.getFamily()+"; Face name: "+font.getFontName()+"; Logical name: "+font.getName()+"; Size: "+font.getSize());
    	System.out.println("Num glyphs: "+font.getNumGlyphs());	
        return font;
    }
    
    private boolean createCharacterList(int blocks[][]) {
        // Count characters.
        nbRequestedCharacters=0;
        for (int i=0; i<blocks.length; i++) {
        	int n=blocks[i][1]-blocks[i][0];
        	if (n<0) {
        		System.out.println("Invalid block: "+i+"; Range: "+blocks[i][0]+"-"+blocks[i][1]);
        		return true;
        	}
        	nbRequestedCharacters+=n+1;
        }
        if (nbRequestedCharacters<=0) {
        	System.out.println("The number of characters is 0.");
        	return true;
        }
        
        // Build characters list.
        requestedCharacters=new int[nbRequestedCharacters];
        {
	        int k=0;
	        for (int i=0; i<blocks.length; i++) {
	        	int n=blocks[i][1]-blocks[i][0];
	        	for (int j=0; j<=n; j++) requestedCharacters[k+j]=blocks[i][0]+j;
	        	k+=n+1;
	        }
        }

        System.out.println("Requested characters: "+nbRequestedCharacters);
        return false;
    }
    
    private boolean createTexture(Parameters p) {
    	if (nbTextures>=MAX_TEXTURES) {
    		System.out.println("Too many textures");
    		return true;
    	}
    	
    	switch (pixelFormat) {
    	case PIXEL_A:
        	image=new java.awt.image.BufferedImage(p.textureWidth, p.textureHeight, BufferedImage.TYPE_BYTE_GRAY);
        	break;
    	case PIXEL_RGBA:
        	image=new java.awt.image.BufferedImage(p.textureWidth, p.textureHeight, BufferedImage.TYPE_4BYTE_ABGR);
        	break;
        default: return true;
    	}
    	lastTextureY=0;
    	
        graphics=image.createGraphics();
    	graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
        if (p.antialiasFlag) {
        	graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        	graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        }
        graphics.setFont(font);
        fontRenderContext=graphics.getFontRenderContext();
        fontMetrics=graphics.getFontMetrics();
    	
    	Texture texture=new Texture();
    	textures[nbTextures++]=texture;
    	texture.width=p.textureWidth; texture.height=p.textureHeight;
    	texture.data=image;
    	
    	return false;
    }

    //--------------------------------------------------------------------------------
    // Infos.
    //--------------------------------------------------------------------------------
    private static final String PIXEL_FORMAT_STRING[]={ "A", "RGBA" };
    
    public void printInfos() {
    	System.out.println("Pixel format: "+PIXEL_FORMAT_STRING[pixelFormat]);
    	System.out.println("Num styles: "+nbStyles);
    	System.out.println("Num textures: "+nbTextures);
    	System.out.println();
    	for (int i=0; i<nbStyles; i++) {
    		if (styles[i]!=null) printInfos(i);
    	}
    }
    
    public void printInfos(int f) {
    	Style fs=styles[f];
    	System.out.println("Num characters: "+fs.nbCharacters);
    	System.out.println("Ascent: "+fs.ascent+"; Descent: "+fs.descent+"; Leading: "+fs.leading+"; Height: "+fs.height);
    	System.out.println("Max ascent: "+fs.maxAscent+"; Max descent: "+fs.maxDescent+"; Max advance: "+fs.maxAdvance);    	
    	System.out.println("Max width: "+fs.maxWidth+"; Max height: "+fs.maxHeight);
    	System.out.println();
    }
    
    public void printInfos(Glyph ci) {
    	System.out.println("codepoint: "+ci.codePoint+"; tx="+ci.tx+"; ty="+ci.ty+"; tw="+ci.tw+"; th="+ci.th+"; x="+ci.x+"; y="+ci.y);
    }
}
