require 'ruby-debug/interface'
require 'ruby-debug/command'

module Debugger

  # Should this be a mixin?
  class Processor # :nodoc
    attr_accessor :interface
    extend Forwardable
    def_delegator :"Debugger.printer", :print, :pr

    def self.protect(mname)
      alias_method "__#{mname}", mname
      module_eval %{
        def #{mname}(*args)
          @mutex.synchronize do
            return unless @interface
            __#{mname}(*args)
          end
        rescue IOError, Errno::EPIPE
          self.interface = nil
        rescue SignalException
          raise
        rescue Exception
          print "INTERNAL ERROR!!! #\{$!\}\n" rescue nil
          print $!.backtrace.map{|l| "\t#\{l\}"}.join("\n") rescue nil
        end
      }
    end

    # Format msg with gdb-style annotation header
    def afmt(msg, newline="\n")
      "\032\032#{msg}#{newline}"
    end

    def aprint(msg)
      print afmt(msg) if Debugger.annotate.to_i > 2
    end

    # FIXME: use delegate?
    def errmsg(*args)
      @interface.errmsg(*args)
    end

    # Callers of this routine should make sure to use comma to
    # separate format argments rather than %. Otherwise it seems that
    # if the string you want to print has format specifier, which
    # could happen if you are trying to show say a source-code line
    # with "puts" or "print" in it, this print routine will give an
    # error saying it is looking for more arguments.
    def print(*args)
      @interface.print(*args)
    end

    # Split commands like this:
    # split_commands("abc;def\\;ghi;jkl") => ["abc", "def;ghi", "jkl"]
    def split_commands(input)
      input.split(/(?<!\\);/).map { |e| e.gsub("\\;", ";") }
    end
  end

  class CommandProcessor < Processor # :nodoc:
    attr_reader   :display

    # FIXME: get from Command regexp method.
    @@Show_breakpoints_postcmd = [
                                  /^\s*b(?:reak)?/,
                                  /^\s* cond(?:ition)? (?:\s+(\d+)\s*(.*))?$/ix,
                                  /^\s*del(?:ete)?(?:\s+(.*))?$/ix,
                                  /^\s* dis(?:able)? (?:\s+(.*))?$/ix,
                                  /^\s* en(?:able)? (?:\s+(.*))?$/ix,
                                  # "tbreak", "clear",
                                 ]
    @@Show_annotations_run     = [
                                  /^\s*c(?:ont(?:inue)?)?(?:\s+(.*))?$/,
                                  /^\s*fin(?:ish)?$/,
                                  /^\s*n(?:ext)?([+-])?(?:\s+(.*))?$/,
                                  /^\s*s(?:tep)?([+-])?(?:\s+(.*))?$/
                                ]

    @@Show_annotations_postcmd = [
                                  /^\s* down (?:\s+(.*))? .*$/x,
                                  /^\s* f(?:rame)? (?:\s+ (.*))? \s*$/x,
                                  /^\s* u(?:p)? (?:\s+(.*))?$/x
                                 ]

    def initialize(interface = LocalInterface.new)
      @interface = interface
      @display = []

      @mutex = Mutex.new
      @last_cmd = nil
      @last_file = nil   # Filename the last time we stopped
      @last_line = nil   # line number the last time we stopped
      @debugger_breakpoints_were_empty = false # Show breakpoints 1st time
      @debugger_displays_were_empty = true # No display 1st time
      @debugger_context_was_dead = true # Assume we haven't started.
    end

    def interface=(interface)
      @mutex.synchronize do
        @interface.close if @interface
        @interface = interface
      end
    end

    require 'pathname'  # For cleanpath

    # Regularize file name.
    # This is also used as a common funnel place if basename is
    # desired or if we are working remotely and want to change the
    # basename. Or we are eliding filenames.
    def self.canonic_file(filename)
      # For now we want resolved filenames
      if Command.settings[:basename]
        File.basename(filename)
      else
        # Cache this?
        Pathname.new(filename).cleanpath.to_s
      end
    end

    def self.print_location_and_text(file, line, context)
      result = Debugger.printer.print("stop.suspend",
        file: canonic_file(file), line_number: line, line: Debugger.line_at(file, line),
        thnum: context && context.thnum, frames: context && context.stack_size
      )
      Debugger.handler.interface.print(annotate_location_and_text(result))
    end

    def self.annotate_location_and_text(output)
      # FIXME: use annotations routines
      if Debugger.annotate.to_i > 2
        "\032\032source #{output}"
      elsif ENV['EMACS']
        "\032\032#{output}"
      else
        output
      end
    end

    def at_breakpoint(context, breakpoint)
      aprint 'stopped' if Debugger.annotate.to_i > 2
      n = Debugger.breakpoints.index(breakpoint) + 1
      file = CommandProcessor.canonic_file(breakpoint.source)
      line = breakpoint.pos
      if Debugger.annotate.to_i > 2
        print afmt("source #{file}:#{line}")
      end
      print pr(
        "breakpoints.stop_at_breakpoint", id: n, file: file, line: line, thread_id: Debugger.current_context.thnum
      )
      @last_breakpoint = breakpoint
    end
    protect :at_breakpoint

    def at_catchpoint(context, excpt)
      aprint 'stopped' if Debugger.annotate.to_i > 2
      file = CommandProcessor.canonic_file(context.frame_file(0))
      line = context.frame_line(0)
      print afmt("%s:%d" % [file, line]) if ENV['EMACS']
      print "Catchpoint at %s:%d: `%s' (%s)\n", file, line, excpt, excpt.class
      fs = context.stack_size
      tb = caller(0)[-fs..-1]
      if tb
        for i in tb
          print "\tfrom %s\n", i
        end
      end
    end
    protect :at_catchpoint

    def at_tracing(context, file, line)
      return if defined?(Debugger::RDEBUG_FILE) &&
        Debugger::RDEBUG_FILE == file # Don't trace ourself
      @last_file = CommandProcessor.canonic_file(file)
      file = CommandProcessor.canonic_file(file)
      unless file == @last_file and @last_line == line and
          Command.settings[:tracing_plus]
        print("Tracing(%d):%s:%s %s" % [context.thnum, file, line, Debugger.line_at(file, line)])
        @last_file = file
        @last_line = line
      end
      always_run(context, file, line, 2)
    end
    protect :at_tracing

    def at_line(context, file, line)
      CommandProcessor.print_location_and_text(file, line, context) unless @last_breakpoint
      process_commands(context, file, line)
    ensure
      @last_breakpoint = nil
    end
    protect :at_line

    def at_return(context, file, line)
      context.stop_frame = -1
      CommandProcessor.print_location_and_text(file, line, context)
      process_commands(context, file, line)
    end

    private

    # The prompt shown before reading a command.
    def prompt(context)
      p = '(rdb:%s) ' % (context.dead?  ? 'post-mortem' : context.thnum)
      p = afmt("pre-prompt")+p+"\n"+afmt("prompt") if
        Debugger.annotate.to_i > 2
      return p
    end

    # Run these commands, for example display commands or possibly
    # the list or irb in an "autolist" or "autoirb".
    # We return a list of commands that are acceptable to run bound
    # to the current state.
    def always_run(context, file, line, run_level)
      event_cmds = Command.commands.select{|cmd| cmd.event }

      # Remove some commands in post-mortem
      event_cmds = event_cmds.find_all do |cmd|
        cmd.allow_in_post_mortem
      end if context.dead?

      state = State.new do |s|
        s.context = context
        s.file    = file
        s.line    = line
        s.binding = context.frame_binding(0)
        s.display = display
        s.interface = interface
        s.commands = event_cmds
      end
      @interface.state = state if @interface.respond_to?('state=')

      # Bind commands to the current state.
      commands = event_cmds.map{|cmd| cmd.new(state)}

      commands.select do |cmd|
        cmd.class.always_run >= run_level
      end.each {|cmd| cmd.execute}
      return state, commands
    end

    # Handle debugger commands
    def process_commands(context, file, line)
      state, commands = always_run(context, file, line, 1)
      $rdebug_state = state if Command.settings[:debuggertesting]

      preloop(commands, context)
      while !state.proceed?
        input = if @interface.command_queue.empty?
          @interface.read_command(prompt(context))
        else
          @interface.command_queue.shift
        end
        break unless input
        catch(:debug_error) do
          if input == ""
            next unless @last_cmd
            input = @last_cmd
          else
            @last_cmd = input
          end
          split_commands(input).each do |cmd|
            one_cmd(commands, context, cmd)
            postcmd(commands, context, cmd)
          end
        end
      end
      postloop(commands, context)
    end # process_commands

    def one_cmd(commands, context, input)
      if cmd = commands.find{ |c| c.match(input) }
        if context.dead? && cmd.class.need_context
          p cmd
          print "Command is unavailable\n"
        else
          cmd.execute
        end
      else
        unknown_cmd = commands.find{ |c| c.class.unknown }
        if unknown_cmd
          unknown_cmd.execute
        else
          errmsg "Unknown command: \"#{input}\".  Try \"help\".\n"
        end
      end
    end

    def preloop(commands, context)
      aprint('stopped') if Debugger.annotate.to_i > 2
      if context.dead?
        unless @debugger_context_was_dead
          if Debugger.annotate.to_i > 2
            aprint('exited')
            print "The program finished.\n"
          end
          @debugger_context_was_dead = true
        end
      end

      if Debugger.annotate.to_i > 2
        # if we are here, the stack frames have changed outside the
        # command loop (e.g. after a "continue" command), so we show
        # the annotations again
        breakpoint_annotations(commands, context)
        display_annotations(commands, context)
        annotation('stack', commands, context, "where")
        annotation('variables', commands, context, "info variables") unless
          context.dead?
      end
    end

    def postcmd(commands, context, cmd)
      if Debugger.annotate.to_i > 0
        cmd = @last_cmd unless cmd
        breakpoint_annotations(commands, context) if
          @@Show_breakpoints_postcmd.find{|pat| cmd =~ pat}
        display_annotations(commands, context)
        if @@Show_annotations_postcmd.find{|pat| cmd =~ pat}
          annotation('stack', commands, context, "where") if
            context.stack_size > 0
          annotation('variables', commands, context, "info variables") unless
            context.dead?
        end
        if not context.dead? and @@Show_annotations_run.find{|pat| cmd =~ pat}
          aprint 'starting'  if Debugger.annotate.to_i > 2

          @debugger_context_was_dead = false
        end
      end
    end

    def postloop(commands, context)
    end

    def annotation(label, commands, context, cmd)
      print afmt(label)
      one_cmd(commands, context, cmd)
      ### FIXME ANNOTATE: the following line should be deleted
      print "\032\032\n"
    end

    def breakpoint_annotations(commands, context)
      unless Debugger.breakpoints.empty? and @debugger_breakpoints_were_empty
        annotation('breakpoints', commands, context, "info breakpoints")
        @debugger_breakpoints_were_empty = Debugger.breakpoints.empty?
      end
    end

    def display_annotations(commands, context)
      return if display.empty?
