/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */
#include "cmDependsC.h"

#include "cmsys/FStream.hxx"
#include <utility>

#include "cmAlgorithms.h"
#include "cmFileTimeComparison.h"
#include "cmLocalGenerator.h"
#include "cmMakefile.h"
#include "cmSystemTools.h"

#define INCLUDE_REGEX_LINE                                                     \
    "^[ \t]*[#%][ \t]*(include|import)[ \t]*[<\"]([^\">]+)([\">])"

#define INCLUDE_REGEX_LINE_MARKER "#IncludeRegexLine: "
#define INCLUDE_REGEX_SCAN_MARKER "#IncludeRegexScan: "
#define INCLUDE_REGEX_COMPLAIN_MARKER "#IncludeRegexComplain: "
#define INCLUDE_REGEX_TRANSFORM_MARKER "#IncludeRegexTransform: "

cmDependsC::cmDependsC()
: ValidDeps(nullptr)
{}

cmDependsC::cmDependsC(cmLocalGenerator* lg, const char* targetDir,
                       const std::string&                             lang,
                       const std::map<std::string, DependencyVector>* validDeps)
: cmDepends(lg, targetDir)
, ValidDeps(validDeps)
{
    cmMakefile* mf = lg->GetMakefile();

    // Configure the include file search path.
    this->SetIncludePathFromLanguage(lang);

    // Configure regular expressions.
    std::string scanRegex     = "^.*$";
    std::string complainRegex = "^$";
    {
        std::string scanRegexVar = "CMAKE_";
        scanRegexVar += lang;
        scanRegexVar += "_INCLUDE_REGEX_SCAN";
        if(const char* sr = mf->GetDefinition(scanRegexVar))
        {
            scanRegex = sr;
        }
        std::string complainRegexVar = "CMAKE_";
        complainRegexVar += lang;
        complainRegexVar += "_INCLUDE_REGEX_COMPLAIN";
        if(const char* cr = mf->GetDefinition(complainRegexVar))
        {
            complainRegex = cr;
        }
    }

    this->IncludeRegexLine.compile(INCLUDE_REGEX_LINE);
    this->IncludeRegexScan.compile(scanRegex.c_str());
    this->IncludeRegexComplain.compile(complainRegex.c_str());
    this->IncludeRegexLineString = INCLUDE_REGEX_LINE_MARKER INCLUDE_REGEX_LINE;
    this->IncludeRegexScanString = INCLUDE_REGEX_SCAN_MARKER;
    this->IncludeRegexScanString += scanRegex;
    this->IncludeRegexComplainString = INCLUDE_REGEX_COMPLAIN_MARKER;
    this->IncludeRegexComplainString += complainRegex;

    this->SetupTransforms();

    this->CacheFileName = this->TargetDirectory;
    this->CacheFileName += "/";
    this->CacheFileName += lang;
    this->CacheFileName += ".includecache";

    this->ReadCacheFile();
}

cmDependsC::~cmDependsC()
{
    this->WriteCacheFile();
    cmDeleteAll(this->FileCache);
}

