/**
 * @file mcd.cpp
 * @author Christian Schenk
 * @brief Main program
 *
 * @copyright Copyright © 1998-2024 Christian Schenk
 *
 * This file is part of MiKTeX Compiler Driver.
 *
 * MiKTeX Compiler Driver is licensed under GNU General Public License version 2
 * or any later version.
 */

/* This program imitates the shell script texi2dvi, which is a part of the GNU
   Texinfo package.  Texi2dvi was originally written by Noah Friedman.

   Most of the source comments are stolen from the texi2dvi script. */

#if defined(MIKTEX_WINDOWS)
#include <windows.h>
#endif

#include <cctype>
#include <cstdlib>

#include <algorithm>
#include <iostream>
#include <memory>
#include <vector>

#include "mcd-version.h"

#include <miktex/Core/BufferSizes>
#include <miktex/Core/CommandLineBuilder>
#include <miktex/Core/Directory>
#include <miktex/Core/DirectoryLister>
#include <miktex/Core/Environment>
#include <miktex/Core/Exceptions>
#include <miktex/Core/File>
#include <miktex/Core/FileStream>
#include <miktex/Core/FileType>
#include <miktex/Core/MemoryMappedFile>
#include <miktex/Core/Paths>
#include <miktex/Core/Process>
#include <miktex/Core/StreamReader>
#include <miktex/Core/StreamWriter>
#include <miktex/Core/TemporaryDirectory>
#include <miktex/Trace/Trace>
#include <miktex/Trace/TraceStream>
#include <miktex/Util/StringUtil>
#include <miktex/Util/inliners.h>
#include <miktex/Wrappers/PoptWrapper>

#if defined(MIKTEX_WINDOWS)
#include <miktex/Core/win/ConsoleCodePageSwitcher>
#endif

#include <fmt/format.h>
#include <fmt/ostream.h>

#include <regex.h>

#include "mcd.h"

using namespace std;
using namespace std::string_literals;

using namespace MiKTeX::App;
using namespace MiKTeX::Core;
using namespace MiKTeX::Trace;
using namespace MiKTeX::Util;
using namespace MiKTeX::Wrappers;

vector<string> DEFAULT_TRACE_OPTIONS = {
    TraceStream::MakeOption("", "", TraceLevel::Info),
    TraceStream::MakeOption(PROGRAM_NAME, "", TraceLevel::Trace),
};

void CopyFiles(const vector<string>& fileNames, const PathName& destDir)
{
    for (const string& fileName : fileNames)
    {
        File::Copy(PathName(fileName), destDir / fileName);
    }
}

vector<char> ReadFile(const PathName& fileName)
{
    size_t fileSize = File::GetSize(fileName);
    vector<char> vec(fileSize + 1);
    FileStream stream(File::Open(fileName, FileMode::Open, FileAccess::Read));
    fileSize = stream.Read(&vec[0], fileSize);
    stream.Close();
    std::replace(vec.begin(), vec.end(), '\0', '\t');
    vec[fileSize] = 0;
    return vec;
}

bool Contains(const PathName& fileName, regex_t* preg)
{
    vector<char> file = ReadFile(fileName);
    return regexec(preg, &file[0], 0, nullptr, 0) == 0;
}

bool Contains(const PathName& fileName, const char* text)
{
    unique_ptr<MemoryMappedFile> mmapFile(MemoryMappedFile::Create());
    const char* ptr = reinterpret_cast<char*>(mmapFile->Open(fileName, false));
    size_t size = mmapFile->GetSize();
    const char* p = text;
    for (size_t i = 0; *p != 0 && i < size; ++i, ++ptr)
    {
        if (*ptr == *p)
        {
            ++p;
        }
        else
        {
            p = text;
        }
    }
    mmapFile->Close();
    return *p == 0;
}

string FlattenStringVector(const vector<string>& vec, char sep)
{
    string str = "";
    for (const string& s : vec)
    {
        if (!str.empty())
        {
            str += sep;
        }
        bool mustQuote = (s.find(sep) != string::npos);
        if (mustQuote)
        {
            str += '"';
        }
        str += s;
        if (mustQuote)
        {
            str += '"';
        }
    }
    return str;
}

Options::Options()
{
    if (regcomp(&regex_bibdata, "^\\\\bibdata", REG_NOSUB | REG_EXTENDED | REG_NEWLINE) != 0)
    {
        MIKTEX_UNEXPECTED();
    }

    if (regcomp(&regex_bibstyle, "^\\\\bibstyle", REG_NOSUB | REG_EXTENDED | REG_NEWLINE) != 0)
    {
        MIKTEX_UNEXPECTED();
    }

    if (regcomp(&regex_citation_undefined, "Warning:.*Citation.*undefined", REG_NOSUB | REG_EXTENDED | REG_NEWLINE) != 0)
    {
        MIKTEX_UNEXPECTED();
    }

    if (regcomp(&regex_no_file_bbl, "No file .*\\.bbl\\.", REG_NOSUB | REG_EXTENDED | REG_NEWLINE) != 0)
    {
        MIKTEX_UNEXPECTED();
    }

#if defined(WITH_TEXINFO)
    if (regcomp(&regex_texinfo_version, "\\[(.*)version (....)-(..)-(..)", REG_EXTENDED | REG_NEWLINE) != 0)
    {
        MIKTEX_UNEXPECTED();
    }
#endif

#if defined(SF464378__CHAPTERBIB)
    if (regcomp(&regex_chapterbib, "^Package: chapterbib", REG_NOSUB | REG_EXTENDED | REG_NEWLINE) != 0)
    {
        MIKTEX_UNEXPECTED();
    }

    if (regcomp(&regex_input_aux, "^\\\\@input\\{.*\\.aux\\}", REG_EXTENDED | REG_NEWLINE) != 0)
    {
        MIKTEX_UNEXPECTED();
    }
#endif

    startDirectory.SetToCurrentDirectory();
    startDirectory.ConvertToUnix();

    bibtexProgram = SetProgramName("BIBTEX", MIKTEX_BIBTEX_EXE);
    latexProgram = SetProgramName("LATEX", "latex");
    makeindexProgram = SetProgramName("MAKEINDEX", MIKTEX_MAKEINDEX_EXE);
    makeinfoProgram = SetProgramName("MAKEINFO", "makeinfo");
    pdflatexProgram = SetProgramName("PDFLATEX", "pdflatex");
    pdftexProgram = SetProgramName("PDFTEX", MIKTEX_PDFTEX_EXE);
    ptexProgram = SetProgramName("PTEX", MIKTEX_EPTEX_EXE);
    eptexProgram = SetProgramName("EPTEX", MIKTEX_EPTEX_EXE);
    euptexProgram = SetProgramName("EUPTEX", MIKTEX_EUPTEX_EXE);
    uptexProgram = SetProgramName("UPTEX", MIKTEX_EUPTEX_EXE);
    texProgram = SetProgramName("TEX", MIKTEX_TEX_EXE);
    texindexProgram = SetProgramName("TEXINDEX", "texindex");
    xelatexProgram = SetProgramName("XELATEX", "xelatex");
    lualatexProgram = SetProgramName("LUALATEX", "lualatex");
    luahblatexProgram = SetProgramName("LUAHBLATEX", "luahblatex");
    xetexProgram = SetProgramName("XETEX", MIKTEX_XETEX_EXE);
    luatexProgram = SetProgramName("LUATEX", MIKTEX_LUATEX_EXE);
    luahbtexProgram = SetProgramName("LUAHBTEX", MIKTEX_LUAHBTEX_EXE);
}

