module Calabash
  module Cucumber
    # @!visibility private
    module Automator

      require "calabash-cucumber/automator/automator"

      # @!visibility private
      class Instruments < Calabash::Cucumber::Automator::Automator

        require "calabash-cucumber/uia"
        include Calabash::Cucumber::UIA

        require "calabash-cucumber/connection_helpers"
        include Calabash::Cucumber::ConnectionHelpers

        require "calabash-cucumber/query_helpers"
        include Calabash::Cucumber::QueryHelpers

        require "calabash-cucumber/status_bar_helpers"
        include Calabash::Cucumber::StatusBarHelpers

        require "calabash-cucumber/rotation_helpers"
        include Calabash::Cucumber::RotationHelpers

        require "calabash-cucumber/map"

        # @!visibility private
        UIA_STRATEGIES = [:preferences, :host, :shared_element]

        attr_reader :run_loop

        # @!visibility private
        def self.expect_valid_init_args(args)
          if args.nil?
            raise(ArgumentError, "Expected non-nil argument for initializer")
          end

          if !args.is_a?(Array)
            raise(ArgumentError, "Expected an array argument for initializer")
          end

          run_loop = args[0]

          if run_loop.nil?
            raise(ArgumentError,
%Q[Expected first element of args to be non-nil:

args = #{args}

])
          end

          if args.count != 1
            raise(ArgumentError,
%Q[Expected args to have exactly one element but found:

args = #{args}
])
          end

          self.expect_valid_run_loop(run_loop)
        end

        # @!visibility private
        def self.expect_valid_run_loop(run_loop)
          if run_loop.nil?
            raise(ArgumentError, "Expected run_loop arg to be non-nil")
          end

          if !run_loop.is_a?(Hash)
            raise(ArgumentError, %Q[
Expected run_loop arg to be a hash, but found:

run_loop = #{run_loop} is_a => #{run_loop.class}

])
          end

          automator = run_loop[:automator]
          if automator && automator != :instruments
            raise(ArgumentError, %Q[
Invalid :@automator. Expected :instruments but found:

#{automator}

in

#{run_loop}

])
          end

          [:pid, :udid, :index, :log_file, :uia_strategy].each do |key|
            if !run_loop[key]
              raise(ArgumentError, %Q[
Expected run_loop to have a truthy value for :#{key} but found:

#{run_loop}

])
            end
          end

          strategy = run_loop[:uia_strategy]
          if !self.valid_uia_strategy?(strategy)
             raise(ArgumentError, %Q[
Expected '#{strategy}' to be one of these supported strategies:

#{UIA_STRATEGIES}

])
          end
          true
        end

        # @!visibility private
        def self.valid_uia_strategy?(strategy)
         UIA_STRATEGIES.include?(strategy)
        end

        # @!visibility private
        def initialize(*args)
          Instruments.expect_valid_init_args(args)
          @run_loop = args[0]
        end

        # @!visibility private
        def name
          :instruments
        end

        # @!visibility private
        def stop
          RunLoop.stop(run_loop)
        end

        # @!visibility private
        def touch(options)
          query_action(options, :uia_tap_offset)
        end

        # @!visibility private
        def double_tap(options)
          query_action(options, :uia_double_tap_offset)
        end

        # @!visibility private
        def two_finger_tap(options)
          query_action(options, :uia_two_finger_tap_offset)
        end

        # @!visibility private
        def flick(options)
          query_action(options) do |offset|
            delta = {:offset => options[:delta] || {}}
            uia_flick_offset(offset, point_from(offset, delta))
          end
        end

        # @!visibility private
        def touch_hold(options)
          query_action(options) do |offset|
            duration = options[:duration] || 3
            uia_touch_hold_offset(duration, offset)
          end
        end

        # @!visibility private
        def swipe(options)
          query_action(options, :uia_swipe_offset, options)
        end

        # @!visibility private
        def pan(from, to, options={})
          query_action(:query => from) do |from_offset|
            query_action(:query => to) do |to_offset|
              uia_pan_offset(from_offset, to_offset, options)
            end
          end
        end

        # @!visibility private
        def pan_coordinates(from, to, options={})
          uia_pan_offset(from, to, options)
          [find_and_normalize("*")]
        end

        # @!visibility private
        def pinch(in_out, options)
          query_action(options) do |offset|
            options[:duration] = options[:duration] || 0.5
            uia_pinch_offset(in_out, offset, options)
          end
        end

        # @!visibility private
        def send_app_to_background(secs)
          uia_send_app_to_background(secs)
        end

        # @!visibility private
        def enter_text_with_keyboard(string, existing_text="")
          uia_type_string(string, existing_text)
        end

        # @!visibility private
        def fast_enter_text(text)
          uia_set_responder_value(text)
        end

        # @!visibility private
        # It is the caller's responsibility to ensure the keyboard is visible.
        def enter_char_with_keyboard(char)
          uia("uia.keyboard().typeString('#{char}')")
        end

        # @!visibility private
        def char_for_keyboard_action(action_key)
          SPECIAL_ACTION_CHARS[action_key]
        end

        # @!visibility private
        # TODO Implement this in JavaScript?
        # See the device_agent implementation of tap_keyboard_action_key and
        # the tap_keyboard_delete_key of this class.
        def tap_keyboard_action_key
          code = char_for_keyboard_action("Return")
          enter_char_with_keyboard(code)
        end

        # @!visibility private
        def tap_keyboard_delete_key
          js_tap_delete = %Q[(function() {
var deleteElement = uia.keyboard().elements().firstWithName('Delete');
if (deleteElement.isValid()) {
  deleteElement.tap();
} else {
  uia.keyboard().elements().firstWithName('delete').tap();
}
})();].gsub!($-0, "")

          uia(js_tap_delete)
        end

        # @!visibility private
        def dismiss_ipad_keyboard
          js = %Q[#{query_uia_hide_keyboard_button}.tap()]
          uia(js)
        end

        # @!visibility private
        def rotate(direction)
          current_orientation = status_bar_orientation.to_sym
          result = rotate_with_uia(direction, current_orientation)
          recalibrate_after_rotation
          ap result if RunLoop::Environment.debug?
          status_bar_orientation.to_sym
        end

        # @!visibility private
        def rotate_home_button_to(position)
          rotate_to_uia_orientation(position)
          recalibrate_after_rotation
          status_bar_orientation.to_sym
        end

        private

        # @!visibility private
        #
        # Calls #point_from which applies any :offset supplied in the options.
        def query_for_coordinates(options)
          ui_query = options[:query]

          first_element, orientation = first_element_for_query(ui_query)

          if first_element.nil?
            msg = %Q[
Could not find any views with query:

#{ui_query}

Try adjusting your query to return at least one view.

]
            Calabash::Cucumber::Map.new.screenshot_and_raise(msg)
          else

            normalize_rect_for_orientation!(orientation, first_element)

            {
              :coordinates => point_from(first_element, options),
              :view => first_element
            }
          end
        end

        # @!visibility private
        def first_element_for_query(ui_query)
          # Will raise if response "outcome" is not SUCCESS
          raw = Calabash::Cucumber::Map.raw_map(ui_query, :query)
          results = raw["results"]
          orientation = raw["status_bar_orientation"]

          if results.empty?
            return nil, nil
          else
            return results[0], orientation
          end
        end

        # @!visibility private
        # Data interface
        # options[:query] or options[:offset]
        def query_action(options, action=nil, *args, &block)
          ui_query = options[:query]
          offset = options[:offset]
          if ui_query
            res = find_and_normalize(ui_query)
            return res if res.empty?
            el = res.first
            final_offset = point_from(el, options)
            if block_given?
              yield final_offset
            else
              self.send(action, final_offset, *args)
            end
            [el]
          else
            ##implies offset
            if block_given?
              yield offset
            else
              self.send(action, offset, *args)
            end
          end
        end

        # @!visibility private
        def find_and_normalize(ui_query)
          raw_result = Calabash::Cucumber::Map.raw_map(ui_query, :query)
          orientation = raw_result["status_bar_orientation"]
          res = raw_result["results"]

          return res if res.empty?

          first_res = res.first
          normalize_rect_for_orientation!(orientation, first_res["rect"]) if first_res["rect"]

          res
        end

        # @!visibility private
        def normalize_rect_for_orientation!(orientation, rect)
          orientation = orientation.to_sym
          launcher = Calabash::Cucumber::Launcher.launcher
          device = launcher.device

          # Coordinate translations for orientation is handled in the server for iOS 8+
          if device.ios_major_version.to_i >= 8
            return
          end

          # We cannot use Device#screen_dimensions here because on iPads the height
          # and width are the opposite of what we expect.
          # @todo Move all coordinate/orientation translation into the server.
          if device.ipad?
            screen_size = { :width => 768, :height => 1024 }
          elsif device.iphone_4in?
            screen_size = { :width => 320, :height => 568 }
          else
            screen_size = { :width => 320, :height => 480 }
          end

          case orientation
            when :right
              cx = rect["center_x"]
              rect["center_x"] = rect["center_y"]
              rect["center_y"] = screen_size[:width] - cx
            when :left
              cx = rect["center_x"]
              rect["center_x"] = screen_size[:height] - rect["center_y"]
              rect["center_y"] = cx
            when :up
              cy = rect["center_y"]
              cx = rect["center_x"]
              rect["center_y"] = screen_size[:height] - cy
              rect["center_x"] = screen_size[:width] - cx
            else
              # no-op by design.
          end
        end

        # @!visibility private
        def recalibrate_after_rotation
          uia_query :window
        end

        # @!visibility private
        def rotate_to_uia_orientation(orientation)
          case orientation
            when :down then key = :portrait
            when :up then key = :upside_down
            when :left then key = :landscape_right
            when :right then key = :landscape_left
            else
              raise ArgumentError,
                    "Expected '#{orientation}' to be :left, :right, :up, or :down"
          end
          value = orientation_for_key(key)
          cmd = "UIATarget.localTarget().setDeviceOrientation(#{value})"
          uia(cmd)
        end

        # @!visibility private
        def rotate_with_uia(direction, current_orientation)
          key = orientation_key(direction, current_orientation)
          value = orientation_for_key(key)
          cmd = "UIATarget.localTarget().setDeviceOrientation(#{value})"
          uia(cmd)
        end

        # @!visibility private
        # Returns a query string for finding the iPad 'Hide keyboard' button.
        def query_uia_hide_keyboard_button
          "uia.keyboard().buttons()['Hide keyboard']"
        end

        # @!visibility private
        #
        # Don't change the single single quotes.
        SPECIAL_ACTION_CHARS = {
          "Delete" => '\b',
          "Return" => '\n'
        }
      end
    end
  end
end

