/*
 * 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.cff;

import java.io.IOException;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Collection;
import java.util.Locale;

/**
 * This class represents a formatter for a given Type1 font.  
 * @author Villu Ruusmann
 * @version $Revision: 1.0 $
 */
public class Type1FontFormatter
{

    private Type1FontFormatter()
    {
    }

    /**
     * Read and convert a given CFFFont.
     * @param font the given CFFFont
     * @return the Type1 font
     * @throws IOException if an error occurs during reading the given font
     */
    public static byte[] format(CFFFont font) throws IOException
    {
        DataOutput output = new DataOutput();
        printFont(font, output);
        return output.getBytes();
    }

    private static void printFont(CFFFont font, DataOutput output)
            throws IOException
    {
        output.println("%!FontType1-1.0 " + font.getName() + " "
                + font.getProperty("version"));

        printFontDictionary(font, output);

        for (int i = 0; i < 8; i++)
        {
            StringBuilder sb = new StringBuilder();

            for (int j = 0; j < 64; j++)
            {
                sb.append("0");
            }

            output.println(sb.toString());
        }

        output.println("cleartomark");
    }

    private static void printFontDictionary(CFFFont font, DataOutput output)
            throws IOException
    {
        output.println("10 dict begin");
        output.println("/FontInfo 10 dict dup begin");
        output.println("/version (" + font.getProperty("version")
                + ") readonly def");
        output.println("/Notice (" + font.getProperty("Notice")
                + ") readonly def");
        output.println("/FullName (" + font.getProperty("FullName")
                + ") readonly def");
        output.println("/FamilyName (" + font.getProperty("FamilyName")
                + ") readonly def");
        output.println("/Weight (" + font.getProperty("Weight")
                + ") readonly def");
        output.println("/ItalicAngle " + font.getProperty("ItalicAngle")
                + " def");
        output.println("/isFixedPitch " + font.getProperty("isFixedPitch")
                + " def");
        output.println("/UnderlinePosition "
                + font.getProperty("UnderlinePosition") + " def");
        output.println("/UnderlineThickness "
                + font.getProperty("UnderlineThickness") + " def");
        output.println("end readonly def");
        output.println("/FontName /" + font.getName() + " def");
        output.println("/PaintType " + font.getProperty("PaintType") + " def");
        output.println("/FontType 1 def");
        NumberFormat matrixFormat = new DecimalFormat("0.########", new DecimalFormatSymbols(Locale.US));
        output.println("/FontMatrix "
                + formatArray(font.getProperty("FontMatrix"), matrixFormat, false)
                + " readonly def");
        output.println("/FontBBox "
                + formatArray(font.getProperty("FontBBox"), false)
                + " readonly def");
        output.println("/StrokeWidth " + font.getProperty("StrokeWidth")
                + " def");

        Collection<CFFFont.Mapping> mappings = font.getMappings();

        output.println("/Encoding 256 array");
        output.println("0 1 255 {1 index exch /.notdef put} for");

        for (CFFFont.Mapping mapping : mappings)
        {
            output.println("dup " + mapping.getCode() + " /"
                    + mapping.getName() + " put");
        }

        output.println("readonly def");
        output.println("currentdict end");

        DataOutput eexecOutput = new DataOutput();

        printEexecFontDictionary(font, eexecOutput);

        output.println("currentfile eexec");

        byte[] eexecBytes = Type1FontUtil.eexecEncrypt(eexecOutput.getBytes());

        String hexString = Type1FontUtil.hexEncode(eexecBytes);
        for (int i = 0; i < hexString.length();)
        {
            String hexLine = hexString.substring(i, Math.min(i + 72, hexString
                    .length()));

            output.println(hexLine);

            i += hexLine.length();
        }
    }

    private static void printEexecFontDictionary(CFFFont font, DataOutput output)
            throws IOException
    {
        output.println("dup /Private 15 dict dup begin");
        output
                .println("/RD {string currentfile exch readstring pop} executeonly def");
        output.println("/ND {noaccess def} executeonly def");
        output.println("/NP {noaccess put} executeonly def");
        output.println("/BlueValues "
                + formatArray(font.getProperty("BlueValues"), true) + " ND");
        output.println("/OtherBlues "
                + formatArray(font.getProperty("OtherBlues"), true) + " ND");
        output.println("/BlueScale " + font.getProperty("BlueScale") + " def");
        output.println("/BlueShift " + font.getProperty("BlueShift") + " def");
        output.println("/BlueFuzz " + font.getProperty("BlueFuzz") + " def");
        output.println("/StdHW " + formatArray(font.getProperty("StdHW"), true)
                + " ND");
        output.println("/StdVW " + formatArray(font.getProperty("StdVW"), true)
                + " ND");
        output.println("/ForceBold " + font.getProperty("ForceBold") + " def");
        output.println("/MinFeature {16 16} def");
        output.println("/password 5839 def");

        Collection<CFFFont.Mapping> mappings = font.getMappings();

        output.println("2 index /CharStrings " + mappings.size()
                + " dict dup begin");

        Type1CharStringFormatter formatter = new Type1CharStringFormatter();

        for (CFFFont.Mapping mapping : mappings)
        {
            byte[] type1Bytes = formatter.format(mapping.toType1Sequence());

            byte[] charstringBytes = Type1FontUtil.charstringEncrypt(
                    type1Bytes, 4);

            output.print("/" + mapping.getName() + " " + charstringBytes.length
                    + " RD ");
            output.write(charstringBytes);
            output.print(" ND");

            output.println();
        }

        output.println("end");
        output.println("end");

        output.println("readonly put");
        output.println("noaccess put");
        output.println("dup /FontName get exch definefont pop");
        output.println("mark currentfile closefile");
    }

    private static String formatArray(Object object, boolean executable)
    {
        return formatArray(object, null, executable);
    }

    private static String formatArray(Object object, NumberFormat format, boolean executable)
    {
        StringBuffer sb = new StringBuffer();

        sb.append(executable ? "{" : "[");

        if (object instanceof Collection)
        {
            String sep = "";

            Collection<?> elements = (Collection<?>) object;
            for (Object element : elements)
            {
                sb.append(sep).append(formatElement(element, format));

                sep = " ";
            }
        } 
        else if (object instanceof Number)
        {
            sb.append(formatElement(object, format));
        }

        sb.append(executable ? "}" : "]");

        return sb.toString();
    }

    private static String formatElement(Object object, NumberFormat format)
    {
        if(format != null)
        {
            if (object instanceof Double || object instanceof Float)
            {
                Number number = (Number)object;
                return format.format(number.doubleValue());
            }
            else if (object instanceof Long || object instanceof Integer)
            {
                Number number = (Number)object;
                return format.format(number.longValue());
            }
        }
        return String.valueOf(object);
    }
}