require 'byebug/state'
require 'byebug/helpers/file'

module Byebug
  #
  # Controls state of Byebug's REPL when in normal mode
  #
  class RegularState < State
    include Helpers::FileHelper

    attr_accessor :context, :frame, :display, :file, :line, :prev_line
    attr_writer :interface

    def initialize(context, display, file, interface, line)
      super(interface)
      @context = context
      @display = display
      @file = file
      @frame = 0
      @line = line
      @prev_line = nil
      @proceed = false
    end

    extend Forwardable
    def_delegators :@interface, :errmsg, :puts, :print, :confirm

    #
    # Checks whether that execution can proceed
    #
    def proceed?
      @proceed
    end

    #
    # Signals the REPL that the execution can proceed
    #
    def proceed
      @proceed = true
    end

    #
    # Current (formatted) location
    #
    def location
      l = "#{normalize(file)} @ #{line}\n"
      l += "#{get_line(file, line)}\n" unless %w((irb) -e').include?(file)
      l
    end

    #
    # Builds a string containing the class associated to frame number +pos+
    # or an empty string if the current +callstyle+ setting is 'short'
    #
    # @param pos [Integer] Frame position.
    #
    def frame_class(pos)
      return '' if Setting[:callstyle] == 'short'

      klass = context.frame_class(pos)
      return '' if klass.to_s.empty?

      "#{klass}."
    end

    #
    # Builds a formatted string containing information about block and method
    # of the frame in position +pos+
    #
    # @param pos [Integer] Frame position.
    #
    def frame_block_and_method(pos)
      deco_regexp = /((?:block(?: \(\d+ levels\))?|rescue) in )?(.+)/
      deco_method = "#{context.frame_method(pos)}"
      block_and_method = deco_regexp.match(deco_method)[1..2]
      block_and_method.map { |x| x.nil? ? '' : x }
    end

    #
    # Builds a string containing all available args in frame number +pos+, in a
    # verbose or non verbose way according to the value of the +callstyle+
    # setting
    #
    # @param pos [Integer] Frame position.
    #
    def frame_args(pos)
      args = context.frame_args(pos)
      return '' if args.empty?

      locals = context.frame_locals(pos) unless Setting[:callstyle] == 'short'
      my_args = args.map do |arg|
        prefix, default = prefix_and_default_from(arg[0])

        kls = if Setting[:callstyle] == 'short' || arg[1].nil? || locals.empty?
                ''
              else
                "##{locals[arg[1]].class}"
              end

        "#{prefix}#{arg[1] || default}#{kls}"
      end

      "(#{my_args.join(', ')})"
    end

    #
    # Builds a formatted string containing information about current method
    # call in frame number +pos+.
    #
    # @param pos [Integer] Frame position.
    #
    def frame_call(pos)
      block, method = frame_block_and_method(pos)

      block + frame_class(pos) + method + frame_args(pos)
    end

    #
    # Formatted filename in frame number +pos+
    #
    # @param pos [Integer] Frame position.
    #
    def frame_file(pos)
      fullpath = context.frame_file(pos)
      Setting[:fullpath] ? fullpath : shortpath(fullpath)
    end

    #
    # Line number in frame number +pos+
    #
    # @param pos [Integer] Frame position.
    #
    def frame_line(pos)
      context.frame_line(pos)
    end

    #
    # Properly formatted frame number of frame in position +pos+
    #
    # @param pos [Integer] Frame position.
    #
    def frame_pos(pos)
      format('%-2d', pos)
    end

    #
    # Formatted mark for number of frame in position +pos+. The mark can
    # contain the current frame symbo (-->), the c_frame symbol (ͱ--) or both
    #
    # @param pos [Integer] Frame position.
    #
    def frame_mark(pos)
      mark = frame == pos ? '-->' : '   '

      c_frame?(pos) ? mark + ' ͱ--' : mark
    end

    #
    # Checks whether the frame in position +pos+ is a c-frame or not
    #
    # @param pos [Integer] Frame position.
    #
    def c_frame?(pos)
      context.frame_binding(pos).nil?
    end

    private

    def shortpath(fullpath)
      components = Pathname(fullpath).each_filename.to_a
      return fullpath if components.size <= 2

      File.join('...', components[-3..-1])
    end

    def prefix_and_default_from(arg_type)
      case arg_type
      when :block
        return ['&', 'block']
      when :rest
        return ['*', 'args']
      else
        return ['', nil]
      end
    end
  end
end
