//===-- ThreadPlanStepUntil.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
//
//===----------------------------------------------------------------------===//

#include "lldb/Target/ThreadPlanStepUntil.h"

#include "lldb/Breakpoint/Breakpoint.h"
#include "lldb/Symbol/SymbolContextScope.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/RegisterContext.h"
#include "lldb/Target/StopInfo.h"
#include "lldb/Target/Target.h"
#include "lldb/Utility/Log.h"

using namespace lldb;
using namespace lldb_private;

// ThreadPlanStepUntil: Run until we reach a given line number or step out of
// the current frame

ThreadPlanStepUntil::ThreadPlanStepUntil(Thread &thread,
                                         lldb::addr_t *address_list,
                                         size_t num_addresses, bool stop_others,
                                         uint32_t frame_idx)
    : ThreadPlan(ThreadPlan::eKindStepUntil, "Step until", thread,
                 eVoteNoOpinion, eVoteNoOpinion),
      m_step_from_insn(LLDB_INVALID_ADDRESS),
      m_return_bp_id(LLDB_INVALID_BREAK_ID),
      m_return_addr(LLDB_INVALID_ADDRESS), m_stepped_out(false),
      m_should_stop(false), m_ran_analyze(false), m_explains_stop(false),
      m_until_points(), m_stop_others(stop_others) {
  // Stash away our "until" addresses:
  TargetSP target_sp(m_thread.CalculateTarget());

  StackFrameSP frame_sp(m_thread.GetStackFrameAtIndex(frame_idx));
  if (frame_sp) {
    m_step_from_insn = frame_sp->GetStackID().GetPC();
    lldb::user_id_t thread_id = m_thread.GetID();

    // Find the return address and set a breakpoint there:
    // FIXME - can we do this more securely if we know first_insn?

    StackFrameSP return_frame_sp(m_thread.GetStackFrameAtIndex(frame_idx + 1));
    if (return_frame_sp) {
      // TODO: add inline functionality
      m_return_addr = return_frame_sp->GetStackID().GetPC();
      Breakpoint *return_bp =
          target_sp->CreateBreakpoint(m_return_addr, true, false).get();

      if (return_bp != nullptr) {
        if (return_bp->IsHardware() && !return_bp->HasResolvedLocations())
          m_could_not_resolve_hw_bp = true;
        return_bp->SetThreadID(thread_id);
        m_return_bp_id = return_bp->GetID();
        return_bp->SetBreakpointKind("until-return-backstop");
      }
    }

    m_stack_id = frame_sp->GetStackID();

    // Now set breakpoints on all our return addresses:
    for (size_t i = 0; i < num_addresses; i++) {
      Breakpoint *until_bp =
          target_sp->CreateBreakpoint(address_list[i], true, false).get();
      if (until_bp != nullptr) {
        until_bp->SetThreadID(thread_id);
        m_until_points[address_list[i]] = until_bp->GetID();
        until_bp->SetBreakpointKind("until-target");
      } else {
        m_until_points[address_list[i]] = LLDB_INVALID_BREAK_ID;
      }
    }
  }
}

ThreadPlanStepUntil::~ThreadPlanStepUntil() { Clear(); }

void ThreadPlanStepUntil::Clear() {
  TargetSP target_sp(m_thread.CalculateTarget());
  if (target_sp) {
    if (m_return_bp_id != LLDB_INVALID_BREAK_ID) {
      target_sp->RemoveBreakpointByID(m_return_bp_id);
      m_return_bp_id = LLDB_INVALID_BREAK_ID;
    }

    until_collection::iterator pos, end = m_until_points.end();
    for (pos = m_until_points.begin(); pos != end; pos++) {
      target_sp->RemoveBreakpointByID((*pos).second);
    }
  }
  m_until_points.clear();
  m_could_not_resolve_hw_bp = false;
}

