require ‘benchmark’

if ENV == ‘true’

ENSURE_IT_REFINED = true

end

module EnsureIt

class Benchmark < Thor
  OBJECTS = [
    nil, true, false, 0, 0.1, '1/3'.to_r, 'test', :test, Object, Class, ->{}
  ]

  class_option :smart, aliases: '-s', desc: 'use smart errors'
  class_option :count, aliases: '-n', desc: 'number of tests', default: 10000
  class_option :profile, aliases: '-p', desc: 'profile'

  desc 'symbol', 'runs benchmarks for ensure_symbol'
  def symbol
    run_benchmark :symbol, ensure_proc: ->(x) { x.ensure_symbol } do |x|
      x = x.to_sym if x.is_a?(String)
      x.is_a?(Symbol) ? x : nil
    end
  end

  desc 'symbol!', 'runs benchmarks for ensure_symbol!'
  def symbol!
    run_benchmark :symbol!, ensure_proc: ->(x) { x.ensure_symbol! } do |x|
      x = x.to_sym if x.is_a?(String)
      raise ArgumentError unless x.is_a?(Symbol)
    end
  end

  desc 'string', 'runs benchmarks for ensure_string'
  def string
    run_benchmark :string, ensure_proc: ->(x) { x.ensure_string } do |x|
      x = x.to_s if x.is_a?(Symbol)
      x.is_a?(String) ? x : nil
    end
  end

  desc 'string!', 'runs benchmarks for ensure_string!'
  def string!
    run_benchmark :string!, ensure_proc: ->(x) { x.ensure_string! } do |x|
      x = x.to_s if x.is_a?(Symbol)
      raise ArgumentError unless x.is_a?(String)
    end
  end

  desc 'integer', 'runs benchmarks for ensure_integer'
  def integer
    run_benchmark :integer, ensure_proc: ->(x) { x.ensure_integer } do |x|
      if x.is_a?(Integer)
        x
      elsif x.is_a?(Float) || x.is_a?(Rational)
        x.round
      elsif x.is_a?(String)
        case x
        when ::EnsureIt::INT_REGEXP then x.to_i
        when ::EnsureIt::HEX_REGEXP then x[2..-1].to_i(16)
        when ::EnsureIt::BIN_REGEXP then x[2..-1].to_i(2)
        else nil
        end
      else
        nil
      end
    end
  end

  desc 'integer!', 'runs benchmarks for ensure_integer!'
  def integer!
    run_benchmark :integer!, ensure_proc: ->(x) { x.ensure_integer! } do |x|
      if x.is_a?(Integer)
        x
      elsif x.is_a?(Float) || x.is_a?(Rational)
        x.round
      elsif x.is_a?(String)
        case x
        when ::EnsureIt::INT_REGEXP then x.to_i
        when ::EnsureIt::HEX_REGEXP then x[2..-1].to_i(16)
        when ::EnsureIt::BIN_REGEXP then x[2..-1].to_i(2)
        else raise ArgumentError
        end
      else
        raise ArgumentError
      end
    end
  end

  desc 'float', 'runs benchmarks for ensure_float'
  def float
    run_benchmark :float, ensure_proc: ->(x) { x.ensure_float } do |x|
      if x.is_a?(Float)
        x
      elsif x.is_a?(Numeric) ||
            x.is_a?(String) && ::EnsureIt::FLOAT_REGEXP =~ x
        x.to_f
      else
        nil
      end
    end
  end

  desc 'float!', 'runs benchmarks for ensure_float!'
  def float!
    run_benchmark :float!, ensure_proc: ->(x) { x.ensure_float! } do |x|
      if x.is_a?(Float)
        x
      elsif x.is_a?(Numeric) ||
            x.is_a?(String) && ::EnsureIt::FLOAT_REGEXP =~ x
        x.to_f
      else
        raise ArgumentError
      end
    end
  end

  desc 'non_bang', 'runs all non-bang benchmarks'
  def non_bang
    invoke(:symbol)
    invoke(:string)
    invoke(:integer)
    invoke(:float)
  end

  desc 'bang', 'runs all bang benchmarks'
  def bang
    invoke(:symbol!)
    invoke(:string!)
    invoke(:integer!)
    invoke(:float!)
  end

  desc 'all', 'runs all benchmarks'
  def all
    invoke(:non_bang)
    invoke(:bang)
  end

  no_commands do
    protected

    def run_benchmark(task_name, ensure_proc: proc {}, &standard_proc)
      load_ensure_it
      ensure_it, standard = [], []
      start_task(task_name)
      ::Benchmark.benchmark do |x|
        start_profile(task_name)
        ensure_it = x.report('ensure_it:    ') do
          OBJECTS.each do |obj|
            count.times { ensure_proc.call(obj) rescue ::EnsureIt::Error }
          end
        end
        standard = x.report('standard way: ') do
          OBJECTS.each do |obj|
            count.times { standard_proc.call(obj) rescue ArgumentError }
          end
        end
        end_profile(task_name)
      end
      end_task(task_name)
      [ensure_it, standard]
    end

    def start_task(task_name)
      text = "Starting benchmarks for #ensure_#{task_name} "
      if ENSURE_IT_REFINED == true
        text << ' with refined version of EnsureIt.'
      else
        text << ' with monkey-patched version of EnsureIt.'
      end
      text << " Errors: #{::EnsureIt.config.errors}."
      text << " Ruby version: #{RUBY_VERSION}"
      say text, :green
    end

    def end_task(task_name); end

    def start_profile(task_name)
      RubyProf.start if profile?
    end

    def end_profile(task_name)
      if profile?
        result = RubyProf.stop
        result.eliminate_methods!([
          /\ABenchmark/,
          /Thor::(?!Sandbox::EnsureIt::Benchmark#run_benchmark)/,
          /Integer#times/, /Struct::Tms/, /<Module::Process>/,
          /<Class::Time>/, /Time/
        ])
        file = File.join(profile_path, "ensure_#{task_name}.dot")
        unless Dir.exist?(File.dirname(file))
          FileUtils.mkpath File.dirname(file)
        end
        printer = RubyProf::DotPrinter.new(result)
        File.open(file, 'w') { |f| printer.print(f, min_percent: 0) }
        file = File.join(profile_path, "ensure_#{task_name}.txt")
        printer = RubyProf::GraphPrinter.new(result)
        File.open(file, 'w') { |f| printer.print(f, min_percent: 0) }
      end
    end

    def count
      n = options['count'].to_i
      n <= 0 ? 10000 : n
    end

    def refined?
      options['refined'] == 'refined' || options['refined'] == 'true'
    end

    def profile?
      options.key?('profile')
    end

    def profile_path
      if options['profile'] == 'profile' || options['profile'] == 'true'
        File.join(Dir.pwd, 'tmp')
      else
        File.expand_path(File.join('..', 'tmp'), __FILE__)
      end
    end

    def errors
      if options['smart'] == 'smart' || options['smart'] == true
        :smart
      else
        :standard
      end
    end

    def load_ensure_it
      lib = File.expand_path(File.join('..', '..'), __FILE__)
      $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
      require(refined? ? 'ensure_it_refined' : 'ensure_it')
      ::EnsureIt.configure do |config|
        config.errors = errors
      end
      require 'ruby-prof' if profile?
    end
  end
end

end