#todo - move this file up to the same level as the others
#todo - organize the methods better

module RAutomation
  module Adapter
    module MsUia
      module UiaDll
        extend FFI::Library

        HowToFind = enum(:hwnd, 1,
                          :id,
                          :value,
                          :focus,
                          :point)


        class FindData < FFI::Union
          layout :string_data, [:uint8, 256],
                 :int_data, :int,
                 :point_data, [:int, 2]
        end

        class SearchCriteria < FFI::Struct
          def self.from_locator(parent, locator)
            info = SearchCriteria.new
            info.parent_window = parent
            info.index = locator[:index] || 0
            info.children_only = locator[:children_only]

            case
              when locator[:hwnd]
                info.how = :hwnd
                info.data = locator[:hwnd]
              when locator[:id]
                info.how = :id
                info.data = locator[:id]
              when locator[:value]
                info.how = :value
                info.data = locator[:value]
              when locator[:point]
                info.how = :point
                info.data = locator[:point]
              when locator[:focus]
                info.how = :focus
            end
            info
          end

          def how
            self[:how]
          end

          def how=(value)
            self[:how] = value
          end

          def index
            self[:index]
          end

          def index=(value)
            self[:index] = value
          end

          def children_only?
            self[:children_only]
          end

          def children_only=(yes_or_no)
            self[:children_only] = yes_or_no
          end

          def parent_window
            self[:hwnd]
          end

          def parent_window=(parent)
            self[:hwnd] = parent
          end

          def data
            case how
              when :hwnd
                return self[:data][:int_data]
              when :id, :value
                return self[:data][:string_data].to_ptr.read_string
              when :point
                return self[:data][:point_data].to_ptr.read_array_of_int(2)
              else
                return nil
            end
          end

          def data=(value)
            case how
              when :hwnd
                self[:data][:int_data] = value || 0 # cannot accept nil
              when :id, :value
                self[:data][:string_data] = value
              when :point
                self[:data][:point_data].to_ptr.write_array_of_int(value)
            end
          end

          layout :hwnd, :int,
                 :index, :int,
                 :children_only, :bool,
                 :how, HowToFind, :data, FindData
        end

        def self.uia_dll_directory
          File.dirname(__FILE__) + '/../../../../ext/UiaDll/Release'
        end


        ffi_lib File.join(uia_dll_directory, 'UiaDll.dll')
        ffi_convention :stdcall

        attach_function :init, :initialize, [:string], :void
        init(uia_dll_directory)

        # Generic Control methods
        attach_function :cached_hwnd, :NativeWindowHandle, [SearchCriteria.by_ref], :long
        attach_function :ElementExists, [SearchCriteria.by_ref], :bool
        attach_function :process_id, :ProcessId, [SearchCriteria.by_ref], :int
        attach_function :Control_GetValue, [SearchCriteria.by_ref, :pointer, :int], :void
        attach_function :set_control_value, :Control_SetValue, [SearchCriteria.by_ref, :string], :void
        attach_function :Text_GetValue, [SearchCriteria.by_ref, :pointer, :int], :void
        attach_function :set_text, :Text_SetValue, [SearchCriteria.by_ref, :string], :void
        attach_function :BoundingRectangle, [SearchCriteria.by_ref, :pointer], :int
        attach_function :current_control_type, :ControlType, [SearchCriteria.by_ref], :int
        attach_function :Name, [SearchCriteria.by_ref, :pointer, :int], :void
        attach_function :ClassName, [SearchCriteria.by_ref, :pointer, :int], :void
        attach_function :is_enabled, :IsEnabled, [SearchCriteria.by_ref], :bool
        attach_function :is_focused, :IsFocused, [SearchCriteria.by_ref], :bool
        attach_function :set_focus, :SetControlFocus, [SearchCriteria.by_ref], :bool
        attach_function :GetClassNames, [SearchCriteria.by_ref, :pointer], :int
        attach_function :HelpText, [SearchCriteria.by_ref, :pointer, :int], :void
        attach_function :SendKeys, [SearchCriteria.by_ref, :string, :pointer, :int], :void

        def self.exists?(search_information)
          ElementExists search_information
        end

        def self.bounding_rectangle(search_information)
          boundary = FFI::MemoryPointer.new :long, 4
          BoundingRectangle search_information, boundary
          boundary.read_array_of_long(4)
        end

        def self.get_control_value(search_information)
          string_from(:Control_GetValue, search_information)
        end

        def self.get_text(search_information)
          string_from(:Text_GetValue, search_information)
        end

        def self.help_text(search_information)
          string_from(:HelpText, search_information)
        end

        def self.name(search_information)
          string_from(:Name, search_information)
        end

        def self.class_name(search_information)
          string_from(:ClassName, search_information)
        end

        def self.children_class_names(search_information)
          strings_from :GetClassNames, search_information
        end

        def self.send_keys(search_information, keys_to_send)
          can_throw(:SendKeys, search_information, keys_to_send)
        end

        # Toggle methods
        attach_function :is_set, :IsSet, [SearchCriteria.by_ref], :bool

        # Selection Item methods
        attach_function :is_selected, :IsSelected, [SearchCriteria.by_ref], :bool

        # Select List methods
        attach_function :SelectList_Selection,
                        [SearchCriteria.by_ref, :pointer, :int], :void
        attach_function :SelectList_Selections,
                        [SearchCriteria.by_ref, :pointer], :int
        attach_function :SelectList_Options,
                        [SearchCriteria.by_ref, :pointer], :int
        attach_function :select_list_count, :SelectList_Count,
                        [SearchCriteria.by_ref], :int
        attach_function :select_list_selected_index, :SelectList_SelectedIndex,
                        [SearchCriteria.by_ref], :int
        attach_function :select_list_value_at, :SelectList_ValueAt,
                        [SearchCriteria.by_ref, :int, :pointer, :int], :bool
        attach_function :select_list_select_index, :SelectList_SelectIndex,
                        [SearchCriteria.by_ref, :int], :bool
        attach_function :select_list_select_value, :SelectList_SelectValue,
                        [SearchCriteria.by_ref, :pointer], :int
        attach_function :SelectList_RemoveIndex,
                        [SearchCriteria.by_ref, :int, :pointer, :int], :void
        attach_function :SelectList_RemoveValue,
                        [SearchCriteria.by_ref, :string, :pointer, :int], :void

        def self.select_options(search_information)
          strings_from(:SelectList_Options, search_information)
        end

        def self.selection(search_information)
          string_from(:SelectList_Selection, search_information)
        end

        def self.selections(search_information)
          strings_from(:SelectList_Selections, search_information)
        end

        def self.remove_from_selection(search_information, which_item)
          case which_item
            when Integer
              can_throw(:SelectList_RemoveIndex, search_information, which_item)
            when String
              can_throw(:SelectList_RemoveValue, search_information, which_item)
          end
        end

        # Spinner methods
        attach_function :Spinner_GetValue,
                        [SearchCriteria.by_ref, :pointer, :int], :double
        attach_function :Spinner_SetValue,
                        [SearchCriteria.by_ref, :double, :pointer, :int], :void
        attach_function :Spinner_Minimum,
                        [SearchCriteria.by_ref, :pointer, :int], :double
        attach_function :Spinner_Maximum,
                        [SearchCriteria.by_ref, :pointer, :int], :double
        attach_function :Spinner_Increment,
                        [SearchCriteria.by_ref, :pointer, :int], :double
        attach_function :Spinner_Decrement,
                        [SearchCriteria.by_ref, :pointer, :int], :double

        def self.spinner_value(search_information)
          can_throw(:Spinner_GetValue, search_information)
        end

        def self.set_spinner_value(search_information, value)
          can_throw(:Spinner_SetValue, search_information, value)
        end

        def self.spinner_min(search_information)
          can_throw(:Spinner_Minimum, search_information)
        end

        def self.spinner_max(search_information)
          can_throw(:Spinner_Maximum, search_information)
        end

        def self.increment_spinner(search_information)
          can_throw(:Spinner_Increment, search_information)
        end

        def self.decrement_spinner(search_information)
          can_throw(:Spinner_Decrement, search_information)
        end

        # Tab Control methods
        attach_function :TabControl_Items,
                        [SearchCriteria.by_ref, :pointer], :int
        attach_function :TabControl_Selection,
                        [SearchCriteria.by_ref, :pointer, :int], :void
        attach_function :TabControl_SelectByIndex,
                        [SearchCriteria.by_ref, :int, :pointer, :int], :void
        attach_function :TabControl_SelectByValue,
                        [SearchCriteria.by_ref, :string, :pointer, :int], :void
        attach_function :tab_control_selected_index, :TabControl_SelectedIndex, [SearchCriteria.by_ref], :int

        def self.tab_items(search_information)
          strings_from(:TabControl_Items, search_information)
        end

        def self.tab_selection(search_information)
          string_from(:TabControl_Selection, search_information)
        end

        def self.select_tab(search_information, which)
          case which
            when Integer
              can_throw(:TabControl_SelectByIndex, search_information, which)
            when String
              can_throw(:TabControl_SelectByValue, search_information, which)
          end
        end

        # Menu methods
        attach_function :select_menu_item, :Menu_SelectPath,
                        [:long, :pointer, :int, :varargs], :void
        attach_function :menu_item_exists, :Menu_ItemExists,
                        [:long, :varargs], :bool

        # Table methods
        attach_function :Table_GetHeaders,
                        [SearchCriteria.by_ref, :pointer], :int
        attach_function :Table_GetValues,
                        [SearchCriteria.by_ref, :pointer], :int
        attach_function :Table_GetSelectedIndexes,
                        [SearchCriteria.by_ref, :pointer], :int
        attach_function :table_row_count, :Table_RowCount,
                        [SearchCriteria.by_ref], :int
        attach_function :Table_CoordinateIsValid,
                        [SearchCriteria.by_ref, :int, :int], :bool
        attach_function :Table_ValueAt,
                        [SearchCriteria.by_ref, :int, :int, :pointer, :int], :void
        attach_function :Table_SelectByIndex,
                        [SearchCriteria.by_ref, :int, :pointer, :int], :void
        attach_function :Table_SingleSelectByIndex,
                        [SearchCriteria.by_ref, :int, :pointer, :int], :void
        attach_function :Table_SelectByValue,
                        [SearchCriteria.by_ref, :string, :pointer, :int], :void
        attach_function :table_row_is_selected, :Table_IsSelectedByIndex,
                        [SearchCriteria.by_ref, :int], :bool
        attach_function :Table_RemoveRowByIndex,
                        [SearchCriteria.by_ref, :int, :pointer, :int], :void
        attach_function :Table_RemoveRowByValue,
                        [SearchCriteria.by_ref, :string, :pointer, :int], :void

        def self.table_selected_indexes(search_information)
          integers_from(:Table_GetSelectedIndexes, search_information)
        end

        def self.table_select(search_information, which_item)
          case which_item
            when Integer
              can_throw(:Table_SelectByIndex, search_information, which_item)
            when String
              can_throw(:Table_SelectByValue, search_information, which_item)
          end
        end

        def self.table_single_select(search_information, which_item)
          can_throw(:Table_SingleSelectByIndex, search_information, which_item)
        end

        def self.table_value_at(search_information, row, column=0)
          string_from(:Table_ValueAt, search_information, row, column)
        end

        def self.table_coordinate_valid?(search_information, row, column=0)
          Table_CoordinateIsValid search_information, row, column
        end

        def self.table_headers(search_information)
          strings_from :Table_GetHeaders, search_information
        end

        def self.table_values(search_information)
          strings_from :Table_GetValues, search_information
        end

        def self.table_remove_from_selection(search_information, which_row)
          case which_row
            when Integer
              can_throw(:Table_RemoveRowByIndex, search_information, which_row)
            when String
              can_throw(:Table_RemoveRowByValue, search_information, which_row)
          end
        end

        # String methods
        attach_function :clean_up_strings, :String_CleanUp,
                        [:pointer, :int], :void

        attach_function :handle_from_point, :HandleFromPoint,
                        [:int, :int], :long
        attach_function :move_mouse, :MoveMouse,
                        [:int,:int], :long
        attach_function :click_mouse, :ClickMouse,
                        [], :long
        attach_function :is_offscreen, :IsOffscreen,
                        [SearchCriteria.by_ref], :bool
        attach_function :expand_by_value, :ExpandItemByValue,
                        [SearchCriteria.by_ref, :string], :void
        attach_function :expand_by_index, :ExpandItemByIndex,
                        [SearchCriteria.by_ref, :int], :void
        attach_function :collapse_by_value, :CollapseItemByValue,
                        [SearchCriteria.by_ref, :string], :void
        attach_function :collapse_by_index, :CollapseItemByIndex,
                        [SearchCriteria.by_ref, :int], :void
        attach_function :Click,
                        [SearchCriteria.by_ref, :pointer, :int], :bool

        def self.control_click(search_information)
          can_throw(:Click, search_information)
        end

        private
        def self.integers_from(method, search_information)
          item_count = send method, search_information, nil
          pointer = FFI::MemoryPointer.new :pointer, item_count
          send method, search_information, pointer
          pointer.read_array_of_int(item_count)
        end

        def self.strings_from(method, hwnd)
          string_count = send method, hwnd, nil
          pointer = FFI::MemoryPointer.new :pointer, string_count
          send method, hwnd, pointer
          strings = pointer.get_array_of_string 0, string_count
          clean_up_strings pointer, string_count
          strings
        end

        def self.string_from(method, *args)
          pointer = FFI::MemoryPointer.new :pointer, 1024
          send method, *(args << pointer << 1024)
          pointer.read_string
        end

        def self.can_throw(method, *args)
          string_buffer = FFI::MemoryPointer.new :char, 1024
          result = send method, *(args << string_buffer << 1024)
          error_info = string_buffer.read_string
          raise error_info unless error_info.empty?
          result
        end
      end
    end
  end
end
