#include <stdio.h>
#include <unistd.h>
#include <getopt.h>
#include <string.h>
#include <ctype.h>
#include <stdbool.h>
#include <time.h>

#include <libxml/tree.h>
#include <libxml/xpath.h>
#include <libxml/xpathInternals.h>
#include <libxml/uri.h>
#include <libxslt/transform.h>

#include "s1kd_tools.h"
#include "xsl.h"

#define PROG_NAME "s1kd-fmgen"
#define VERSION "3.6.0"

#define ERR_PREFIX PROG_NAME ": ERROR: "
#define INF_PREFIX PROG_NAME ": INFO: "

#define EXIT_BAD_DATE 1
#define EXIT_NO_TYPE 2
#define EXIT_BAD_TYPE 3
#define EXIT_MERGE 4
#define EXIT_BAD_STYLESHEET 5

#define S_NO_PM_ERR ERR_PREFIX "No publication module.\n"
#define S_NO_TYPE_ERR ERR_PREFIX "No FM type specified.\n"
#define S_BAD_TYPE_ERR ERR_PREFIX "Unknown front matter type: %s\n"
#define E_BAD_LIST ERR_PREFIX "Could not read list: %s\n"
#define E_BAD_DATE ERR_PREFIX "Bad date: %s\n"
#define E_MERGE_NAME ERR_PREFIX "Failed to update %s: no <%s> element to merge on.\n"
#define E_MERGE_ELEM ERR_PREFIX "Failed to update %s: no front matter contents generated.\n"
#define E_BAD_STYLESHEET ERR_PREFIX "Failed to update %s: %s is not a valid stylesheet.\n"
#define I_GENERATE INF_PREFIX "Generating FM content for %s (%s)...\n"
#define I_NO_INFOCODE INF_PREFIX "Skipping %s as no FM type is associated with info code: %s%s\n"
#define I_TRANSFORM INF_PREFIX "Applying transformation %s...\n"
#define I_XPROC_TRANSFORM INF_PREFIX "Applying XProc transforation %s/%s...\n"
#define I_XPROC_TRANSFORM_NONAME INF_PREFIX "Applying XProc transformation %s/line %ld...\n"

static enum verbosity { QUIET, NORMAL, VERBOSE, DEBUG } verbosity = NORMAL;

static xmlNodePtr first_xpath_node(xmlDocPtr doc, xmlNodePtr node, const xmlChar *expr)
{
       xmlXPathContextPtr ctx;
       xmlXPathObjectPtr obj;
       xmlNodePtr first;

       ctx = xmlXPathNewContext(doc ? doc : node->doc);
       ctx->node = node;

       obj = xmlXPathEvalExpression(BAD_CAST expr, ctx);

       first = xmlXPathNodeSetIsEmpty(obj->nodesetval) ? NULL : obj->nodesetval->nodeTab[0];

       xmlXPathFreeObject(obj);
       xmlXPathFreeContext(ctx);

       return first;
}

static xmlChar *first_xpath_string(xmlDocPtr doc, xmlNodePtr node, const xmlChar *expr)
{
       return xmlNodeGetContent(first_xpath_node(doc, node, expr));
}

/* Determine whether a set of parameters already contains a name. */
static bool has_param(const char **params, const char *name)
{
       int i;

       for (i = 0; params[i]; i += 2) {
               if (strcmp(params[i], name) == 0) {
                       return true;
               }
       }

       return false;
}

