/*
 * 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$ */

package org.apache.fop.fonts;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

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

import org.apache.fop.apps.FOPException;
import org.apache.fop.configuration.Configuration;
import org.apache.fop.configuration.ConfigurationException;
import org.apache.fop.events.EventProducer;
import org.apache.fop.util.LogUtil;

/**
 * The font configuration data for the more generic fonts such as TTF and Type1, that are used by
 * most the renderers.
 */
public final class DefaultFontConfig implements FontConfig {

    private static final Log log = LogFactory.getLog(DefaultFontConfig.class);

    private final List<Directory> directories = new ArrayList<Directory>();

    private final List<Font> fonts = new ArrayList<Font>();

    private final List<String> referencedFontFamilies = new ArrayList<String>();

    private final boolean autoDetectFonts;

    private DefaultFontConfig(boolean autoDetectFonts) {
        this.autoDetectFonts = autoDetectFonts;
    }

    /**
     * Parses the morge generic font information.
     */
    public static final class DefaultFontConfigParser implements FontConfig.FontConfigParser {
        /**
         * Parses the font configuration and return the configuration object.
         *
         * @param cfg the configuration data
         * @param strict whether or not to enforce strict validation
         * @return the font configuration object
         * @throws FOPException if an error occurs when creating the configuration object
         */
        public DefaultFontConfig parse(Configuration cfg, boolean strict) throws FOPException {
            return new ParserHelper(cfg, strict).instance;
        }

        /** {@inheritDoc} */
        public DefaultFontConfig parse(Configuration cfg, boolean strict,
                                       FontEventAdapter eventAdapter) throws FOPException {
            return new ParserHelper(cfg, strict, eventAdapter).instance;
        }

        /** {@inheritDoc} */
        public FontConfig parse(Configuration cfg, FontManager fontManager, boolean strict,
                EventProducer eventProducer) throws FOPException {
            return parse(cfg, strict);
        }
    }

    private  static final class ParserHelper {

        private  boolean strict;

        private Configuration config;
        private  Configuration fontInfoCfg;
        private FontEventAdapter eventAdapter;

        private  DefaultFontConfig instance;

        private ParserHelper(Configuration cfg, boolean strict) throws FOPException {
            this(cfg, strict, null);
        }

        private ParserHelper(Configuration cfg, boolean strict, FontEventAdapter eventAdapter)
                throws FOPException {
            this.eventAdapter = eventAdapter;
            if (cfg == null || cfg.getChild("fonts", false) == null) {
                instance = null;
            } else {
                this.strict = strict;
                this.config = cfg;
                this.fontInfoCfg = cfg.getChild("fonts", false);
                instance = new DefaultFontConfig(fontInfoCfg.getChild("auto-detect", false) != null);
                parse();
            }
        }

        private void parse() throws FOPException {
            parseFonts();
            parseReferencedFonts();
            parseDirectories();
        }

        private void parseFonts() throws FOPException {
            for (Configuration fontCfg : fontInfoCfg.getChildren("font")) {
                String embed = fontCfg.getAttribute("embed-url", null);
                if (embed == null) {
                    LogUtil.handleError(log, "Font configuration without embed-url attribute",
                            strict);
                    continue;
                }
                Font font = new Font(fontCfg.getAttribute("metrics-url", null), embed,
                        fontCfg.getAttribute("embed-url-afm", null),
                        fontCfg.getAttribute("embed-url-pfm", null),
                        fontCfg.getAttribute("sub-font", null),
                        fontCfg.getAttributeAsBoolean("kerning", true),
                        fontCfg.getAttributeAsBoolean("advanced", true),
                        fontCfg.getAttribute("encoding-mode", EncodingMode.AUTO.getName()),
                        fontCfg.getAttribute("embedding-mode", EncodingMode.AUTO.getName()),
                        fontCfg.getAttributeAsBoolean("simulate-style", false),
                        fontCfg.getAttributeAsBoolean("embed-as-type1", false),
                        fontCfg.getAttributeAsBoolean("svg", true));
                instance.fonts.add(font);
                boolean hasTriplets = false;
                for (Configuration tripletCfg : fontCfg.getChildren("font-triplet")) {
                    FontTriplet fontTriplet = getFontTriplet(tripletCfg, strict);
                    font.tripletList.add(fontTriplet);
                    hasTriplets = true;
                }
                // no font triplet info
                if (!hasTriplets) {
                    LogUtil.handleError(log, "font without font-triplet", strict);
                }
                try {
                    if (eventAdapter != null && font.getSimulateStyle()
                            && !config.getAttribute("mime").equals("application/pdf")) {
                        eventAdapter.fontFeatureNotSuppprted(this, "simulate-style", "PDF");
                    }
                    if (eventAdapter != null && font.getEmbedAsType1()
                            && !config.getAttribute("mime").equals("application/postscript")) {
                        throw new FOPException("The embed-as-type1 attribute is only supported in postscript");
                    }
                } catch (ConfigurationException ex) {
                    LogUtil.handleException(log, ex, true);
                }
            }
        }

        private void parseReferencedFonts() throws FOPException {
            Configuration referencedFontsCfg = fontInfoCfg.getChild("referenced-fonts", false);
            if (referencedFontsCfg != null) {
                for (Configuration match : referencedFontsCfg.getChildren("match")) {
                    try {
                        instance.referencedFontFamilies.add(match.getAttribute("font-family"));
                    } catch (ConfigurationException ce) {
                        LogUtil.handleException(log, ce, strict);
                        continue;
                    }
                }
            }
        }

