// system headers
#include <algorithm>
#include <iostream>
#include <memory>
#include <sstream>
#include <stdexcept>
#include <utility>
#include <unistd.h>
#include <memory.h>
#include <wait.h>

// local headers
#include "includes/process.h"
#include "includes/subprocess.h"
#include "includes/assert.h"

// shorter than using namespace ...
using namespace linuxdeploy::subprocess;

int process::pid() const {
    return child_pid_;
}

int process::stdout_fd() const {
    return stdout_fd_;
}

int process::stderr_fd() const {
    return stderr_fd_;
}

process::process(std::initializer_list<std::string> args, const subprocess_env_map_t& env)
    : process(std::vector<std::string>(args), env) {}

process::process(const std::vector<std::string>& args, const subprocess_env_map_t& env) {
    // preconditions
    util::assert::assert_not_empty(args);

    // pipes for both stdout and stderr
    // the order is, as seen from the child: [read, write]
    int stdout_pipe_fds[2];
    int stderr_pipe_fds[2];

    // FIXME: for debugging of #150
    auto create_pipe = [](int fds[]) {
        const auto rv = pipe(fds);

        if (rv != 0) {
            const auto error = errno;
            throw std::logic_error("failed to create pipe: " + std::string(strerror(error)));
        }
    };

    // create actual pipes
    create_pipe(stdout_pipe_fds);
    create_pipe(stderr_pipe_fds);

    // create child process
    child_pid_ = fork();

    if (child_pid_ < 0) {
        throw std::runtime_error{"fork() failed"};
    }

    if (child_pid_ == 0) {
        // we're in the child process

        // first step: close the read end of both pipes
        close_pipe_fd_(stdout_pipe_fds[READ_END_]);
        close_pipe_fd_(stderr_pipe_fds[READ_END_]);

        auto connect_fd = [](int fd, int fileno) {
            for (;;) {
                if (dup2(fd, fileno) == -1) {
                    const auto error = errno;
                    if (error != EINTR) {
                        throw std::logic_error{"failed to connect pipes: " + std::string(strerror(error))};
                    }
                    continue;
                }

                break;
            }
        };

        connect_fd(stdout_pipe_fds[WRITE_END_], STDOUT_FILENO);
        connect_fd(stderr_pipe_fds[WRITE_END_], STDERR_FILENO);

        // now, we also have to close the write end of both pipes
        close_pipe_fd_(stdout_pipe_fds[WRITE_END_]);
        close_pipe_fd_(stderr_pipe_fds[WRITE_END_]);

        // prepare arguments for exec*
        auto exec_args = make_args_vector_(args);
        auto exec_env = make_env_vector_(env);

        // call subprocess
        execvpe(args.front().c_str(), exec_args.data(), exec_env.data());

        // only reached if exec* fails

        // clean up memory if exec should ever return
        // prevents memleaks if the exception below would be handled by a caller
        auto deleter = [](char* ptr) {
            free(ptr);
            ptr = nullptr;
        };

        std::for_each(exec_args.begin(), exec_args.end(), deleter);
        std::for_each(exec_env.begin(), exec_env.end(), deleter);

        throw std::runtime_error{"exec() failed: " + std::string(strerror(errno))};
    }

    // parent code

    // we do not intend to write to the processes
    close_pipe_fd_(stdout_pipe_fds[WRITE_END_]);
    close_pipe_fd_(stderr_pipe_fds[WRITE_END_]);

    // store file descriptors
    stdout_fd_ = stdout_pipe_fds[READ_END_];
    stderr_fd_ = stderr_pipe_fds[READ_END_];
}

int process::close() {
    if (!exited_) {
        close_pipe_fd_(stdout_fd_);
        stdout_fd_ = -1;

        close_pipe_fd_(stderr_fd_);
        stderr_fd_ = -1;

        {
            int status;

            if (waitpid(child_pid_, &status, 0) == -1) {
                throw std::logic_error{"waitpid() failed"};
            }

            exited_ = true;
            exit_code_ = check_waitpid_status_(status);
        }
    }

    return exit_code_;
}

process::~process() {
    (void) close();
}

std::vector<char*> process::make_args_vector_(const std::vector<std::string>& args) {
    std::vector<char*> rv{};
    rv.reserve(args.size());

    for (const auto& arg : args) {
        rv.emplace_back(strdup(arg.c_str()));
    }

    // execv* want a nullptr-terminated array
    rv.emplace_back(nullptr);

    return rv;
}

std::vector<char*> process::make_env_vector_(const subprocess_env_map_t& env) {
    std::vector<char*> rv;

    // first, copy existing environment
    // we cannot reserve space in the vector unfortunately, as we don't know the size of environ before the iteration
    if (environ != nullptr) {
        for (auto** current_env_var = environ; *current_env_var != nullptr; ++current_env_var) {
            rv.emplace_back(strdup(*current_env_var));
        }
    }

    // add own environment variables, overwriting existing ones if necessary
    for (const auto& env_var : env) {
        const auto& key = env_var.first;
        const auto& value = env_var.second;

        auto predicate = [&key](char* existing_env_var) {
            char* equal_sign = strstr(existing_env_var, "=");

            if (equal_sign == nullptr) {
                throw std::runtime_error{"no equal sign in environment variable"};
            }

            return strncmp(existing_env_var, key.c_str(), std::distance(equal_sign, existing_env_var)) == 0;
        };

        // delete existing env var, if any
        rv.erase(std::remove_if(rv.begin(), rv.end(), predicate), rv.end());

        // insert new value
        std::ostringstream oss;
        oss << key;
        oss << "=";
        oss << value;

        rv.emplace_back(strdup(oss.str().c_str()));
    }

    // exec*e want a nullptr-terminated array
    rv.emplace_back(nullptr);

    return rv;
}

void process::kill(int signal) const {
    if (::kill(child_pid_, signal) != 0) {
        throw std::logic_error{"failed to kill child process"};
    }

    if (waitpid(child_pid_, nullptr, 0)) {
        throw std::logic_error{"failed to wait for killed child"};
    }
}

bool process::is_running() {
    if (exited_) {
        return false;
    }

    int status;
    auto result = waitpid(child_pid_, &status, WNOHANG);

    if (result == 0) {
        return true;
    }

    if (result == child_pid_) {
        // TODO: extract the following lines from both this method and close() to eliminate duplicate code
        close_pipe_fd_(stdout_fd_);
        stdout_fd_ = -1;

        close_pipe_fd_(stderr_fd_);
        stderr_fd_ = -1;

        exited_ = true;
        exit_code_ = check_waitpid_status_(status);

        return false;
    }

    if (result < 0) {
        // TODO: check errno == ECHILD
        throw std::logic_error{"waitpid() failed: " + std::string(strerror(errno))};
    }

    // can only happen if waitpid() returns an unknown process ID
    throw std::logic_error{"unknown error occured"};
}

int process::check_waitpid_status_(int status) {
    if (WIFSIGNALED(status) != 0) {
        // TODO: consider treating child exit caused by signals separately
        return WTERMSIG(status);
    } else if (WIFEXITED(status) != 0) {
        return WEXITSTATUS(status);
    }

    throw std::logic_error{"unknown child process state"};
}

void process::close_pipe_fd_(int fd) {
    const auto rv = ::close(fd);

    if (rv != 0) {
        const auto error = errno;
        throw std::logic_error("failed to close pipe fd: " + std::string(strerror(error)));
    }
}
