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

#include <map>
#include <set>
#include <sstream>
#include <utility>

#include "cmAlgorithms.h"
#include "cmMakefile.h"
#include "cmSystemTools.h"
#include "cmake.h"

class cmExecutionStatus;

static std::string
escape_arg(const std::string& arg)
{
    // replace ";" with "\;" so output argument lists will split correctly
    std::string escapedArg;
    for(char i : arg)
    {
        if(i == ';')
        {
            escapedArg += '\\';
        }
        escapedArg += i;
    }
    return escapedArg;
}

bool
cmParseArgumentsCommand::InitialPass(std::vector<std::string> const& args,
                                     cmExecutionStatus&)
{
    // cmake_parse_arguments(prefix options single multi <ARGN>)
    //                         1       2      3      4
    // or
    // cmake_parse_arguments(PARSE_ARGV N prefix options single multi)
    if(args.size() < 4)
    {
        this->SetError("must be called with at least 4 arguments.");
        return false;
    }

    std::vector<std::string>::const_iterator argIter = args.begin(),
                                             argEnd  = args.end();
    bool          parseFromArgV                      = false;
    unsigned long argvStart                          = 0;
    if(*argIter == "PARSE_ARGV")
    {
        if(args.size() != 6)
        {
            this->Makefile->IssueMessage(
                cmake::FATAL_ERROR,
                "PARSE_ARGV must be called with exactly 6 arguments.");
            cmSystemTools::SetFatalErrorOccured();
            return true;
        }
        parseFromArgV = true;
        argIter++;  // move past PARSE_ARGV
        if(!cmSystemTools::StringToULong(argIter->c_str(), &argvStart))
        {
            this->Makefile->IssueMessage(cmake::FATAL_ERROR,
                                         "PARSE_ARGV index '" + *argIter +
                                             "' is not an unsigned integer");
            cmSystemTools::SetFatalErrorOccured();
            return true;
        }
        argIter++;  // move past N
    }
    // the first argument is the prefix
    const std::string prefix = (*argIter++) + "_";

    // define the result maps holding key/value pairs for
    // options, single values and multi values
    typedef std::map<std::string, bool>                     options_map;
    typedef std::map<std::string, std::string>              single_map;
    typedef std::map<std::string, std::vector<std::string>> multi_map;
    options_map                                             options;
    single_map                                              singleValArgs;
    multi_map                                               multiValArgs;

    // anything else is put into a vector of unparsed strings
    std::vector<std::string> unparsed;

    // remember already defined keywords
    std::set<std::string> used_keywords;
    const std::string     dup_warning = "keyword defined more than once: ";

    // the second argument is a (cmake) list of options without argument
    std::vector<std::string> list;
    cmSystemTools::ExpandListArgument(*argIter++, list);
    for(std::string const& iter : list)
    {
        if(!used_keywords.insert(iter).second)
        {
            this->GetMakefile()->IssueMessage(cmake::WARNING,
                                              dup_warning + iter);
        }
        options[iter];  // default initialize
    }

    // the third argument is a (cmake) list of single argument options
    list.clear();
    cmSystemTools::ExpandListArgument(*argIter++, list);
    for(std::string const& iter : list)
    {
        if(!used_keywords.insert(iter).second)
        {
            this->GetMakefile()->IssueMessage(cmake::WARNING,
                                              dup_warning + iter);
        }
        singleValArgs[iter];  // default initialize
    }

    // the fourth argument is a (cmake) list of multi argument options
    list.clear();
    cmSystemTools::ExpandListArgument(*argIter++, list);
    for(std::string const& iter : list)
    {
        if(!used_keywords.insert(iter).second)
        {
            this->GetMakefile()->IssueMessage(cmake::WARNING,
                                              dup_warning + iter);
        }
        multiValArgs[iter];  // default initialize
    }

    enum insideValues
    {
        NONE,
        SINGLE,
        MULTI
    } insideValues = NONE;
    std::string currentArgName;

    list.clear();
    if(!parseFromArgV)
    {
        // Flatten ;-lists in the arguments into a single list as was done
        // by the original function(CMAKE_PARSE_ARGUMENTS).
        for(; argIter != argEnd; ++argIter)
        {
            cmSystemTools::ExpandListArgument(*argIter, list);
        }
    } else
    {
        // in the PARSE_ARGV move read the arguments from ARGC and ARGV#
        std::string   argc = this->Makefile->GetSafeDefinition("ARGC");
        unsigned long count;
        if(!cmSystemTools::StringToULong(argc.c_str(), &count))
        {
            this->Makefile->IssueMessage(
                cmake::FATAL_ERROR, "PARSE_ARGV called with ARGC='" + argc +
                                        "' that is not an unsigned integer");
            cmSystemTools::SetFatalErrorOccured();
            return true;
        }
        for(unsigned long i = argvStart; i < count; ++i)
        {
            std::ostringstream argName;
            argName << "ARGV" << i;
            const char* arg = this->Makefile->GetDefinition(argName.str());
            if(!arg)
            {
                this->Makefile->IssueMessage(cmake::FATAL_ERROR,
                                             "PARSE_ARGV called with " +
                                                 argName.str() + " not set");
                cmSystemTools::SetFatalErrorOccured();
                return true;
            }
            list.push_back(arg);
        }
    }

    // iterate over the arguments list and fill in the values where applicable
    for(std::string const& arg : list)
    {
        const options_map::iterator optIter = options.find(arg);
        if(optIter != options.end())
        {
            insideValues    = NONE;
            optIter->second = true;
            continue;
        }

        const single_map::iterator singleIter = singleValArgs.find(arg);
        if(singleIter != singleValArgs.end())
        {
            insideValues   = SINGLE;
            currentArgName = arg;
            continue;
        }

        const multi_map::iterator multiIter = multiValArgs.find(arg);
        if(multiIter != multiValArgs.end())
        {
            insideValues   = MULTI;
            currentArgName = arg;
            continue;
        }

        switch(insideValues)
        {
            case SINGLE:
                singleValArgs[currentArgName] = arg;
                insideValues                  = NONE;
                break;
            case MULTI:
                if(parseFromArgV)
                {
                    multiValArgs[currentArgName].push_back(escape_arg(arg));
                } else
                {
                    multiValArgs[currentArgName].push_back(arg);
                }
                break;
            default:
                if(parseFromArgV)
                {
                    unparsed.push_back(escape_arg(arg));
                } else
                {
                    unparsed.push_back(arg);
                }
                break;
        }
    }

    // now iterate over the collected values and update their definition
    // within the current scope. undefine if necessary.

    for(auto const& iter : options)
    {
        this->Makefile->AddDefinition(prefix + iter.first,
                                      iter.second ? "TRUE" : "FALSE");
    }
    for(auto const& iter : singleValArgs)
    {
        if(!iter.second.empty())
        {
            this->Makefile->AddDefinition(prefix + iter.first,
                                          iter.second.c_str());
        } else
        {
            this->Makefile->RemoveDefinition(prefix + iter.first);
        }
    }

    for(auto const& iter : multiValArgs)
    {
        if(!iter.second.empty())
        {
            this->Makefile->AddDefinition(
                prefix + iter.first,
                cmJoin(cmMakeRange(iter.second), ";").c_str());
        } else
        {
            this->Makefile->RemoveDefinition(prefix + iter.first);
        }
    }

    if(!unparsed.empty())
    {
        this->Makefile->AddDefinition(
            prefix + "UNPARSED_ARGUMENTS",
            cmJoin(cmMakeRange(unparsed), ";").c_str());
    } else
    {
        this->Makefile->RemoveDefinition(prefix + "UNPARSED_ARGUMENTS");
    }

    return true;
}
