/* automatic.{cc,hh} -- code for automatic mode and interfacing with kpathsea
 *
 * Copyright (c) 2003-2005 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.
 */

#include <config.h>
#include "automatic.hh"
#include "kpseinterface.h"
#include "util.hh"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#ifdef HAVE_UNISTD_H
# include <unistd.h>
#endif
#include <sys/stat.h>
#include <sys/types.h>
#ifdef HAVE_SYS_TIME_H
# include <sys/time.h>
#endif
#ifdef HAVE_SYS_WAIT_H
# include <sys/wait.h>
#endif
#include <lcdf/error.hh>
#include <lcdf/straccum.hh>
#ifdef HAVE_FCNTL_H
# include <fcntl.h>
#endif
#include <algorithm>

#ifdef WIN32
# define mkdir(dir, access) mkdir(dir)
#endif

#if HAVE_AUTO_T1DOTLESSJ
enum { T1DOTLESSJ_EXIT_J_NODOT = 2 };
#endif

static String::Initializer initializer;
static String odir[NUMODIR];
static String typeface;
static String vendor;
static String map_file;
#define DEFAULT_VENDOR "lcdftools"
#define DEFAULT_TYPEFACE "unknown"

static const struct {
    const char *name;
    const char *envvar;
    const char *texdir;
} odir_info[] = {
    { "encoding", "ENCODINGDESTDIR", "#fonts/enc/dvips/@#dvips/@" },
    { "TFM", "TFMDESTDIR", "fonts/tfm/%" },
    { "PL", "PLDESTDIR", "fonts/pl/%" },
    { "VF", "VFDESTDIR", "fonts/vf/%" },
    { "VPL", "VPLDESTDIR", "fonts/vpl/%" },
    { "Type 1", "T1DESTDIR", "fonts/type1/%" },
    { "DVIPS map", "DVIPS directory", "#fonts/map/dvips/@#dvips/@" },
    { "DVIPS updmap", "DVIPS directory", "dvips" }
};

#if HAVE_KPATHSEA
static String odir_kpathsea[NUMODIR];

static bool writable_texdir_tried = false;
static String writable_texdir;	// always ends with directory separator
static int tds_1_1 = -1;

static bool mktexupd_tried = false;
static String mktexupd;

static String
kpsei_string(char* x)
{
    String s(x);
    free((void*)x);
    return s;
}

static void
look_for_writable_texdir(const char *path_variable, bool create)
{
    String path = kpsei_string(kpsei_path_expand(path_variable));
    while (path && !writable_texdir) {
	const char* colon = std::find(path.begin(), path.end(), kpsei_env_sep_char);
	String texdir = path.substring(path.begin(), colon);
	path = path.substring(colon + 1, path.end());
	if (access(texdir.c_str(), W_OK) >= 0)
	    writable_texdir = texdir;
	else if (create && errno != EACCES && mkdir(texdir.c_str(), 0777) >= 0)
	    // create file if it doesn't exist already
	    writable_texdir = texdir;
    }   
    if (writable_texdir && writable_texdir.back() != '/')
	writable_texdir += "/";
}

static void
find_writable_texdir(ErrorHandler *errh, const char *)
{
    look_for_writable_texdir("$TEXMFVAR", true);
    if (!writable_texdir)
	look_for_writable_texdir("$VARTEXMF", false);
    if (!writable_texdir)
	look_for_writable_texdir("$TEXMF", false);
    if (!writable_texdir) {
	errh->warning("no writable directory found in $TEXMFVAR or $TEXMF");
	errh->message("(You probably need to set your TEXMF environment variable; see\n\
the manual for more information. The current TEXMF path is\n\
'%s'.)", kpsei_string(kpsei_path_expand("$TEXMF")).c_str());
    }
    writable_texdir_tried = true;
}

static String
get_vendor()
{
    return (vendor ? vendor : String(DEFAULT_VENDOR));
}

static String
get_typeface()
{
    return (typeface ? typeface : String(DEFAULT_TYPEFACE));
}
#endif

bool
set_vendor(const String &s)
{
    bool had = (bool) vendor;
    vendor = s;
    return !had;
}

bool
set_typeface(const String &s, bool override)
{
    bool had = (bool) typeface;
    if (!had || override)
	typeface = s;
    return !had;
}

