# selector.rb: Prompt user to choose an option in a list of options
# See packages/hunspell.rb for example usage
require_relative 'color'
require_relative 'crewlog'
require 'io/console'

class Selector
  @@default_prompt = {
      heading: 'There are %{total_opts} provider(s) for this package: ',
    countdown: 'Default selected in %%2.2i second(s). Enter your choice [1 = %{default}]: '
  }

  def initialize(options, prompt = @@default_prompt, timeout = 10)
    @options = options
    # Set timeout to zero if a non-interactive console.
    # Check noninteractive usage with `setsid command`.
    @timeout = if !IO.console&.console_mode || IO.console&.winsize == [0, 0]
                 1
               else
                 timeout
               end

    # substitute expressions in the message ("%{variable}")
    @prompt = prompt.transform_values { |p| format(p, { total_opts: @options.size, default: @options[0][:value] }) }
  end

  def show_prompt
    # show_prompt(): Show prompt based on given options
    warn "#{@prompt[:heading]}\n\n"

    @options.each_with_index do |opt, i|
      $stderr.print "  #{i + 1}: #{opt[:value]}"
      $stderr.print " (#{opt[:description]})" if opt[:description]
      $stderr.print "\n"
    end

    warn "\n"

    # only show prompt when crew is running in an interactive terminal
    if $stdin.isatty
      begin
        fire_timer { Thread.kill(@io_read) }
        start_reading

        @io_read.join
      ensure
        Thread.kill(@countdown)
      end
    end

    if @io_read.nil? || @io_read[:input].to_s.chomp.empty?
      # empty input or timeout
      warn "Selected \"#{@options[0][:value]}\" by default.".yellow
      choice = 1
    elsif Integer(@io_read[:input], exception: false)&.between?(1, @options.size)
      # when input is valid (is an integer and in range)
      choice = @io_read[:input].to_i
    else
      # invalid input
      warn <<~EOT.yellow
        I don't understand "#{@io_read[:input]}". :(

        Selected "#{@options[0][:value]}" by default.
      EOT
      choice = 1
    end

    # return result
    return @options[choice - 1][:value]
  end

  private

  def fire_timer(&when_timeout)
    # fire_timer(): start a timer in separate thread
    @countdown = Thread.new do
      @timeout.downto(0).each do |remaining_time|
        # print current countdown
        $stderr.print "\r#{format(@prompt[:countdown], remaining_time)}"

        if remaining_time.zero?
          warn "\nTime expired.\n".yellow
          when_timeout.call
        else
          sleep 1
        end
      end
    end
  end

  def start_reading
    # start_reading(): read from terminal in separate thread
    @io_read = Thread.new do
      # discard any input in the input buffer
      $stdin.read_nonblock(1024)
    rescue IO::WaitReadable
      # We wait here for reading as per https://docs.ruby-lang.org/en/master/IO.html#method-c-select
    ensure
      Thread.current[:input] = $stdin.getc
    end
  end
end
