#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <getopt.h>
#include <stdbool.h>
#include <ctype.h>
#include <libgen.h>
#include <regex.h>
#include <libxml/tree.h>
#include <libxml/xpath.h>
#include <libxslt/transform.h>
#include "s1kd_tools.h"
#include "xslt.h"
#include "elems.h"

#define PROG_NAME "s1kd-ref"
#define VERSION "3.8.0"

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

#define EXIT_MISSING_FILE 1
#define EXIT_BAD_INPUT 2
#define EXIT_BAD_ISSUE 3
#define EXIT_BAD_XPATH 4

#define OPT_TITLE     (int) 0x001
#define OPT_ISSUE     (int) 0x002
#define OPT_LANG      (int) 0x004
#define OPT_DATE      (int) 0x008
#define OPT_SRCID     (int) 0x010
#define OPT_CIRID     (int) 0x020
#define OPT_INS       (int) 0x040
#define OPT_URL       (int) 0x080
#define OPT_CONTENT   (int) 0x100
#define OPT_NONSTRICT (int) 0x200

/* Regular expressions to match references. */

/* Common components */
#define ISSNO_REGEX "(_[0-9]{3}-[0-9]{2})?"
#define LANG_REGEX  "(_[A-Z]{2}-[A-Z]{2})?"

/* Optional prefix */
#define DME_REGEX "(DME-)?[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX
#define DMC_REGEX "(DMC-)?[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX
#define CSN_REGEX "(CSN-)?[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT]"
#define PME_REGEX "(PME-)?[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define PMC_REGEX "(PMC-)?[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define SME_REGEX "(SME-)?[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define SMC_REGEX "(SMC-)?[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define COM_REGEX "(COM-)?[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9]{4}-[0-9]{5}-[QIR]" LANG_REGEX
#define DML_REGEX "(DML-)?[0-9A-Z]{2,14}-[0-9A-Z]{5}-[CPS]-[0-9]{4}-[0-9]{5}" ISSNO_REGEX

/* Mandatory prefix */
#define DME_REGEX_STRICT "DME-[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX
#define DMC_REGEX_STRICT "DMC-[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX
#define CSN_REGEX_STRICT "CSN-[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT]"
#define PME_REGEX_STRICT "PME-[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define PMC_REGEX_STRICT "PMC-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define SME_REGEX_STRICT "SME-[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define SMC_REGEX_STRICT "SMC-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX
#define COM_REGEX_STRICT "COM-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9]{4}-[0-9]{5}-[QIR]" LANG_REGEX
#define DML_REGEX_STRICT "DML-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[CPS]-[0-9]{4}-[0-9]{5}" ISSNO_REGEX
#define ICN_REGEX "(ICN-[A-Z0-9]{5}-[A-Z0-9]{5,10}-[0-9]{3}-[0-9]{2})|(ICN-[A-Z0-9]{2,14}-[A-Z0-9]{1,4}-[A-Z0-9]{6,9}-[A-Z0-9]{1}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z]{1}-[0-9]{2,3}-[0-9]{1,2})"

/* No prefix */
#define DME_REGEX_NOPRE "^[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX "$"
#define DMC_REGEX_NOPRE "^[0-9A-Z]{2,14}-[0-9A-Z]{1,4}-[0-9A-Z]{2,3}-[0-9A-Z]{2}-[0-9A-Z]{2,4}-[0-9A-Z]{3,5}-[0-9A-Z]{4}-[ABCDT](-[0-9A-Z]{4})?" ISSNO_REGEX LANG_REGEX "$"
#define PME_REGEX_NOPRE "^[0-9A-Z]+-[0-9A-Z]+-[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX "$"
#define PMC_REGEX_NOPRE "^[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9A-Z]{5}-[0-9]{2}" ISSNO_REGEX LANG_REGEX "$"
#define COM_REGEX_NOPRE "^[0-9A-Z]{2,14}-[0-9A-Z]{5}-[0-9]{4}-[0-9]{5}-[QIR]" LANG_REGEX "$"
#define DML_REGEX_NOPRE "^[0-9A-Z]{2,14}-[0-9A-Z]{5}-[CPS]-[0-9]{4}-[0-9]{5}" ISSNO_REGEX "$"

/* Issue of the S1000D specification to create references for. */
enum issue { ISS_20, ISS_21, ISS_22, ISS_23, ISS_30, ISS_40, ISS_41, ISS_42, ISS_50 };

#define DEFAULT_S1000D_ISSUE ISS_50

/* Verbosity of the program's output. */
static enum verbosity { QUIET, NORMAL, VERBOSE, DEBUG } verbosity = NORMAL;

/* Function for creating a new reference node. */
typedef xmlNodePtr (*newref_t)(const char *, const char *, int);

static xmlNode *find_child(xmlNode *parent, char *name)
{
       xmlNode *cur;

       if (!parent) return NULL;

       for (cur = parent->children; cur; cur = cur->next) {
               if (strcmp((char *) cur->name, name) == 0) {
                       return cur;
               }
       }

       return NULL;
}

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

       ctx = xmlXPathNewContext(doc ? doc : node->doc);
       ctx->node = node;
       obj = xmlXPathEvalExpression(xpath, ctx);

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

       xmlXPathFreeObject(obj);
       xmlXPathFreeContext(ctx);

       return first;
}

static xmlChar *first_xpath_value(xmlDocPtr doc, xmlNodePtr node, xmlChar *xpath)
{
       return xmlNodeGetContent(first_xpath_node(doc, node, xpath));
}

static xmlNodePtr find_or_create_refs(xmlDocPtr doc)
{
       xmlNodePtr refs;

       refs = first_xpath_node(doc, NULL, BAD_CAST "//content//refs");

       if (!refs) {
               xmlNodePtr content, child;
               content = first_xpath_node(doc, NULL, BAD_CAST "//content");
               child = xmlFirstElementChild(content);
               refs = xmlNewNode(NULL, BAD_CAST "refs");
               if (child) {
                       refs = xmlAddPrevSibling(child, refs);
               } else {
                       refs = xmlAddChild(content ,refs);
               }
       }

       return refs;
}

static void dump_node(xmlNodePtr node, const char *dst)
{
       xmlBufferPtr buf;
       buf = xmlBufferCreate();
       xmlNodeDump(buf, NULL, node, 0, 0);
       if (strcmp(dst, "-") == 0) {
               puts((char *) buf->content);
       } else {
               FILE *f;
               f = fopen(dst, "w");
               fputs((char *) buf->content, f);
               fclose(f);
       }
       xmlBufferFree(buf);
}

static xmlNodePtr new_issue_info(char *s)
{
       char n[4], w[3];
       xmlNodePtr issue_info;

       if (sscanf(s, "_%3[^-]-%2s", n, w) != 2) {
               return NULL;
       }

       issue_info = xmlNewNode(NULL, BAD_CAST "issueInfo");
       xmlSetProp(issue_info, BAD_CAST "issueNumber", BAD_CAST n);
       xmlSetProp(issue_info, BAD_CAST "inWork", BAD_CAST w);

       return issue_info;
}

static xmlNodePtr new_language(char *s)
{
       char l[4], c[3];
       xmlNodePtr language;

       if (sscanf(s, "_%3[^-]-%2s", l, c) != 2) {
               return NULL;
       }

       lowercase(l);

       language = xmlNewNode(NULL, BAD_CAST "language");
       xmlSetProp(language, BAD_CAST "languageIsoCode", BAD_CAST l);
       xmlSetProp(language, BAD_CAST "countryIsoCode", BAD_CAST c);

       return language;
}

static void set_xlink(xmlNodePtr node, const char *href)
{
       xmlNsPtr xlink;
       xlink = xmlNewNs(node, BAD_CAST "http://www.w3.org/1999/xlink", BAD_CAST "xlink");
       xmlSetNsProp(node, xlink, BAD_CAST "href", BAD_CAST href);
}

#define SME_FMT "SME-%255[^-]-%255[^-]-%14[^-]-%5s-%5s-%2s"
#define SMC_FMT "SMC-%14[^-]-%5s-%5s-%2s"

