/*
 * 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: PFMFile.java 1758773 2016-09-01 13:02:29Z ssteiner $ */

package org.apache.fop.fonts.type1;

// Java
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;

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

import org.apache.xmlgraphics.fonts.Glyphs;

/**
 * This class represents a PFM file (or parts of it) as a Java object.
 */
public class PFMFile {

    // Header stuff
    private String windowsName;
    private String postscriptName;
    private short dfItalic;
    //private int dfWeight;
    private short dfCharSet;
    private short dfPitchAndFamily;
    private int dfAvgWidth;
    private int dfMaxWidth;
    private int dfMinWidth;
    private short dfFirstChar;
    private short dfLastChar;

    // Extension stuff
    // ---

    // Extend Text Metrics
    private int etmCapHeight;
    private int etmXHeight;
    private int etmLowerCaseAscent;
    private int etmLowerCaseDescent;

    // Extent table
    private int[] extentTable;

    private Map<Integer, Map<Integer, Integer>> kerningTab = new HashMap<Integer, Map<Integer, Integer>>();

    /**
     * logging instance
     */
    protected Log log = LogFactory.getLog(PFMFile.class);

    /**
     * Parses a PFM file
     *
     * @param  inStream The stream from which to read the PFM file.
     * @throws IOException In case of an I/O problem
     */
    public void load(InputStream inStream) throws IOException {
        byte[] pfmBytes = IOUtils.toByteArray(inStream);
        InputStream bufin = inStream;
        bufin = new ByteArrayInputStream(pfmBytes);
        PFMInputStream in = new PFMInputStream(bufin);
        bufin.mark(512);
        short sh1 = in.readByte();
        short sh2 = in.readByte();
        if (sh1 == 128 && sh2 == 1) {
            //Found the first section header of a PFB file!
            IOUtils.closeQuietly(in);
            throw new IOException("Cannot parse PFM file. You probably specified the PFB file"
                    + " of a Type 1 font as parameter instead of the PFM.");
        }
        bufin.reset();
        byte[] b = new byte[16];
        if ((bufin.read(b) == b.length) && new String(b, "US-ASCII").equalsIgnoreCase("StartFontMetrics")) {
            //Found the header of a AFM file!
            IOUtils.closeQuietly(in);
            throw new IOException("Cannot parse PFM file. You probably specified the AFM file"
                    + " of a Type 1 font as parameter instead of the PFM.");
        }
        bufin.reset();
        final int version = in.readShort();
        if (version != 256) {
            log.warn("PFM version expected to be '256' but got '" + version + "'."
                    + " Please make sure you specify the PFM as parameter"
                    + " and not the PFB or the AFM.");
        }
        //final long filesize = in.readInt();
        bufin.reset();

        loadHeader(in);
        loadExtension(in);
    }

    /**
     * Parses the header of the PFM file.
     *
     * @param  inStream The stream from which to read the PFM file.
     * @throws IOException In case of an I/O problem
     */
    private void loadHeader(PFMInputStream inStream) throws IOException {
        if (inStream.skip(80) != 80) {
            throw new IOException("premature EOF when skipping 80 bytes");
        }
        dfItalic = inStream.readByte();
        if (inStream.skip(2) != 2) {
            throw new IOException("premature EOF when skipping 2 bytes");
        }
        inStream.readShort(); // dfWeight =
        dfCharSet = inStream.readByte();
        if (inStream.skip(4) != 4) {
            throw new IOException("premature EOF when skipping 4 bytes");
        }
        dfPitchAndFamily = inStream.readByte();
        dfAvgWidth = inStream.readShort();
        dfMaxWidth = inStream.readShort();
        dfFirstChar = inStream.readByte();
        dfLastChar = inStream.readByte();
        if (inStream.skip(8) != 8) {
            throw new IOException("premature EOF when skipping 8 bytes");
        }
        long faceOffset = inStream.readInt();

        inStream.reset();
        if (inStream.skip(faceOffset) != faceOffset) {
            throw new IOException("premature EOF when skipping faceOffset bytes");
        }
        windowsName = inStream.readString();

        inStream.reset();
        if (inStream.skip(117) != 117) {
            throw new IOException("premature EOF when skipping 117 bytes");
        }
    }

