/*
    This file is part of Msc-generator.
    Copyright (C) 2008-2024 Zoltan Turanyi
    Distributed under GNU Affero General Public License.

    Msc-generator is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Msc-generator is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with Msc-generator.  If not, see <http://www.gnu.org/licenses/>.
*/

/** @file commandline.cpp All that is needed to perform a command-line action.
 * @ingroup libcgencommon_files */

#define _CRT_SECURE_NO_DEPRECATE //For visual studio to allow sscanf
#define _CRT_NONSTDC_NO_DEPRECATE

#ifdef _WIN32
#include <io.h>      //for _setmode
#endif
#include <fcntl.h>   //for _O_BINARY
#include <iostream>
#include <assert.h>
#include <limits>
#include <cstdint>
#include <algorithm>
#include "cairo.h"
#include "utf8utils.h"
#include "commandline.h"
#include "chartbase.h"
#include "ppt.h"


/** Returns "vX.Y.Z" based on a, b and c.*/
std::string VersionText(char a, char b, char c)
{
    char buff[20];
    if (c<0) c=0;
    if (b<0) b=0;
    if (a<0) a=0;
    if (c!=0)
        sprintf(buff, "v%u.%u.%u", unsigned(a), unsigned(b), unsigned(c));
    else
        sprintf(buff, "v%u.%u", unsigned(a), unsigned(b));
    return buff;
}


/** Reads a text file from `in`.
 * If the file is non empty, we enforce trailing newline (as per POSIX)
 * (This is to handle trailing contexts for strings ending in one dot
 * just before EOF.)
 * @param [in] in The file to read from.
 * @param [in] till_zero If true, we stop reading at a zero, else we read to EOF.
 * @param [in] add_newline If true, we add a newline at the end, if there is none.*/
std::string ReadFile(FILE *in, bool till_zero, bool add_newline)
{
    if (!in) return std::string();
    std::string buff;
    size_t alloc = 16384;
    size_t len = 0;
    buff.resize(alloc);
    bool zero = false;
    while (!feof(in) && !zero) {
        if (till_zero)
            while (!feof(in) && !zero && len<alloc-1) {
                int v = fgetc(in);
                if (v == EOF || v == 0)
                    zero = true;
                else
                    buff[len++] = v;
            }
        else
            len += fread(&buff[len], 1, alloc-1-len, in);
        if (len == alloc-1)
            buff.resize(alloc+=16384);
    }
    buff.resize(len);
    if (len && add_newline && buff.back()!='\n')
        buff.push_back('\n');
    return buff;
}


//split load data per language.
//Note: load data version 0 was simply the load data for signalling charts.
//format version 1 and above starts with a 0x1 byte and then come a version number (1 for now)
//and then a 0x1 again and then a 0x1 separated list of load data in the format <name>:<load_data>
LoadDataSet SplitLoadData(std::string *load_data, MscError *Error, const FileLineCol *opt_pos)
{
    LoadDataSet ldset;
    if (load_data && load_data->size()) {
        std::string_view ld = *load_data;
        if (load_data->front()!='\x1') {
            //load data format version 0
            ldset["signalling"] = *load_data;
        } else {
            size_t pos1 = 1;
            size_t pos = ld.find_first_of(1, pos1);
            int version = to_int(ld.substr(pos1, pos-pos1));
            if (version<=1) {
                for (; pos<ld.length(); /*nope*/) {
                    pos++;
                    pos1 = ld.find_first_of(1, pos);
                    size_t colon = ld.find_first_of(':', pos);
                    if (colon > ld.length()) {
                        if (Error && opt_pos)
                            Error->Warning(*opt_pos, "Error in load data file. Clearing past load data and will regenerate.");
                        break;
                    }
                    ldset[string(ld.substr(pos, colon-pos))] = ld.substr(colon+1, pos1-colon-1);
                    pos = pos1;
                }
            } else if (Error && opt_pos)
                Error->Warning(*opt_pos, "Load data file version ("+std::to_string(version)
                                +")higher than what I understand (0 or 1). Clearing past load data and will regenerate.",
                                "Maybe it was created with a later version of msc-gen.");
        }
    }
    return ldset;
}

std::string PackLoadData(const LoadDataSet &ldset)
{
    std::string ret = "\x1" "1";
    for (auto &i : ldset) {
        ret.push_back('\x1');
        ret.append(i.first);
        ret.push_back(':');
        ret.append(i.second);
    }
    return ret;
}

std::pair<int, std::string>
Examples::Load(const std::vector<std::string> &example_dirs,
               OpenNamedFileFunction* file_open_proc) {
    if (example_dirs.empty()) return {0,{}};
    const Csh* csh = nullptr;
    for (std::string_view lname : L->GetLanguages())
        if (const LanguageData* l = L->GetLanguage(lname))
            if (l->pCsh && l->pCsh->file_list_proc) {
                csh = l->pCsh.get();
                break;
            }
    if (!csh) return {0,{}};
    std::vector<std::string> preamble;
    preamble.reserve(2);
    for (const std::string& example_dir : example_dirs) {
        if (example_dir.empty()) continue;
        std::vector<std::string> example_files =
            csh->file_list_proc(example_dir, {});
        std::ranges::sort(example_files);
        for (const std::string& e: example_files) {
            if (e.empty()) continue;
            const size_t dot_pos = e.find_last_of('.');
            if (dot_pos==std::string::npos) continue;
            const auto ext = std::string_view(e).substr(dot_pos+1);
            const LanguageData* lang = L->GetLanguageByExtension(ext);
            if (!lang) continue;
            FILE* fin = file_open_proc(e.c_str(), /*write=*/false, /*binary=*/false);
            if (!fin) continue;
            const std::string text = ReadFile(fin);
            fclose(fin);
            //File shall start with some double comment lines, like:
            // ## User friendly example names
            // ## keyword1 keyword2
            // ## [optional Explanatory text]
            // ## [optionally more such lines, we ignore now]
            std::string_view t = text;
            preamble.clear();
            while (t.size()) {
                if (!t.starts_with("##")) break;
                size_t line_end = t.find('\n');
                if (line_end==std::string::npos) line_end = t.size();
                std::string& s = preamble.emplace_back(t.substr(2, line_end-2));
                while (true) //replace ##s inside the string to newlines.
                    if (size_t pos = s.find("##"); pos!=s.npos) s.replace(pos, 2, "\n");
                    else break;
                t.remove_prefix(std::min(t.size(), line_end+1));
            }
            if (preamble.size()<2) continue;
            if (t.empty()) continue;
            examples[lang].emplace_back(std::move(preamble[0]),
                                        std::move(preamble[1]),
                                        preamble.size()>=3 ? std::move(preamble[2]) : "",
                                        std::string(t));
        }
        if (examples.size())
            //Only the first directory is read from.
            return { std::accumulate(examples.begin(), examples.end(), 0, [](int sum, auto& v) {return sum+v.second.size(); }),
                     example_dir};
    }
    return {0,{}};;
}


static std::vector<std::string_view> Split(std::string_view s) {
    std::vector<std::string_view> ss;
    size_t p = 0;
    while (p<s.size()) {
        const size_t p2 = s.find(' ', p);
        if (p2!=std::string_view::npos) {
            if (p<p2-1) ss.emplace_back(s.substr(p, p2-p));
            p = p2+1;
        } else {
            if (p<s.size()) ss.emplace_back(s.substr(p));
            break;
        }
    }
    return ss;
}

int Example::match(const std::vector<std::string_view>& search) const noexcept {
    //every prefixed search keyword count as one, every full match counts as 2
    int ret = 1;
    const auto kw = Split(keywords);
    for (std::string_view s : search) {
        const int match = [s, &kw] {
            int max = 0;
            for (std::string_view k : kw)
                if (CaseInsensitiveEqual(s, k)) return 3;
                else if (CaseInsensitiveBeginsWith(k, s)) max = 2;
                else if (CaseInsensitiveContains(k, s)) max = std::max(max, 1);
            return max;
        }();
        ret *= match;
        if (!ret) break;
    }
    return ret;
}

/** Find what examples match the search string.*/
std::vector<Example*> Examples::Filter(std::string_view search, const LanguageData* lang) {
    std::vector<Example*> ret;
    if (!lang) return ret;
    const auto i = examples.find(lang);
    if (i==examples.end()) return ret;
    ret.reserve(examples.size());
    if (search.empty()) { //for empty search we list all examples
        for (Example& e : i->second)
            ret.push_back(&e);
        return ret;
    }
    auto ss = Split(search);
    std::map<int, std::vector<Example*>> sorted;
    for (Example& e : i->second)
        if (const int m = e.match(ss))
            sorted[-m].push_back(&e); //negate so that better match are smaller number
    for (auto [_, vexp] : sorted)
        std::ranges::copy(vexp, std::back_inserter(ret));
    return ret;
}

/** For a command-line argument returns the languages for which it is a valid language-specific CLI option .*/
std::vector<std::string_view> IsValidChartSpecificCommandLineArg(const LanguageCollection& languages, const std::string &arg) {
    std::vector<std::string_view> ret;
    for (std::string_view l : languages.GetLanguages())
        if (auto pChart = languages.GetLanguage(l)->create_chart(nullptr, nullptr))
            if (pChart->AddCommandLineArg(arg))
                ret.push_back(l);
    return ret;
}

/** For a command-line option returns the languages for which it is a valid language-specific CLI option .*/
std::pair<Attribute, std::vector<std::string_view>> IsValidChartSpecificCommandLineOption(const LanguageCollection& languages, const std::string& arg) {
    std::vector<std::string_view> ret;
    std::string aname, avalue;
    const size_t eq = arg.find("=");
    Attribute a = eq == arg.npos ? Attribute(arg, "yes", {}, {}) : Attribute(arg.substr(0, eq), arg.substr(eq + 1));
    for (std::string_view l : languages.GetLanguages())
        if (auto pChart = languages.GetLanguage(l)->create_chart(nullptr, nullptr)) {
            pChart->AddCommandLineOption(a);
            if (!pChart->Error.hasErrors())
                ret.push_back(l);
        }
    return {std::move(a), std::move(ret)};
}

/* Indicate to the user that we are reading from stdin.
 * Print any errors/warnings before */
void IndicateStdin(bool oProgress, bool oPaths, bool oWarning, bool oTechnicalInfo, MscError& Error) {
    if (!oProgress) return;
    if (!oPaths)
        Error.RemovePathFromErrorMessages();
    std::cerr << Error.Print(oWarning, oTechnicalInfo);
    std::cerr << "Taking input file from stdin." << std::endl;
    Error.Clear();
}


