/*
 * 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: Font.java 1827168 2018-03-19 08:49:57Z ssteiner $ */

package org.apache.fop.fonts;

import java.util.Collections;
import java.util.List;
import java.util.Map;

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

import org.apache.fop.complexscripts.fonts.Positionable;
import org.apache.fop.complexscripts.fonts.Substitutable;
import org.apache.fop.render.java2d.CustomFontMetricsMapper;
import org.apache.fop.util.CharUtilities;

/**
 * This class holds font state information and provides access to the font
 * metrics.
 */
public class Font implements Substitutable, Positionable {

    /** Extra Bold font weight */
    public static final int WEIGHT_EXTRA_BOLD = 800;

    /** Bold font weight */
    public static final int WEIGHT_BOLD = 700;

    /** Normal font weight */
    public static final int WEIGHT_NORMAL = 400;

    /** Light font weight */
    public static final int WEIGHT_LIGHT = 200;

    /** Normal font style */
    public static final String STYLE_NORMAL = "normal";

    /** Italic font style */
    public static final String STYLE_ITALIC = "italic";

    /** Oblique font style */
    public static final String STYLE_OBLIQUE = "oblique";

    /** Inclined font style */
    public static final String STYLE_INCLINED = "inclined";

    /** Default selection priority */
    public static final int PRIORITY_DEFAULT = 0;

    /** Default fallback key */
    public static final FontTriplet DEFAULT_FONT = new FontTriplet(
                    "any", STYLE_NORMAL, WEIGHT_NORMAL, PRIORITY_DEFAULT);

    /** logger */
    private  static Log log = LogFactory.getLog(Font.class);

    private final String fontName;
    private final FontTriplet triplet;
    private final int fontSize;

    /**
     * normal or small-caps font
     */
    //private int fontVariant;

    private final FontMetrics metric;

    /**
     * Main constructor
     * @param key key of the font
     * @param triplet the font triplet that was used to lookup this font (may be null)
     * @param met font metrics
     * @param fontSize font size
     */
    public Font(String key, FontTriplet triplet, FontMetrics met, int fontSize) {
        this.fontName = key;
        this.triplet = triplet;
        this.metric = met;
        this.fontSize = fontSize;
    }

    /**
     * Returns the associated font metrics object.
     * @return the font metrics
     */
    public FontMetrics getFontMetrics() {
        return this.metric;
    }

    /**
     * Determines whether the font is a multibyte font.
     * @return True if it is multibyte
     */
    public boolean isMultiByte() {
        return getFontMetrics().isMultiByte();
    }

    /**
     * Returns the font's ascender.
     * @return the ascender
     */
    public int getAscender() {
        return metric.getAscender(fontSize) / 1000;
    }

    /**
     * Returns the font's CapHeight.
     * @return the capital height
     */
    public int getCapHeight() {
        return metric.getCapHeight(fontSize) / 1000;
    }

    /**
     * Returns the font's Descender.
     * @return the descender
     */
    public int getDescender() {
        return metric.getDescender(fontSize) / 1000;
    }

    /**
     * Returns the font's name.
     * @return the font name
     */
    public String getFontName() {
        return fontName;
    }

    /** @return the font triplet that selected this font */
    public FontTriplet getFontTriplet() {
        return this.triplet;
    }

    /**
     * Returns the font size
     * @return the font size
     */
    public int getFontSize() {
        return fontSize;
    }

    /**
     * Returns the XHeight
     * @return the XHeight
     */
    public int getXHeight() {
        return metric.getXHeight(fontSize) / 1000;
    }

    /** @return true if the font has kerning info */
    public boolean hasKerning() {
        return metric.hasKerningInfo();
    }

    /** @return true if the font has feature (i.e., at least one lookup matches) */
    public boolean hasFeature(int tableType, String script, String language, String feature) {
        return metric.hasFeature(tableType, script, language, feature);
    }

    /**
     * Returns the font's kerning table
     * @return the kerning table
     */
    public Map<Integer, Map<Integer, Integer>> getKerning() {
        if (metric.hasKerningInfo()) {
            return metric.getKerningInfo();
        } else {
            return Collections.emptyMap();
        }
    }

