require "mutex_m"
require "debrew/irb"

module Debrew
  extend Mutex_m

  Ignorable = Module.new

  module Raise
    def raise(*)
      super
    rescue Exception => e # rubocop:disable Lint/RescueException
      e.extend(Ignorable)
      super(e) unless Debrew.debug(e) == :ignore
    end

    alias fail raise
  end

  module Formula
    def install
      Debrew.debrew { super }
    end

    def patch
      Debrew.debrew { super }
    end

    def test
      Debrew.debrew { super }
    end
  end

  class Menu
    Entry = Struct.new(:name, :action)

    attr_accessor :prompt, :entries

    def initialize
      @entries = []
    end

    def choice(name, &action)
      entries << Entry.new(name.to_s, action)
    end

    def self.choose
      menu = new
      yield menu

      choice = nil
      while choice.nil?
        menu.entries.each_with_index { |e, i| puts "#{i + 1}. #{e.name}" }
        print menu.prompt unless menu.prompt.nil?

        input = $stdin.gets || exit
        input.chomp!

        i = input.to_i
        if i.positive?
          choice = menu.entries[i - 1]
        else
          possible = menu.entries.select { |e| e.name.start_with?(input) }

          case possible.size
          when 0 then puts "No such option"
          when 1 then choice = possible.first
          else puts "Multiple options match: #{possible.map(&:name).join(" ")}"
          end
        end
      end

      choice[:action].call
    end
  end

  @active = false
  @debugged_exceptions = Set.new

  class << self
    extend Predicable
    alias original_raise raise
    attr_predicate :active?
    attr_reader :debugged_exceptions
  end

  def self.debrew
    @active = true
    Object.send(:include, Raise)

    begin
      yield
    rescue SystemExit
      original_raise
    rescue Exception => e # rubocop:disable Lint/RescueException
      debug(e)
    ensure
      @active = false
    end
  end

  def self.debug(e)
    original_raise(e) unless active? &&
                             debugged_exceptions.add?(e) &&
                             try_lock

    begin
      puts e.backtrace.first.to_s
      puts Formatter.error(e, label: e.class.name)

      loop do
        Menu.choose do |menu|
          menu.prompt = "Choose an action: "

          menu.choice(:raise) { original_raise(e) }
          menu.choice(:ignore) { return :ignore } if e.is_a?(Ignorable)
          menu.choice(:backtrace) { puts e.backtrace }

          if e.is_a?(Ignorable)
            menu.choice(:irb) do
              puts "When you exit this IRB session, execution will continue."
              set_trace_func proc { |event, _, _, id, binding, klass|
                if klass == Raise && id == :raise && event == "return"
                  set_trace_func(nil)
                  synchronize { IRB.start_within(binding) }
                end
              }

              return :ignore
            end
          end

          menu.choice(:shell) do
            puts "When you exit this shell, you will return to the menu."
            interactive_shell
          end
        end
      end
    ensure
      unlock
    end
  end
end
