module RAutomation
  module Adapter
    module Win32
      # @private
      module Functions
        extend FFI::Library

        ffi_lib 'user32', 'kernel32', 'ole32', File.dirname(__FILE__) + '/../../../../ext/IAccessibleDLL/Release/iaccessibleDll.dll'
        ffi_convention :stdcall

        callback :enum_callback, [:long, :pointer], :bool

        # user32
        attach_function :_get_window_rect, :GetWindowRect,
                        [:long, :pointer], :bool
        attach_function :_move_window, :MoveWindow,
                        [:long, :int, :int, :int, :int, :bool], :bool
        attach_function :enum_windows, :EnumWindows,
                        [:enum_callback, :pointer], :long
        attach_function :enum_child_windows, :EnumChildWindows,
                        [:long, :enum_callback, :pointer], :long
        attach_function :_close_window, :CloseWindow,
                        [:long], :bool
        attach_function :minimized, :IsIconic,
                        [:long], :bool
        attach_function :_window_title, :GetWindowTextA,
                        [:long, :pointer, :int], :int
        attach_function :window_title_length, :GetWindowTextLengthA,
                        [:long], :int
        attach_function :window_exists, :IsWindow,
                        [:long], :bool
        attach_function :_window_class, :GetClassNameA,
                        [:long, :pointer, :int], :int
        attach_function :window_visible, :IsWindowVisible,
                        [:long], :bool
        attach_function :show_window, :ShowWindow,
                        [:long, :int], :bool
        attach_function :send_message, :SendMessageA,
                        [:long, :uint, :uint, :pointer], :long
        attach_function :send_message_timeout, :SendMessageTimeoutA,
                        [:long, :uint, :uint, :pointer, :uint, :uint, :pointer], :bool
        attach_function :post_message, :PostMessageA,
                        [:long, :uint, :uint, :pointer], :bool
        attach_function :window_thread_process_id, :GetWindowThreadProcessId,
                        [:long, :pointer], :long
        attach_function :attach_thread_input, :AttachThreadInput,
                        [:long, :long, :bool], :bool
        attach_function :set_foreground_window, :SetForegroundWindow,
                        [:long], :bool
        attach_function :bring_window_to_top, :BringWindowToTop,
                        [:long], :bool
        attach_function :set_active_window, :SetActiveWindow,
                        [:long], :long
        attach_function :foreground_window, :GetForegroundWindow,
                        [], :long
        attach_function :send_key, :keybd_event,
                        [:uchar, :uchar, :int, :pointer], :void
        attach_function :control_id, :GetDlgCtrlID,
                        [:long], :int
        attach_function :_set_control_focus, :SetFocus,
                        [:long], :long
        attach_function :get_window, :GetWindow,
                        [:long, :uint], :long
        attach_function :get_last_error, :GetLastError,
                        [], :long
        attach_function :send_input, :SendInput,
                        [:uint, :pointer, :int], :int
        attach_function :_get_cursor_pos, :GetCursorPos,
                        [:pointer], :bool
        attach_function :set_cursor_pos, :SetCursorPos,
                        [:int, :int], :int

        # kernel32
        attach_function :current_thread_id, :GetCurrentThreadId,
                        [], :long
        attach_function :open_process, :OpenProcess,
                        [:int, :bool, :int], :long
        attach_function :terminate_process, :TerminateProcess,
                        [:long, :uint], :bool
        attach_function :close_handle, :CloseHandle,
                        [:long], :bool
        attach_function :_load_library_x86, :LoadLibraryA,
                        [:string], :long
        attach_function :_load_library_x64, :LoadLibraryA,
                        [:string], :uint64

        # ole32
        attach_function :co_initialize, :CoInitialize,
                        [:pointer], :uint16

        # iaccessible
        attach_function :get_button_state, :get_button_state,
                        [:long], :long
        attach_function :get_table_row_strings, :get_table_row_strings,
                        [:long, :long, :pointer, :long, :pointer], :void
        attach_function :_select_table_row_x86, :select_table_row,
                        [:long, :long, :long], :void
        attach_function :_select_table_row_x64, :select_table_row,
                        [:uint64, :long, :long], :void
        attach_function :_get_table_row_state_x86, :get_table_row_state,
                        [:long, :long, :long], :long
        attach_function :_get_table_row_state_x64, :get_table_row_state,
                        [:uint64, :long, :long], :long

        class << self

          def window_title(hwnd)
            title_length = window_title_length(hwnd) + 1
            title = FFI::MemoryPointer.new :char, title_length
            _window_title(hwnd, title, title_length)
            title.read_string
          end

          alias_method :control_title, :window_title

          def move_window(hwnd, x, y, width, height)
            _move_window(hwnd, x, y, width, height, true)
          end

          def get_cursor_pos
            ptr = FFI::MemoryPointer.new(:long, 2)
            _get_cursor_pos(ptr)
            x, y = ptr.read_array_of_long(2)
            return {:x => x, :y => y}
          end

          def window_rect(hwnd)
            x = FFI::MemoryPointer.new(:long, 4)
            _get_window_rect(hwnd, x)
            x.read_array_of_long(4)
          end

          def window_text(hwnd)
            found_text = []
            window_callback = FFI::Function.new(:bool, [:long, :pointer], {:convention => :stdcall}) do |child_hwnd, _|
              found_text << text_for(child_hwnd)
              true
            end
            enum_child_windows(hwnd, window_callback, nil)
            found_text.join(" ")
          end

          alias_method :control_text, :window_text

          def window_hwnd(locators)
            find_hwnd(locators) do |hwnd|
              window_visible(hwnd) && locators_match?(locators, window_properties(hwnd, locators))
            end
          end

          def child_window_locators(parent_hwnd, locators)
            child_hwnd = locators[:hwnd] || child_hwnd(parent_hwnd, locators)
            if child_hwnd
              locators.merge!(:hwnd => child_hwnd)
            else
              popup_hwnd = get_window(parent_hwnd, Constants::GW_ENABLEDPOPUP)
              if popup_hwnd != parent_hwnd
                popup_properties = window_properties(popup_hwnd, locators)
                locators.merge!(:hwnd => popup_hwnd) if locators_match?(locators, popup_properties)
              end
            end
            locators
          end

          def window_pid(hwnd)
            pid = FFI::MemoryPointer.new :int
            window_thread_process_id(hwnd, pid)
            pid.read_int
          end

          def window_class(hwnd)
            class_name = FFI::MemoryPointer.new :char, 512
            _window_class(hwnd, class_name, 512)
            class_name.read_string
          end

          alias_method :control_class, :window_class

          def close_window(hwnd)
            _close_window(hwnd)
            closed = send_message_timeout(hwnd, Constants::WM_CLOSE,
                                          0, nil, Constants::SMTO_ABORTIFHUNG, 1000, nil)
            # force it to close
            unless closed
              process_hwnd = open_process(Constants::PROCESS_ALL_ACCESS, false, window_pid(hwnd))
              terminate_process(process_hwnd, 0)
              close_handle(process_hwnd)
            end
          end

          def activate_window(hwnd)
            set_foreground_window(hwnd)
            set_active_window(hwnd)
            bring_window_to_top(hwnd)
            within_foreground_thread(hwnd) do
              set_foreground_window(hwnd)
              set_active_window(hwnd)
              bring_window_to_top(hwnd)
            end
          end

          alias_method :activate_control, :activate_window

          def control_hwnd(window_hwnd, locators)
            find_hwnd(locators, window_hwnd) do |hwnd|
              locators_match?(locators, control_properties(hwnd, locators))
            end
          end

          alias_method :child_hwnd, :control_hwnd

          def control_value(control_hwnd)
            text_for(control_hwnd)
          end

          def control_click(control_hwnd)
            post_message(control_hwnd, Constants::BM_CLICK, 0, nil)
          end

          def set_control_focus(control_hwnd)
            within_foreground_thread control_hwnd do
              _set_control_focus(control_hwnd)
            end
          end

          def set_control_text(control_hwnd, text)
            send_message(control_hwnd, Constants::WM_SETTEXT, 0, text)
          end

          def control_set?(control_hwnd)
            get_button_state(control_hwnd) & Constants::STATE_SYSTEM_CHECKED != 0
          end

          def has_focus?(control_hwnd)
            get_button_state(control_hwnd) & Constants::STATE_SYSTEM_FOCUSED != 0
          end

          def unavailable?(control_hwnd)
            get_button_state(control_hwnd) & Constants::STATE_SYSTEM_UNAVAILABLE != 0
          end

          def retrieve_combobox_item_text(control_hwnd, item_no)
            text_len = send_message(control_hwnd, Constants::CB_GETLBTEXTLEN, item_no, nil)

            string_buffer = FFI::MemoryPointer.new :char, text_len
            send_message(control_hwnd, Constants::CB_GETLBTEXT, item_no, string_buffer)
            string_buffer.read_string
          end

          def load_library(dll)
            send("_load_library_#{Platform.architecture}", dll)
          end

          def get_table_row_state(oleacc_module_handle, control_hwnd, row)
            send("_get_table_row_state_#{Platform.architecture}", oleacc_module_handle, control_hwnd, row)
          end

          def select_table_row(oleacc_module_handle, control_hwnd, row)
            send("_select_table_row_#{Platform.architecture}", oleacc_module_handle, control_hwnd, row)
          end

          private

          def within_foreground_thread(hwnd)
            foreground_thread = current_thread_id
            other_thread = window_thread_process_id(hwnd, nil)
            attach_thread_input(foreground_thread, other_thread, true) unless other_thread == foreground_thread
            yield
          ensure
            attach_thread_input(foreground_thread, other_thread, false) unless other_thread == foreground_thread
          end

          def window_properties(hwnd, locators)
            element_properties(:window, hwnd, locators)
          end

          def control_properties(hwnd, locators)
            element_properties(:control, hwnd, locators)
          end

          def element_properties(type, hwnd, locators)
            locators.inject({}) do |properties, locator|
              if locator[0] == :hwnd
                properties[locator[0]] = hwnd
              elsif locator[0] != :index
                properties[locator[0]] = self.send("#{type}_#{locator[0]}", hwnd)
              end
              properties
            end
          end

          def locators_match?(locators, properties)
            locators.all? do |locator, value|
              locator == :index or
                value.is_a?(Regexp) ? properties[locator] =~ value : properties[locator] == value
            end
          end

          def find_hwnd(locators, window_hwnd = nil)
            found_hwnd = nil
            found_index = -1
            window_callback = FFI::Function.new(:bool, [:long, :pointer], {:convention => :stdcall}) do |hwnd, _|
              if yield(hwnd)
                found_index += 1

                if locators[:index]
                  found_hwnd = hwnd if locators[:index] == found_index
                else
                  found_hwnd = hwnd
                end
              end
              !found_hwnd
            end

            unless window_hwnd
              enum_windows(window_callback, nil)
            else
              enum_child_windows(window_hwnd, window_callback, nil)
            end

            found_hwnd
          end

          def text_for(hwnd)
            text_length = send_message(hwnd, Constants::WM_GETTEXTLENGTH, 0, nil) + 1
            text = FFI::MemoryPointer.new :char, text_length
            send_message(hwnd, Constants::WM_GETTEXT, text_length, text)
            text.read_string
          end

        end
      end
    end
  end
end

