class DEBUGGER__::ThreadClient

def wait_next_action_

def wait_next_action_
  # assertions
  raise "@mode is #{@mode}" if !waiting?
  unless SESSION.active?
    pp caller
    set_mode :running
    return
  end
  while true
    begin
      set_mode :waiting if !waiting?
      cmds = @q_cmd.pop
      # pp [self, cmds: cmds]
      break unless cmds
    ensure
      set_mode :running
    end
    cmd, *args = *cmds
    case cmd
    when :continue
      break
    when :step
      step_type = args[0]
      iter = args[1]
      case step_type
      when :in
        iter = iter || 1
        if @recorder&.replaying?
          @recorder.step_forward iter
          raise SuspendReplay
        else
          step_tp iter do
            true
          end
          break
        end
      when :next
        frame = @target_frames.first
        path = frame.location.absolute_path || "!eval:#{frame.path}"
        line = frame.location.lineno
        label = frame.location.base_label
        if frame.iseq
          frame.iseq.traceable_lines_norec(lines = {})
          next_line = lines.keys.bsearch{|e| e > line}
          if !next_line && (last_line = frame.iseq.last_line) > line
            next_line = last_line
          end
        end
        depth = @target_frames.first.frame_depth
        step_tp iter do |tp|
          loc = caller_locations(2, 1).first
          loc_path = loc.absolute_path || "!eval:#{loc.path}"
          loc_label = loc.base_label
          loc_depth = DEBUGGER__.frame_depth - 3
          case
          when loc_depth == depth && loc_label == label
            true
          when loc_depth < depth
            # lower stack depth
            true
          when (next_line &&
                loc_path == path &&
                (loc_lineno = loc.lineno) > line &&
                loc_lineno <= next_line)
            # different frame (maybe block) but the line is before next_line
            true
          end
        end
        break
      when :finish
        finish_frames = (iter || 1) - 1
        frame = @target_frames.first
        goal_depth = frame.frame_depth - finish_frames - (frame.has_return_value ? 1 : 0)
        step_tp nil, [:return, :b_return] do
          DEBUGGER__.frame_depth - 3 <= goal_depth ? true : false
        end
        break
      when :until
        location = iter&.strip
        frame = @target_frames.first
        depth = frame.frame_depth - (frame.has_return_value ? 1 : 0)
        target_location_label = frame.location.base_label
        case location
        when nil, /\A(?:(.+):)?(\d+)\z/
          no_loc = !location
          file = $1 || frame.location.path
          line = ($2 ||  frame.location.lineno + 1).to_i
          step_tp nil, [:line, :return] do |tp|
            if tp.event == :line
              next false if no_loc && depth < DEBUGGER__.frame_depth - 3
              next false unless tp.path.end_with?(file)
              next false unless tp.lineno >= line
              true
            else
              true if depth >= DEBUGGER__.frame_depth - 3 &&
                      caller_locations(2, 1).first.label == target_location_label
                      # TODO: imcomplete condition
            end
          end
        else
          pat = location
          if /\A\/(.+)\/\z/ =~ pat
            pat = Regexp.new($1)
          end
          step_tp nil, [:call, :c_call, :return] do |tp|
            case tp.event
            when :call, :c_call
              true if pat === tp.callee_id.to_s
            else # :return, :b_return
              true if depth >= DEBUGGER__.frame_depth - 3 &&
                      caller_locations(2, 1).first.label == target_location_label
                      # TODO: imcomplete condition
            end
          end
        end
        break
      when :back
        iter = iter || 1
        if @recorder&.can_step_back?
          unless @recorder.backup_frames
            @recorder.backup_frames = @target_frames
          end
          @recorder.step_back iter
          raise SuspendReplay
        else
          puts "Can not step back more."
          event! :result, nil
        end
      when :reset
        if @recorder&.replaying?
          @recorder.step_reset
          raise SuspendReplay
        end
      else
        raise "unknown: #{type}"
      end
    when :eval
      eval_type, eval_src = *args
      result_type = nil
      case eval_type
      when :p
        result = frame_eval(eval_src)
        puts "=> " + color_pp(result, 2 ** 30)
        if alloc_path = ObjectSpace.allocation_sourcefile(result)
          puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}"
        end
      when :pp
        result = frame_eval(eval_src)
        puts color_pp(result, SESSION.width)
        if alloc_path = ObjectSpace.allocation_sourcefile(result)
          puts "allocated at #{alloc_path}:#{ObjectSpace.allocation_sourceline(result)}"
        end
      when :call
        result = frame_eval(eval_src)
      when :irb
        require 'irb' # prelude's binding.irb doesn't have show_code option
        begin
          result = frame_eval('binding.irb(show_code: false)', binding_location: true)
        ensure
          # workaround: https://github.com/ruby/debug/issues/308
          Reline.prompt_proc = nil if defined? Reline
        end
      when :display, :try_display
        failed_results = []
        eval_src.each_with_index{|src, i|
          result = frame_eval(src){|e|
            failed_results << [i, e.message]
            "<error: #{e.message}>"
          }
          puts "#{i}: #{src} = #{result}"
        }
        result_type = eval_type
        result = failed_results
      else
        raise "unknown error option: #{args.inspect}"
      end
      event! :result, result_type, result
    when :frame
      type, arg = *args
      case type
      when :up
        if @current_frame_index + 1 < @target_frames.size
          @current_frame_index += 1
          show_src max_lines: 1
          show_frame(@current_frame_index)
        end
      when :down
        if @current_frame_index > 0
          @current_frame_index -= 1
          show_src max_lines: 1
          show_frame(@current_frame_index)
        end
      when :set
        if arg
          index = arg.to_i
          if index >= 0 && index < @target_frames.size
            @current_frame_index = index
          else
            puts "out of frame index: #{index}"
          end
        end
        show_src max_lines: 1
        show_frame(@current_frame_index)
      else
        raise "unsupported frame operation: #{arg.inspect}"
      end
      event! :result, nil
    when :show
      type = args.shift
      case type
      when :backtrace
        max_lines, pattern = *args
        show_frames max_lines, pattern
      when :list
        show_src(update_line: true, **(args.first || {}))
      when :whereami
        show_src ignore_show_line: true
        show_frames CONFIG[:show_frames]
      when :edit
        show_by_editor(args.first)
      when :default
        pat = args.shift
        show_locals pat
        show_ivars  pat
        show_consts pat, only_self: true
      when :locals
        pat = args.shift
        show_locals pat
      when :ivars
        pat = args.shift
        expr = args.shift
        show_ivars pat, expr
      when :consts
        pat = args.shift
        expr = args.shift
        show_consts pat, expr
      when :globals
        pat = args.shift
        show_globals pat
      when :outline
        show_outline args.first || 'self'
      else
        raise "unknown show param: " + [type, *args].inspect
      end
      event! :result, nil
    when :breakpoint
      case args[0]
      when :method
        bp = make_breakpoint args
        event! :result, :method_breakpoint, bp
      when :watch
        ivar, cond, command, path = args[1..]
        result = frame_eval(ivar)
        if @success_last_eval
          object =
            if b = current_frame.binding
              b.receiver
            else
              current_frame.self
            end
          bp = make_breakpoint [:watch, ivar, object, result, cond, command, path]
          event! :result, :watch_breakpoint, bp
        else
          event! :result, nil
        end
      end
    when :trace
      case args.shift
      when :object
        begin
          obj = frame_eval args.shift, re_raise: true
          opt = args.shift
          obj_inspect = DEBUGGER__.safe_inspect(obj)
          width = 50
          if obj_inspect.length >= width
            obj_inspect = truncate(obj_inspect, width: width)
          end
          event! :result, :trace_pass, M_OBJECT_ID.bind_call(obj), obj_inspect, opt
        rescue => e
          puts e.message
          event! :result, nil
        end
      else
        raise "unreachable"
      end
    when :record
      case args[0]
      when nil
        # ok
      when :on
        # enable recording
        if !@recorder
          @recorder = Recorder.new
        end
        @recorder.enable
      when :off
        if @recorder&.enabled?
          @recorder.disable
        end
      else
        raise "unknown: #{args.inspect}"
      end
      if @recorder&.enabled?
        puts "Recorder for #{Thread.current}: on (#{@recorder.log.size} records)"
      else
        puts "Recorder for #{Thread.current}: off"
      end
      event! :result, nil
    when :quit
      sleep # wait for SystemExit
    when :dap
      process_dap args
    when :cdp
      process_cdp args
    else
      raise [cmd, *args].inspect
    end
  end
rescue SuspendReplay, SystemExit, Interrupt
  raise
rescue Exception => e
  STDERR.puts e.cause.inspect
  STDERR.puts e.inspect
  Thread.list.each{|th|
    STDERR.puts "@@@ #{th}"
    th.backtrace.each{|b|
      STDERR.puts " > #{b}"
    }
  }
  p ["DEBUGGER Exception: #{__FILE__}:#{__LINE__}", e, e.backtrace]
  raise
ensure
  @returning = false
end