/* Apply an XProc XSLT step to a document. */
static xmlDocPtr apply_xproc_xslt(const xmlDocPtr doc, const char *xslpath, const xmlNodePtr xslt, const char **params)
{
       xmlXPathContextPtr ctx;
       xmlXPathObjectPtr obj;
       xmlDocPtr res;
       const char **combined_params;
       int i, nparams = 0;

       if (verbosity >= DEBUG) {
               xmlChar *name = xmlGetProp(xslt, BAD_CAST "name");

               if (name) {
                       fprintf(stderr, I_XPROC_TRANSFORM, xslpath, name);
               } else {
                       fprintf(stderr, I_XPROC_TRANSFORM_NONAME, xslpath, xmlGetLineNo(xslt));
               }

               xmlFree(name);
       }

       ctx = xmlXPathNewContext(xslt->doc);
       xmlXPathRegisterNs(ctx, BAD_CAST "p", BAD_CAST "http://www.w3.org/ns/xproc");
       xmlXPathSetContextNode(xslt, ctx);

       /* Get number of current params. */
       for (i = 0; params[i]; i += 2, ++nparams);

       /* Combine the current params with additional params from the XSLT step. */
       obj = xmlXPathEvalExpression(BAD_CAST "p:with-param", ctx);
       if (xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
               /* If there are no additional params, simply copy the current
                * params. */
               combined_params = malloc((nparams * 2 + 1) * sizeof(char *));

               for (i = 0; params[i]; ++i) {
                       combined_params[i] = strdup(params[i]);
               }

               combined_params[i] = NULL;
       } else {
               /* If there are additional params, copy the current params and
                * then append the additional ones. */
               int j;

               combined_params = malloc(((nparams + obj->nodesetval->nodeNr) * 2 + 1) * sizeof(char *));

               for (j = 0; params[j]; ++j) {
                       combined_params[j] = strdup(params[j]);
               }

               for (i = 0; i < obj->nodesetval->nodeNr; ++i) {
                       char *name;

                       name = (char *) xmlGetProp(obj->nodesetval->nodeTab[i], BAD_CAST "name");

                       /* User-defined parameters override XProc parameters. */
                       if (has_param(params, name)) {
                               xmlFree(name);
                       } else {
                               char *select;

                               select = (char *) xmlGetProp(obj->nodesetval->nodeTab[i], BAD_CAST "select");

                               combined_params[j++] = name;
                               combined_params[j++] = select;
                       }
               }

               combined_params[j] = NULL;
       }
       xmlXPathFreeObject(obj);

       /* Read the XProc stylesheet input. */
       obj = xmlXPathEvalExpression(BAD_CAST "p:input[@port='stylesheet']/p:*", ctx);
       if (xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
               res = xmlCopyDoc(doc, 1);
       } else {
               xmlNodePtr src = obj->nodesetval->nodeTab[0];
               xmlDocPtr s;

               if (xmlStrcmp(src->name, BAD_CAST "document") == 0) {
                       xmlChar *href, *URI;
                       href = xmlGetProp(obj->nodesetval->nodeTab[0], BAD_CAST "href");
                       URI = xmlBuildURI(href, BAD_CAST xslpath);
                       s = read_xml_doc((char *) URI);
                       xmlFree(href);
                       xmlFree(URI);
               } else if (xmlStrcmp(src->name, BAD_CAST "inline") == 0) {
                       s = xmlNewDoc(BAD_CAST "1.0");
                       xmlDocSetRootElement(s, xmlCopyNode(xmlFirstElementChild(src), 1));
               } else {
                       s = NULL;
               }

               if (s) {
                       xsltStylesheetPtr style;
                       style = xsltParseStylesheetDoc(s);
                       res   = xsltApplyStylesheet(style, doc, combined_params);
                       xsltFreeStylesheet(style);
               } else {
                       res = xmlCopyDoc(doc, 1);
               }
       }
       xmlXPathFreeObject(obj);

       xmlXPathFreeContext(ctx);

       for (i = 0; combined_params[i]; i += 2) {
               xmlFree((char *) combined_params[i]);
               xmlFree((char *) combined_params[i + 1]);
       }
       free(combined_params);

       return res;
}

static xmlDocPtr transform_doc(xmlDocPtr doc, const char *xslpath, const char **params)
{
       xmlDocPtr styledoc, res = NULL;
       xsltStylesheetPtr style;
       xmlXPathContextPtr ctx;
       xmlXPathObjectPtr obj;

       if (verbosity >= DEBUG) {
               fprintf(stderr, I_TRANSFORM, xslpath);
       }

       styledoc = read_xml_doc(xslpath);

       ctx = xmlXPathNewContext(styledoc);
       xmlXPathRegisterNs(ctx, BAD_CAST "p", BAD_CAST "http://www.w3.org/ns/xproc");
       obj = xmlXPathEvalExpression(BAD_CAST "/p:pipeline/p:xslt", ctx);

       if (xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
               style = xsltParseStylesheetDoc(styledoc);
               res = xsltApplyStylesheet(style, doc, params);
               xsltFreeStylesheet(style);
               styledoc = NULL;
       } else {
               int i;
               xmlDocPtr d = xmlCopyDoc(doc, 1);

               for (i =0; i < obj->nodesetval->nodeNr; ++i) {
                       res = apply_xproc_xslt(d, xslpath, obj->nodesetval->nodeTab[i], params);
                       xmlFreeDoc(d);
                       d = res;
               }
       }

       xmlXPathFreeObject(obj);
       xmlXPathFreeContext(ctx);

       xmlFreeDoc(styledoc);

       return res;
}

