/*
 * 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: Type1FontLoader.java 1761026 2016-09-16 12:50:43Z ssteiner $ */

package org.apache.fop.fonts.type1;

import java.awt.geom.RectangularShape;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

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

import org.apache.fop.apps.io.InternalResourceResolver;
import org.apache.fop.fonts.CodePointMapping;
import org.apache.fop.fonts.EmbeddingMode;
import org.apache.fop.fonts.FontLoader;
import org.apache.fop.fonts.FontType;
import org.apache.fop.fonts.FontUris;
import org.apache.fop.fonts.SingleByteEncoding;
import org.apache.fop.fonts.SingleByteFont;

/**
 * Loads a Type 1 font into memory directly from the original font file.
 */
public class Type1FontLoader extends FontLoader {

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

    private SingleByteFont singleFont;

    private EmbeddingMode embeddingMode;

    private final FontUris fontUris;

    /**
     * Constructs a new Type 1 font loader.
     * @param fontFileURI the URI to the PFB file of a Type 1 font
     * @param embedded indicates whether the font is embedded or referenced
     * @param useKerning indicates whether to load kerning information if available
     * @param resourceResolver the font resolver used to resolve URIs
     * @throws IOException In case of an I/O error
     */
    public Type1FontLoader(FontUris fontUris, boolean embedded, EmbeddingMode embeddingMode,
            boolean useKerning, InternalResourceResolver resourceResolver) throws IOException {
        super(fontUris.getEmbed(), embedded, useKerning, true, resourceResolver);
        this.embeddingMode = embeddingMode;
        this.fontUris = fontUris;
    }

    private String getPFMURI(String pfbURI) {
        String pfbExt = pfbURI.substring(pfbURI.length() - 3, pfbURI.length());
        String pfmExt = pfbExt.substring(0, 2)
                + (Character.isUpperCase(pfbExt.charAt(2)) ? "M" : "m");
        return pfbURI.substring(0, pfbURI.length() - 4) + "." + pfmExt;
    }

    private static final String[] AFM_EXTENSIONS = new String[] {".AFM", ".afm", ".Afm"};

    /** {@inheritDoc} */
    @Override
    protected void read() throws IOException {
        AFMFile afm = null;
        PFMFile pfm = null;

        InputStream afmIn = null;
        String fontFileStr = fontFileURI.toASCIIString();
        String partialAfmUri = fontFileStr.substring(0, fontFileStr.length() - 4);
        String afmUri = (fontUris.getAfm() != null) ? fontUris.getAfm().toASCIIString() : null;
        if (afmUri == null) {
            for (String afmExtension : AFM_EXTENSIONS) {
                try {
                    afmUri = partialAfmUri + afmExtension;
                    afmIn = resourceResolver.getResource(afmUri);
                    if (afmIn != null) {
                        break;
                    }
                } catch (IOException ioe) {
                    // Ignore, AFM probably not available under the URI
                } catch (URISyntaxException e) {
                    // Ignore, AFM probably not available under the URI
                }
            }
        } else {
            try {
                afmIn = resourceResolver.getResource(afmUri);
            } catch (URISyntaxException e) {
                throw new IOException(e);
            }
        }
        if (afmIn != null) {
            try {
                AFMParser afmParser = new AFMParser();
                afm = afmParser.parse(afmIn, afmUri);
            } finally {
                IOUtils.closeQuietly(afmIn);
            }
        }

        String pfmUri = (fontUris.getPfm() == null) ? getPFMURI(fontFileStr) : fontUris.getPfm()
                .toASCIIString();
        InputStream pfmIn = null;
        try {
            pfmIn = resourceResolver.getResource(pfmUri);
        } catch (IOException ioe) {
            // Ignore, PFM probably not available under the URI
        } catch (URISyntaxException e) {
            // Ignore, PFM probably not available under the URI
        }
        if (pfmIn != null) {
            try {
                pfm = new PFMFile();
                pfm.load(pfmIn);
            } catch (IOException ioe) {
                if (afm == null) {
                    // Ignore the exception if we have a valid PFM. PFM is only the fallback.
                    throw ioe;
                }
            } finally {
                IOUtils.closeQuietly(pfmIn);
            }
        }

        if (afm == null && pfm == null) {
            throw new java.io.FileNotFoundException(
                    "Neither an AFM nor a PFM file was found for " + this.fontFileURI);
        }
        buildFont(afm, pfm);
        this.loaded = true;
    }