    /**
     * Returns the amount of kerning between two characters.
     *
     * The value returned measures in pt. So it is already adjusted for font size.
     *
     * @param ch1 first character
     * @param ch2 second character
     * @return the distance to adjust for kerning, 0 if there's no kerning
     */
    public int getKernValue(int ch1, int ch2) {
        // Isolate surrogate pair
        if ((ch1 >= 0xD800) && (ch1 <= 0xE000)) {
            return 0;
        } else if ((ch2 >= 0xD800) && (ch2 <= 0xE000)) {
            return 0;
        }

        Map<Integer, Integer> kernPair = getKerning().get(ch1);
        if (kernPair != null) {
            Integer width = kernPair.get(ch2);
            if (width != null) {
                return width * getFontSize() / 1000;
            }
        }
        return 0;
    }

    /**
     * Returns the width of a character
     * @param charnum character to look up
     * @return width of the character
     */
    public int getWidth(int charnum) {
        // returns width of given character number in millipoints
        return (metric.getWidth(charnum, fontSize) / 1000);
    }

    /**
     * Map a java character (unicode) to a font character.
     * Default uses CodePointMapping.
     * @param c character to map
     * @return the mapped character
     */
    public char mapChar(char c) {

        if (metric instanceof org.apache.fop.fonts.Typeface) {
            return ((org.apache.fop.fonts.Typeface)metric).mapChar(c);
        }

        // Use default CodePointMapping
        char d = CodePointMapping.getMapping("WinAnsiEncoding").mapChar(c);
        if (d != SingleByteEncoding.NOT_FOUND_CODE_POINT) {
            c = d;
        } else {
            log.warn("Glyph " + (int) c + " not available in font " + fontName);
            c = Typeface.NOT_FOUND;
        }

        return c;
    }

    /**
     * Map a unicode code point to a font character.
     * Default uses CodePointMapping.
     * @param cp code point to map
     * @return the mapped character
     */
    public int mapCodePoint(int cp) {
        FontMetrics fontMetrics = getRealFontMetrics();

        if (fontMetrics instanceof CIDFont) {
            return ((CIDFont) fontMetrics).mapCodePoint(cp);
        }

        if (CharUtilities.isBmpCodePoint(cp)) {
            return mapChar((char) cp);
        }

        return Typeface.NOT_FOUND;
    }

    /**
     * Determines whether this font contains a particular character/glyph.
     * @param c character to check
     * @return True if the character is supported, False otherwise
     */
    public boolean hasChar(char c) {
        if (metric instanceof org.apache.fop.fonts.Typeface) {
            return ((org.apache.fop.fonts.Typeface)metric).hasChar(c);
        } else {
            // Use default CodePointMapping
            return (CodePointMapping.getMapping("WinAnsiEncoding").mapChar(c) > 0);
        }
    }

    /**
     * Determines whether this font contains a particular code point/glyph.
     * @param cp code point to check
     * @return True if the code point is supported, False otherwise
     */
    public boolean hasCodePoint(int cp) {
        FontMetrics realFont = getRealFontMetrics();

        if (realFont instanceof CIDFont) {
            return ((CIDFont) realFont).hasCodePoint(cp);
        }

        if (CharUtilities.isBmpCodePoint(cp)) {
            return hasChar((char) cp);
        }

        return false;
    }

    /**
     * Get the real underlying font if it is wrapped inside some container such as a {@link LazyFont} or a
     * {@link CustomFontMetricsMapper}.
     *
     * @return instance of the font
     */
    private FontMetrics getRealFontMetrics() {
        FontMetrics realFontMetrics = metric;

        if (realFontMetrics instanceof CustomFontMetricsMapper) {
            realFontMetrics = ((CustomFontMetricsMapper) realFontMetrics).getRealFont();
        }

        if (realFontMetrics instanceof LazyFont) {
            return ((LazyFont) realFontMetrics).getRealFont();
        }

        return realFontMetrics;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        StringBuffer sbuf = new StringBuffer(super.toString());
        sbuf.append('{');
        /*
        sbuf.append(fontFamily);
        sbuf.append(',');*/
        sbuf.append(fontName);
        sbuf.append(',');
        sbuf.append(fontSize);
        /*
        sbuf.append(',');
        sbuf.append(fontStyle);
        sbuf.append(',');
        sbuf.append(fontWeight);*/
        sbuf.append('}');
        return sbuf.toString();
    }