bool
cmDependsC::WriteDependencies(const std::set<std::string>& sources,
                              const std::string& obj, std::ostream& makeDepends,
                              std::ostream& internalDepends)
{
    // Make sure this is a scanning instance.
    if(sources.empty() || sources.begin()->empty())
    {
        cmSystemTools::Error("Cannot scan dependencies without a source file.");
        return false;
    }
    if(obj.empty())
    {
        cmSystemTools::Error(
            "Cannot scan dependencies without an object file.");
        return false;
    }

    std::set<std::string> dependencies;
    bool                  haveDeps = false;

    std::string binDir = this->LocalGenerator->GetBinaryDirectory();

    // Compute a path to the object file to write to the internal depend file.
    // Any existing content of the internal depend file has already been
    // loaded in ValidDeps with this path as a key.
    std::string obj_i =
        this->LocalGenerator->ConvertToRelativePath(binDir, obj);

    if(this->ValidDeps != nullptr)
    {
        std::map<std::string, DependencyVector>::const_iterator tmpIt =
            this->ValidDeps->find(obj_i);
        if(tmpIt != this->ValidDeps->end())
        {
            dependencies.insert(tmpIt->second.begin(), tmpIt->second.end());
            haveDeps = true;
        }
    }

    if(!haveDeps)
    {
        // Walk the dependency graph starting with the source file.
        int srcFiles = static_cast<int>(sources.size());
        this->Encountered.clear();

        for(std::string const& src : sources)
        {
            UnscannedEntry root;
            root.FileName = src;
            this->Unscanned.push(root);
            this->Encountered.insert(src);
        }

        std::set<std::string> scanned;

        // Use reserve to allocate enough memory for tempPathStr
        // so that during the loops no memory is allocated or freed
        std::string tempPathStr;
        tempPathStr.reserve(4 * 1024);

        while(!this->Unscanned.empty())
        {
            // Get the next file to scan.
            UnscannedEntry current = this->Unscanned.front();
            this->Unscanned.pop();

            // If not a full path, find the file in the include path.
            std::string fullName;
            if((srcFiles > 0) ||
               cmSystemTools::FileIsFullPath(current.FileName))
            {
                if(cmSystemTools::FileExists(current.FileName, true))
                {
                    fullName = current.FileName;
                }
            } else if(!current.QuotedLocation.empty() &&
                      cmSystemTools::FileExists(current.QuotedLocation, true))
            {
                // The include statement producing this entry was a double-quote
                // include and the included file is present in the directory of
                // the source containing the include statement.
                fullName = current.QuotedLocation;
            } else
            {
                std::map<std::string, std::string>::iterator headerLocationIt =
                    this->HeaderLocationCache.find(current.FileName);
                if(headerLocationIt != this->HeaderLocationCache.end())
                {
                    fullName = headerLocationIt->second;
                } else
                {
                    for(std::string const& i : this->IncludePath)
                    {
                        // Construct the name of the file as if it were in the
                        // current include directory.  Avoid using a leading
                        // "./".

                        tempPathStr = cmSystemTools::CollapseCombinedPath(
                            i, current.FileName);

                        // Look for the file in this location.
                        if(cmSystemTools::FileExists(tempPathStr, true))
                        {
                            fullName                              = tempPathStr;
                            HeaderLocationCache[current.FileName] = fullName;
                            break;
                        }
                    }
                }
            }

            // Complain if the file cannot be found and matches the complain
            // regex.
            if(fullName.empty() &&
               this->IncludeRegexComplain.find(current.FileName))
            {
                cmSystemTools::Error("Cannot find file \"",
                                     current.FileName.c_str(), "\".");
                return false;
            }

            // Scan the file if it was found and has not been scanned already.
            if(!fullName.empty() && (scanned.find(fullName) == scanned.end()))
            {
                // Record scanned files.
                scanned.insert(fullName);

                // Check whether this file is already in the cache
                std::map<std::string, cmIncludeLines*>::iterator fileIt =
                    this->FileCache.find(fullName);
                if(fileIt != this->FileCache.end())
                {
                    fileIt->second->Used = true;
                    dependencies.insert(fullName);
                    for(UnscannedEntry const& inc :
                        fileIt->second->UnscannedEntries)
                    {
                        if(this->Encountered.find(inc.FileName) ==
                           this->Encountered.end())
                        {
                            this->Encountered.insert(inc.FileName);
                            this->Unscanned.push(inc);
                        }
                    }
                } else
                {
                    // Try to scan the file.  Just leave it out if we cannot
                    // find it.
                    cmsys::ifstream fin(fullName.c_str());
                    if(fin)
                    {
                        cmsys::FStream::BOM bom = cmsys::FStream::ReadBOM(fin);
                        if(bom == cmsys::FStream::BOM_None ||
                           bom == cmsys::FStream::BOM_UTF8)
                        {
                            // Add this file as a dependency.
                            dependencies.insert(fullName);

                            // Scan this file for new dependencies.  Pass the
                            // directory containing the file to handle
                            // double-quote includes.
                            std::string dir =
                                cmSystemTools::GetFilenamePath(fullName);
                            this->Scan(fin, dir.c_str(), fullName);
                        } else
                        {
                            // Skip file with encoding we do not implement.
                        }
                    }
                }
            }

            srcFiles--;
        }
    }

    // Write the dependencies to the output stream.  Makefile rules
    // written by the original local generator for this directory
    // convert the dependencies to paths relative to the home output
    // directory.  We must do the same here.
    std::string obj_m = cmSystemTools::ConvertToOutputPath(obj_i);
    internalDepends << obj_i << std::endl;

    for(std::string const& dep : dependencies)
    {
        makeDepends << obj_m << ": "
                    << cmSystemTools::ConvertToOutputPath(
                           this->LocalGenerator->ConvertToRelativePath(binDir,
                                                                       dep))
                    << std::endl;
        internalDepends << " " << dep << std::endl;
    }
    makeDepends << std::endl;

    return true;
}