static xmlDocPtr transform_doc_builtin(xmlDocPtr doc, unsigned char *xsl, unsigned int len, const char **params)
{
       xmlDocPtr styledoc, res;
       xsltStylesheetPtr style;

       styledoc = read_xml_mem((const char *) xsl, len);

       style = xsltParseStylesheetDoc(styledoc);

       res = xsltApplyStylesheet(style, doc, params);

       xsltFreeStylesheet(style);

       return res;
}

static void get_builtin_xsl(const char *type, unsigned char **xsl, unsigned int *len)
{
       if (strcmp(type, "TP") == 0) {
               *xsl = xsl_tp_xsl;
               *len = xsl_tp_xsl_len;
       } else if (strcmp(type, "TOC") == 0) {
               *xsl = xsl_toc_xsl;
               *len = xsl_toc_xsl_len;
       } else if (strcmp(type, "HIGH") == 0) {
               *xsl = xsl_high_xsl;
               *len = xsl_high_xsl_len;
       } else if (strcmp(type, "LOEDM") == 0) {
               *xsl = xsl_loedm_xsl;
               *len = xsl_loedm_xsl_len;
       } else if (strcmp(type, "LOA") == 0) {
               *xsl = xsl_loa_xsl;
               *len = xsl_loa_xsl_len;
       } else if (strcmp(type, "LOASD") == 0) {
               *xsl = xsl_loasd_xsl;
               *len = xsl_loasd_xsl_len;
       } else if (strcmp(type, "LOI") == 0) {
               *xsl = xsl_loi_xsl;
               *len = xsl_loi_xsl_len;
       } else if (strcmp(type, "LOS") == 0) {
               *xsl = xsl_los_xsl;
               *len = xsl_los_xsl_len;
       } else if (strcmp(type, "LOT") == 0) {
               *xsl = xsl_lot_xsl;
               *len = xsl_lot_xsl_len;
       } else if (strcmp(type, "LOTBL") == 0) {
               *xsl = xsl_lotbl_xsl;
               *len = xsl_lotbl_xsl_len;
       } else {
               if (verbosity >= NORMAL) {
                       fprintf(stderr, S_BAD_TYPE_ERR, type);
               }
               exit(EXIT_BAD_TYPE);
       }
}

static xmlDocPtr generate_fm(xmlDocPtr doc, const char *type, const char **params)
{
       unsigned char *xsl;
       unsigned int len;

       get_builtin_xsl(type, &xsl, &len);

       return transform_doc_builtin(doc, xsl, len, params);
}

static void set_def_param(const char **params, int i, const char *val)
{
       char *s;
       s = malloc(strlen(val) + 3);
       sprintf(s, "'%s'", val);
       free((char *) params[i]);
       params[i] = s;
}

/* Default types of frontmatter where "deleted" objects and elements should be
* ignored. */
static bool default_ignore_del(const char *type)
{
       return
               strcmp(type, "LOA")   == 0 ||
               strcmp(type, "LOASD") == 0 ||
               strcmp(type, "LOEDM") == 0 ||
               strcmp(type, "LOI")   == 0 ||
               strcmp(type, "LOS")   == 0 ||
               strcmp(type, "LOT")   == 0 ||
               strcmp(type, "LOTBL") == 0 ||
               strcmp(type, "TOC")   == 0 ||
               strcmp(type, "TP")    == 0;
}

/* Remove "deleted" DMs from the flattend PM format. */
static void rem_deleted_dmodules(xmlDocPtr doc)
{
       xmlXPathContextPtr ctx;
       xmlXPathObjectPtr obj;

       ctx = xmlXPathNewContext(doc);
       obj = xmlXPathEvalExpression(BAD_CAST "//dmodule[identAndStatusSection/dmStatus/@issueType='deleted' or status/issno/@isstype='deleted']", ctx);

       if (!xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
               int i;
               for (i = 0; i < obj->nodesetval->nodeNr; ++i) {
                       xmlUnlinkNode(obj->nodesetval->nodeTab[i]);
                       xmlFreeNode(obj->nodesetval->nodeTab[i]);
                       obj->nodesetval->nodeTab[i] = NULL;
               }
       }

       xmlXPathFreeObject(obj);
       xmlXPathFreeContext(ctx);
}

