/*
 *
 *  Copyright (C) 2000-2016, OFFIS e.V.
 *  All rights reserved.  See COPYRIGHT file for details.
 *
 *  This software and supporting documentation were developed by
 *
 *    OFFIS e.V.
 *    R&D Division Health
 *    Escherweg 2
 *    D-26121 Oldenburg, Germany
 *
 *
 *  Module: dcmsr
 *
 *  Author: Joerg Riesmeier
 *
 *  Purpose:
 *    render the contents of a DICOM structured reporting file in HTML format
 *
 */


#include "dcmtk/config/osconfig.h"    /* make sure OS specific configuration is included first */

#include "dcmtk/dcmsr/dsrdoc.h"       /* for main interface class DSRDocument */

#include "dcmtk/dcmdata/dctk.h"       /* for typical set of "dcmdata" headers */

#include "dcmtk/ofstd/ofstream.h"
#include "dcmtk/ofstd/ofconapp.h"

#ifdef WITH_ZLIB
#include <zlib.h>                     /* for zlibVersion() */
#endif
#ifdef DCMTK_ENABLE_CHARSET_CONVERSION
#include "dcmtk/ofstd/ofchrenc.h"     /* for OFCharacterEncoding */
#endif

#define OFFIS_CONSOLE_APPLICATION "dsr2html"

static OFLogger dsr2htmlLogger = OFLog::getLogger("dcmtk.apps." OFFIS_CONSOLE_APPLICATION);

static char rcsid[] = "$dcmtk: " OFFIS_CONSOLE_APPLICATION " v"
  OFFIS_DCMTK_VERSION " " OFFIS_DCMTK_RELEASEDATE " $";


// ********************************************


static OFCondition renderFile(STD_NAMESPACE ostream &out,
                              const char *ifname,
                              const char *cssName,
                              const char *defaultCharset,
                              const E_FileReadMode readMode,
                              const E_TransferSyntax xfer,
                              const size_t readFlags,
                              const size_t renderFlags,
                              const OFBool checkAllStrings,
                              const OFBool convertToUTF8)
{
    OFCondition result = EC_Normal;

    if ((ifname == NULL) || (strlen(ifname) == 0))
    {
        OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": invalid filename: <empty string>");
        return EC_IllegalParameter;
    }

    DcmFileFormat *dfile = new DcmFileFormat();
    if (dfile != NULL)
    {
        if (readMode == ERM_dataset)
            result = dfile->getDataset()->loadFile(ifname, xfer);
        else
            result = dfile->loadFile(ifname, xfer);
        if (result.bad())
        {
            OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": error (" << result.text()
                << ") reading file: " << ifname);
        }
    } else
        result = EC_MemoryExhausted;

#ifdef DCMTK_ENABLE_CHARSET_CONVERSION
    /* convert all DICOM strings to UTF-8 (if requested) */
    if (result.good() && convertToUTF8)
    {
        DcmDataset *dset = dfile->getDataset();
        OFLOG_INFO(dsr2htmlLogger, "converting all element values that are affected by SpecificCharacterSet (0008,0005) to UTF-8");
        // check whether SpecificCharacterSet is absent but needed
        if ((defaultCharset != NULL) && !dset->tagExistsWithValue(DCM_SpecificCharacterSet) &&
            dset->containsExtendedCharacters(OFFalse /*checkAllStrings*/))
        {
            // use the manually specified source character set
            result = dset->convertCharacterSet(defaultCharset, OFString("ISO_IR 192"));
        } else {
            // expect that SpecificCharacterSet contains the correct value
            result = dset->convertToUTF8();
        }
        if (result.bad())
        {
            OFLOG_FATAL(dsr2htmlLogger, result.text() << ": converting file to UTF-8: " << ifname);
        }
    }
#else
    // avoid compiler warning on unused variable
    (void)convertToUTF8;