void
cmDependsC::ReadCacheFile()
{
    if(this->CacheFileName.empty())
    {
        return;
    }
    cmsys::ifstream fin(this->CacheFileName.c_str());
    if(!fin)
    {
        return;
    }

    std::string     line;
    cmIncludeLines* cacheEntry   = nullptr;
    bool            haveFileName = false;

    while(cmSystemTools::GetLineFromStream(fin, line))
    {
        if(line.empty())
        {
            cacheEntry   = nullptr;
            haveFileName = false;
            continue;
        }
        // the first line after an empty line is the name of the parsed file
        if(!haveFileName)
        {
            haveFileName               = true;
            int                  newer = 0;
            cmFileTimeComparison comp;
            bool res = comp.FileTimeCompare(this->CacheFileName.c_str(),
                                            line.c_str(), &newer);

            if(res && newer == 1)  // cache is newer than the parsed file
            {
                cacheEntry            = new cmIncludeLines;
                this->FileCache[line] = cacheEntry;
            }
            // file doesn't exist, check that the regular expressions
            // haven't changed
            else if(!res)
            {
                if(line.find(INCLUDE_REGEX_LINE_MARKER) == 0)
                {
                    if(line != this->IncludeRegexLineString)
                    {
                        return;
                    }
                } else if(line.find(INCLUDE_REGEX_SCAN_MARKER) == 0)
                {
                    if(line != this->IncludeRegexScanString)
                    {
                        return;
                    }
                } else if(line.find(INCLUDE_REGEX_COMPLAIN_MARKER) == 0)
                {
                    if(line != this->IncludeRegexComplainString)
                    {
                        return;
                    }
                } else if(line.find(INCLUDE_REGEX_TRANSFORM_MARKER) == 0)
                {
                    if(line != this->IncludeRegexTransformString)
                    {
                        return;
                    }
                }
            }
        } else if(cacheEntry != nullptr)
        {
            UnscannedEntry entry;
            entry.FileName = line;
            if(cmSystemTools::GetLineFromStream(fin, line))
            {
                if(line != "-")
                {
                    entry.QuotedLocation = line;
                }
                cacheEntry->UnscannedEntries.push_back(std::move(entry));
            }
        }
    }
}

void
cmDependsC::WriteCacheFile() const
{
    if(this->CacheFileName.empty())
    {
        return;
    }
    cmsys::ofstream cacheOut(this->CacheFileName.c_str());
    if(!cacheOut)
    {
        return;
    }

    cacheOut << this->IncludeRegexLineString << "\n\n";
    cacheOut << this->IncludeRegexScanString << "\n\n";
    cacheOut << this->IncludeRegexComplainString << "\n\n";
    cacheOut << this->IncludeRegexTransformString << "\n\n";

    for(auto const& fileIt : this->FileCache)
    {
        if(fileIt.second->Used)
        {
            cacheOut << fileIt.first << std::endl;

            for(UnscannedEntry const& inc : fileIt.second->UnscannedEntries)
            {
                cacheOut << inc.FileName << std::endl;
                if(inc.QuotedLocation.empty())
                {
                    cacheOut << "-" << std::endl;
                } else
                {
                    cacheOut << inc.QuotedLocation << std::endl;
                }
            }
            cacheOut << std::endl;
        }
    }
}

