/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* $Id: FontInfo.java 1761026 2016-09-16 12:50:43Z ssteiner $ */

package org.apache.fop.fonts;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;


/**
 * The FontInfo holds font information for the layout and rendering of a fo document.
 * This stores the list of available fonts that are setup by
 * the renderer. The font name can be retrieved for the
 * family style and weight.
 * <br>
 * Currently font supported font-variant small-caps is not
 * implemented.
 */
public class FontInfo {

    /** logging instance */
    protected static final Log log = LogFactory.getLog(FontInfo.class);

    /** Map containing fonts that have been used */
    private Map<String, Typeface> usedFonts; //(String = font key)

    /** look up a font-triplet to find a font-name */
    private Map<FontTriplet, String> triplets; //(String = font key)

    /** look up a font-triplet to find its priority
     *  (only used inside addFontProperties()) */
    private Map<FontTriplet, Integer> tripletPriorities; //Map<FontTriplet,Integer>

    /** look up a font-name to get a font (that implements FontMetrics at least) */
    private Map<String, Typeface> fonts; //(String = font key)

    /** Cache for Font instances. */
    private Map<FontTriplet, Map<Integer, Font>> fontInstanceCache;

    /** Event listener for font events */
    private FontEventListener eventListener;

    /**
     * Main constructor
     */
    public FontInfo() {
        this.triplets = new HashMap<FontTriplet, String>();
        this.tripletPriorities = new HashMap<FontTriplet, Integer>();
        this.fonts = new HashMap<String, Typeface>();
        this.usedFonts = new HashMap<String, Typeface>();
    }

    /**
     * Sets the font event listener that can be used to receive events about particular events
     * in this class.
     * @param listener the font event listener
     */
    public void setEventListener(FontEventListener listener) {
        this.eventListener = listener;
    }

    /**
     * Checks if the font setup is valid (At least the ultimate fallback font
     * must be registered.)
     * @return True if valid
     */
    public boolean isSetupValid() {
        //We're only called when font setup is done:
        tripletPriorities = null; // candidate for garbage collection
        return triplets.containsKey(Font.DEFAULT_FONT);
    }

    /**
     * Adds a new font triplet.
     * @param name internal key
     * @param family font family name
     * @param style font style (normal, italic, oblique...)
     * @param weight font weight
     */
    public void addFontProperties(String name, String family, String style, int weight) {
        addFontProperties(name, createFontKey(family, style, weight));
    }

    /**
     * Adds a series of new font triplets given an array of font family names.
     * @param name internal key
     * @param families an array of font family names
     * @param style font style (normal, italic, oblique...)
     * @param weight font weight
     */
    public void addFontProperties(String name, String[] families, String style, int weight) {
        for (String family : families) {
            addFontProperties(name, family, style, weight);
        }
    }

    /**
     * Adds a new font triplet.
     * @param internalFontKey internal font key
     * @param triplet the font triplet to associate with the internal key
     */
    public void addFontProperties(String internalFontKey, FontTriplet triplet) {
        /*
         * add the given family, style and weight as a lookup for the font
         * with the given name
         */
        if (log.isDebugEnabled()) {
            log.debug("Registering: " + triplet + " under " + internalFontKey);
        }
        String oldName = triplets.get(triplet);
        int newPriority = triplet.getPriority();
        if (oldName != null) {
            int oldPriority = tripletPriorities.get(triplet);
            if (oldPriority < newPriority) {
                logDuplicateFont(triplet, false, oldName, oldPriority, internalFontKey, newPriority);
                return;
            } else {
                logDuplicateFont(triplet, true, oldName, oldPriority, internalFontKey, newPriority);
            }
        }
        this.triplets.put(triplet, internalFontKey);
        this.tripletPriorities.put(triplet, newPriority);
    }

