/*
 * 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.
 */
package org.apache.fontbox.util;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.fontbox.ttf.NamingTable;
import org.apache.fontbox.ttf.TTFParser;
import org.apache.fontbox.ttf.TrueTypeFont;
import org.apache.fontbox.util.autodetect.FontFileFinder;

/**
 * This class is used as manager for local fonts. It's based on the font manager provided by Apache FOP. see
 * org.apache.fop.fonts.FontManager.java
 */

public class FontManager
{
    /**
     * Log instance.
     */
    private static final Log LOG = LogFactory.getLog(FontManager.class);

    private static HashMap<String, String> ttfFontfiles = new HashMap<String, String>();

    private static boolean fontsLoaded = false;

    // HashMap with all known true type fonts
    private static HashMap<String, String> fontMappingTTF = new HashMap<String, String>();

    private FontManager()
    {
    }

    /**
     * Load all available fonts from the environment.
     */
    private static void loadFonts()
    {
        try
        {
            FontFileFinder fontfinder = new FontFileFinder();
            List<URI> fonts = fontfinder.find();
            for (URI font : fonts)
            {
                try
                {
                    // the URL may contain some escaped characters like spaces
                    // use the URI to decode such escape sequences
                    String fontfilename = new File(font).getPath();
                    if (fontfilename.toLowerCase().endsWith(".ttf"))
                    {
                        analyzeTTF(fontfilename);
                    }
                    else
                    {
                        LOG.debug("Unsupported font format for external font: " + fontfilename);
                    }
                }
                catch (IOException exception)
                {
                    LOG.debug("Can't read external font: " + font.getPath(), exception);
                }
            }
            addFontMapping(fontfinder.getCommonTTFMapping(), fontMappingTTF);
            createFontmapping();
        }
        catch (IOException exception)
        {
            LOG.error("An error occured when collecting external fonts.", exception);
        }
        finally
        {
            fontsLoaded = true;
        }
    }

    /**
     * Analyze the given true type font.
     * 
     * @param ttfFilename the filename of the true type type
     * @throws IOException if something went wrong
     */
    private static void analyzeTTF(String ttfFilename) throws IOException
    {
        TTFParser ttfParser = new TTFParser(false,true);
        TrueTypeFont ttfFont = ttfParser.parseTTF(ttfFilename);
        if (ttfFont != null)
        {
            NamingTable namingTable = ttfFont.getNaming();
            if (namingTable != null && namingTable.getPSName() != null)
            {
                String normalizedName = normalizeFontname(namingTable.getPSName());
                if (!ttfFontfiles.containsKey(normalizedName))
                {
                    LOG.debug("Added font mapping "+normalizedName + " -=> "+ttfFilename);
                    ttfFontfiles.put(normalizedName, ttfFilename);
                }
            }
            // take the font family name into account
            if (namingTable != null && namingTable.getFontFamily() != null)
            {
                String normalizedName = normalizeFontFamily(namingTable.getFontFamily(), namingTable.getPSName());
                if (!ttfFontfiles.containsKey(normalizedName))
                {
                    LOG.debug("Added font mapping "+normalizedName + " -=> "+ttfFilename);
                    ttfFontfiles.put(normalizedName, ttfFilename);
                }
            }
        }   
    }

    /**
     * Normalize the fontname.
     * 
     * @param fontname The name of the font.
     * 
     * @return The normalized name of the font.
     */
    private static String normalizeFontname(String fontname)
    {
        // Terminate all whitespaces, commas and hyphens
        String normalizedFontname = fontname.toLowerCase().replaceAll(" ", "").replaceAll(",", "")
                .replaceAll("-", "");
        // Terminate trailing characters up to the "+".
        // As far as I know, these characters are used in names of embedded fonts
        // If the embedded font can't be read, we'll try to find it here
        if (normalizedFontname.indexOf("+") > -1)
        {
            normalizedFontname = normalizedFontname.substring(normalizedFontname.indexOf("+") + 1);
        }
        // normalize all kinds of fonttypes. There are several possible version which have to be normalized
        // e.g. Arial,Bold Arial-BoldMT Helevtica-oblique ...
        boolean isBold = normalizedFontname.indexOf("bold") > -1;
        boolean isItalic = normalizedFontname.indexOf("italic") > -1 || normalizedFontname.indexOf("oblique") > -1;
        normalizedFontname = normalizedFontname.replaceAll("bold", "")
                .replaceAll("italic", "").replaceAll("oblique", "");
        if (isBold)
        {
            normalizedFontname += "bold";
        }
        if (isItalic)
        {
            normalizedFontname += "italic";
        }
        return normalizedFontname;
    }

