package com.srbenoit.font;

import java.awt.Font;
import java.awt.FontFormatException;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import com.srbenoit.log.LoggedObject;
import com.srbenoit.util.ResourceLoader;

/**
 * Provides font management for a packaged set of fonts. This allows fonts to be bundled with an
 * application, without dependence on a set of fonts being installed on a client machine.
 */
public final class BundledFontManager extends LoggedObject {

    /** object on which to synchronize static instance creation */
    private static final Object SYNCH = new Object();

    /** predefined font name for the default serif font */
    public static final String SERIF = "Times New Roman";

    /** predefined font name for the default serif font */
    public static final String SERIF_PREFIX = "Times";

    /** predefined font name for the default sans serif font */
    public static final String SANS = "Arial";

    /** predefined font name for the default sans serif font */
    public static final String SANS_PREFIX = "Arial";

    /** the singleton instance of the font manager */
    private static BundledFontManager instance = null;

    /** the <code>Graphics</code> of the offscreen buffered image */
    private transient Graphics grx;

    /** a map of 1-point fonts as read from the font directory */
    private final transient Map<String, Font> fonts;

    /** storage for error messages generated by the font manager */
    private final transient List<String> reasons;

    /** the names of the installed fonts */
    private transient String[] names = null;

    /**
     * Constructs a <code>BundledFontManager</code> object.
     */
    private BundledFontManager() {

        BufferedImage image;

        this.fonts = new HashMap<String, Font>(20);

        image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB);
        this.grx = image.getGraphics();
        this.reasons = new ArrayList<String>(20);
    }

    /**
     * Retrieves the singleton instance of the bundled font manager, creating it if it does not yet
     * exist.
     *
     * @return  the <code>BundledFontManager</code> instance
     */
    public static BundledFontManager getInstance() {

        synchronized (SYNCH) {

            if (instance == null) {
                instance = new BundledFontManager();
                instance.scanFonts();
            }
        }

        return instance;
    }

    /**
     * Scans the font directory, importing all fonts found.
     */
    private void scanFonts() {

        try {
            addFonts("minfonts2.list");
        } catch (IOException e) {
            logError(e.getLocalizedMessage());
        }
    }

    /**
     * Given the name of a file that contains a list of fonts, tries loading each font name found in
     * that file.
     *
     * @param   listFileName  the name of the file containing the font name list. This file will be
     *                        loaded as a resource, so it should be in a JAR that is in the
     *                        CLASSPATH
     * @throws  IOException  if there is an error reading the font list file or a font
     */
    private void addFonts(final String listFileName) throws IOException {

        Font onePoint;
        InputStream input;
        String[] fontsList;
        String fontName;
        String name;

        fontsList = ResourceLoader.loadFileLines(BundledFontManager.class, listFileName);

        if (fontsList == null) {
            logError(listFileName + " not found");
        } else {

            for (int i = 0; i < fontsList.length; i++) {
                fontName = fontsList[i];

                input = ResourceLoader.getInputStream(BundledFontManager.class, fontName);

                if (input != null) {

                    if (fontName.toLowerCase(Locale.getDefault()).endsWith(".ttf")) {

                        try {
                            onePoint = Font.createFont(Font.TRUETYPE_FONT, input);
                            name = onePoint.getFontName();

                            if (name.startsWith(SANS_PREFIX)) {
                                name = SANS;
                            } else if (name.startsWith(SERIF_PREFIX)) {
                                name = SERIF;
                            }

                            this.fonts.put(name, onePoint);
                        } catch (FontFormatException e) {
                            logError(e.getLocalizedMessage());
                        }
                    } else if ((fontName.toLowerCase(Locale.getDefault()).endsWith(".pfa"))
                            || (fontName.toLowerCase(Locale.getDefault()).endsWith(".pfb"))) {

                        try {
                            onePoint = Font.createFont(Font.TYPE1_FONT, input);
                            name = onePoint.getFontName();

                            if (name.startsWith(SANS_PREFIX)) {
                                name = SANS;
                            } else if (name.startsWith(SERIF_PREFIX)) {
                                name = SERIF;
                            }

                            this.fonts.put(name, onePoint);
                        } catch (FontFormatException e) {
                            logError(e.getLocalizedMessage());
                        }
                    }

                    input.close();
                }
            }
        }
    }

    /**
     * Tests whether a font name is valid.
     *
     * @param   name  the name of the font to test
     * @return  <code>true</code> if the name is valid, <code>false</code> otherwise
     */
    public boolean isFontNameValid(final String name) {

        boolean valid;

        if ("SERIF".equals(name) || "SANS".equals(name) || "MONOSPACE".equals(name)) {
            valid = true;
        } else {

            synchronized (this) {
                valid = this.fonts.containsKey(name);
            }
        }

        return valid;
    }

    /**
     * Generates a list of names of the installed fonts.
     *
     * @return  the list of font names
     */
    public String[] fontNames() {

        int inx = 0;
        String[] list;

        synchronized (this) {

            if (this.names == null) {
                this.names = new String[this.fonts.size()];

                for (String elem : this.fonts.keySet()) {
                    this.names[inx] = elem;
                    inx++;
                }

                Arrays.sort(this.names);
            }

            list = new String[this.names.length];
            System.arraycopy(this.names, 0, list, 0, list.length);
        }

        return list;
    }

    /**
     * Retrieves a particular font, in a particular size and style.
     *
     * @param   spec  the specification of the font to retrieve
     * @return  the generated font, or <code>null</code> if the name is not valid
     */
    public Font getFont(final FontSpec spec) {

        return getFont(spec.getFontName(), spec.getFontSize(), spec.getFontStyle());
    }

    /**
     * Retrieves a particular font, in a particular size and style.
     *
     * @param   name   the name of the font face to retrieve
     * @param   size   the point size to retrieve
     * @param   style  the style, as defined in the <code>Font</code> class
     * @return  the generated font, or <code>null</code> if the name is not valid
     */
    public Font getFont(final String name, final float size, final int style) {

        String actual;
        Font onePoint;
        Font derived;
        int style2;

        if ("SANS".equals(name)) {
            actual = SANS;
        } else if ("SERIF".equals(name)) {
            actual = SERIF;
        } else {
            actual = name;
        }

        style2 = style & (Font.BOLD | Font.ITALIC);

        onePoint = this.fonts.get(actual);

        if (onePoint == null) {

            // Emergency fall back
            derived = new Font(SANS, style2, (int) size);
        } else {
            derived = onePoint.deriveFont(style2, size);
        }

        return derived;
    }

    /**
     * Sets the internal <code>Graphics</code> used to generate font metrics.
     *
     * @param  graphics  the <code>Graphics</code> object
     */
    public void setGraphics(final Graphics graphics) {

        this.grx = graphics;
    }

    /**
     * Retrieves a font metrics object for a font, using the <code>Graphics</code> that is
     * associated with the offscreen image.
     *
     * @param   font  the font for which to get metrics
     * @return  the metrics for the font
     */
    public FontMetrics getFontMetrics(final Font font) {

        return this.grx.getFontMetrics(font);
    }

    /**
     * Adds an error message to the error log.
     *
     * @param  err  the error message
     */
    public void logError(final String err) {

        LOG.warning(err);
        this.reasons.add(err);
    }

    /**
     * Gets the list of errors that have been encountered by the font manager since it was
     * instantiated.
     *
     * @return  an array of <code>String</code> error messages
     */
    public String[] errors() {

        String[] result;

        result = new String[this.reasons.size()];
        this.reasons.toArray(result);

        return result;
    }

    /**
     * Main method for testing.
     *
     * @param  args  command-line arguments
     */
    public static void main(final String... args) {

        BundledFontManager obj;
        String[] names;
        int inx;

        obj = BundledFontManager.getInstance();
        names = obj.fontNames();

        for (inx = 0; inx < names.length; inx++) {
            LOG.info(obj.getFont(names[inx], 15.0f, Font.BOLD).getName());
        }
    }
}