    /**
     * Log warning about duplicate font triplets.
     *
     * @param triplet the duplicate font triplet
     * @param replacing true iff the new font will replace the old one
     * @param oldKey the old internal font name
     * @param oldPriority the priority of the existing font mapping
     * @param newKey the new internal font name
     * @param newPriority the priority of the duplicate font mapping
     */
    private void logDuplicateFont(FontTriplet triplet, boolean replacing, String oldKey, int oldPriority,
            String newKey, int newPriority) {
        if (log.isDebugEnabled()) {
            log.debug(triplet
                    + (replacing ? ": Replacing " : ": Not replacing ")
                    + fonts.get(triplets.get(triplet)).getFullName()
                    + " (priority=" + oldPriority + ") by "
                    + fonts.get(newKey).getFullName()
                    + " (priority=" + newPriority + ")");
        }
    }

    /**
     * Adds font metrics for a specific font.
     * @param internalFontKey internal key
     * @param metrics metrics to register
     */
    public void addMetrics(String internalFontKey, FontMetrics metrics) {
        // add the given metrics as a font with the given name

        if (metrics instanceof Typeface) {
            ((Typeface)metrics).setEventListener(this.eventListener);
        }
        this.fonts.put(internalFontKey, (Typeface)metrics);
    }

    /**
     * Lookup a font.
     * <br>
     * Locate the font name for a given family, style and weight.
     * The font name can then be used as a key as it is unique for
     * the associated document.
     * This also adds the font to the list of used fonts.
     * @param family font family
     * @param style font style
     * @param weight font weight
     * @param substitutable true if the font may be substituted with the
     *                  default font if not found
     * @return internal font triplet key
     */
    private FontTriplet fontLookup(String family, String style, int weight, boolean substitutable) {
        if (log.isTraceEnabled()) {
            log.trace("Font lookup: " + family + " " + style + " " + weight
                    + (substitutable ? " substitutable" : ""));
        }

        FontTriplet startKey = createFontKey(family, style, weight);
        FontTriplet fontTriplet = startKey;
        // first try given parameters
        String internalFontKey = getInternalFontKey(fontTriplet);
        if (internalFontKey == null) {
            fontTriplet = fuzzyFontLookup(family, style, weight, startKey, substitutable);
        }

        if (fontTriplet != null) {
            if (fontTriplet != startKey) {
                notifyFontReplacement(startKey, fontTriplet);
            }
            return fontTriplet;
        } else {
            return null;
        }
    }

    private FontTriplet fuzzyFontLookup(String family, String style,
            int weight, FontTriplet startKey, boolean substitutable) {
        FontTriplet key;
        String internalFontKey = null;
        if (!family.equals(startKey.getName())) {
            key = createFontKey(family, style, weight);
            internalFontKey = getInternalFontKey(key);
            if (internalFontKey != null) {
                return key;
            }
        }

        // adjust weight, favouring normal or bold
        key = findAdjustWeight(family, style, weight);
        if (key != null) {
            internalFontKey = getInternalFontKey(key);
        }

        // return null if not found and not substitutable
        if (!substitutable && internalFontKey == null) {
            return null;
        }

        // only if the font may be substituted
        // fallback 1: try the same font-family and weight with default style
        if (internalFontKey == null && !style.equals(Font.STYLE_NORMAL)) {
            key = createFontKey(family, Font.STYLE_NORMAL, weight);
            internalFontKey = getInternalFontKey(key);
        }

        // fallback 2: try the same font-family with default style and try to adjust weight
        if (internalFontKey == null && !style.equals(Font.STYLE_NORMAL)) {
            key = findAdjustWeight(family, Font.STYLE_NORMAL, weight);
            if (key != null) {
                internalFontKey = getInternalFontKey(key);
            }
        }

        // fallback 3: try any family with original style/weight
        if (internalFontKey == null) {
            return fuzzyFontLookup("any", style, weight, startKey, false);
        }

        // last resort: use default
        if (key == null && internalFontKey == null) {
            key = Font.DEFAULT_FONT;
            internalFontKey = getInternalFontKey(key);
        }

        if (internalFontKey != null) {
            return key;
        } else {
            return null;
        }
    }