String
getodir(int o, ErrorHandler *errh)
{
    assert(o >= 0 && o < NUMODIR);
    
    if (!odir[o] && automatic && odir_info[o].envvar)
	odir[o] = getenv(odir_info[o].envvar);
    
#ifdef HAVE_KPATHSEA
    if (!odir[o] && automatic && !writable_texdir_tried)
	find_writable_texdir(errh, odir_info[o].name);

    if (!odir[o] && automatic && writable_texdir) {
	String suffix = odir_info[o].texdir;
	
	// May need to behave differently on TDS 1.1 rather than TDS 1.0. 
	if (suffix[0] == '#') {
	    // check type of TDS
	    if (tds_1_1 < 0) {
		// using a procedure suggested by Olaf Weber
		String encfonts = kpsei_string(kpsei_path_expand("$TEXMFMAIN/fonts/enc"));
		if (!encfonts)
		    encfonts = kpsei_string(kpsei_path_expand("$TEXMFDIST/fonts/enc"));
		tds_1_1 = (encfonts != String());
	    }
	    if (tds_1_1 == 0)
		suffix = suffix.substring(std::find(suffix.begin() + 1, suffix.end(), '#') + 1, suffix.end());
	    else
		suffix = suffix.substring(suffix.begin() + 1, std::find(suffix.begin() + 1, suffix.end(), '#'));
	}
	
	String dir = writable_texdir + suffix;

	if (dir.back() == '%')
	    dir = dir.substring(0, -1) + get_vendor() + "/" + get_typeface();
	else if (dir.back() == '@')
	    dir = dir.substring(0, -1) + get_vendor();
	
	// create parent directories as appropriate
	int slash = writable_texdir.length() - 1;
	while (access(dir.c_str(), F_OK) < 0 && slash < dir.length()) {
	    if ((slash = dir.find_left('/', slash + 1)) < 0)
		slash = dir.length();
	    String subdir = dir.substring(0, slash);
	    if (access(subdir.c_str(), F_OK) < 0
		&& !no_create
		&& mkdir(subdir.c_str(), 0777) < 0)
		goto kpathsea_done;
	}

	// that's our answer
	odir[o] = dir;
	odir_kpathsea[o] = dir;
    }
  kpathsea_done:
#endif
    
    if (!odir[o]) {
	if (automatic)
	    errh->warning("%s not specified, placing %s files in '.'", odir_info[o].envvar, odir_info[o].name);
	odir[o] = ".";
    }
    
    while (odir[o].length() && odir[o].back() == '/')
	odir[o] = odir[o].substring(0, -1);
    
    if (verbose)
	errh->message("placing %s files in '%s'", odir_info[o].name, odir[o].c_str());
    return odir[o];
}

bool
setodir(int o, const String &value)
{
    assert(o >= 0 && o < NUMODIR);
    bool had = (bool) odir[o];
    odir[o] = value;
    return !had;
}

const char *
odirname(int o)
{
    assert(o >= 0 && o < NUMODIR);
    return odir_info[o].name;
}

#if HAVE_KPATHSEA
static bool
file_in_kpathsea_odir(int o, const String &file)
{
    return odir_kpathsea[o]
	&& file.length() > odir[o].length()
	&& memcmp(file.data(), odir[o].data(), odir[o].length()) == 0
	&& file[odir[o].length()] == '/';
}
#endif