Options::~Options()
{
    regfree(&regex_bibdata);
    regfree(&regex_bibstyle);
    regfree(&regex_citation_undefined);
    regfree(&regex_no_file_bbl);
#if defined(WITH_TEXINFO)
    regfree(&regex_texinfo_version);
#endif
#if defined(SF464378__CHAPTERBIB)
    regfree(&regex_chapterbib);
    regfree(&regex_input_aux);
#endif
}

void McdApp::Verbose(const string& s)
{
    traceStream->WriteLine(PROGRAM_NAME, s);
    if (options.verbose)
    {
        cout << s << "\n";
    }
}

void McdApp::MyTrace(const string& s)
{
    traceStream->WriteLine(PROGRAM_NAME, s);
}

void McdApp::Version()
{
    cout
        << Utils::MakeProgramVersionString(THE_NAME_OF_THE_GAME, VersionNumber(MIKTEX_COMPONENT_VERSION_STR)) << "\n"
        << "\n"
        << MIKTEX_COMP_COPYRIGHT_STR << "\n"
        << "This is free software; see the source for copying conditions.  There is NO" << "\n"
        << "warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." << endl;
}

Driver::~Driver()
{
    try
    {
        if (options != nullptr && options->clean)
        {
            Directory::SetCurrent(options->startDirectory);
        }
        if (tempDirectory != nullptr)
        {
            tempDirectory->Delete();
        }
    }
    catch (const exception&)
    {
    }
}

void Driver::Initialize(McdApp* app, Options* options, const char* fileName)
{
    this->app = app;
    this->options = options;

    app->MyTrace(T_("initializing driver..."));

    givenFileName = fileName;

    // Get the name of the current directory.  We want the full path because in
    // clean mode we are in tmp, in which case a relative path has no meaning.
    originalInputFile = givenFileName;
    originalInputFile.MakeFullyQualified();
    originalInputFile.ConvertToUnix();

    app->MyTrace(fmt::format(T_("input file: {}"), Q_(originalInputFile)));

    if (options->jobName.empty())
    {
        jobName = givenFileName.GetFileNameWithoutExtension();
    }
    else
    {
        jobName = options->jobName;
    }

    inputName = givenFileName;
    inputName.RemoveDirectorySpec();

    // create a super-temp directory
    tempDirectory = TemporaryDirectory::Create();

    // create scratch directory
    workingDirectory = tempDirectory->GetPathName() / "_src";
    Directory::Create(workingDirectory);
    workingDirectory.ConvertToUnix();
    app->MyTrace(fmt::format(T_("working directory: {}"), Q_(workingDirectory)));

#if defined(WITH_TEXINFO)
    // create extra directory
    extraDirectory = tempDirectory->GetPathName() / "_xtr";
    Directory::Create(extraDirectory);
    extraDirectory.ConvertToUnix();
    app->MyTrace(fmt::format(T_("extra directory: {}"), Q_(extraDirectory)));
#endif

    // create aux directory
    auxDirectory = tempDirectory->GetPathName() / "_aux";
    Directory::Create(auxDirectory);
    auxDirectory.ConvertToUnix();
    app->MyTrace(fmt::format(T_("aux directory: {}"), Q_(auxDirectory)));

    // If the user explicitly specified the language, use that. Otherwise, if
    // the first line is \input texinfo, assume it's texinfo.  Otherwise, guess
    // from the file extension.
    macroLanguage = options->macroLanguage;
    if (macroLanguage == MacroLanguage::None)
    {
        macroLanguage = GuessMacroLanguage(givenFileName);
    }

    // make fully qualified path to the given input file
    if (givenFileName.IsAbsolute())
    {
        pathInputFile = givenFileName;
    }
    else
    {
        pathInputFile = options->startDirectory / givenFileName.ToString();
    }

    originalInputDirectory = pathInputFile;
    originalInputDirectory.RemoveFileSpec();
    originalInputDirectory.ConvertToUnix();
}

MacroLanguage Driver::GuessMacroLanguage(const PathName& fileName)
{
    StreamReader reader(fileName);
    string firstLine;
    if (!reader.ReadLine(firstLine))
    {
        return MacroLanguage::None;
    }
    reader.Close();
#if defined(WITH_TEXINFO)
    if (firstLine.find("input texinfo") != string::npos)
    {
        return MacroLanguage::Texinfo;
    }
#endif
    if (fileName.HasExtension(".dtx") || fileName.HasExtension(".tex") || fileName.HasExtension(".latex") || fileName.HasExtension(".ltx"))
    {
        return MacroLanguage::LaTeX;
    }
    else
    {
#if defined(WITH_TEXINFO)
        return MacroLanguage::Texinfo;
#else
        return MacroLanguage::None;
#endif
    }
}