#       have_display = display.find{|d| d[0]}
#       return unless have_display and @debugger_displays_were_empty
#       @debugger_displays_were_empty = have_display
      annotation('display', commands, context, "display")
    end

    class State # :nodoc:
      attr_accessor :context, :file, :line, :binding
      attr_accessor :frame_pos, :previous_line, :display
      attr_accessor :interface, :commands

      def initialize
        super()
        @frame_pos = 0
        @previous_line = nil
        @proceed = false
        yield self
      end

      # FIXME: use delegate?
      def errmsg(*args)
        @interface.errmsg(*args)
      end

      def print(*args)
        @interface.print(*args)
      end

      def print_debug(*args)
        @interface.print_debug(*args)
      end

      def confirm(*args)
        @interface.confirm(*args)
      end

      def proceed?
        @proceed
      end

      def proceed
        @proceed = true
      end
    end
  end

  class ControlCommandProcessor < Processor # :nodoc:
    def initialize(interface)
      super()
      @interface = interface
      @debugger_context_was_dead = true # Assume we haven't started.
    end

    def process_commands(verbose=false)
      control_cmds = Command.commands.select do |cmd|
        cmd.allow_in_control
      end
      state = State.new(@interface, control_cmds)
      commands = control_cmds.map{|cmd| cmd.new(state) }

      unless @debugger_context_was_dead
        if Debugger.annotate.to_i > 2
          aprint 'exited'
          print "The program finished.\n"
        end
        @debugger_context_was_dead = true
      end

      while input = @interface.read_command(prompt(nil))
        print "+#{input}" if verbose
        catch(:debug_error) do
          if cmd = commands.find{|c| c.match(input) }
            cmd.execute
          else
            errmsg "Unknown command\n"
          end
        end
      end
    rescue IOError, Errno::EPIPE
    rescue Exception
      print "INTERNAL ERROR!!! #{$!}\n" rescue nil
      print $!.backtrace.map{|l| "\t#{l}"}.join("\n") rescue nil
    ensure
      @interface.close
    end

    # The prompt shown before reading a command.
    # Note: have an unused 'context' parameter to match the local interface.
    def prompt(context)
      p = '(rdb:ctrl) '
      p = afmt("pre-prompt")+p+"\n"+afmt("prompt") if
        Debugger.annotate.to_i > 2
      return p
    end

    class State # :nodoc:
      attr_reader :commands, :interface

      def initialize(interface, commands)
        @interface = interface
        @commands = commands
      end

      def proceed
      end

      def errmsg(*args)
        @interface.print(*args)
      end

      def print(*args)
        @interface.print(*args)
      end

      def confirm(*args)
        'y'
      end

      def context
        nil
      end

      def file
        errmsg "No filename given.\n"
        throw :debug_error
      end
    end # State
  end
end
