require 'timeout'
require 'windows/process'
require 'windows/synchronize'
require 'windows/handle'
require "win32/process"
require "win32/clipboard"

module Autogui

  # Wrapper class for text portion of the RubyGem win32/clipboard 
  # @see http://github.com/djberg96/win32-clipboard
  class Clipboard

    # Clipboard text getter
    # 
    # @return [String] clipboard data
    #
    def text 
      Win32::Clipboard.data
    end

    # Clipboard text setter
    #
    # @param [String] str text to load onto the clipboard
    #
    def text=(str)
      Win32::Clipboard.set_data(str)
    end

  end

  # The Application class wraps a binary application so 
  # that it can be started and controlled via Ruby.  This
  # class is meant to be subclassed.
  #
  # @example
  #
  #   class Calculator < Autogui::Application
  #
  #     def initialize(options = {})
  #       defaults = {
  #                    :name => "calc",
  #                    :title => "Calculator",
  #                    :logger_logfile => 'log/calc.log'
  #                  }
  #       super defaults.merge(options)
  #     end
  #
  #     def edit_window
  #       main_window.children.find {|w| w.window_class == 'Edit'}
  #     end
  #
  #     def dialog_about
  #       Autogui::EnumerateDesktopWindows.new.find do |w| 
  #         w.title.match(/About Calculator/) && (w.pid == pid)
  #       end
  #     end
  #
  #     def clear_entry
  #       set_focus
  #       keystroke(VK_DELETE)
  #     end
  #   end
  #
  class Application
    include Windows::Process           
    include Windows::Synchronize
    include Windows::Handle
    include Autogui::Logging

    # @return [String] the executable name of the application
    attr_accessor :name  
    
    # @return [String] the executable application parameters 
    attr_accessor :parameters  
    
    # @return [String] window title of the application
    attr_accessor :title  

    # @return [Number] the process identifier (PID) returned by Process.create
    attr_reader :pid

    # @return [Number] the process thread id returned by Process.create
    attr_reader :thread_id

    # @return [Number] the main_window wait timeout in seconds
    attr_accessor :main_window_timeout
    
    # @return [Number] the wait timeout in seconds used by Process.create
    attr_accessor :create_process_timeout
    
    # @example initialize an application on the path
    #
    #   app = Application.new :name => "calc"  
    #
    # @example initialize with relative DOS path
    #
    #   app = Application.new :name => "binaries\\mybinary.exe"  
    #
    # @example initialize with full DOS path
    #
    #   app = Application.new :name => "\\windows\\system32\\calc.exe"  
    #
    # @example initialize with logging to file at the default WARN level  (STDOUT logging is the default)
    #
    #   app = Application.new :name => "calc", :logger_logfile => 'log/calc.log' 
    #
    # @example initialize with logging to file at DEBUG level
    #
    #   include Autogui::Logging
    #   app = Application.new :name => "calc", :logger_logfile => 'log/calc.log', :logger.level => Log4r::DEBUG
    #
    # @example initialize without logging to file and turn it on later
    #
    #   include Autogui::Logging
    #   app = Application.new :name => "calc"
    #   logger.logfile = 'app.log'
    #
    # @param [Hash] options initialize options
    # @option options [String] :name a valid win32 exe name with optional path
    # @option options [String] :title the application window title, used along with the pid to locate the application main window, defaults to :name
    # @option options [Number] :parameters command line parameters used by Process.create
    # @option options [Number] :create_process_timeout (10) timeout in seconds to wait for the create_process to return 
    # @option options [Number] :main_window_timeout (10) timeout in seconds to wait for main_window to appear
    # @option options [String] :logger_logfile (nil) initialize Log4r::Logger's output filename
    # @option options [String] :logger_level (Log4r::WARN) initialize Log4r::Logger's initial level
    #
    def initialize(options = {})

      unless options.kind_of?(Hash)
        raise_error ArgumentError, 'Initialize expecting options to be a Hash'
      end

      @name = options[:name] || name
      @title = options[:title] || name
      @main_window_timeout = options[:main_window_timeout] || 10
      @create_process_timeout = options[:create_process_timeout] || 10
      @parameters = options[:parameters]

      # logger setup
      logger.logfile = options[:logger_logfile] if options[:logger_logfile]
      logger.level = options[:logger_level] if options[:logger_level]

      # sanity checks
      raise_error 'application name not set' unless name 

      start
    end
    
    # Start up the binary application via Process.create and
    # set the window focus to the main_window
    #
    # @raise [Exception] if create_process_timeout exceeded
    # @raise [Exception] if start failed for any reason other than create_process_timeout
    #
    # @return [Number] the pid
    #
    def start
      
      command_line = name
      command_line = name + ' ' + parameters if parameters

      # returns a struct, raises an error if fails
      process_info = Process.create(
         :command_line => command_line,
         :close_handles => false,
         :creation_flags => Process::DETACHED_PROCESS
      )
      @pid = process_info.process_id
      @thread_id = process_info.thread_id
      process_handle = process_info.process_handle
      thread_handle = process_info.thread_handle

      # wait for process
      ret = WaitForInputIdle(process_handle, (create_process_timeout * 1000))

      # done with the handles
      CloseHandle(process_handle)
      CloseHandle(thread_handle)

      raise_error "start command failed on create_process_timeout" if ret == WAIT_TIMEOUT 
      raise_error "start command failed while waiting for idle input, reason unknown" unless (ret == 0)
      @pid
    end

    # The application main window found by enumerating windows 
    # by title and application pid.  This method will keep looking
    # unit main_window_timeout (default: 10s) is exceeded.
    #
    # @raise [Exception] if the main window cannot be found
    #
    # @return [Autogui::Window]
    # @see initialize for options
    # 
    def main_window
      return @main_window if @main_window

      # pre sanity checks
      raise_error "calling main_window without a pid, application not initialized properly" unless @pid
      raise_error "calling main_window without a window title, application not initialized properly" unless @title

      timeout(main_window_timeout) do
        begin
          # There may be multiple instances, use title and pid to id our main window
          @main_window = Autogui::EnumerateDesktopWindows.new.find do |w| 
            w.title.match(title) && w.pid == pid 
          end
          sleep 0.1 
         end until @main_window
      end

      # post sanity checks
      raise_error "cannot find main_window, check application title" unless @main_window

      @main_window
    end

    # Call the main_window's close method
    #
    # PostMessage SC_CLOSE and optionally wait for the window to close
    #
    # @param [Hash] options
    # @option options [Boolean] :wait_for_close (true) sleep while waiting for timeout or close
    # @option options [Boolean] :timeout (5) wait_for_close timeout in seconds
    #
    def close(options={})
      main_window.close(options)
    end

    # Send SIGKILL to force the application to die
    def kill
      Process::kill(9, pid)
    end

    # @return [Boolean] if the application is currently running
    def running?
      main_window && (main_window.is_window?)
    end

    # Set the application input focus to the main_window
    # 
    # @return [Number] nonzero number if sucess, nil or zero if failed
    # 
    def set_focus
      main_window.set_focus if running? 
    end

    # The main_window text including all child windows 
    # joined together with newlines. Faciliates matching text.
    #
    # @example partial match of the Window's calulator's about dialog copywrite text
    #
    #   dialog_about = @calculator.dialog_about
    #   dialog_about.title.should == "About Calculator"
    #   dialog_about.combined_text.should match(/Microsoft . Calculator/)
    #
    # @return [String] with newlines
    #
    def combined_text
      main_window.combined_text if running? 
    end

    # @example set the clipboard text and paste it with Control-V
    #
    #   @calculator.edit_window.set_focus
    #   @calculator.clipboard.text = "12345"
    #   @calculator.edit_window.text.strip.should == "0."
    #   keystroke(VK_CONTROL, VK_V) 
    #   @calculator.edit_window.text.strip.should == "12,345."
    #
    # @return [Clipboard] 
    #
    def clipboard
      @clipboard || Autogui::Clipboard.new
    end

    private

    # @overload raise_error(exception, message)
    #   raise and log specific exception with message
    #   @param [Exception] to raise
    #   @param [String] message error message to raise
    #
    # @overload raise_error(message)
    #   raise and log generic exception with message
    #   @param [String] message error message to raise
    #
    def raise_error(*args)
      if args.first.kind_of?(Exception)
        exception_type = args.shift
        error_message = args.shift || 'Unknown error'
      else
        raise ArgumentError unless args.first.is_a?(String)
        exception_type = RuntimeError
        error_message = args.shift || 'Unknown error'
      end

      logger.error error_message
      raise  exception_type, error_message
    end

  end

end