    /**
     * Parses the extension part of the PFM file.
     *
     * @param     inStream The stream from which to read the PFM file.
     */
    private void loadExtension(PFMInputStream inStream) throws IOException {
        final int size = inStream.readShort();
        if (size != 30) {
            log.warn("Size of extension block was expected to be "
                + "30 bytes, but was " + size + " bytes.");
        }
        final long extMetricsOffset = inStream.readInt();
        final long extentTableOffset = inStream.readInt();
        if (inStream.skip(4) != 4) { //Skip dfOriginTable
            throw new IOException("premature EOF when skipping dfOriginTable bytes");
        }
        final long kernPairOffset = inStream.readInt();
        if (inStream.skip(4) != 4) { //Skip dfTrackKernTable
            throw new IOException("premature EOF when skipping dfTrackKernTable bytes");
        }
        long driverInfoOffset = inStream.readInt();

        if (kernPairOffset > 0) {
            inStream.reset();
            if (inStream.skip(kernPairOffset) != kernPairOffset) {
                throw new IOException("premature EOF when skipping kernPairOffset bytes");
            }
            loadKernPairs(inStream);
        }

        inStream.reset();
        if (inStream.skip(driverInfoOffset) != driverInfoOffset) {
            throw new IOException("premature EOF when skipping driverInfoOffset bytes");
        }
        postscriptName = inStream.readString();

        if (extMetricsOffset != 0) {
            inStream.reset();
            if (inStream.skip(extMetricsOffset) != extMetricsOffset) {
                throw new IOException("premature EOF when skipping extMetricsOffset bytes");
            }
            loadExtMetrics(inStream);
        }
        if (extentTableOffset != 0) {
            inStream.reset();
            if (inStream.skip(extentTableOffset) != extentTableOffset) {
                throw new IOException("premature EOF when skipping extentTableOffset bytes");
            }
            loadExtentTable(inStream);
        }

    }

    /**
     * Parses the kernPairs part of the pfm file
     *
     * @param     inStream The stream from which to read the PFM file.
     */
    private void loadKernPairs(PFMInputStream inStream) throws IOException {
        int i = inStream.readShort();


        if (log.isTraceEnabled()) {
            log.trace(i + " kerning pairs");
        }
        while (i > 0) {
            int g1 = (int) inStream.readByte();
            i--;

            int g2 = (int) inStream.readByte();

            int adj = inStream.readShort();
            if (adj > 0x8000) {
                adj = -(0x10000 - adj);
            }

            if (log.isTraceEnabled()) {
                log.trace("Char no: (" + g1 + ", " + g2 + ") kern: " + adj);
                final String glyph1 = Glyphs.TEX8R_GLYPH_NAMES[g1];
                final String glyph2 = Glyphs.TEX8R_GLYPH_NAMES[g2];
                log.trace("glyphs: " + glyph1 + ", " + glyph2);
            }

            Map<Integer, Integer> adjTab = kerningTab.get(g1);
            if (adjTab == null) {
                adjTab = new HashMap<Integer, Integer>();
            }
            adjTab.put(g2, adj);
            kerningTab.put(g1, adjTab);
        }
    }

    /**
     * Parses the extended metrics part of the PFM file.
     *
     * @param     inStream The stream from which to read the PFM file.
     */
    private void loadExtMetrics(PFMInputStream inStream) throws IOException {
        final int size = inStream.readShort();
        if (size != 52) {
            log.warn("Size of extension block was expected to be "
                + "52 bytes, but was " + size + " bytes.");
        }
        if (inStream.skip(12) != 12) { //Skip etmPointSize, etmOrientation, etmMasterHeight,
                                       //etmMinScale, etmMaxScale, emtMasterUnits
            throw new IOException("premature EOF when skipping etmPointSize, ... bytes");
        }
        etmCapHeight = inStream.readShort();
        etmXHeight = inStream.readShort();
        etmLowerCaseAscent = inStream.readShort();
        etmLowerCaseDescent = -(inStream.readShort());
        //Ignore the rest of the values
    }

    /**
     * Parses the extent table of the PFM file.
     *
     * @param     inStream The stream from which to read the PFM file.
     */
    private void loadExtentTable(PFMInputStream inStream) throws IOException {
        extentTable = new int[dfLastChar - dfFirstChar + 1];
        dfMinWidth = dfMaxWidth;
        for (short i = dfFirstChar; i <= dfLastChar; i++) {
            extentTable[i - dfFirstChar] = inStream.readShort();
            if (extentTable[i - dfFirstChar] < dfMinWidth) {
                dfMinWidth = extentTable[i - dfFirstChar];
            }
        }
    }

    /**
     * Returns the Windows name of the font.
     *
     * @return The Windows name.
     */
    public String getWindowsName() {
        return windowsName;
    }

    /**
     * Return the kerning table. The kerning table is a Map with
     * strings with glyphnames as keys, containing Maps as value.
     * The value map contains a glyph name string key and an Integer value
     *
     * @return A Map containing the kerning table
     */
    public Map<Integer, Map<Integer, Integer>> getKerning() {
        return kerningTab;
    }

    /**
     * Returns the Postscript name of the font.
     *
     * @return The Postscript name.
     */
    public String getPostscriptName() {
        return postscriptName;
    }

    /**
     * Returns the charset used for the font.
     *
     * @return The charset (0=WinAnsi).
     */
    public short getCharSet() {
        return dfCharSet;
    }