    private static String normalizeFontFamily(String fontFamily, String psFontName)
    {
        String normalizedFontFamily=fontFamily.toLowerCase().replaceAll(" ", "").replaceAll(",", "").replaceAll("-", "");
        if (psFontName!=null) 
        {
            psFontName=psFontName.toLowerCase();
               
            boolean isBold = psFontName.indexOf("bold") > -1;
            boolean isItalic = psFontName.indexOf("italic") > -1 || psFontName.indexOf("oblique") > -1;
            
            if (isBold)
            {
                normalizedFontFamily += "bold";
            }
            if (isItalic)
            {
                normalizedFontFamily += "italic";
            }
        }
        return normalizedFontFamily;
    }
     
    /**
     * Add a font-mapping.
     * 
     * @param font The name of the font.
     * 
     * @param mappedName The name of the mapped font.
     */
    private static void addFontMapping(String font, String mappedName, Map<String, String> mapping)
    {
        String fontname = normalizeFontname(font);
        // is there already a font mapping ?
        if (mapping.containsKey(fontname))
        {
            return;
        }
        String mappedFontname = normalizeFontname(mappedName);
        // is there any font with the mapped name ?
        if (ttfFontfiles.containsKey(mappedFontname))
        {
            mapping.put(fontname, mappedFontname);
        }
        else
        {
            // is there any recursive mapping ?
            if (mapping.containsKey(mappedFontname))
            {
                mapping.put(fontname, mapping.get(mappedFontname));
            }
        }
    }

    /**
     * Add the given mappings to the font mapping.
     * 
     * @param fontMappingSrc the given mapping
     */
    private static void addFontMapping(Map<String, String> fontMappingSrc,
            Map<String, String> fontMappingDest)
    {
        for (String fontname : fontMappingSrc.keySet())
        {
            addFontMapping(fontname, fontMappingSrc.get(fontname), fontMappingDest);
        }
    }

    /**
     * Search for a mapped true type font name.
     * 
     * @param fontname the given font name
     * @return the mapped font name
     */
    private static String getMappedTTFName(String fontname)
    {
        String normalizedFontname = normalizeFontname(fontname);
        if (fontMappingTTF.containsKey(normalizedFontname))
        {
            return fontMappingTTF.get(normalizedFontname);
        }
        return null;
    }

    /**
     * Create a mapping for the some font families.
     */
    private static void createFontmapping()
    {
        addFontFamilyMapping("ArialNarrow", "Arial", fontMappingTTF);
        addFontFamilyMapping("ArialMT", "Arial", fontMappingTTF);
        addFontFamilyMapping("CourierNew", "Courier", fontMappingTTF);
        addFontFamilyMapping("TimesNewRomanPSMT", "TimesNewRoman", fontMappingTTF);
    }

    /**
     * Create a mapping for the given font family.
     * 
     * @param fontfamily the font family to be mapped
     * @param mappedFontfamily the mapped font family
     */
    private static void addFontFamilyMapping(String fontfamily, String mappedFontfamily,
            Map<String, String> mapping)
    {
        addFontMapping(fontfamily + ",BoldItalic", mappedFontfamily + ",BoldItalic", mapping);
        addFontMapping(fontfamily + ",Bold", mappedFontfamily + ",Bold", mapping);
        addFontMapping(fontfamily + ",Italic", mappedFontfamily + ",Italic", mapping);
        addFontMapping(fontfamily, mappedFontfamily, mapping);
    }

    /**
     * Search for a font for the given font name.
     * 
     * @param fontname the given font name
     * @return the name of the mapped font
     */
    public static String findTTFontname(String fontname)
    {
        if (!fontsLoaded)
        {
            loadFonts();
        }
        String fontfile = null;
        String normalizedFontname = normalizeFontname(fontname);
        if (ttfFontfiles.containsKey(normalizedFontname))
        {
            fontfile = ttfFontfiles.get(normalizedFontname);
        }
        if (fontfile == null)
        {
            String mappedFontname = getMappedTTFName(fontname);
            if (mappedFontname != null && ttfFontfiles.containsKey(mappedFontname))
            {
                fontfile = ttfFontfiles.get(mappedFontname);
            }
        }
        if (fontfile != null)
        {
            LOG.debug("Using ttf mapping "+fontname + " -=> "+fontfile);
        }
        else
        {
            LOG.warn("Font not found: " + fontname);
        }
        return fontfile;
    }

    /**
     * Search for a true type font for the given font name.
     * 
     * @param fontname the given font name
     * @return the mapped true type font
     * @throws IOException if something went wrong
     */
    public static TrueTypeFont findTTFont(String fontname) throws IOException
    {
        String ttffontname = findTTFontname(fontname);
        TrueTypeFont ttfFont = null;
        if (ttffontname != null)
        {
            TTFParser ttfParser = new TTFParser();
            InputStream fontStream = ResourceLoader.loadResource(ttffontname);
            if (fontStream == null)
            {
                throw new IOException("Can't load external font: " + ttffontname);
            }
            ttfFont = ttfParser.parseTTF(fontStream);
        }
        return ttfFont;
    }
}