    /**
     * Helper method for getting the width of a unicode char
     * from the current fontstate.
     * This also performs some guessing on widths on various
     * versions of space that might not exists in the font.
     * @param c character to inspect
     * @return the width of the character or -1 if no width available
     */
    public int getCharWidth(char c) {
        int width;

        if ((c == '\n') || (c == '\r') || (c == '\t') || (c == '\u00A0')) {
            width = getCharWidth(' ');
        } else {
            if (hasChar(c)) {
                int mappedChar = mapChar(c);
                width = getWidth(mappedChar);
            } else {
                width = -1;
            }
            if (width <= 0) {
                // Estimate the width of spaces not represented in
                // the font
                int em = getFontSize(); //http://en.wikipedia.org/wiki/Em_(typography)
                int en = em / 2; //http://en.wikipedia.org/wiki/En_(typography)

                if (c == ' ') {
                    width = em;
                } else if (c == '\u2000') {
                    width = en;
                } else if (c == '\u2001') {
                    width = em;
                } else if (c == '\u2002') {
                    width = em / 2;
                } else if (c == '\u2003') {
                    width = getFontSize();
                } else if (c == '\u2004') {
                    width = em / 3;
                } else if (c == '\u2005') {
                    width = em / 4;
                } else if (c == '\u2006') {
                    width = em / 6;
                } else if (c == '\u2007') {
                    width = getCharWidth('0');
                } else if (c == '\u2008') {
                    width = getCharWidth('.');
                } else if (c == '\u2009') {
                    width = em / 5;
                } else if (c == '\u200A') {
                    width = em / 10;
                } else if (c == '\u200B') {
                    width = 0;
                } else if (c == '\u202F') {
                    width = getCharWidth(' ') / 2;
                } else if (c == '\u2060') {
                    width = 0;
                } else if (c == '\u3000') {
                    width = getCharWidth(' ') * 2;
                } else if (c == '\ufeff') {
                    width = 0;
                } else {
                    //Will be internally replaced by "#" if not found
                    width = getWidth(mapChar(c));
                }
            }
        }

        return width;
    }

    /**
     * Helper method for getting the width of a unicode char
     * from the current fontstate.
     * This also performs some guessing on widths on various
     * versions of space that might not exists in the font.
     * @param c character to inspect
     * @return the width of the character or -1 if no width available
     */
    public int getCharWidth(int c) {
        if (c < 0x10000) {
            return getCharWidth((char) c);
        }

        if (hasCodePoint(c)) {
            int mappedChar = mapCodePoint(c);
            return getWidth(mappedChar);
        }

        return -1;
    }

    /**
     * Calculates the word width.
     * @param word text to get width for
     * @return the width of the text
     */
    public int getWordWidth(String word) {
        if (word == null) {
            return 0;
        }
        int wordLength = word.length();
        int width = 0;
        char[] characters = new char[wordLength];
        word.getChars(0, wordLength, characters, 0);
        for (int i = 0; i < wordLength; i++) {
            width += getCharWidth(characters[i]);
        }
        return width;
    }

    /** {@inheritDoc} */
    public boolean performsSubstitution() {
        if (metric instanceof Substitutable) {
            Substitutable s = (Substitutable) metric;
            return s.performsSubstitution();
        } else {
            return false;
        }
    }

    /** {@inheritDoc} */
    public CharSequence performSubstitution(CharSequence cs,
        String script, String language, List associations, boolean retainControls) {
        if (metric instanceof Substitutable) {
            Substitutable s = (Substitutable) metric;
            return s.performSubstitution(cs, script, language, associations, retainControls);
        } else {
            throw new UnsupportedOperationException();
        }
    }

    /** {@inheritDoc} */
    public CharSequence reorderCombiningMarks(CharSequence cs, int[][] gpa,
        String script, String language, List associations) {
        if (metric instanceof Substitutable) {
            Substitutable s = (Substitutable) metric;
            return s.reorderCombiningMarks(cs, gpa, script, language, associations);
        } else {
            throw new UnsupportedOperationException();
        }
    }

    /** {@inheritDoc} */
    public boolean performsPositioning() {
        if (metric instanceof Positionable) {
            Positionable p = (Positionable) metric;
            return p.performsPositioning();
        } else {
            return false;
        }
    }

    /** {@inheritDoc} */
    public int[][] performPositioning(CharSequence cs, String script, String language, int fontSize) {
        if (metric instanceof Positionable) {
            Positionable p = (Positionable) metric;
            return p.performPositioning(cs, script, language, fontSize);
        } else {
            throw new UnsupportedOperationException();
        }
    }

    /** {@inheritDoc} */
    public int[][] performPositioning(CharSequence cs, String script, String language) {
        return performPositioning(cs, script, language, fontSize);
    }

}
