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

#include "cmSystemTools.h"
#include "cmVisualStudioSlnData.h"
#include "cmsys/FStream.hxx"

#include <cassert>
#include <stack>

namespace
{
enum LineFormat
{
    LineMultiValueTag,
    LineSingleValueTag,
    LineKeyValuePair,
    LineVerbatim
};
}

class cmVisualStudioSlnParser::ParsedLine
{
public:
    bool IsComment() const;
    bool IsKeyValuePair() const;

    const std::string& GetTag() const { return this->Tag; }
    const std::string& GetArg() const { return this->Arg.first; }
    std::string        GetArgVerbatim() const;
    size_t             GetValueCount() const { return this->Values.size(); }
    const std::string& GetValue(size_t idxValue) const;
    std::string        GetValueVerbatim(size_t idxValue) const;

    void SetTag(const std::string& tag) { this->Tag = tag; }
    void SetArg(const std::string& arg) { this->Arg = StringData(arg, false); }
    void SetQuotedArg(const std::string& arg)
    {
        this->Arg = StringData(arg, true);
    }
    void AddValue(const std::string& value)
    {
        this->Values.push_back(StringData(value, false));
    }
    void AddQuotedValue(const std::string& value)
    {
        this->Values.push_back(StringData(value, true));
    }

    void CopyVerbatim(const std::string& line) { this->Tag = line; }

private:
    typedef std::pair<std::string, bool> StringData;
    std::string                          Tag;
    StringData                           Arg;
    std::vector<StringData>              Values;
    static const std::string             BadString;
    static const std::string             Quote;
};

const std::string cmVisualStudioSlnParser::ParsedLine::BadString;
const std::string cmVisualStudioSlnParser::ParsedLine::Quote("\"");

bool
cmVisualStudioSlnParser::ParsedLine::IsComment() const
{
    assert(!this->Tag.empty());
    return (this->Tag[0] == '#');
}

bool
cmVisualStudioSlnParser::ParsedLine::IsKeyValuePair() const
{
    assert(!this->Tag.empty());
    return this->Arg.first.empty() && this->Values.size() == 1;
}

std::string
cmVisualStudioSlnParser::ParsedLine::GetArgVerbatim() const
{
    if(this->Arg.second)
        return Quote + this->Arg.first + Quote;
    else
        return this->Arg.first;
}

const std::string&
cmVisualStudioSlnParser::ParsedLine::GetValue(size_t idxValue) const
{
    if(idxValue < this->Values.size())
        return this->Values[idxValue].first;
    else
        return BadString;
}

std::string
cmVisualStudioSlnParser::ParsedLine::GetValueVerbatim(size_t idxValue) const
{
    if(idxValue < this->Values.size())
    {
        const StringData& data = this->Values[idxValue];
        if(data.second)
            return Quote + data.first + Quote;
        else
            return data.first;
    } else
        return BadString;
}

class cmVisualStudioSlnParser::State
{
public:
    explicit State(DataGroupSet requestedData);

    size_t GetCurrentLine() const { return this->CurrentLine; }
    bool   ReadLine(std::istream& input, std::string& line);

    LineFormat NextLineFormat() const;

    bool Process(const cmVisualStudioSlnParser::ParsedLine& line,
                 cmSlnData&                                 output,
                 cmVisualStudioSlnParser::ResultData&       result);

    bool Finished(cmVisualStudioSlnParser::ResultData& result);

private:
    enum FileState
    {
        FileStateStart,
        FileStateTopLevel,
        FileStateProject,
        FileStateProjectDependencies,
        FileStateGlobal,
        FileStateSolutionConfigurations,
        FileStateProjectConfigurations,
        FileStateSolutionFilters,
        FileStateGlobalSection,
        FileStateIgnore
    };
    std::stack<FileState> Stack;
    std::string           EndIgnoreTag;
    DataGroupSet          RequestedData;
    size_t                CurrentLine;

    void IgnoreUntilTag(const std::string& endTag);
};

cmVisualStudioSlnParser::State::State(DataGroupSet requestedData)
: RequestedData(requestedData)
, CurrentLine(0)
{
    if(this->RequestedData.test(DataGroupProjectDependenciesBit))
        this->RequestedData.set(DataGroupProjectsBit);
    this->Stack.push(FileStateStart);
}