/* _________________________________________________________________________

   Driver::SetIncludeDirectories

   Source file might include additional sources.  Put `.' and directory where
   source file(s) reside in MIKTEX_CWD before anything else.  Include orig_pwd
   in case we are in clean mode, where we've cd'd to a temp directory.
   _________________________________________________________________________ */

void Driver::SetIncludeDirectories()
{
    session->AddInputDirectory(options->startDirectory, true);
    if (originalInputDirectory != options->startDirectory)
    {
        session->AddInputDirectory(originalInputDirectory, true);
    }
    for (const string& dir : options->includeDirectories)
    {
        session->AddInputDirectory(PathName(dir), true);
    }
}

/* _________________________________________________________________________

   Driver::RunMakeinfo

   Expand macro commands in the original source file using Makeinfo. Always use
   `end' footnote style, since the `separate' style generates different output
   (arguably this is a bug in -E).  Discard main info output, the user asked to
   run TeX, not makeinfo.
   _________________________________________________________________________ */

bool Driver::RunMakeinfo(const PathName& pathFrom, const PathName& pathTo)
{
    PathName pathExe;

    if (!session->FindFile(options->makeinfoProgram, FileType::EXE, pathExe))
    {
        FatalUtilityError(options->makeinfoProgram);
    }

    vector<string> args{ options->makeinfoProgram };

    args.push_back("--footnote-style="s + "end");
    args.push_back("-I");
    args.push_back(originalInputDirectory.ToString());

    for (const string& dir : options->includeDirectories)
    {
        args.push_back("-I");
        args.push_back(dir);
    }

    args.push_back("-o");
#if defined(MIKTEX_WINDOWS)
    args.push_back("nul");
#else
    args.push_back("/dev/null");
#endif

    args.push_back("--macro-expand="s + pathTo.ToString());

    args.push_back(pathFrom.ToString());

    int exitCode = 0;

    ProcessOutputTrash trash;
    Process::Run(pathExe, args, &trash, &exitCode, nullptr);

    return exitCode == 0;
}

void Driver::ExpandMacros()
{
#if defined(WITH_TEXINFO)
    if (macroLanguage != MacroLanguage::Texinfo)
    {
        return;
    }
#else
    return;
#endif

#if defined(WITH_TEXINFO)
    // Unless required by the user, makeinfo expansion is wanted only if
    // texinfo.tex is too old.
    bool expand = options->expand;
    if (!expand)
    {
        expand = Check_texinfo_tex();
    }
    if (!expand)
    {
        return;
    }

    PathName pathTmpFile1;
    PathName pathTmpFile2;

    pathTmpFile1.SetToTempFile();
    pathTmpFile2.SetToTempFile();

    PathName path(workingDirectory / inputName.ToString());

    app->Verbose(fmt::format(T_("macro-expanding {} to {}..."), Q_(givenFileName), Q_(path)));

    TexinfoPreprocess(originalInputFile, pathTmpFile1);

    bool expanded = RunMakeinfo(pathTmpFile1, pathTmpFile2);

    TexinfoUncomment(pathTmpFile2, path);

    File::Delete(pathTmpFile1);
    File::Delete(pathTmpFile2);

    if (expanded && File::Exists(path))
    {
        pathInputFile = path;
        pathInputFile.ConvertToUnix();
    }
    else
    {
        // If makeinfo failed (or was not even run), use the original file as
        // input.
        app->Verbose(fmt::format(T_("reverting to {}..."), Q_(givenFileName)));
    }
#endif
}

/* _________________________________________________________________________

   Driver::InsertCommands

   Used most commonly for @finalout, @smallbook, etc.
   _________________________________________________________________________ */

void Driver::InsertCommands()
{
#if defined(WITH_TEXINFO)
    if (!(options->texinfoCommands.size() > 0 && macroLanguage == MacroLanguage::Texinfo))
    {
        return;
    }
    string extra = FlattenStringVector(options->texinfoCommands, '\n');
    app->Verbose(fmt::format(T_("inserting extra commands: {}"), extra));
    PathName path(extraDirectory / inputName.ToString());
    StreamWriter writer(path);
    bool inserted = false;
    StreamReader reader(pathInputFile);
    string line;
    while (reader.ReadLine(line))
    {
        writer.WriteLine(line);
        if (!inserted && IsPrefixOf("@setfilename", line))
        {
            writer.WriteLine(extra);
            inserted = true;
        }
    }
    writer.Close();
    reader.Close();
    pathInputFile = path;
    pathInputFile.ConvertToUnix();
#endif
}

/* _________________________________________________________________________

   Driver::RunBibTeX

   Run bibtex on current file:
   - If its input (AUX) exists.
   - If AUX contains both '\bibdata' and '\bibstyle'.
   - If some citations are missing (LOG contains 'Citation') or the LOG
     complains of a missing .bbl.

   We run bibtex first, because I can see reasons for the indexes to change
   after bibtex is run, but I see no reason for the converse.

   Don't try to be too smart.  Running bibtex only if the bbl file exists and is
   older than the LaTeX file is wrong, since the document might include files
   that have changed.  Because there can be several AUX (if there are
   \include's), but a single LOG, looking for missing citations in LOG is
   easier, though we take the risk to match false messages.
   _________________________________________________________________________ */

