/*
 * $Id: NativeFont.java,v 1.4 2009-03-15 20:47:38 tomoke Exp $
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package com.sun.pdfview.font;

import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.OpenType;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

import com.sun.pdfview.PDFObject;
import com.sun.pdfview.PDFParseException;
import com.sun.pdfview.font.ttf.CMap;
import com.sun.pdfview.font.ttf.CMapFormat0;
import com.sun.pdfview.font.ttf.CMapFormat4;
import com.sun.pdfview.font.ttf.CmapTable;
import com.sun.pdfview.font.ttf.HeadTable;
import com.sun.pdfview.font.ttf.HmtxTable;
import com.sun.pdfview.font.ttf.NameTable;
import com.sun.pdfview.font.ttf.PostTable;
import com.sun.pdfview.font.ttf.TrueTypeFont;
import com.sun.pdfview.font.ttf.TrueTypeTable;

/**
 * a font object derived from a true type font.
 *
 * @author Mike Wessler
 */
public class NativeFont extends OutlineFont {

    /** Control characters to filter out of the underlying font */
    protected static final char[] controlChars = {0x9, 0xa, 0xd};

    /** the ids of our favorite CMaps */
    protected static final short[] mapIDs = {
        3, 1, /* Microsoft Unicode */
        0, 0, /* unicode default */
        0, 3, /* unicode 2.0 map */
        1, 0 /* macintosh */};

    /** the actual font in use */
    private Font f;

    /** the font render context */
    private FontRenderContext basecontext =
                              new FontRenderContext (new AffineTransform (),
            true, true);

    /** the cmap table from a TrueType font */
    private CmapTable cmapTable;

    /** the post table from a TrueType font */
    private PostTable postTable;

    /** the number of font units in one em */
    private int unitsPerEm;

    /** the hmtx table from the TrueType font */
    private HmtxTable hmtxTable;

    /**
     * create a new NativeFont object based on a description of the
     * font from the PDF file.  If the description happens to contain
     * an in-line true-type font file (under key "FontFile2"), use the
     * true type font.  Otherwise, parse the description for key information
     * and use that to generate an appropriate font.
     */
    public NativeFont (String baseFont, PDFObject fontObj,
                       PDFFontDescriptor descriptor)
            throws IOException {
        super (baseFont, fontObj, descriptor);

        String fontName = descriptor.getFontName ();

        PDFObject ttf = descriptor.getFontFile2 ();
        if (ttf != null) {
            byte[] fontdata = ttf.getStream ();

            try {
                setFont (fontdata);
            } catch (FontFormatException ffe) {
                throw new PDFParseException ("Font format exception: " + ffe);
            }
        } else {
            int flags = descriptor.getFlags ();
            int style = ((flags & PDFFontDescriptor.FORCEBOLD) != 0) ? Font.BOLD : Font.PLAIN;

            if (fontName.indexOf ("Bold") > 0) {
                style |= Font.BOLD;
            }
            if (descriptor.getItalicAngle () != 0) {
                style |= Font.ITALIC;
            }
            if ((flags & PDFFontDescriptor.FIXED_PITCH) != 0) { // fixed width
                setFont (new Font ("Monospaced", style, 1));
            } else if ((flags & PDFFontDescriptor.SERIF) != 0) {  // serif font
                setFont (new Font ("Serif", style, 1));
            } else {
                setFont (new Font ("Sans-serif", style, 1));
            }
        }
    }

    /**
     * Get a glyph outline by name
     *
     * @param name the name of the desired glyph
     * @return the glyph outline, or null if unavailable
     */
    protected GeneralPath getOutline (String name, float width) {
        if (postTable != null && cmapTable != null) {
            // map this character name to a glyph ID
            short glyphID = postTable.getGlyphNameIndex (name);

            if (glyphID == 0) {
                // no glyph -- try by index
                return null;
            }

            // the mapped character
            char mappedChar = 0;

            for (int i = 0; i < mapIDs.length; i += 2) {
                CMap map = cmapTable.getCMap (mapIDs[i], mapIDs[i + 1]);
                if (map != null) {
                    mappedChar = map.reverseMap (glyphID);

                    // we found a character
                    if (mappedChar != 0) {
                        break;
                    }
                }
            }

            return getOutline (mappedChar, width);
        }

        // no maps found, hope the font can deal
        return null;
    }