        private void parseDirectories() throws FOPException {
            for (Configuration directoriesCfg : fontInfoCfg.getChildren("directory")) {
                boolean recursive = directoriesCfg.getAttributeAsBoolean("recursive", false);
                String directory;
                try {
                    directory = directoriesCfg.getValue();
                } catch (ConfigurationException e) {
                    LogUtil.handleException(log, e, strict);
                    continue;
                }
                if (directory == null) {
                    LogUtil.handleException(log,
                            new FOPException("directory defined without value"), strict);
                    continue;
                }
                instance.directories.add(new Directory(directory, recursive));
            }
        }

        /**
         * Creates a new FontTriplet given a triple Configuration
         *
         * @param tripletCfg a triplet configuration
         * @return a font triplet font key
         * @throws FOPException thrown if a FOP exception occurs
         */
        private FontTriplet getFontTriplet(Configuration tripletCfg, boolean strict)
                throws FOPException {
            try {
                String name = tripletCfg.getAttribute("name");
                if (name == null) {
                    LogUtil.handleError(log, "font-triplet without name", strict);
                    return null;
                }
                String weightStr = tripletCfg.getAttribute("weight");
                if (weightStr == null) {
                    LogUtil.handleError(log, "font-triplet without weight", strict);
                    return null;
                }
                int weight = FontUtil.parseCSS2FontWeight(FontUtil.stripWhiteSpace(weightStr));
                String style = tripletCfg.getAttribute("style");
                if (style == null) {
                    LogUtil.handleError(log, "font-triplet without style", strict);
                    return null;
                } else {
                    style = FontUtil.stripWhiteSpace(style);
                }
                return FontInfo.createFontKey(name, style, weight);
            } catch (ConfigurationException e) {
                LogUtil.handleException(log, e, strict);
            }
            return null;
        }

    }

    /**
     * Returns the list of fonts that were parsed.
     * @return a list of fonts
     */
    public List<Font> getFonts() {
        return Collections.unmodifiableList(fonts);
    }

    /**
     * Returns a list of directories that were parsed.
     * @return a list of directories
     */
    public List<Directory> getDirectories() {
        return Collections.unmodifiableList(directories);
    }

    /**
     * Returns a list of referenced font families.
     * @return the referenced font families
     */
    public List<String> getReferencedFontFamily() {
        return Collections.unmodifiableList(referencedFontFamilies);
    }

    /**
     * Whether or not to enable auto-detecting of fonts in the system.
     * @return true to enable auto-detect
     */
    public boolean isAutoDetectFonts() {
        return autoDetectFonts;
    }

    /**
     * The directory to find fonts within.
     */
    public static final class Directory {

        private final String directory;

        private final boolean recursive;

        private Directory(String directory, boolean recurse) {
            this.directory = directory;
            this.recursive = recurse;
        }

        /**
         * Returns a String representing the directory to find fonts within.
         * @return the directory
         */
        public String getDirectory() {
            return directory;
        }

        /**
         * Returns whether or not to recurse through the directory when finding fonts.
         * @return true to recurse through the directory and sub-directories
         */
        public boolean isRecursive() {
            return recursive;
        }
    }

    /**
     * Represents a font object within the FOP conf.
     */
    public static final class Font {

        private final String metrics;

        private final String embedUri;

        private String afm;

        private String pfm;

        private final String subFont;

        private final boolean kerning;

        private final boolean advanced;

        private final String encodingMode;

        private final String embeddingMode;

        public String getEncodingMode() {
            return encodingMode;
        }

        private final boolean embedAsType1;
        private final boolean simulateStyle;

        private final boolean useSVG;

        private final List<FontTriplet> tripletList = new ArrayList<FontTriplet>();

        public List<FontTriplet> getTripletList() {
            return Collections.unmodifiableList(tripletList);
        }

        private Font(String metrics, String embed, String afm, String pfm, String subFont, boolean kerning,
                     boolean advanced, String encodingMode, String embeddingMode, boolean simulateStyle,
                     boolean embedAsType1, boolean useSVG) {
            this.metrics = metrics;
            this.embedUri = embed;
            this.afm = afm;
            this.pfm = pfm;
            this.subFont = subFont;
            this.kerning = kerning;
            this.advanced = advanced;
            this.encodingMode = encodingMode;
            this.embeddingMode = embeddingMode;
            this.simulateStyle = simulateStyle;
            this.embedAsType1 = embedAsType1;
            this.useSVG = useSVG;
        }

        /**
         * Whether or not to allow kerning of glyphs.
         * @return true to allow glyph kerning
         */
        public boolean isKerning() {
            return kerning;
        }

        public boolean isAdvanced() {
            return advanced;
        }

        /**
         * Gets the String representing the metrics file.
         * @return the metrics file
         */
        public String getMetrics() {
            return metrics;
        }

        /**
         * Gets the URI of the font to embed.
         * @return the font URI
         */
        public String getEmbedURI() {
            return embedUri;
        }

        /**
         * Gets the sub font within, for example, a TTC.
         * @return the sub font name
         */
        public String getSubFont() {
            return subFont;
        }

        public String getEmbeddingMode() {
            return embeddingMode;
        }

        public String getAfm() {
            return afm;
        }

        public String getPfm() {
            return pfm;
        }

        public boolean getSimulateStyle() {
            return simulateStyle;
        }

        public boolean getEmbedAsType1() {
            return embedAsType1;
        }

        public boolean getUseSVG() {
            return useSVG;
        }
    }
}