bool
cmVisualStudioSlnParser::State::ReadLine(std::istream& input, std::string& line)
{
    ++this->CurrentLine;
    return !std::getline(input, line).fail();
}

LineFormat
cmVisualStudioSlnParser::State::NextLineFormat() const
{
    switch(this->Stack.top())
    {
        case FileStateStart:
            return LineVerbatim;
        case FileStateTopLevel:
            return LineMultiValueTag;
        case FileStateProject:
            return LineSingleValueTag;
        case FileStateProjectDependencies:
            return LineKeyValuePair;
        case FileStateGlobal:
            return LineSingleValueTag;
        case FileStateSolutionConfigurations:
            return LineKeyValuePair;
        case FileStateProjectConfigurations:
            return LineKeyValuePair;
        case FileStateSolutionFilters:
            return LineKeyValuePair;
        case FileStateGlobalSection:
            return LineKeyValuePair;
        case FileStateIgnore:
            return LineVerbatim;
        default:
            assert(false);
            return LineVerbatim;
    }
}

bool
cmVisualStudioSlnParser::State::Process(
    const cmVisualStudioSlnParser::ParsedLine& line, cmSlnData& output,
    cmVisualStudioSlnParser::ResultData& result)
{
    assert(!line.IsComment());
    switch(this->Stack.top())
    {
        case FileStateStart:
            if(!cmSystemTools::StringStartsWith(
                   line.GetTag().c_str(),
                   "Microsoft Visual Studio Solution File"))
            {
                result.SetError(ResultErrorInputStructure,
                                this->GetCurrentLine());
                return false;
            }
            this->Stack.pop();
            this->Stack.push(FileStateTopLevel);
            break;
        case FileStateTopLevel:
            if(line.GetTag().compare("Project") == 0)
            {
                if(line.GetValueCount() != 3)
                {
                    result.SetError(ResultErrorInputStructure,
                                    this->GetCurrentLine());
                    return false;
                }
                if(this->RequestedData.test(DataGroupProjectsBit))
                {
                    if(!output.AddProject(line.GetValue(2), line.GetValue(0),
                                          line.GetValue(1)))
                    {
                        result.SetError(ResultErrorInputData,
                                        this->GetCurrentLine());
                        return false;
                    }
                    this->Stack.push(FileStateProject);
                } else
                    this->IgnoreUntilTag("EndProject");
            } else if(line.GetTag().compare("Global") == 0)
                this->Stack.push(FileStateGlobal);
            else
            {
                result.SetError(ResultErrorInputStructure,
                                this->GetCurrentLine());
                return false;
            }
            break;
        case FileStateProject:
            if(line.GetTag().compare("EndProject") == 0)
                this->Stack.pop();
            else if(line.GetTag().compare("ProjectSection") == 0)
            {
                if(line.GetArg().compare("ProjectDependencies") == 0 &&
                   line.GetValue(0).compare("postProject") == 0)
                {
                    if(this->RequestedData.test(
                           DataGroupProjectDependenciesBit))
                        this->Stack.push(FileStateProjectDependencies);
                    else
                        this->IgnoreUntilTag("EndProjectSection");
                } else
                    this->IgnoreUntilTag("EndProjectSection");
            } else
            {
                result.SetError(ResultErrorInputStructure,
                                this->GetCurrentLine());
                return false;
            }
            break;
        case FileStateProjectDependencies:
            if(line.GetTag().compare("EndProjectSection") == 0)
                this->Stack.pop();
            else if(line.IsKeyValuePair())
                // implement dependency storing here, once needed
                ;
            else
            {
                result.SetError(ResultErrorInputStructure,
                                this->GetCurrentLine());
                return false;
            }
            break;
        case FileStateGlobal:
            if(line.GetTag().compare("EndGlobal") == 0)
                this->Stack.pop();
            else if(line.GetTag().compare("GlobalSection") == 0)
            {
                if(line.GetArg().compare("SolutionConfigurationPlatforms") ==
                       0 &&
                   line.GetValue(0).compare("preSolution") == 0)
                {
                    if(this->RequestedData.test(
                           DataGroupSolutionConfigurationsBit))
                        this->Stack.push(FileStateSolutionConfigurations);
                    else
                        this->IgnoreUntilTag("EndGlobalSection");
                } else if(line.GetArg().compare(
                              "ProjectConfigurationPlatforms") == 0 &&
                          line.GetValue(0).compare("postSolution") == 0)
                {
                    if(this->RequestedData.test(
                           DataGroupProjectConfigurationsBit))
                        this->Stack.push(FileStateProjectConfigurations);
                    else
                        this->IgnoreUntilTag("EndGlobalSection");
                } else if(line.GetArg().compare("NestedProjects") == 0 &&
                          line.GetValue(0).compare("preSolution") == 0)
                {
                    if(this->RequestedData.test(DataGroupSolutionFiltersBit))
                        this->Stack.push(FileStateSolutionFilters);
                    else
                        this->IgnoreUntilTag("EndGlobalSection");
                } else if(this->RequestedData.test(
                              DataGroupGenericGlobalSectionsBit))
                    this->Stack.push(FileStateGlobalSection);
                else
                    this->IgnoreUntilTag("EndGlobalSection");
            } else
            {
                result.SetError(ResultErrorInputStructure,
                                this->GetCurrentLine());
                return false;
            }
            break;
        case FileStateSolutionConfigurations:
            if(line.GetTag().compare("EndGlobalSection") == 0)
                this->Stack.pop();
            else if(line.IsKeyValuePair())
                // implement configuration storing here, once needed
                ;
            else
            {
                result.SetError(ResultErrorInputStructure,
                                this->GetCurrentLine());
                return false;
            }
            break;
        case FileStateProjectConfigurations:
            if(line.GetTag().compare("EndGlobalSection") == 0)
                this->Stack.pop();
            else if(line.IsKeyValuePair())
                // implement configuration storing here, once needed
                ;
            else
            {
                result.SetError(ResultErrorInputStructure,
                                this->GetCurrentLine());
                return false;
            }
            break;
        case FileStateSolutionFilters:
            if(line.GetTag().compare("EndGlobalSection") == 0)
                this->Stack.pop();
            else if(line.IsKeyValuePair())
                // implement filter storing here, once needed
                ;
            else
            {
                result.SetError(ResultErrorInputStructure,
                                this->GetCurrentLine());
                return false;
            }
            break;
        case FileStateGlobalSection:
            if(line.GetTag().compare("EndGlobalSection") == 0)
                this->Stack.pop();
            else if(line.IsKeyValuePair())
                // implement section storing here, once needed
                ;
            else
            {
                result.SetError(ResultErrorInputStructure,
                                this->GetCurrentLine());
                return false;
            }
            break;
        case FileStateIgnore:
            if(line.GetTag() == this->EndIgnoreTag)
            {
                this->Stack.pop();
                this->EndIgnoreTag.clear();
            }
            break;
        default:
            result.SetError(ResultErrorBadInternalState,
                            this->GetCurrentLine());
            return false;
    }
    return true;
}