    /**
     * Get a glyph outline by character code
     *
     * Note this method must always return an outline 
     *
     * @param src the character code of the desired glyph
     * @return the glyph outline
     */
    protected GeneralPath getOutline (char src, float width) {
        // some true type fonts put characters in the undefined
        // region of Unicode instead of as normal characters.
        if (!f.canDisplay (src) && f.canDisplay ((char) (src + 0xf000))) {
            src += 0xf000;
        }

        // filter out control characters
        for (int i = 0; i < controlChars.length; i++) {
            if (controlChars[i] == src) {
                src = (char) (0xf000 | src);
                break;
            }
        }

        char[] glyph = new char[1];
        glyph[0] = src;

        GlyphVector gv = f.createGlyphVector (basecontext, glyph);
        GeneralPath gp = new GeneralPath (gv.getGlyphOutline (0));

        // this should be gv.getGlyphMetrics(0).getAdvance(), but that is
        // broken on the Mac, so we need to read the advance from the
        // hmtx table in the font
        CMap map = cmapTable.getCMap (mapIDs[0], mapIDs[1]);
        int glyphID = map.map (src);
        float advance = (float) hmtxTable.getAdvance (glyphID) / (float) unitsPerEm;

        float widthfactor = width / advance;
        gp.transform (AffineTransform.getScaleInstance (widthfactor, -1));

        return gp;
    }

    /**
     * Set the font
     *
     * @param f the font to use
     */
    protected void setFont (Font f) {
        this.f = f;

        // if it's an OpenType font, parse the relevant tables to get
        // glyph name to code mappings
        if (f instanceof OpenType) {
            OpenType ot = (OpenType) f;

            byte[] cmapData = ot.getFontTable (OpenType.TAG_CMAP);
            byte[] postData = ot.getFontTable (OpenType.TAG_POST);

            TrueTypeFont ttf = new TrueTypeFont (0x10000);

            cmapTable =
            (CmapTable) TrueTypeTable.createTable (ttf, "cmap",
                    ByteBuffer.wrap (cmapData));
            ttf.addTable ("cmap", cmapTable);

            postTable =
            (PostTable) TrueTypeTable.createTable (ttf, "post",
                    ByteBuffer.wrap (postData));
            ttf.addTable ("post", postTable);
        }
    }

    /**
     * Set the font
     *
     * @param fontdata the font data as a byte array
     */
    protected void setFont (byte[] fontdata)
            throws FontFormatException, IOException {

        // System.out.println("Loading " + getBaseFont());
        // FileOutputStream fos = new FileOutputStream("/tmp/" + getBaseFont() + ".ttf");
        // fos.write(fontdata);
        // fos.close();

        try {
            // read the true type information
            TrueTypeFont ttf = TrueTypeFont.parseFont (fontdata);

            // System.out.println(ttf.toString());

            // get the cmap, post, and hmtx tables for later use
            cmapTable = (CmapTable) ttf.getTable ("cmap");
            postTable = (PostTable) ttf.getTable ("post");
            hmtxTable = (HmtxTable) ttf.getTable ("hmtx");

            // read the units per em from the head table
            HeadTable headTable = (HeadTable) ttf.getTable ("head");
            unitsPerEm = headTable.getUnitsPerEm ();

            /* Find out if we have the right info in our name table.
             * This is a hack because Java can only deal with fonts that
             * have a Microsoft encoded name in their name table (PlatformID 3).
             * We'll 'adjust' the font to add it if not, and take our chances
             * with our parsing, since it wasn't going to work anyway.
             */
            NameTable nameTable = null;

            try {
                nameTable = (NameTable) ttf.getTable ("name");
            } catch (Exception ex) {
                System.out.println ("Error reading name table for font " +
                        getBaseFont () + ".  Repairing!");
            }

            boolean nameFixed = fixNameTable (ttf, nameTable);

            /* Figure out if we need to hack the CMap table.  This might
             * be the case if we use characters that Java considers control
             * characters (0x9, 0xa and 0xd), that have to be re-mapped
             */
            boolean cmapFixed = fixCMapTable (ttf, cmapTable);

            // use the parsed font instead of the original
            if (nameFixed || cmapFixed) {
                // System.out.println("Using fixed font!");
                // System.out.println(ttf.toString());
                fontdata = ttf.writeFont ();

                // FileOutputStream fos2 = new FileOutputStream("/tmp/" + getBaseFont() + ".fix");
                // fos2.write(fontdata);
                // fos2.close();
            }
        } catch (Exception ex) {
            System.out.println ("Error parsing font : " + getBaseFont ());
            ex.printStackTrace ();
        }

        ByteArrayInputStream bais = new ByteArrayInputStream (fontdata);
        f = Font.createFont (Font.TRUETYPE_FONT, bais);
        bais.close ();
    }