static xmlDocPtr generate_fm_content_for_type(xmlDocPtr doc, const char *type, const char *fmxsl, const char *xslpath, const char **params, const bool ignore_del)
{
       xmlDocPtr src, res = NULL;
       int i = 0;

       /* Supply values to default parameters. */
       for (i = 0; params[i]; i += 2) {
               if (strcmp(params[i], "type") == 0) {
                       set_def_param(params, i + 1, type);
               }
       }

       /* Certain types of FM should ignore "deleted" objects/elements. */
       if (ignore_del) {
               src = xmlCopyDoc(doc, 1);
               rem_deleted_dmodules(src);
               rem_delete_elems(src);
       } else {
               src = doc;
       }

       /* Generate contents. */
       if (xslpath) {
               res = transform_doc(src, xslpath, params);
       } else if (fmxsl) {
               res = transform_doc(src, fmxsl, params);
       } else {
               res = generate_fm(src, type, params);
       }

       if (ignore_del) {
               xmlFreeDoc(src);
       }

       return res;
}

static xmlNodePtr find_fm(xmlDocPtr fmtypes, const xmlChar *incode, const xmlChar *incodev)
{
       xmlXPathContextPtr ctx;
       xmlXPathObjectPtr obj;
       xmlNodePtr node;

       ctx = xmlXPathNewContext(fmtypes);
       xmlXPathRegisterVariable(ctx, BAD_CAST "c", xmlXPathNewString(BAD_CAST incode));
       xmlXPathRegisterVariable(ctx, BAD_CAST "v", xmlXPathNewString(BAD_CAST incodev));

       obj = xmlXPathEvalExpression(BAD_CAST "//fm[starts-with(concat($c,$v),@infoCode)]", ctx);

       if (xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
               node = NULL;
       } else {
               node = obj->nodesetval->nodeTab[0];
       }

       xmlXPathFreeObject(obj);
       xmlXPathFreeContext(ctx);

       return node;
}

/* Copy elements from the source TP DM that can't be derived from the PM. */
static void copy_tp_elems(xmlDocPtr res, xmlDocPtr doc)
{
       xmlNodePtr fmtp, node;

       fmtp = first_xpath_node(res, NULL, BAD_CAST "//frontMatterTitlePage");

       if (!fmtp) {
               return;
       }

       if ((node = first_xpath_node(doc, NULL, BAD_CAST "//productIntroName"))) {
               xmlAddPrevSibling(first_xpath_node(res, fmtp,
                       BAD_CAST "pmTitle"),
                       xmlCopyNode(node, 1));
       }

       if ((node = first_xpath_node(doc, NULL, BAD_CAST "//externalPubCode"))) {
               xmlAddNextSibling(first_xpath_node(res, fmtp,
                       BAD_CAST "issueDate"),
                       xmlCopyNode(node, 1));
       }

       if ((node = first_xpath_node(doc, NULL, BAD_CAST "//productAndModel"))) {
               xmlAddNextSibling(first_xpath_node(res, fmtp,
                       BAD_CAST "(issueDate|externalPubCode)[last()]"),
                       xmlCopyNode(node, 1));
       }

       if ((node = first_xpath_node(doc, NULL, BAD_CAST "//productIllustration"))) {
               xmlAddNextSibling(first_xpath_node(res, fmtp,
                       BAD_CAST "(security|derivativeClassification|dataRestrictions)[last()]"),
                       xmlCopyNode(node, 1));
       }

       if ((node = first_xpath_node(doc, NULL, BAD_CAST "//enterpriseSpec"))) {
               xmlAddNextSibling(first_xpath_node(res, fmtp,
                       BAD_CAST "(security|derivativeClassification|dataRestrictions|productIllustration)[last()]"),
                       xmlCopyNode(node, 1));
       }

       if ((node = first_xpath_node(doc, NULL, BAD_CAST "//enterpriseLogo"))) {
               xmlAddNextSibling(first_xpath_node(res, fmtp,
                       BAD_CAST "(security|derivativeClassification|dataRestrictions|productIllustration|enterpriseSpec)[last()]"),
                       xmlCopyNode(node, 1));
       }

       if ((node = first_xpath_node(doc, NULL, BAD_CAST "//barCode"))) {
               xmlAddNextSibling(first_xpath_node(res, fmtp,
                       BAD_CAST "(responsiblePartnerCompany|publisherLogo)[last()]"),
                       xmlCopyNode(node, 1));
       }

       if ((node = first_xpath_node(doc, NULL, BAD_CAST "//frontMatterInfo"))) {
               xmlAddNextSibling(first_xpath_node(res, fmtp,
                       BAD_CAST "(responsiblePartnerCompany|publisherLogo|barCode)[last()]"),
                       xmlCopyNode(node, 1));
       }
}