bool
cmVisualStudioSlnParser::State::Finished(
    cmVisualStudioSlnParser::ResultData& result)
{
    if(this->Stack.top() != FileStateTopLevel)
    {
        result.SetError(ResultErrorInputStructure, this->GetCurrentLine());
        return false;
    }
    result.Result = ResultOK;
    return true;
}

void
cmVisualStudioSlnParser::State::IgnoreUntilTag(const std::string& endTag)
{
    this->Stack.push(FileStateIgnore);
    this->EndIgnoreTag = endTag;
}

cmVisualStudioSlnParser::ResultData::ResultData()
: Result(ResultOK)
, ResultLine(0)
{}

void
cmVisualStudioSlnParser::ResultData::Clear()
{
    *this = ResultData();
}

void
cmVisualStudioSlnParser::ResultData::SetError(ParseResult error, size_t line)
{
    this->Result     = error;
    this->ResultLine = line;
}

const cmVisualStudioSlnParser::DataGroupSet
    cmVisualStudioSlnParser::DataGroupProjects(
        1 << cmVisualStudioSlnParser::DataGroupProjectsBit);

const cmVisualStudioSlnParser::DataGroupSet
    cmVisualStudioSlnParser::DataGroupProjectDependencies(
        1 << cmVisualStudioSlnParser::DataGroupProjectDependenciesBit);

const cmVisualStudioSlnParser::DataGroupSet
    cmVisualStudioSlnParser::DataGroupSolutionConfigurations(
        1 << cmVisualStudioSlnParser::DataGroupSolutionConfigurationsBit);