void
update_odir(int o, String file, ErrorHandler *errh)
{
    assert(o >= 0 && o < NUMODIR);
#if HAVE_KPATHSEA
    if (file.find_left('/') < 0)
	file = odir[o] + "/" + file;

    // exit if this directory was not found via kpathsea, or the file is not
    // in the kpathsea directory
    if (!file_in_kpathsea_odir(o, file))
	return;

    assert(writable_texdir && writable_texdir.length() <= odir[o].length()
	   && memcmp(file.data(), writable_texdir.data(), writable_texdir.length()) == 0);

    // divide the filename into portions
    // file == writable_texdir + directory + file
    file = file.substring(writable_texdir.length());
    while (file && file[0] == '/')
	file = file.substring(1);
    int last_slash = file.find_right('/');
    String directory = (last_slash >= 0 ? file.substring(0, last_slash) : String());
    file = file.substring(last_slash >= 0 ? last_slash + 1 : 0);
    if (!file)			// no filename to update
	return;

    // return if nocreate
    if (no_create) {
	errh->message("would update %sls-R for %s/%s", writable_texdir.c_str(), directory.c_str(), file.c_str());
	return;
    } else if (verbose)
	errh->message("updating %sls-R for %s/%s", writable_texdir.c_str(), directory.c_str(), file.c_str());
    
    // try to update ls-R ourselves, rather than running mktexupd --
    // mktexupd's runtime is painful: a half second to update a file
    String ls_r = writable_texdir + "ls-R";
    bool success = false;
    if (access(ls_r.c_str(), R_OK) >= 0) // make sure it already exists
	if (FILE *f = fopen(ls_r.c_str(), "a")) {
	    fprintf(f, "./%s:\n%s\n", directory.c_str(), file.c_str());
	    success = true;
	    fclose(f);
	}

    // otherwise, run mktexupd
    if (!success && writable_texdir.find_left('\'') < 0 && directory.find_left('\'') < 0 && file.find_left('\'') < 0) {
	// look for mktexupd script
	if (!mktexupd_tried) {
	    mktexupd = kpsei_string(kpsei_find_file("mktexupd", KPSEI_FMT_WEB2C));
	    mktexupd_tried = true;
	}

	// run script
	if (mktexupd) {
	    String command = mktexupd + " " + shell_quote(writable_texdir + directory) + " " + shell_quote(file);
	    int retval = system(command.c_str());
	    if (retval == 127)
		errh->error("could not run '%s'", command.c_str());
	    else if (retval < 0)
		errh->error("could not run '%s': %s", command.c_str(), strerror(errno));
	    else if (retval != 0)
		errh->error("'%s' failed", command.c_str());
	}
    }
#else
    (void) file, (void) errh;
#endif
}

bool
set_map_file(const String &s)
{
    bool had = (bool) map_file;
    map_file = s;
    return !had;
}

String
installed_type1(const String &otf_filename, const String &ps_fontname, bool allow_generate, ErrorHandler *errh)
{
    if (!ps_fontname)
	return String();

#if HAVE_KPATHSEA
# if HAVE_AUTO_CFFTOT1
    if (!(force && allow_generate && otf_filename && otf_filename != "-" && getodir(O_TYPE1, errh))) {
# endif
	// look for .pfb and .pfa
	String file, path;
	if ((file = ps_fontname + ".pfb", path = kpsei_string(kpsei_find_file(file.c_str(), KPSEI_FMT_TYPE1)))
	    || (file = ps_fontname + ".pfa", path = kpsei_string(kpsei_find_file(file.c_str(), KPSEI_FMT_TYPE1)))) {
	    if (verbose)
		errh->message("Type 1 file %s found with kpathsea at %s", file.c_str(), path.c_str());
	    return path;
	}
# if HAVE_AUTO_CFFTOT1
    }
# endif
#endif

#if HAVE_AUTO_CFFTOT1
    // if not found, and can generate on the fly, run cfftot1
    if (allow_generate && otf_filename && otf_filename != "-" && getodir(O_TYPE1, errh)) {
	String pfb_filename = odir[O_TYPE1] + "/" + ps_fontname + ".pfb";
	if (pfb_filename.find_left('\'') >= 0 || otf_filename.find_left('\'') >= 0)
	    return String();
	String command = "cfftot1 " + shell_quote(otf_filename) + " -n " + shell_quote(ps_fontname) + " " + shell_quote(pfb_filename);
	int retval = mysystem(command.c_str(), errh);
	if (retval == 127)
	    errh->error("could not run '%s'", command.c_str());
	else if (retval < 0)
	    errh->error("could not run '%s': %s", command.c_str(), strerror(errno));
	else if (retval != 0)
	    errh->error("'%s' failed", command.c_str());
	if (retval == 0) {
	    update_odir(O_TYPE1, pfb_filename, errh);
	    return pfb_filename;
	}
    }
#endif
    
    return String();
}

