//===-- RNBContext.cpp ------------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
//  Created by Greg Clayton on 12/12/07.
//
//===----------------------------------------------------------------------===//

#include "RNBContext.h"

#include <sstream>
#include <sys/stat.h>

#if defined(__APPLE__)
#include <pthread.h>
#include <sched.h>
#endif

#include "CFString.h"
#include "DNB.h"
#include "DNBLog.h"
#include "RNBRemote.h"

// Destructor
RNBContext::~RNBContext() { SetProcessID(INVALID_NUB_PROCESS); }

// RNBContext constructor

const char *RNBContext::EnvironmentAtIndex(size_t index) {
  if (index < m_env_vec.size())
    return m_env_vec[index].c_str();
  else
    return NULL;
}

static std::string GetEnvironmentKey(const std::string &env) {
  std::string key = env.substr(0, env.find('='));
  if (!key.empty() && key.back() == '=')
    key.pop_back();
  return key;
}

void RNBContext::PushEnvironmentIfNeeded(const char *arg) {
  if (!arg)
    return;
  std::string arg_key = GetEnvironmentKey(arg);

  for (const std::string &entry: m_env_vec) {
    if (arg_key == GetEnvironmentKey(entry))
      return;
  }
  m_env_vec.push_back(arg);
}

const char *RNBContext::ArgumentAtIndex(size_t index) {
  if (index < m_arg_vec.size())
    return m_arg_vec[index].c_str();
  else
    return NULL;
}

bool RNBContext::SetWorkingDirectory(const char *path) {
  struct stat working_directory_stat;
  if (::stat(path, &working_directory_stat) != 0) {
    m_working_directory.clear();
    return false;
  }
  m_working_directory.assign(path);
  return true;
}

void RNBContext::SetProcessID(nub_process_t pid) {
  // Delete and events we created
  if (m_pid != INVALID_NUB_PROCESS) {
    StopProcessStatusThread();
    // Unregister this context as a client of the process's events.
  }
  // Assign our new process ID
  m_pid = pid;

  if (pid != INVALID_NUB_PROCESS) {
    StartProcessStatusThread();
  }
}

void RNBContext::StartProcessStatusThread() {
  DNBLogThreadedIf(LOG_RNB_PROC, "RNBContext::%s called", __FUNCTION__);
  if ((m_events.GetEventBits() & event_proc_thread_running) == 0) {
    int err = ::pthread_create(&m_pid_pthread, NULL,
                               ThreadFunctionProcessStatus, this);
    if (err == 0) {
      // Our thread was successfully kicked off, wait for it to
      // set the started event so we can safely continue
      m_events.WaitForSetEvents(event_proc_thread_running);
      DNBLogThreadedIf(LOG_RNB_PROC, "RNBContext::%s thread got started!",
                       __FUNCTION__);
    } else {
      DNBLogThreadedIf(LOG_RNB_PROC,
                       "RNBContext::%s thread failed to start: err = %i",
                       __FUNCTION__, err);
      m_events.ResetEvents(event_proc_thread_running);
      m_events.SetEvents(event_proc_thread_exiting);
    }
  }
}

void RNBContext::StopProcessStatusThread() {
  DNBLogThreadedIf(LOG_RNB_PROC, "RNBContext::%s called", __FUNCTION__);
  if ((m_events.GetEventBits() & event_proc_thread_running) ==
      event_proc_thread_running) {
    struct timespec timeout_abstime;
    DNBTimer::OffsetTimeOfDay(&timeout_abstime, 2, 0);
    // Wait for 2 seconds for the rx thread to exit
    if (m_events.WaitForSetEvents(RNBContext::event_proc_thread_exiting,
                                  &timeout_abstime) ==
        RNBContext::event_proc_thread_exiting) {
      DNBLogThreadedIf(LOG_RNB_PROC,
                       "RNBContext::%s thread stopped as requeseted",
                       __FUNCTION__);
    } else {
      DNBLogThreadedIf(LOG_RNB_PROC,
                       "RNBContext::%s thread did not stop in 2 seconds...",
                       __FUNCTION__);
      // Kill the RX thread???
    }
  }
}

