// Copyright 2001-2006, FreeHEP.
package org.freehep.graphicsio.svg;

import java.awt.Font;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.font.TextAttribute;
import java.awt.font.TextLayout;
import java.awt.geom.AffineTransform;
import java.text.AttributedCharacterIterator.Attribute;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import org.freehep.graphics2d.font.FontUtilities;
import org.freehep.graphicsio.font.FontTable;

/**
 * A table to remember which glyphs were used while writing a svg file.
 * Entries are added by calling {@link #addGlyphs(String, java.awt.Font)}.
 * The final SVG tag for the <defs> section is generated using {@link #toString()}.
 * Use {@link #normalize(java.util.Map)} for referencing embedded glyphs
 * in <text> tags.
 *
 * @author Steffen Greiffenberg
 * @version $Id: freehep-graphicsio-svg/src/main/java/org/freehep/graphicsio/svg/SVGFontTable.java 4c4708a97391 2007/06/12 22:32:31 duns $
 */
public class SVGFontTable {

    /**
     * Stores fonts and a glyph-hashtable. The font key ist normalized using
     * {@link #untransform(java.awt.Font)}
     */
    private Hashtable/*<Font, Hashtable<String, SVGGlyph>*/<Font, Hashtable<String, SVGGlyph>> glyphs =
        new Hashtable/*<Font, Hashtable<String SVGGlyph>>*/<Font, Hashtable<String, SVGGlyph>>();

    /**
     * creates a glyph for the string character
     *
     * @param c
     * @param font
     * @return unique font name
     */
    private SVGGlyph addGlyph(int c, Font font) {
        // is the font stored?
        Hashtable/*<String, SVGGlyph>*/<String, SVGGlyph> glyphs = getGlyphs(font);

        // does a glyph allready exist?
        SVGGlyph result = glyphs.get(String.valueOf(c));

        // create a new one?
        if (result == null) {
            // create and store the SVG Glyph
            result = createGlyph(c, font);
            glyphs.put(String.valueOf(c), result);
        }

        return result;
    }

    /**
     * @param c
     * @param font
     * @return GlyphVector using a default rendering context
     */
    private SVGGlyph createGlyph(int c, Font font) {
        GlyphVector glyphVector = font.createGlyphVector(
            // flipping is done by SVGGlyph
            new FontRenderContext(null, true, true),
            // unicode to char
            String.valueOf((char) c));

        // create and store the SVG Glyph
        return new SVGGlyph(
            glyphVector.getGlyphOutline(0),
            c,
            glyphVector.getGlyphMetrics(0));
    }

    /**
     * creates the glyph for the string
     *
     * @param string
     * @param font
     */
    protected void addGlyphs(String string, Font font) {
        font = untransform(font);

        // add characters
        for (int i = 0; i < string.length(); i ++) {
            addGlyph(string.charAt(i), font);
        }
    }

    /**
     * @param font
     * @return glyph vectors for font
     */
    private Hashtable/*<String SVGGlyph>*/<String, SVGGlyph> getGlyphs(Font font) {
        // derive a default font for the font table
        font = untransform(font);

        Hashtable/*<String SVGGlyph>*/<String, SVGGlyph> result =
            glyphs.get(font);
        if (result == null) {
            result = new Hashtable/*<String SVGGlyph>*/<String, SVGGlyph>();
            glyphs.put(font, result);
        }
        return result;
    }