/** Return program version as a string. */
std::string version(const LanguageCollection &languages)
{
    char buff[2000];
    snprintf(buff, sizeof(buff),
        "Msc-generator %s (using %s)\n"
        "Copyright (C) 2008-2024 Zoltan Turanyi\n"
        "Msc-generator comes with ABSOLUTELY NO WARRANTY.\n"
        "This is free software, and you are welcome to redistribute it under certain\n"
        "conditions; type `msc-gen -l' for details.\n",
        VersionText(LIBMSCGEN_MAJOR, LIBMSCGEN_MINOR, LIBMSCGEN_SUPERMINOR).c_str(),
        languages.GetLibraryNames().c_str());
    return buff;
}
/** Print program usage and return. */
static void usage()
{
    printf(
"Msc-generator draws signalling charts from textual descriptions.\n"
"\n"
"Usage: msc-gen [OPTIONS] [infile]\n"
"\n"
"Program information:\n"
" -h, --help  Display this help and exit.\n"
" -l          Display program license and exit.\n"
" --version   Display version information and exit.\n"
"\n"
"Operation mode selection:\n"
" --gui       Using this mode pops up a GUI for interactive editing of a chart.\n"
"             Some options apply in GUI mode, such as -S to start an empty chart\n"
"             of a given type; 'infile' to open a file; --nodesigns, -D to\n"
"             control design libraries; -F and -L to control the default font.\n"
" --tei-mode  Using this option puts msc-generator into text editor integration\n"
"             mode. In this mode msc-gen can produce color syntax highlighting,\n"
"             quick error, text indent and hinting/autocomplete information.\n"
"             Input file(s) are read from standard input, output is written to\n"
"             standard output, thus no input or output filename can be specified\n"
"             and options -i and -o are invalid. Same with option -T option, but\n"
"             -S is mandatory. Option --tei-mode implies -Pno, as well. See\n"
"             the documentation for details.\n"
" --list-embedded\n"
"             Lists the charts embedded in a PPT file.\n"
"\n"
"Input control:\n"
" <infile>    The file from which to read input. If specified as '-', input will\n"
"             be read from stdin (and '-S' must be used). The filename extension\n"
"             determines what type of chart the input file contains. You can\n"
"             also specify a PNG image, in which case Msc-generator searches for\n"
"             the chart text embedded in an iTXT chunk.\n"
"             You can also specify a PPT file optionally followed by a slide\n"
"             number, like: 'my.pptx:2'. In this case the chart text is looked\n"
"             for in the PPT file on the given slide (2). You can omit the slide\n"
"             if the file contains only one embedded chart. If the slide contains\n"
"             multiple embedded charts, you can append a number (in increasing\n"
"             Z-order of the chart) to select one. (You can embed charts with\n"
"             Msc-generator using, e.g., '-T embed:1' or update embedded charts\n"
"             by using them both as input and output:\n"
"             'msc-gen my.pptx:2:1 -T embed:2:1 -o my.pptx'\n"
" -i <infile> To retain compatibility with mscgen, this is an alternate way to\n"
"             specify the input file.\n"
" -S <lang>   Forces Msc-generator to interpret the input file as a specific\n"
"             type of chart. This overrides the guess from the filename\n"
"             extension. Current chart types are 'signalling' for Signalling\n"
"             Charts and 'graph' for Graphviz graphs and 'block' for Block\n"
"             Diagrams.\n"
" --utf8      Forces the input file to be interpreted as UTF-8, even if it does\n"
"             not look like it.\n"
" --utf16     Forces the input file to be interpreted as UTF-16, even if it\n"
"             looks like UTF-8 or ASCII.\n"
" --opt=<chart_option>=<value>\n"
"             These options will be evaluated before the input file. Any value\n"
"             here will be overwritten by a conflicting option in the file.\n"
" --design=<chart_design>\n"
"             Any chart design can be specified here, taking precedence over the\n"
"             design specified in the input file.\n"
"\n"
"Output control:\n"
" -a[h]       Automatic pagination. Used only with full-page output. If\n"
"             specified, scale cannot be 'auto'. Specifying -ah will\n"
"             insert a heading after automatically inserted page breaks.\n"
" -D <design_file>\n"
"             Load file containing additional chart design definitions.\n"
"             You can have multiple of this option to load several design\n"
"             files, after the default ones. These files are loaded even if\n"
"             --nodesigns is specified.\n"
" -e          If output type is PNG, specifying this will embed the chart\n"
"             text into the PNG file as an iTXt chunk. In case of multiple\n"
"             pages (and multiple output files) all will have it embedded.\n"
"             Results in a warning if the output type is not PNG.\n"
" -F <font>   Use specified font. This must be a font name available in the\n"
"             local system, and overrides the MSCGEN_FONT environment variable\n"
"             if that is also set. (On Linux\\Unix try fontconfig.)\n"
" -L <lang>   Use the specified language to select fonts. It must be an RFC 3066\n"
"             style language specifier, and will override any value in the\n"
"             MSCGEN_FONT_LANG environment variable. Use either 2 letter lang\n"
"             codes, such as 'ja' for Japanese, 'zh' for Chinese, etc.\n"
"             Ignored on Windows, as font names are language specific there.\n"
" -m{lrud}=<margin>\n"
"             Useful only for full-page output, specifies the margin.\n"
"             A separate option is needed to specify the left, right, upwards\n"
"             and downwards margins, denoted by the second letter of the option.\n"
"             Margins are to be specified in inches (number only) or in\n"
"             centimeters, if appended with 'cm' (no spaces). The default\n"
"             margin is half inches everywhere.\n"
" --nodesigns\n"
"             If you specify this no design files will be loaded (except the\n"
"             ones you specify with -D). This is useful to increase performance\n"
"             when you do not use them anyway.\n"
" --nopaths   Only the filename will be printed in error messages without its\n"
"             path.\n"
" -o <file>   Write output to the named file. If omitted, the input filename\n"
"             will be appended by an extension suitable for the output format.\n"
"             If neither input nor output file is given, msc-gen_out.* will be\n"
"             used. Specifying '-' will result in the out be written to\n"
"             standard output.\n"
" -p[=<page size]\n"
"             Full-page output. (PDF or PPT only, automatically implied for PPT)\n"
"             In this case the chart is drawn on fixed-size pages (following\n"
"             pagination) with one pixel equaling to 1/72 inches for PDF and\n"
"             1/96 inches for PPT. If a chart page is larger than a physical\n"
"             page it is simply cropped with a warning. Setting the scale with\n"
"             the -s option enables zooming. Page size can be set to ISO sizes\n"
"             from A0 to A6, and to US sizes, such as letter, legal, ledger and\n"
"             tabloid. Append a 'p' or an 'l' for portrait and landscape,\n"
"             respectively (except for 'tabloid' and 'ledger', which are\n"
"             by definition portrait and landscape, resp.). E.g., use 'A4p',\n"
"             'A2l' or 'letter_l'. Default is 'A4p' for PDF and 'widescreen_l'\n"
"             for PPT, but you can also use 'on_screen_4_3_l' or\n"
"             'on_screen_16_9' as well.\n"
" --no-pedantic\n"
" --pedantic  Set or unset the pedantic chart option (defaults to what is set in\n"
"             the design lib or false if not defined).\n"
" -Pno        No progress indicator displayed.\n"
" -q          No progress indicator displayed, plus any final 'Success.' is\n"
"             also suppressed. On success with no warnings msc-gen prints\n"
"             nothing.\n"
" -s=<scale>  Can be used to scale chart size up or down. Default is 1.0.\n"
"             Cannot be used together with any of the -x or -y options.\n"
"             Only for bitmap or full-page output (-p).\n"
"             For full-page output, you can set <scale> to 'width' which\n"
"             results in the chart width being set to the page width, or to\n"
"             'auto', which scales such that all pages fit. For full-page\n"
"             output, you can specify multiple -s options, which makes\n"
"             msc-gen to try them in the order specified until one is\n"
"             found for which no pages need to be cropped. If none is\n"
"             such, the last one will be used and a warning will be given.\n"
" -T <type>   Specifies the output file type, which may be 'png', 'eps',\n"
"             'pdf', 'svg', 'ppt' or 'emf' (if supported on your system).\n"
"             Default is 'png'. The token 'ismap' produces an NCSA formatted\n"
"             ismap file. The token 'lmap' is accepted and results in a text\n"
"             file listing (the first line of) all labels and their page number\n"
"             and coordinates. 'src' will simply save the chart text; useful if\n"
"             you want to extract the source of charts embedded in PNG and PPT\n"
"             input files. You can also use 'embed' with a PPT output file to\n"
"             append a page with a PNG of the chart that includes the chart text\n"
"             (and can be later edited by Msc-generator). 'embed:<page>' will\n"
"             add the PNG to an existing page, while 'embed:<page>:<chart>' will\n"
"             update an existing chart (<chart> is the number of the chart on\n"
"             that page in Z-order starting from 1).\n"
" --TI        Additional Technical Info is printed about compilation.\n"
" -va=<center|up|down>\n"
" -ha=<center|left|right>\n"
"             Set the vertical and horizontal alignment within a page for full-\n"
"             page output.\n"
" -Wno        No warnings displayed.\n"
" -x=<width>  Specifies chart width (in pixels). Only for bitmap output.\n"
" -y=<height> Specifies chart height (in pixels). If only one of -x or -y\n"
"             is specified, the aspect ratio is kept. Only for bitmap output.\n"
" --fps=<fps> Can be used to select the maximum frames per second for the GUI.\n"
"             Silently ignored when not using the GUI. Ignored on Windows, as\n"
"             there frame presentation is synchronized to the refresh of the\n"
"             screen maxing the FPS to the screen FPS.\n"
"\n"
"Signalling chart specific options:\n"
" --force-mscgen\n"
"             Forces the chart to be interpreted in mscgen mode. Note that many\n"
"             Msc-generator attributes, commands and keywords are still\n"
"             recognized. This setting makes conflicting syntax be interpreted\n"
"             as mscgen would do. Without this switch Msc-generator uses the\n"
"             mscgen mode only if the chart starts with the text 'msc {'.\n"
" --prevent-mscgen\n"
"             Prevents the chart to be interpreted in mscgen mode. Note that\n"
"             some mscgen attributes and symbols are still recognized. This\n"
"             setting makes conflicting syntax be interpreted as Msc-generator\n"
"             would do. Without this switch Msc-generator uses the mscgen mode\n"
"             if the chart starts with the text 'msc {'.\n"
" -Wno-mscgen Disables warnings for deprecated constructs kept only for\n"
"             backwards compatibility with mscgen. Has no effect with\n"
"             --force-mscgen, in that case no such warnings are emitted anyway.\n"
"\n"
"Environment variables:\n"
"  MSCGEN_FONT: can be used to define the default font similar to the -F option.\n"
"  MSCGEN_FONT_LANG: can be used to define the default language for fontconfig\n"
"                    based systems, similar to the -L option.\n"
"  MSCGEN_NOFONT: When set (to any value), Msc-generator draws no text to any\n"
"                 output (but considers the place of the text at layout as\n"
"                 normally. Useful for testing.\n"
"  MSCGEN_FPS: When set to a number, the GUI will use it as maximum frames per\n"
"              second. Can be overridden by the --fps option. Ignored on\n"
"              Windows, as there frame presentation is synchronized to the\n"
"              refresh of the screen maxing the FPS to the screen FPS.\n"
"\n");
}




/** return program license as a string. */
std::string license(const LanguageCollection &languages) {
    std::string my =
"Msc-generator, a chart renderer.\n"
"Copyright (C) 2008-2024 Zoltan Turanyi\n"
"Distributed under GNU Affero General Public License.\n"
"\n"
"Msc-generator is free software: you can redistribute it and/or modify\n"
"it under the terms of the GNU Affero General Public License as published by\n"
"the Free Software Foundation, either version 3 of the License, or\n"
"(at your option) any later version.\n"
"\n"
"Msc-generator is distributed in the hope that it will be useful,\n"
"but WITHOUT ANY WARRANTY; without even the implied warranty of\n"
"MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n"
"GNU Affero General Public License for more details.\n"
"\n"
"You should have received a copy of the GNU Affero General Public License\n"
"along with Msc-generator.  If not, see <http://www.gnu.org/licenses/>.\n";
    for (auto &[lib, lic] : languages.GetLibraries())
        if (lic.empty()) continue;
        else my.append("\n\n").append(lib).append(" license:\n").append(lic);
    return my;
}