static xmlNodePtr new_smc_ref(const char *ref, const char *fname, int opts)
{
       char extension_producer[256] = "";
       char extension_code[256]     = "";
       char model_ident_code[15]    = "";
       char smc_issuer[6]            = "";
       char smc_number[6]            = "";
       char smc_volume[3]            = "";
       xmlNode *smc_ref;
       xmlNode *smc_ref_ident;
       xmlNode *smc_code;
       bool is_extended;
       int n;

       is_extended = strncmp(ref, "SME-", 4) == 0;

       if (is_extended) {
               n = sscanf(ref, SME_FMT,
                       extension_producer,
                       extension_code,
                       model_ident_code,
                       smc_issuer,
                       smc_number,
                       smc_volume);
               if (n != 6) {
                       if (verbosity > QUIET) {
                               fprintf(stderr, ERR_PREFIX "SCORM content package extended code invalid: %s\n", ref);
                       }
                       exit(EXIT_BAD_INPUT);
               }
       } else {
               n = sscanf(ref, SMC_FMT,
                       model_ident_code,
                       smc_issuer,
                       smc_number,
                       smc_volume);
               if (n != 4) {
                       if (verbosity > QUIET) {
                               fprintf(stderr, ERR_PREFIX "SCORM content package code invalid: %s\n", ref);
                       }
                       exit(EXIT_BAD_INPUT);
               }
       }

       smc_ref = xmlNewNode(NULL, BAD_CAST "scormContentPackageRef");
       smc_ref_ident = xmlNewChild(smc_ref, NULL, BAD_CAST "scormContentPackageRefIdent", NULL);

       if (is_extended) {
               xmlNode *ident_extension;
               ident_extension = xmlNewChild(smc_ref_ident, NULL, BAD_CAST "identExtension", NULL);
               xmlSetProp(ident_extension, BAD_CAST "extensionProducer", BAD_CAST extension_producer);
               xmlSetProp(ident_extension, BAD_CAST "extensionCode", BAD_CAST extension_code);
       }

       smc_code = xmlNewChild(smc_ref_ident, NULL, BAD_CAST "scormContentPackageCode", NULL);

       xmlSetProp(smc_code, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
       xmlSetProp(smc_code, BAD_CAST "scormContentPackageIssuer", BAD_CAST smc_issuer);
       xmlSetProp(smc_code, BAD_CAST "scormContentPackageNumber", BAD_CAST smc_number);
       xmlSetProp(smc_code, BAD_CAST "scormContentPackageVolume", BAD_CAST smc_volume);

       if (opts) {
               xmlDocPtr doc;
               xmlNodePtr ref_smc_address = NULL;
               xmlNodePtr ref_smc_ident = NULL;
               xmlNodePtr ref_smc_address_items = NULL;
               xmlNodePtr ref_smc_title = NULL;
               xmlNodePtr ref_smc_issue_date = NULL;
               xmlNodePtr issue_info = NULL;
               xmlNodePtr language = NULL;
               char *s;

               if ((doc = read_xml_doc(fname))) {
                       ref_smc_address = first_xpath_node(doc, NULL, BAD_CAST "//scormContentPackageAddress");
                       ref_smc_ident = find_child(ref_smc_address, "scormContentPackageIdent");
                       ref_smc_address_items = find_child(ref_smc_address, "scormContentPackageAddressItems");
                       ref_smc_title = find_child(ref_smc_address_items, "scormContentPackageTitle");
                       ref_smc_issue_date = find_child(ref_smc_address_items, "issueDate");
               }

               s = strchr(ref, '_');

               if (optset(opts, OPT_ISSUE)) {
                       if (doc) {
                               issue_info = xmlCopyNode(find_child(ref_smc_ident, "issueInfo"), 1);
                       } else if (s && isdigit((unsigned char) s[1])) {
                               issue_info = new_issue_info(s);
                       } else {
                               if (verbosity > QUIET) {
                                       fprintf(stderr, WRN_PREFIX "Could not read issue info from SCORM content package: %s\n", ref);
                               }
                               issue_info = NULL;
                       }

                       xmlAddChild(smc_ref_ident, issue_info);
               }

               if (optset(opts, OPT_LANG)) {
                       if (doc) {
                               language = xmlCopyNode(find_child(ref_smc_ident, "language"), 1);
                       } else if (s && (s = strchr(s + 1, '_'))) {
                               language = new_language(s);
                       } else {
                               if (verbosity > QUIET) {
                                       fprintf(stderr, WRN_PREFIX "Could not read language from SCORM content package: %s\n", ref);
                               }
                               language = NULL;
                       }

                       xmlAddChild(smc_ref_ident, language);
               }

               if (optset(opts, OPT_TITLE) || optset(opts, OPT_DATE)) {
                       xmlNodePtr smc_ref_address_items = NULL, smc_title, issue_date;

                       if (doc) {
                               smc_ref_address_items = xmlNewChild(smc_ref, NULL, BAD_CAST "scormContentPackageRefAddressItems", NULL);
                               smc_title = ref_smc_title;
                               issue_date = ref_smc_issue_date;
                       } else {
                               smc_title = NULL;
                               issue_date = NULL;
                       }

                       if (optset(opts, OPT_TITLE)) {
                               if (smc_title) {
                                       xmlAddChild(smc_ref_address_items, xmlCopyNode(smc_title, 1));
                               } else {
                                       if (verbosity > QUIET) {
                                               fprintf(stderr, WRN_PREFIX "Could not read title from SCORM content package: %s\n", ref);
                                       }
                               }
                       }
                       if (optset(opts, OPT_DATE)) {
                               if (issue_date) {
                                       xmlAddChild(smc_ref_address_items, xmlCopyNode(issue_date, 1));
                               } else {
                                       if (verbosity > QUIET) {
                                               fprintf(stderr, WRN_PREFIX "Could not read date from SCORM content package: %s\n", ref);
                                       }
                               }
                       }
               }

               xmlFreeDoc(doc);

               if (optset(opts, OPT_SRCID)) {
                       xmlNodePtr smc, issno, lang, src;

                       smc   = xmlCopyNode(smc_code, 1);
                       issno = xmlCopyNode(issue_info, 1);
                       lang  = xmlCopyNode(language, 1);

                       src = xmlNewNode(NULL, BAD_CAST "sourceScormContentPackageIdent");
                       xmlAddChild(src, smc);
                       xmlAddChild(src, lang);
                       xmlAddChild(src, issno);

                       xmlFreeNode(smc_ref);
                       smc_ref = src;
               }

               if (optset(opts, OPT_URL)) {
                       set_xlink(smc_ref, fname);
               }
       }

       return smc_ref;
}

#define PME_FMT "PME-%255[^-]-%255[^-]-%14[^-]-%5s-%5s-%2s"
#define PMC_FMT "PMC-%14[^-]-%5s-%5s-%2s"

static xmlNodePtr new_pm_ref(const char *ref, const char *fname, int opts)
{
       char extension_producer[256] = "";
       char extension_code[256]     = "";
       char model_ident_code[15]    = "";
       char pm_issuer[6]            = "";
       char pm_number[6]            = "";
       char pm_volume[3]            = "";
       xmlNode *pm_ref;
       xmlNode *pm_ref_ident;
       xmlNode *pm_code;
       bool is_extended;
       int n;

       is_extended = strncmp(ref, "PME-", 4) == 0;

       if (is_extended) {
               n = sscanf(ref, PME_FMT,
                       extension_producer,
                       extension_code,
                       model_ident_code,
                       pm_issuer,
                       pm_number,
                       pm_volume);
               if (n != 6) {
                       if (verbosity > QUIET) {
                               fprintf(stderr, ERR_PREFIX "Publication module extended code invalid: %s\n", ref);
                       }
                       exit(EXIT_BAD_INPUT);
               }
       } else {
               n = sscanf(ref, PMC_FMT,
                       model_ident_code,
                       pm_issuer,
                       pm_number,
                       pm_volume);
               if (n != 4) {
                       if (verbosity > QUIET) {
                               fprintf(stderr, ERR_PREFIX "Publication module code invalid: %s\n", ref);
                       }
                       exit(EXIT_BAD_INPUT);
               }
       }

       pm_ref = xmlNewNode(NULL, BAD_CAST "pmRef");
       pm_ref_ident = xmlNewChild(pm_ref, NULL, BAD_CAST "pmRefIdent", NULL);

       if (is_extended) {
               xmlNode *ident_extension;
               ident_extension = xmlNewChild(pm_ref_ident, NULL, BAD_CAST "identExtension", NULL);
               xmlSetProp(ident_extension, BAD_CAST "extensionProducer", BAD_CAST extension_producer);
               xmlSetProp(ident_extension, BAD_CAST "extensionCode", BAD_CAST extension_code);
       }

       pm_code = xmlNewChild(pm_ref_ident, NULL, BAD_CAST "pmCode", NULL);

       xmlSetProp(pm_code, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
       xmlSetProp(pm_code, BAD_CAST "pmIssuer", BAD_CAST pm_issuer);
       xmlSetProp(pm_code, BAD_CAST "pmNumber", BAD_CAST pm_number);
       xmlSetProp(pm_code, BAD_CAST "pmVolume", BAD_CAST pm_volume);

       if (opts) {
               xmlDocPtr doc;
               xmlNodePtr ref_pm_address = NULL;
               xmlNodePtr ref_pm_ident = NULL;
               xmlNodePtr ref_pm_address_items = NULL;
               xmlNodePtr ref_pm_title = NULL;
               xmlNodePtr ref_pm_issue_date = NULL;
               xmlNodePtr issue_info = NULL;
               xmlNodePtr language = NULL;
               char *s;

               if ((doc = read_xml_doc(fname))) {
                       ref_pm_address = first_xpath_node(doc, NULL, BAD_CAST "//pmAddress|//pmaddres");

                       if (xmlStrcmp(ref_pm_address->name, BAD_CAST "pmaddres") == 0) {
                               ref_pm_ident = ref_pm_address;
                               ref_pm_address_items = ref_pm_address;
                               ref_pm_title = find_child(ref_pm_address_items, "pmtitle");
                               ref_pm_issue_date = find_child(ref_pm_address_items, "issdate");
                       } else {
                               ref_pm_ident = find_child(ref_pm_address, "pmIdent");
                               ref_pm_address_items = find_child(ref_pm_address, "pmAddressItems");
                               ref_pm_title = find_child(ref_pm_address_items, "pmTitle");
                               ref_pm_issue_date = find_child(ref_pm_address_items, "issueDate");
                       }
               }

               s = strchr(ref, '_');

               if (optset(opts, OPT_ISSUE)) {
                       if (doc) {
                               xmlNodePtr node;
                               xmlChar *issno, *inwork;

                               node   = first_xpath_node(doc, ref_pm_ident, BAD_CAST "issueInfo|issno");
                               issno  = first_xpath_value(doc, node, BAD_CAST "@issueNumber|@issno");
                               inwork = first_xpath_value(doc, node, BAD_CAST "@inWork|@inwork");
                               if (!inwork) inwork = xmlStrdup(BAD_CAST "00");

                               issue_info = xmlNewNode(NULL, BAD_CAST "issueInfo");
                               xmlSetProp(issue_info, BAD_CAST "issueNumber", issno);
                               xmlSetProp(issue_info, BAD_CAST "inWork", inwork);

                               xmlFree(issno);
                               xmlFree(inwork);
                       } else if (s && isdigit((unsigned char) s[1])) {
                               issue_info = new_issue_info(s);
                       } else {
                               if (verbosity > QUIET) {
                                       fprintf(stderr, WRN_PREFIX "Could not read issue info from publication module: %s\n", ref);
                               }
                               issue_info = NULL;
                       }

                       xmlAddChild(pm_ref_ident, issue_info);
               }

               if (optset(opts, OPT_LANG)) {
                       if (doc) {
                               xmlNodePtr node;
                               xmlChar *l, *c;

                               node = find_child(ref_pm_ident, "language");
                               l    = first_xpath_value(doc, node, BAD_CAST "@languageIsoCode|@language");
                               c    = first_xpath_value(doc, node, BAD_CAST "@countryIsoCode|@country");

                               language = xmlNewNode(NULL, BAD_CAST "language");
                               xmlSetProp(language, BAD_CAST "languageIsoCode", l);
                               xmlSetProp(language, BAD_CAST "countryIsoCode", c);

                               xmlFree(l);
                               xmlFree(c);
                       } else if (s && (s = strchr(s + 1, '_'))) {
                               language = new_language(s);
                       } else {
                               if (verbosity > QUIET) {
                                       fprintf(stderr, WRN_PREFIX "Could not read language from publication module: %s\n", ref);
                               }
                               language = NULL;
                       }

                       xmlAddChild(pm_ref_ident, language);
               }

               if (optset(opts, OPT_TITLE) || optset(opts, OPT_DATE)) {
                       xmlNodePtr pm_ref_address_items = NULL, pm_title, issue_date;

                       if (doc) {
                               pm_ref_address_items = xmlNewChild(pm_ref, NULL, BAD_CAST "pmRefAddressItems", NULL);
                               pm_title = ref_pm_title;
                               issue_date = ref_pm_issue_date;
                       } else {
                               pm_title = NULL;
                               issue_date = NULL;
                       }

                       if (optset(opts, OPT_TITLE)) {
                               if (pm_title) {
                                       pm_title = xmlAddChild(pm_ref_address_items, xmlCopyNode(pm_title, 1));
                                       xmlNodeSetName(pm_title, BAD_CAST "pmTitle");
                               } else {
                                       if (verbosity > QUIET) {
                                               fprintf(stderr, WRN_PREFIX "Could not read title from publication module: %s\n", ref);
                                       }
                               }
                       }
                       if (optset(opts, OPT_DATE)) {
                               if (issue_date) {
                                       issue_date = xmlAddChild(pm_ref_address_items, xmlCopyNode(issue_date, 1));
                                       xmlNodeSetName(issue_date, BAD_CAST "issueDate");
                               } else {
                                       if (verbosity > QUIET) {
                                               fprintf(stderr, WRN_PREFIX "Could not read date from publication module: %s\n", ref);
                                       }
                               }
                       }
               }

               xmlFreeDoc(doc);

               if (optset(opts, OPT_SRCID)) {
                       xmlNodePtr pmc, issno, lang, src;

                       pmc   = xmlCopyNode(pm_code, 1);
                       issno = xmlCopyNode(issue_info, 1);
                       lang  = xmlCopyNode(language, 1);

                       src = xmlNewNode(NULL, BAD_CAST "sourcePmIdent");
                       xmlAddChild(src, pmc);
                       xmlAddChild(src, lang);
                       xmlAddChild(src, issno);

                       xmlFreeNode(pm_ref);
                       pm_ref = src;
               }

               if (optset(opts, OPT_URL)) {
                       set_xlink(pm_ref, fname);
               }
       }

       return pm_ref;
}

#define DME_FMT "DME-%255[^-]-%255[^-]-%14[^-]-%4[^-]-%3[^-]-%1s%1s-%4[^-]-%2s%3[^-]-%3s%1s-%1s-%3s%1s"
#define DMC_FMT "DMC-%14[^-]-%4[^-]-%3[^-]-%1s%1s-%4[^-]-%2s%3[^-]-%3s%1s-%1s-%3s%1s"

static xmlNodePtr new_dm_ref(const char *ref, const char *fname, int opts)
{
       char extension_producer[256] = "";
       char extension_code[256]     = "";
       char model_ident_code[15]    = "";
       char system_diff_code[5]     = "";
       char system_code[4]          = "";
       char assy_code[5]            = "";
       char item_location_code[2]   = "";
       char learn_code[4]           = "";
       char learn_event_code[2]     = "";
       char sub_system_code[2]      = "";
       char sub_sub_system_code[2]  = "";
       char disassy_code[3]         = "";
       char disassy_code_variant[4] = "";
       char info_code[4]            = "";
       char info_code_variant[2]    = "";
       xmlNode *dm_ref;
       xmlNode *dm_ref_ident;
       xmlNode *dm_code;
       bool is_extended;
       bool has_learn;
       int n;

       is_extended = strncmp(ref, "DME-", 4) == 0;

       if (is_extended) {
               n = sscanf(ref, DME_FMT,
                       extension_producer,
                       extension_code,
                       model_ident_code,
                       system_diff_code,
                       system_code,
                       sub_system_code,
                       sub_sub_system_code,
                       assy_code,
                       disassy_code,
                       disassy_code_variant,
                       info_code,
                       info_code_variant,
                       item_location_code,
                       learn_code,
                       learn_event_code);
               if (n != 15 && n != 13) {
                       if (verbosity > QUIET) {
                               fprintf(stderr, ERR_PREFIX "Data module extended code invalid: %s\n", ref);
                       }
                       exit(EXIT_BAD_INPUT);
               }
               has_learn = n == 15;
       } else {
               n = sscanf(ref, DMC_FMT,
                       model_ident_code,
                       system_diff_code,
                       system_code,
                       sub_system_code,
                       sub_sub_system_code,
                       assy_code,
                       disassy_code,
                       disassy_code_variant,
                       info_code,
                       info_code_variant,
                       item_location_code,
                       learn_code,
                       learn_event_code);
               if (n != 13 && n != 11) {
                       if (verbosity > QUIET) {
                               fprintf(stderr, ERR_PREFIX "Data module code invalid: %s\n", ref);
                       }
                       exit(EXIT_BAD_INPUT);
               }
               has_learn = n == 13;
       }

       dm_ref = xmlNewNode(NULL, BAD_CAST "dmRef");
       dm_ref_ident = xmlNewChild(dm_ref, NULL, BAD_CAST "dmRefIdent", NULL);

       if (is_extended) {
               xmlNode *ident_extension;
               ident_extension = xmlNewChild(dm_ref_ident, NULL, BAD_CAST "identExtension", NULL);
               xmlSetProp(ident_extension, BAD_CAST "extensionProducer", BAD_CAST extension_producer);
               xmlSetProp(ident_extension, BAD_CAST "extensionCode", BAD_CAST extension_code);
       }

       dm_code = xmlNewChild(dm_ref_ident, NULL, BAD_CAST "dmCode", NULL);

       xmlSetProp(dm_code, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
       xmlSetProp(dm_code, BAD_CAST "systemDiffCode", BAD_CAST system_diff_code);
       xmlSetProp(dm_code, BAD_CAST "systemCode", BAD_CAST system_code);
       xmlSetProp(dm_code, BAD_CAST "subSystemCode", BAD_CAST sub_system_code);
       xmlSetProp(dm_code, BAD_CAST "subSubSystemCode", BAD_CAST sub_sub_system_code);
       xmlSetProp(dm_code, BAD_CAST "assyCode", BAD_CAST assy_code);
       xmlSetProp(dm_code, BAD_CAST "disassyCode", BAD_CAST disassy_code);
       xmlSetProp(dm_code, BAD_CAST "disassyCodeVariant", BAD_CAST disassy_code_variant);
       xmlSetProp(dm_code, BAD_CAST "infoCode", BAD_CAST info_code);
       xmlSetProp(dm_code, BAD_CAST "infoCodeVariant", BAD_CAST info_code_variant);
       xmlSetProp(dm_code, BAD_CAST "itemLocationCode", BAD_CAST item_location_code);
       if (has_learn) {
               xmlSetProp(dm_code, BAD_CAST "learnCode", BAD_CAST learn_code);
               xmlSetProp(dm_code, BAD_CAST "learnEventCode", BAD_CAST learn_event_code);
       }

       if (opts) {
               xmlDocPtr doc;
               xmlNodePtr ref_dm_address = NULL;
               xmlNodePtr ref_dm_ident = NULL;
               xmlNodePtr ref_dm_address_items = NULL;
               xmlNodePtr ref_dm_title = NULL;
               xmlNodePtr ref_dm_issue_date = NULL;
               xmlNodePtr issue_info = NULL;
               xmlNodePtr language = NULL;
               char *s;

               if ((doc = read_xml_doc(fname))) {
                       ref_dm_address = first_xpath_node(doc, NULL, BAD_CAST "//dmAddress|//dmaddres");

                       if (xmlStrcmp(ref_dm_address->name, BAD_CAST "dmaddres") == 0) {
                               ref_dm_ident = ref_dm_address;
                               ref_dm_address_items = ref_dm_address;
                               ref_dm_title = find_child(ref_dm_address_items, "dmtitle");
                               ref_dm_issue_date = find_child(ref_dm_address_items, "issdate");
                       } else {
                               ref_dm_ident = find_child(ref_dm_address, "dmIdent");
                               ref_dm_address_items = find_child(ref_dm_address, "dmAddressItems");
                               ref_dm_title = find_child(ref_dm_address_items, "dmTitle");
                               ref_dm_issue_date = find_child(ref_dm_address_items, "issueDate");
                       }
               }

               s = strchr(ref, '_');

               if (optset(opts, OPT_ISSUE)) {
                       if (doc) {
                               xmlNodePtr node;
                               xmlChar *issno, *inwork;

                               node   = first_xpath_node(doc, ref_dm_ident, BAD_CAST "issueInfo|issno");
                               issno  = first_xpath_value(doc, node, BAD_CAST "@issueNumber|@issno");
                               inwork = first_xpath_value(doc, node, BAD_CAST "@inWork|@inwork");
                               if (!inwork) inwork = xmlStrdup(BAD_CAST "00");

                               issue_info = xmlNewNode(NULL, BAD_CAST "issueInfo");
                               xmlSetProp(issue_info, BAD_CAST "issueNumber", issno);
                               xmlSetProp(issue_info, BAD_CAST "inWork", inwork);

                               xmlFree(issno);
                               xmlFree(inwork);
                       } else if (s && isdigit((unsigned char) s[1])) {
                               issue_info = new_issue_info(s);
                       } else {
                               if (verbosity > QUIET) {
                                       fprintf(stderr, WRN_PREFIX "Could not read issue info from data module: %s\n", ref);
                               }
                               issue_info = NULL;
                       }

                       xmlAddChild(dm_ref_ident, issue_info);
               }

               if (optset(opts, OPT_LANG)) {
                       if (doc) {
                               xmlNodePtr node;
                               xmlChar *l, *c;

                               node = find_child(ref_dm_ident, "language");
                               l    = first_xpath_value(doc, node, BAD_CAST "@languageIsoCode|@language");
                               c    = first_xpath_value(doc, node, BAD_CAST "@countryIsoCode|@country");

                               language = xmlNewNode(NULL, BAD_CAST "language");
                               xmlSetProp(language, BAD_CAST "languageIsoCode", l);
                               xmlSetProp(language, BAD_CAST "countryIsoCode", c);

                               xmlFree(l);
                               xmlFree(c);
                       } else if (s && (s = strchr(s + 1, '_'))) {
                               language = new_language(s);
                       } else {
                               if (verbosity > QUIET) {
                                       fprintf(stderr, WRN_PREFIX "Could not read language from data module: %s\n", ref);
                               }
                               language = NULL;
                       }

                       xmlAddChild(dm_ref_ident, language);
               }

               if (optset(opts, OPT_TITLE) || optset(opts, OPT_DATE)) {
                       xmlNodePtr dm_ref_address_items = NULL, dm_title = NULL, issue_date = NULL;

                       if (doc) {
                               dm_ref_address_items = xmlNewChild(dm_ref, NULL, BAD_CAST "dmRefAddressItems", NULL);

                               if (optset(opts, OPT_TITLE)) {
                                       xmlChar *tech, *info, *infv;

                                       tech = first_xpath_value(doc, ref_dm_title, BAD_CAST "techName|techname");
                                       info = first_xpath_value(doc, ref_dm_title, BAD_CAST "infoName|infoname");
                                       infv = first_xpath_value(doc, ref_dm_title, BAD_CAST "infoNameVariant");

                                       dm_title = xmlNewNode(NULL, BAD_CAST "dmTitle");
                                       xmlNewTextChild(dm_title, NULL, BAD_CAST "techName", tech);
                                       if (info) xmlNewTextChild(dm_title, NULL, BAD_CAST "infoName", info);
                                       if (infv) xmlNewTextChild(dm_title, NULL, BAD_CAST "infoNameVariant", infv);

                                       xmlFree(tech);
                                       xmlFree(info);
                                       xmlFree(infv);
                               }

                               if (optset(opts, OPT_DATE)) {
                                       issue_date = xmlCopyNode(ref_dm_issue_date, 1);
                                       xmlNodeSetName(issue_date, BAD_CAST "issueDate");
                               }

                       }

                       if (optset(opts, OPT_TITLE)) {
                               if (dm_title) {
                                       xmlAddChild(dm_ref_address_items, dm_title);
                               } else {
                                       if (verbosity > QUIET) {
                                               fprintf(stderr, WRN_PREFIX "Could not read title from data module: %s\n", ref);
                                       }
                               }
                       }
                       if (optset(opts, OPT_DATE)) {
                               if (issue_date) {
                                       xmlAddChild(dm_ref_address_items, issue_date);
                               } else {
                                       if (verbosity > QUIET) {
                                               fprintf(stderr, WRN_PREFIX "Could not read issue date from data module: %s\n", ref);
                                       }
                               }
                       }
               }

               xmlFreeDoc(doc);

               if (optset(opts, OPT_SRCID)) {
                       xmlNodePtr dmc, issno, lang, src;

                       dmc   = xmlCopyNode(dm_code, 1);
                       issno = xmlCopyNode(issue_info, 1);
                       lang  = xmlCopyNode(language, 1);

                       src = xmlNewNode(NULL, BAD_CAST (optset(opts, OPT_CIRID) ? "repositorySourceDmIdent" : "sourceDmIdent"));
                       xmlAddChild(src, dmc);
                       xmlAddChild(src, lang);
                       xmlAddChild(src, issno);

                       xmlFreeNode(dm_ref);
                       dm_ref = src;
               }

               if (optset(opts, OPT_URL)) {
                       set_xlink(dm_ref, fname);
               }
       }

       return dm_ref;
}

#define COM_FMT "COM-%14[^-]-%5s-%4s-%5s-%1s"

static xmlNodePtr new_com_ref(const char *ref, const char *fname, int opts)
{
       char model_ident_code[15]  = "";
       char sender_ident[6]       = "";
       char year_of_data_issue[5] = "";
       char seq_number[6]         = "";
       char comment_type[2]       = "";

       int n;

       xmlNodePtr comment_ref, comment_ref_ident, comment_code;

       n = sscanf(ref, COM_FMT,
               model_ident_code,
               sender_ident,
               year_of_data_issue,
               seq_number,
               comment_type);
       if (n != 5) {
               if (verbosity > QUIET) {
                       fprintf(stderr, ERR_PREFIX "Comment code invalid: %s\n", ref);
               }
               exit(EXIT_BAD_INPUT);
       }

       lowercase(comment_type);

       comment_ref = xmlNewNode(NULL, BAD_CAST "commentRef");
       comment_ref_ident = xmlNewChild(comment_ref, NULL, BAD_CAST "commentRefIdent", NULL);
       comment_code = xmlNewChild(comment_ref_ident, NULL, BAD_CAST "commentCode", NULL);

       xmlSetProp(comment_code, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
       xmlSetProp(comment_code, BAD_CAST "senderIdent", BAD_CAST sender_ident);
       xmlSetProp(comment_code, BAD_CAST "yearOfDataIssue", BAD_CAST year_of_data_issue);
       xmlSetProp(comment_code, BAD_CAST "seqNumber", BAD_CAST seq_number);
       xmlSetProp(comment_code, BAD_CAST "commentType", BAD_CAST comment_type);

       if (opts) {
               xmlDocPtr doc;
               xmlNodePtr ref_comment_address;
               xmlNodePtr ref_comment_ident;
               char *s;

               if ((doc = read_xml_doc(fname))) {
                       ref_comment_address = first_xpath_node(doc, NULL, BAD_CAST "//commentAddress");
                       ref_comment_ident = find_child(ref_comment_address, "commentIdent");
               }

               s = strchr(ref, '_');

               if (optset(opts, OPT_LANG)) {
                       xmlNodePtr language;

                       if (doc) {
                               language = xmlCopyNode(find_child(ref_comment_ident, "language"), 1);
                       } else if (s && (s = strchr(s + 1, '_'))) {
                               language = new_language(s);
                       } else {
                               if (verbosity > QUIET) {
                                       fprintf(stderr, WRN_PREFIX "Could not read language from comment: %s\n", ref);
                               }
                               language = NULL;
                       }

                       xmlAddChild(comment_ref_ident, language);
               }

               xmlFreeDoc(doc);

               if (optset(opts, OPT_URL)) {
                       set_xlink(comment_ref, fname);
               }
       }

       return comment_ref;
}

#define DML_FMT "DML-%14[^-]-%5s-%1s-%4s-%5s"

static xmlNodePtr new_dml_ref(const char *ref, const char *fname, int opts)
{
       char model_ident_code[15]  = "";
       char sender_ident[6]       = "";
       char dml_type[2]           = "";
       char year_of_data_issue[5] = "";
       char seq_number[6]         = "";

       int n;

       xmlNodePtr dml_ref, dml_ref_ident, dml_code;

       n = sscanf(ref, DML_FMT,
               model_ident_code,
               sender_ident,
               dml_type,
               year_of_data_issue,
               seq_number);
       if (n != 5) {
               if (verbosity > QUIET) {
                       fprintf(stderr, ERR_PREFIX "DML code invalid: %s\n", ref);
               }
               exit(EXIT_BAD_INPUT);
       }

       lowercase(dml_type);

       dml_ref = xmlNewNode(NULL, BAD_CAST "dmlRef");
       dml_ref_ident = xmlNewChild(dml_ref, NULL, BAD_CAST "dmlRefIdent", NULL);
       dml_code = xmlNewChild(dml_ref_ident, NULL, BAD_CAST "dmlCode", NULL);

       xmlSetProp(dml_code, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
       xmlSetProp(dml_code, BAD_CAST "senderIdent", BAD_CAST sender_ident);
       xmlSetProp(dml_code, BAD_CAST "dmlType", BAD_CAST dml_type);
       xmlSetProp(dml_code, BAD_CAST "yearOfDataIssue", BAD_CAST year_of_data_issue);
       xmlSetProp(dml_code, BAD_CAST "seqNumber", BAD_CAST seq_number);

       if (opts) {
               xmlDocPtr doc;
               xmlNodePtr ref_dml_address;
               xmlNodePtr ref_dml_ident;
               char *s;

               if ((doc = read_xml_doc(fname))) {
                       ref_dml_address = first_xpath_node(doc, NULL, BAD_CAST "//dmlAddress");
                       ref_dml_ident = find_child(ref_dml_address, "dmlIdent");
               }

               s = strchr(ref, '_');

               if (optset(opts, OPT_ISSUE)) {
                       xmlNodePtr issue_info;

                       if (doc) {
                               issue_info = xmlCopyNode(find_child(ref_dml_ident, "issueInfo"), 1);
                       } else if (s && isdigit((unsigned char) s[1])) {
                               issue_info = new_issue_info(s);
                       } else {
                               if (verbosity > QUIET) {
                                       fprintf(stderr, WRN_PREFIX "Could not read issue info from DML: %s\n", ref);
                               }
                               issue_info = NULL;
                       }

                       xmlAddChild(dml_ref_ident, issue_info);
               }

               xmlFreeDoc(doc);

               if (optset(opts, OPT_URL)) {
                       set_xlink(dml_ref, fname);
               }
       }

       return dml_ref;
}

static xmlNodePtr new_icn_ref(const char *ref, const char *fname, int opts)
{
       xmlNodePtr info_entity_ref;

       info_entity_ref = xmlNewNode(NULL, BAD_CAST "infoEntityRef");

       xmlSetProp(info_entity_ref, BAD_CAST "infoEntityRefIdent", BAD_CAST ref);

       return info_entity_ref;
}

#define CSN_FMT "CSN-%14[^-]-%4[^-]-%3[^-]-%1s%1s-%4[^-]-%2s%3[^-]-%3s%1s-%1s"

static xmlNodePtr new_csn_ref(const char *ref, const char *fname, int opts)
{
       char model_ident_code[15]    = "";
       char system_diff_code[5]     = "";
       char system_code[4]          = "";
       char assy_code[5]            = "";
       char item_location_code[2]   = "";
       char sub_system_code[2]      = "";
       char sub_sub_system_code[2]  = "";
       char figure_number[3]         = "";
       char figure_number_variant[4] = "";
       char item[4]            = "";
       char item_variant[2]    = "";
       xmlNode *csn_ref;
       int n;

       n = sscanf(ref, CSN_FMT,
               model_ident_code,
               system_diff_code,
               system_code,
               sub_system_code,
               sub_sub_system_code,
               assy_code,
               figure_number,
               figure_number_variant,
               item,
               item_variant,
               item_location_code);
       if (n != 11) {
               if (verbosity > QUIET) {
                       fprintf(stderr, ERR_PREFIX "CSN invalid: %s\n", ref);
               }
               exit(EXIT_BAD_INPUT);
       }

       csn_ref = xmlNewNode(NULL, BAD_CAST "catalogSeqNumberRef");

       xmlSetProp(csn_ref, BAD_CAST "modelIdentCode", BAD_CAST model_ident_code);
       xmlSetProp(csn_ref, BAD_CAST "systemDiffCode", BAD_CAST system_diff_code);
       xmlSetProp(csn_ref, BAD_CAST "systemCode", BAD_CAST system_code);
       xmlSetProp(csn_ref, BAD_CAST "subSystemCode", BAD_CAST sub_system_code);
       xmlSetProp(csn_ref, BAD_CAST "subSubSystemCode", BAD_CAST sub_sub_system_code);
       xmlSetProp(csn_ref, BAD_CAST "assyCode", BAD_CAST assy_code);
       xmlSetProp(csn_ref, BAD_CAST "figureNumber", BAD_CAST figure_number);
       if (strcmp(figure_number_variant, "*") != 0) {
               xmlSetProp(csn_ref, BAD_CAST "figureNumberVariant", BAD_CAST figure_number_variant);
       }
       xmlSetProp(csn_ref, BAD_CAST "item", BAD_CAST item);
       if (strcmp(item_variant, "*") != 0) {
               xmlSetProp(csn_ref, BAD_CAST "itemVariant", BAD_CAST item_variant);
       }
       xmlSetProp(csn_ref, BAD_CAST "itemLocationCode", BAD_CAST item_location_code);

       if (optset(opts, OPT_URL)) {
               set_xlink(csn_ref, fname);
       }

       return csn_ref;
}

static bool is_smc_ref(const char *ref)
{
       return strncmp(ref, "SMC-", 4) == 0 || strncmp(ref, "SME-", 4) == 0;
}

static bool is_pm_ref(const char *ref)
{
       return strncmp(ref, "PMC-", 4) == 0 || strncmp(ref, "PME-", 4) == 0;
}

static bool is_dm_ref(const char *ref)
{
       return strncmp(ref, "DMC-", 4) == 0 || strncmp(ref, "DME-", 4) == 0;
}

static bool is_com_ref(const char *ref)
{
       return strncmp(ref, "COM-", 4) == 0;
}

static bool is_dml_ref(const char *ref)
{
       return strncmp(ref, "DML-", 4) == 0;
}

static bool is_icn_ref(const char *ref)
{
       return strncmp(ref, "ICN-", 4) == 0;
}

static bool is_csn_ref(const char *ref)
{
       return strncmp(ref, "CSN-", 4) == 0;
}

static void add_ref(const char *src, const char *dst, xmlNodePtr ref, int opts)
{
       xmlDocPtr doc;
       xmlNodePtr refs;

       if (!(doc = read_xml_doc(src))) {
               if (verbosity > QUIET) {
                       fprintf(stderr, ERR_PREFIX "Could not read source data module: %s\n", src);
               }
               exit(EXIT_MISSING_FILE);
       }

       if (optset(opts, OPT_SRCID)) {
               xmlNodePtr src, node;

               src  = first_xpath_node(doc, NULL, BAD_CAST "//dmStatus/sourceDmIdent|//pmStatus/sourcePmIdent|//status/srcdmaddres");
               node = first_xpath_node(doc, NULL, BAD_CAST "(//dmStatus/repositorySourceDmIdent|//dmStatus/security|//pmStatus/security|//status/security)[1]");
               if (node) {
                       if (src) {
                               xmlUnlinkNode(src);
                               xmlFreeNode(src);
                       }
                       xmlAddPrevSibling(node, xmlCopyNode(ref, 1));
               }
       } else {
               refs = find_or_create_refs(doc);
               xmlAddChild(refs, xmlCopyNode(ref, 1));
       }

       save_xml_doc(doc, dst);

       xmlFreeDoc(doc);
}

/* Apply a built-in XSLT transform to a doc in place. */
static void transform_doc(xmlDocPtr doc, unsigned char *xsl, unsigned int len)
{
       xmlDocPtr styledoc, src, res;
       xsltStylesheetPtr style;
       xmlNodePtr old;

       src = xmlCopyDoc(doc, 1);

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

       res = xsltApplyStylesheet(style, src, NULL);

       old = xmlDocSetRootElement(doc, xmlCopyNode(xmlDocGetRootElement(res), 1));
       xmlFreeNode(old);

       xmlFreeDoc(src);
       xmlFreeDoc(res);
       xsltFreeStylesheet(style);
}

static xmlNodePtr find_ext_pub(xmlDocPtr extpubs, const char *ref)
{
       xmlChar xpath[512];
       xmlNodePtr node;

       /* Attempt to match an exact code (e.g., "ABC") */
       xmlStrPrintf(xpath, 512, "//externalPubRef[externalPubRefIdent/externalPubCode='%s']", ref);
       node = first_xpath_node(extpubs, NULL, xpath);

       /* Attempt to match a file name (e.g., "ABC.PDF") */
       if (!node) {
               xmlStrPrintf(xpath, 512, "//externalPubRef[starts-with('%s', externalPubRefIdent/externalPubCode)]", ref);
               node = first_xpath_node(extpubs, NULL, xpath);
       }

       return xmlCopyNode(node, 1);
}

static xmlNodePtr new_ext_pub(const char *ref, const char *fname, int opts)
{
       xmlNodePtr epr, epr_ident;

       epr = xmlNewNode(NULL, BAD_CAST "externalPubRef");
       epr_ident = xmlNewChild(epr, NULL, BAD_CAST "externalPubRefIdent", NULL);

       if (optset(opts, OPT_TITLE)) {
               xmlNewTextChild(epr_ident, NULL, BAD_CAST "externalPubTitle", BAD_CAST ref);
       } else {
               xmlNewTextChild(epr_ident, NULL, BAD_CAST "externalPubCode", BAD_CAST ref);
       }

       if (optset(opts, OPT_URL)) {
               set_xlink(epr, fname);
       }

       return epr;
}

static xmlNodePtr find_ref_type(const char *fname, int opts)
{
       xmlDocPtr doc, styledoc, res;
       xsltStylesheetPtr style;
       xmlNodePtr node = NULL;

       if (!(doc = read_xml_doc(fname))) {
               return NULL;
       }

       styledoc = read_xml_mem((const char *) ref_xsl, ref_xsl_len);
       style = xsltParseStylesheetDoc(styledoc);

       res = xsltApplyStylesheet(style, doc, NULL);

       if (res->children) {
               const char *ref;
               xmlNodePtr (*f)(const char *, const char *, int) = NULL;

               ref = (char *) res->children->content;

               if (is_dm_ref(ref)) {
                       f = new_dm_ref;
               } else if (is_pm_ref(ref)) {
                       f = new_pm_ref;
               } else if (is_smc_ref(ref)) {
                       f = new_smc_ref;
               } else if (is_com_ref(ref)) {
                       f = new_com_ref;
               } else if (is_dml_ref(ref)) {
                       f = new_dml_ref;
               } else if (is_icn_ref(ref)) {
                       f = new_icn_ref;
               }

               if (f) {
                       node = f(ref, fname, opts);
               }
       }

       xmlFreeDoc(res);
       xsltFreeStylesheet(style);

       xmlFreeDoc(doc);

       return node;
}

/* Determine whether a string is matched by a regular expression. */
static bool matches_regex(const char *s, const char *regex)
{
       regex_t re;
       bool match;
       regcomp(&re, regex, REG_EXTENDED);
       match = regexec(&re, s, 0, NULL, 0) == 0;
       regfree(&re);
       return match;
}

/* Attempt to automatically add the prefix to a ref. */
static char *add_prefix(const char *ref)
{
       int n = strlen(ref) + 5;
       char *s = malloc(n);

       /* Notes:
        *   Check against extended variants (DME, PME, SME) before
        *   non-extended variants (DMC, PMC, SMC).
        *
        *   There is no need to check for CSN, SME or SMC, as these are
        *   indistinguishable from DMC, PME and PMC without a prefix or an
        *   XML context.
        */
       if (matches_regex(ref, DME_REGEX_NOPRE)) {
               snprintf(s, n, "DME-%s", ref);
       } else if (matches_regex(ref, DMC_REGEX_NOPRE)) {
               snprintf(s, n, "DMC-%s", ref);
       } else if (matches_regex(ref, PME_REGEX_NOPRE)) {
               snprintf(s, n, "PME-%s", ref);
       } else if (matches_regex(ref, PMC_REGEX_NOPRE)) {
               snprintf(s, n, "PMC-%s", ref);
       } else if (matches_regex(ref, COM_REGEX_NOPRE)) {
               snprintf(s, n, "COM-%s", ref);
       } else if (matches_regex(ref, DML_REGEX_NOPRE)) {
               snprintf(s, n, "DML-%s", ref);
       } else {
               snprintf(s, n, "%s", ref);
       }

       return s;
}

static void print_ref(const char *src, const char *dst, const char *ref,
       const char *fname, int opts, bool overwrite, enum issue iss,
       xmlDocPtr extpubs)
{
       xmlNodePtr node;
       xmlNodePtr (*f)(const char *, const char *, int);
       char *fullref;

       /* If -p is given, try automatically adding the prefix. */
       if (optset(opts, OPT_NONSTRICT)) {
               fullref = add_prefix(ref);
       /* Otherwise, just copy the ref as-is. */
       } else {
               fullref = strdup(ref);
       }

       if (is_dm_ref(fullref)) {
               f = new_dm_ref;
       } else if (is_pm_ref(fullref)) {
               f = new_pm_ref;
       } else if (is_smc_ref(fullref)) {
               f = new_smc_ref;
       } else if (is_com_ref(fullref)) {
               f = new_com_ref;
       } else if (is_dml_ref(fullref)) {
               f = new_dml_ref;
       } else if (is_icn_ref(fullref)) {
               f = new_icn_ref;
       } else if (is_csn_ref(fullref)) {
               f = new_csn_ref;
       } else if (extpubs && (node = find_ext_pub(extpubs, fullref))) {
               f = NULL;
       } else if ((node = find_ref_type(fname, opts))) {
               f = NULL;
       } else {
               f = new_ext_pub;
       }

       if (f) {
               node = f(fullref, fname, opts);
       }

       if (iss < DEFAULT_S1000D_ISSUE) {
               unsigned char *xsl;
               unsigned int len;
               xmlDocPtr doc;

               switch (iss) {
                       case ISS_20:
                               xsl = ___common_to20_xsl;
                               len = ___common_to20_xsl_len;
                               break;
                       case ISS_21:
                               xsl = ___common_to21_xsl;
                               len = ___common_to21_xsl_len;
                               break;
                       case ISS_22:
                               xsl = ___common_to22_xsl;
                               len = ___common_to22_xsl_len;
                               break;
                       case ISS_23:
                               xsl = ___common_to23_xsl;
                               len = ___common_to23_xsl_len;
                               break;
                       case ISS_30:
                               xsl = ___common_to30_xsl;
                               len = ___common_to30_xsl_len;
                               break;
                       case ISS_40:
                               xsl = ___common_to40_xsl;
                               len = ___common_to40_xsl_len;
                               break;
                       case ISS_41:
                               xsl = ___common_to41_xsl;
                               len = ___common_to41_xsl_len;
                               break;
                       case ISS_42:
                               xsl = ___common_to42_xsl;
                               len = ___common_to42_xsl_len;
                               break;
                       default:
                               xsl = NULL;
                               len = 0;
                               break;
               }

               doc = xmlNewDoc(BAD_CAST "1.0");
               xmlDocSetRootElement(doc, node);

               transform_doc(doc, xsl, len);

               node = xmlCopyNode(xmlDocGetRootElement(doc), 1);

               xmlFreeDoc(doc);
       }

       if (optset(opts, OPT_INS)) {
               if (verbosity >= VERBOSE) {
                       fprintf(stderr, INF_PREFIX "Adding reference %s to %s...\n", fullref, src);
               }

               if (overwrite) {
                       add_ref(src, src, node, opts);
               } else {
                       add_ref(src, dst, node, opts);
               }
       } else {
               dump_node(node, dst);
       }

       xmlFreeNode(node);
       free(fullref);
}

static char *trim(char *str)
{
       char *end;

       while (isspace((unsigned char) *str)) str++;

       if (*str == 0) return str;

       end = str + strlen(str) - 1;
       while (end > str && isspace((unsigned char) *end)) end--;

       *(end + 1) = 0;

       return str;
}

static enum issue spec_issue(const char *s)
{
       if (strcmp(s, "2.0") == 0) {
               return ISS_20;
       } else if (strcmp(s, "2.1") == 0) {
               return ISS_21;
       } else if (strcmp(s, "2.2") == 0) {
               return ISS_22;
       } else if (strcmp(s, "2.3") == 0) {
               return ISS_23;
       } else if (strcmp(s, "3.0") == 0) {
               return ISS_30;
       } else if (strcmp(s, "4.0") == 0) {
               return ISS_40;
       } else if (strcmp(s, "4.1") == 0) {
               return ISS_41;
       } else if (strcmp(s, "4.2") == 0) {
               return ISS_42;
       } else if (strcmp(s, "5.0") == 0) {
               return ISS_50;
       }

       if (verbosity > QUIET) {
               fprintf(stderr, ERR_PREFIX "Unsupported issue: %s\n", s);
       }
       exit(EXIT_BAD_ISSUE);
}

/* Skip a reference if it has a conflicting prefix. */
static bool skip_confl_ref(xmlNodePtr *node, xmlChar **content, regoff_t so, regoff_t eo, const char *pre)
{
       xmlChar *p = (*content) + so - 4;

       if (p > (*content) && (xmlStrncmp(p, BAD_CAST pre, 4) == 0)) {
               xmlChar *s1, *s2;

               s1 = xmlStrndup((*content), eo);
               s2 = xmlStrdup((*content) + eo);

               xmlFree(*content);
               xmlNodeSetContent(*node, s1);
               xmlFree(s1);

               *node = xmlAddNextSibling(*node, xmlNewText(s2));
               *content = s2;

               return true;
       }

       return false;
}

/* Replace a textual reference with XML. */
static void transform_ref(xmlNodePtr *node, const char *path, xmlChar **content, regoff_t so, regoff_t eo, const char *prefix, newref_t f, int opts)
{
       xmlChar *r, *s1, *s2;
       xmlNodePtr ref;

       if (verbosity >= DEBUG) {
               const char *type;

               if (f == new_dm_ref) {
                       type = "DM";
               } else if (f == new_pm_ref) {
                       type = "PM";
               } else if (f == new_csn_ref) {
                       type = "CSN";
               } else if (f == new_icn_ref) {
                       type = "ICN";
               } else if (f == new_dml_ref) {
                       type = "DML";
               } else if (f == new_smc_ref) {
                       type = "SMC";
               } else if (f == new_ext_pub) {
                       type = "external pub";
               } else {
                       type = "unknown";
               }

               fprintf(stderr, INF_PREFIX "%s: Found %s ref %.*s\n", path, type, (int) (eo - so), (*content) + so);
       }

       /* If prefixes are not required, some types of references have
        * overlapping formats. This will look backwards to determine if a
        * reference has a conflicting prefix.
        *
        * FIXME: Does not account for extended variants (DME, PME, SME). */
       if (optset(opts, OPT_NONSTRICT)) {
               if (f ==  new_dm_ref && skip_confl_ref(node, content, so, eo, "CSN-")) return;
               if (f == new_csn_ref && skip_confl_ref(node, content, so, eo, "DMC-")) return;
               if (f ==  new_pm_ref && skip_confl_ref(node, content, so, eo, "SMC-")) return;
               if (f == new_smc_ref && skip_confl_ref(node, content, so, eo, "PMC-")) return;
       }

       if (prefix && xmlStrncmp((*content) + so, BAD_CAST prefix, 4) != 0) {
               r = xmlStrdup(BAD_CAST prefix);
       } else {
               r = xmlStrdup(BAD_CAST "");
       }
       r = xmlStrncat(r, (*content) + so, eo - so);

       s1 = xmlStrndup((*content), so);
       s2 = xmlStrdup((*content) + eo);

       xmlFree(*content);

       xmlNodeSetContent(*node, s1);
       xmlFree(s1);

       ref = xmlAddNextSibling(*node, f((char *) r, NULL, opts));

       xmlFree(r);

       *node = xmlAddNextSibling(ref, xmlNewText(s2));

       *content = s2;
}

/* Transform all textual references in a particular node. */
static void transform_refs_in_node(xmlNodePtr node, const char *path, const char *regex, const char *prefix, newref_t f, const int opts)
{
       xmlChar *content;
       regex_t re;
       regmatch_t pmatch[1];

       content = xmlNodeGetContent(node);

       regcomp(&re, regex, REG_EXTENDED);

       while (regexec(&re, (char *) content, 1, pmatch, 0) == 0) {
               transform_ref(&node, path, &content, pmatch[0].rm_so, pmatch[0].rm_eo, prefix, f, opts);
       }

       regfree(&re);
       xmlFree(content);
}

/* Transform all textual references in text nodes in an XML document. */
static void transform_refs_in_doc(const xmlDocPtr doc, const char *path, const xmlChar *xpath, const char *regex, const char *prefix, newref_t f, const int opts)
{
       xmlXPathContextPtr ctx;
       xmlXPathObjectPtr obj;

       ctx = xmlXPathNewContext(doc);

       /* If the -c option is given, only transform refs in the content section. */
       if (optset(opts, OPT_CONTENT)) {
               xmlXPathSetContextNode(first_xpath_node(doc, NULL, BAD_CAST "//content"), ctx);
       } else {
               xmlXPathSetContextNode(xmlDocGetRootElement(doc), ctx);
       }

       /* Use the user-specified XPath. */
       if (xpath) {
               if (!(obj = xmlXPathEvalExpression(xpath, ctx))) {
                       if (verbosity > QUIET) {
                               fprintf(stderr, ERR_PREFIX "Invalid XPath expression: %s\n", (char *) xpath);
                       }
                       exit(EXIT_BAD_XPATH);
               }
       /* Use the appropriate built-in XPath based on ref type. */
       } else {
               unsigned char *els;
               unsigned int len;
               xmlChar *s;
               int n;

               if (f == new_dm_ref) {
                       els = elems_dmc_txt;
                       len = elems_dmc_txt_len;
               } else if (f == new_pm_ref) {
                       els = elems_pmc_txt;
                       len = elems_pmc_txt_len;
               } else if (f == new_csn_ref) {
                       els = elems_csn_txt;
                       len = elems_csn_txt_len;
               } else if (f == new_dml_ref) {
                       els = elems_dml_txt;
                       len = elems_dml_txt_len;
               } else if (f == new_icn_ref) {
                       els = elems_icn_txt;
                       len = elems_icn_txt_len;
               } else if (f == new_smc_ref) {
                       els = elems_smc_txt;
                       len = elems_smc_txt_len;
               } else if (f == new_ext_pub) {
                       els = elems_epr_txt;
                       len = elems_epr_txt_len;
               } else {
                       els = BAD_CAST "descendant-or-self::*/text()";
                       len = 11;
               }

               n = len + 1;

               s = malloc(n * sizeof(xmlChar));
               xmlStrPrintf(s, n, "%.*s", len, els);

               if (!(obj = xmlXPathEvalExpression(s, ctx))) {
                       if (verbosity > QUIET) {
                               fprintf(stderr, ERR_PREFIX "Invalid XPath expression: %s\n", (char *) s);
                       }
                       exit(EXIT_BAD_XPATH);
               }

               xmlFree(s);
       }

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

               for (i = 0; i < obj->nodesetval->nodeNr; ++i) {
                       transform_refs_in_node(obj->nodesetval->nodeTab[i], path, regex, prefix, f, opts);
               }
       }

       xmlXPathFreeObject(obj);
       xmlXPathFreeContext(ctx);
}

/* Build a regex pattern to match a string. */
static char *regex_esc(const char *s)
{
       int i, j;
       char *esc;

       /* At most, the resulting pattern will be twice the length of the original string. */
       esc = malloc(strlen(s) * 2 + 1);

       for (i = 0, j = 0; s[i]; ++i, ++j) {
               switch (s[i]) {
                       /* These special characters must be escaped. */
                       case '.':
                       case '[':
                       case '{':
                       case '}':
                       case '(':
                       case ')':
                       case '\\':
                       case '*':
                       case '+':
                       case '?':
                       case '|':
                       case '^':
                       case '$':
                               esc[j++] = '\\';
                       /* All other characters match themselves. */
                       default:
                               esc[j] = s[i];
                               break;
               }
       }

       /* Ensure the pattern is null-terminated. */
       esc[j] = '\0';

       return esc;
}

/* Transform all external pub refs in an XML document. */
static void transform_extpub_refs_in_doc(const xmlDocPtr doc, const char *path, const xmlChar *xpath, const xmlDocPtr extpubs, int opts)
{
       xmlXPathContextPtr ctx;
       xmlXPathObjectPtr obj;

       ctx = xmlXPathNewContext(extpubs);
       obj = xmlXPathEvalExpression(BAD_CAST "//externalPubCode", ctx);

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

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

                       code = (char *) xmlNodeGetContent(obj->nodesetval->nodeTab[i]);
                       code_esc = regex_esc(code);
                       xmlFree(code);

                       transform_refs_in_doc(doc, path, xpath, (char *) code_esc, NULL, new_ext_pub, opts);

                       free(code_esc);
               }
       }

       xmlXPathFreeObject(obj);
       xmlXPathFreeContext(ctx);
}

/* Transform all textual references in a file. */
static void transform_refs_in_file(const char *path, const char *transform, const xmlChar *xpath, const xmlDocPtr extpubs, bool overwrite, const int opts)
{
       xmlDocPtr doc;
       int i;
       bool nonstrict = optset(opts, OPT_NONSTRICT);

       if (!(doc = read_xml_doc(path))) {
               if (verbosity > QUIET) {
                       fprintf(stderr, ERR_PREFIX "Could not read object: %s\n", path);
               }
               exit(EXIT_MISSING_FILE);
       }

       if (verbosity >= VERBOSE) {
               fprintf(stderr, INF_PREFIX "Transforming textual references in %s...\n", path);
       }

       for (i = 0; transform[i]; ++i) {
               switch (transform[i]) {
                       case 'C':
                               transform_refs_in_doc(doc, path, xpath,
                                       nonstrict ? COM_REGEX : COM_REGEX_STRICT,
                                       "COM-", new_com_ref, opts);
                               break;
                       case 'D':
                               transform_refs_in_doc(doc, path, xpath,
                                       nonstrict ? DME_REGEX : DME_REGEX_STRICT,
                                       "DME-", new_dm_ref, opts);
                               transform_refs_in_doc(doc, path, xpath,
                                       nonstrict ? DMC_REGEX : DMC_REGEX_STRICT,
                                       "DMC-", new_dm_ref, opts);
                               break;
                       case 'E':
                               if (extpubs) {
                                       transform_extpub_refs_in_doc(doc, path, xpath, extpubs, opts);
                               }
                               break;
                       case 'G':
                               transform_refs_in_doc(doc, path, xpath, ICN_REGEX, NULL, new_icn_ref, opts);
                               break;
                       case 'L':
                               transform_refs_in_doc(doc, path, xpath,
                                       nonstrict ? DML_REGEX : DML_REGEX_STRICT,
                                       "DML-", new_dml_ref, opts);
                               break;
                       case 'P':
                               transform_refs_in_doc(doc, path, xpath,
                                       nonstrict ? PME_REGEX : PME_REGEX_STRICT,
                                       "PME-", new_pm_ref, opts);
                               transform_refs_in_doc(doc, path, xpath,
                                       nonstrict ? PMC_REGEX : PMC_REGEX_STRICT,
                                       "PMC-", new_pm_ref, opts);
                               break;
                       case 'S':
                               transform_refs_in_doc(doc, path, xpath,
                                       nonstrict ? SME_REGEX : SME_REGEX_STRICT,
                                       "SME-", new_smc_ref, opts);
                               transform_refs_in_doc(doc, path, xpath,
                                       nonstrict ? SMC_REGEX : SMC_REGEX_STRICT,
                                       "SMC-", new_smc_ref, opts);
                               break;
                       case 'Y':
                               transform_refs_in_doc(doc, path, xpath,
                                       nonstrict ? CSN_REGEX : CSN_REGEX_STRICT,
                                       "CSN-", new_csn_ref, opts);
                               break;
                       default:
                               if (verbosity > QUIET) {
                                       fprintf(stderr, WRN_PREFIX "Unknown reference type: %c\n", transform[i]);
                               }
                               break;
               }
       }

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

       xmlFreeDoc(doc);
}

/* Transform all textual references in all files in a list. */
static void transform_refs_in_list(const char *path, const char *transform, const xmlChar *xpath, const xmlDocPtr extpubs, bool overwrite, const int opts)
{
       FILE *f;
       char line[PATH_MAX];

       if (path) {
               if (!(f = fopen(path, "r"))) {
                       if (verbosity > QUIET) {
                               fprintf(stderr, ERR_PREFIX "Could not read list: %s\n", path);
                       }
                       return;
               }
       } else {
               f = stdin;
       }

       while (fgets(line, PATH_MAX, f)) {
               strtok(line, "\t\r\n");
               transform_refs_in_file(line, transform, xpath, extpubs, overwrite, opts);
       }

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

/* Show usage message. */
static void show_help(void)
{
       puts("Usage: " PROG_NAME " [-cdfgiLlqRrStuvh?] [-$ <issue>] [-s <src>] [-T <opts>] [-o <dst>] [-x <xpath>] [-3 <file>] [<code>|<file> ...]");
       puts("");
       puts("Options:");
       puts("  -$, --issue <issue>        Output XML for the specified issue of S1000D.");
       puts("  -c, --content              Only transform textual references in the content section.");
       puts("  -d, --include-date         Include issue date (target must be file).");
       puts("  -f, --overwrite            Overwrite source data module instead of writing to stdout.");
       puts("  -h, -?, --help             Show this help message.");
       puts("  -i, --include-issue        Include issue info.");
       puts("  -L, --list                 Treat input as a list of CSDB objects.");
       puts("  -l, --include-lang         Include language.");
       puts("  -o, --out <dst>            Output to <dst> instead of stdout.");
       puts("  -g, --guess-prefix         Accept references without a prefix.");
       puts("  -q, --quiet                Quiet mode. Do not print errors.");
       puts("  -R, --repository-id        Generate a <repositorySourceDmIdent>.");
       puts("  -r, --add                  Add reference to data module's <refs> table.");
       puts("  -S, --source-id            Generate a <sourceDmIdent> or <sourcePmIdent>.");
       puts("  -s, --source <src>         Source data module to add references to.");
       puts("  -T, --transform <opts>     Transform textual references to XML in objects.");
       puts("  -t, --include-title        Include title (target must be file)");
       puts("  -u, --include-url          Include xlink:href to the full URL/filename.");
       puts("  -v, --verbose              Verbose output.");
       puts("  -x, --xpath <xpath>        Transform textual references using <xpath>.");
       puts("  -3, --externalpubs <file>  Use a custom .externalpubs file.");
       puts("  --version                  Show version information.");
       puts("  <code>                     The code of the reference (must include prefix DMC/PMC/etc.).");
       puts("  <file>                     A file to reference, or transform references in (-T).");
       LIBXML2_PARSE_LONGOPT_HELP
}

/* Show version information. */
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)
{
       char scratch[PATH_MAX];
       int i;
       int opts = 0;
       char src[PATH_MAX] = "-";
       char dst[PATH_MAX] = "-";
       bool overwrite = false;
       enum issue iss = DEFAULT_S1000D_ISSUE;
       char extpubs_fname[PATH_MAX] = "";
       xmlDocPtr extpubs = NULL;
       char *transform = NULL;
       xmlChar *transform_xpath = NULL;
       bool is_list = false;

       const char *sopts = "3:cfgiLlo:qRrSs:T:tvd$:ux:h?";
       struct option lopts[] = {
               {"version"      , no_argument      , 0, 0},
               {"help"         , no_argument      , 0, 'h'},
               {"externalpubs" , required_argument, 0, '3'},
               {"content"      , no_argument      , 0, 'c'},
               {"overwrite"    , no_argument      , 0, 'f'},
               {"guess-prefix" , no_argument      , 0, 'g'},
               {"include-issue", no_argument      , 0, 'i'},
               {"include-lang" , no_argument      , 0, 'l'},
               {"out"          , required_argument, 0, 'o'},
               {"quiet"        , no_argument      , 0, 'q'},
               {"add"          , no_argument      , 0, 'r'},
               {"repository-id", no_argument      , 0, 'R'},
               {"source-id"    , no_argument      , 0, 'S'},
               {"source"       , required_argument, 0, 's'},
               {"transform"    , required_argument, 0, 'T'},
               {"include-title", no_argument      , 0, 't'},
               {"verbose"      , no_argument      , 0, 'v'},
               {"include-date" , no_argument      , 0, 'd'},
               {"issue"        , required_argument, 0, '$'},
               {"include-url"  , no_argument      , 0, 'u'},
               {"xpath"        , required_argument, 0, 'x'},
               LIBXML2_PARSE_LONGOPT_DEFS
               {0, 0, 0, 0}
       };
       int loptind = 0;

       while ((i = getopt_long(argc, argv, sopts, lopts, &loptind)) != -1) {
               switch (i) {
                       case 0:
                               if (strcmp(lopts[loptind].name, "version") == 0) {
                                       show_version();
                                       goto cleanup;
                               }
                               LIBXML2_PARSE_LONGOPT_HANDLE(lopts, loptind, optarg)
                               break;
                       case '3':
                               strncpy(extpubs_fname, optarg, PATH_MAX - 1);
                               break;
                       case 'c':
                               opts |= OPT_CONTENT;
                               break;
                       case 'f':
                               overwrite = true;
                               break;
                       case 'g':
                               opts |= OPT_NONSTRICT;
                               break;
                       case 'i':
                               opts |= OPT_ISSUE;
                               break;
                       case 'L':
                               is_list = true;
                               break;
                       case 'l':
                               opts |= OPT_LANG;
                               break;
                       case 'o':
                               strcpy(dst, optarg);
                               break;
                       case 'q':
                               --verbosity;
                               break;
                       case 'r':
                               opts |= OPT_INS;
                               break;
                       case 'R':
                               opts |= OPT_CIRID;
                       case 'S':
                               opts |= OPT_SRCID;
                               opts |= OPT_ISSUE;
                               opts |= OPT_LANG;
                               break;
                       case 's':
                               strcpy(src, optarg);
                               break;
                       case 'T':
                                 free(transform);
                                 if (strcmp(optarg, "all") == 0) {
                                         transform = strdup("CDEGLPSY");
                                 } else {
                                         transform = strdup(optarg);
                                 }
                                 break;
                       case 't':
                                 opts |= OPT_TITLE;
                                 break;
                       case 'v':
                                 ++verbosity;
                                 break;
                       case 'd':
                                 opts |= OPT_DATE;
                                 break;
                       case '$':
                                 iss = spec_issue(optarg);
                                 break;
                       case 'u':
                                 opts |= OPT_URL;
                                 break;
                       case 'x':
                                 free(transform_xpath);
                                 transform_xpath = xmlCharStrdup(optarg);
                                 break;
                       case '?':
                       case 'h': show_help(); goto cleanup;
               }
       }

       /* Load .externalpubs config file. */
       if (strcmp(extpubs_fname, "") != 0 || find_config(extpubs_fname, DEFAULT_EXTPUBS_FNAME)) {
               extpubs = read_xml_doc(extpubs_fname);
       }

       if (optind < argc) {
               for (i = optind; i < argc; ++i) {
                       if (transform) {
                               if (is_list) {
                                       transform_refs_in_list(argv[i], transform, transform_xpath, extpubs, overwrite, opts);
                               } else {
                                       transform_refs_in_file(argv[i], transform, transform_xpath, extpubs, overwrite, opts);
                               }
                       } else {
                               char *base;

                               if (strncmp(argv[i], "URN:S1000D:", 11) == 0) {
                                       base = argv[i] + 11;
                               } else {
                                       strcpy(scratch, argv[i]);
                                       base = basename(scratch);
                               }

                               print_ref(src, dst, base, argv[i], opts, overwrite, iss, extpubs);
                       }
               }
       } else if (transform) {
               if (is_list) {
                       transform_refs_in_list(NULL, transform, transform_xpath, extpubs, overwrite, opts);
               } else {
                       transform_refs_in_file("-", transform, transform_xpath, extpubs, overwrite, opts);
               }
       } else {
               while (fgets(scratch, PATH_MAX, stdin)) {
                       print_ref(src, dst, trim(scratch), NULL, opts, overwrite, iss, extpubs);
               }
       }

cleanup:
       xmlFreeDoc(extpubs);
       free(transform);
       xmlFree(transform_xpath);
       xsltCleanupGlobals();
       xmlCleanupParser();

       return 0;
}