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

#include <sstream>

#include "cmExecutionStatus.h"
#include "cmListFileCache.h"
#include "cmMakefile.h"
#include "cmSystemTools.h"
#include "cmVariableWatch.h"
#include "cmake.h"

struct cmVariableWatchCallbackData
{
    bool        InCallback;
    std::string Command;
};

static void
cmVariableWatchCommandVariableAccessed(const std::string& variable,
                                       int access_type, void* client_data,
                                       const char*       newValue,
                                       const cmMakefile* mf)
{
    cmVariableWatchCallbackData* data =
        static_cast<cmVariableWatchCallbackData*>(client_data);

    if(data->InCallback)
    {
        return;
    }
    data->InCallback = true;

    cmListFileFunction newLFF;
    cmListFileArgument arg;
    bool               processed = false;
    const char* accessString = cmVariableWatch::GetAccessAsString(access_type);
    const char* currentListFile = mf->GetDefinition("CMAKE_CURRENT_LIST_FILE");

    /// Ultra bad!!
    cmMakefile* makefile = const_cast<cmMakefile*>(mf);

    std::string stack = makefile->GetProperty("LISTFILE_STACK");
    if(!data->Command.empty())
    {
        newLFF.Arguments.clear();
        newLFF.Arguments.emplace_back(variable, cmListFileArgument::Quoted,
                                      9999);
        newLFF.Arguments.emplace_back(accessString, cmListFileArgument::Quoted,
                                      9999);
        newLFF.Arguments.emplace_back(newValue ? newValue : "",
                                      cmListFileArgument::Quoted, 9999);
        newLFF.Arguments.emplace_back(currentListFile,
                                      cmListFileArgument::Quoted, 9999);
        newLFF.Arguments.emplace_back(stack, cmListFileArgument::Quoted, 9999);
        newLFF.Name = data->Command;
        newLFF.Line = 9999;
        cmExecutionStatus status;
        if(!makefile->ExecuteCommand(newLFF, status))
        {
            std::ostringstream error;
            error << "Error in cmake code at\nUnknown:0:\n"
                  << "A command failed during the invocation of callback \""
                  << data->Command << "\".";
            cmSystemTools::Error(error.str().c_str());
            data->InCallback = false;
            return;
        }
        processed = true;
    }
    if(!processed)
    {
        std::ostringstream msg;
        msg << "Variable \"" << variable << "\" was accessed using "
            << accessString << " with value \"" << (newValue ? newValue : "")
            << "\".";
        makefile->IssueMessage(cmake::LOG, msg.str());
    }

    data->InCallback = false;
}

static void
deleteVariableWatchCallbackData(void* client_data)
{
    cmVariableWatchCallbackData* data =
        static_cast<cmVariableWatchCallbackData*>(client_data);
    delete data;
}

cmVariableWatchCommand::cmVariableWatchCommand() {}

cmVariableWatchCommand::~cmVariableWatchCommand()
{
    for(std::string const& wv : this->WatchedVariables)
    {
        this->Makefile->GetCMakeInstance()->GetVariableWatch()->RemoveWatch(
            wv, cmVariableWatchCommandVariableAccessed);
    }
}

bool
cmVariableWatchCommand::InitialPass(std::vector<std::string> const& args,
                                    cmExecutionStatus&)
{
    if(args.empty())
    {
        this->SetError("must be called with at least one argument.");
        return false;
    }
    std::string const& variable = args[0];
    std::string        command;
    if(args.size() > 1)
    {
        command = args[1];
    }
    if(variable == "CMAKE_CURRENT_LIST_FILE")
    {
        std::ostringstream ostr;
        ostr << "cannot be set on the variable: " << variable;
        this->SetError(ostr.str());
        return false;
    }

    cmVariableWatchCallbackData* data = new cmVariableWatchCallbackData;

    data->InCallback = false;
    data->Command    = command;

    this->WatchedVariables.insert(variable);
    if(!this->Makefile->GetCMakeInstance()->GetVariableWatch()->AddWatch(
           variable, cmVariableWatchCommandVariableAccessed, data,
           deleteVariableWatchCallbackData))
    {
        deleteVariableWatchCallbackData(data);
        return false;
    }

    return true;
}