/** The main routine to perform a command-line action.
 * Reads and writes input and output files. Errors are emitted to std::cerr.
 * @param [in] args The list of command-line arguments.
 * @param [in] languages The languages supported by the build.
 * @param [in] design_files A list of <name,content> pairs of design library files,
 *                          which shall be parsed before the input file. Any error
 *                          occurring in them will be also reported. We include the
 *                          design library for all languages, do_main needs to parse
 *                          only the ones relevant for the language selected by the user.
 *                          If 'content' is empty, we use OpenNamedFile('name') to load it.
 * @param [in] OpenNamedFile The function to use to read a named file. Filename is in utf8.
 * @param [in] file_read_proc A function that opens&reads included files. This differs from
 *             OpenNamedFile, because it may handle "http:/xxx"-type includes, too. Filename is in utf8.
 * @param [in] file_read_proc_param The parameter passed transparently to file_read_proc.
 * @param [in] csh_textformat The text format to use for csh-ized processing.
 * @param [in] display_only Set to true by the caller if msc-gen was invoked NOT from
 *             a terminal AND the system has a graphical display. This causes the GUI mode
 *             to start if there are no command-line arguments.
 * @param [in] cb The callback to call regularly during processing to show progress.
 * @param [in] param The parameter to pass to the progress callback.
 * @param load_data Information on how much each processing step relatively takes.
 *                  Used to make progress display as much linear as possible.
 *                  It is also updated (refined) during processing, so the caller
 *                  can write it out. Load data contains the load data for all
 *                  languages, we will just use and update the one for our language.
 * @returns either an int on command-line operation, which should be returned;
 *   or a GUIInit, which means the GUI needs to be displayed.*/
