/*
 * $Id: TTFFont.java,v 1.11 2009-03-19 04:03:54 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.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.util.Collection;
import java.util.Collections;

import com.sun.pdfview.PDFObject;
import com.sun.pdfview.font.ttf.*;

/**
 * A true-type font
 */
public class TTFFont extends OutlineFont {

    /** the truetype font itself */
    private TrueTypeFont font;

    /** the number of units per em in the font */
    private float unitsPerEm;

    public TTFFont (String baseFont, PDFObject fontObj,
                    PDFFontDescriptor descriptor) throws IOException {
        this(baseFont, fontObj, descriptor, null);
    }
    /**
     * create a new TrueTypeFont 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 TTFFont (String baseFont, PDFObject fontObj,
                    PDFFontDescriptor descriptor, File fontFile)
            throws IOException {
        super (baseFont, fontObj, descriptor);

        String fontName = descriptor.getFontName ();
        PDFObject ttfObj = descriptor.getFontFile2 ();

        // try {
        //    byte[] fontData = ttfObj.getStream();
        //    java.io.FileOutputStream fis = new java.io.FileOutputStream("/tmp/" + fontName + ".ttf");
        //    fis.write(fontData);
        //    fis.flush();
        //    fis.close();
        // } catch (Exception ex) {
        //    ex.printStackTrace();
        // }
        if (ttfObj != null || fontFile != null) {
            if (ttfObj != null) {
                font = TrueTypeFont.parseFont (ttfObj.getStreamBuffer ());
            } else {
                final RandomAccessFile raFile = fontFile != null ? new RandomAccessFile(fontFile, "r") : null;
                final FileChannel fc = raFile.getChannel();
                try {
                    MappedByteBuffer mappedFont = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
                    font = TrueTypeFont.parseFont(mappedFont);
                    mappedFont = null;
                } finally {
                    try {
                        fc.close();
                    } catch (IOException ioEx) {
                        // swallow
                    }
                    try {
                        raFile.close();
                    } catch (IOException ioEx) {
                        // swallow
                    }
                }
            }
            // read the units per em from the head table
            HeadTable head = (HeadTable) font.getTable ("head");
            unitsPerEm = head.getUnitsPerEm ();
        } else {
            font = null;
        }
//        System.out.println ("TTFFont: ttfObj: " + ttfObj + ", fontName: " + fontName);

    }

    public Collection<String> getNames()
    {
        return font.getNames();
    }

    /**
     * Get the outline of a character given the character code
     */
    protected synchronized GeneralPath getOutline (char src, float width) {
        // find the cmaps
        CmapTable cmap = (CmapTable) font.getTable ("cmap");

        // if there are no cmaps, this is (hopefully) a cid-mapped font,
        // so just trust the value we were given for src
        if (cmap == null) {
            return getOutline ((int) src, width);
        }

        CMap[] maps = cmap.getCMaps ();

        // try the maps in order
        for (int i = 0; i < maps.length; i++) {
            int idx = maps[i].map (src);
            if (idx != 0) {
                return getOutline (idx, width);
            }
        }

        // not found, return the empty glyph
        return getOutline (0, width);
    }

    /**
     * lookup the outline using the CMAPs, as specified in 32000-1:2008,
     * 9.6.6.4, when an Encoding is specified.
     * 
     * @param val
     * @param width
     * @return GeneralPath
     */
    protected synchronized GeneralPath getOutlineFromCMaps (char val,
                                                            float width) {
        // find the cmaps
        CmapTable cmap = (CmapTable) font.getTable ("cmap");

        if (cmap == null) {
            return null;
        }

        // try maps in required order of (3, 1), (1, 0)
        CMap map = cmap.getCMap ((short) 3, (short) 1);
        if (map == null) {
            map = cmap.getCMap ((short) 1, (short) 0);
        }
        int idx = map.map (val);
        if (idx != 0) {
            return getOutline (idx, width);
        }

        return null;
    }

    /**
     * Get the outline of a character given the character name
     */
    protected synchronized GeneralPath getOutline (String name, float width) {
        int idx;
        PostTable post = (PostTable) font.getTable ("post");
        if (post != null) {
            idx = post.getGlyphNameIndex (name);
            if (idx != 0) {
                return getOutline (idx, width);
            }
            return null;
        }

        Integer res = AdobeGlyphList.getGlyphNameIndex (name);
        if (res != null) {
            idx = res;
            return getOutlineFromCMaps ((char) idx, width);
        }
        return null;
    }