    /**
     * Tells this class that the font with the given internal name has been used.
     * @param internalName the internal font name (F1, F2 etc.)
     */
    public void useFont(String internalName) {
        usedFonts.put(internalName, fonts.get(internalName));
    }

    private Map<FontTriplet, Map<Integer, Font>> getFontInstanceCache() {
        if (fontInstanceCache == null) {
            fontInstanceCache = new HashMap<FontTriplet, Map<Integer, Font>>();
        }
        return fontInstanceCache;
    }

    /**
     * Retrieves a (possibly cached) Font instance based on a FontTriplet and a font size.
     *
     * @param triplet the font triplet designating the requested font
     * @param fontSize the font size
     * @return the requested Font instance
     */
    public Font getFontInstance(FontTriplet triplet, int fontSize) {
        Map<Integer, Font> sizes = getFontInstanceCache().get(triplet);
        if (sizes == null) {
            sizes = new HashMap<Integer, Font>();
            getFontInstanceCache().put(triplet, sizes);
        }
        Integer size = fontSize;
        Font font = sizes.get(size);
        if (font == null) {
            String fontKey = getInternalFontKey(triplet);
            useFont(fontKey);
            FontMetrics metrics = getMetricsFor(fontKey);
            font = new Font(fontKey, triplet, metrics, fontSize);
            sizes.put(size, font);
        }
        return font;
    }

    private List<FontTriplet> getTripletsForName(String fontName) {
        List<FontTriplet> matchedTriplets = new ArrayList<FontTriplet>();
        for (FontTriplet triplet : triplets.keySet()) {
            String tripletName = triplet.getName();
            if (tripletName.toLowerCase().equals(fontName.toLowerCase())) {
                matchedTriplets.add(triplet);
            }
        }
        return matchedTriplets;
    }

    /**
     * Returns a suitable internal font given an AWT Font instance.
     *
     * @param awtFont the AWT font
     * @return a best matching internal Font
     */
    public Font getFontInstanceForAWTFont(java.awt.Font awtFont) {
        String awtFontName = awtFont.getName();
        String awtFontFamily = awtFont.getFamily();
        String awtFontStyle = awtFont.isItalic() ? Font.STYLE_ITALIC : Font.STYLE_NORMAL;
        int awtFontWeight = awtFont.isBold() ? Font.WEIGHT_BOLD : Font.WEIGHT_NORMAL;

        FontTriplet matchedTriplet = null;
        List<FontTriplet> triplets = getTripletsForName(awtFontName);
        if (!triplets.isEmpty()) {
            for (FontTriplet triplet : triplets) {
                boolean styleMatched = triplet.getStyle().equals(awtFontStyle);
                boolean weightMatched = triplet.getWeight() == awtFontWeight;
                if (styleMatched && weightMatched) {
                    matchedTriplet = triplet;
                    break;
                }
            }
        }

        // not matched on font name so do a lookup using family
        if (matchedTriplet == null) {
            if (awtFontFamily.equals("sanserif")) {
                awtFontFamily = "sans-serif";
            }
            matchedTriplet = fontLookup(awtFontFamily, awtFontStyle, awtFontWeight);
        }
        int fontSize = Math.round(awtFont.getSize2D() * 1000);
        return getFontInstance(matchedTriplet, fontSize);
    }

    /**
     * Lookup a font.
     * <br>
     * Locate the font name for a given family, style and weight.
     * The font name can then be used as a key as it is unique for
     * the associated document.
     * This also adds the font to the list of used fonts.
     * @param family font family
     * @param style font style
     * @param weight font weight
     * @return the font triplet of the font chosen
     */
    public FontTriplet fontLookup(String family, String style, int weight) {
        return fontLookup(family, style, weight, true);
    }

    private List<FontTriplet> fontLookup(String[] families, String style, int weight, boolean substitutable) {
        List<FontTriplet> matchingTriplets = new ArrayList<FontTriplet>();
        FontTriplet triplet = null;
        for (String family : families) {
            triplet = fontLookup(family, style, weight, substitutable);
            if (triplet != null) {
                matchingTriplets.add(triplet);
            }
        }
        return matchingTriplets;
    }