String
installed_type1_dotlessj(const String &otf_filename, const String &ps_fontname, bool allow_generate, ErrorHandler *errh)
{
    if (!ps_fontname)
	return String();
    if (verbose)
	errh->message("searching for dotless-j font for %s", ps_fontname.c_str());
    
    String j_ps_fontname = ps_fontname + "LCDFJ";
    
#if HAVE_KPATHSEA
# if HAVE_AUTO_T1DOTLESSJ
    if (!(force && allow_generate && getodir(O_TYPE1, errh))) {
# endif
	// look for existing .pfb or .pfa
	String file, path;
	if ((file = j_ps_fontname + ".pfb", path = kpsei_string(kpsei_find_file(file.c_str(), KPSEI_FMT_TYPE1)))
	    || (file = j_ps_fontname + ".pfa", path = kpsei_string(kpsei_find_file(file.c_str(), KPSEI_FMT_TYPE1)))) {
	    if (verbose)
		errh->message("Type 1 file %s found with kpathsea at %s", file.c_str(), path.c_str());
	    return path;
	}
# if HAVE_AUTO_T1DOTLESSJ
    }
# endif
#endif

#if HAVE_AUTO_T1DOTLESSJ
    // if not found, and can generate on the fly, try running t1dotlessj
    if (allow_generate && getodir(O_TYPE1, errh)) {
	if (String base_filename = installed_type1(otf_filename, ps_fontname, allow_generate, errh)) {
	    String pfb_filename = odir[O_TYPE1] + "/" + j_ps_fontname + ".pfb";
	    if (pfb_filename.find_left('\'') >= 0 || base_filename.find_left('\'') >= 0)
		return String();
	    String command = "t1dotlessj " + shell_quote(base_filename) + " -n " + shell_quote(j_ps_fontname) + " " + shell_quote(pfb_filename);
	    int retval = mysystem(command.c_str(), errh);
	    if (retval == 127)
		errh->warning("could not run '%s'", command.c_str());
	    else if (retval < 0)
		errh->warning("could not run '%s': %s", command.c_str(), strerror(errno));
	    else if (WEXITSTATUS(retval) == T1DOTLESSJ_EXIT_J_NODOT)
		return String("\0", 1);
	    else if (retval != 0)
		errh->warning("'%s' failed (%d)", command.c_str(), retval);
	    if (retval == 0) {
		update_odir(O_TYPE1, pfb_filename, errh);
		return pfb_filename;
	    } else
		errh->warning("output font will not contain a dotless-j character");
	}
    }
#endif

    return String();
}

