require 'rjb'
require 'rjbextension'
$LOAD_PATH << ENV['SIKULI_HOME']
require 'sikuli-script.jar'
Rjb::load

package_members = [
                   "java.io.FileOutputStream",
                   "java.io.PrintStream",
                   "java.lang.System",
                   "org.sikuli.script.Finder",
                   "org.sikuli.script.Key",
                   "org.sikuli.script.KeyModifier",
                   "org.sikuli.script.Location",
                   "org.sikuli.script.Match",
                   "org.sikuli.script.Pattern",
                   "org.sikuli.script.Region",
                   "org.sikuli.script.Screen",
                   "org.sikuli.script.Settings",
                  ]

translations = Hash[
                    "org.sikuli.script", "Sikuli",
                    "java.lang", "Java::Lang",
                    "java.io", "Java::Io",
                   ]

for p in package_members
  imported_class = Rjb::import(p)
  package, ignore, class_name = p.rpartition(".")
  next if ! translations.include? package
  mod_name = translations[package]
  mod = mod_name.split("::").inject(Object) do |parent_obj, child_name|
    if parent_obj.const_defined?(child_name, false)
      parent_obj.const_get(child_name, false)
    else
      child_obj = Module.new
      parent_obj.const_set(child_name, child_obj)
    end
  end
  mod.const_set(class_name, imported_class)
end

# Bind Java's stdout to debug_log() via our magical pseudo fifo
# logger.
def bind_java_to_pseudo_fifo_logger
  file_output_stream = Java::Io::FileOutputStream.new(DEBUG_LOG_PSEUDO_FIFO)
  print_stream = Java::Io::PrintStream.new(file_output_stream)
  Java::Lang::System.setOut(print_stream)
end

def findfailed_hook(pic)
  pause("FindFailed for: '#{pic}'")
end

# Since rjb imports Java classes without creating a corresponding
# Ruby class (it's just an instance of Rjb_JavaProxy) we can't
# monkey patch any class, so additional methods must be added
# to each Screen object.
#
# All Java classes' methods are immediately available in the proxied
# Ruby classes, but care has to be given to match their type. For a
# list of methods, see: <http://doc.sikuli.org/javadoc/index.html>.
# The type "PRSML" is a union of Pattern, Region, Screen, Match and
# Location.
#
# Also, due to limitations in Ruby's syntax we can't do:
#     def Sikuli::Screen.new
# so we work around it with the following vairable.
sikuli_script_proxy = Sikuli::Screen
$_original_sikuli_screen_new ||= Sikuli::Screen.method :new

# For waitAny()/findAny() we are forced to throw this exception since
# Rjb::throw doesn't block until the Java exception has been received
# by Ruby, so strange things can happen.
class FindAnyFailed < StandardError
end

def sikuli_script_proxy.new(*args)
  s = $_original_sikuli_screen_new.call(*args)

  if $config["SIKULI_RETRY_FINDFAILED"]
    # The usage of `_invoke()` below exemplifies how one can wrap
    # around Java objects' methods when they're imported using RJB. It
    # isn't pretty. The seconds argument is the parameter signature,
    # which can be obtained by creating the intended Java object using
    # RJB, and then calling its `java_methods` method.

    def s.wait(pic, time)
      self._invoke('wait', 'Ljava.lang.Object;D', pic, time)
    rescue FindFailed => e
      findfailed_hook(pic)
      self._invoke('wait', 'Ljava.lang.Object;D', pic, time)
    end

    def s.find(pic)
      self._invoke('find', 'Ljava.lang.Object;', pic)
    rescue FindFailed => e
      findfailed_hook(pic)
      self._invoke('find', 'Ljava.lang.Object;', pic)
    end

    def s.waitVanish(pic, time)
      self._invoke('waitVanish', 'Ljava.lang.Object;D', pic, time)
    rescue FindFailed => e
      findfailed_hook(pic)
      self._invoke('waitVanish', 'Ljava.lang.Object;D', pic, time)
    end

    def s.click(pic)
      self._invoke('click', 'Ljava.lang.Object;', pic)
    rescue FindFailed => e
      findfailed_hook(pic)
      self._invoke('click', 'Ljava.lang.Object;', pic)
    end
  end

  def s.click_point(x, y)
    self.click(Sikuli::Location.new(x, y))
  end

  def s.doubleClick_point(x, y)
    self.doubleClick(Sikuli::Location.new(x, y))
  end

  def s.click_mid_right_edge(pic)
    r = self.find(pic)
    top_right = r.getTopRight()
    x = top_right.getX
    y = top_right.getY + r.getH/2
    self.click_point(x, y)
  end

  def s.wait_and_click(pic, time)
    self.click(self.wait(pic, time))
  end

  def s.wait_and_double_click(pic, time)
    self.doubleClick(self.wait(pic, time))
  end

  def s.wait_and_right_click(pic, time)
    self.rightClick(self.wait(pic, time))
  end

  def s.wait_and_hover(pic, time)
    self.hover(self.wait(pic, time))
  end

  def s.existsAny(images)
    images.each do |image|
      region = self.exists(image)
      return [image, region] if region
    end
    return nil
  end

  def s.findAny(images)
    images.each do |image|
      begin
        return [image, self.find(image)]
      rescue FindFailed
        # Ignore. We deal we'll throw an appropriate exception after
        # having looped through all images and found none of them.
      end
    end
    # If we've reached this point, none of the images could be found.
    raise FindAnyFailed.new("can not find any of the images #{images} on the " +
                            "screen")
  end

  def s.waitAny(images, time)
    Timeout::timeout(time) do
      loop do
        result = self.existsAny(images)
        return result if result
      end
    end
  rescue Timeout::Error
    raise FindAnyFailed.new("can not find any of the images #{images} on the " +
                            "screen")
  end

  def s.hover_point(x, y)
    self.hover(Sikuli::Location.new(x, y))
  end

  def s.hide_cursor
    self.hover_point(self.w, self.h/2)
  end

  s
end

# Configure sikuli

# ruby and rjb doesn't play well together when it comes to static
# fields (and possibly methods) so we instantiate and access the field
# via objects instead. It actually works inside this file, but when
# it's required from "outside", and the file has been completely
# required, ruby's require method complains that the method for the
# field accessor is missing.
sikuli_settings = Sikuli::Settings.new
sikuli_settings.OcrDataPath = $config["TMPDIR"]
# sikuli_ruby, which we used before, defaulted to 0.9 minimum
# similarity, so all our current images are adapted to that value.
# Also, Sikuli's default of 0.7 is simply too low (many false
# positives).
sikuli_settings.MinSimilarity = 0.9
sikuli_settings.ActionLogs = true
sikuli_settings.DebugLogs = false
sikuli_settings.InfoLogs = true
sikuli_settings.ProfileLogs = true
sikuli_settings.WaitScanRate = 0.25