    /**
     * Looks up a set of fonts.
     * <br>
     * Locate the font name(s) for the given families, style and weight.
     * The font name(s) can then be used as a key as they are unique for
     * the associated document.
     * This also adds the fonts to the list of used fonts.
     * @param families  font families (priority list)
     * @param style     font style
     * @param weight    font weight
     * @return the set of font triplets of all supported and chosen font-families
     *          in the specified style and weight.
     */
    public FontTriplet[] fontLookup(String[] families, String style, int weight) {
        if (families.length == 0) {
            throw new IllegalArgumentException("Specify at least one font family");
        }

        // try matching without substitutions
        List<FontTriplet> matchedTriplets = fontLookup(families, style, weight, false);

        // if there are no matching font triplets found try with substitutions
        if (matchedTriplets.size() == 0) {
            matchedTriplets = fontLookup(families, style, weight, true);
        }

        // no matching font triplets found!
        if (matchedTriplets.size() == 0) {
            StringBuffer sb = new StringBuffer();
            for (int i = 0, c = families.length; i < c; i++) {
                if (i > 0) {
                    sb.append(", ");
                }
                sb.append(families[i]);
            }
            throw new IllegalStateException(
                    "fontLookup must return an array with at least one "
                            + "FontTriplet on the last call. Lookup: " + sb);

        }
        FontTriplet[] fontTriplets = new FontTriplet[matchedTriplets.size()];
        matchedTriplets.toArray(fontTriplets);

        // found some matching fonts so return them
        return fontTriplets;
    }

    private void notifyFontReplacement(FontTriplet replacedKey, FontTriplet newKey) {
        if (this.eventListener != null) {
            this.eventListener.fontSubstituted(this, replacedKey, newKey);
        }
    }

    /**
     * Notify listeners that the SVG text for the given font will be stroked as shapes.
     * @param fontFamily a SVG font family
     */
    public void notifyStrokingSVGTextAsShapes(String fontFamily) {
        if (this.eventListener != null) {
            this.eventListener.svgTextStrokedAsShapes(this, fontFamily);
        }
    }

    /**
     * Find a font with a given family and style by trying
     * different font weights according to the spec.
     * @param family font family
     * @param style font style
     * @param weight font weight
     * @return internal key
     */
    public FontTriplet findAdjustWeight(String family, String style, int weight) {
        FontTriplet key = null;
        String f = null;
        int newWeight = weight;
        if (newWeight < 400) {
            while (f == null && newWeight > 100) {
                newWeight -= 100;
                key = createFontKey(family, style, newWeight);
                f = getInternalFontKey(key);
            }
            newWeight = weight;
            while (f == null && newWeight < 400) {
                newWeight += 100;
                key = createFontKey(family, style, newWeight);
                f = getInternalFontKey(key);
            }
        } else if (newWeight == 400 || newWeight == 500) {
            key = createFontKey(family, style, 400);
            f = getInternalFontKey(key);
        } else if (newWeight > 500) {
            while (f == null && newWeight < 1000) {
                newWeight += 100;
                key = createFontKey(family, style, newWeight);
                f = getInternalFontKey(key);
            }
            newWeight = weight;
            while (f == null && newWeight > 400) {
                newWeight -= 100;
                key = createFontKey(family, style, newWeight);
                f = getInternalFontKey(key);
            }
        }
        if (f == null && weight != 400) {
            key = createFontKey(family, style, 400);
            f = getInternalFontKey(key);
        }

        if (f != null) {
            return key;
        } else {
            return null;
        }
    }

    /**
     * Determines if a particular font is available.
     * @param family font family
     * @param style font style
     * @param weight font weight
     * @return True if available
     */
    public boolean hasFont(String family, String style, int weight) {
        FontTriplet key = createFontKey(family, style, weight);
        return this.triplets.containsKey(key);
    }

    /**
     * Returns the internal font key (F1, F2, F3 etc.) for a given triplet.
     * @param triplet the font triplet
     * @return the associated internal key or null, if not found
     */
    public String getInternalFontKey(FontTriplet triplet) {
        return triplets.get(triplet);
    }

