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

#include "cmsys/Process.h"
#include <ctype.h> /* isspace */
#include <sstream>
#include <stdio.h>

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

class cmExecutionStatus;

static bool
cmExecuteProcessCommandIsWhitespace(char c)
{
    return (isspace(static_cast<int>(c)) || c == '\n' || c == '\r');
}

void
cmExecuteProcessCommandFixText(std::vector<char>& output,
                               bool               strip_trailing_whitespace);
void
cmExecuteProcessCommandAppend(std::vector<char>& output, const char* data,
                              int length);

// cmExecuteProcessCommand
bool
cmExecuteProcessCommand::InitialPass(std::vector<std::string> const& args,
                                     cmExecutionStatus&)
{
    if(args.empty())
    {
        this->SetError("called with incorrect number of arguments");
        return false;
    }
    std::vector<std::vector<const char*>> cmds;
    std::string                           arguments;
    bool                                  doing_command        = false;
    size_t                                command_index        = 0;
    bool                                  output_quiet         = false;
    bool                                  error_quiet          = false;
    bool                      output_strip_trailing_whitespace = false;
    bool                      error_strip_trailing_whitespace  = false;
    std::string               timeout_string;
    std::string               input_file;
    std::string               output_file;
    std::string               error_file;
    std::string               output_variable;
    std::string               error_variable;
    std::string               result_variable;
    std::string               results_variable;
    std::string               working_directory;
    cmProcessOutput::Encoding encoding = cmProcessOutput::None;
    for(size_t i = 0; i < args.size(); ++i)
    {
        if(args[i] == "COMMAND")
        {
            doing_command = true;
            command_index = cmds.size();
            cmds.push_back(std::vector<const char*>());
        } else if(args[i] == "OUTPUT_VARIABLE")
        {
            doing_command = false;
            if(++i < args.size())
            {
                output_variable = args[i];
            } else
            {
                this->SetError(" called with no value for OUTPUT_VARIABLE.");
                return false;
            }
        } else if(args[i] == "ERROR_VARIABLE")
        {
            doing_command = false;
            if(++i < args.size())
            {
                error_variable = args[i];
            } else
            {
                this->SetError(" called with no value for ERROR_VARIABLE.");
                return false;
            }
        } else if(args[i] == "RESULT_VARIABLE")
        {
            doing_command = false;
            if(++i < args.size())
            {
                result_variable = args[i];
            } else
            {
                this->SetError(" called with no value for RESULT_VARIABLE.");
                return false;
            }
        } else if(args[i] == "RESULTS_VARIABLE")
        {
            doing_command = false;
            if(++i < args.size())
            {
                results_variable = args[i];
            } else
            {
                this->SetError(" called with no value for RESULTS_VARIABLE.");
                return false;
            }
        } else if(args[i] == "WORKING_DIRECTORY")
        {
            doing_command = false;
            if(++i < args.size())
            {
                working_directory = args[i];
            } else
            {
                this->SetError(" called with no value for WORKING_DIRECTORY.");
                return false;
            }
        } else if(args[i] == "INPUT_FILE")
        {
            doing_command = false;
            if(++i < args.size())
            {
                input_file = args[i];
            } else
            {
                this->SetError(" called with no value for INPUT_FILE.");
                return false;
            }
        } else if(args[i] == "OUTPUT_FILE")
        {
            doing_command = false;
            if(++i < args.size())
            {
                output_file = args[i];
            } else
            {
                this->SetError(" called with no value for OUTPUT_FILE.");
                return false;
            }
        } else if(args[i] == "ERROR_FILE")
        {
            doing_command = false;
            if(++i < args.size())
            {
                error_file = args[i];
            } else
            {
                this->SetError(" called with no value for ERROR_FILE.");
                return false;
            }
        } else if(args[i] == "TIMEOUT")
        {
            doing_command = false;
            if(++i < args.size())
            {
                timeout_string = args[i];
            } else
            {
                this->SetError(" called with no value for TIMEOUT.");
                return false;
            }
        } else if(args[i] == "OUTPUT_QUIET")
        {
            doing_command = false;
            output_quiet  = true;
        } else if(args[i] == "ERROR_QUIET")
        {
            doing_command = false;
            error_quiet   = true;
        } else if(args[i] == "OUTPUT_STRIP_TRAILING_WHITESPACE")
        {
            doing_command                    = false;
            output_strip_trailing_whitespace = true;
        } else if(args[i] == "ERROR_STRIP_TRAILING_WHITESPACE")
        {
            doing_command                   = false;
            error_strip_trailing_whitespace = true;
        } else if(args[i] == "ENCODING")
        {
            doing_command = false;
            if(++i < args.size())
            {
                encoding = cmProcessOutput::FindEncoding(args[i]);
            } else
            {
                this->SetError(" called with no value for ENCODING.");
                return false;
            }
        } else if(doing_command)
        {
            cmds[command_index].push_back(args[i].c_str());
        } else
        {
            std::ostringstream e;
            e << " given unknown argument \"" << args[i] << "\".";
            this->SetError(e.str());
            return false;
        }
    }

    if(!this->Makefile->CanIWriteThisFile(output_file))
    {
        std::string e = "attempted to output into a file: " + output_file +
                        " into a source directory.";
        this->SetError(e);
        cmSystemTools::SetFatalErrorOccured();
        return false;
    }

    // Check for commands given.
    if(cmds.empty())
    {
        this->SetError(" called with no COMMAND argument.");
        return false;
    }
    for(auto& cmd : cmds)
    {
        if(cmd.empty())
        {
            this->SetError(" given COMMAND argument with no value.");
            return false;
        }
        // Add the null terminating pointer to the command argument list.
        cmd.push_back(nullptr);
    }

    // Parse the timeout string.
    double timeout = -1;
    if(!timeout_string.empty())
    {
        if(sscanf(timeout_string.c_str(), "%lg", &timeout) != 1)
        {
            this->SetError(
                " called with TIMEOUT value that could not be parsed.");
            return false;
        }
    }

    // Create a process instance.
    cmsysProcess* cp = cmsysProcess_New();

    // Set the command sequence.
    for(auto const& cmd : cmds)
    {
        cmsysProcess_AddCommand(cp, &*cmd.begin());
    }

    // Set the process working directory.
    if(!working_directory.empty())
    {
        cmsysProcess_SetWorkingDirectory(cp, working_directory.c_str());
    }

    // Always hide the process window.
    cmsysProcess_SetOption(cp, cmsysProcess_Option_HideWindow, 1);

    // Check the output variables.
    bool merge_output = false;
    if(!input_file.empty())
    {
        cmsysProcess_SetPipeFile(cp, cmsysProcess_Pipe_STDIN,
                                 input_file.c_str());
    }
    if(!output_file.empty())
    {
        cmsysProcess_SetPipeFile(cp, cmsysProcess_Pipe_STDOUT,
                                 output_file.c_str());
    }
    if(!error_file.empty())
    {
        if(error_file == output_file)
        {
            merge_output = true;
        } else
        {
            cmsysProcess_SetPipeFile(cp, cmsysProcess_Pipe_STDERR,
                                     error_file.c_str());
        }
    }
    if(!output_variable.empty() && output_variable == error_variable)
    {
        merge_output = true;
    }
    if(merge_output)
    {
        cmsysProcess_SetOption(cp, cmsysProcess_Option_MergeOutput, 1);
    }

    // Set the timeout if any.
    if(timeout >= 0)
    {
        cmsysProcess_SetTimeout(cp, timeout);
    }

    // Start the process.
    cmsysProcess_Execute(cp);

    // Read the process output.
    std::vector<char> tempOutput;
    std::vector<char> tempError;
    int               length;
    char*             data;
    int               p;
    cmProcessOutput   processOutput(encoding);
    std::string       strdata;
    while((p = cmsysProcess_WaitForData(cp, &data, &length, nullptr), p))
    {
        // Put the output in the right place.
        if(p == cmsysProcess_Pipe_STDOUT && !output_quiet)
        {
            if(output_variable.empty())
            {
                processOutput.DecodeText(data, length, strdata, 1);
                cmSystemTools::Stdout(strdata.c_str(), strdata.size());
            } else
            {
                cmExecuteProcessCommandAppend(tempOutput, data, length);
            }
        } else if(p == cmsysProcess_Pipe_STDERR && !error_quiet)
        {
            if(error_variable.empty())
            {
                processOutput.DecodeText(data, length, strdata, 2);
                cmSystemTools::Stderr(strdata.c_str(), strdata.size());
            } else
            {
                cmExecuteProcessCommandAppend(tempError, data, length);
            }
        }
    }
    if(!output_quiet && output_variable.empty())
    {
        processOutput.DecodeText(std::string(), strdata, 1);
        if(!strdata.empty())
        {
            cmSystemTools::Stdout(strdata.c_str(), strdata.size());
        }
    }
    if(!error_quiet && error_variable.empty())
    {
        processOutput.DecodeText(std::string(), strdata, 2);
        if(!strdata.empty())
        {
            cmSystemTools::Stderr(strdata.c_str(), strdata.size());
        }
    }

    // All output has been read.  Wait for the process to exit.
    cmsysProcess_WaitForExit(cp, nullptr);
    processOutput.DecodeText(tempOutput, tempOutput);
    processOutput.DecodeText(tempError, tempError);

    // Fix the text in the output strings.
    cmExecuteProcessCommandFixText(tempOutput,
                                   output_strip_trailing_whitespace);
    cmExecuteProcessCommandFixText(tempError, error_strip_trailing_whitespace);

    // Store the output obtained.
    if(!output_variable.empty() && !tempOutput.empty())
    {
        this->Makefile->AddDefinition(output_variable, &*tempOutput.begin());
    }
    if(!merge_output && !error_variable.empty() && !tempError.empty())
    {
        this->Makefile->AddDefinition(error_variable, &*tempError.begin());
    }

    // Store the result of running the process.
    if(!result_variable.empty())
    {
        switch(cmsysProcess_GetState(cp))
        {
            case cmsysProcess_State_Exited:
            {
                int  v = cmsysProcess_GetExitValue(cp);
                char buf[16];
                sprintf(buf, "%d", v);
                this->Makefile->AddDefinition(result_variable, buf);
            }
            break;
            case cmsysProcess_State_Exception:
                this->Makefile->AddDefinition(
                    result_variable, cmsysProcess_GetExceptionString(cp));
                break;
            case cmsysProcess_State_Error:
                this->Makefile->AddDefinition(result_variable,
                                              cmsysProcess_GetErrorString(cp));
                break;
            case cmsysProcess_State_Expired:
                this->Makefile->AddDefinition(
                    result_variable, "Process terminated due to timeout");
                break;
        }
    }
    // Store the result of running the processes.
    if(!results_variable.empty())
    {
        switch(cmsysProcess_GetState(cp))
        {
            case cmsysProcess_State_Exited:
            {
                std::vector<std::string> res;
                for(size_t i = 0; i < cmds.size(); ++i)
                {
                    switch(
                        cmsysProcess_GetStateByIndex(cp, static_cast<int>(i)))
                    {
                        case kwsysProcess_StateByIndex_Exited:
                        {
                            int exitCode = cmsysProcess_GetExitValueByIndex(
                                cp, static_cast<int>(i));
                            char buf[16];
                            sprintf(buf, "%d", exitCode);
                            res.push_back(buf);
                        }
                        break;
                        case kwsysProcess_StateByIndex_Exception:
                            res.push_back(
                                cmsysProcess_GetExceptionStringByIndex(
                                    cp, static_cast<int>(i)));
                            break;
                        case kwsysProcess_StateByIndex_Error:
                        default:
                            res.push_back(
                                "Error getting the child return code");
                            break;
                    }
                }
                this->Makefile->AddDefinition(results_variable,
                                              cmJoin(res, ";").c_str());
            }
            break;
            case cmsysProcess_State_Exception:
                this->Makefile->AddDefinition(
                    results_variable, cmsysProcess_GetExceptionString(cp));
                break;
            case cmsysProcess_State_Error:
                this->Makefile->AddDefinition(results_variable,
                                              cmsysProcess_GetErrorString(cp));
                break;
            case cmsysProcess_State_Expired:
                this->Makefile->AddDefinition(
                    results_variable, "Process terminated due to timeout");
                break;
        }
    }

    // Delete the process instance.
    cmsysProcess_Delete(cp);

    return true;
}