void Driver::RunBibTeX()
{
    PathName pathExe;

    if (!session->FindFile(options->bibtexProgram, FileType::EXE, pathExe))
    {
        FatalUtilityError(options->bibtexProgram);
    }

    PathName logName(jobName);
    logName.AppendExtension(".log");
    PathName auxName(jobName);
    auxName.AppendExtension(".aux");

    int exitCode;

#if defined(SF464378__CHAPTERBIB)
    if ((File::Exists(auxName) && File::Exists(logName) && Contains(logName, &options->regex_chapterbib) && (Contains(logName, &options->regex_citation_undefined) || Contains(logName, &options->regex_no_file_bbl))))
    {
        app->Verbose(T_("ChapterBib detected. Preparing to run BibTeX on first-level aux files..."));

        // read the main .aux file
        vector<char> auxFile = ReadFile(auxName);

        regmatch_t regMatch[2];

        for (int offset = 0; ((regexec(&options->regex_input_aux, &auxFile[offset], 1, regMatch, offset != 0 ? REG_NOTBOL : 0) == 0) && (regMatch[0].rm_so > -1)); offset += regMatch[0].rm_eo)
        {
            // get SubAuxNameNoExt out of \@input{SubAuxNameNoExt.aux}
            string tmp(&auxFile[offset + regMatch[0].rm_so + 8], regMatch[0].rm_eo - regMatch[0].rm_so - 13);
            PathName subAuxNameNoExt(tmp);

            // append .aux extension
            PathName subAuxName(subAuxNameNoExt);
            subAuxName.AppendExtension(".aux");
            if (!(File::Exists(subAuxName) && Contains(subAuxName, &options->regex_bibdata) && Contains(subAuxName, &options->regex_bibstyle)))
            {
                continue;
            }

            PathName subDir;

            if (strchr(subAuxNameNoExt.GetData(), PathNameUtil::UnixDirectoryDelimiter) != 0)
            {
                // we have \@input{SubDir/SubAuxNameNoExt.aux}
                if (options->clean)
                {
                    MIKTEX_FATAL_ERROR(T_("Sub-directories not supported when --clean is in effect."));
                }
                subDir = subAuxNameNoExt;
                subDir.RemoveFileSpec();
                subDir.MakeFullyQualified();
                subAuxNameNoExt.RemoveDirectorySpec();
            }

            vector<string> args{ options->bibtexProgram };

            args.push_back(subAuxNameNoExt.ToString());

            app->Verbose(fmt::format(T_("running {}..."), CommandLineBuilder(args).ToString()));

            exitCode = 0;

            ProcessOutputTrash trash;

            Process::Run(pathExe, args, (options->quiet ? &trash : nullptr), &exitCode, subDir.Empty() ? nullptr : subDir.GetData());
            if (exitCode != 0)
            {
                MIKTEX_FATAL_ERROR(T_("BibTeX failed for some reason."));
            }
        }
    }
#endif // SF464378__CHAPTERBIB

    if (!(File::Exists(auxName) && Contains(auxName, &options->regex_bibdata) && Contains(auxName, &options->regex_bibstyle) && File::Exists(logName) && (Contains(logName, &options->regex_citation_undefined) || Contains(logName, &options->regex_no_file_bbl))))
    {
        return;
    }

    vector<string> args{ options->bibtexProgram };

    args.push_back(jobName.ToString());

    app->Verbose(fmt::format(T_("running {}..."), CommandLineBuilder(args).ToString()));

    ProcessOutputTrash trash;

    Process::Run(pathExe, args, (options->quiet ? &trash : nullptr), &exitCode, nullptr);

    if (exitCode != 0)
    {
        MIKTEX_FATAL_ERROR(T_("BibTeX failed for some reason."));
    }
}

/* _________________________________________________________________________

   Driver::RunIndexGenerator

   Run texindex (or makeindex) on current index files.  If they already exist,
   and after running TeX a first time the index files don't change, then there's
   no reason to run TeX again.  But we won't know that if the index files are
   out of date or nonexistent.
   _________________________________________________________________________ */

void Driver::RunIndexGenerator(const vector<string>& idxFiles)
{
#if defined(WITH_TEXINFO)
    const string indexGenerator = macroLanguage == MacroLanguage::Texinfo
        ? options->texindexProgram
        : options->makeindexProgram;
#else
    const string indexGenerator = options->makeindexProgram;
#endif

    PathName pathExe;

    if (!session->FindFile(indexGenerator, FileType::EXE, pathExe))
    {
        FatalUtilityError(indexGenerator);
    }

    vector<string> args{ indexGenerator };

    args.insert(args.end(), options->makeindexOptions.begin(), options->makeindexOptions.end());
    args.insert(args.end(), idxFiles.begin(), idxFiles.end());

    ProcessOutputTrash trash;

    int exitCode = 0;

    app->Verbose(fmt::format(T_("running {}..."), CommandLineBuilder(args).ToString()));

    Process::Run(pathExe, args, (options->quiet ? &trash : nullptr), &exitCode, nullptr);

    if (exitCode != 0)
    {
        MIKTEX_FATAL_ERROR(T_("MakeIndex failed for some reason."));
    }
}

void Driver::InstallProgram(const char* program)
{
    ALWAYS_UNUSED(program);
    PathName pathExe;
    if (!session->FindFile("miktex", FileType::EXE, pathExe))
    {
        FatalUtilityError("miktex");
    }
    ProcessOutputTrash trash;
    Process::Run(pathExe, vector<string>{"miktex", "links", "install"}, (options->quiet ? &trash : nullptr));
}

PathName Driver::GetTeXEnginePath(string& exeName)
{
#if defined(WITH_TEXINFO)
    if (macroLanguage == MacroLanguage::Texinfo)
    {
        if (options->outputType == OutputType::PDF)
        {
            if (options->engine == Engine::XeTeX)
            {
                exeName = options->xetexProgram;
            }
            else if (options->engine == Engine::LuaTeX)
            {
                exeName = options->luatexProgram;
            }
            else
            {
                exeName = options->pdftexProgram;
            }
        }
        else
        {
            if (options->engine == Engine::pdfTeX)
            {
                exeName = options->pdftexProgram;
            }
            else
            {
                exeName = options->texProgram;
            }
        }
    }
    else
#endif
    {
        if (options->outputType == OutputType::PDF)
        {
            if (options->engine == Engine::XeTeX)
            {
                exeName = options->xelatexProgram;
            }
            else if (options->engine == Engine::LuaTeX)
            {
                exeName = options->lualatexProgram;
            }
            else if (options->engine == Engine::LuaHBTeX)
            {
                exeName = options->luahblatexProgram;
            }
            else
            {
                exeName = options->pdflatexProgram;
            }
        }
        else
        {
            exeName = options->latexProgram;
        }
    }
    PathName pathExe;
    if (!session->FindFile(exeName, FileType::EXE, pathExe))
    {
        InstallProgram(exeName.c_str());
        if (!session->FindFile(exeName, FileType::EXE, pathExe))
        {
            MIKTEX_FATAL_ERROR(T_("The TeX engine could not be found."));
        }
    }
    return pathExe;
}