std::variant<int, GUIInit>
do_main(const std::vector<std::string> &args,
    LanguageCollection &languages,
    std::vector<std::pair<std::string, std::string>> &&design_files,
    OpenNamedFileFunction OpenNamedFile,
    Chart::FileReadProcedure file_read_proc, void *file_read_proc_param,
    string csh_textformat,
    bool display_only,
    ProgressBase::ProgressCallback cb, void *param,
    std::string *load_data)
{
    using OUnsignedPair = std::optional<std::pair<unsigned, unsigned>>;

    std::string             oTextOutType;
    Canvas::EOutputType     oOutType = Canvas::PNG;
    string                  oOutputFile;
    string                  oInputFile;
    bool                    oUseStdIn = false;
    bool                    oWarning = true;
    bool                    oTechnicalInfo = false;
    bool                    oProgress = true;
    bool                    oPrintSuccess = true;
    bool                    oCsh = false;
    bool                    oSrc = false; //save the chart source
    bool                    oLmap = false;
    int                     oX = -1;
    int                     oY = -1;
    bool                    oForceUnicode = false;
    bool                    oForceUTF8 = false;
    bool                    oUseWidthAttr = true;
    std::vector<double>     oScale; //-2=auto, -1=width, other values = given scale
    std::optional<EPageSize>oPageSize;
    int                     oVA = -2, oHA = -2; //-1=left/top, 0=center, +1=right/bottom (-2==not set)
    double                  margins[] = {36, 36, 36, 36}; // half inches everywhere
    bool                    oA = false;
    bool                    oAH = false;
    bool                    oLoadDesigns = true;
    string                  oFont;
    string                  oLang;
    std::string             oForcedLanguage;
    std::string             oForcedDesign;
    bool                    oNoCopyright = false;
    std::map<std::string, std::vector<std::string_view>> oChartArguments; //maps CLI arguments to the languages supporting them as language-specific CLI option.
    std::vector<std::pair<Attribute, std::vector<std::string_view>>> oChartOptions;
    bool                    oPaths = true;
    bool                    oTEIMode = false;
    bool                    oGUI = args.empty() && display_only;
    bool                    oListEmbedded = false;
    bool                    oIncludeChartText = false;
    std::optional<bool>     oPedantic;
    OUnsignedPair           oEmbed;   //If the user says -T embed:slide:chart, these will contain 'slide' and 'chart' (both default to 0 as in 'insert')
    OUnsignedPair           oExtract; //If the user specified a filename ending in ":<num>:<num> this contains the two numbers.
    double                  oFPS = 60;


    if (const char* const font1 = getenv("MSCGEN_FONT")) oFont = font1;
    else if (const char* const font2 = getenv("CHARTGEN_FONT")) oFont = font2;
    if (const char* const lang1 = getenv("MSCGEN_FONT_LANG")) oLang = lang1;
    else if (const char* const lang2 = getenv("CHARTGEN_FONT_LANG")) oLang = lang2;

    MscError Error;
    const FileLineCol opt_pos(Error.AddFile("[options]"), 0, 0);
    const FileLineColRange opt_pos_range(opt_pos, opt_pos);
    bool show_usage = false;
    std::vector<std::string_view> tmp_langs;

    if (const char* const fps = getenv("MSCGEN_FPS"))
        if (from_chars(fps, oFPS) || oFPS < 1 || 240 < oFPS) {
            Error.Error(opt_pos, StrCat("The MSCGEN_FPS environment variable is supposed be a number between [1..240] and not '", fps, "'. Ignoring it."));
            oFPS = 60;
        }

    for (auto i=args.begin(); i!=args.end(); i++) {
        if (i->length()==0) continue;
        else if (i->length()>=2 && i->at(0) == '-' && i->at(1) == 'x') {
            if (i->at(2) != '=' || sscanf(i->c_str()+3, "%d", &oX)!=1)
                Error.Error(opt_pos, "Missing size after '-x='. Using native size.");
            else if (oX<10 || oX>200000) {
                Error.Error(opt_pos, "Invalid x size, it should be between [10..200000]. Using native size.");
                oX = -1;
            } else
                oUseWidthAttr = false;
        } else if (i->length()>=3 && i->at(0) == '-' && i->at(1) == 'y') {
            if (i->at(2) != '=' || sscanf(i->c_str()+3, "%d", &oY)!=1)
                Error.Error(opt_pos, "Missing size after '-y='. Using native size.");
            else if (oY<10 || oY>200000) {
                Error.Error(opt_pos, "Invalid y size, it should be between [10..200000]. Using native size.");
                oY = -1;
            } else
                oUseWidthAttr = false;
        } else if (i->length()>=4 && i->at(0) == '-' && i->at(1) == 's') {
            double os;
            if (i->at(2) != '=' ||
                (sscanf(i->c_str()+3, "%lf", &os)!=1 && tolower(i->at(3)) != 'a' && tolower(i->at(3)) != 'w'))
                Error.Error(opt_pos, "Missing scale after '-s='. Using native size.");
            else if (tolower(i->at(3)) == 'a') {
                oScale.push_back(-2); //auto
                oUseWidthAttr = false;
            } else if (tolower(i->at(3)) == 'w') {
                oScale.push_back(-1); //width
                oUseWidthAttr = false;
            } else if (os<=0.001 || os>100) {
                Error.Error(opt_pos, "Invalid scale, it should be between [0.001..100]. Ignoring it.");
            } else {
                oScale.push_back(os);
                oUseWidthAttr = false;
            }
        } else if (i->length()>=2 && i->at(0) == '-' && i->at(1) == 'p') {
            if (i->length()==2)
                oPageSize = EPageSize::NO_PAGE;
            else {
                oPageSize = i->at(2)=='=' ? ConvertPageSize(i->c_str()+3) : EPageSize::NO_PAGE;
                if (oPageSize == EPageSize::NO_PAGE)
                    Error.Error(opt_pos, "Invalid page size. Should be one of the ISO A-series, such as 'A4p' or 'A3l', or 'letter', 'legal', 'ledger', 'tabloid'. "
                                "You can also use 'widescreen_l', 'on_screen_4_3_l', 'on_screen_16_9_l' or 'on_screen_16_10_l' suitable for PPT output (use _p for portrait). "
                                "Using 'A4p' as default (or widescreen_l for PPT).");
            }
            oUseWidthAttr = false;
        } else if (i->length()>=3 && i->at(0) == '-' && (i->at(1) == 'v' || i->at(1) == 'h') &&
                   i->at(2) == 'a') {
            if (i->length()<5 || i->at(3) != '=')
                Error.Error(opt_pos, "I need a value for " + i->substr(0,3) + ". Ignoring this.");
            else {
                static const char h[] = "lcr", v[] = "ucd";
                static const char h_err[] = "left/center/right", v_err[] = "up/center/down";
                const char * const at = strchr(i->at(1)=='h' ? h : v, tolower(i->at(4)));
                if (at==nullptr)
                    Error.Error(opt_pos, "Bad value for "+ i->substr(0,3) + ". Use one of " +
                                   (i->at(1)=='h' ? h_err : v_err) + ". Ignoring this.");
                else
                    (i->at(1)=='h' ? oHA : oVA) = int(at - (i->at(1)=='h' ? h : v)) - 1;
            }
        } else if (i->length()>=2 && i->at(0) == '-' && i->at(1) == 'm') {
            static const char *dirs = "lrud";
            const char *at = strchr(dirs, tolower(i->at(2)));
            if (at==nullptr)
                Error.Error(opt_pos, "Invalid margin selector. Use one of '-ml', '-mr', '-mu' or '-md' for left, right, upper or lower margins, respectively.  Ignoring this.");
            else if (i->at(3) != '=')
                Error.Error(opt_pos, "After the margin option, I need a number with no spaces, like '-mu=1.2cm'. Ignoring this.");
            else {
                double mul = 72; //pixels per unit. Default is inch.
                char *buff = (char*)malloc(i->length());
                buff[0] = 0;
                double res;
                if (sscanf(i->c_str()+4, "%lf%s", &res, buff)==0)
                    Error.Error(opt_pos, "After the margin option, I need a number with no spaces, like '-mu=1.2cm'. Ignoring this.");
                else {
                    if (!strcmp("cm", buff)) mul = PT_PER_CM;
                    else if (buff[0] && strcmp("in", buff))
                        Error.Error(opt_pos, "After margin size value, specify the measurement unit: 'in' or 'cm'. Assuming inches.");
                    free(buff);
                    margins[at-dirs] = res*mul;
                }
            }
        } else if (*i == "--utf16") {
            oForceUnicode = true;
        } else if (*i == "--utf8") {
            oForceUTF8 = true;
        } else if (*i == "-a") {
            oA = true; oAH = false;
        } else if (*i == "-ah") {
            oA = true; oAH = true;
        } else if (*i=="-q") {
            oPrintSuccess = oProgress = false;
        } else if (*i == "-Pno") {
            oProgress = false;
        } else if (*i == "--nodesigns") {
            oLoadDesigns = false;
        } else if (*i == "--nopaths") {
            oPaths = false;
        } else if (*i == "--gui") {
            oGUI = true;
        } else if (*i == "--tei-mode") {
            oTEIMode = true;
            oProgress = false;
        } else if (*i == "--list-embedded") {
            oListEmbedded = true;
        } else if (*i == "--pedantic") {
            oPedantic = true;
        } else if (*i == "--no-pedantic") {
            oPedantic = false;
        } else if (i->length()>=2 && i->at(0)=='-' && i->at(1)=='S') { //begins with "-S"
            if (i->length()>2) {
                oForcedLanguage = i->substr(2);
            } else if (i==--args.end()) {
                Error.Error(opt_pos, "Missing chart type after option -S.");
                show_usage = true;
            } else {
                oForcedLanguage = *++i;
            }
        } else if (*i == "-e") {
            oIncludeChartText = true;
        } else if (*i == "-o") {
            if (i==--args.end()) {
                Error.FatalError(opt_pos, "Missing output filename after '-o'.");
                show_usage = true;
            } else
                oOutputFile = *(++i);
        } else if (*i == "-F") {
            if (++i==args.end()) {
                Error.Error(opt_pos,
                    "Missing font name after '-F'.",
                    "Using " + (oFont.length() ? "'"+oFont+"' instead." : "default font."));
                show_usage = true;
            } else {
                //above we have already moved i to the parameter of -F
                string font_name = *i;
                if (Canvas::HasFontFace(font_name)) {
                    oFont = *i;
                } else if (font_name.length()) {
                    oFont = font_name;
                    Error.Warning(opt_pos,
                        "Font '"+*i+"' is not available.",
                        "Using '"+oFont+"' instead.");
                } else {
                    Error.Error(opt_pos,
                        "Font '"+*i+"' is not available. Ignoring option.",
                        "Using " + (oFont.length() ? "previously set '"+oFont+"' instead." : "the default font."));
                }
            }
        } else if (*i == "-L") {
            if (++i==args.end()) {
                Error.Error(opt_pos,
                            "Missing language spec after '-L'.",
                            "Using " + (oLang.length() ? "'"+oLang+"' instead." : " the default language."));
                show_usage = true;
            } else
                oLang = *i; //above we have already moved i to the parameter of -L
        } else if (*i == "-D") {
            if (i==--args.end()) {
                Error.Error(opt_pos,
                    "Missing design file name after '-F'.");
                show_usage = true;
            } else {
                //add to design files
                design_files.emplace_back(*++i, "");
            }
        } else if (i->length()>=2 && i->at(0)=='-' && i->at(1)=='T') { //begins with "-T"
            if (i->length()>2)
                oTextOutType = i->substr(2);
            else if (i==--args.end()) {
                Error.Error(opt_pos,
                                "Missing output type after '-T'.",
                                "Assuming 'png'.");
                oOutType = Canvas::PNG;
                show_usage = true;
            } else {
                i++;
                oTextOutType = *i;
            }
            if (oTextOutType.length()) {
                oCsh = oLmap = false;
                if (oTextOutType == "csh") {
                    oCsh = true;
                } else if (oTextOutType == "src") {
                    oSrc = true;
                } else
#ifdef CAIRO_HAS_PNG_FUNCTIONS
                if (oTextOutType == "png") {
                    oOutType = Canvas::PNG;
                } else if (oTextOutType.starts_with("embed")) {
                    oOutType = Canvas::PNG;
                    oTextOutType.erase(0, 5);
                    oEmbed = { 0,0 };
                    if (oTextOutType.length()) {
                        if (oTextOutType.front()!=':')
                            embed_problem:
                            Error.FatalError(opt_pos,
                                             "Strange output type after '-T'.",
                                            "Use 'embed:<slide>:<chart>' to update an existing chart, 'embed:<slide>' to insert a new one or just 'embed' to insert it on a new slide.");
                        else {
                            oTextOutType.erase(0, 1);
                            while (oTextOutType.size() && '0'<=oTextOutType.front() && oTextOutType.front()<='9') {
                                oEmbed->first = oEmbed->first*10 + oTextOutType.front() - '0';
                                oTextOutType.erase(0, 1);
                            }
                            if (oTextOutType.length()) {
                                if (oTextOutType.front()!=':') goto embed_problem;
                                oTextOutType.erase(0, 1);
                                while (oTextOutType.size() && '0'<=oTextOutType.front() && oTextOutType.front()<='9') {
                                    oEmbed->second = oEmbed->second*10 + oTextOutType.front() - '0';
                                    oTextOutType.erase(0, 1);
                                }
                                if (oTextOutType.length()) goto embed_problem;
                            }
                        }
                    }
                } else
#endif
#ifdef CAIRO_HAS_PS_SURFACE
                 if (oTextOutType == "eps") {
                     oOutType = Canvas::EPS;
                 } else
#endif
#ifdef CAIRO_HAS_PDF_SURFACE
                 if (oTextOutType == "pdf") {
                     oOutType = Canvas::PDF;
                 } else
#endif
#ifdef CAIRO_HAS_SVG_SURFACE
                 if (oTextOutType == "svg") {
                     oOutType = Canvas::SVG;
                 } else
#endif
#ifdef CAIRO_HAS_WIN32_SURFACE
                 if (oTextOutType == "emf") {
                     oOutType = Canvas::EMF;
                 } else if (oTextOutType == "wmf")  {//undocumented
                     oOutType = Canvas::WMF;
                 } else
#endif
                 if (oTextOutType == "ismap") { //undocumented
                     oOutType = Canvas::ISMAP;
                 } else if (oTextOutType == "lmap") {
                     oLmap = true;
                     oOutType = Canvas::PDF; //use this format for laying out (-p also works with it)
                 } else if (oTextOutType == "ppt")
                     oOutType = Canvas::PPT;
                 else {
                     Error.Error(opt_pos,
                                     "Unknown output format '" + oTextOutType + "'. "
                                     "Use one of"
#ifdef CAIRO_HAS_PNG_FUNCTIONS
                     " 'png'"
#endif
#ifdef CAIRO_HAS_PS_SURFACE
                     " 'eps'"
#endif
#ifdef CAIRO_HAS_PDF_SURFACE
                     " 'pdf'"
#endif
#ifdef CAIRO_HAS_SVG_SURFACE
                     " 'svg'"
#endif
#ifdef CAIRO_HAS_WIN32_SURFACE
                     " 'emf' 'wmf'"
#endif
                     " 'lmap', 'embed:<slide>:<chart>' or 'csh'. Using 'png'.");
                     oOutType = Canvas::PNG;
                 }
            }
        } else if (*i == "-l") {
            std::cout<<license(languages);
            return EXIT_SUCCESS;
        } else if (*i == "-h" || *i=="--help") {
            usage();
            return EXIT_SUCCESS;
        } else if (*i == "--version") {
            std::cout<<version(languages);
            return EXIT_SUCCESS;
        } else if (*i == "-Wno") {
            oWarning = false;
        } else if (*i == "--TI") {
            oTechnicalInfo = true;
        } else if (*i == "-Pno") {
            oProgress = false;
        } else if (*i == "--nocopyright") {
            oNoCopyright = true;
        } else if (*i == "--nodesigns") {
            oLoadDesigns= false;
        } else if (i->substr(0,13) == "--csh_format=") {
            csh_textformat += i->substr(13);
        } else if (*i == "-i") {
            if (i==--args.end()) {
                if (oInputFile=="")
                    Error.FatalError(opt_pos, "Missing input filename after '-i'.");
                else
                    Error.FatalError(opt_pos, "You have specified an input file, "
                                                  "ignoring '-i' at the end of command line.");
                show_usage = true;
            } else
                if (oInputFile=="")
                    oInputFile = *(++i);
                else {
                    Error.Error(opt_pos, "Already specified the input file as: '"
                                             + oInputFile + "'. Ignoring this option: '-i "+*(++i) +"'.");
                    show_usage = true;
                }
        } else if (i->starts_with("--opt=")) {
            oChartOptions.push_back(IsValidChartSpecificCommandLineOption(languages, i->substr(6)));
            if (oChartOptions.back().second.empty()) {
                Error.Error(opt_pos, StrCat("Chart option '", i->substr(6), "' is not supported by any of the languages. Ignoring it."));
                oChartOptions.pop_back();
            }
        } else if (i->starts_with("--design=")) {
            oForcedDesign = i->substr(9);
        } else if (i->starts_with("--fps")) {
            if (i->length()<6 || i->at(5)!='=')
                Error.Error(opt_pos, "Missing '=' after '--fps'. (Apply no spaces.)");
            else if (i->length()==6)
                Error.Error(opt_pos, "Missing number after '--fps='. (Apply no spaces.)");
            else if (from_chars(i->substr(6), oFPS))
                Error.Error(opt_pos, StrCat("I need a number after '--fps=' and not '", i->substr(6), "'."));
            else if (oFPS < 1 || 240 < oFPS) {
                Error.Error(opt_pos, StrCat("FPS should be a number between [1..240] and not '", i->substr(6), "'. Using max FPS of 60."));
                oFPS = 60;
            }
        } else if (i->length()>=2 && i->at(0) == '-' &&
                   (tmp_langs = IsValidChartSpecificCommandLineArg(languages, *i)).size()) { //may be a chart-specific attribute, test if we accept it
            oChartArguments[std::move(*i)] = std::move(tmp_langs);
        } else if (i->at(0) == '-' && i->size()>1) {
            Error.Error(opt_pos, "Unrecognized option: '"+*i+"'. Ignoring it.");
            show_usage = true;
        } else if (i->at(0) == '-') {
            oUseStdIn = true;
        } else if (oInputFile=="") { //argument does not start with a hyphen - use as input file
            oInputFile = *i;
        } else {
            Error.Error(opt_pos, "Already specified the input file as: '"
                        + oInputFile + "'. Ignoring option '"+*i+"'.");
            show_usage = true;
        }
    }

    if (oListEmbedded) {
        if (oGUI) {
            Error.FatalError(opt_pos, "--gui and --list-embedded cannot be specified together.");
            oGUI = false;
        } else if (oTEIMode) {
            Error.FatalError(opt_pos, "--tei-mode and --list-embedded cannot be specified together.");
            oTEIMode = false;
        } else if (oEmbed || oCsh || oSrc || oOutType!=Canvas::PNG) {
            Error.FatalError(opt_pos, "--T and --list-embedded cannot be specified together.");
            oEmbed.reset();
            oCsh = oSrc = false;
        } else if (oOutputFile.length()) {
            Error.Error(opt_pos, "--list-embedded works to the standard output. Ignoring output file '"+oOutputFile+"'.");
        } else if (oForcedLanguage.length()) {
            Error.FatalError(opt_pos, "--S and --list-embedded cannot be specified together.");
            oForcedLanguage.clear();
        }

    }

    std::string saved_ppt_outfile;
    if (oEmbed) {
        if (!CaseInsensitiveEndsWith(oOutputFile, "pptx"))
            Error.FatalError(opt_pos, "You need to specify a PPT output file with '-T embed'.");
        else
            saved_ppt_outfile = std::exchange(oOutputFile, my_tmpnam());
        if (oScale.empty()) oScale.push_back(4.0); //x4 zoom when embedding to PPT by default.
    }

    //Check if the input filename ends with :<num>:<num>
    if (oInputFile.length())
        if (const size_t pos2 = oInputFile.find_last_of(':'); pos2!=string::npos) {
            if (pos2==0)
                Error.FatalError(opt_pos, "I am confused by an input filename starting with a colon: '"+oInputFile+"'.");
            //Note: valid Windows paths may contain *one* colon (after the drive letter or server name)
            //It is possible that the colon is followed by a number as filename, as "C:123", but we
            //decide to interpret that as a slide number. In the case of "C:123" we will fail to open "C:"
            //and get an error allowing the user to adjust the path, e.g., to "C:.\123"
            else if (const size_t pos1 = oInputFile.find_last_of(':', pos2 - 1); pos1 != string::npos) {
                //we have at least two colons. The text after the last one must be a number
                if (unsigned last_num; from_chars(std::string_view(oInputFile).substr(pos2 + 1), last_num))
                    //E.g, "C:file.txt:aaa
                    Error.FatalError(opt_pos, "Could not parse slide number after input file '" + oInputFile + "'.");
                else if (unsigned first_num; from_chars(std::string_view(oInputFile).substr(pos1 + 1, pos2 - pos1 - 1), first_num)) {
                    //The thing between the two colons is not a number
                    if (std::string_view(oInputFile).substr(0, pos1).find(':')!=std::string_view::npos) //there is a third colon before pos2 - that may be part of the filename
                        //E.g., "C:file.txt:aaa:123"
                        Error.FatalError(opt_pos, "Could not parse slide number after input file '" + oInputFile + "'.");
                    else { //assume the text between the first and second colon it is part of the filename.
                        //E.g., "C:file.txt:123"
                        oExtract.emplace(last_num, 0);
                        oInputFile.erase(pos2);
                    }
                } else {
                    //Both are valid numbers, e.g., "filename.txt:1:2" or "C:file.txt:1:2"
                    oExtract.emplace(first_num, last_num);
                    oInputFile.erase(pos1);
                }
            } else if (unsigned num; !from_chars(std::string_view(oInputFile).substr(pos2 + 1), num)) {
                //valid number after the single colon: interpret as slide number
                //E.g: "file.txt:1", but also "C:123"
                oExtract.emplace(num, 0);
                oInputFile.erase(pos2);
            } //else if not a num, we assume it is part of the filename, e.g.: "C:file.txt"
        }

    //Finished parsing the arguments
    //First determine language
    string type;
    if (oForcedLanguage.length()) {
        if (oForcedLanguage == "png")
            type = oForcedLanguage;
        else if (std::string lang = languages.LanguageForExtension(oForcedLanguage); lang.size())
            type = oForcedLanguage = lang;
        else
            Error.Error(opt_pos, "Unrecognized chart type after -S: '"+oForcedLanguage+"'. Ignoring it.",
                "Supported chart types are: " + PrintStrings(languages.GetLanguages()) + ".");
    }

    if (type.empty() && oInputFile.length()) {
        const size_t pos = oInputFile.find_last_of('.');
        if (pos!=string::npos) {
            const std::string_view ext = std::string_view(oInputFile).substr(pos+1);
            if (CaseInsensitiveEqual(ext, "png"))
                type = "png";   //the real language will be substituted later
            else if (CaseInsensitiveEqual(ext, "pptx"))
                type = "ppt";   //the real language will be substituted later
            else
                type = languages.LanguageForExtension(ext);
        }
    }
    if (oListEmbedded) {
        if (oExtract) {
            Error.Error(opt_pos, "Ignoring ':<slide>:<chart>' specified after the input file name with --list-embedded.");
            oExtract.reset();
        } else if (oUseStdIn) {
            Error.FatalError(opt_pos, "--list-embedded requires a PPT file as input and cannot work from stdin.");
        } else if (type!="ppt")
            Error.FatalError(opt_pos, "--list-embedded requires a PPT file as input.");
        if (oPageSize.has_value()) {
            Error.Error(opt_pos, "Ignoring -p with --list-embedded.");
            oPageSize.reset();
        }
        if (oForceUnicode) {
            Error.Error(opt_pos, "Ignoring -utf16 with --list-embedded.");
            oForceUnicode = false;
        }
        if (oForceUTF8) {
            Error.Error(opt_pos, "Ignoring -utf8 with --list-embedded.");
            oForceUTF8 = false;
        }
    }
    if (oExtract && type!="ppt")
        Error.Error(opt_pos, "Only 'pptx' files may have a :<slide>:<chart> specifier after the input filename. Ignoring this.");

    if (oUseStdIn && oInputFile.size()) {
        Error.FatalError(opt_pos, "You have specified both an input file name '"+oInputFile+"' and the '-' (for stdin). Use only one of these.");
        oUseStdIn = false;
    }

    if (oGUI) {
        if (oTEIMode) {
            Error.FatalError(opt_pos, "Don't use --tei-mode and --gui together.");
            oGUI = false;
        } else {
            if (oUseStdIn && type.empty())
                Error.FatalError(opt_pos, "Stdin is specified as input, but no chart type is given. Use '-S' to specify chart type.");
            if (oSrc || oCsh || oTextOutType.size())
                Error.FatalError(opt_pos, "Cannot use an output type when invoking the GUI. "
                                          "Retry without the '-T' option.");
            if (oOutputFile.size())
                Error.FatalError(opt_pos, "No output file needed when invoking the GUI. "
                                          "Retry without the '-o "+oOutputFile+"' option.");
            if (oIncludeChartText)
                Error.FatalError(opt_pos, "This option is not valid for the GUI: -e. Retry without it.");
            if (oPageSize.has_value())
                Error.FatalError(opt_pos, "This option is not valid for the GUI: -p. Retry without it.");
            if (oScale.size())
                Error.FatalError(opt_pos, "This option is not valid for the GUI: -s. Retry without it.");
            if (oHA!=-2 || oVA!=-2 || oA)
                Error.FatalError(opt_pos, "These options are not valid for the GUI: -ha, -va, -a. Retry without them.");
            if (oForceUnicode)
                Error.FatalError(opt_pos, "Sorry, the GUI can only work with UTF-8 files. Retry without --utf16.");
            if (oExtract)
                Error.Warning(opt_pos, "Ignoring <slide> and <chart> setting after the input filename.");
            if (oLoadDesigns)
                for (auto &p : design_files) {
                    //see if the design file is of known type
                    size_t pos = p.first.find_last_of('.');
                    if (pos != string::npos && languages.LanguageForExtension(std::string_view(p.first).substr(pos+1)).size()) {
                        // remove path if the user wanted so
                        std::string fn = p.first;
                        if (!oPaths) {
                            size_t p = fn.find_last_of("\\/");
                            if (p!=std::string::npos)
                                fn.erase(0, p+1);
                        }
                        if (p.second.length()==0) { //not yet loaded
                            FILE *in = OpenNamedFile(p.first.c_str(), false, false);
                            if (in) {
                                p.second = ReadFile(in);
                                fclose(in);
                            } else
                                Error.FatalError(opt_pos, "Failed to read in design file '" + p.first +"'.");
                        }
                    }
                }
            std::erase_if(design_files, [](auto &p) { return p.second.empty(); });
            languages.CompileDesignLibs(std::move(design_files));
        }
    } else if (!oTEIMode) {
        if (oInputFile.empty() && !oUseStdIn) {
            Error.FatalError(opt_pos, "No input specified. Use a filename or '-' for stdin or '--gui' if you want to start the GUI.");
            show_usage = true;
        } else if (type.empty()) { //here type is 'png' if user proscribed a png input file
            if (oInputFile.empty()) {
                Error.FatalError(opt_pos, "Stdin is specified as input, but no chart type is given. Use '-S' to specify chart type.");
                show_usage = true;
            } else {
                const size_t dot_pos = oInputFile.find_last_of('.');
                const std::string ext = dot_pos==std::string::npos ? "" : oInputFile.substr(dot_pos+1);
                Error.FatalError(opt_pos, "Could not recognize chart type: '" + ext + "'", "Use -S or an input filename with known extensions. "
                                 "Supported extensions types are: " + PrintStrings(languages.GetExtensions()) + " or 'png'.");
            }
        }
    }
    _ASSERT(oGUI || oTEIMode || oUseStdIn || oInputFile.size() || Error.hasFatal());

    //-p is implied for PPT
    if (oOutType==Canvas::PPT && !oPageSize.has_value())
        oPageSize = EPageSize::NO_PAGE;

    if (oTEIMode) {
        if (oCsh || oSrc || oTextOutType.length())
            Error.FatalError(opt_pos, "The option '--tei-mode' is can not be used with option -T.");
        else if (oInputFile.length())
            Error.FatalError(opt_pos, "The option '--tei-mode' uses the standard input. You cannot specify an input file.");
        else if (oOutputFile.length())
            Error.FatalError(opt_pos, "The option '--tei-mode' uses the standard output. You cannot specify an output file.");
        else if (type.empty())
            Error.FatalError(opt_pos, "You need to specify the chart language for the '--tei-mode' option. Use '-S <language>'.");
#ifdef WIN32
        else
            _setmode(_fileno(stdin), _O_BINARY); //Binray stdin, so that on windows no \r\n->\n conversions take place screwing up character positions
#endif // WIN32
    }

    //Check that we extract from a ppt or png file.
    if (oSrc) {
        if (oOutputFile.empty())
            Error.FatalError(opt_pos, "Need an output file (or '-' for stdout) when extracting chart text.");
        else if (type!="png" && type!="ppt")
            Error.FatalError(opt_pos, "Need a PNG or PPT input file for '-T src'.");
    }

    //OK, resolve a png/ppt input type
    std::string input;
    std::string gui_state;
    bool already_has_input = false;
    if (type=="png" && !oGUI && !oListEmbedded) {
        if (oInputFile.length()) {
            FILE *in = fopen(oInputFile.c_str(), "rb");
            EmbedChartData data = pngutil::Select(in, languages, Error, opt_pos);
            fclose(in);
            type = std::move(data.chart_type);
            input = std::move(data.chart_text);
            gui_state = std::move(data.gui_state);
        } else {
            IndicateStdin(oProgress, oPaths, oWarning, oTechnicalInfo, Error);
            EmbedChartData data = pngutil::Select(stdin, languages, Error, opt_pos);
            type = std::move(data.chart_type);
            input = std::move(data.chart_text);
            gui_state = std::move(data.gui_state);
        }
        if (type.length())
            already_has_input = true;
    }

    if (type=="ppt" && !oGUI && !oListEmbedded) {
        if (oInputFile.empty())
            Error.FatalError(opt_pos, "Cannot take PPT input from the stdin. Specify an input file name.");
        else {
            std::vector<PptFile::ChartInPPT> charts;
            std::string err = PptFile::parse_charts(ZipArchive::FromFile(oInputFile), charts);
            if (err.size())
                Error.FatalError(opt_pos, err);
            else if (charts.empty())
                Error.FatalError(opt_pos, "The file '"+oInputFile+"' contains no charts embedded by Msc-generator.");
            else {
                std::optional<size_t> idx;
                if (!oExtract || oExtract->first==0) {
                    if (charts.size()>1)
                        Error.FatalError(opt_pos, "The file '"+oInputFile+"' contains " + std::to_string(charts.size())
                                         + " charts embedded by Msc-generator.",
                                         "Specify the slide number after the filename, like '"+oInputFile+":5'.");
                    else
                        idx = 0;
                } else {
                    //Collect pages containing charts
                    std::set<int> pages;
                    for (const PptFile::ChartInPPT& c : charts)
                        pages.insert(c.slide);
                    std::erase_if(charts, [slide = oExtract->first](const PptFile::ChartInPPT& c) { return c.slide!=slide; });
                    if (charts.empty()) {
                        std::string s;
                        if (pages.size()==1)
                            s = "Use slide "+std::to_string(*pages.begin())+" instead.";
                        else {
                            s = "Slides that contain charts are: ";
                            for (int i : pages)
                                s.append(std::to_string(i)).append(", ");
                            s.pop_back();
                            s.back() = '.';
                        }
                        Error.FatalError(opt_pos, "The file '"+oInputFile+"' contains no embedded charts on slide "
                                         +std::to_string(oExtract->first)+".", s);
                    } else {
                        if (oExtract->second==0) {
                            if (charts.size()>1)
                                Error.FatalError(opt_pos, "The file '"+oInputFile+"' contains " + std::to_string(charts.size())
                                                 + " embedded charts on slide "+std::to_string(oExtract->first) + ".",
                                                 "Specify the chart number after the slide number, like '"+oInputFile+":"
                                                 + std::to_string(oExtract->first) + ":2'.");
                            else
                                idx = 0;
                        } else {
                            if (charts.size()<oExtract->second)
                                Error.FatalError(opt_pos, "The file '"+oInputFile+"' contains only "+std::to_string(charts.size())
                                                 +" embedded charts on slide "+std::to_string(oExtract->first)+".");
                            else
                                idx = oExtract->second - 1;
                        }
                    }
                }
                if (idx) {
                    type = std::move(charts[*idx].data.chart_type);
                    input = std::move(charts[*idx].data.chart_text);
                    gui_state = std::move(charts[*idx].data.gui_state);
                    already_has_input = true;
                } else
                    _ASSERT(Error.hasFatal());
            }
        }
    }

    if (oForceUnicode && oForceUTF8)
        Error.FatalError(opt_pos, "You can only specify either --utf16 or --utf8, not both.");

    if (Error.hasFatal()) {
        //no chart type, a fatal error
        if (!oPaths)
            Error.RemovePathFromErrorMessages();
        std::cerr << Error.Print(oWarning, oTechnicalInfo);
        if (show_usage)
            std::cerr<<"Use 'msc-gen --help' for full command-line and options syntax."<<std::endl;
        std::cerr << "Bailing out." << std::endl;
        return EXIT_FAILURE;
    }

    if (oListEmbedded) {
        std::vector<PptFile::ChartInPPT> charts;
        std::vector<std::string> slides;
        std::string err = PptFile::parse_charts(ZipArchive::FromFile(oInputFile), charts, &slides);
        if (err.size())
            Error.FatalError(FileLineCol(), err);
        if (!oPaths)
            Error.RemovePathFromErrorMessages();
        if (Error.GetErrorNum(oWarning, oTechnicalInfo))
            std::cerr << Error.Print(oWarning, oTechnicalInfo)<<std::endl;
        if (err.empty()) {
            if (charts.empty())
                std::cerr << "No charts in '"<<oInputFile<<"'."<<std::endl;
            else {
                unsigned last_page = 0;
                std::string last_title;
                for (const PptFile::ChartInPPT& c : charts) {
                    if (last_page != c.slide) {
                        if (last_page && slides[last_page-1].length())
                            std::cout<<"  Slide Title: "<<slides[last_page-1]<<"\n";
                        last_page = c.slide;
                    }
                    std::cout<<oInputFile<<":"<<c.slide<<":"<<c.chart<<" ("<<languages.GetLanguage(c.data.chart_type)->description<<")\n";
                }
                if (last_title.size())
                    std::cout<<"  Slide Title: "<<last_title<<"\n";
            }
            return EXIT_SUCCESS;
        } else
            return EXIT_FAILURE;
    }

    std::string CopyrightText = oNoCopyright
        ? ""
        : "\\md(0)\\mu(2)\\mr(0)\\mn(10)\\pr\\c(150,150,150)"
          "https://gitlab.com/msc-generator "
          + VersionText(LIBMSCGEN_MAJOR, LIBMSCGEN_MINOR, LIBMSCGEN_SUPERMINOR);

    if (oGUI) {
        if (Error.GetErrorNum(oWarning, oTechnicalInfo)) {
            if (!oPaths)
                Error.RemovePathFromErrorMessages();
            std::cerr << Error.Print(oWarning, oTechnicalInfo);
        }
        MscInitializeCshAppearanceList();
        if (oUseStdIn) {
            _ASSERT(type != "png");
            input = ReadFile(stdin, false);
        }
        return GUIInit{languages, file_read_proc, file_read_proc_param,
                       OpenNamedFile, CopyrightText, oInputFile, oInputFile, type, oFont, oLang,
                       std::move(oChartArguments), std::move(oChartOptions), oPedantic,
                       std::move(input), std::move(oForcedDesign), load_data, oFPS};
    }

    _ASSERT(type.length());

    auto ldset = SplitLoadData(load_data, &Error, &opt_pos);

    const LanguageData *lang = languages.GetLanguage(type);
    _ASSERT(lang);
    std::unique_ptr<Chart> chart = lang->create_chart(file_read_proc, file_read_proc_param);
    chart->prepare_for_tracking = false;
    chart->prepare_element_controls = false;
    chart->GetProgress()->callback = oProgress ? cb : nullptr;
    chart->GetProgress()->data = param;
    chart->GetProgress()->ReadLoadData(ldset[lang->GetName()]);
    chart->copyrightText = std::move(CopyrightText);
    chart->SetPedantic(oPedantic.value_or(lang->pedantic));
    chart->Error = std::move(Error); //from now on report errors to chart->Error

    chart->GetProgress()->StartParse();
    //Load deisgn definitions
    if (oLoadDesigns)
        for (auto &p : design_files) {
            //see if the design file is of our type
            size_t pos = p.first.find_last_of('.');
            if (pos != string::npos && lang->HasExtension(std::string_view(p.first).substr(pos+1))) {
                // remove path if the user wanted so
                std::string fn = p.first;
                if (!oPaths) {
                    size_t p = fn.find_last_of("\\/");
                    if (p!=std::string::npos)
                        fn.erase(0, p+1);
                }
                if (p.second.length()==0) {
                    //no, a file that we need to load
                    FILE * in = OpenNamedFile(p.first.c_str(), false, false);
                    if (in) {
                        p.second = ReadFile(in);
                        fclose(in);
                    } else
                        chart->Error.FatalError(opt_pos, "Failed to read in design file '" + p.first +"'.");
                }
                if (p.second.length())
                    chart->ParseText(p.second, fn);
            }
        }

    //Add Chart arguments
    for (auto &[arg, langs] : oChartArguments)
        if (arg.length() && !chart->AddCommandLineArg(arg)) {
            chart->Error.Error(opt_pos, "Option: "+arg+" is only valid for "+PrintStrings(langs)+" chart(s). Ignoring it.");
            show_usage = true;
        }
    if (oForcedDesign.size() && !chart->ApplyForcedDesign(oForcedDesign)) {
        chart->Error.Error(opt_pos, "Unrecognized design: '"+oForcedDesign+"'. Ignoring it.",
                    "You can use the following designs: "+chart->GetDesignNamesAsString(true));
        oForcedDesign.clear();
    }
    for (auto& [attr, langs] : oChartOptions)
        if (std::ranges::find(langs, lang->GetName())!=langs.end())
            chart->AddCommandLineOption(attr);
        else
            chart->Error.Error(opt_pos, "Chart option: " + attr.name + " is only valid for " + PrintStrings(langs) + " chart(s). Ignoring it.");

    //Add font
    if (oFont.length() || oLang.length())
        chart->SetDefaultFont(oFont, oLang, opt_pos_range);

    /* Determine output filename */
    if (oOutputFile == "" && oInputFile.size()) {
        oOutputFile = oInputFile;
        size_t dot=oInputFile.find_last_of('.');
        size_t dash=oInputFile.find_last_of("/\\");
        //Remove extension, if any and not only an extension
        if (dot!=oInputFile.npos && dot!=0 &&
            (dash==oInputFile.npos || dash<dot))
            oOutputFile.erase(dot);
        if (oCsh)
            oOutputFile.append(".csh");
        else if (oLmap)
            oOutputFile.append(".map");
        else if (oSrc)
            _ASSERT(0);
        else switch (oOutType) {
        case Canvas::PNG:
            oOutputFile.append(".png"); break;
        case Canvas::EPS:
            oOutputFile.append(".eps"); break;
        case Canvas::PDF:
            oOutputFile.append(".pdf"); break;
        case Canvas::SVG:
            oOutputFile.append(".svg"); break;
        case Canvas::EMF:
            oOutputFile.append(".emf"); break;
        case Canvas::WMF:
            oOutputFile.append(".wmf"); break;
        case Canvas::ISMAP:
            oOutputFile.append(".map"); break;
        case Canvas::PPT:
            oOutputFile.append(".pptx"); break;
        default:
            _ASSERT(0);
        }
    } else if (oOutputFile=="-") {
        //this means standard output
        oOutputFile.clear();
    } else if (oSrc) {
        //Append correct extension to output file, if none
        size_t dot = oOutputFile.find_last_of('.');
        size_t dash = oOutputFile.find_last_of("/\\");
        //Append extension, if none
        if (dot==oOutputFile.npos || (dash!=oOutputFile.npos && dot<dash))
            oOutputFile.append(".").append(lang->GetName());
    }

    if (oIncludeChartText && (oCsh || oSrc || oEmbed || oOutType!=Canvas::PNG)) {
        chart->Error.Warning(opt_pos, "The '-e' option is applicable to PNG output format only. Ignoring it.");
        oIncludeChartText = false;
    }

    //Determine option compatibility
    if (oPageSize.has_value() && (oCsh || oSrc || oEmbed || (oOutType != Canvas::PDF && oOutType != Canvas::PPT))) {
        chart->Error.Error(opt_pos, "-p can only be used with PDF or PPT output. Ignoring it.");
        oPageSize.reset();
    }
    bool has_auto_or_width = false;
    for (unsigned s=0; s<oScale.size() && !has_auto_or_width; s++)
        has_auto_or_width = oScale[s]<=0;
    if (has_auto_or_width && !oPageSize.has_value()) {
        chart->Error.Error(opt_pos, "-s=auto and -s=width can only be used with full-page output. Using default scale of 1.0.");
        oScale.assign(1, 1.0);
    }
    if (oScale.size()>1 && !oPageSize.has_value()) {
        chart->Error.Error(opt_pos, "Multiple -s options can only be applied to full-page output. Using default scale of 1.0.");
        oScale.assign(1, 1.0);
    }
    if (!oPageSize.has_value() && oHA!=-2) {
        chart->Error.Error(opt_pos, "-ha can only be used with full-page output (-p). Ignoring it.");
        oHA = -1;
    }
    if (oHA == -2) oHA = -1;
    if (!oPageSize.has_value() && oVA!=-2) {
        chart->Error.Error(opt_pos, "-va can only be used with full-page output (-p). Ignoring it.");
        oVA = -1;
    }
    if (oVA == -2) oVA = -1;
    if (!oPageSize.has_value() && oA) {
        chart->Error.Error(opt_pos, "-a can only be used with full-page output (-p). Ignoring it.");
        oA = false;
    }
    if (oA) {
        bool had_auto = false;
        for (int s=0; s<(int)oScale.size() && !had_auto; s++)
            if (oScale[s]==-2) {
                had_auto = true;
                oScale.erase(oScale.begin()+s);
                s--;
            }
        if (had_auto && oScale.size())
            chart->Error.Error(opt_pos, "Scale to fit page ('auto') cannot be used with automatic pagination (-a). Ignoring it.");
        else if (had_auto && oScale.size()==0) {
            chart->Error.Error(opt_pos, "Scale to fit page ('auto') cannot be used with automatic pagination (-a). Replacing with scale to fit page width ('width').");
            oScale.push_back(-1);
        }
        if (oScale.size()>1) {
            chart->Error.Error(opt_pos, "Multiple -s options cannot be applied to automatic pagination. Using default scale of 1.0.");
            oScale.assign(1, 1.0);
        }
    }

    if (chart->Error.hasFatal()) goto fatal;

    /* Parse input, either from a file, or stdin */
    if (!already_has_input) {
        if (oInputFile.empty()) {
            IndicateStdin(oProgress, oPaths, oWarning, oTechnicalInfo, chart->Error);
            input = ReadFile(stdin, oTEIMode, !oTEIMode);
        } else {
            FILE *in = fopen(oInputFile.c_str(), "r");
            if (in) {
                input = ReadFile(in, oTEIMode, !oTEIMode);
                fclose(in);
            } else
                chart->Error.FatalError(opt_pos, "Failed to open input file '" + oInputFile +"'.");
        }
        if (chart->Error.hasFatal()) goto fatal;
    }

    //Convert to UTF8 (if nonzero len)
    if (!oForceUTF8 && input.length())
        ConvertToUTF8(input, oForceUnicode);

    if (oSrc) {
        FILE* out = oOutputFile.length() ? OpenNamedFile(oOutputFile.c_str(), true, false) : stdout;
        if (out) {
            //For TEI mode we write the terminating zero, in one go
            fwrite(input.c_str(), 1, input.length(), out);
            if (oOutputFile.length())
                fclose(out);
            else
                fflush(stdout);
        } else if (oOutputFile.length())
            chart->Error.FatalError(opt_pos, "Failed to open output file '" + oOutputFile +"'.");
        else
            chart->Error.FatalError(opt_pos, "Internal error: no stdout.");
    } else if (oCsh || oTEIMode) {
        //All sorts of csh processing is done here.
        //Replace chart text with the cshized version of it
        MscInitializeCshAppearanceList();
        lang->pCsh->pShapes = &chart->Shapes; //contains all the shapes from the design libs
        //read the design libs
        if (oLoadDesigns) {
            for (auto &p : design_files) {
                size_t pos = p.first.find_last_of('.');
                if (pos != string::npos && lang->HasExtension(std::string_view(p.first).substr(pos+1)))
                    lang->pCsh->ParseDesignText(std::move(p.second));
            }
            lang->pCsh->CleanupAfterDesignlib();
        }
        bool first_pass = true;
        std::string prev_text; //the chart text from the previous call during tei mode
        do {
            //explicitly leave CRLF, CR and LF as is in input.
            if (input.length()>(size_t)std::numeric_limits<int>::max()-10) {
                string err = "The input '";
                err << oInputFile << "' is too long (" << input.length() << " bytes).";
                chart->Error.FatalError(opt_pos, err);
                err = "Error: Chart-generator cannot handle files longer than ";
                err << std::numeric_limits<int>::max()-10 << ".";
                chart->Error.Error(opt_pos, opt_pos, err);
                break;
            }
            bool fColors = false, fErrors = false, fHasText = true;
            CharByteIndex fHint = CharByteIndex::invalid(), fIndent = CharByteIndex::invalid(), fDeltaPos = CharByteIndex::invalid(), fDeltaIns;
            std::string first_line;
            if (oTEIMode) {
                //pick first line
                const char *s = input.c_str();
                while (*s && *s!='\n')
                    if (*s==' ' || *s=='\t' || *s=='\r') {
                        s++;
                        continue;
                    } else if (*s=='H' || *s=='I' || *s=='D') {
                        CharByteIndex &fCommand = *s=='H' ? fHint : *s=='D' ? fDeltaPos : fIndent;
                        int f = 0;
                        bool had = false;
                        s++;
                        while (*s==' ' || *s=='\t') s++;//skip spaces
                        while ('0'<=*s && *s<='9') {
                            had = true;
                            f = f*10 + (*s-'0');
                            s++;
                        }
                        if (had) {
                            fCommand.char_index = f;
                            if (&fCommand == &fDeltaPos) {
                                if (*s!=',')
                                    chart->Error.FatalError(opt_pos, "TEI first line error: Missing ',' after 'D<pos>' on first line.");
                                else {
                                    bool negative = *++s=='-';
                                    if (negative) s++;
                                    had = false;
                                    f = 0;
                                    while ('0'<=*s && *s<='9') {
                                        had = true;
                                        f = f*10 + (*s-'0');
                                        s++;
                                    }
                                    if (negative)
                                        f = -f;
                                    if (!had)
                                        chart->Error.FatalError(opt_pos, "TEI first line error: expected second number in stream before input file, after 'D<pos>,'.");
                                    else if (first_pass)
                                        fColors = true; //on the first pass a 'D' command is falls back to a 'C'
                                    else {
                                        fDeltaIns.char_index = f;
                                        fColors = false;
                                    }                                         
                                }
                            }
                        }
                        else
                            chart->Error.FatalError(opt_pos, "TEI first line error: expected number in stream before input file, after H or I.");
                    } else if (*s=='C') {
                        fColors = true; s++;
                    } else if (*s=='E') {
                        fErrors = true; s++;
                    } else if (*s=='N') {
                        if (first_pass)
                            chart->Error.FatalError(opt_pos, "TEI first line error: The first command after starting msc-gen in TEI mode must have chart text and cannot include 'N' on the first line of input.");
                        fHasText= false; s++;
                    } else {
                        chart->Error.FatalError(opt_pos, "TEI first line error: Expected one of 'CDEHIN' on the first line of input.");
                        s++;
                    }
                first_line = std::string(input.c_str(), s);
                if (*s=='\n') s++;
                if (!chart->Error.hasFatal()) { //modify input only if we have no error
                    //kill first line or re-use text from previous run
                    if (fHasText) 
                        input.erase(0, s - input.c_str());
                    //If fHasText is false, then 'chart_text_to_use' may have to be updated by a 'D' command.
                    //However, its part before 'fDeltaPos' is not impacted by such an update
                    const std::string_view chart_text_to_use = fHasText ? input : prev_text;
                    if (fDeltaPos.char_index >= 0) {
                        _ASSERT(!first_pass);
                        fDeltaPos.byte_index = GetUTF8ByteIndex(chart_text_to_use, fDeltaPos.char_index);
                        if (fDeltaPos.byte_index < 0)
                            chart->Error.FatalError(opt_pos, "TEI first line error: D<pos> indexes beyond the end of the file.");
                        //now calculate 'fDeltaIns.byte_index'
                        if (fDeltaIns.char_index) {
                            if (fHasText) {
                                fDeltaIns.byte_index += (int)input.size() - (int)prev_text.size();
                                if (!fDeltaIns.IsValidDiff())
                                    chart->Error.FatalError(opt_pos, "TEI delta text error: D<pos>,<insert> says you have inserted/deleted " + std::to_string(fDeltaIns.char_index)
                                                            + "characters, but the full chart you provided differs from the previous in " + std::to_string(fDeltaIns.byte_index) + " bytes of length.");
                            } else {
                                if (fDeltaIns.char_index > 0) {
                                    const std::string_view to_insert(s);
                                    if ((int)UTF8len(to_insert) != fDeltaIns.char_index)
                                        chart->Error.FatalError(opt_pos, "TEI delta text error: D<pos>,<insert> says you inserted " + std::to_string(fDeltaIns.char_index)
                                                                + " characters, but you provided " + std::to_string(UTF8len(to_insert)) + " characters after the first line.");
                                    else {
                                        fDeltaIns.byte_index = to_insert.size();
                                        prev_text.insert(fDeltaPos.byte_index, to_insert);
                                    }
                                } else {
                                    fDeltaIns.byte_index = -GetUTF8ByteIndex(std::string_view(prev_text).substr(fDeltaPos.byte_index), -fDeltaIns.char_index);
                                    if (0 < fDeltaIns.byte_index)
                                        chart->Error.FatalError(opt_pos, "TEI delta text error: bad UTF-8 at D<pos>.");
                                    else
                                        prev_text.erase(fDeltaPos.byte_index, -fDeltaIns.byte_index);
                                }
                            }
                        }
                    }
                    if (!fHasText)
                        input = std::move(prev_text);
                }
                //Now 'input' holds the new chart text
                if (!chart->Error.hasFatal()) {
                    //lookup the byte_indices
                    if (fHint.char_index >= 0) {
                        fHint.byte_index = GetUTF8ByteIndex(input, fHint.char_index);
                        if (fHint.byte_index < 0)
                            chart->Error.FatalError(opt_pos, "TEI first line error: H<pos> indexes beyond the end of the file.");
                    }
                    if (fIndent.char_index >= 0) {
                        fIndent.byte_index = GetUTF8ByteIndex(input, fIndent.char_index);
                        if (fIndent.byte_index < 0)
                            chart->Error.FatalError(opt_pos, "TEI first line error: I<pos> indexes beyond the end of the file.");
                    }
                }
            }
            if (chart->Error.hasFatal()) {
                chart->Error.Error(opt_pos, "The offending first line was: '"+first_line+"'.");
                break; //will stop and emit errors
            }
            CshListType prev_csh = std::move(lang->pCsh->CshList);
            lang->pCsh->ParseText(std::string(input), CharByteIndex{}, fHint, chart->GetPedantic());
            string output;
            constexpr int scheme = 1;
            if (oCsh)
                output = Cshize(input, lang->pCsh->CshList, scheme, csh_textformat);
            else {
                _ASSERT(oTEIMode);
                bool separator = false;
                CshListType csh_delta;
                CshListType *emit_csh = nullptr;
                //emit coloring
                if (fColors) {
                    lang->pCsh->CshList.SortByPos();
                    emit_csh = &lang->pCsh->CshList;
                } else if (fDeltaPos.IsValid()) {
                    //csh_list is unfortunately not always sorted by first_pos, but close.
                    lang->pCsh->CshList.SortByPos();
                    //Update last coloring with a shift in text.
                    CshPos::AdjustList(prev_csh, fDeltaPos, fDeltaIns);
                    //If we inserted, destroy any marking overlapping with the insertion.
                    if (0 < fDeltaIns.char_index)
                        for (auto &pos : prev_csh)
                            if (pos.first_pos <= fDeltaPos + fDeltaIns && fDeltaPos < pos.end_pos)
                                pos.color = COLOR_NORMAL;
                    //create a diff
                    csh_delta.DiffInto(prev_csh, lang->pCsh->CshList, COLOR_NORMAL);
                    emit_csh = &csh_delta;
                }
                if (emit_csh) {
                    for (const auto &h : *emit_csh) {
                        std::string tmp(std::to_string(h.first_pos.char_index));
                        tmp.push_back('-');
                        tmp.append(std::to_string(h.end_pos.char_index)); 
                        tmp.push_back(':');
                        tmp.append(std::to_string(h.color));
                        tmp.push_back(' ');
                        tmp.append(MscCshAppearanceList[scheme][h.color].Print());
                        tmp.push_back('\n');
                        output.append(tmp);
                    }
                    //emit a separator
                    output.push_back('\n');
                    separator = true;
                }
                //emit errors
                if (fErrors) {
                    for (const auto &h : lang->pCsh->CshErrors.error_ranges) {
                        std::string tmp(std::to_string(h.first_pos.char_index));
                        tmp.push_back('-');
                        tmp.append(std::to_string(h.end_pos.char_index));
                        tmp.push_back(':');
                        tmp.append(h.text);
                        tmp.push_back('\n');
                        output.append(tmp);
                    }
                    //emit a separator
                    output.push_back('\n');
                    separator = true;
                }
                //emit hints
                if (fHint.char_index>=0) {
                    //we requested hints at a particular location
                    Canvas canvas(Canvas::Empty::Query);
                    lang->pCsh->ProcessHints(canvas, nullptr, {}, EHintFilter::None, false);
                    if (lang->pCsh->allow_anything && lang->pCsh->Hints.size())
                        output.append("*\01\01" "0\01" "0,0,0\01" "0\01" "1\01\n"); //indicate anything
                    for (const auto &h : lang->pCsh->Hints) {
                        const StringFormat sf(h.decorated);
                        std::string descr = h.description;
                        std::ranges::replace(descr, '\n', '\02');
                        const ColorType color = sf.color.value_or(ColorType::black());
                        output.append(StrCat(
                            h.plain, '\01',
                            h.replaceto, '\01',
                            h.selectable ? "1\01" : "0\01",
                            std::to_string(color.r), ',',
                            std::to_string(color.g), ',',
                            std::to_string(color.b), '\01',
                            sf.bold.value_or(ETriState::no)==ETriState::no ? "0\01" : "1\01",
                            sf.italics.value_or(ETriState::no)==ETriState::no ? "0\01" : "1\01",
                            descr, '\n'
                        ));
                    }
                    //emit a separator
                    output.push_back('\n');
                    separator = true;
                }
                //emit indent info
                if (fIndent.char_index>=0) {
                    CharByteIndex line_begin;
                    int current_indent;
                    int indent = lang->pCsh->FindProperLineIndent(fIndent, false, &current_indent, &line_begin);
                    if (current_indent<0) {
                        //count how many spaces in an empty line
                        for (current_indent=0;
                             lang->pCsh->input_text[line_begin.byte_index+current_indent]==' ' ||
                             lang->pCsh->input_text[line_begin.byte_index+current_indent]=='\t';
                             current_indent++) {}
                    }

                    output.append(std::to_string(line_begin.char_index));
                    output.push_back(' ');
                    output.append(std::to_string(line_begin.byte_index));
                    output.push_back(' ');
                    output.append(std::to_string(current_indent));
                    output.push_back(' ');
                    output.append(std::to_string(indent));
                    output.push_back('\n');
                    //dont emit a separator, we are last in any case
                    separator = false;
                }
                if (separator)
                    output.pop_back();
            }
            FILE* const out = oOutputFile.length() ? OpenNamedFile(oOutputFile.c_str(), true, false) : stdout;
            if (out) {
                //For TEI mode we write the terminating zero, in one go
                fwrite(output.c_str(), 1, output.length()+(oTEIMode?1:0), out);
                if (oOutputFile.length())
                    fclose(out);
                else
                    fflush(stdout);
            } else if (oOutputFile.length())
                chart->Error.FatalError(opt_pos, "Failed to open output file '" + oOutputFile +"'.");
            else
                chart->Error.FatalError(opt_pos, "Internal error: no stdout.");
            if (oTEIMode) {
                if (feof(stdin)) break;
                prev_text = std::move(input); //save input chart text for potential next run.
                input = ReadFile(stdin, true, false);
                if (input.length()==0) break;
                first_pass = false;
            }
        } while (oTEIMode && !chart->Error.hasFatal());
    } else {
        //Here either we have a valid graphics output format (or ISMAP or PPT) or we have oLmap==true
        chart->DeserializeGUIState(gui_state); //silently ignore any error in GUI state
        //parse input text;
        try {
            chart->ParseText(input, oInputFile);
        }
        catch (...) {
            chart->Error.FatalError(FileLineCol(), "Internal error: Input parser crashed.",
                "Try altering your chart. "
                "Consider submitting this as a bug at https://gitlab.com/msc-generator/msc-generator/-/issues");
            goto fatal;
        }
        XY pageSize(0,0);
        if (oPageSize == EPageSize::NO_PAGE)
            oPageSize = oOutType==Canvas::PPT ? DEFAULT_PPT_PAGE_SIZE : EPageSize::A4P;
        if (oA) {
            _ASSERT(oPageSize); //should have cleared oA if we have no page size
            pageSize = GetPhysicalPageSize(oPageSize.value());
            pageSize.x -= margins[0] + margins[1];
            pageSize.y -= margins[2] + margins[3];
            if (oScale.size() && oScale[0]>0)
                pageSize /= oScale[0];
            if (pageSize.x<10 || pageSize.y<10)
                oA=false;
        }
        try {
            chart->CompleteParse(oA, oAH, pageSize, oScale.size() ? oScale[0]==-1 : false);
        } catch (...) {
            chart->Error.FatalError(FileLineCol(), "Internal error: Layouting has crashed.",
                "Try altering your chart. "
                "Consider submitting this as a bug at https://gitlab.com/msc-generator/msc-generator/-/issues");
            goto fatal;
        }

        //Determine scaling
        std::vector<XY> scale(std::max(std::vector<XY>::size_type(1), oScale.size()), XY(1., 1.));
        //MscChart specific stuff, ugly, but hard to change
        if (oUseWidthAttr)
            oX = (int)chart->GetWidthAttr(); //nothing happens if equals -1
        if (oX>0 || oY>0) {
            if (oScale.size())
                chart->Error.Error(opt_pos, "Conflicting scaing options. Use either -s or one/both of -x/-y. Using no scaling.");
            else if (oX>0 && oY>0) {
                scale[0].x = double(oX)/double(chart->GetTotal().x.Spans());
                scale[0].y = double(oY)/double(chart->GetTotal().y.Spans());
            } else if (oX>0)
                scale[0].x = scale[0].y = double(oX)/double(chart->GetTotal().x.Spans());
            else if (oY>0)
                scale[0].x = scale[0].y = double(oY)/double(chart->GetTotal().y.Spans());
        } else if (oScale.size()==1)  //one specified
            scale[0].x = scale[0].y = oScale[0];
        else if (oScale.size()>1) { //multiple specified
            if (!oPageSize.has_value()) {
                for (unsigned u = 0; u<oScale.size(); u++)
                    if (oScale[u]>0) {
                        oScale[0] = oScale[u];
                        break;
                    }
                oScale.resize(1);
                string s = "Multiple -s options are valid for full-page output (-p). Using scale of ";
                s << oScale[0] << ".";
                chart->Error.Error(opt_pos, s);
            } else
                for (unsigned u = 0; u<oScale.size(); u++)
                    scale[u].x = scale[u].y = oScale[u];
        }

        if (oOutputFile.empty() && !oLmap) { //to stdout
            //We cannot write to standard output a multi-page file, unless -p.
            if (chart->GetPageNum()>1 && !oPageSize.has_value()) {
                chart->Error.Error(opt_pos, "Cannot write multiple files (one per page) to the standard output.");
                goto fatal;
            }
            if (oOutType==Canvas::EMF || oOutType==Canvas::WMF || oOutType==Canvas::EMFWMF) {
                chart->Error.Error(opt_pos, "Cannot write .emf and .wmf files to the standard output.");
                goto fatal;
            }
        }
        const auto pv = chart->GetPageVector();
        const XY scale_to_use = Canvas::DetermineScaling(chart->GetTotal(), scale,
            GetPhysicalPageSize(oPageSize.value_or(EPageSize::NO_PAGE)), margins,
            chart->GetCopyrightTextHeight(), &pv);
        if (oLmap) {
            //Register all the labels
            chart->RegisterAllLabels();
            FILE* const fout = oOutputFile.length() ? OpenNamedFile(oOutputFile.c_str(), true, true) : stdout;
            chart->labelData.sort([](const LabelInfo &a, const LabelInfo &b) {return a.coord.y.from<b.coord.y.from; });
            for (const auto &l : chart->labelData) {
                //check page number, step until upper left corner of label is on a page
                unsigned p = 0;
                while (chart->GetPageNum() > p &&
                       !Block(chart->GetPageData(p)->xy, chart->GetPageData(p)->xy + chart->GetPageData(p)->wh)
                            .IsWithinBool(l.coord.UpperLeft()))
                    p++;
                _ASSERT(chart->GetPageNum() > p);
                if (chart->GetPageNum()<=p)
                    continue;
                //p is now the page number indexed from 0.
                auto pb = chart->GetPageData(p);
                _ASSERT(pb);
                Block b(l.coord);
                //Shift the coordinates to compensate with where the page starts
                b.Shift(XY(-chart->GetTotal().x.from, -pb->xy.y + pb->headingLeftingSize.y));
                //scale with the requested user scaling & round to integers
                b.Scale(scale_to_use).RoundWider();
                //Get the first line
                const string first_line = l.text.substr(0, l.text.find_first_of('\n'));
                //emit line
                //T P X1 Y1 X2 Y2 S
                //type page bb first line
                fprintf(fout, "%c %d %g %g %g %g %s\n",
                              LabelInfo::labelTypeChar[l.type], p,
                              b.x.from, b.y.from, b.x.till, b.y.till,
                              first_line.c_str());
            }
            fclose(fout);
        } else {
            //Now cycle through pages and write them to individual files or a full-page one
            try {
                //Draw all chart on a single PNG when embedding into a PPT, but show page breaks
                const bool ignore_pagebreaks = oEmbed.has_value();
                const bool show_pagebreaks = oEmbed.has_value();
                chart->DrawToFile(oOutType, scale_to_use, oOutputFile, show_pagebreaks, ignore_pagebreaks,
                                  oIncludeChartText ? input.c_str() : nullptr, 0,
                                  GetPhysicalPageSize(oPageSize.value_or(EPageSize::NO_PAGE)),
                                  margins, oHA, oVA, true);
            }
            catch (...) {
                chart->Error.FatalError(FileLineCol(), "Internal error: Drawing the chart crashed.",
                    "Try altering your chart. "
                    "Consider submitting this as a bug at https://gitlab.com/msc-generator/msc-generator/-/issues");
                goto fatal;
            }

            //Embed the resulting chart in a PPT
            if (oEmbed) {
                //The total chart size with all headers, footers, etc. We assume we draw the whole chart.
                unsigned page_idx = oEmbed->first, ch_idx = oEmbed->second;
                EmbedChartData data{.chart_type=lang->GetName(), .chart_text=std::move(input), .chart_size=chart->GetCanvasSize()};
                //'input' is invalid from here
                std::string error =
                    PptFile::add_image(ZipArchive::FromFile(saved_ppt_outfile), oOutputFile, page_idx, ch_idx,
                                       std::move(data), OpenNamedFile);
                if (error.size())
                    chart->Error.FatalError(FileLineCol(), "Could not write to PPT: "+error);
                else if (oPrintSuccess && oEmbed->second==0)
                    std::cerr << "Added new chart "<<saved_ppt_outfile<<":"<<page_idx<<":"<<ch_idx<<std::endl;
                std::error_code ec;
                std::filesystem::remove(oOutputFile, ec);
            }

            chart->GetProgress()->Done();
            if (load_data) {
                ldset[lang->GetName()] = chart->GetProgress()->WriteLoadData();
                *load_data = PackLoadData(ldset);
            }
        }
    }

fatal:
    if (!oPaths)
        chart->Error.RemovePathFromErrorMessages();
    std::cerr << chart->Error.Print(oWarning, oTechnicalInfo);
    if (show_usage)
        std::cerr<<"Use 'msc-gen --help' for full command-line and options syntax."<<std::endl;

    if (chart->Error.hasFatal()) {
        std::cerr << "Fatal Error, bailing out." << std::endl;
    } else if (chart->Error.hasErrors()) {
        std::cerr << "There were errors, but ";
        if (oLmap || oOutType == Canvas::ISMAP)
            std::cerr << "a map";
        else if (oCsh)
            std::cerr << "the colorized chart";
        else
            std::cerr << "a chart";
        std::cerr << " has been produced." << std::endl;
    } else if (oPrintSuccess)
        std::cerr << "Success." << std::endl;
    return EXIT_SUCCESS;
}