void
cmExecuteProcessCommandFixText(std::vector<char>& output,
                               bool               strip_trailing_whitespace)
{
    // Remove \0 characters and the \r part of \r\n pairs.
    unsigned int in_index  = 0;
    unsigned int out_index = 0;
    while(in_index < output.size())
    {
        char c = output[in_index++];
        if((c != '\r' ||
            !(in_index < output.size() && output[in_index] == '\n')) &&
           c != '\0')
        {
            output[out_index++] = c;
        }
    }

    // Remove trailing whitespace if requested.
    if(strip_trailing_whitespace)
    {
        while(out_index > 0 &&
              cmExecuteProcessCommandIsWhitespace(output[out_index - 1]))
        {
            --out_index;
        }
    }

    // Shrink the vector to the size needed.
    output.resize(out_index);

    // Put a terminator on the text string.
    output.push_back('\0');
}

void
cmExecuteProcessCommandAppend(std::vector<char>& output, const char* data,
                              int length)
{
#if defined(__APPLE__)
    // HACK on Apple to work around bug with inserting at the
    // end of an empty vector.  This resulted in random failures
    // that were hard to reproduce.
    if(output.empty() && length > 0)
    {
        output.push_back(data[0]);
        ++data;
        --length;
    }
#endif
    output.insert(output.end(), data, data + length);
}