void Driver::RunTeX()
{
    string exeName;
    PathName pathExe = GetTeXEnginePath(exeName);

    vector<string> args{ pathExe.GetFileNameWithoutExtension().ToString() };

    if (!options->jobName.empty())
    {
        args.push_back("--job-name="s + jobName.ToString());
    }

#if defined(SUPPORT_OPT_SRC_SPECIALS)
    if (options->sourceSpecials)
    {
        if (!options->sourceSpecialsWhere.empty())
        {
            args.push_back("--src-specials="s + options->sourceSpecialsWhere);
        }
        else
        {
            args.push_back("--src-specials");
        }
    }
#endif

    if (options->synctex != SyncTeXOption::Disabled)
    {
        args.push_back("--synctex="s + (options->synctex == SyncTeXOption::Compressed ? "1" : "-1"));
    }

    if (options->quiet)
    {
        args.push_back("--quiet");
    }
    if (options->batch && !options->quiet)
    {
        args.push_back("--interaction="s + "scrollmode");
    }
    args.insert(args.end(), options->texOptions.begin(), options->texOptions.end());
#if 0
    if (options->traceStreams.length() > 0)
    {
        args.push_back("--trace="s + options->traceStreams);
    }
#endif
    args.push_back(pathInputFile.ToString());

    app->Verbose(fmt::format(T_("running {}..."), CommandLineBuilder(args).ToString()));

    int exitCode = 0;
    Process::Run(pathExe, args, nullptr, &exitCode, nullptr);
    if (exitCode != 0)
    {
        PathName logName(jobName);
        logName.AppendExtension(".log");
        if (options->clean)
        {
            try
            {
                File::Copy(logName, options->startDirectory / logName.ToString());
            }
            catch (const exception&)
            {
                MIKTEX_FATAL_ERROR(T_("TeX engine failed for some reason."));
            }
        }
        MIKTEX_FATAL_ERROR(T_("TeX engine failed for some reason (see log file)."));
    }
}

/* _________________________________________________________________________

   Driver::Ready

   Decide if looping again is needed.

   LaTeX (and the package changebar) report in the LOG file if it should be
   rerun.  This is needed for files included from subdirs, since texi2dvi does
   not try to compare xref files in subdirs. Performing xref files test is still
   good since LaTeX does not report changes in xref files.
   _________________________________________________________________________ */

bool Driver::Ready()
{
    PathName logName(jobName);
    logName.AppendExtension(".log");

    if (Contains(logName, "Rerun to get"))
    {
        return false;
    }

    vector<string> auxFiles;

    GetAuxFiles(auxFiles);

    // If old and new lists don't at least have the same file list, then one
    // file or another has definitely changed.
    if (previousAuxFiles != auxFiles)
    {
        return false;
    }

    // File list is the same.  We must compare each file until we find a
    // difference.
    for (const string& aux : auxFiles)
    {
        PathName auxFile(auxDirectory / aux);
        app->Verbose(fmt::format(T_("comparing xref file {}..."), Q_(aux)));
        // We only need to keep comparing until we find one that differs,
        // because we'll have to run texindex & tex again no matter how many
        // more there might be.
        if (!File::Equals(PathName(aux), auxFile))
        {
            app->Verbose(fmt::format(T_("xref file {} differed..."), Q_(aux)));
            return false;
        }
    }

    return true;
}

void Driver::InstallOutputFile()
{
    const char* ext = options->outputType == OutputType::PDF ? ".pdf" : ".dvi";
    app->Verbose(fmt::format(T_("copying {} file from {} to {}..."), ext, Q_(workingDirectory), Q_(options->startDirectory)));
    PathName pathSource(workingDirectory / jobName.ToString());
    pathSource.AppendExtension(ext);
    PathName pathDest(options->startDirectory / jobName.ToString());
    pathDest.AppendExtension(ext);
    File::Copy(pathSource, pathDest);
    if (options->synctex != SyncTeXOption::Disabled)
    {
        const char* synctexExt = options->synctex == SyncTeXOption::Compressed ? ".synctex.gz" : ".synctex";
        PathName pathSyncTeXSource(workingDirectory / jobName.ToString());
        pathSyncTeXSource.AppendExtension(synctexExt);
        PathName pathSyncTeXDest(options->startDirectory / jobName.ToString());
        pathSyncTeXDest.AppendExtension(synctexExt);
        File::Copy(pathSyncTeXSource, pathSyncTeXDest);
    }
}

void Driver::GetAuxFiles(const PathName& baseName, const char* extension, vector<string>& vec)
{
    PathName pattern(baseName);
    pattern.AppendExtension(extension);

    PathName curDir;
    curDir.SetToCurrentDirectory();

    app->MyTrace(fmt::format(T_("collecting {} in {}..."), Q_(pattern), Q_(curDir)));

    unique_ptr<DirectoryLister> pLister = DirectoryLister::Open(curDir, pattern.GetData());

    DirectoryEntry2 entry;

    while (pLister->GetNext(entry))
    {
        // If file is empty, skip it.
        if (entry.isDirectory || entry.size == 0)
        {
            continue;
        }

        // If the file is not suitable to be an index or xref file, don't
        // process it.  The file can't be if its first character is not a
        // backslash or single quote.
        FileStream stream(File::Open(PathName(entry.name), FileMode::Open, FileAccess::Read));
        char buf[1];
        if (stream.Read(buf, 1) == 1 && (buf[0] == '\\' || buf[0] == '\''))
        {
            vec.push_back(entry.name);
        }
        stream.Close();
    }
}

