module Calabash
  module IOS
    # @!visibility private
    module Routes

      # @!visibility private
      module UIARouteMixin

        require 'run_loop'
        require 'edn'

        # @!visibility private
        def uia_route(command)
          unless run_loop
            if defined?(IRB)
              logger.log('', :info)
              logger.log(Color.red('This console is not attached to a run-loop.'), :info)
              logger.log('', :info)
              logger.log(Color.green('If your have not launched your app yet, launch it with:'), :info)
              logger.log('', :info)
              logger.log(Color.cyan('> start_app'), :info)
              logger.log('', :info)
              logger.log('', :info)
              logger.log(Color.green('If your Calabash app is already running, ' \
                                     "you can attach to it's run-loop:"), :info)
              logger.log('', :info)
              logger.log(Color.cyan('> console_attach                # Attaches with the default strategy.'),
                         :info)
              logger.log(Color.cyan('> console_attach(uia_strategy)  # Attaches with a specific strategy.'),
                         :info)
              logger.log('', :info)
              logger.log('', :info)
              raise 'This console is not attached a run-loop.'
            else
              raise 'This device does not have a connection to run-loop.  Call start_app first.'
            end
          end

          strategy = uia_strategy

          case strategy
            when :preferences
              uia_over_http(command, 'uia')
            when :shared_element
              uia_over_http(command, 'uia-shared')
            when :host
              uia_over_host(command)
            else
              raise Calabash::IOS::RouteError, "Invalid :uia_strategy '#{strategy}'.  Valid strategies are: '#{UIA_STRATEGIES}"
          end
        end

        # @!visibility private
        def uia_serialize_and_call(uia_command, *query_args)
          command = uia_serialize_command(uia_command, *query_args)
          result = uia_route(command)
          result.first
        end

        # @!visibility private
        # @todo Extract argument consing and unit test
        def uia_query_then_make_javascript_calls(uia_command, query_parts, *javascript_parts)
          if javascript_parts.empty?
            uia_serialize_and_call(uia_command, *query_parts)
          else
            javascript_command = uia_serialize_command(uia_command, *query_parts)

            javascript_args = []
            javascript_parts.each do |invocation|
              javascript_args << case invocation
                when Symbol
                  "#{invocation}()"
                when Hash
                  method = invocation.keys.first
                  method_args = invocation[method]

                  if method_args.is_a?(Array)
                    serialized_args = (method_args.map &:to_json).join(',')
                  else
                    serialized_args = method_args.to_json
                  end

                  "#{method}(#{serialized_args})"
                else
                  raise Calabash::IOS::RouteError,
                        "Invalid invocation spec #{invocation}"
              end
            end
            command = "#{javascript_command}.#{javascript_args.join('.')}"

            result = uia_route(command)
            result.first
          end
        end

        private

        UIA_STRATEGIES = [:preferences, :host, :shared_element]

        # Careful.  The UIA route can return all manner of weird responses.
        def uia_over_http(command, route)
          request = make_uia_request(command, route)
          response = route_post_request(request)
          parsed = parse_response_body(response)
          handle_uia_results(parsed['results'], command)
        end

        # Careful.  The UIA route can return all manner of weird responses.
        def uia_over_host(command)
          [RunLoop.send_command(run_loop, command)]
        end

        # Called _after_ route_handle_response or _after_ RunLoop.send_command
        def handle_uia_results(response, command)
          expect_uia_results_is_array(response)

          expect_uia_results_has_one_element(response)

          hash = response.first

          expect_uia_result_has_valid_status_key(hash, response, command)

          expect_uia_result_has_value_key(hash, response, command)

          status = hash['status']
          value = hash['value']

          if status == 'error'
            raise "Executing command '#{command}'\n" \
                    "resulted in an error: '#{value}'"
          else
            handle_uia_result_with_success(value)
          end
        end

        def expect_uia_results_is_array(response)
          if !response.is_a? Array
            raise "Expected '#{response}' to be an array."
          end
          response
        end

        def expect_uia_results_has_one_element(response)
          if response.length != 1
            raise "Expected '#{response}' to have exactly one element"
          end
          response
        end

        def expect_uia_response_element_is_hash(hash)
          if !hash.is_a? Hash
            raise "Expected first result of '#{hash}' to be a Hash"
          end
          hash
        end

        def expect_uia_result_has_valid_status_key(hash, response, command)
          status = hash['status']

          case status
            when 'error', 'success'
              hash
            else
              raise "Executing command '#{command}'\n" \
                    "returned an invalid status key: '#{status}'.\n" \
                    "Expected 'error' or 'success'.\n" \
                    "The raw response was '#{response}'"
          end
          hash
        end

        def expect_uia_result_has_value_key(hash, response, command)
          if !hash.has_key? 'value'
            raise "Executing command '#{command}'\n" \
                  "returned a value with no 'value' key.\n" \
                  "Then raw response was '#{response}'"
          end
          hash
        end

        def handle_uia_result_with_success(value)
          if value.is_a? Array
            value
          elsif value == ':nil'
            [nil]
          else
            [value]
          end
        end

        def make_uia_parameters(command)
          {
                :command => command
          }
        end

        def make_uia_request(command, route)
          parameters = make_uia_parameters(command)
          begin
            Calabash::HTTP::Request.request(route, parameters)
          rescue => e
            raise Calabash::IOS::RouteError, e
          end
        end

        # @todo Verify this is the correct way to escape '\n in string
        def uia_escape_string(string)
          Calabash::Text.escape_single_quotes(string).gsub("\n", "\\\\n")
        end

        def uia_serialize_argument(part)
          if part.is_a?(String)
            "'#{uia_escape_string(part)}'"
          else
            "'#{uia_escape_string(part.to_edn)}'"
          end
        end

        def uia_serialize_arguments(argument_array)
          argument_array.map do |part|
            uia_serialize_argument(part)
          end
        end

        def uia_serialize_command(uia_command, *query_args)
          args = uia_serialize_arguments(query_args)
          %Q[uia.#{uia_command}(#{args.join(', ')})]
        end

        # @todo Move to a translate mixin (or helper)
        # @todo No unit tests for this yet!
        def uia_center_of_view(view_hash)
          rect = view_hash['rect']

          # Why would the view hash not have a rect?
          unless rect
            raise "Expected '#{view_hash}' to have a 'rect' key"
          end
          {:x => rect['center_x'], :y => rect['center_y']}
        end

        # @todo Move to a translate mixin (or helper)
        # def point_from(query_result, options={})
        #   offset_x = 0
        #   offset_y = 0
        #   if options[:offset]
        #     offset_x += options[:offset][:x] || 0
        #     offset_y += options[:offset][:y] || 0
        #   end
        #   x = offset_x
        #   y = offset_y
        #   rect = query_result['rect'] || query_result[:rect]
        #   if rect
        #     x += rect['center_x'] || rect[:center_x] || rect[:x] || 0
        #     y += rect['center_y'] || rect[:center_y] || rect[:y] || 0
        #   else
        #     x += query_result['center_x'] || query_result[:center_x] || query_result[:x] || 0
        #     y += query_result['center_y'] || query_result[:center_y] || query_result[:y] || 0
        #   end
        #
        #   {:x => x, :y => y}
        # end

        # @todo Is this worth keeping?
        # def uia_result(s)
        #   query_result = s.first
        #   if query_result['status'] == 'success'
        #     query_result['value']
        #   else
        #     query_result['status']
        #   end
        # end
      end
    end
  end
end