    private void buildFont(AFMFile afm, PFMFile pfm) {
        if (afm == null && pfm == null) {
            throw new IllegalArgumentException("Need at least an AFM or a PFM!");
        }
        singleFont = new SingleByteFont(resourceResolver, embeddingMode);
        singleFont.setFontType(FontType.TYPE1);
        if (this.embedded) {
            singleFont.setEmbedURI(this.fontFileURI);
        }
        returnFont = singleFont;

        handleEncoding(afm, pfm);
        handleFontName(afm, pfm);
        handleMetrics(afm, pfm);
    }

    private void handleEncoding(AFMFile afm, PFMFile pfm) {
        // Encoding
        if (afm != null) {
            String encoding = afm.getEncodingScheme();
            singleFont.setUseNativeEncoding(true);
            if ("AdobeStandardEncoding".equals(encoding)) {
                singleFont.setEncoding(CodePointMapping.STANDARD_ENCODING);
                addUnencodedBasedOnEncoding(afm);
            } else {
                String effEncodingName;
                if ("FontSpecific".equals(encoding)) {
                    effEncodingName = afm.getFontName() + "Encoding";
                } else {
                    effEncodingName = encoding;
                }
                if (log.isDebugEnabled()) {
                    log.debug("Unusual font encoding encountered: "
                            + encoding + " -> " + effEncodingName);
                }
                CodePointMapping mapping = buildCustomEncoding(effEncodingName, afm);
                singleFont.setEncoding(mapping);
                addUnencodedBasedOnAFM(afm);
            }
        } else {
            if (pfm.getCharSet() == 2 && !pfm.getCharSetName().equals("Symbol")) {
                int[] table = new int[256];
                String[] charNameMap = new String[256];
                int j = 0;
                for (int i = pfm.getFirstChar(); i < pfm.getLastChar(); i++) {
                    if (j < table.length) {
                        table[j] = i;
                        table[j + 1] = i;
                        j += 2;
                    }
                    charNameMap[i] = String.format("x%03o", i);
                }
                CodePointMapping mapping  = new CodePointMapping("custom", table, charNameMap);
                singleFont.setEncoding(mapping);
            } else if (pfm.getCharSet() >= 0 && pfm.getCharSet() <= 2) {
                singleFont.setEncoding(pfm.getCharSetName() + "Encoding");
            } else {
                log.warn("The PFM reports an unsupported encoding ("
                        + pfm.getCharSetName() + "). The font may not work as expected.");
                singleFont.setEncoding("WinAnsiEncoding"); // Try fallback, no guarantees!
            }
        }
    }

    private Set<String> toGlyphSet(String[] glyphNames) {
        Set<String> glyphSet = new java.util.HashSet<String>();
        Collections.addAll(glyphSet, glyphNames);
        return glyphSet;
    }

    /**
     * Adds characters not encoded in the font's primary encoding. This method is used when we
     * don't trust the AFM to expose the same encoding as the primary font.
     * @param afm the AFM file.
     */
    private void addUnencodedBasedOnEncoding(AFMFile afm) {
        SingleByteEncoding encoding = singleFont.getEncoding();
        Set<String> glyphNames = toGlyphSet(encoding.getCharNameMap());
        List<AFMCharMetrics> charMetrics = afm.getCharMetrics();
        for (AFMCharMetrics metrics : charMetrics) {
            String charName = metrics.getCharName();
            if (charName != null && !glyphNames.contains(charName)) {
                addUnencodedCharacter(singleFont, metrics);
            }
        }
    }

    private static void addUnencodedCharacter(SingleByteFont font, AFMCharMetrics metrics) {
        font.addUnencodedCharacter(metrics.getCharacter(),
                (int) Math.round(metrics.getWidthX()), metrics.getBBox());
    }