/* _________________________________________________________________________

   Driver::GetAuxFiles

   Get list of xref files (indexes, tables and lists).  Find all files having
   root filename with a two-letter extension, saves the ones that are really
   Texinfo-related files.  .?o? catches LaTeX tables and lists.
   _________________________________________________________________________ */

void Driver::GetAuxFiles(vector<string>& auxFiles, vector<string>* idxFiles)
{
    auxFiles.clear();

    if (idxFiles != nullptr)
    {
        idxFiles->clear();
    }

    GetAuxFiles(jobName, ".?o?", auxFiles);
    GetAuxFiles(jobName, ".aux", auxFiles);

    vector<string> files;

    GetAuxFiles(jobName, ".??", files);

    auxFiles.insert(auxFiles.end(), files.begin(), files.end());

    if (idxFiles != nullptr)
    {
        idxFiles->insert(idxFiles->end(), files.begin(), files.end());
    }

    files.clear();

    GetAuxFiles(jobName, ".idx", files);

    auxFiles.insert(auxFiles.end(), files.begin(), files.end());

    if (idxFiles != nullptr)
    {
        idxFiles->insert(idxFiles->end(), files.begin(), files.end());
    }

    sort(auxFiles.begin(), auxFiles.end());
}

void Driver::RunViewer()
{
    const char* ext = options->outputType == OutputType::PDF ? ".pdf" : ".dvi";

    PathName pathFileName(jobName);
    pathFileName.AppendExtension(ext);

    PathName pathDest(options->startDirectory / pathFileName.ToString());

    if (options->viewerOptions.empty())
    {
        app->Verbose(fmt::format(T_("opening {}..."), Q_(pathDest)));
#if defined(MIKTEX_WINDOWS)
        if (ShellExecuteW(nullptr, L"open", pathDest.ToWideCharString().c_str(), 0, options->startDirectory.ToWideCharString().c_str(), SW_SHOW) <= reinterpret_cast<HINSTANCE>(32))
        {
            MIKTEX_FATAL_ERROR(T_("The viewer could not be started."));
        }
#else
        UNIMPLEMENTED();
#endif
    }
    else
    {
        wchar_t szExecutable[BufferSizes::MaxPath];
#if defined(MIKTEX_WINDOWS)
        if (FindExecutableW(pathDest.ToWideCharString().c_str(), options->startDirectory.ToWideCharString().c_str(), szExecutable) <= reinterpret_cast<HINSTANCE>(32))
        {
            MIKTEX_FATAL_ERROR(T_("The viewer could not be located."));
        }
#else
        UNIMPLEMENTED();
#endif
        vector<string> args{ PathName(szExecutable).GetFileNameWithoutExtension().ToString() };
        args.insert(args.end(), options->viewerOptions.begin(), options->viewerOptions.end());
        args.push_back(pathDest.ToString());
        app->Verbose(fmt::format(T_("running {}..."), CommandLineBuilder(args).ToString()));
        Process::Start(PathName(szExecutable), args, nullptr, nullptr, nullptr, nullptr, options->startDirectory.GetData());
    }
}

void Driver::Run()
{
    ExpandMacros();
    app->CheckCancel();
    InsertCommands();

    SetIncludeDirectories();

    // If clean mode was specified, then cd to the temporary working directory.
    if (options->clean)
    {
        app->Verbose(fmt::format("cd {}", Q_(workingDirectory)));
        Directory::SetCurrent(workingDirectory);
    }

    for (int i = 0; i < options->maxIterations; ++i)
    {
        app->CheckCancel();
        vector<string> idxFiles;
        GetAuxFiles(previousAuxFiles, &idxFiles);
        if (!previousAuxFiles.empty())
        {
            app->Verbose(fmt::format(T_("backing up xref files: {}"), FlattenStringVector(previousAuxFiles, ' ')));
            CopyFiles(previousAuxFiles, auxDirectory);
        }
        RunBibTeX();
        if (idxFiles.size() > 0)
        {
            app->CheckCancel();
            RunIndexGenerator(idxFiles);
        }
        app->CheckCancel();
        RunTeX();
        if (Ready())
        {
            break;
        }
    }

    // If we were in clean mode, compilation was in a tmp directory. Copy the
    // DVI (or PDF) file into the directory where the compilation has been done.
    // (The temp dir is about to get removed anyway.)  We also return to the
    // original directory so that
    // - the next file is processed in correct conditions
    // - the temporary file can be removed
    if (options->clean)
    {
        InstallOutputFile();
    }

    if (options->runViewer)
    {
        RunViewer();
    }
}

enum CommandLineOptions
{
    OPT_AAA = 1000,
#if defined(WITH_TEXINFO)
    OPT_AT,
#endif
    OPT_BATCH,
    OPT_CLEAN,
    OPT_DEBUG,
    OPT_ENGINE,
    OPT_EXPAND,
    OPT_INCLUDE,
    OPT_JOB_NAME,
    OPT_LANGUAGE,
    OPT_MAX_ITER,
    OPT_MKIDX_OPTION,
    OPT_PDF,
    OPT_QUIET,
    OPT_RUN_VIEWER,
#if defined(SUPPORT_OPT_SRC_SPECIALS)
    OPT_SRC,
    OPT_SRC_SPECIALS,
#endif
    OPT_SYNCTEX,
#if defined(WITH_TEXINFO)
    OPT_TEXINFO,
#endif
    OPT_TEX_OPTION,
    OPT_TRACE,
    OPT_VERBOSE,
    OPT_VERSION,
    OPT_VIEWER_OPTION,
};