int
update_autofont_map(const String &fontname, String mapline, ErrorHandler *errh)
{
#if HAVE_KPATHSEA
    if (automatic && !map_file && getodir(O_MAP, errh))
	map_file = odir[O_MAP] + "/" + get_vendor() + ".map";
#endif

    if (map_file == "" || map_file == "-")
	fputs(mapline.c_str(), stdout);
    else {
	// report no_create/verbose
	if (no_create) {
	    errh->message("would update %s for %s", map_file.c_str(), String(fontname).c_str());
	    return 0;
	} else if (verbose)
	    errh->message("updating %s for %s", map_file.c_str(), String(fontname).c_str());
	
	int fd = open(map_file.c_str(), O_RDWR | O_CREAT, 0666);
	if (fd < 0)
	    return errh->error("%s: %s", map_file.c_str(), strerror(errno));
	FILE *f = fdopen(fd, "r+");
	// NB: also change encoding logic if you change this code

#if defined(F_SETLKW) && defined(HAVE_FTRUNCATE)
	{
	    struct flock lock;
	    lock.l_type = F_WRLCK;
	    lock.l_whence = SEEK_SET;
	    lock.l_start = 0;
	    lock.l_len = 0;
	    int result;
	    while ((result = fcntl(fd, F_SETLKW, &lock)) < 0 && errno == EINTR)
		/* try again */;
	    if (result < 0) {
		result = errno;
		fclose(f);
		return errh->error("locking %s: %s", map_file.c_str(), strerror(result));
	    }
	}
#endif

	// read old data from map file
	StringAccum sa;
	while (!feof(f))
	    if (char *x = sa.reserve(8192)) {
		int amt = fread(x, 1, 8192, f);
		sa.forward(amt);
	    } else {
		fclose(f);
		return errh->error("Out of memory!");
	    }
	String text = sa.take_string();

	// add comment if necessary
	bool created = (!text);
	if (created)
	    text = "% Automatically maintained by otftotfm or other programs. Do not edit.\n\n";
	if (text.back() != '\n')
	    text += "\n";

	// append old encodings
	int fl = 0;
	int nl = text.find_left('\n') + 1;
	bool changed = created;
	while (fl < text.length()) {
	    if (fl + fontname.length() + 1 < nl
		&& memcmp(text.data() + fl, fontname.data(), fontname.length()) == 0
		&& text[fl + fontname.length()] == ' ') {
		// found the old name
		if (text.substring(fl, nl - fl) == mapline) {
		    // duplicate of old name, don't change it
		    fclose(f);
		    if (verbose)
			errh->message("%s unchanged", map_file.c_str());
		    return 0;
		} else {
		    text = text.substring(0, fl) + text.substring(nl);
		    nl = fl;
		    changed = true;
		}
	    }
	    fl = nl;
	    nl = text.find_left('\n', fl) + 1;
	}

	// special case: empty mapline, unchanged file
	if (!mapline && !changed) {
	    fclose(f);
	    if (verbose)
		errh->message("%s unchanged", map_file.c_str());
	    return 0;
	}
	
	// add our text
	text += mapline;

	// rewind file
#ifdef HAVE_FTRUNCATE
	rewind(f);
	ftruncate(fd, 0);
#else
	fclose(f);
	f = fopen(map_file.c_str(), "w");
#endif

	// write data
	fwrite(text.data(), 1, text.length(), f);
	
	fclose(f);

	// inform about the new file if necessary
	if (created)
	    update_odir(O_MAP, map_file, errh);

#if HAVE_KPATHSEA && !WIN32
	// run 'updmap' if present
	String updmap_dir, updmap_file;
	if (automatic && (output_flags & G_UPDMAP))
	    updmap_dir = getodir(O_MAP_PARENT, errh);
	if (updmap_dir && (updmap_file = updmap_dir + "/updmap")
	    && access(updmap_file.c_str(), X_OK) >= 0) {
	    // want to run 'updmap' from its directory, can't use system()
	    if (verbose)
		errh->message("running %s", updmap_file.c_str());
	    
	    pid_t child = fork();
	    if (child < 0)
		errh->fatal("%s during fork", strerror(errno));
	    else if (child == 0) {
		// change to updmap directory, run it
		if (chdir(updmap_dir.c_str()) < 0)
		    errh->fatal("%s: %s during chdir", updmap_dir.c_str(), strerror(errno));
		if (execl("./updmap", updmap_file.c_str(), (const char*) 0) < 0)
		    errh->fatal("%s: %s during exec", updmap_file.c_str(), strerror(errno));
		exit(1);	// should never get here
	    }
	    
# if HAVE_WAITPID
	    // wait for updmap to finish
	    int status;
	    while (1) {
		pid_t answer = waitpid(child, &status, 0);
		if (answer >= 0)
		    break;
		else if (errno != EINTR)
		    errh->fatal("%s during wait", strerror(errno));
	    }
	    if (!WIFEXITED(status))
		errh->warning("%s exited abnormally", updmap_file.c_str());
	    else if (WEXITSTATUS(status) != 0)
		errh->warning("%s exited with status %d", updmap_file.c_str(), WEXITSTATUS(status));
# else
#  error "need waitpid() support: report this bug to the maintainer"
# endif
	    goto ran_updmap;
	}

# if HAVE_AUTO_UPDMAP
	// run system updmap
	if (output_flags & G_UPDMAP) {
	    String filename = map_file;
	    int slash = filename.find_right('\'');
	    if (slash >= 0)
		filename = filename.substring(slash + 1);
	    String command = "updmap --nomkmap --enable Map " + shell_quote(filename) + "; updmap";
	    if (verbose)
		command += " 1>&2";
	    else
		command += " >/dev/null 2>&1";
	    int retval = mysystem(command.c_str(), errh);
	    if (retval == 127)
		errh->warning("could not run '%s'", command.c_str());
	    else if (retval < 0)
		errh->warning("could not run '%s': %s", command.c_str(), strerror(errno));
	    else if (retval != 0)
		errh->warning("'%s' exited with status %d;\nrun it manually to check for errors", command.c_str(), WEXITSTATUS(retval));
	    goto ran_updmap;
	}
# endif
	
	if (verbose)
	    errh->message("not running updmap");

      ran_updmap: ;
#endif
    }

    return 0;
}

String
locate_encoding(String encfile, ErrorHandler *errh, bool literal)
{
    if (!encfile || encfile == "-")
	return encfile;
    
    if (!literal) {
	int slash = encfile.find_right('/');
	int dot = encfile.find_left('.', slash >= 0 ? slash : 0);
	if (dot < 0)
	    if (String file = locate_encoding(encfile + ".enc", errh, true))
		return file;
    }
    
#if HAVE_KPATHSEA
    if (String file = kpsei_string(kpsei_find_file(encfile.c_str(), KPSEI_FMT_ENCODING))) {
	if (verbose)
	    errh->message("encoding file %s found with kpathsea at %s", encfile.c_str(), file.c_str());
	return file;
    } else if (verbose)
	errh->message("encoding file %s not found with kpathsea", encfile.c_str());
#endif

    if (access(encfile.c_str(), R_OK) >= 0)
	return encfile;
    else
	return String();
}