static void set_issue_date(xmlDocPtr doc, xmlDocPtr pm, const char *issdate)
{
       xmlNodePtr dm_issue_date;
       xmlChar *year, *month, *day;

       if (!(dm_issue_date = first_xpath_node(doc, NULL, BAD_CAST "//issueDate|//issdate"))) {
               return;
       }

       if (strcasecmp(issdate, "pm") == 0) {
               xmlNodePtr pm_issue_date;

               if (!(pm_issue_date = first_xpath_node(pm, NULL, BAD_CAST "//issueDate|//issdate"))) {
                       return;
               }

               year  = xmlGetProp(pm_issue_date, BAD_CAST "year");
               month = xmlGetProp(pm_issue_date, BAD_CAST "month");
               day   = xmlGetProp(pm_issue_date, BAD_CAST "day");
       } else {
               year  = malloc(5 * sizeof(xmlChar));
               month = malloc(3 * sizeof(xmlChar));
               day   = malloc(3 * sizeof(xmlChar));

               if (strcmp(issdate, "-") == 0) {
                       time_t now;
                       struct tm *local;

                       time(&now);
                       local = localtime(&now);

                       xmlStrPrintf(year, 5, "%.4d", local->tm_year + 1900);
                       xmlStrPrintf(month, 3, "%.2d", local->tm_mon + 1);
                       xmlStrPrintf(day, 3, "%.2d", local->tm_mday);
               } else {
                       int n;

                       n = sscanf(issdate, "%4s-%2s-%2s", year, month, day);

                       if (n != 3) {
                               if (verbosity >= NORMAL) {
                                       fprintf(stderr, E_BAD_DATE, issdate);
                               }
                               exit(EXIT_BAD_DATE);
                       }
               }
       }

       xmlSetProp(dm_issue_date, BAD_CAST "year", year);
       xmlSetProp(dm_issue_date, BAD_CAST "month", month);
       xmlSetProp(dm_issue_date, BAD_CAST "day", day);

       xmlFree(year);
       xmlFree(month);
       xmlFree(day);
}

static void generate_fm_content_for_dm(
       xmlDocPtr pm,
       const char *dmpath,
       xmlDocPtr fmtypes,
       const char *fmtype,
       bool overwrite,
       const char *xslpath,
       const char **params,
       const char *issdate)
{
       xmlDocPtr doc, res = NULL;
       char *type, *fmxsl;
       bool ignore_del;

       if (!(doc = read_xml_doc(dmpath))) {
               return;
       }

       if (fmtype) {
               type  = strdup(fmtype);
               fmxsl = NULL;
               ignore_del = default_ignore_del(type);
       } else {
               xmlChar *incode, *incodev;
               xmlNodePtr fm;

               incode  = first_xpath_string(doc, NULL, BAD_CAST "//@infoCode|//incode");
               incodev = first_xpath_string(doc, NULL, BAD_CAST "//@infoCodeVariant|//incodev");

               fm = find_fm(fmtypes, incode, incodev);

               if (fm) {
                       xmlChar *igndel;

                       type  = (char *) xmlGetProp(fm, BAD_CAST "type");
                       fmxsl = (char *) xmlGetProp(fm, BAD_CAST "xsl");

                       if ((igndel = xmlGetProp(fm, BAD_CAST "ignoreDel"))) {
                               ignore_del = xmlStrcmp(igndel, BAD_CAST "yes") == 0;
                       } else {
                               ignore_del = default_ignore_del(type);
                       }

                       xmlFree(igndel);
               } else {
                       if (verbosity >= DEBUG) {
                               fprintf(stderr, I_NO_INFOCODE, dmpath, incode, incodev);
                       }

                       type = NULL;
                       fmxsl = NULL;
                       ignore_del = false;
               }

               xmlFree(incode);
               xmlFree(incodev);
       }

       if (type) {
               xmlNodePtr root;

               if (verbosity >= VERBOSE) {
                       fprintf(stderr, I_GENERATE, dmpath, type);
               }

               if (!(res = generate_fm_content_for_type(pm, type, fmxsl, xslpath, params, ignore_del))) {
                       if (verbosity >= NORMAL) {
                               fprintf(stderr, E_BAD_STYLESHEET, dmpath, xslpath ? xslpath : fmxsl);
                       }
                       exit(EXIT_BAD_STYLESHEET);
               }

               if (strcmp(type, "TP") == 0) {
                       copy_tp_elems(res, doc);
               }

               /* Merge the results of the transformation with the original DM
                * based on the name of the root element. */
               if ((root = xmlDocGetRootElement(res))) {
                       xmlXPathContextPtr ctx;
                       xmlXPathObjectPtr obj;

                       ctx = xmlXPathNewContext(doc);
                       xmlXPathRegisterVariable(ctx, BAD_CAST "name", xmlXPathNewString(root->name));
                       obj = xmlXPathEvalExpression(BAD_CAST "//*[name()=$name]", ctx);

                       if (xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
                               if (verbosity >= NORMAL) {
                                       fprintf(stderr, E_MERGE_NAME, dmpath, (char *) root->name);
                               }
                               exit(EXIT_MERGE);
                       } else {
                               xmlAddNextSibling(obj->nodesetval->nodeTab[0], xmlCopyNode(root, 1));
                               xmlUnlinkNode(obj->nodesetval->nodeTab[0]);
                               xmlFreeNode(obj->nodesetval->nodeTab[0]);
                               obj->nodesetval->nodeTab[0] = NULL;
                       }

                       xmlXPathFreeObject(obj);
                       xmlXPathFreeContext(ctx);
               } else {
                       if (verbosity >= NORMAL) {
                               fprintf(stderr, E_MERGE_ELEM, dmpath);
                       }
                       exit(EXIT_MERGE);
               }

               if (issdate) {
                       set_issue_date(doc, pm, issdate);
               }

               if (overwrite) {
                       save_xml_doc(doc, dmpath);
               } else {
                       save_xml_doc(doc, "-");
               }
       }

       xmlFree(type);
       xmlFree(fmxsl);
       xmlFreeDoc(doc);
       xmlFreeDoc(res);
}

