module Scan
  # This class detects all kinds of default values
  class DetectValues
    # This is needed as these are more complex default values
    # Returns the finished config object
    def self.set_additional_default_values
      config = Scan.config

      # First, try loading the Scanfile from the current directory
      config.load_configuration_file(Scan.scanfile_name)

      # Detect the project
      FastlaneCore::Project.detect_projects(config)
      Scan.project = FastlaneCore::Project.new(config)

      # Go into the project's folder, as there might be a Snapfile there
      imported_path = File.expand_path(Scan.scanfile_name)
      Dir.chdir(File.expand_path("..", Scan.project.path)) do
        config.load_configuration_file(Scan.scanfile_name) unless File.expand_path(Scan.scanfile_name) == imported_path
      end

      Scan.project.select_scheme

      devices = Scan.config[:devices] || Array(Scan.config[:device]) # important to use Array(nil) for when the value is nil
      if devices.count > 0
        detect_simulator(devices, '', '', '', nil)
      else
        if Scan.project.ios?
          # An iPhone 5s is a reasonably small and useful default for tests
          detect_simulator(devices, 'iOS', 'IPHONEOS_DEPLOYMENT_TARGET', 'iPhone 5s', nil)
        elsif Scan.project.tvos?
          detect_simulator(devices, 'tvOS', 'TVOS_DEPLOYMENT_TARGET', 'Apple TV 1080p', 'TV')
        end
      end
      detect_destination

      default_derived_data

      return config
    end

    def self.default_derived_data
      return unless Scan.config[:derived_data_path].to_s.empty?
      default_path = Scan.project.build_settings(key: "BUILT_PRODUCTS_DIR")
      # => /Users/.../Library/Developer/Xcode/DerivedData/app-bqrfaojicpsqnoglloisfftjhksc/Build/Products/Release-iphoneos
      # We got 3 folders up to point to ".../DerivedData/app-[random_chars]/"
      default_path = File.expand_path("../../..", default_path)
      UI.verbose("Detected derived data path '#{default_path}'")
      Scan.config[:derived_data_path] = default_path
    end

    def self.filter_simulators(simulators, operator = :greater_than_or_equal, deployment_target)
      deployment_target_version = Gem::Version.new(deployment_target)
      simulators.select do |s|
        sim_version = Gem::Version.new(s.os_version)
        if operator == :greater_than_or_equal
          sim_version >= deployment_target_version
        elsif operator == :equal
          sim_version == deployment_target_version
        else
          false # this will show an error message in the detect_simulator method
        end
      end
    end

    def self.regular_expression_for_split_on_whitespace_followed_by_parenthesized_version
      # %r{
      #   \s # a whitespace character
      #   (?= # followed by -- using lookahead
      #   \( # open parenthesis
      #   [\d\.]+ # our version -- one or more digits or full stops
      #   \) # close parenthesis
      #   $ # end of line
      #   ) # end of lookahead
      # }
      /\s(?=\([\d\.]+\)$)/
    end

    def self.detect_simulator(devices, requested_os_type, deployment_target_key, default_device_name, simulator_type_descriptor)
      require 'set'

      deployment_target_version = Scan.project.build_settings(key: deployment_target_key) || '0'

      simulators = filter_simulators(
        FastlaneCore::DeviceManager.simulators(requested_os_type).tap do |array|
          if array.empty?
            UI.user_error!(['No', simulator_type_descriptor, 'simulators found on local machine'].reject(&:nil?).join(' '))
          end
        end,
        :greater_than_or_equal,
        deployment_target_version
      ).tap do |sims|
        if sims.empty?
          UI.error("No simulators found that are greater than or equal to the version of deployment target (#{deployment_target_version})")
        end
      end

      # At this point we have all simulators for the given deployment target (or higher)

      # We create 2 lambdas, which we iterate over later on
      # If the first lambda `matches` found a simulator to use
      # we'll never call the second one

      matches = lambda do
        set_of_simulators = devices.inject(
          Set.new # of simulators
        ) do |set, device_string|
          pieces = device_string.split(regular_expression_for_split_on_whitespace_followed_by_parenthesized_version)

          selector = ->(sim) { pieces.count > 0 && sim.name == pieces.first }

          set + (
            if pieces.count == 0
              [] # empty array
            elsif pieces.count == 1
              simulators
                .select(&selector)
                .reverse # more efficient, because `simctl` prints higher versions first
                .sort_by! { |sim| Gem::Version.new(sim.os_version) }
                .pop(1)
            else # pieces.count == 2 -- mathematically, because of the 'end of line' part of our regular expression
              version = pieces[1].tr('()', '')
              potential_emptiness_error = lambda do |sims|
                if sims.empty?
                  UI.error("No simulators found that are equal to the version " \
                  "of specifier (#{version}) and greater than or equal to the version " \
                  "of deployment target (#{deployment_target_version})")
                end
              end
              filter_simulators(simulators, :equal, version).tap(&potential_emptiness_error).select(&selector)
            end
          ).tap do |array|
            UI.error("Ignoring '#{device_string}', couldn’t find matching simulator") if array.empty?
          end
        end

        set_of_simulators.to_a
      end

      default = lambda do
        UI.error("Couldn't find any matching simulators for '#{devices}' - falling back to default simulator")

        result = Array(
          simulators
            .select { |sim| sim.name == default_device_name }
            .reverse # more efficient, because `simctl` prints higher versions first
            .sort_by! { |sim| Gem::Version.new(sim.os_version) }
            .last || simulators.first
        )

        UI.error("Found simulator \"#{result.first.name} (#{result.first.os_version})\"") if result.first

        result
      end

      # grab the first unempty evaluated array
      Scan.devices = [matches, default].lazy.map { |x|
        arr = x.call
        arr unless arr.empty?
      }.reject(&:nil?).first
    end

    def self.min_xcode8?
      Helper.xcode_version.split(".").first.to_i >= 8
    end

    def self.detect_destination
      if Scan.config[:destination]
        UI.important("It's not recommended to set the `destination` value directly")
        UI.important("Instead use the other options available in `fastlane scan --help`")
        UI.important("Using your value '#{Scan.config[:destination]}' for now")
        UI.important("because I trust you know what you're doing...")
        return
      end

      # building up the destination now
      if Scan.devices && Scan.devices.count > 0
        Scan.config[:destination] = Scan.devices.map { |d| "platform=#{d.os_type} Simulator,id=#{d.udid}" }
      else
        Scan.config[:destination] = min_xcode8? ? ["platform=macOS"] : ["platform=OS X"]
      end
    end
  end
end
