require 'run_loop/directory'
require 'run_loop/environment'
require 'run_loop/logging'
require 'run_loop/process_terminator'
require 'run_loop/process_waiter'
require 'run_loop/lldb'
require 'run_loop/dylib_injector'
require 'run_loop/fifo'
require 'run_loop/core'
require 'run_loop/version'
require 'run_loop/xctools'
require 'run_loop/plist_buddy'
require 'run_loop/app'
require 'run_loop/ipa'
require 'run_loop/sim_control'
require 'run_loop/device'
require 'run_loop/instruments'
require 'run_loop/lipo'
require 'run_loop/cache/cache'
require 'run_loop/host_cache'
require 'run_loop/patches/awesome_print'
require 'run_loop/patches/retriable'
require 'run_loop/simctl/bridge'
require 'run_loop/simctl/plists'

module RunLoop

  class TimeoutError < RuntimeError
  end

  class WriteFailedError < RuntimeError
  end

  def self.run(options={})

    if RunLoop::Instruments.new.instruments_app_running?
      msg =
          [
              "Please quit the Instruments.app.",
              "If Instruments.app is open, the instruments command line",
              "tool cannot take control of your application."
          ]
      raise msg.join("\n")
    end

    uia_strategy = options[:uia_strategy]
    if options[:script]
      script = validate_script(options[:script])
    else
      if uia_strategy
        script = default_script_for_uia_strategy(uia_strategy)
      else
        if options[:calabash_lite]
          uia_strategy = :host
          script = Core.script_for_key(:run_loop_host)
        else
          uia_strategy = :preferences
          script = default_script_for_uia_strategy(uia_strategy)
        end
      end
    end
    # At this point, 'script' has been chosen, but uia_strategy might not
    unless uia_strategy
      desired_script = options[:script]
      if desired_script.is_a?(String) #custom path to script
        uia_strategy = :host
      elsif desired_script == :run_loop_host
        uia_strategy = :host
      elsif desired_script == :run_loop_fast_uia
        uia_strategy = :preferences
      elsif desired_script == :run_loop_shared_element
        uia_strategy = :shared_element
      else
        raise "Inconsistent state: desired script #{desired_script} has not uia_strategy"
      end
    end
    # At this point script and uia_strategy selected

    options[:script] = script
    options[:uia_strategy] = uia_strategy

    Core.run_with_options(options)
  end

  def self.send_command(run_loop, cmd, options={timeout: 60}, num_retries=0, last_error=nil)
    if num_retries > 3
      if last_error
        raise last_error
      else
        raise "Max retries exceeded #{num_retries} > 3. No error recorded."
      end
    end

    if options.is_a?(Numeric)
      options = {timeout: options}
    end

    if not cmd.is_a?(String)
      raise "Illegal command #{cmd} (must be a string)"
    end

    if not options.is_a?(Hash)
      raise "Illegal options #{options} (must be a Hash (or number for compatibility))"
    end

    timeout = options[:timeout] || 60
    logger = options[:logger]
    interrupt_retry_timeout = options[:interrupt_retry_timeout] || 25

    expected_index = run_loop[:index]
    result = nil
    begin
      expected_index = Core.write_request(run_loop, cmd, logger)
    rescue RunLoop::WriteFailedError, Errno::EINTR => write_error
      # Attempt recover from interrupt by attempting to read result (assuming write went OK)
      # or retry if attempted read result fails
      run_loop[:index] = expected_index # restore expected index in case it changed
      log_info(logger, "Core.write_request failed: #{write_error}. Attempting recovery...")
      log_info(logger, "Attempting read in case the request was received... Please wait (#{interrupt_retry_timeout})...")
      begin
        Timeout::timeout(interrupt_retry_timeout, TimeoutError) do
          result = Core.read_response(run_loop, expected_index)
        end
        # Update run_loop expected index since we succeeded in reading the index
        run_loop[:index] = expected_index + 1
        log_info(logger, "Did read response for interrupted request of index #{expected_index}... Proceeding.")
        return result
      rescue TimeoutError => _
        log_info(logger, "Read did not result in a response for index #{expected_index}... Retrying send_command...")
        return send_command(run_loop, cmd, options, num_retries+1, write_error)
      end
    end


    begin
      Timeout::timeout(timeout, TimeoutError) do
        result = Core.read_response(run_loop, expected_index)
      end
    rescue TimeoutError => _
      raise TimeoutError, "Time out waiting for UIAutomation run-loop for command #{cmd}. Waiting for index:#{expected_index}"
    end

    result
  end

  def self.stop(run_loop, out=Dir.pwd)
    return if run_loop.nil?
    results_dir = run_loop[:results_dir]
    dest = out

    RunLoop::Instruments.new.kill_instruments

    FileUtils.mkdir_p(dest)
    if results_dir
      pngs = Dir.glob(File.join(results_dir, 'Run 1', '*.png'))
    else
      pngs = []
    end
    FileUtils.cp(pngs, dest) if pngs and pngs.length > 0
  end

  def self.default_script_for_uia_strategy(uia_strategy)
    case uia_strategy
      when :preferences
        Core.script_for_key(:run_loop_fast_uia)
      when :host
        Core.script_for_key(:run_loop_host)
      when :shared_element
        Core.script_for_key(:run_loop_shared_element)
      else
        Core.script_for_key(:run_loop_basic)
    end
  end

  def self.validate_script(script)
    if script.is_a?(String)
      unless File.exist?(script)
        raise "Unable to find file: #{script}"
      end
    elsif script.is_a?(Symbol)
      script = Core.script_for_key(script)
      unless script
        raise "Unknown script for symbol: #{script}. Options: #{Core::SCRIPTS.keys.join(', ')}"
      end
    else
      raise "Script must be a symbol or path: #{script}"
    end
    script
  end

  def self.log_info(*args)
    RunLoop::Logging.log_info(*args)
  end

end