    /**
     * Fix a broken font name table for a TrueType font.  Some fonts do not
     * have Microsoft-specific name information, but Java won't work without
     * it (grrr.).  This method takes a font and adds the Microsoft data into
     * it.
     *
     * @param ttf the font
     * @param name the font's name table
     * @return true if the table was fixed, or false if it was left as is
     */
    private boolean fixNameTable (TrueTypeFont ttf, NameTable name) {
        // if we didn't find the table, or there was an exception,
        // just create a new one
        if (name == null) {
            name = (NameTable) TrueTypeTable.createTable (ttf, "name");
            ttf.addTable ("name", name);
        }

        // first, figure out some info about the font
        String fName = this.getBaseFont ();
        String style = "Regular";

        if (fName.indexOf ("Italic") > -1 || fName.indexOf ("italic") > -1) {
            style = "Italic";
        } else if (fName.indexOf ("Bold") > -1 || fName.indexOf ("bold") > -1) {
            style = "Bold";
        }

        if (fName.indexOf ('-') > -1) {
            fName = fName.substring (0, fName.indexOf ('-'));
        }

        short platID = NameTable.PLATFORMID_MICROSOFT;
        short encID = 1;
        short langID = 1033;

        short[] nameIDs = {
            NameTable.NAMEID_COPYRIGHT,
            NameTable.NAMEID_FAMILY,
            NameTable.NAMEID_SUBFAMILY,
            NameTable.NAMEID_SUBFAMILY_UNIQUE,
            NameTable.NAMEID_FULL_NAME,
            NameTable.NAMEID_VERSION,
            NameTable.NAMEID_POSTSCRIPT_NAME,
            NameTable.NAMEID_TRADEMARK
        };

        String[] defaultValues = {
            "No copyright",
            fName,
            style,
            fName + " " + style,
            fName + " " + style,
            "1.0 (Fake)",
            fName,
            "No Trademark"
        };

        boolean changed = false;

        for (int i = 0; i < nameIDs.length; i++) {
            if (name.getRecord (platID, encID, langID, nameIDs[i]) == null) {
                name.addRecord (platID, encID, langID, nameIDs[i],
                        defaultValues[i]);
                changed = true;
            }
        }

        return changed;
    }

    /**
     * Fix the CMap table.  This can be necessary if characters are mapped to
     * control characters (0x9, 0xa, 0xd) Java will not render them, even 
     * though they are valid.
     *
     * Also, Java tends to not like it when there is only a Format 0 CMap,
     * which happens frequently when included Format 4 CMaps are broken.
     * Since PDF prefers the Format 0 map, while Java prefers the Format 4 map,
     * it is generally necessary to re-write the Format 0 map as a Format 4 map
     * to make most PDFs work.
     *
     * @param ttf the font
     * @param cmap the CMap table
     * @return true if the font was changed, or false if it was left as-is
     */
    private boolean fixCMapTable (TrueTypeFont ttf, CmapTable cmap) {
        CMapFormat4 fourMap = null;
        CMapFormat0 zeroMap = null;

        for (int i = 0; i < mapIDs.length; i += 2) {
            CMap map = cmapTable.getCMap (mapIDs[i], mapIDs[i + 1]);
            if (map != null) {
                if (fourMap == null && map instanceof CMapFormat4) {
                    fourMap = (CMapFormat4) map;
                } else if (zeroMap == null && map instanceof CMapFormat0) {
                    zeroMap = (CMapFormat0) map;
                }
            }
        }

        // if there were no maps, we could have problems.  Just try creating
        // an identity map
        if (zeroMap == null && fourMap == null) {
            fourMap = (CMapFormat4) CMap.createMap ((short) 4, (short) 0);
            fourMap.addSegment ((short) getFirstChar (),
                    (short) getLastChar (),
                    (short) 0);
        }

        // create our map based on the type 0 map, since PDF seems
        // to prefer a type 0 map (Java prefers a unicode map)
        if (zeroMap != null) {
            fourMap = (CMapFormat4) CMap.createMap ((short) 4, (short) 0);

            // add the mappings from 0 to null and 1 to notdef
            fourMap.addSegment ((short) 0, (short) 1, (short) 0);

            for (int i = getFirstChar (); i <= getLastChar (); i++) {
                short value = (short) (zeroMap.map ((byte) i) & 0xff);
                if (value != 0) {
                    fourMap.addSegment ((short) i, (short) i,
                            (short) (value - i));
                }
            }
        }

        // now that we have a type four map, remap control characters
        for (int i = 0; i < controlChars.length; i++) {
            short idx = (short) (0xf000 | controlChars[i]);
            short value = (short) fourMap.map (controlChars[i]);

            fourMap.addSegment (idx, idx, (short) (value - idx));
        }

        // create a whole new table with just our map
        cmap = (CmapTable) TrueTypeTable.createTable (ttf, "cmap");
        cmap.addCMap ((short) 3, (short) 1, fourMap);

        // replace the table in the font
        ttf.addTable ("cmap", cmap);

        // change the stored table
        cmapTable = cmap;

        return true;
    }
}