const struct poptOption optionTable[] = {
#if defined(WITH_TEXINFO)
    {
        "texiat",
        '@',
        POPT_ARG_NONE,
        nullptr,
        OPT_AT,
        T_("Use @input instead of \\input; for preloaded Texinfo."),
        nullptr,
    },
#endif

    {
        "batch",
        'b',
        POPT_ARG_NONE,
        nullptr,
        OPT_BATCH,
        T_("No interaction."),
        nullptr,
    },

    {
        "clean",
        'c',
        POPT_ARG_NONE,
        nullptr,
        OPT_CLEAN,
        T_("Remove all auxiliary files."),
        nullptr,
    },

    {
        "debug",
        'D',
        POPT_ARG_NONE | POPT_ARGFLAG_DOC_HIDDEN,
        nullptr,
        OPT_DEBUG,
        T_("Print debug information."),
        nullptr,
    },

    {
        "engine",
        0,
        POPT_ARG_STRING,
        nullptr,
        OPT_ENGINE,
        T_("Use the specified TeX engine."),
        T_("ENGINE"),
    },

    {
        "expand",
        'e',
        POPT_ARG_NONE,
        nullptr,
        OPT_EXPAND,
        T_("Force macro expansion using makeinfo."),
        nullptr,
    },

    {
        "include-directory",
        'I',
        POPT_ARG_STRING,
        nullptr,
        OPT_INCLUDE,
        T_("Prepend DIR to the input search path."),
        T_("DIR"),
    },

    {
        "job-name",
        0,
        POPT_ARG_STRING,
        nullptr,
        OPT_JOB_NAME,
        T_("Set the job name and hence the name(s) of the output file(s)."),
        T_("JOBNAME"),
    },

    {
        "language",
        'l',
        POPT_ARG_STRING,
        nullptr,
#if defined(WITH_TEXINFO)
        OPT_LANGUAGE,
        T_("Specify the LANG of FILE: LaTeX or Texinfo."),
#else
        OPT_LANGUAGE | POPT_ARGFLAG_DOC_HIDDEN,
        T_(""),
#endif
        T_("LANG"),
    },

    {
        "pdf",
        'p',
        POPT_ARG_NONE,
        nullptr,
        OPT_PDF,
        T_("Use pdftex or pdflatex for processing."),
        nullptr,
    },

    {
        "quiet",
        'q',
        POPT_ARG_NONE,
        nullptr,
        OPT_QUIET,
        T_("No output unless errors (implies --batch)."),
        nullptr,
    },

    {
        "silent",
        's',
        POPT_ARG_NONE | POPT_ARGFLAG_DOC_HIDDEN,
        nullptr,
        OPT_QUIET,
        T_("No output unless errors (implies --batch)."),
        nullptr,
    },

#if defined(WITH_TEXINFO)
    {
        "texinfo",
        't',
        POPT_ARG_STRING,
        nullptr,
        OPT_TEXINFO,
        T_("Insert CMD after @setfilename in copy of input file."),
        "CMD",
    },
#endif

    {
        "version",
        'v',
        POPT_ARG_NONE,
        nullptr,
        OPT_VERSION,
        T_("Display version information and exit successfully."),
        nullptr,
    },

    {
        "verbose",
        'V',
        POPT_ARG_NONE,
        nullptr,
        OPT_VERBOSE,
        T_("Report on what is done."),
        nullptr,
    },

    // --- now the MiKTeX extensions

    {
        "max-iterations",
        0,
        POPT_ARG_STRING,
        nullptr,
        OPT_MAX_ITER,
        T_("Limit number of iterations."),
        "N",
    },

    {
        "mkidx-option",
        0,
        POPT_ARG_STRING,
        nullptr,
        OPT_MKIDX_OPTION,
        T_("Pass OPTION to the index generator."),
        T_("OPTION"),
    },

#if defined(SUPPORT_OPT_SRC_SPECIALS)
    {
        "src",
        0,
        POPT_ARG_NONE | POPT_ARGFLAG_DOC_HIDDEN,
        nullptr,
        OPT_SRC,
        T_("Pass option --src-specials to the TeX engine."),
        nullptr,
    },

    {
        "src-specials",
        0,
        POPT_ARG_STRING | POPT_ARGFLAG_OPTIONAL,
        nullptr,
        OPT_SRC_SPECIALS,
        T_("Pass option --src-specials[=SRCSPECIALS] to the TeX engine."),
        T_("SRCSPECIALS"),
    },
#endif

    {
        "synctex",
        0,
        POPT_ARG_STRING | POPT_ARGFLAG_OPTIONAL,
        nullptr,
        OPT_SYNCTEX,
        T_("Pass option --synctex=N to the TeX engine."),
        "N",
    },

    {
        "tex-option",
        0,
        POPT_ARG_STRING,
        nullptr,
        OPT_TEX_OPTION,
        T_("Pass OPTION to the TeX engine."),
        T_("OPTION"),
    },

    {
        "trace",
        0,
        POPT_ARG_STRING | POPT_ARGFLAG_OPTIONAL,
        nullptr,
        OPT_TRACE,
        T_("Turn on tracing.  TRACESTREAMS, if specified, is a comma-separated list of trace stream names (see the MiKTeX manual)."),
        T_("TRACESTREAMS"),
    },

    {
        "run-viewer",
        0,
        POPT_ARG_NONE,
        nullptr,
        OPT_RUN_VIEWER,
        T_("Run a viewer on the resulting DVI/PDF file."),
        nullptr,
    },

    {
        "viewer-option",
        0,
        POPT_ARG_STRING,
        nullptr,
        OPT_VIEWER_OPTION,
        T_("Pass OPTION to the viewer."),
        T_("OPTION"),
    },

    POPT_AUTOHELP
        POPT_TABLEEND };