#endif
    if (result.good())
    {
        result = EC_CorruptedData;
        DcmDataset *dset = dfile->getDataset();
        DSRDocument *dsrdoc = new DSRDocument();
        if (dsrdoc != NULL)
        {
            result = dsrdoc->read(*dset, readFlags);
            if (result.good())
            {
                // check extended character set
                OFString charset;
                if ((dsrdoc->getSpecificCharacterSet(charset).bad() || charset.empty()) &&
                    dset->containsExtendedCharacters(checkAllStrings))
                {
                    // we have an unspecified extended character set
                    if (defaultCharset == NULL)
                    {
                        // the dataset contains non-ASCII characters that really should not be there
                        OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": SpecificCharacterSet (0008,0005) "
                            << "element absent but extended characters used in file: " << ifname);
                        OFLOG_DEBUG(dsr2htmlLogger, "use option --charset-assume to manually specify an appropriate character set");
                        result = EC_IllegalCall;
                    } else {
                        // use the default character set specified by the user
                        result = dsrdoc->setSpecificCharacterSet(defaultCharset);
                        if (dsrdoc->getSpecificCharacterSetType() == DSRTypes::CS_unknown)
                        {
                            OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": Character set '"
                                << defaultCharset << "' specified with option --charset-assume not supported");
                            result = EC_IllegalCall;
                        }
                        else if (result.bad())
                        {
                            OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": Cannot use character set '"
                                << defaultCharset << "' specified with option --charset-assume: " << result.text());
                        }
                    }
                }
                if (result.good())
                    result = dsrdoc->renderHTML(out, renderFlags, cssName);
            } else {
                OFLOG_FATAL(dsr2htmlLogger, OFFIS_CONSOLE_APPLICATION << ": error (" << result.text()
                    << ") parsing file: " << ifname);
            }
        }
        delete dsrdoc;
    }
    delete dfile;

    return result;
}


#define SHORTCOL 3
#define LONGCOL 22