static void generate_fm_content_for_list(
       xmlDocPtr pm,
       const char *path,
       xmlDocPtr fmtypes,
       const char *fmtype,
       bool overwrite,
       const char *xslpath,
       const char **params,
       const char *issdate)
{
       FILE *f;
       char line[PATH_MAX];

       if (path) {
               if (!(f = fopen(path, "r"))) {
                       if (verbosity >= NORMAL) {
                               fprintf(stderr, E_BAD_LIST, path);
                       }
                       return;
               }
       } else {
               f = stdin;
       }

       while (fgets(line, PATH_MAX, f)) {
               strtok(line, "\t\r\n");
               generate_fm_content_for_dm(pm, line, fmtypes, fmtype, overwrite, xslpath, params, issdate);
       }

       if (path) {
               fclose(f);
       }
}

static void dump_fmtypes_xml(void)
{
       xmlDocPtr doc;
       doc = read_xml_mem((const char *) fmtypes_xml, fmtypes_xml_len);
       save_xml_doc(doc, "-");
       xmlFreeDoc(doc);
}

static void dump_fmtypes_txt(void)
{
       printf("%.*s", fmtypes_txt_len, fmtypes_txt);
}

static void dump_builtin_xsl(const char *type)
{
       unsigned char *xsl;
       unsigned int len;

       get_builtin_xsl(type, &xsl, &len);

       printf("%.*s", len, xsl);
}

static void fix_fmxsl_paths(xmlDocPtr doc, const char *path)
{
       xmlXPathContextPtr ctx;
       xmlXPathObjectPtr obj;

       ctx = xmlXPathNewContext(doc);
       obj = xmlXPathEvalExpression(BAD_CAST "//@xsl", ctx);

       if (!xmlXPathNodeSetIsEmpty(obj->nodesetval)) {
               int i;

               for (i = 0; i < obj->nodesetval->nodeNr; ++i) {
                       xmlChar *xsl = xmlNodeGetContent(obj->nodesetval->nodeTab[i]);
                       xmlChar *URI = xmlBuildURI(xsl, BAD_CAST path);
                       xmlNodeSetContent(obj->nodesetval->nodeTab[i], URI);
                       xmlFree(xsl);
                       xmlFree(URI);
               }
       }

       xmlXPathFreeObject(obj);
       xmlXPathFreeContext(ctx);
}

