##
# Contains all logic for gathering and displaying backtraces.

class Rubinius::Backtrace
  include Enumerable

  MAX_WIDTH_PERCENTAGE = 45
  MIN_WIDTH = 20

  # If passed nil, we assume someone forgot to create a backtrace
  # in the VM.
  def self.backtrace(locations)
    locations ? new(locations) : new([Rubinius::Location::Missing.new])
  end

  def initialize(locations, width = Rubinius::TERMINAL_WIDTH, min_width = MIN_WIDTH)
    color_config = Rubinius::Config["rbx.colorize_backtraces"]
    if ENV['RBX_NOCOLOR'] or color_config == "no" or color_config == "NO"
      @colorize = false
    else
      @colorize = true
    end

    @locations = locations
    @bold = "\033[0;1m"
    @inline_effect = "\033[0;4m"

    @width = width
    @min_width = min_width

    @mri_backtrace = nil
  end

  def [](index)
    @locations[index]
  end

  def show(sep="\n", show_color=true)
    show_color = false unless @colorize
    clear = show_color ? "\033[0m" : ""

    max = 0
    lines = []
    last_method = nil
    last_line = nil
    last_name = nil
    times = 0

    @locations.reverse_each do |loc|
      if loc.name == last_name and loc.method == last_method \
                               and loc.line == last_line
        times += 1
      else
        lines.last[-1] = times if lines.size > 0
        last_method = loc.method
        last_line = loc.line
        last_name = loc.name

        receiver_name = loc.describe_receiver

        if loc.is_block
          method_name = "{ } in #{loc.describe_method}"
        else
          method_name = loc.describe_method
        end

        size = method_name.size + 4 # additional space needed for {#, .}
        max = size if size > max

        lines << [receiver_name, method_name, loc, 1]
        times = 0
      end
    end

    cwd = Dir.getwd

    str = ""
    lines.each do |receiver_name, method_name, location, rec_times|
      next unless location

      full_name = "#{method_name} #{receiver_name[-1]} "

      if show_color
        receiver_method = "#{@bold}#{full_name}#{receiver_name[0..-2]}#{clear}"
      else
        receiver_method = "#{full_name}#{receiver_name[0..-2]}"
      end

      spaces = max - full_name.size
      spaces = 0 if spaces < 0

      if show_color and location.inlined?
        start = " #{' ' * spaces}#{receiver_method} #{@inline_effect}at#{clear}#{color} "
      else
        start = " #{' ' * spaces}#{receiver_method} at "
      end

      # start.size without the escapes
      start_size = 1 + spaces + receiver_method.size + 4

      pos = location.position cwd

      if rec_times > 1
        pos << " (#{rec_times} times)"
      end

      if location.is_jit and $DEBUG
        pos << " (jit)"
      end

      if start_size + pos.size > @width
        str << " #{start}\n        #{pos}"
      else
        str << " #{start}#{pos}"
      end

      str << sep
    end

    return str
  end

  def join(sep)
    show
  end

  alias_method :to_s, :show

  def each
    @locations.each { |f| yield f }
    self
  end

  def self.detect_backtrace(obj)
    if bt = obj.instance_variable_get(:@rbx_backtrace)
      return bt if bt.same_mri_backtrace?(obj)
    end

    return nil
  end

  def to_mri
    return @mri_backtrace if @mri_backtrace

    @mri_backtrace = @locations.map do |loc|
      "#{loc.position}:in `#{loc.describe_method}'"
    end

    # A little extra magic, so we can carry the backtrace along and reuse it

    @mri_backtrace.instance_variable_set(:@rbx_backtrace, self)

    return @mri_backtrace
  end

  def same_mri_backtrace?(obj)
    currently = @locations.map do |loc|
      "#{loc.position}:in `#{loc.describe_method}'"
    end

    currently == obj
  end
end