int main(int argc, char *argv[])
{
    size_t opt_readFlags = 0;
    size_t opt_renderFlags = DSRTypes::HF_renderDcmtkFootnote;
    const char *opt_cssName = NULL;
    const char *opt_defaultCharset = NULL;
    E_FileReadMode opt_readMode = ERM_autoDetect;
    E_TransferSyntax opt_ixfer = EXS_Unknown;
    OFBool opt_checkAllStrings = OFFalse;
    OFBool opt_convertToUTF8 = OFFalse;

    OFConsoleApplication app(OFFIS_CONSOLE_APPLICATION, "Render DICOM SR file and data set to HTML/XHTML", rcsid);
    OFCommandLine cmd;
    cmd.setOptionColumns(LONGCOL, SHORTCOL);
    cmd.setParamColumn(LONGCOL + SHORTCOL + 4);

    cmd.addParam("dsrfile-in",   "DICOM SR input filename to be rendered", OFCmdParam::PM_Mandatory);
    cmd.addParam("htmlfile-out", "HTML/XHTML output filename (default: stdout)", OFCmdParam::PM_Optional);

    cmd.addGroup("general options:", LONGCOL, SHORTCOL + 2);
      cmd.addOption("--help",                   "-h",     "print this help text and exit", OFCommandLine::AF_Exclusive);
      cmd.addOption("--version",                          "print version information and exit", OFCommandLine::AF_Exclusive);
      OFLog::addOptions(cmd);

    cmd.addGroup("input options:");
      cmd.addSubGroup("input file format:");
        cmd.addOption("--read-file",            "+f",     "read file format or data set (default)");
        cmd.addOption("--read-file-only",       "+fo",    "read file format only");
        cmd.addOption("--read-dataset",         "-f",     "read data set without file meta information");
      cmd.addSubGroup("input transfer syntax:");
        cmd.addOption("--read-xfer-auto",       "-t=",    "use TS recognition (default)");
        cmd.addOption("--read-xfer-detect",     "-td",    "ignore TS specified in the file meta header");
        cmd.addOption("--read-xfer-little",     "-te",    "read with explicit VR little endian TS");
        cmd.addOption("--read-xfer-big",        "-tb",    "read with explicit VR big endian TS");
        cmd.addOption("--read-xfer-implicit",   "-ti",    "read with implicit VR little endian TS");

    cmd.addGroup("processing options:");
      cmd.addSubGroup("additional information:");
        cmd.addOption("--processing-details",   "-Ip",    "show currently processed content item");
      cmd.addSubGroup("error handling:");
        cmd.addOption("--unknown-relationship", "-Er",    "accept unknown/missing relationship type");
        cmd.addOption("--invalid-item-value",   "-Ev",    "accept invalid content item value\n(e.g. violation of VR or VM definition)");
        cmd.addOption("--ignore-constraints",   "-Ec",    "ignore relationship content constraints");
        cmd.addOption("--ignore-item-errors",   "-Ee",    "do not abort on content item errors, just warn\n(e.g. missing value type specific attributes)");
        cmd.addOption("--skip-invalid-items",   "-Ei",    "skip invalid content items (incl. sub-tree)");
        cmd.addOption("--disable-vr-checker",   "-Dv",    "disable check for VR-conformant string values");
      cmd.addSubGroup("character set:");
        cmd.addOption("--charset-require",      "+Cr",    "require declaration of ext. charset (default)");
        cmd.addOption("--charset-assume",       "+Ca", 1, "[c]harset: string",
                                                          "assume charset c if no extended charset declared");
        cmd.addOption("--charset-check-all",              "check all data elements with string values\n(default: only PN, LO, LT, SH, ST, UC and UT)");
#ifdef DCMTK_ENABLE_CHARSET_CONVERSION
        cmd.addOption("--convert-to-utf8",      "+U8",    "convert all element values that are affected\nby Specific Character Set (0008,0005) to UTF-8");
#endif
    cmd.addGroup("output options:");
      cmd.addSubGroup("HTML/XHTML compatibility:");
        cmd.addOption("--html-3.2",             "+H3",    "use only HTML version 3.2 compatible features");
        cmd.addOption("--html-4.0",             "+H4",    "allow all HTML version 4.01 features (default)");
        cmd.addOption("--xhtml-1.1",            "+X1",    "comply with XHTML version 1.1 specification");
        cmd.addOption("--add-document-type",    "+Hd",    "add reference to SGML document type definition");
      cmd.addSubGroup("cascading style sheet (CSS), not with HTML 3.2:");
        cmd.addOption("--css-reference",        "+Sr", 1, "URL: string",
                                                          "add reference to specified CSS to document");
        cmd.addOption("--css-file",             "+Sf", 1, "[f]ilename: string",
                                                          "embed content of specified CSS into document");
      cmd.addSubGroup("general rendering:");
        cmd.addOption("--expand-inline",        "+Ri",    "expand short content items inline (default)");
        cmd.addOption("--never-expand-inline",  "-Ri",    "never expand content items inline");
        cmd.addOption("--always-expand-inline", "+Ra",    "always expand content items inline");
        cmd.addOption("--render-full-data",     "+Rd",    "render full data of content items");
        cmd.addOption("--section-title-inline", "+Rt",    "render section titles inline, not separately");
      cmd.addSubGroup("document rendering:");
        cmd.addOption("--document-type-title",  "+Dt",    "use document type as document title (default)");
        cmd.addOption("--patient-info-title",   "+Dp",    "use patient information as document title");
        cmd.addOption("--no-document-header",   "-Dh",    "do not render general document information");
      cmd.addSubGroup("code rendering:");
        cmd.addOption("--render-inline-codes",  "+Ci",    "render codes in continuous text blocks");
        cmd.addOption("--concept-name-codes",   "+Cn",    "render code of concept names");
        cmd.addOption("--numeric-unit-codes",   "+Cu",    "render code of numeric measurement units");
        cmd.addOption("--code-value-unit",      "+Cv",    "use code value as measurement unit (default)");
        cmd.addOption("--code-meaning-unit",    "+Cm",    "use code meaning as measurement unit");
        cmd.addOption("--render-all-codes",     "+Cc",    "render all codes (implies +Ci, +Cn and +Cu)");
        cmd.addOption("--code-details-tooltip", "+Ct",    "render code details as a tooltip (implies +Cc)");

    /* evaluate command line */
    prepareCmdLineArgs(argc, argv, OFFIS_CONSOLE_APPLICATION);
    if (app.parseCommandLine(cmd, argc, argv))
    {
        /* check exclusive options first */
        if (cmd.hasExclusiveOption())
        {
            if (cmd.findOption("--version"))
            {
                app.printHeader(OFTrue /*print host identifier*/);
                COUT << OFendl << "External libraries used:";
#if !defined(WITH_ZLIB) && !defined(DCMTK_ENABLE_CHARSET_CONVERSION)
                COUT << " none" << OFendl;
#else
                COUT << OFendl;
#endif
#ifdef WITH_ZLIB
                COUT << "- ZLIB, Version " << zlibVersion() << OFendl;
#endif
#ifdef DCMTK_ENABLE_CHARSET_CONVERSION
                COUT << "- " << OFCharacterEncoding::getLibraryVersionString() << OFendl;
#endif
                return 0;
            }
        }

        /* general options */

        OFLog::configureFromCommandLine(cmd, app);

        /* input options */

        cmd.beginOptionBlock();
        if (cmd.findOption("--read-file")) opt_readMode = ERM_autoDetect;
        if (cmd.findOption("--read-file-only")) opt_readMode = ERM_fileOnly;
        if (cmd.findOption("--read-dataset")) opt_readMode = ERM_dataset;
        cmd.endOptionBlock();

        cmd.beginOptionBlock();
        if (cmd.findOption("--read-xfer-auto"))
            opt_ixfer = EXS_Unknown;
        if (cmd.findOption("--read-xfer-detect"))
            dcmAutoDetectDatasetXfer.set(OFTrue);
        if (cmd.findOption("--read-xfer-little"))
        {
            app.checkDependence("--read-xfer-little", "--read-dataset", opt_readMode == ERM_dataset);
            opt_ixfer = EXS_LittleEndianExplicit;
        }
        if (cmd.findOption("--read-xfer-big"))
        {
            app.checkDependence("--read-xfer-big", "--read-dataset", opt_readMode == ERM_dataset);
            opt_ixfer = EXS_BigEndianExplicit;
        }
        if (cmd.findOption("--read-xfer-implicit"))
        {
            app.checkDependence("--read-xfer-implicit", "--read-dataset", opt_readMode == ERM_dataset);
            opt_ixfer = EXS_LittleEndianImplicit;
        }
        cmd.endOptionBlock();

        /* processing options */

        if (cmd.findOption("--processing-details"))
        {
            app.checkDependence("--processing-details", "verbose mode", dsr2htmlLogger.isEnabledFor(OFLogger::INFO_LOG_LEVEL));
            opt_readFlags |= DSRTypes::RF_showCurrentlyProcessedItem;
        }
        if (cmd.findOption("--unknown-relationship"))
            opt_readFlags |= DSRTypes::RF_acceptUnknownRelationshipType;
        if (cmd.findOption("--invalid-item-value"))
            opt_readFlags |= DSRTypes::RF_acceptInvalidContentItemValue;
        if (cmd.findOption("--ignore-constraints"))
            opt_readFlags |= DSRTypes::RF_ignoreRelationshipConstraints;
        if (cmd.findOption("--ignore-item-errors"))
            opt_readFlags |= DSRTypes::RF_ignoreContentItemErrors;
        if (cmd.findOption("--skip-invalid-items"))
            opt_readFlags |= DSRTypes::RF_skipInvalidContentItems;
        if (cmd.findOption("--disable-vr-checker"))
            dcmEnableVRCheckerForStringValues.set(OFFalse);

        /* character set options */
        cmd.beginOptionBlock();
        if (cmd.findOption("--charset-require"))
           opt_defaultCharset = NULL;
        if (cmd.findOption("--charset-assume"))
          app.checkValue(cmd.getValue(opt_defaultCharset));
        cmd.endOptionBlock();
        if (cmd.findOption("--charset-check-all"))
            opt_checkAllStrings = OFTrue;
#ifdef DCMTK_ENABLE_CHARSET_CONVERSION
        if (cmd.findOption("--convert-to-utf8"))
            opt_convertToUTF8 = OFTrue;
#endif

        /* output options */

        /* HTML compatibility */
        cmd.beginOptionBlock();
        if (cmd.findOption("--html-3.2"))
            opt_renderFlags = (opt_renderFlags & ~DSRTypes::HF_XHTML11Compatibility) | DSRTypes::HF_HTML32Compatibility;
        if (cmd.findOption("--html-4.0"))
            opt_renderFlags = (opt_renderFlags & ~(DSRTypes::HF_XHTML11Compatibility | DSRTypes::HF_HTML32Compatibility));
        if (cmd.findOption("--xhtml-1.1"))
            opt_renderFlags = (opt_renderFlags & ~DSRTypes::HF_HTML32Compatibility) | DSRTypes::HF_XHTML11Compatibility | DSRTypes::HF_addDocumentTypeReference;
        cmd.endOptionBlock();

        if (cmd.findOption("--add-document-type"))
            opt_renderFlags |= DSRTypes::HF_addDocumentTypeReference;

        /* cascading style sheet */
        cmd.beginOptionBlock();
        if (cmd.findOption("--css-reference"))
        {
            app.checkConflict("--css-reference", "--html-3.2", (opt_renderFlags & DSRTypes::HF_HTML32Compatibility) > 0);
            opt_renderFlags &= ~DSRTypes::HF_copyStyleSheetContent;
            app.checkValue(cmd.getValue(opt_cssName));
        }
        if (cmd.findOption("--css-file"))
        {
            app.checkConflict("--css-file", "--html-3.2", (opt_renderFlags & DSRTypes::HF_HTML32Compatibility) > 0);
            opt_renderFlags |= DSRTypes::HF_copyStyleSheetContent;
            app.checkValue(cmd.getValue(opt_cssName));
        }
        cmd.endOptionBlock();

        /* general rendering */
        cmd.beginOptionBlock();
        if (cmd.findOption("--expand-inline"))
        {
            /* default */
        }
        if (cmd.findOption("--never-expand-inline"))
            opt_renderFlags |= DSRTypes::HF_neverExpandChildrenInline;
        if (cmd.findOption("--always-expand-inline"))
            opt_renderFlags |= DSRTypes::HF_alwaysExpandChildrenInline;
        cmd.endOptionBlock();

        if (cmd.findOption("--render-full-data"))
            opt_renderFlags |= DSRTypes::HF_renderFullData;

        if (cmd.findOption("--section-title-inline"))
            opt_renderFlags |= DSRTypes::HF_renderSectionTitlesInline;

        /* document rendering */
        cmd.beginOptionBlock();
        if (cmd.findOption("--document-type-title"))
        {
            /* default */
        }
        if (cmd.findOption("--patient-info-title"))
            opt_renderFlags |= DSRTypes::HF_renderPatientTitle;
        cmd.endOptionBlock();

        if (cmd.findOption("--no-document-header"))
            opt_renderFlags |= DSRTypes::HF_renderNoDocumentHeader;

        /* code rendering */
        if (cmd.findOption("--render-inline-codes"))
            opt_renderFlags |= DSRTypes::HF_renderInlineCodes;
        if (cmd.findOption("--concept-name-codes"))
            opt_renderFlags |= DSRTypes::HF_renderConceptNameCodes;
        if (cmd.findOption("--numeric-unit-codes"))
            opt_renderFlags |= DSRTypes::HF_renderNumericUnitCodes;
        if (cmd.findOption("--code-value-unit"))
            opt_renderFlags &= ~DSRTypes::HF_useCodeMeaningAsUnit;
        if (cmd.findOption("--code-meaning-unit"))
            opt_renderFlags |= DSRTypes::HF_useCodeMeaningAsUnit;
        if (cmd.findOption("--render-all-codes"))
            opt_renderFlags |= DSRTypes::HF_renderAllCodes;
        if (cmd.findOption("--code-details-tooltip"))
        {
            app.checkConflict("--code-details-tooltip", "--html-3.2", (opt_renderFlags & DSRTypes::HF_HTML32Compatibility) > 0);
            opt_renderFlags |= DSRTypes::HF_useCodeDetailsTooltip;
        }
    }

    /* print resource identifier */
    OFLOG_DEBUG(dsr2htmlLogger, rcsid << OFendl);

    /* make sure data dictionary is loaded */
    if (!dcmDataDict.isDictionaryLoaded())
    {
        OFLOG_WARN(dsr2htmlLogger, "no data dictionary loaded, check environment variable: "
            << DCM_DICT_ENVIRONMENT_VARIABLE);
    }

    // map "old" charset names to DICOM defined terms
    if (opt_defaultCharset != NULL)
    {
        OFString charset(opt_defaultCharset);
        if (charset == "latin-1")
            opt_defaultCharset = "ISO_IR 100";
        else if (charset == "latin-2")
            opt_defaultCharset = "ISO_IR 101";
        else if (charset == "latin-3")
            opt_defaultCharset = "ISO_IR 109";
        else if (charset == "latin-4")
            opt_defaultCharset = "ISO_IR 110";
        else if (charset == "latin-5")
            opt_defaultCharset = "ISO_IR 148";
        else if (charset == "cyrillic")
            opt_defaultCharset = "ISO_IR 144";
        else if (charset == "arabic")
            opt_defaultCharset = "ISO_IR 127";
        else if (charset == "greek")
            opt_defaultCharset = "ISO_IR 126";
        else if (charset == "hebrew")
            opt_defaultCharset = "ISO_IR 138";
    }

    int result = 0;
    const char *ifname = NULL;
    /* first parameter is treated as the input filename */
    cmd.getParam(1, ifname);
    if (cmd.getParamCount() == 2)
    {
        /* second parameter specifies the output filename */
        const char *ofname = NULL;
        cmd.getParam(2, ofname);
        STD_NAMESPACE ofstream stream(ofname);
        if (stream.good())
        {
            if (renderFile(stream, ifname, opt_cssName, opt_defaultCharset, opt_readMode, opt_ixfer, opt_readFlags,
                opt_renderFlags, opt_checkAllStrings, opt_convertToUTF8).bad())
            {
                result = 2;
            }
        } else
            result = 1;
    } else {
        /* use standard output */
        if (renderFile(COUT, ifname, opt_cssName, opt_defaultCharset, opt_readMode, opt_ixfer, opt_readFlags,
            opt_renderFlags, opt_checkAllStrings, opt_convertToUTF8).bad())
        {
            result = 3;
        }
    }

    return result;
}