const cmVisualStudioSlnParser::DataGroupSet
    cmVisualStudioSlnParser::DataGroupProjectConfigurations(
        1 << cmVisualStudioSlnParser::DataGroupProjectConfigurationsBit);

const cmVisualStudioSlnParser::DataGroupSet
    cmVisualStudioSlnParser::DataGroupSolutionFilters(
        1 << cmVisualStudioSlnParser::DataGroupSolutionFiltersBit);

const cmVisualStudioSlnParser::DataGroupSet
    cmVisualStudioSlnParser::DataGroupGenericGlobalSections(
        1 << cmVisualStudioSlnParser::DataGroupGenericGlobalSectionsBit);

const cmVisualStudioSlnParser::DataGroupSet
    cmVisualStudioSlnParser::DataGroupAll(~0);

bool
cmVisualStudioSlnParser::Parse(std::istream& input, cmSlnData& output,
                               DataGroupSet dataGroups)
{
    this->LastResult.Clear();
    if(!this->IsDataGroupSetSupported(dataGroups))
    {
        this->LastResult.SetError(ResultErrorUnsupportedDataGroup, 0);
        return false;
    }
    State state(dataGroups);
    return this->ParseImpl(input, output, state);
}

bool
cmVisualStudioSlnParser::ParseFile(const std::string& file, cmSlnData& output,
                                   DataGroupSet dataGroups)
{
    this->LastResult.Clear();
    if(!this->IsDataGroupSetSupported(dataGroups))
    {
        this->LastResult.SetError(ResultErrorUnsupportedDataGroup, 0);
        return false;
    }
    cmsys::ifstream f(file.c_str());
    if(!f)
    {
        this->LastResult.SetError(ResultErrorOpeningInput, 0);
        return false;
    }
    State state(dataGroups);
    return this->ParseImpl(f, output, state);
}

cmVisualStudioSlnParser::ParseResult
cmVisualStudioSlnParser::GetParseResult() const
{
    return this->LastResult.Result;
}

size_t
cmVisualStudioSlnParser::GetParseResultLine() const
{
    return this->LastResult.ResultLine;
}

bool
cmVisualStudioSlnParser::GetParseHadBOM() const
{
    return this->LastResult.HadBOM;
}

bool
cmVisualStudioSlnParser::IsDataGroupSetSupported(DataGroupSet dataGroups) const
{
    return (dataGroups & DataGroupProjects) == dataGroups;
    // only supporting DataGroupProjects for now
}

bool
cmVisualStudioSlnParser::ParseImpl(std::istream& input, cmSlnData& output,
                                   State& state)
{
    std::string line;
    // Does the .sln start with a Byte Order Mark?
    if(!this->ParseBOM(input, line, state))
        return false;
    do
    {
        line = cmSystemTools::TrimWhitespace(line);
        if(line.empty())
            continue;
        ParsedLine parsedLine;
        switch(state.NextLineFormat())
        {
            case LineMultiValueTag:
                if(!this->ParseMultiValueTag(line, parsedLine, state))
                    return false;
                break;
            case LineSingleValueTag:
                if(!this->ParseSingleValueTag(line, parsedLine, state))
                    return false;
                break;
            case LineKeyValuePair:
                if(!this->ParseKeyValuePair(line, parsedLine, state))
                    return false;
                break;
            case LineVerbatim:
                parsedLine.CopyVerbatim(line);
                break;
        }
        if(parsedLine.IsComment())
            continue;
        if(!state.Process(parsedLine, output, this->LastResult))
            return false;
    } while(state.ReadLine(input, line));
    return state.Finished(this->LastResult);
}

bool
cmVisualStudioSlnParser::ParseBOM(std::istream& input, std::string& line,
                                  State& state)
{
    char bom[4];
    if(!input.get(bom, 4))
    {
        this->LastResult.SetError(ResultErrorReadingInput, 1);
        return false;
    }
    this->LastResult.HadBOM =
        (bom[0] == char(0xEF) && bom[1] == char(0xBB) && bom[2] == char(0xBF));
    if(!state.ReadLine(input, line))
    {
        this->LastResult.SetError(ResultErrorReadingInput, 1);
        return false;
    }
    if(!this->LastResult.HadBOM)
        line = bom + line;  // it wasn't a BOM, prepend it to first line
    return true;
}