// This thread's sole purpose is to watch for any status changes in the
// child process.
void *RNBContext::ThreadFunctionProcessStatus(void *arg) {
  RNBRemoteSP remoteSP(g_remoteSP);
  RNBRemote *remote = remoteSP.get();
  if (remote == NULL)
    return NULL;
  RNBContext &ctx = remote->Context();

  nub_process_t pid = ctx.ProcessID();
  DNBLogThreadedIf(LOG_RNB_PROC,
                   "RNBContext::%s (arg=%p, pid=%4.4x): thread starting...",
                   __FUNCTION__, arg, pid);
  ctx.Events().SetEvents(RNBContext::event_proc_thread_running);

#if defined(__APPLE__)
  pthread_setname_np("child process status watcher thread");
#if defined(__arm__) || defined(__arm64__) || defined(__aarch64__)
  struct sched_param thread_param;
  int thread_sched_policy;
  if (pthread_getschedparam(pthread_self(), &thread_sched_policy,
                            &thread_param) == 0) {
    thread_param.sched_priority = 47;
    pthread_setschedparam(pthread_self(), thread_sched_policy, &thread_param);
  }
#endif
#endif

  bool done = false;
  while (!done) {
    DNBLogThreadedIf(LOG_RNB_PROC,
                     "RNBContext::%s calling DNBProcessWaitForEvent(pid, "
                     "eEventProcessRunningStateChanged | "
                     "eEventProcessStoppedStateChanged | eEventStdioAvailable "
                     "| eEventProfileDataAvailable, true)...",
                     __FUNCTION__);
    nub_event_t pid_status_event = DNBProcessWaitForEvents(
        pid,
        eEventProcessRunningStateChanged | eEventProcessStoppedStateChanged |
            eEventStdioAvailable | eEventProfileDataAvailable,
        true, NULL);
    DNBLogThreadedIf(LOG_RNB_PROC,
                     "RNBContext::%s calling DNBProcessWaitForEvent(pid, "
                     "eEventProcessRunningStateChanged | "
                     "eEventProcessStoppedStateChanged | eEventStdioAvailable "
                     "| eEventProfileDataAvailable, true) => 0x%8.8x",
                     __FUNCTION__, pid_status_event);

    if (pid_status_event == 0) {
      DNBLogThreadedIf(LOG_RNB_PROC, "RNBContext::%s (pid=%4.4x) got ZERO back "
                                     "from DNBProcessWaitForEvent....",
                       __FUNCTION__, pid);
      //    done = true;
    } else {
      if (pid_status_event & eEventStdioAvailable) {
        DNBLogThreadedIf(
            LOG_RNB_PROC,
            "RNBContext::%s (pid=%4.4x) got stdio available event....",
            __FUNCTION__, pid);
        ctx.Events().SetEvents(RNBContext::event_proc_stdio_available);
        // Wait for the main thread to consume this notification if it requested
        // we wait for it
        ctx.Events().WaitForResetAck(RNBContext::event_proc_stdio_available);
      }

      if (pid_status_event & eEventProfileDataAvailable) {
        DNBLogThreadedIf(
            LOG_RNB_PROC,
            "RNBContext::%s (pid=%4.4x) got profile data event....",
            __FUNCTION__, pid);
        ctx.Events().SetEvents(RNBContext::event_proc_profile_data);
        // Wait for the main thread to consume this notification if it requested
        // we wait for it
        ctx.Events().WaitForResetAck(RNBContext::event_proc_profile_data);
      }

      if (pid_status_event & (eEventProcessRunningStateChanged |
                              eEventProcessStoppedStateChanged)) {
        nub_state_t pid_state = DNBProcessGetState(pid);
        DNBLogThreadedIf(
            LOG_RNB_PROC,
            "RNBContext::%s (pid=%4.4x) got process state change: %s",
            __FUNCTION__, pid, DNBStateAsString(pid_state));

        // Let the main thread know there is a process state change to see
        ctx.Events().SetEvents(RNBContext::event_proc_state_changed);
        // Wait for the main thread to consume this notification if it requested
        // we wait for it
        ctx.Events().WaitForResetAck(RNBContext::event_proc_state_changed);

        switch (pid_state) {
        case eStateStopped:
          break;

        case eStateInvalid:
        case eStateExited:
        case eStateDetached:
          done = true;
          break;
        default:
          break;
        }
      }

      // Reset any events that we consumed.
      DNBProcessResetEvents(pid, pid_status_event);
    }
  }
  DNBLogThreadedIf(LOG_RNB_PROC,
                   "RNBContext::%s (arg=%p, pid=%4.4x): thread exiting...",
                   __FUNCTION__, arg, pid);
  ctx.Events().ResetEvents(event_proc_thread_running);
  ctx.Events().SetEvents(event_proc_thread_exiting);
  return NULL;
}

const char *RNBContext::EventsAsString(nub_event_t events, std::string &s) {
  s.clear();
  if (events & event_proc_state_changed)
    s += "proc_state_changed ";
  if (events & event_proc_thread_running)
    s += "proc_thread_running ";
  if (events & event_proc_thread_exiting)
    s += "proc_thread_exiting ";
  if (events & event_proc_stdio_available)
    s += "proc_stdio_available ";
  if (events & event_proc_profile_data)
    s += "proc_profile_data ";
  if (events & event_darwin_log_data_available)
    s += "darwin_log_data_available ";
  if (events & event_read_packet_available)
    s += "read_packet_available ";
  if (events & event_read_thread_running)
    s += "read_thread_running ";
  if (events & event_read_thread_running)
    s += "read_thread_running ";
  return s.c_str();
}

const char *RNBContext::LaunchStatusAsString(std::string &s) {
  s.clear();

  const char *err_str = m_launch_status.AsString();
  if (err_str)
    s = err_str;
  else {
    char error_num_str[64];
    snprintf(error_num_str, sizeof(error_num_str), "%u",
             m_launch_status.Status());
    s = error_num_str;
  }
  return s.c_str();
}

bool RNBContext::ProcessStateRunning() const {
  nub_state_t pid_state = DNBProcessGetState(m_pid);
  return pid_state == eStateRunning || pid_state == eStateStepping;
}