void McdApp::Run(int argc, const char** argv)
{
#if defined(MIKTEX_WINDOWS)
    ConsoleCodePageSwitcher cpSwitcher;
#endif

    Session::InitInfo initInfo(argv[0]);
    vector<const char*> newargv(&argv[0], &argv[argc + 1]);
    ExamineArgs(newargv, initInfo);

    PoptWrapper popt(newargv.size() - 1, &newargv[0], optionTable);

    int option;

    bool optVersion = false;

    while ((option = popt.GetNextOpt()) >= 0)
    {
        string optArg = popt.GetOptArg();
        switch (option)
        {
#if defined(WITH_TEXINFO)
        case OPT_AT:
            break;
#endif
        case OPT_BATCH:
            options.batch = true;
            break;
        case OPT_CLEAN:
            options.clean = true;
            break;
        case OPT_DEBUG:
            options.traceStreams = StringUtil::Flatten(DEFAULT_TRACE_OPTIONS, ',');
            break;
        case OPT_ENGINE:
            if (!options.SetEngine(optArg))
            {
                FatalError(fmt::format(T_("{}: unknown engine"), optArg.c_str()));
            }
            break;
        case OPT_EXPAND:
            options.expand = true;
            break;
        case OPT_INCLUDE:
        {
            PathName path;
            if (PathNameUtil::IsAbsolutePath(optArg))
            {
                path = optArg;
            }
            else
            {
                path = options.startDirectory / optArg;
            }
            path.ConvertToUnix();
            options.includeDirectories.push_back(path.GetData());
            break;
        }
        case OPT_JOB_NAME:
            options.jobName = optArg;
            break;
        case OPT_LANGUAGE:
            if (StringCompare(optArg, "latex", true) == 0)
            {
                options.macroLanguage = MacroLanguage::LaTeX;
            }
#if defined(WITH_TEXINFO)
            else if (StringCompare(optArg, "texinfo", true) == 0)
            {
                options.macroLanguage = MacroLanguage::Texinfo;
            }
#endif
            else
            {
                FatalError(fmt::format(T_("{}: unknown language"), optArg));
            }
            break;
        case OPT_PDF:
            options.outputType = OutputType::PDF;
            break;
        case OPT_QUIET:
            options.quiet = true;
            options.batch = true;
            break;
#if defined(WITH_TEXINFO)
        case OPT_TEXINFO:
            options.texinfoCommands.push_back(optArg);
            break;
#endif
        case OPT_VERSION:
            optVersion = true;
            break;
        case OPT_VERBOSE:
            options.verbose = true;
            break;
#if defined(SUPPORT_OPT_SRC_SPECIALS)
        case OPT_SRC:
            options.sourceSpecialsWhere = "";
            options.sourceSpecials = true;
            break;
        case OPT_SRC_SPECIALS:
            if (!optArg.empty())
            {
                options.sourceSpecialsWhere = optArg;
            }
            options.sourceSpecials = true;
            break;
#endif
        case OPT_SYNCTEX:
            if (optArg.empty())
            {
                options.synctex = SyncTeXOption::Compressed;
            }
            else
            {
                int synctex = std::stoi(optArg);
                if (synctex == 0)
                {
                    options.synctex = SyncTeXOption::Disabled;
                }
                else
                {
                    options.synctex = synctex < 0 ? SyncTeXOption::Uncompressed : SyncTeXOption::Compressed;
                }
            }
            break;
        case OPT_MKIDX_OPTION:
            options.makeindexOptions.push_back(optArg);
            break;
        case OPT_TEX_OPTION:
            for (const string& o : forbiddenTexOptions)
            {
                if (optArg.find(o) != string::npos)
                {
                    FatalError(fmt::format(T_("TeX option \"--{}\" is not supported."), o));
                }
            }
            options.texOptions.push_back(optArg);
            break;
        case OPT_VIEWER_OPTION:
            options.viewerOptions.push_back(optArg);
            break;
        case OPT_RUN_VIEWER:
            options.runViewer = true;
            break;
        case OPT_MAX_ITER:
            options.maxIterations = std::stoi(optArg);
            break;
        case OPT_TRACE:
            if (optArg.empty())
            {
                options.traceStreams = StringUtil::Flatten(DEFAULT_TRACE_OPTIONS, ',');
            }
            else
            {
                options.traceStreams = optArg;
            }
            break;
        }
    }

    if (option != -1)
    {
        string msg = popt.BadOption(POPT_BADOPTION_NOALIAS);
        msg += ": ";
        msg += popt.Strerror(option);
        FatalError(msg);
    }

    vector<string> leftovers = popt.GetLeftovers();

    if (leftovers.empty() && !optVersion)
    {
        FatalError(T_("Missing file argument."));
    }

    if (options.traceStreams.length() > 0)
    {
        initInfo.SetTraceFlags(options.traceStreams);
    }

    Init(initInfo);

    if (optVersion)
    {
        Version();
        return;
    }

    for (const string& fileName : leftovers)
    {
        Verbose(fmt::format(T_("processing {}..."), Q_(fileName)));

        // See if the file exists.  If it doesn't we're in trouble since, even
        // though the user may be able to reenter a valid filename at the tex
        // prompt (assuming they're attending the terminal), this script won't
        // be able to find the right xref files and so forth.
        if (!File::Exists(PathName(fileName)))
        {
            FatalError(T_("The input file could not be found."));
        }

        Driver driver;
        driver.Initialize(this, &options, fileName.c_str());
        driver.Run();
    }

    Finalize2(0);
}

#if defined(_UNICODE)
#define MAIN wmain
#define MAINCHAR wchar_t
#else
#define MAIN main
#define MAINCHAR char
#endif

int MAIN(int argc, MAINCHAR* argv[])
{
    McdApp app;
    try
    {
        vector<string> utf8args;
        utf8args.reserve(argc);
        vector<const char*> newargv;
        newargv.reserve(argc + 1);
        for (int idx = 0; idx < argc; ++idx)
        {
#if defined(_UNICODE)
            utf8args.push_back(StringUtil::WideCharToUTF8(argv[idx]));
#elif defined(MIKTEX_WINDOWS)
            utf8args.push_back(StringUtil::AnsiToUTF8(argv[idx]));
#else
            utf8args.push_back(argv[idx]);
#endif
            newargv.push_back(utf8args[idx].c_str());
        }
        newargv.push_back(nullptr);
        app.Run(argc, &newargv[0]);
        return 0;
    }

    catch (const MiKTeXException& ex)
    {
        app.Sorry(THE_NAME_OF_THE_GAME, ex);
        ex.Save();
        return 1;
    }

    catch (const exception& ex)
    {
        app.Sorry(THE_NAME_OF_THE_GAME, ex);
        return 1;
    }

    catch (int exitCode)
    {
        return exitCode;
    }
}
