/**
 * Run the C preprocessor on a C source file.
 *
 * Specification: C11
 *
 * Copyright:   Copyright (C) 2022-2026 by The D Language Foundation, All Rights Reserved
 * Authors:     $(LINK2 https://www.digitalmars.com, Walter Bright)
 * License:     $(LINK2 https://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
 * Source:      $(LINK2 https://github.com/dlang/dmd/blob/master/compiler/src/dmd/cpreprocess.d, _cpreprocess.d)
 * Documentation:  https://dlang.org/phobos/dmd_cpreprocess.html
 * Coverage:    https://codecov.io/gh/dlang/dmd/src/master/compiler/src/dmd/cpreprocess.d
 */

module dmd.cpreprocess;

import core.stdc.stdio;
import core.stdc.stdlib;
import core.stdc.string;

import dmd.astenums;
import dmd.errors;
import dmd.errorsink;
import dmd.link;
import dmd.location;
import dmd.target;
import dmd.vsoptions;

import dmd.common.outbuffer;

import dmd.root.array;
import dmd.root.file;
import dmd.root.filename;
import dmd.root.rmem;
import dmd.root.string;

// Use default for other versions
version (Posix)   version = runPreprocessor;
version (Windows) version = runPreprocessor;

/***************************************
 * Preprocess C file.
 * Params:
 *      csrcfile = C file to be preprocessed, with .c or .h extension
 *      loc = The source location where preprocess is requested from
 *      defines = buffer to append any `#define` and `#undef` lines encountered to
 * Result:
 *      the text of the preprocessed file
 */
extern (C++)
DArray!ubyte preprocess(FileName csrcfile, Loc loc, ref OutBuffer defines)
{
    import dmd.globals;
    /* Look for "importc.h" by searching along import path.
     */
    const(char)* importc_h = findImportcH(global.importPaths[]);

    auto eSink = global.errorSink;
    if (importc_h)
    {
        if (global.params.v.verbose)
            eSink.message(Loc.init, "include   %s", importc_h);
    }
    else
    {
        eSink.error(loc, "cannot find \"importc.h\" along import path");
        fatal();
    }

    //printf("preprocess %s\n", csrcfile.toChars());
    version (runPreprocessor)
    {
        const includePath = FileName.replaceName(toDString(importc_h), "include");
        scope(exit) FileName.free(includePath.ptr);
        const command = global.params.cpp ? toDString(global.params.cpp) : cppCommand();
        DArray!ubyte text;
        int status = runPreprocessor(loc, command, csrcfile.toString(), importc_h, includePath, global.params.cppswitches, global.params.v.verbose, global.errorSink, defines, text);
        if (status)
            fatal();
        return text;
    }
    else
    {
        return DArray!ubyte(global.fileManager.getFileContents(csrcfile));
    }
}


/***************************************
 * Find importc.h by looking along the path
 * Params:
 *      path = import path
 * Returns:
 *      importc.h file name, null if not found
 */
const(char)* findImportcH(const(char)*[] path)
{
    /* Look for "importc.h" by searching along import path.
     * It should be in the same place as "object.d"
     */
    foreach (entry; path)
    {
        auto f = FileName.combine(entry, "importc.h");
        if (FileName.exists(f) == 1)
        {
            return FileName.toAbsolute(f);
        }
        FileName.free(f);
    }
    return null;
}

/******************************************
 * Pick the C preprocessor program to run.
 */
private const(char)[] cppCommand()
{
    if (const p = getenv("CPPCMD"))
        return toDString(p);

    version (Windows)
    {
        if (target.objectFormat() == Target.ObjectFormat.coff)
        {
            VSOptions vsopt;
            vsopt.initialize();
            const path = vsopt.compilerPath(target.isX86_64);
            //if the path to cl.exe is found, check if cl.exe is in the path.
            if(FileName.exists(path) != 1)
            {
                import dmd.globals : global;
                auto eSink = global.errorSink;
                eSink.error(Loc.initial, "cl.exe not found. Please ensure that Visual Studio Build Tools are installed and properly configured.");
                fatal();
            }
            return toDString(path);
        }
        // Perhaps we are cross-compiling.
        return "cpp";
    }
    else version (OpenBSD)
    {
        // On OpenBSD, we need to use the actual binary /usr/libexec/cpp
        // rather than the shell script wrapper /usr/bin/cpp ...
        // Turns out the shell script doesn't really understand -o
        return "/usr/libexec/cpp";
    }
    else version (OSX)
    {
        return "clang";
    }
    else
    {
        return "cpp";
    }
}