    /**
     * Get the outline of a character given the glyph id
     */
    protected synchronized GeneralPath getOutline (int glyphId, float width) {
        // find the glyph itself
        GlyfTable glyf = (GlyfTable) font.getTable ("glyf");
        Glyf g = glyf.getGlyph (glyphId);

        GeneralPath gp = null;
        if (g instanceof GlyfSimple) {
            gp = renderSimpleGlyph ((GlyfSimple) g);
        } else if (g instanceof GlyfCompound) {
            gp = renderCompoundGlyph (glyf, (GlyfCompound) g);
        } else {
            gp = new GeneralPath ();
        }

        // calculate the advance
        HmtxTable hmtx = (HmtxTable) font.getTable ("hmtx");
        float advance = (float) hmtx.getAdvance (glyphId) / (float) unitsPerEm;

        // scale the glyph to match the desired advance
        float widthfactor = width / advance;

        // the base transform scales the glyph to 1x1
        AffineTransform at = AffineTransform.getScaleInstance (1 / unitsPerEm,
                1 / unitsPerEm);
        at.concatenate (AffineTransform.getScaleInstance (widthfactor, 1));

        gp.transform (at);

        return gp;
    }

    /**
     * Render a simple glyf
     */
    protected GeneralPath renderSimpleGlyph (GlyfSimple g) {
        // the current contour
        int curContour = 0;

        // the render state
        RenderState rs = new RenderState ();
        rs.gp = new GeneralPath ();

        for (int i = 0; i < g.getNumPoints (); i++) {
            PointRec rec = new PointRec (g, i);

            if (rec.onCurve) {
                addOnCurvePoint (rec, rs);
            } else {
                addOffCurvePoint (rec, rs);
            }

            // see if we just ended a contour
            if (i == g.getContourEndPoint (curContour)) {
                curContour++;

                if (rs.firstOff != null) {
                    addOffCurvePoint (rs.firstOff, rs);
                }

                if (rs.firstOn != null) {
                    addOnCurvePoint (rs.firstOn, rs);
                }

                rs.firstOn = null;
                rs.firstOff = null;
                rs.prevOff = null;
            }
        }

        return rs.gp;
    }

    /**
     * Render a compound glyf
     */
    protected GeneralPath renderCompoundGlyph (GlyfTable glyf, GlyfCompound g) {
        GeneralPath gp = new GeneralPath ();

        for (int i = 0; i < g.getNumComponents (); i++) {
            // find and render the component glyf
            Glyf gl = glyf.getGlyph (g.getGlyphIndex (i));
            GeneralPath path = null;
            if (gl instanceof GlyfSimple) {
                path = renderSimpleGlyph ((GlyfSimple) gl);
            } else if (gl instanceof GlyfCompound) {
                path = renderCompoundGlyph (glyf, (GlyfCompound) gl);
            } else {
                throw new RuntimeException (
                        "Unsupported glyph type " + gl.getClass ().getCanonicalName ());
            }

            // multiply the translations by units per em
            double[] matrix = g.getTransform (i);

            // transform the path
            path.transform (new AffineTransform (matrix));

            // add it to the global path
            gp.append (path, false);
        }

        return gp;
    }

    /** add a point on the curve */
    private void addOnCurvePoint (PointRec rec, RenderState rs) {
        // if the point is on the curve, either move to it,
        // or draw a line from the previous point
        if (rs.firstOn == null) {
            rs.firstOn = rec;
            rs.gp.moveTo (rec.x, rec.y);
        } else if (rs.prevOff != null) {
            rs.gp.quadTo (rs.prevOff.x, rs.prevOff.y, rec.x, rec.y);
            rs.prevOff = null;
        } else {
            rs.gp.lineTo (rec.x, rec.y);
        }
    }

    /** add a point off the curve */
    private void addOffCurvePoint (PointRec rec, RenderState rs) {
        if (rs.prevOff != null) {
            PointRec oc = new PointRec ((rec.x + rs.prevOff.x) / 2,
                    (rec.y + rs.prevOff.y) / 2,
                    true);
            addOnCurvePoint (oc, rs);
        } else if (rs.firstOn == null) {
            rs.firstOff = rec;
        }
        rs.prevOff = rec;
    }

    class RenderState {
        // the shape itself

        GeneralPath gp;
        // the first off and on-curve points in the current segment

        PointRec firstOn;

        PointRec firstOff;
        // the previous off and on-curve points in the current segment

        PointRec prevOff;

    }

    /** a point on the stack of points */
    class PointRec {

        int x;

        int y;

        boolean onCurve;

        public PointRec (int x, int y, boolean onCurve) {
            this.x = x;
            this.y = y;
            this.onCurve = onCurve;
        }

        public PointRec (GlyfSimple g, int idx) {
            x = g.getXCoord (idx);
            y = g.getYCoord (idx);
            onCurve = g.onCurve (idx);
        }
    }
}