void ThreadPlanStepUntil::GetDescription(Stream *s,
                                         lldb::DescriptionLevel level) {
  if (level == lldb::eDescriptionLevelBrief) {
    s->Printf("step until");
    if (m_stepped_out)
      s->Printf(" - stepped out");
  } else {
    if (m_until_points.size() == 1)
      s->Printf("Stepping from address 0x%" PRIx64 " until we reach 0x%" PRIx64
                " using breakpoint %d",
                (uint64_t)m_step_from_insn,
                (uint64_t)(*m_until_points.begin()).first,
                (*m_until_points.begin()).second);
    else {
      until_collection::iterator pos, end = m_until_points.end();
      s->Printf("Stepping from address 0x%" PRIx64 " until we reach one of:",
                (uint64_t)m_step_from_insn);
      for (pos = m_until_points.begin(); pos != end; pos++) {
        s->Printf("\n\t0x%" PRIx64 " (bp: %d)", (uint64_t)(*pos).first,
                  (*pos).second);
      }
    }
    s->Printf(" stepped out address is 0x%" PRIx64 ".",
              (uint64_t)m_return_addr);
  }
}

bool ThreadPlanStepUntil::ValidatePlan(Stream *error) {
  if (m_could_not_resolve_hw_bp) {
    if (error)
      error->PutCString(
          "Could not create hardware breakpoint for thread plan.");
    return false;
  } else if (m_return_bp_id == LLDB_INVALID_BREAK_ID) {
    if (error)
      error->PutCString("Could not create return breakpoint.");
    return false;
  } else {
    until_collection::iterator pos, end = m_until_points.end();
    for (pos = m_until_points.begin(); pos != end; pos++) {
      if (!LLDB_BREAK_ID_IS_VALID((*pos).second))
        return false;
    }
    return true;
  }
}

void ThreadPlanStepUntil::AnalyzeStop() {
  if (m_ran_analyze)
    return;

  StopInfoSP stop_info_sp = GetPrivateStopInfo();
  m_should_stop = true;
  m_explains_stop = false;

  if (stop_info_sp) {
    StopReason reason = stop_info_sp->GetStopReason();

    if (reason == eStopReasonBreakpoint) {
      // If this is OUR breakpoint, we're fine, otherwise we don't know why
      // this happened...
      BreakpointSiteSP this_site =
          m_thread.GetProcess()->GetBreakpointSiteList().FindByID(
              stop_info_sp->GetValue());
      if (!this_site) {
        m_explains_stop = false;
        return;
      }

      if (this_site->IsBreakpointAtThisSite(m_return_bp_id)) {
        // If we are at our "step out" breakpoint, and the stack depth has
        // shrunk, then this is indeed our stop. If the stack depth has grown,
        // then we've hit our step out breakpoint recursively. If we are the
        // only breakpoint at that location, then we do explain the stop, and
        // we'll just continue. If there was another breakpoint here, then we
        // don't explain the stop, but we won't mark ourselves Completed,
        // because maybe that breakpoint will continue, and then we'll finish
        // the "until".
        bool done;
        StackID cur_frame_zero_id;

        done = (m_stack_id < cur_frame_zero_id);

        if (done) {
          m_stepped_out = true;
          SetPlanComplete();
        } else
          m_should_stop = false;

        if (this_site->GetNumberOfOwners() == 1)
          m_explains_stop = true;
        else
          m_explains_stop = false;
        return;
      } else {
        // Check if we've hit one of our "until" breakpoints.
        until_collection::iterator pos, end = m_until_points.end();
        for (pos = m_until_points.begin(); pos != end; pos++) {
          if (this_site->IsBreakpointAtThisSite((*pos).second)) {
            // If we're at the right stack depth, then we're done.

            bool done;
            StackID frame_zero_id =
                m_thread.GetStackFrameAtIndex(0)->GetStackID();

            if (frame_zero_id == m_stack_id)
              done = true;
            else if (frame_zero_id < m_stack_id)
              done = false;
            else {
              StackFrameSP older_frame_sp = m_thread.GetStackFrameAtIndex(1);

              // But if we can't even unwind one frame we should just get out
              // of here & stop...
              if (older_frame_sp) {
                const SymbolContext &older_context =
                    older_frame_sp->GetSymbolContext(eSymbolContextEverything);
                SymbolContext stack_context;
                m_stack_id.GetSymbolContextScope()->CalculateSymbolContext(
                    &stack_context);

                done = (older_context == stack_context);
              } else
                done = false;
            }

            if (done)
              SetPlanComplete();
            else
              m_should_stop = false;

            // Otherwise we've hit this breakpoint recursively.  If we're the
            // only breakpoint here, then we do explain the stop, and we'll
            // continue. If not then we should let higher plans handle this
            // stop.
            if (this_site->GetNumberOfOwners() == 1)
              m_explains_stop = true;
            else {
              m_should_stop = true;
              m_explains_stop = false;
            }
            return;
          }
        }
      }
      // If we get here we haven't hit any of our breakpoints, so let the
      // higher plans take care of the stop.
      m_explains_stop = false;
      return;
    } else if (IsUsuallyUnexplainedStopReason(reason)) {
      m_explains_stop = false;
    } else {
      m_explains_stop = true;
    }
  }
}