    /**
     * Returns the charset of the font as a string.
     *
     * @return The name of the charset.
     */
    public String getCharSetName() {
        //TODO Had to remove the detection for Expert(Subset) encoding. The PFM is not suitable
        //for detecting these character sets. We have to parse the AFM for that.
        switch (dfCharSet) {
        case 0:
            return "WinAnsi"; // AKA ISOAdobe
        case 2:
            if ("Symbol".equals(getPostscriptName())) {
                return "Symbol";
            }
            break;
        case 128:
            return "Shift-JIS (Japanese)";
        default:
            log.warn("Unknown charset detected (" + dfCharSet
                    + ", 0x" + Integer.toHexString(dfCharSet)
                    + "). Trying fallback to WinAnsi.");
        }
        return "WinAnsi";
    }

    /**
     * Returns the number of the character that defines
     * the first entry in the widths list.
     *
     * @return The number of the first character.
     */
    public short getFirstChar() {
        return dfFirstChar;
    }

    /**
     * Returns the number of the character that defines
     * the last entry in the widths list.
     *
     * @return The number of the last character.
     */
    public short getLastChar() {
        return dfLastChar;
    }

    /**
     * Returns the CapHeight parameter for the font (height of uppercase H).
     *
     * @return The CapHeight parameter.
     */
    public int getCapHeight() {
        return etmCapHeight;
    }

    /**
     * Returns the XHeight parameter for the font (height of lowercase x).
     *
     * @return The CapHeight parameter.
     */
    public int getXHeight() {
        return etmXHeight;
    }

    /**
     * Returns the LowerCaseAscent parameter for the font (height of lowercase d).
     *
     * @return The LowerCaseAscent parameter.
     */
    public int getLowerCaseAscent() {
        return etmLowerCaseAscent;
    }

    /**
     * Returns the LowerCaseDescent parameter for the font (height of lowercase p).
     *
     * @return The LowerCaseDescent parameter.
     */
    public int getLowerCaseDescent() {
        return etmLowerCaseDescent;
    }

    /**
     * Tells whether the font has proportional character spacing.
     *
     * @return ex. true for Times, false for Courier.
     */
    public boolean getIsProportional() {
        return ((dfPitchAndFamily & 1) == 1);
    }

    /**
     * Returns the bounding box for the font.
     * Note: this value is just an approximation,
     * it does not really exist in the PFM file.
     *
     * @return The calculated Font BBox.
     */
    public int[] getFontBBox() {
        int[] bbox = new int[4];

        // Just guessing....
        if (!getIsProportional() && (dfAvgWidth == dfMaxWidth)) {
            bbox[0] = -20;
        } else {
            bbox[0] = -100;
        }
        bbox[1] = getLowerCaseDescent() - 5;
        bbox[2] = dfMaxWidth + 10;
        bbox[3] = getLowerCaseAscent() + 5;
        return bbox;
    }

    /**
     * Indicates whether the font is non-symbolic (Font uses the Adobe standard Latin character
     * set or a subset of it).
     * @return true if the font is non-symbolic
     */
    public boolean isNonSymbolic() {
        return (dfCharSet != 2); //!= Symbol fonts
    }

    /**
     * Returns the characteristics flags for the font as
     * needed for a PDF font descriptor (See PDF specs).
     *
     * @return The characteristics flags.
     */
    public int getFlags() {
        int flags = 0;
        if (!getIsProportional()) {
            flags |= 1; //bit 1: FixedPitch
        }
        if (isNonSymbolic()) {
            flags |= 32; //bit 6: Nonsymbolic
        } else {
            flags |= 4; //bit 3: Symbolic
        }
        //int serif = dfPitchAndFamily & 0xFFFE;
        if ((dfPitchAndFamily & 16) != 0) {
            flags |= 2; //bit 2: Serif
        }
        if ((dfPitchAndFamily & 64) != 0) {
            flags |= 8; //bit 4: Script
        }
        if (dfItalic != 0) {
            flags |= 64; //bit 7: Italic
        }
        return flags;
    }

    /**
     * Returns the width of the dominant vertical stems of the font.
     * Note: this value is just an approximation,
     * it does not really exist in the PFM file.
     *
     * @return The vertical stem width.
     */
    public int getStemV() {
        // Just guessing....
        if (dfItalic != 0) {
            return (int) Math.round(dfMinWidth * 0.25);
        } else {
            return (int) Math.round(dfMinWidth * 0.6);
        }
    }

    /**
     * Returns the italic angle of the font.
     * Note: this value is just an approximation,
     * it does not really exist in the PFM file.
     *
     * @return The italic angle.
     */
    public int getItalicAngle() {
        if (dfItalic != 0) {
            return -16;    // Just guessing....
        } else {
            return 0;
        }
    }

    /**
     * Returns the width of a character
     *
     * @param  which The number of the character for which the width is requested.
     * @return The width of a character.
     */
    public int getCharWidth(short which) {
        if (extentTable != null) {
            return extentTable[which - dfFirstChar];
        } else {
            //Fixed-width font (PFM may have no extent table)
            //we'll just use the average width
            return this.dfAvgWidth;
        }
    }

}
