require 'tempfile'
require 'shellwords'

module Commander
  
  ##
  # = User Interaction
  #
  # Commander's user interaction module mixes in common
  # methods which extend HighLine's functionality such 
  # as a #password method rather than calling #ask directly.
  
  module UI
    
    module_function
    
    #--
    # Auto include growl when available.
    #++
    
    begin
      require 'growl'
    rescue LoadError
      # Do nothing
    else
      include Growl
    end
    
    ##
    # Ask the user for a password. Specify a custom
    # _message_ other than 'Password: ' or override the 
    # default _mask_ of '*'.
    
    def password message = 'Password: ', mask = '*'
      pass = ask(message) { |q| q.echo = mask }
      pass = password message, mask if pass.nil? || pass.empty?
      pass
    end
    
    ##
    # Choose from a set array of _choices_.
    
    def choose message, *choices
      say message
      super(*choices)
    end
    
    ##
    # 'Log' an _action_ to the terminal. This is typically used
    # for verbose output regarding actions performed. For example:
    #
    #   create  path/to/file.rb
    #   remove  path/to/old_file.rb
    #   remove  path/to/old_file2.rb
    #
    
    def log action, *args
      say '%15s  %s' % [action, args.join(' ')]
    end

    ##
    # 'Say' something using the OK color (green).
    #
    # === Examples
    #   say_ok 'Everything is fine'
    #   say_ok 'It is ok', 'This is ok too'
    #

    def say_ok *args
      args.each do |arg|
        say $terminal.color(arg, :green)
      end
    end

    ##
    # 'Say' something using the WARNING color (yellow).
    #
    # === Examples
    #   say_warning 'This is a warning'
    #   say_warning 'Be careful', 'Think about it'
    #

    def say_warning *args
      args.each do |arg|
        say $terminal.color(arg, :yellow)
      end
    end

    ##
    # 'Say' something using the ERROR color (red).
    #
    # === Examples
    #   say_error 'Everything is not fine'
    #   say_error 'It is not ok', 'This is not ok too'
    #

    def say_error *args
      args.each do |arg|
        say $terminal.color(arg, :red)
      end
    end

    ##
    # 'Say' something using the specified color
    #
    # === Examples
    #   color 'I am blue', :blue
    #   color 'I am bold', :bold
    #   color 'White on Red', :white, :on_red
    #
    # === Notes
    #   You may use:
    #   * color:    black blue cyan green magenta red white yellow
    #   * style:    blink bold clear underline
    #   * highligh: on_<color>

    def color(*args)
      say $terminal.color(*args)
    end

    ##
    # Speak _message_ using _voice_ at a speaking rate of _rate_
    # 
    # Voice defaults to 'Alex', which is one of the better voices.
    # Speaking rate defaults to 175 words per minute
    #
    # === Examples
    #    
    #   speak 'What is your favorite food? '
    #   food = ask 'favorite food?: '
    #   speak "Wow, I like #{food} too. We have so much in common." 
    #   speak "I like #{food} as well!", "Victoria", 190
    #
    # === Notes
    #
    # * MacOS only
    #
    
    def speak message, voice = :Alex, rate = 175
      Thread.new { applescript "say #{message.inspect} using #{voice.to_s.inspect} speaking rate #{rate}" }
    end
    
    ##
    # Converse with speech recognition. 
    #
    # Currently a "poorman's" DSL to utilize applescript and
    # the MacOS speech recognition server.
    #
    # === Examples
    #
    #   case converse 'What is the best food?', :cookies => 'Cookies', :unknown => 'Nothing'
    #   when :cookies 
    #     speak 'o.m.g. you are awesome!'
    #   else
    #     case converse 'That is lame, shall I convince you cookies are the best?', :yes => 'Ok', :no => 'No', :maybe => 'Maybe another time'
    #     when :yes
    #       speak 'Well you see, cookies are just fantastic.'
    #     else
    #       speak 'Ok then, bye.'
    #     end
    #   end
    #
    # === Notes
    #
    # * MacOS only
    #
    
    def converse prompt, responses = {}
      i, commands = 0, responses.map { |key, value| value.inspect }.join(',')
      statement = responses.inject '' do |statement, (key, value)|
        statement << (((i += 1) == 1 ? 
          %(if response is "#{value}" then\n):
            %(else if response is "#{value}" then\n))) <<
              %(do shell script "echo '#{key}'"\n)
      end
      applescript(%(
        tell application "SpeechRecognitionServer" 
          set response to listen for {#{commands}} with prompt "#{prompt}"
          #{statement}
          end if
        end tell
      )).strip.to_sym
    end
    
    ##
    # Execute apple _script_.
    
    def applescript script
      `osascript -e "#{ script.gsub('"', '\"') }"`
    end
    
    ##
    # Normalize IO streams, allowing for redirection of
    # +input+ and/or +output+, for example:
    #
    #   $ foo              # => read from terminal I/O
    #   $ foo in           # => read from 'in' file, output to terminal output stream
    #   $ foo in out       # => read from 'in' file, output to 'out' file
    #   $ foo < in > out   # => equivalent to above (essentially)
    #
    # Optionally a +block+ may be supplied, in which case
    # IO will be reset once the block has executed.
    #
    # === Examples
    #
    #   command :foo do |c|
    #     c.syntax = 'foo [input] [output]'
    #     c.when_called do |args, options|
    #       # or io(args.shift, args.shift)
    #       io *args
    #       str = $stdin.gets
    #       puts 'input was: ' + str.inspect
    #     end
    #   end
    #
    
    def io input = nil, output = nil, &block
      $stdin = File.new(input) if input
      $stdout = File.new(output, 'r+') if output
      if block
        yield
        reset_io
      end
    end
    
    ##
    # Reset IO to initial constant streams.
    
    def reset_io
      $stdin, $stdout = STDIN, STDOUT
    end

    ##
    # Find an editor available in path. Optionally supply the _preferred_
    # editor. Returns the name as a string, nil if none is available.

    def available_editor preferred = nil
      [preferred, ENV['EDITOR'], 'mate -w', 'vim', 'vi', 'emacs', 'nano', 'pico'].
        compact.
        find {|name| system("hash #{name.split.first} 2>&-") }
    end
    
    ##
    # Prompt an editor for input. Optionally supply initial
    # _input_ which is written to the editor.
    #
    # _preferred_editor_ can be hinted.
    #
    # === Examples
    #
    #   ask_editor                # => prompts EDITOR with no input
    #   ask_editor('foo')         # => prompts EDITOR with default text of 'foo'
    #   ask_editor('foo', 'mate -w')  # => prompts TextMate with default text of 'foo'
    #
       
    def ask_editor input = nil, preferred_editor = nil
      editor = available_editor preferred_editor
      program = Commander::Runner.instance.program(:name).downcase rescue 'commander'
      tmpfile = Tempfile.new program
      begin
        tmpfile.write input if input
        tmpfile.close
        system("#{editor} #{tmpfile.path.shellescape}") ? IO.read(tmpfile.path) : nil
      ensure
        tmpfile.unlink
      end
    end
    
    ##
    # Enable paging of output after called.
    
    def enable_paging
      return unless $stdout.tty?
      return unless Process.respond_to? :fork
      read, write = IO.pipe

      # Kernel.fork is not supported on all platforms and configurations.
      # As of Ruby 1.9, `Process.respond_to? :fork` should return false on
      # configurations that don't support it, but versions before 1.9 don't
      # seem to do this reliably and instead raise a NotImplementedError
      # (which is rescued below).
      
      if Kernel.fork
        $stdin.reopen read
        write.close; read.close
        Kernel.select [$stdin]
        ENV['LESS'] = 'FSRX'
        pager = ENV['PAGER'] || 'less'
        exec pager rescue exec '/bin/sh', '-c', pager
      else
        # subprocess
        $stdout.reopen write
        $stderr.reopen write if $stderr.tty?
        write.close; read.close
      end
    rescue NotImplementedError
    ensure
      write.close if write && !write.closed?
      read.close if read && !read.closed?
    end

    ##
    # Output progress while iterating _arr_.
    #
    # === Examples
    #
    #   uris = %w( http://vision-media.ca http://google.com )
    #   progress uris, :format => "Remaining: :time_remaining" do |uri|
    #     res = open uri
    #   end
    #

    def progress arr, options = {}, &block
      bar = ProgressBar.new arr.length, options
      bar.show
      arr.each { |v| bar.increment yield(v) }
    end
        
    ##
    # Implements ask_for_CLASS methods.
    
    module AskForClass
      # All special cases in HighLine::Question#convert, except those that implement #parse
      ([Float, Integer, String, Symbol, Regexp, Array, File, Pathname] +
      # All Classes that respond to #parse
      Object.constants.map do |const|
        # const_get(:Config) issues a deprecation warning on ruby 1.8.7
        Object.const_get(const) unless const == :Config
      end.select do |const|
        const.class == Class && const.respond_to?(:parse)
      end).each do |klass|
        define_method "ask_for_#{klass.to_s.downcase}" do |prompt|
          $terminal.ask(prompt, klass)
        end
      end
    end
    
    ##
    # Substitute _hash_'s keys with their associated values in _str_.
    
    def replace_tokens str, hash #:nodoc:
      hash.inject str do |str, (key, value)|
        str.gsub ":#{key}", value.to_s
      end
    end
    
    ##
    # = Progress Bar
    #
    # Terminal progress bar utility. In its most basic form
    # requires that the developer specifies when the bar should
    # be incremented. Note that a hash of tokens may be passed to
    # #increment, (or returned when using Object#progress).
    #
    #   uris = %w( 
    #     http://vision-media.ca
    #     http://yahoo.com
    #     http://google.com
    #     )
    #   
    #   bar = Commander::UI::ProgressBar.new uris.length, options
    #   threads = []
    #   uris.each do |uri|
    #     threads << Thread.new do
    #       begin
    #         res = open uri
    #         bar.increment :uri => uri
    #       rescue Exception => e
    #         bar.increment :uri => "#{uri} failed"
    #       end
    #     end
    #   end
    #   threads.each { |t| t.join }
    #
    # The Object method #progress is also available:
    #
    #   progress uris, :width => 10 do |uri|
    #     res = open uri
    #     { :uri => uri } # Can now use :uri within :format option
    #   end
    #

    class ProgressBar

      ##
      # Creates a new progress bar.
      #
      # === Options
      #    
      #   :title              Title, defaults to "Progress"
      #   :width              Width of :progress_bar
      #   :progress_str       Progress string, defaults to "="
      #   :incomplete_str     Incomplete bar string, defaults to '.'
      #   :format             Defaults to ":title |:progress_bar| :percent_complete% complete "
      #   :tokens             Additional tokens replaced within the format string
      #   :complete_message   Defaults to "Process complete"
      #
      # === Tokens
      #
      #   :title 
      #   :percent_complete
      #   :progress_bar
      #   :step
      #   :steps_remaining
      #   :total_steps
      #   :time_elapsed
      #   :time_remaining
      #

      def initialize total, options = {}
        @total_steps, @step, @start_time = total, 0, Time.now
        @title = options.fetch :title, 'Progress'
        @width = options.fetch :width, 25
        @progress_str = options.fetch :progress_str, '='
        @incomplete_str = options.fetch :incomplete_str, '.'
        @complete_message = options.fetch :complete_message, 'Process complete'
        @format = options.fetch :format, ':title |:progress_bar| :percent_complete% complete '
        @tokens = options.fetch :tokens, {}
      end
      
      ##
      # Completion percentage.
      
      def percent_complete
        if @total_steps.zero?
          100
        else
          @step * 100 / @total_steps
        end
      end
      
      ##
      # Time that has elapsed since the operation started.
      
      def time_elapsed
        Time.now - @start_time
      end
      
      ##
      # Estimated time remaining.
      
      def time_remaining
        (time_elapsed / @step) * steps_remaining
      end
      
      ##
      # Number of steps left.
      
      def steps_remaining
        @total_steps - @step
      end
      
      ##
      # Formatted progress bar.
      
      def progress_bar
        (@progress_str * (@width * percent_complete / 100)).ljust @width, @incomplete_str
      end
      
      ##
      # Generates tokens for this step.
      
      def generate_tokens
        {
          :title => @title,
          :percent_complete => percent_complete,
          :progress_bar => progress_bar, 
          :step => @step,
          :steps_remaining => steps_remaining,
          :total_steps => @total_steps, 
          :time_elapsed => "%0.2fs" % time_elapsed,
          :time_remaining => @step > 0 ? "%0.2fs" % time_remaining : '',
        }.
        merge! @tokens
      end

      ##
      # Output the progress bar.

      def show
        unless finished?
          erase_line
          if completed?
            $terminal.say UI.replace_tokens(@complete_message, generate_tokens) if @complete_message.is_a? String
          else
            $terminal.say UI.replace_tokens(@format, generate_tokens) << ' '
          end
        end
      end
      
      ##
      # Whether or not the operation is complete, and we have finished.
      
      def finished?
        @step == @total_steps + 1
      end

      ##
      # Whether or not the operation has completed.

      def completed?
        @step == @total_steps
      end

      ##
      # Increment progress. Optionally pass _tokens_ which
      # can be displayed in the output format.

      def increment tokens = {}
        @step += 1
        @tokens.merge! tokens if tokens.is_a? Hash
        show
      end

      ##
      # Erase previous terminal line.

      def erase_line
        # highline does not expose the output stream
        $terminal.instance_variable_get('@output').print "\r\e[K"
      end
    end
  end
end