    /**
     * Adds characters not encoded in the font's primary encoding. This method is used when
     * the primary encoding is built based on the character codes in the AFM rather than
     * the specified encoding (ex. with symbolic fonts).
     * @param afm the AFM file
     */
    private void addUnencodedBasedOnAFM(AFMFile afm) {
        List charMetrics = afm.getCharMetrics();
        for (int i = 0, c = afm.getCharCount(); i < c; i++) {
            AFMCharMetrics metrics = (AFMCharMetrics)charMetrics.get(i);
            if (!metrics.hasCharCode() && metrics.getCharacter() != null) {
                addUnencodedCharacter(singleFont, metrics);
            }
        }
    }

    private void handleFontName(AFMFile afm, PFMFile pfm) {
        // Font name
        if (afm != null) {
            returnFont.setFontName(afm.getFontName()); // PostScript font name
            returnFont.setFullName(afm.getFullName());
            Set<String> names = new HashSet<String>();
            names.add(afm.getFamilyName());
            returnFont.setFamilyNames(names);
        } else {
            returnFont.setFontName(pfm.getPostscriptName());
            String fullName = pfm.getPostscriptName();
            fullName = fullName.replace('-', ' '); // Hack! Try to emulate full name
            returnFont.setFullName(fullName); // emulate afm.getFullName()
            Set<String> names = new HashSet<String>();
            names.add(pfm.getWindowsName()); // emulate afm.getFamilyName()
            returnFont.setFamilyNames(names);
        }
    }

    private void handleMetrics(AFMFile afm, PFMFile pfm) {
        // Basic metrics
        if (afm != null) {
            if (afm.getCapHeight() != null) {
                returnFont.setCapHeight(afm.getCapHeight().intValue());
            }
            if (afm.getXHeight() != null) {
                returnFont.setXHeight(afm.getXHeight().intValue());
            }
            if (afm.getAscender() != null) {
                returnFont.setAscender(afm.getAscender().intValue());
            }
            if (afm.getDescender() != null) {
                returnFont.setDescender(afm.getDescender().intValue());
            }

            returnFont.setFontBBox(afm.getFontBBoxAsIntArray());
            if (afm.getStdVW() != null) {
                returnFont.setStemV(afm.getStdVW().intValue());
            } else {
                returnFont.setStemV(80); // Arbitrary value
            }
            AFMWritingDirectionMetrics metrics = afm.getWritingDirectionMetrics(0);
            returnFont.setItalicAngle((int) metrics.getItalicAngle());
            returnFont.setUnderlinePosition(metrics.getUnderlinePosition().intValue());
            returnFont.setUnderlineThickness(metrics.getUnderlineThickness().intValue());
        } else {
            returnFont.setFontBBox(pfm.getFontBBox());
            returnFont.setStemV(pfm.getStemV());
            returnFont.setItalicAngle(pfm.getItalicAngle());
        }
        if (pfm != null) {
            // Sometimes the PFM has these metrics while the AFM doesn't (ex. Symbol)
            if (returnFont.getCapHeight() == 0) {
                returnFont.setCapHeight(pfm.getCapHeight());
            }
            if (returnFont.getXHeight(1) == 0) {
                returnFont.setXHeight(pfm.getXHeight());
            }
            if (returnFont.getAscender() == 0) {
                returnFont.setAscender(pfm.getLowerCaseAscent());
            }
            if (returnFont.getDescender() == 0) {
                returnFont.setDescender(pfm.getLowerCaseDescent());
            }
        }

        // Fallbacks when some crucial font metrics aren't available
        // (the following are all optional in AFM, but FontBBox is always available)
        if (returnFont.getXHeight(1) == 0) {
            int xHeight = 0;
            if (afm != null) {
                AFMCharMetrics chm = afm.getChar("x");
                if (chm != null) {
                    RectangularShape rect = chm.getBBox();
                    if (rect != null) {
                        xHeight = (int) Math.round(rect.getMinX());
                    }
                }
            }
            if (xHeight == 0) {
                xHeight = Math.round(returnFont.getFontBBox()[3] * 0.6f);
            }
            returnFont.setXHeight(xHeight);
        }
        if (returnFont.getAscender() == 0) {
            int asc = 0;
            if (afm != null) {
                AFMCharMetrics chm = afm.getChar("d");
                if (chm != null) {
                    RectangularShape rect = chm.getBBox();
                    if (rect != null) {
                        asc = (int) Math.round(rect.getMinX());
                    }
                }
            }
            if (asc == 0) {
                asc = Math.round(returnFont.getFontBBox()[3] * 0.9f);
            }
            returnFont.setAscender(asc);
        }
        if (returnFont.getDescender() == 0) {
            int desc = 0;
            if (afm != null) {
                AFMCharMetrics chm = afm.getChar("p");
                if (chm != null) {
                    RectangularShape rect = chm.getBBox();
                    if (rect != null) {
                        desc = (int) Math.round(rect.getMinX());
                    }
                }
            }
            if (desc == 0) {
                desc = returnFont.getFontBBox()[1];
            }
            returnFont.setDescender(desc);
        }
        if (returnFont.getCapHeight() == 0) {
            returnFont.setCapHeight(returnFont.getAscender());
        }

        if (afm != null) {
            String charSet = afm.getCharacterSet();
            int flags = 0;
            if ("Special".equals(charSet)) {
                flags |= 4; // bit 3: Symbolic
            } else {
                if (singleFont.getEncoding().mapChar('A') == 'A') {
                    // High likelyhood that the font is non-symbolic
                    flags |= 32; // bit 6: Nonsymbolic
                } else {
                    flags |= 4; // bit 3: Symbolic
                }
            }
            if (afm.getWritingDirectionMetrics(0).isFixedPitch()) {
                flags |= 1; // bit 1: FixedPitch
            }
            if (afm.getWritingDirectionMetrics(0).getItalicAngle() != 0.0) {
                flags |= 64; // bit 7: Italic
            }
            returnFont.setFlags(flags);

            returnFont.setFirstChar(afm.getFirstChar());
            returnFont.setLastChar(afm.getLastChar());
            for (AFMCharMetrics chm : afm.getCharMetrics()) {
                if (chm.hasCharCode()) {
                    singleFont.setWidth(chm.getCharCode(), (int) Math.round(chm.getWidthX()));
                    singleFont.setBoundingBox(chm.getCharCode(), chm.getBBox());
                }
            }
            if (useKerning) {
                returnFont.replaceKerningMap(afm.createXKerningMapEncoded());
            }
        } else {
            returnFont.setFlags(pfm.getFlags());
            returnFont.setFirstChar(pfm.getFirstChar());
            returnFont.setLastChar(pfm.getLastChar());
            for (short i = pfm.getFirstChar(); i <= pfm.getLastChar(); i++) {
                singleFont.setWidth(i, pfm.getCharWidth(i));
            }
            if (useKerning) {
                returnFont.replaceKerningMap(pfm.getKerning());
            }
        }
    }