bool
cmVisualStudioSlnParser::ParseMultiValueTag(const std::string& line,
                                            ParsedLine&        parsedLine,
                                            State&             state)
{
    size_t             idxEqualSign = line.find('=');
    const std::string& fullTag      = line.substr(0, idxEqualSign);
    if(!this->ParseTag(fullTag, parsedLine, state))
        return false;
    if(idxEqualSign != line.npos)
    {
        size_t idxFieldStart = idxEqualSign + 1;
        if(idxFieldStart < line.size())
        {
            size_t idxParsing = idxFieldStart;
            bool   inQuotes   = false;
            for(;;)
            {
                idxParsing     = line.find_first_of(",\"", idxParsing);
                bool fieldOver = false;
                if(idxParsing == line.npos)
                {
                    fieldOver = true;
                    if(inQuotes)
                    {
                        this->LastResult.SetError(ResultErrorInputStructure,
                                                  state.GetCurrentLine());
                        return false;
                    }
                } else if(line[idxParsing] == ',' && !inQuotes)
                    fieldOver = true;
                else if(line[idxParsing] == '"')
                    inQuotes = !inQuotes;
                if(fieldOver)
                {
                    if(!this->ParseValue(
                           line.substr(idxFieldStart,
                                       idxParsing - idxFieldStart),
                           parsedLine))
                        return false;
                    if(idxParsing == line.npos)
                        break;  // end of last field
                    idxFieldStart = idxParsing + 1;
                }
                ++idxParsing;
            }
        }
    }
    return true;
}

bool
cmVisualStudioSlnParser::ParseSingleValueTag(const std::string& line,
                                             ParsedLine&        parsedLine,
                                             State&             state)
{
    size_t             idxEqualSign = line.find('=');
    const std::string& fullTag      = line.substr(0, idxEqualSign);
    if(!this->ParseTag(fullTag, parsedLine, state))
        return false;
    if(idxEqualSign != line.npos)
    {
        if(!this->ParseValue(line.substr(idxEqualSign + 1), parsedLine))
            return false;
    }
    return true;
}

bool
cmVisualStudioSlnParser::ParseKeyValuePair(const std::string& line,
                                           ParsedLine&        parsedLine,
                                           State& /*state*/)
{
    size_t idxEqualSign = line.find('=');
    if(idxEqualSign == line.npos)
    {
        parsedLine.CopyVerbatim(line);
        return true;
    }
    const std::string& key = line.substr(0, idxEqualSign);
    parsedLine.SetTag(cmSystemTools::TrimWhitespace(key));
    const std::string& value = line.substr(idxEqualSign + 1);
    parsedLine.AddValue(cmSystemTools::TrimWhitespace(value));
    return true;
}

bool
cmVisualStudioSlnParser::ParseTag(const std::string& fullTag,
                                  ParsedLine& parsedLine, State& state)
{
    size_t idxLeftParen = fullTag.find('(');
    if(idxLeftParen == fullTag.npos)
    {
        parsedLine.SetTag(cmSystemTools::TrimWhitespace(fullTag));
        return true;
    }
    parsedLine.SetTag(
        cmSystemTools::TrimWhitespace(fullTag.substr(0, idxLeftParen)));
    size_t idxRightParen = fullTag.rfind(')');
    if(idxRightParen == fullTag.npos)
    {
        this->LastResult.SetError(ResultErrorInputStructure,
                                  state.GetCurrentLine());
        return false;
    }
    const std::string& arg = cmSystemTools::TrimWhitespace(
        fullTag.substr(idxLeftParen + 1, idxRightParen - idxLeftParen - 1));
    if(arg[0] == '"')
    {
        if(arg[arg.size() - 1] != '"')
        {
            this->LastResult.SetError(ResultErrorInputStructure,
                                      state.GetCurrentLine());
            return false;
        }
        parsedLine.SetQuotedArg(arg.substr(1, arg.size() - 2));
    } else
        parsedLine.SetArg(arg);
    return true;
}

bool
cmVisualStudioSlnParser::ParseValue(const std::string& value,
                                    ParsedLine&        parsedLine)
{
    const std::string& trimmed = cmSystemTools::TrimWhitespace(value);
    if(trimmed.empty())
        parsedLine.AddValue(trimmed);
    else if(trimmed[0] == '"' && trimmed[trimmed.size() - 1] == '"')
        parsedLine.AddQuotedValue(trimmed.substr(1, trimmed.size() - 2));
    else
        parsedLine.AddValue(trimmed);
    return true;
}
