// -*- related-file-name: "../include/efont/afm.hh" -*-

/* afm.{cc,hh} -- Adobe Font Metrics files
 *
 * Copyright (c) 1998-2012 Eddie Kohler
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version. This program is distributed in the hope that it will be
 * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
 * Public License for more details.
 */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <efont/afm.hh>
#include <efont/afmparse.hh>
#include <efont/t1cs.hh>	/* for UNKDOUBLE */
#include <lcdf/error.hh>
#include <ctype.h>
#include <assert.h>
namespace Efont {

AfmReader::AfmReader(AfmParser &parser, Metrics *afm, AfmMetricsXt *afm_xt,
		     ErrorHandler *errh)
    : _afm(afm), _afm_xt(afm_xt), _l(parser),
      _composite_warned(false), _metrics_sets_warned(false), _y_width_warned(0)
{
    _errh = errh ? errh : ErrorHandler::silent_handler();
}

Metrics *
AfmReader::read(Slurper &slurp, ErrorHandler *errh)
{
    AfmParser p(slurp);
    if (!p.ok())
	return 0;

    Metrics *afm = new Metrics;
    AfmMetricsXt *afm_xt = new AfmMetricsXt;
    afm->add_xt(afm_xt);
    AfmReader reader(p, afm, afm_xt, errh);

    if (!reader.read()) {
	delete afm;
	return 0;
    } else
	return afm;
}

Metrics *
AfmReader::read(const Filename &fn, ErrorHandler *errh)
{
    Slurper slurpy(fn);
    return read(slurpy, errh);
}


void
AfmReader::lwarning(const char *format, ...) const
{
    va_list val;
    va_start(val, format);
    _errh->vxmessage(_l.landmark(), ErrorHandler::e_warning, format, val);
    va_end(val);
}

void
AfmReader::lerror(const char *format, ...) const
{
    va_list val;
    va_start(val, format);
    _errh->vxmessage(_l.landmark(), ErrorHandler::e_error, format, val);
    va_end(val);
}

GlyphIndex
AfmReader::find_err(PermString name, const char *) const
{
    GlyphIndex gi = _afm->find(name);
    if (gi < 0)
	lerror("character `%s' doesn't exist", name.c_str());
    return gi;
}

void
AfmReader::composite_warning() const
{
    if (!_composite_warned)
	lwarning("composite fonts not supported");
    _composite_warned = 1;
}

void
AfmReader::metrics_sets_warning() const
{
    if (!_metrics_sets_warned)
	lwarning("only metrics set 0 is supported");
    _metrics_sets_warned = 1;
}

void
AfmReader::y_width_warning() const
{
    if (_y_width_warned < 40) {
	lwarning("character has a nonzero Y width");
	_y_width_warned++;
	if (_y_width_warned == 40)
	    lwarning("I won't warn you again.");
    }
}

void
AfmReader::no_match_warning(const char *context) const
{
    // keyword() will fail (and a warning won't get printed) only if the string
    // is all whitespace, which the spec allows
    PermString keyword = _l.keyword();
    if (!keyword) return;
    if (_l.key_matched()) {
	lwarning(context ? "bad `%s' command in %s:"
		 : "bad `%s' command:", keyword.c_str(), context);
	lwarning("field %d %s", _l.fail_field(), _l.message().c_str());
    } else
	lwarning(context ? "unknown command `%s' in %s"
		 : "unknown command `%s'", keyword.c_str(), context);
    _l.clear_message();
}


bool
AfmReader::read()
{
    AfmParser &l = _l;
    assert(_afm && _afm_xt);

    // First, read all opening comments into an array so we can print them out
    // later.
    PermString comment;
    while (l.next_line()) {
	if (l.isall("Comment %+s", &comment))
	    _afm_xt->opening_comments.push_back(comment);
	else if (l.isall("StartFontMetrics %g", (double *)0))
	    ;
	else {
	    l.save_line();
	    break;
	}
    }

    _afm->set_scale(1000);
    unsigned invalid_lines = 0;
    PermString s;
    bool isbasefont;
    int metrics_sets;
    int direction;

    while (l.next_line())
	switch (l.first()) {

	  case 'A':
	    if (l.isall("Ascender %g", &fd( fdAscender )))
		break;
	    goto invalid;

	  case 'C':
	    if (l.isall("Characters %d", (int *)0))
		break;
	    if (l.isall("CapHeight %g", &fd( fdCapHeight )))
		break;
	    if (l.isall("CharacterSet %+s", (PermString *) 0))
		break;
	    if (l.isall("CharWidth %g %g", (double *)0, (double *)0))
		break;
	    if (l.isall("Comment %+s", (PermString *) 0))
		break;
	    goto invalid;

	  case 'D':
	    if (l.isall("Descender %g", &fd( fdDescender )))
		break;
	    goto invalid;

	  case 'E':
	    if (l.isall("EncodingScheme %+s", &_afm_xt->encoding_scheme))
		break;
	    if (l.isall("EndDirection"))
		break;
	    if (l.isall("EndFontMetrics"))
		goto done;
	    if (l.isall("EscChar %d", (int *)0)) {
		composite_warning();
		break;
	    }
	    goto invalid;

	  case 'F':
	    if (l.isall("FontName %+s", &s)) {
		_afm->set_font_name(s);
		break;
	    }
	    if (l.isall("FullName %+s", &s)) {
		_afm->set_full_name(s);
		break;
	    }
	    if (l.isall("FamilyName %+s", &s)) {
		_afm->set_family(s);
		break;
	    }
	    if (l.isall("FontBBox %g %g %g %g",
			&fd( fdFontBBllx ), &fd( fdFontBBlly ),
			&fd( fdFontBBurx ), &fd( fdFontBBury )))
		break;
	    goto invalid;

	  case 'I':
	    if (l.isall("ItalicAngle %g", &fd( fdItalicAngle )))
		break;
	    if (l.isall("IsBaseFont %b", &isbasefont)) {
		if (isbasefont == 0)
		    composite_warning();
		break;
	    }
	    if (l.isall("IsFixedV %b", (bool *)0)) {
		metrics_sets_warning();
		break;
	    }
	    if (l.isall("IsFixedPitch %b", (bool *)0))
		break;
	    goto invalid;

	  case 'M':
	    if (l.isall("MappingScheme %d", (int *)0)) {
		composite_warning();
		break;
	    }
	    if (l.isall("MetricsSets %d", &metrics_sets)) {
		if (metrics_sets != 0)
		    metrics_sets_warning();
		break;
	    }
	    goto invalid;

	  case 'N':
	    if (l.isall("Notice %+s", &_afm_xt->notice))
		break;
	    goto invalid;

	  case 'S':
	    if (l.isall("StartDirection %d", &direction)) {
		if (direction != 0)
		    metrics_sets_warning();
		break;
	    }
	    if (l.isall("StartCharMetrics %d", (int *)0)) {
		read_char_metrics();
		break;
	    }
	    if (l.isall("StartKernData")) {
		read_kerns();
		break;
	    }
	    if (l.isall("StartComposites %d", (int *)0)) {
		read_composites();
		break;
	    }
	    if (l.isall("StdHW %g", &fd( fdStdHW )))
		break;
	    if (l.isall("StdVW %g", &fd( fdStdVW )))
		break;
	    if (l.isall("StartFontMetrics %g", (double *)0))
		break;
	    goto invalid;

	  case 'U':
	    if (l.isall("UnderlinePosition %g", &fd( fdUnderlinePosition )))
		break;
	    if (l.isall("UnderlineThickness %g", &fd( fdUnderlineThickness )))
		break;
	    goto invalid;

	  case 'V':
	    if (l.isall("Version %+s", &s)) {
		_afm->set_version(s);
		break;
	    }
	    if (l.isall("VVector %g %g", (double *)0, (double *)0)) {
		metrics_sets_warning();
		break;
	    }
	    goto invalid;

	  case 'W':
	    if (l.isall("Weight %+s", &s)) {
		_afm->set_weight(s);
		break;
	    }
	    goto invalid;

	  case 'X':
	    if (l.isall("XHeight %g", &fd( fdXHeight )))
		break;
	    goto invalid;

	  default:
	  invalid:
	    invalid_lines++;
	    no_match_warning();

	}

  done:
    if (invalid_lines >= l.lineno() - 10)
	return false;
    else
	return true;
}


static Vector<PermString> ligature_left;
static Vector<PermString> ligature_right;
static Vector<PermString> ligature_result;

void
AfmReader::read_char_metric_data() const
{
    int c = -1;
    double wx = UNKDOUBLE;
    double bllx = UNKDOUBLE, blly = 0, burx = 0, bury = 0;
    PermString n;
    PermString ligright, ligresult;

    AfmParser &l = _l;

    l.is("C %d ; WX %g ; N %/s ; B %g %g %g %g ;",
	 &c, &wx, &n, &bllx, &blly, &burx, &bury);

    while (l.left()) {

	switch (l.first()) {

	  case 'B':
	    if (l.is("B %g %g %g %g", &bllx, &blly, &burx, &bury))
		break;
	    goto invalid;

	  case 'C':
	    if (l.is("C %d", &c))
		break;
	    if (l.is("CH <%x>", &c))
		break;
	    goto invalid;

	  case 'E':
	    if (l.isall("EndCharMetrics"))
		return;
	    goto invalid;

	  case 'L':
	    if (l.is("L %/s %/s", &ligright, &ligresult)) {
		if (!n)
		    lerror("ligature given, but character has no name");
		else {
		    ligature_left.push_back(n);
		    ligature_right.push_back(ligright);
		    ligature_result.push_back(ligresult);
		}
		break;
	    }
	    goto invalid;

	  case 'N':
	    if (l.is("N %/s", &n))
		break;
	    goto invalid;

	  case 'W':
	    if (l.is("WX %g", &wx) ||
		l.is("W0X %g", &wx))
		break;
	    if (l.is("W %g %g", &wx, (double *)0) ||
		l.is("W0 %g %g", &wx, (double *)0) ||
		l.is("W0Y %g", (double *)0)) {
		y_width_warning();
		break;
	    }
	    if (l.is("W1X %g", (double *)0) ||
		l.is("W1Y %g", (double *)0) ||
		l.is("W1 %g %g", (double *)0, (double *)0)) {
		metrics_sets_warning();
		break;
	    }
	    goto invalid;

	  default:
	  invalid:
	    // always warn about unknown directives here!
	    no_match_warning("character metrics");
	    l.skip_until(';');
	    break;

	}

	l.is(";"); // get rid of any possible semicolon
    }

    // create the character
    if (!n)
	lwarning("character without a name ignored");
    else {
	if (_afm->find(n) != -1)
	    lwarning("character %s defined twice", n.c_str());

	GlyphIndex gi = _afm->add_glyph(n);

	_afm->wd(gi) = wx;
	_afm->lf(gi) = bllx;
	_afm->rt(gi) = burx;
	_afm->tp(gi) = bury;
	_afm->bt(gi) = blly;

	if (c != -1)
	    _afm->set_code(gi, c);
    }
}


void
AfmReader::read_char_metrics() const
{
    assert(!ligature_left.size());

    while (_l.next_line())
	// Grok the whole line. Are we on a character metric data line?
	switch (_l.first()) {

	  case 'C':
	    if (isspace(_l[1]) || (_l[1] == 'H' && isspace(_l[2]))) {
		read_char_metric_data();
		break;
	    }
	    if (_l.is("Comment"))
		break;
	    goto invalid;

	  case 'E':
	    if (_l.isall("EndCharMetrics"))
		goto end_char_metrics;
	    goto invalid;

	  default:
	  invalid:
	    no_match_warning();

	}

  end_char_metrics:

    for (int i = 0; i < ligature_left.size(); i++) {
	GlyphIndex leftgi = find_err(ligature_left[i], "ligature");
	GlyphIndex rightgi = find_err(ligature_right[i], "ligature");
	GlyphIndex resultgi = find_err(ligature_result[i], "ligature");
	if (leftgi >= 0 && rightgi >= 0 && resultgi >= 0)
	    if (_afm->add_lig(leftgi, rightgi, resultgi))
		lwarning("duplicate ligature; first ignored");
    }
    ligature_left.clear();
    ligature_right.clear();
    ligature_result.clear();
}


void
AfmReader::read_kerns() const
{
    double kx;
    PermString left, right;
    GlyphIndex leftgi, rightgi;

    AfmParser &l = _l;
    // AFM files have reversed pair programs when read.
    _afm->pair_program()->set_reversed(true);

    while (l.next_line())
	switch (l.first()) {

	  case 'C':
	    if (l.is("Comment"))
		break;
	    goto invalid;

	  case 'E':
	    if (l.isall("EndKernPairs"))
		break;
	    if (l.isall("EndKernData"))
		return;
	    if (l.isall("EndTrackKern"))
		break;
	    goto invalid;

	  case 'K':
	    if (l.isall("KPX %/s %/s %g", &left, &right, &kx)) {
		goto validkern;
	    }
	    if (l.isall("KP %/s %/s %g %g", &left, &right, &kx, (double *)0)) {
		y_width_warning();
		goto validkern;
	    }
	    if (l.isall("KPY %/s %/s %g", &left, &right, (double *)0)) {
		y_width_warning();
		break;
	    }
	    if (l.isall("KPH <%x> <%x> %g %g", (int *)0, (int *)0,
			(double *)0, (double *)0)) {
		lwarning("KPH not supported");
		break;
	    }
	    goto invalid;

	  validkern:
	    leftgi = find_err(left, "kern");
	    rightgi = find_err(right, "kern");
	    if (leftgi >= 0 && rightgi >= 0)
		// A kern with 0 amount is NOT useless!
		// (Because of multiple masters.)
		if (_afm->add_kern(leftgi, rightgi, _afm->add_kv(kx)))
		    lwarning("duplicate kern; first pair ignored");
	    break;

	  case 'S':
	    if (l.isall("StartKernPairs %d", (int *)0) ||
		l.isall("StartKernPairs0 %d", (int *)0))
		break;
	    if (l.isall("StartKernPairs1 %d", (int *)0)) {
		metrics_sets_warning();
		break;
	    }
	    if (l.isall("StartTrackKern %d", (int *)0))
		break;
	    goto invalid;

	  case 'T':
	    if (l.isall("TrackKern %g %g %g %g %g", (double *)0, (double *)0,
			(double *)0, (double *)0, (double *)0))
		break; // FIXME: implement TrackKern
	    goto invalid;

	  default:
	  invalid:
	    no_match_warning();
	    break;

	}
}


void
AfmReader::read_composites() const
{
    while (_l.next_line())
	switch (_l.first()) {

	  case 'C':
	    if (_l.is("Comment"))
		break;
	    if (_l.is("CC"))
		break;
	    goto invalid;

	  case 'E':
	    if (_l.isall("EndComposites"))
		return;
	    goto invalid;

	  default:
	  invalid:
	    no_match_warning();
	    break;

	}
}

}