    /**
     * Creates a key from the given strings.
     * @param family font family
     * @param style font style
     * @param weight font weight
     * @return internal key
     */
    public static FontTriplet createFontKey(String family, String style, int weight) {
        return new FontTriplet(family, style, weight);
    }

    /**
     * Gets a Map of all registered fonts.
     * @return a read-only Map with font key/FontMetrics pairs
     */
    public Map<String, Typeface> getFonts() {
        return Collections.unmodifiableMap(this.fonts);
    }

    /**
     * Gets a Map of all registered font triplets.
     * @return a Map with FontTriplet/font key pairs
     */
    public Map<FontTriplet, String> getFontTriplets() {
        return this.triplets;
    }

    /**
     * This is used by the renderers to retrieve all the
     * fonts used in the document.
     * This is for embedded font or creating a list of used fonts.
     * @return a read-only Map with font key/FontMetrics pairs
     */
    public Map<String, Typeface> getUsedFonts() {
        return this.usedFonts;
    }

    /**
     * Returns the FontMetrics for a particular font
     * @param fontName internal key
     * @return font metrics
     */
    public FontMetrics getMetricsFor(String fontName) {
        Typeface metrics = fonts.get(fontName);
        usedFonts.put(fontName, metrics);
        return metrics;
    }

    /**
     * Returns all font triplet matching the given font name.
     * @param fontName The font name we are looking for
     * @return A list of matching font triplets
     */
    public List<FontTriplet> getTripletsFor(String fontName) {
        List<FontTriplet> foundTriplets = new ArrayList<FontTriplet>();
        for (Map.Entry<FontTriplet, String> tripletEntry : triplets.entrySet()) {
            if (fontName.equals((tripletEntry.getValue()))) {
                foundTriplets.add(tripletEntry.getKey());
            }
        }
        return foundTriplets;
    }

    /**
     * Returns the first triplet matching the given font name.
     * As there may be multiple triplets matching the font name
     * the result set is sorted first to guarantee consistent results.
     * @param fontName The font name we are looking for
     * @return The first triplet for the given font name
     */
    public FontTriplet getTripletFor(String fontName) {
        List<FontTriplet> foundTriplets = getTripletsFor(fontName);
        if (foundTriplets.size() > 0) {
            Collections.sort(foundTriplets);
            return foundTriplets.get(0);
        }
        return null;
    }

    /**
     * Returns the font style for a particular font.
     * There may be multiple font styles matching this font. Only the first
     * found is returned. Searching is done on a sorted list to guarantee consistent
     * results.
     * @param fontName internal key
     * @return font style
     */
    public String getFontStyleFor(String fontName) {
        FontTriplet triplet = getTripletFor(fontName);
        if (triplet != null) {
            return triplet.getStyle();
        } else {
            return "";
        }
    }

    /**
     * Returns the font weight for a particular font.
     * There may be multiple font weights matching this font. Only the first
     * found is returned. Searching is done on a sorted list to guarantee consistent
     * results.
     * @param fontName internal key
     * @return font weight
     */
    public int getFontWeightFor(String fontName) {
        FontTriplet triplet = getTripletFor(fontName);
        if (triplet != null) {
            return triplet.getWeight();
        } else {
            return 0;
        }
    }

    /**
     * Diagnostic method for logging all registered fonts to System.out.
     */
    public void dumpAllTripletsToSystemOut() {
        SortedSet<String> entries = new TreeSet<String>();
        for (FontTriplet triplet : this.triplets.keySet()) {
            String key = getInternalFontKey(triplet);
            FontMetrics metrics = getMetricsFor(key);
            entries.add(triplet.toString() + " -> " + key + " -> " + metrics.getFontName() + "\n");
        }
        StringBuffer stringBuffer = new StringBuffer();
        for (String str : entries) {
            stringBuffer.append(str);
        }
        System.out.println(stringBuffer.toString());
    }
}