static xmlDocPtr read_fmtypes(const char *path)
{
       xmlDocPtr doc;

       if (!(doc = read_xml_doc(path))) {
               FILE *f;
               char line[256];
               xmlNodePtr root;

               doc = xmlNewDoc(BAD_CAST "1.0");
               root = xmlNewNode(NULL, BAD_CAST "fmtypes");
               xmlDocSetRootElement(doc, root);

               f = fopen(path, "r");

               while (fgets(line, 256, f)) {
                       char incode[5] = "", type[32] = "", xsl[1024] = "";
                       xmlNodePtr fm;
                       int n;

                       n = sscanf(line, "%4s %31s %1023[^\n]", incode, type, xsl);

                       fm = xmlNewChild(root, NULL, BAD_CAST "fm", NULL);
                       xmlSetProp(fm, BAD_CAST "infoCode", BAD_CAST incode);
                       xmlSetProp(fm, BAD_CAST "type", BAD_CAST type);
                       if (n == 3) {
                               xmlSetProp(fm, BAD_CAST "xsl", BAD_CAST xsl);
                       }
               }

               fclose(f);
       }

       fix_fmxsl_paths(doc, path);

       return doc;
}

static void add_param(xmlNodePtr params, char *s)
{
       char *n, *v;
       xmlNodePtr p;

       n = strtok(s, "=");
       v = strtok(NULL, "");

       p = xmlNewChild(params, NULL, BAD_CAST "param", NULL);
       xmlSetProp(p, BAD_CAST "name", BAD_CAST n);
       xmlSetProp(p, BAD_CAST "value", BAD_CAST v);
}

static void add_def_param(xmlNodePtr params, const char *s)
{
       xmlNodePtr p;
       p = xmlNewChild(params, NULL, BAD_CAST "param", NULL);
       xmlSetProp(p, BAD_CAST "name", BAD_CAST s);
       xmlSetProp(p, BAD_CAST "value", BAD_CAST "");
}

static void show_help(void)
{
       puts("Usage: " PROG_NAME " [-D <TYPE>] [-F <FMTYPES>] [-I <date>] [-P <PM>] [-p <name>=<val> ...] [-t <TYPE>] [-x <XSL>] [-,flqvh?] [<DM>...]");
       puts("");
       puts("Options:");
       puts("  -,, --dump-fmtypes-xml      Dump the built-in .fmtypes file in XML format.");
       puts("  -., --dump-fmtypes          Dump the built-in .fmtypes file in simple text format.");
       puts("  -D, --dump-xsl <TYPE>       Dump the built-in XSLT for a type of front matter.");
       puts("  -F, --fmtypes <FMTYPES>     Specify .fmtypes file.");
       puts("  -f, --overwrite             Overwrite input data modules.");
       puts("  -h, -?, --help              Show usage message.");
       puts("  -I, --date <date>           Set the issue date of the generated front matter.");
       puts("  -l, --list                  Treat input as list of data modules.");
       puts("  -P, --pm <PM>               Generate front matter from the specified PM.");
       puts("  -p, --param <name>=<value>  Pass parameters to the XSLT used to generate the front matter.");
       puts("  -q, --quiet                 Quiet mode.");
       puts("  -t, --type <TYPE>           Generate the specified type of front matter.");
       puts("  -v, --verbose               Verbose output.");
       puts("  -x, --xsl <XSL>             Override built-in or user-configured XSLT.");
       puts("  --version                   Show version information.");
       puts("  <DM>                        Generate front matter content based on the specified data modules.");
       LIBXML2_PARSE_LONGOPT_HELP
}

static void show_version(void)
{
       printf("%s (s1kd-tools) %s\n", PROG_NAME, VERSION);
       printf("Using libxml %s and libxslt %s\n", xmlParserVersion, xsltEngineVersion);
}

