module Rollbar
  class Item
    class Backtrace
      attr_reader :exception
      attr_reader :message
      attr_reader :extra
      attr_reader :configuration

      def initialize(exception, options = {})
        @exception = exception
        @message = options[:message]
        @extra = options[:extra]
        @configuration = options[:configuration]
      end

      def build
        traces = trace_chain

        traces[0][:exception][:description] = message if message
        traces[0][:extra] = extra if extra

        if traces.size > 1
          { :trace_chain => traces }
        elsif traces.size == 1
          { :trace => traces[0] }
        end
      end

      private

      def trace_chain
        exception
        traces = [trace_data(exception)]
        visited = [exception]

        current_exception = exception

        while current_exception.respond_to?(:cause) && (cause = current_exception.cause) && cause.is_a?(Exception) && !visited.include?(cause)
          traces << trace_data(cause)
          visited << cause
          current_exception = cause
        end

        traces
      end

      def trace_data(current_exception)
        frames = reduce_frames(current_exception)
        # reverse so that the order is as rollbar expects
        frames.reverse!

        {
          :frames => frames,
          :exception => {
            :class => current_exception.class.name,
            :message => current_exception.message
          }
        }
      end

      def reduce_frames(current_exception)
        exception_backtrace(current_exception).map do |frame|
          # parse the line
          match = frame.match(/(.*):(\d+)(?::in `([^']+)')?/)

          if match
            { :filename => match[1], :lineno => match[2].to_i, :method => match[3] }
          else
            { :filename => '<unknown>', :lineno => 0, :method => frame }
          end
        end
      end

      # Returns the backtrace to be sent to our API. There are 3 options:
      #
      # 1. The exception received has a backtrace, then that backtrace is returned.
      # 2. configuration.populate_empty_backtraces is disabled, we return [] here
      # 3. The user has configuration.populate_empty_backtraces is enabled, then:
      #
      # We want to send the caller as backtrace, but the first lines of that array
      # are those from the user's Rollbar.error line until this method. We want
      # to remove those lines.
      def exception_backtrace(current_exception)
        return current_exception.backtrace if current_exception.backtrace.respond_to?(:map)
        return [] unless configuration.populate_empty_backtraces

        caller_backtrace = caller
        caller_backtrace.shift while caller_backtrace[0].include?(rollbar_lib_gem_dir)
        caller_backtrace
      end

      def rollbar_lib_gem_dir
        Gem::Specification.find_by_name('rollbar').gem_dir + '/lib'
      end
    end
  end
end