bool ThreadPlanStepUntil::DoPlanExplainsStop(Event *event_ptr) {
  // We don't explain signals or breakpoints (breakpoints that handle stepping
  // in or out will be handled by a child plan.
  AnalyzeStop();
  return m_explains_stop;
}

bool ThreadPlanStepUntil::ShouldStop(Event *event_ptr) {
  // If we've told our self in ExplainsStop that we plan to continue, then do
  // so here.  Otherwise, as long as this thread has stopped for a reason, we
  // will stop.

  StopInfoSP stop_info_sp = GetPrivateStopInfo();
  if (!stop_info_sp || stop_info_sp->GetStopReason() == eStopReasonNone)
    return false;

  AnalyzeStop();
  return m_should_stop;
}

bool ThreadPlanStepUntil::StopOthers() { return m_stop_others; }

StateType ThreadPlanStepUntil::GetPlanRunState() { return eStateRunning; }

bool ThreadPlanStepUntil::DoWillResume(StateType resume_state,
                                       bool current_plan) {
  if (current_plan) {
    TargetSP target_sp(m_thread.CalculateTarget());
    if (target_sp) {
      Breakpoint *return_bp =
          target_sp->GetBreakpointByID(m_return_bp_id).get();
      if (return_bp != nullptr)
        return_bp->SetEnabled(true);

      until_collection::iterator pos, end = m_until_points.end();
      for (pos = m_until_points.begin(); pos != end; pos++) {
        Breakpoint *until_bp =
            target_sp->GetBreakpointByID((*pos).second).get();
        if (until_bp != nullptr)
          until_bp->SetEnabled(true);
      }
    }
  }

  m_should_stop = true;
  m_ran_analyze = false;
  m_explains_stop = false;
  return true;
}

bool ThreadPlanStepUntil::WillStop() {
  TargetSP target_sp(m_thread.CalculateTarget());
  if (target_sp) {
    Breakpoint *return_bp = target_sp->GetBreakpointByID(m_return_bp_id).get();
    if (return_bp != nullptr)
      return_bp->SetEnabled(false);

    until_collection::iterator pos, end = m_until_points.end();
    for (pos = m_until_points.begin(); pos != end; pos++) {
      Breakpoint *until_bp = target_sp->GetBreakpointByID((*pos).second).get();
      if (until_bp != nullptr)
        until_bp->SetEnabled(false);
    }
  }
  return true;
}

bool ThreadPlanStepUntil::MischiefManaged() {
  // I'm letting "PlanExplainsStop" do all the work, and just reporting that
  // here.
  bool done = false;
  if (IsPlanComplete()) {
    Log *log(lldb_private::GetLogIfAllCategoriesSet(LIBLLDB_LOG_STEP));
    if (log)
      log->Printf("Completed step until plan.");

    Clear();
    done = true;
  }
  if (done)
    ThreadPlan::MischiefManaged();

  return done;
}