int main(int argc, char **argv)
{
       int i;

       const char *sopts = ",.D:F:fI:lP:p:qt:vx:h?";
       struct option lopts[] = {
               {"version"         , no_argument      , 0, 0},
               {"help"            , no_argument      , 0, 'h'},
               {"dump-fmtypes-xml", no_argument      , 0, ','},
               {"dump-fmtypes"    , no_argument      , 0, '.'},
               {"dump-xsl"        , required_argument, 0, 'D'},
               {"fmtypes"         , required_argument, 0, 'F'},
               {"date"            , required_argument, 0, 'I'},
               {"overwrite"       , no_argument      , 0, 'f'},
               {"list"            , no_argument      , 0, 'l'},
               {"pm"              , required_argument, 0, 'P'},
               {"param"           , required_argument, 0, 'p'},
               {"quiet"           , no_argument      , 0, 'q'},
               {"type"            , required_argument, 0, 't'},
               {"verbose"         , no_argument      , 0, 'v'},
               {"xsl"             , required_argument, 0, 'x'},
               LIBXML2_PARSE_LONGOPT_DEFS
               {0, 0, 0, 0}
       };
       int loptind = 0;

       char *pmpath = NULL;
       char *fmtype = NULL;

       xmlDocPtr pm, fmtypes = NULL;

       bool overwrite = false;
       bool islist = false;
       char *xslpath = NULL;
       xmlNodePtr params_node;
       int nparams = 0;
       const char **params = NULL;
       char *issdate = NULL;

       params_node = xmlNewNode(NULL, BAD_CAST "params");

       /* Default parameter placeholders. */
       add_def_param(params_node, "type"); ++nparams;

       while ((i = getopt_long(argc, argv, sopts, lopts, &loptind)) != -1) {
               switch (i) {
                       case 0:
                               if (strcmp(lopts[loptind].name, "version") == 0) {
                                       show_version();
                                       return 0;
                               }
                               LIBXML2_PARSE_LONGOPT_HANDLE(lopts, loptind, optarg)
                               break;
                       case ',':
                               dump_fmtypes_xml();
                               return 0;
                       case '.':
                               dump_fmtypes_txt();
                               return 0;
                       case 'D':
                               dump_builtin_xsl(optarg);
                               return 0;
                       case 'F':
                               if (!fmtypes) {
                                       fmtypes = read_fmtypes(optarg);
                               }
                               break;
                       case 'f':
                               overwrite = true;
                               break;
                       case 'I':
                               issdate = strdup(optarg);
                               break;
                       case 'l':
                               islist = true;
                               break;
                       case 'P':
                               pmpath = strdup(optarg);
                               break;
                       case 'p':
                               add_param(params_node, optarg);
                               ++nparams;
                               break;
                       case 'q':
                               --verbosity;
                               break;
                       case 't':
                               fmtype = strdup(optarg);
                               break;
                       case 'v':
                               ++verbosity;
                               break;
                       case 'x':
                               xslpath = strdup(optarg);
                               break;
                       case 'h':
                       case '?':
                               show_help();
                               return 0;
               }
       }

       if (nparams > 0) {
               xmlNodePtr p;
               int n = 0;

               params = malloc((nparams * 2 + 1) * sizeof(char *));

               for (p = params_node->children; p; p = p->next) {
                       char *name, *value;

                       name  = (char *) xmlGetProp(p, BAD_CAST "name");
                       value = (char *) xmlGetProp(p, BAD_CAST "value");

                       params[n++] = name;
                       params[n++] = value;
               }

               params[n] = NULL;
       }
       xmlFreeNode(params_node);

       if (!fmtypes) {
               char fmtypes_fname[PATH_MAX];

               if (find_config(fmtypes_fname, DEFAULT_FMTYPES_FNAME)) {
                       fmtypes = read_fmtypes(fmtypes_fname);
               } else {
                       fmtypes = read_xml_mem((const char *) fmtypes_xml, fmtypes_xml_len);
               }
       }

       if (!pmpath) {
               pmpath = strdup("-");
       }

       pm = read_xml_doc(pmpath);

       if (optind < argc) {
               void (*gen_fn)(xmlDocPtr, const char *, xmlDocPtr, const char *,
                       bool, const char *, const char **, const char *);

               if (islist) {
                       gen_fn = generate_fm_content_for_list;
               } else {
                       gen_fn = generate_fm_content_for_dm;
               }

               for (i = optind; i < argc; ++i) {
                       gen_fn(pm, argv[i], fmtypes, fmtype, overwrite, xslpath, params, issdate);
               }
       } else if (fmtype) {
               xmlDocPtr res;
               res = generate_fm_content_for_type(pm, fmtype, NULL, xslpath, params, default_ignore_del(fmtype));
               save_xml_doc(res, "-");
               xmlFreeDoc(res);
       } else if (islist) {
               generate_fm_content_for_list(pm, NULL, fmtypes, fmtype, overwrite, xslpath, params, issdate);
       } else {
               if (verbosity >= NORMAL) {
                       fprintf(stderr, S_NO_TYPE_ERR);
               }
               exit(EXIT_NO_TYPE);
       }

       for (i = 0; i < nparams; ++i) {
               xmlFree((char *) params[i * 2]);
               xmlFree((char *) params[i * 2 + 1]);
       }
       free(params);

       xmlFreeDoc(pm);
       xmlFreeDoc(fmtypes);

       free(pmpath);
       free(fmtype);
       free(xslpath);

       xmlFree(issdate);

       xsltCleanupGlobals();
       xmlCleanupParser();

       return 0;
}