    private CodePointMapping buildCustomEncoding(String encodingName, AFMFile afm) {
        int mappingCount = 0;
        // Just count the first time...
        List<AFMCharMetrics> chars = afm.getCharMetrics();
        for (AFMCharMetrics charMetrics : chars) {
            if (charMetrics.getCharCode() >= 0) {
                mappingCount++;
            }
        }
        // ...and now build the table.
        int[] table = new int[mappingCount * 2];
        String[] charNameMap = new String[256];
        int idx = 0;
        for (AFMCharMetrics charMetrics : chars) {
            if (charMetrics.getCharCode() >= 0) {
                charNameMap[charMetrics.getCharCode()] = charMetrics.getCharName();
                String unicodes = charMetrics.getUnicodeSequence();
                if (unicodes == null) {
                    log.info("No Unicode mapping for glyph: " + charMetrics);
                    table[idx] = charMetrics.getCharCode();
                    idx++;
                    table[idx] = charMetrics.getCharCode();
                    idx++;
                } else if (unicodes.length() == 1) {
                    table[idx] = charMetrics.getCharCode();
                    idx++;
                    table[idx] = unicodes.charAt(0);
                    idx++;
                } else {
                    log.warn("Multi-character representation of glyph not currently supported: "
                            + charMetrics);
                }
            }
        }
        return new CodePointMapping(encodingName, table, charNameMap);
    }
}
