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

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

/**
 * TrueType font file parser.
 * 
 * @author Ben Litchfield
 */
public class TTFParser
{
    private boolean isEmbedded = false;
    private boolean parseOnDemandOnly = false;

    /**
     * Constructor.
     */
    public TTFParser()
    {
        this(false);
    }

    /**
     * Constructor.
     *  
     * @param isEmbedded true if the font is embedded in PDF
     */
    public TTFParser(boolean isEmbedded)
    {
        this(isEmbedded, false);
    }

    /**
     *  Constructor.
     *  
     * @param isEmbedded true if the font is embedded in PDF
     * @param parseOnDemand true if the tables of the font should be parsed on demand
     */
    public TTFParser(boolean isEmbedded, boolean parseOnDemand)
    {
        this.isEmbedded = isEmbedded;
        parseOnDemandOnly = parseOnDemand;
    }

    /**
     * Parse a file and return a TrueType font.
     *
     * @param ttfFile The TrueType font filename.
     * @return A TrueType font.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    public TrueTypeFont parse(String ttfFile) throws IOException
    {
        return parse(new File(ttfFile));
    }

    /**
     * Parse a file and return a TrueType font.
     *
     * @param ttfFile The TrueType font file.
     * @return A TrueType font.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    public TrueTypeFont parse(File ttfFile) throws IOException
    {
        RAFDataStream raf = new RAFDataStream(ttfFile, "r");
        try
        {
            return parse(raf);
        }
        catch (IOException ex)
        {
            // close only on error (file is still being accessed later)
            raf.close();
            throw ex;
        }
    }

    /**
     * Parse an input stream and return a TrueType font.
     *
     * @param inputStream The TTF data stream to parse from. It will be closed before returning.
     * @return A TrueType font.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    public TrueTypeFont parse(InputStream inputStream) throws IOException
    {
        return parse(new MemoryTTFDataStream(inputStream));
    }

    /**
     * Parse an input stream and return a TrueType font that is to be embedded.
     *
     * @param inputStream The TTF data stream to parse from. It will be closed before returning.
     * @return A TrueType font.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    public TrueTypeFont parseEmbedded(InputStream inputStream) throws IOException
    {
        this.isEmbedded = true;
        return parse(new MemoryTTFDataStream(inputStream));
    }

    /**
     * Parse a file and get a true type font.
     *
     * @param raf The TTF file.
     * @return A TrueType font.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    TrueTypeFont parse(TTFDataStream raf) throws IOException
    {
        TrueTypeFont font = newFont(raf);
        font.setVersion(raf.read32Fixed());
        int numberOfTables = raf.readUnsignedShort();
        int searchRange = raf.readUnsignedShort();
        int entrySelector = raf.readUnsignedShort();
        int rangeShift = raf.readUnsignedShort();
        for (int i = 0; i < numberOfTables; i++)
        {
            TTFTable table = readTableDirectory(font, raf);
            
            // skip tables with zero length
            if (table != null)
            {
                font.addTable(table);
            }
        }
        // parse tables if wanted
        if (!parseOnDemandOnly)
        {
            parseTables(font);
        }

        return font;
    }

    TrueTypeFont newFont(TTFDataStream raf)
    {
        return new TrueTypeFont(raf);
    }

    /**
     * Parse all tables and check if all needed tables are present.
     *
     * @param font the TrueTypeFont instance holding the parsed data.
     * @throws IOException If there is an error parsing the TrueType font.
     */
    private void parseTables(TrueTypeFont font) throws IOException
    {
        for (TTFTable table : font.getTables())
        {
            if (!table.getInitialized())
            {
                font.readTable(table);
            }
        }

        boolean isPostScript = allowCFF() && font.tables.containsKey(CFFTable.TAG);
        
        HeaderTable head = font.getHeader();
        if (head == null)
        {
            throw new IOException("head is mandatory");
        }

        HorizontalHeaderTable hh = font.getHorizontalHeader();
        if (hh == null)
        {
            throw new IOException("hhead is mandatory");
        }

        MaximumProfileTable maxp = font.getMaximumProfile();
        if (maxp == null)
        {
            throw new IOException("maxp is mandatory");
        }

        PostScriptTable post = font.getPostScript();
        if (post == null && !isEmbedded)
        {
            // in an embedded font this table is optional
            throw new IOException("post is mandatory");
        }

        if (!isPostScript)
        {
            IndexToLocationTable loc = font.getIndexToLocation();
            if (loc == null)
            {
                throw new IOException("loca is mandatory");
            }

            if (font.getGlyph() == null)
            {
                throw new IOException("glyf is mandatory");
            }
        }
        
        if (font.getNaming() == null && !isEmbedded)
        {
            throw new IOException("name is mandatory");
        }
        
        if (font.getHorizontalMetrics() == null)
        {
            throw new IOException("hmtx is mandatory");
        }
        
        if (!isEmbedded && font.getCmap() == null)
        {
            throw new IOException("cmap is mandatory");
        }
    }

    protected boolean allowCFF()
    {
        return false;
    }

    private TTFTable readTableDirectory(TrueTypeFont font, TTFDataStream raf) throws IOException
    {
        TTFTable table;
        String tag = raf.readString(4);
        switch (tag)
        {
            case CmapTable.TAG:
                table = new CmapTable(font);
                break;
            case GlyphTable.TAG:
                table = new GlyphTable(font);
                break;
            case HeaderTable.TAG:
                table = new HeaderTable(font);
                break;
            case HorizontalHeaderTable.TAG:
                table = new HorizontalHeaderTable(font);
                break;
            case HorizontalMetricsTable.TAG:
                table = new HorizontalMetricsTable(font);
                break;
            case IndexToLocationTable.TAG:
                table = new IndexToLocationTable(font);
                break;
            case MaximumProfileTable.TAG:
                table = new MaximumProfileTable(font);
                break;
            case NamingTable.TAG:
                table = new NamingTable(font);
                break;
            case OS2WindowsMetricsTable.TAG:
                table = new OS2WindowsMetricsTable(font);
                break;
            case PostScriptTable.TAG:
                table = new PostScriptTable(font);
                break;
            case DigitalSignatureTable.TAG:
                table = new DigitalSignatureTable(font);
                break;
            case KerningTable.TAG:
                table = new KerningTable(font);
                break;
            case VerticalHeaderTable.TAG:
                table = new VerticalHeaderTable(font);
                break;
            case VerticalMetricsTable.TAG:
                table = new VerticalMetricsTable(font);
                break;
            case VerticalOriginTable.TAG:
                table = new VerticalOriginTable(font);
                break;
            case GlyphSubstitutionTable.TAG:
                table = new GlyphSubstitutionTable(font);
                break;
            default:
                table = readTable(font, tag);
                break;
        }
        table.setTag(tag);
        table.setCheckSum(raf.readUnsignedInt());
        table.setOffset(raf.readUnsignedInt());
        table.setLength(raf.readUnsignedInt());
        
        // skip tables with zero length (except glyf)
        if (table.getLength() == 0 && !tag.equals(GlyphTable.TAG))
        {
            return null;
        }

        return table;
    }

    protected TTFTable readTable(TrueTypeFont font, String tag)
    {
        // unknown table type but read it anyway.
        return new TTFTable(font);
    }
}
