#include "cmParseJacocoCoverage.h"

#include "cmCTest.h"
#include "cmCTestCoverageHandler.h"
#include "cmSystemTools.h"
#include "cmXMLParser.h"

#include "cmsys/Directory.hxx"
#include "cmsys/FStream.hxx"
#include "cmsys/Glob.hxx"
#include <stdlib.h>
#include <string.h>

class cmParseJacocoCoverage::XMLParser : public cmXMLParser
{
public:
    XMLParser(cmCTest* ctest, cmCTestCoverageHandlerContainer& cont)
    : CTest(ctest)
    , Coverage(cont)
    {
        this->FilePath.clear();
        this->PackagePath.clear();
        this->PackageName.clear();
    }

    ~XMLParser() override {}

protected:
    void EndElement(const std::string& /*name*/) override {}

    void StartElement(const std::string& name, const char** atts) override
    {
        if(name == "package")
        {
            this->PackageName = atts[1];
            this->PackagePath.clear();
        } else if(name == "sourcefile")
        {
            std::string fileName = atts[1];

            if(this->PackagePath.empty())
            {
                if(!this->FindPackagePath(fileName))
                {
                    cmCTestLog(this->CTest, ERROR_MESSAGE,
                               "Cannot find file: " << this->PackageName << "/"
                                                    << fileName << std::endl);
                    this->Coverage.Error++;
                    return;
                }
            }

            cmCTestOptionalLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
                               "Reading file: " << fileName << std::endl,
                               this->Coverage.Quiet);

            this->FilePath = this->PackagePath + "/" + fileName;
            cmsys::ifstream fin(this->FilePath.c_str());
            if(!fin)
            {
                cmCTestLog(this->CTest, ERROR_MESSAGE,
                           "Jacoco Coverage: Error opening " << this->FilePath
                                                             << std::endl);
            }
            std::string    line;
            FileLinesType& curFileLines =
                this->Coverage.TotalCoverage[this->FilePath];
            if(fin)
            {
                curFileLines.push_back(-1);
            }
            while(cmSystemTools::GetLineFromStream(fin, line))
            {
                curFileLines.push_back(-1);
            }
        } else if(name == "line")
        {
            int tagCount = 0;
            int nr       = -1;
            int ci       = -1;
            while(true)
            {
                if(strcmp(atts[tagCount], "ci") == 0)
                {
                    ci = atoi(atts[tagCount + 1]);
                } else if(strcmp(atts[tagCount], "nr") == 0)
                {
                    nr = atoi(atts[tagCount + 1]);
                }
                if(ci > -1 && nr > 0)
                {
                    FileLinesType& curFileLines =
                        this->Coverage.TotalCoverage[this->FilePath];
                    if(!curFileLines.empty())
                    {
                        curFileLines[nr - 1] = ci;
                    }
                    break;
                }
                ++tagCount;
            }
        }
    }

    virtual bool FindPackagePath(std::string const& fileName)
    {
        // Search for the source file in the source directory.
        if(this->PackagePathFound(fileName, this->Coverage.SourceDir))
        {
            return true;
        }

        // If not found there, check the binary directory.
        if(this->PackagePathFound(fileName, this->Coverage.BinaryDir))
        {
            return true;
        }
        return false;
    }

    virtual bool PackagePathFound(std::string const& fileName,
                                  std::string const& baseDir)
    {
        // Search for the file in the baseDir and its subdirectories.
        std::string packageGlob = baseDir;
        packageGlob += "/";
        packageGlob += fileName;
        cmsys::Glob gl;
        gl.RecurseOn();
        gl.RecurseThroughSymlinksOn();
        gl.FindFiles(packageGlob);
        std::vector<std::string> const& files = gl.GetFiles();
        if(files.empty())
        {
            return false;
        }

        // Check if any of the locations found match our package.
        for(std::string const& f : files)
        {
            std::string dir = cmsys::SystemTools::GetParentDirectory(f);
            if(cmsys::SystemTools::StringEndsWith(dir,
                                                  this->PackageName.c_str()))
            {
                cmCTestOptionalLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
                                   "Found package directory for "
                                       << fileName << ": " << dir << std::endl,
                                   this->Coverage.Quiet);
                this->PackagePath = dir;
                return true;
            }
        }
        return false;
    }

private:
    std::string FilePath;
    std::string PackagePath;
    std::string PackageName;
    typedef cmCTestCoverageHandlerContainer::SingleFileCoverageVector
                                     FileLinesType;
    cmCTest*                         CTest;
    cmCTestCoverageHandlerContainer& Coverage;
};

cmParseJacocoCoverage::cmParseJacocoCoverage(
    cmCTestCoverageHandlerContainer& cont, cmCTest* ctest)
: Coverage(cont)
, CTest(ctest)
{}

bool
cmParseJacocoCoverage::LoadCoverageData(std::vector<std::string> const& files)
{
    // load all the jacoco.xml files in the source directory
    cmsys::Directory dir;
    size_t           i;
    std::string      path;
    size_t           numf = files.size();
    for(i = 0; i < numf; i++)
    {
        path = files[i];

        cmCTestOptionalLog(this->CTest, HANDLER_VERBOSE_OUTPUT,
                           "Reading XML File " << path << std::endl,
                           this->Coverage.Quiet);
        if(cmSystemTools::GetFilenameLastExtension(path) == ".xml")
        {
            if(!this->ReadJacocoXML(path.c_str()))
            {
                return false;
            }
        }
    }
    return true;
}

bool
cmParseJacocoCoverage::ReadJacocoXML(const char* file)
{
    cmParseJacocoCoverage::XMLParser parser(this->CTest, this->Coverage);
    parser.ParseFile(file);
    return true;
}