    /**
     * creates the font entry:
     * <PRE>
     * <font>
     * <glyph ... />
     * ...
     * </font>
     * </PRE>
     *
     * @return string representing the entry
     */
    public String toString() {
        StringBuffer result = new StringBuffer();

        Enumeration/*<Font>*/<Font> fonts = this.glyphs.keys();
        while (fonts.hasMoreElements()) {
            Font font = fonts.nextElement();

            // replace font family for svg
            Map /*<TextAttribute, ?>*/<Attribute, Object> attributes = FontUtilities.getAttributes(font);

            // Dialog -> Helvetica
            normalize(attributes);

            // familiy
            result.append("<font id=\"");
            result.append(attributes.get(TextAttribute.FAMILY));
            result.append("\">\n");

            // font-face
            result.append("<font-face font-family=\"");
            result.append(attributes.get(TextAttribute.FAMILY));
            result.append("\" ");

            // bold
            if (TextAttribute.WEIGHT_BOLD.equals(attributes.get(TextAttribute.WEIGHT))) {
                result.append("font-weight=\"bold\" ");
            } else {
                result.append("font-weight=\"normal\" ");
            }

            // italic
            if (TextAttribute.POSTURE_OBLIQUE.equals(attributes.get(TextAttribute.POSTURE))) {
                result.append("font-style=\"italic\" ");
            } else {
                result.append("font-style=\"normal\" ");
            }

            // size
            Float size = (Float) attributes.get(TextAttribute.SIZE);
            result.append("font-size=\"");
            result.append(SVGGraphics2D.fixedPrecision(size.floatValue()));
            result.append("\" ");

            // number of coordinate units on the em square,
            // the size of the design grid on which glyphs are laid out
            result.append("units-per-em=\"");
            result.append(SVGGraphics2D.fixedPrecision(SVGGlyph.FONT_SIZE));
            result.append("\" ");

            TextLayout tl = new TextLayout("By", font, new FontRenderContext(new AffineTransform(), true, true));

            // The maximum unaccented height of the font within the font coordinate system.
            // If the attribute is not specified, the effect is as if the attribute were set
            // to the difference between the units-per-em value and the vert-origin-y value
            // for the corresponding font.
            result.append("ascent=\"");
            result.append(tl.getAscent());
            result.append("\" ");

            // The maximum unaccented depth of the font within the font coordinate system.
            // If the attribute is not specified, the effect is as if the attribute were set
            // to the vert-origin-y value for the corresponding font.
            result.append("desscent=\"");
            result.append(tl.getDescent());
            result.append("\" ");

            // For horizontally oriented glyph layouts, indicates the alignment
            // coordinate for glyphs to achieve alphabetic baseline alignment.
            // result.append("alphabetic=\"0\"

            // close "<font-face"
            result.append("/>\n");

            // missing glyph
            SVGGlyph glyph = createGlyph(font.getMissingGlyphCode(), font);
            result.append("<missing-glyph ");
            result.append(glyph.getHorizontalAdvanceXString());
            result.append(" ");
            result.append(glyph.getPathString());
            result.append("/>\n");

            // regular glyphs
            Iterator<SVGGlyph> glyphs = getGlyphs(font).values().iterator();
            while (glyphs.hasNext()) {
                result.append(glyphs.next().toString());
                result.append("\n");
            }

            // close "<font>"
            result.append("</font>\n");
        }

        return result.toString();
    }

    /**
     * creates a font based on the parameter. The size will be {@link SVGGlyph#FONT_SIZE}
     * and transformation will be removed. Example:<BR>
     * <code>java.awt.Font[family=SansSerif,name=SansSerif,style=plain,size=30]</code><BR>
     * will result to:<BR>
     * <code>java.awt.Font[family=SansSerif,name=SansSerif,style=plain,size=100]</code><BR><BR>
     *
     * This method does not substitute font name or family.
     *
     * @param font
     * @return font based on the parameter
     */
    private Font untransform(Font font) {
        // replace font family
        Map<Attribute, Object> attributes = FontUtilities.getAttributes(font);

        // set default font size
        attributes.put(TextAttribute.SIZE, new Float(SVGGlyph.FONT_SIZE));

        // remove font transformation
        attributes.remove(TextAttribute.TRANSFORM);
        attributes.remove(TextAttribute.SUPERSCRIPT);

        return new Font(attributes);
    }

    /**
     * font replacements makes SVG in AdobeViewer look better, firefox replaces
     * all font settings, even the family fame
     */
    private static final Properties replaceFonts = new Properties();
    static {
        replaceFonts.setProperty("dialog", "Helvetica");
        replaceFonts.setProperty("dialoginput", "Courier New");
        // FIXME: works well on windows, others?
        // "TimesRoman" is not valid under Firefox 1.5
        replaceFonts.setProperty("serif", "Times");
        replaceFonts.setProperty("timesroman", "Times");
        replaceFonts.setProperty("sansserif", "Helvetica");
        // FIXME: works well on windows, others?
        // "Courier" is not valid under Firefox 1.5
        replaceFonts.setProperty("monospaced", "Courier New");
        // FIXME: replacement for zapfdingbats?
        replaceFonts.setProperty("zapfdingbats", "Wingdings");
    }

    /**
     * Replaces TextAttribute.FAMILY by values of replaceFonts. When a
     * font created using the result of this method the transformation would be:
     *
     * <code>java.awt.Font[family=SansSerif,name=SansSerif,style=plain,size=30]</code><BR>
     * will result to:<BR>
     * <code>java.awt.Font[family=SansSerif,name=Helvetica,style=plain,size=30]</code><BR><BR>
     *
     * Uses {@link FontTable#normalize(java.util.Map)} first.
     *
     * @param attributes with font name to change
     */
    public static void normalize(Map /*<TextAttribute, ?>*/<Attribute, Object> attributes) {
        // dialog.bold -> Dialog with TextAttribute.WEIGHT_BOLD
        FontTable.normalize(attributes);

        // get replaced font family name (Yes it's right, not the name!)
        String family = replaceFonts.getProperty(
            ((String) attributes.get(TextAttribute.FAMILY)).toLowerCase());
        if (family == null) {
            family = (String) attributes.get(TextAttribute.FAMILY);
        }
        
        // replace the family (Yes it's right, not the name!) in the attributes
        attributes.put(TextAttribute.FAMILY, family);
    }
}
