require 'mspec/expectations/expectations'
require 'mspec/runner/actions/timer'
require 'mspec/runner/actions/tally'
require 'mspec/utils/options'

if ENV['CHECK_LEAKS']
  require 'mspec/runner/actions/leakchecker'
end

if ENV['CHECK_LEAKS'] || ENV['CHECK_CONSTANT_LEAKS']
  require 'mspec/runner/actions/constants_leak_checker'
end

class BaseFormatter
  attr_reader :exceptions, :timer, :tally

  def initialize(out = nil)
    @current_state = nil
    @exception = false
    @failure = false
    @exceptions = []

    @count = 0 # For subclasses

    if out
      @out = File.open out, "w"
    else
      @out = $stdout
    end

    err = MSpecOptions.latest && MSpecOptions.latest.config[:error_output]
    if err
      @err = (err == 'stderr') ? $stderr : File.open(err, "w")
    else
      @err = @out
    end
  end

  # Creates the +TimerAction+ and +TallyAction+ instances and registers them.
  def register
    (@timer = TimerAction.new).register
    (@tally = TallyAction.new).register
    @counter = @tally.counter

    if ENV['CHECK_LEAKS']
      LeakCheckerAction.new.register
    end

    if ENV['CHECK_LEAKS'] || ENV['CHECK_CONSTANT_LEAKS']
      save = ENV['CHECK_LEAKS'] == 'save' || ENV['CHECK_CONSTANT_LEAKS'] == 'save'
      ConstantsLeakCheckerAction.new(save).register
    end

    MSpec.register :abort,     self
    MSpec.register :before,    self
    MSpec.register :after,     self
    MSpec.register :exception, self
    MSpec.register :finish,    self
  end

  def abort
    if @current_state
      puts "\naborting example: #{@current_state.description}"
    end
  end

  # Returns true if any exception is raised while running
  # an example. This flag is reset before each example
  # is evaluated.
  def exception?
    @exception
  end

  # Returns true if all exceptions during the evaluation
  # of an example are failures rather than errors. See
  # <tt>ExceptionState#failure</tt>. This flag is reset
  # before each example is evaluated.
  def failure?
    @failure
  end

  # Callback for the MSpec :before event. Resets the
  # +#exception?+ and +#failure+ flags.
  def before(state = nil)
    @current_state = state
    @failure = @exception = false
  end

  # Callback for the MSpec :exception event. Stores the
  # +ExceptionState+ object to generate the list of backtraces
  # after all the specs are run. Also updates the internal
  # +#exception?+ and +#failure?+ flags.
  def exception(exception)
    @count += 1
    @failure = @exception ? @failure && exception.failure? : exception.failure?
    @exception = true
    @exceptions << exception
  end

  # Callback for the MSpec :after event.
  def after(state = nil)
    @current_state = nil
  end

  # Callback for the MSpec :start event. Calls :after event.
  # Defined here, in the base class, and used by MultiFormatter.
  def start
    after
  end

  # Callback for the MSpec :unload event. Calls :after event.
  # Defined here, in the base class, and used by MultiFormatter.
  def unload
    after
  end

  # Callback for the MSpec :finish event. Prints a description
  # and backtrace for every exception that occurred while
  # evaluating the examples.
  def finish
    print "\n"

    if MSpecOptions.latest && MSpecOptions.latest.config[:print_skips]
      print "\nSkips:\n" unless MSpec.skips.empty?
      MSpec.skips.each do |skip, block|
        print "#{skip.message} in #{(block.source_location || ['?']).join(':')}\n"
      end
    end

    count = 0
    @exceptions.each do |exc|
      count += 1
      print_exception(exc, count)
    end
    print "\n#{@timer.format}\n\n#{@tally.format}\n"
  end

  def print_exception(exc, count)
    outcome = exc.failure? ? "FAILED" : "ERROR"
    @err.print "\n#{count})\n#{exc.description} #{outcome}\n"
    @err.print exc.message, "\n"
    @err.print exc.backtrace, "\n"
  end

  # A convenience method to allow printing to different outputs.
  def print(*args)
    @out.print(*args)
    @out.flush
  end
end