void
cmDependsC::Scan(std::istream& is, const char* directory,
                 const std::string& fullName)
{
    cmIncludeLines* newCacheEntry = new cmIncludeLines;
    newCacheEntry->Used           = true;
    this->FileCache[fullName]     = newCacheEntry;

    // Read one line at a time.
    std::string line;
    while(cmSystemTools::GetLineFromStream(is, line))
    {
        // Transform the line content first.
        if(!this->TransformRules.empty())
        {
            this->TransformLine(line);
        }

        // Match include directives.
        if(this->IncludeRegexLine.find(line))
        {
            // Get the file being included.
            UnscannedEntry entry;
            entry.FileName = this->IncludeRegexLine.match(2);
            cmSystemTools::ConvertToUnixSlashes(entry.FileName);
            if(this->IncludeRegexLine.match(3) == "\"" &&
               !cmSystemTools::FileIsFullPath(entry.FileName))
            {
                // This was a double-quoted include with a relative path.  We
                // must check for the file in the directory containing the
                // file we are scanning.
                entry.QuotedLocation = cmSystemTools::CollapseCombinedPath(
                    directory, entry.FileName);
            }

            // Queue the file if it has not yet been encountered and it
            // matches the regular expression for recursive scanning.  Note
            // that this check does not account for the possibility of two
            // headers with the same name in different directories when one
            // is included by double-quotes and the other by angle brackets.
            // It also does not work properly if two header files with the same
            // name exist in different directories, and both are included from a
            // file their own directory by simply using "filename.h" (#12619)
            // This kind of problem will be fixed when a more
            // preprocessor-like implementation of this scanner is created.
            if(this->IncludeRegexScan.find(entry.FileName))
            {
                newCacheEntry->UnscannedEntries.push_back(entry);
                if(this->Encountered.find(entry.FileName) ==
                   this->Encountered.end())
                {
                    this->Encountered.insert(entry.FileName);
                    this->Unscanned.push(entry);
                }
            }
        }
    }
}

void
cmDependsC::SetupTransforms()
{
    // Get the transformation rules.
    std::vector<std::string> transformRules;
    cmMakefile*              mf = this->LocalGenerator->GetMakefile();
    if(const char* xform = mf->GetDefinition("CMAKE_INCLUDE_TRANSFORMS"))
    {
        cmSystemTools::ExpandListArgument(xform, transformRules, true);
    }
    for(std::string const& tr : transformRules)
    {
        this->ParseTransform(tr);
    }

    this->IncludeRegexTransformString = INCLUDE_REGEX_TRANSFORM_MARKER;
    if(!this->TransformRules.empty())
    {
        // Construct the regular expression to match lines to be
        // transformed.
        std::string xform = "^([ \t]*[#%][ \t]*(include|import)[ \t]*)(";
        const char* sep   = "";
        for(auto const& tr : this->TransformRules)
        {
            xform += sep;
            xform += tr.first;
            sep = "|";
        }
        xform += ")[ \t]*\\(([^),]*)\\)";
        this->IncludeRegexTransform.compile(xform.c_str());

        // Build a string that encodes all transformation rules and will
        // change when rules are changed.
        this->IncludeRegexTransformString += xform;
        for(auto const& tr : this->TransformRules)
        {
            this->IncludeRegexTransformString += " ";
            this->IncludeRegexTransformString += tr.first;
            this->IncludeRegexTransformString += "(%)=";
            this->IncludeRegexTransformString += tr.second;
        }
    }
}

void
cmDependsC::ParseTransform(std::string const& xform)
{
    // A transform rule is of the form SOME_MACRO(%)=value-with-%
    // We can simply separate with "(%)=".
    std::string::size_type pos = xform.find("(%)=");
    if(pos == std::string::npos || pos == 0)
    {
        return;
    }
    std::string name           = xform.substr(0, pos);
    std::string value          = xform.substr(pos + 4);
    this->TransformRules[name] = value;
}

void
cmDependsC::TransformLine(std::string& line)
{
    // Check for a transform rule match.  Return if none.
    if(!this->IncludeRegexTransform.find(line))
    {
        return;
    }
    TransformRulesType::const_iterator tri =
        this->TransformRules.find(this->IncludeRegexTransform.match(3));
    if(tri == this->TransformRules.end())
    {
        return;
    }

    // Construct the transformed line.
    std::string newline = this->IncludeRegexTransform.match(1);
    std::string arg     = this->IncludeRegexTransform.match(4);
    for(const char* c = tri->second.c_str(); *c; ++c)
    {
        if(*c == '%')
        {
            newline += arg;
        } else
        {
            newline += *c;
        }
    }

    // Return the transformed line.
    line = newline;
}
