# frozen_string_literal: true
require "json"
require "faraday"

module CITA
  class Http
    attr_accessor :url

    DEFAULT_JSONRPC = "2.0"
    DEFAULT_PARAMS = [].freeze
    DEFAULT_ID = 83

    def initialize(url)
      @url = url
    end

    # wrapper for call rpc method
    #
    # @param method [String] method you want to call
    # @param jsonrpc [String] jsonrpc version
    # @param params [Array] rpc params
    # @param id [Integer] jsonrpc id
    #
    # @return [Faraday::Response]
    def call_rpc(method, jsonrpc: DEFAULT_JSONRPC, params: DEFAULT_PARAMS, id: DEFAULT_ID)
      conn.post("/", rpc_params(method, jsonrpc: jsonrpc, params: params, id: id))
    end

    # wrapper for rpc params
    #
    # @param method [String] method you want to call
    # @param jsonrpc [String] jsonrpc version
    # @param params [Array] rpc params
    # @param id [Integer] jsonrpc id
    #
    # @return [String] json string
    def rpc_params(method, jsonrpc: DEFAULT_JSONRPC, params: DEFAULT_PARAMS, id: DEFAULT_ID)
      {
        jsonrpc: jsonrpc,
        id: id,
        method: method,
        params: params
      }.to_json
    end

    # wrapper faraday object with CITA URL and Content-Type
    #
    # @return [Faraday]
    def conn
      Faraday.new(url: url) do |faraday|
        faraday.headers["Content-Type"] = "application/json"
        faraday.request  :url_encoded # form-encode POST params
        faraday.adapter  Faraday.default_adapter # make requests with Net::HTTP
      end
    end
  end
end


# frozen_string_literal: true
require "active_support/inflector"

module CITA
  class RPC
    attr_reader :url, :http

    # CITA v0.23 RPC list
    METHOD_NAMES = %w(
      peerCount
      blockNumber
      sendRawTransaction
      getBlockByHash
      getBlockByNumber
      getTransaction
      getTransactionReceipt
      getLogs
      call
      getTransactionCount
      getCode
      getAbi
      getBalance
      newFilter
      newBlockFilter
      uninstallFilter
      getFilterChanges
      getFilterLogs
      getTransactionProof
      getMetaData
      getBlockHeader
      getStateProof
      getVersion
      peersInfo
    ).freeze

    def initialize(url)
      @url = url
      @http = Http.new(@url)
    end

    # generate rpc methods
    METHOD_NAMES.each do |name|
      define_method name do |*params|
        call_rpc(name, params: params)
      end

      define_method name.underscore do |*params|
        send(name, *params)
      end
    end

    # @return [Hash] response body
    def call_rpc(method, jsonrpc: Http::DEFAULT_JSONRPC, params: Http::DEFAULT_PARAMS, id: Http::DEFAULT_ID)
      resp = http.call_rpc(method, params: params, jsonrpc: jsonrpc, id: id)
      JSON.parse(resp.body)
    end

    # @param transaction [CITA::Transaction]
    # @return [Hash]
    def send_transaction(transaction, private_key)
      content = TransactionSigner.encode(transaction, private_key)
      send_raw_transaction(content)
    end

    # easy to transfer tokens
    #
    # @param to [String] to address
    # @param private_key [String]
    # @param value [String | Integer] hex string or decimal integer
    # @param quota [Integer] default to 30_000
    #
    # @return [Hash]
    def transfer(to:, private_key:, value:, quota: 30_000)
      valid_until_block = block_number["result"].hex + 88
      meta_data = get_meta_data("latest")["result"]
      version = meta_data["version"]
      chain_id = if version.zero?
                   meta_data["chainId"]
                 elsif version == 1
                   meta_data["chainIdV1"]
                 end
      transaction = Transaction.new(nonce: Utils.nonce, valid_until_block: valid_until_block, chain_id: chain_id, to: to, value: value, quota: quota, version: version)
      send_transaction(transaction, private_key)
    end
  end
end


require 'json'
require 'stringio'

module Browser

# A {Storage} allows you to store data across page loads and browser
# restarts.
#
# Compatibility
# -------------
# The compatibility layer will try various implementations in the following
# order.
#
# + [window.localStorage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#localStorage)
# + [window.globalStorage](https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#globalStorage)
# + [document.body.addBehavior](http://msdn.microsoft.com/en-us/library/ms531424(VS.85).aspx)
# + [document.cookie](https://developer.mozilla.org/en-US/docs/Web/API/document.cookie)
#
# @see https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage
# @todo remove method_defined? checks when require order is fixed
class Storage
  def self.json_create(data)
    data.delete(JSON.create_id)

    Hash[data.map {|key, value|
      [JSON.parse(key), value]
    }]
  end

  # @!attribute [r] name
  # @return [String] the name of the storage
  attr_reader :name

  # Create a new storage on the given window with the given name.
  #
  # @param window [native] the window to save the storage to
  # @param name [String] the name to use to discern different storages
  def initialize(window, name)
    super()

    @window = window
    @name   = name
    @data   = {}

    autosave!
    reload
  end

  # Check if autosaving is enabled.
  #
  # When autosaving is enabled the {Storage} is saved every time a change is
  # made, otherwise you'll have to save it manually yourself.
  def autosave?
    @autosave
  end

  # Enable autosaving.
  def autosave!
    @autosave = true
  end

  # Disable autosaving.
  def no_autosave!
    @autosave = false
  end

  include Enumerable

  # Iterate over the (key, value) pairs in the storage.
  #
  # @yield [key, value]
  def each(&block)
    return enum_for :each unless block

    @data.each(&block)

    self
  end

  def method_missing(*args, &block)
    @data.__send__(*args, &block)
  end

  # Set a value in the storage.
  def []=(key, value)
    @data[key] = value

    save if autosave?
  end

  # Delete a value from the storage.
  def delete(key)
    @data.delete(key).tap {
      save if autosave?
    }
  end

  # Clear the storage.
  def clear
    @data.clear.tap {
      save if autosave?
    }
  end

  # Replace the current storage with the given one.
  #
  # @param new [Hash, String] if new is a {String} it will be parsed as JSON
  def replace(new)
    if String === new
      @data.replace(JSON.parse(new))
    else
      @data.replace(new)
    end
  end

  # Call the block between a [#reload] and [#save].
  def commit(&block)
    autosave  = @autosave
    @autosave = false
    result    = nil

    reload

    begin
      result = block.call
      save
    rescue
      reload
      raise
    ensure
      @autosave = autosave
    end

    result
  end

  def to_h
    @data
  end

  # @!method reload
  #   Load the storage.

  # @!method save
  #   Persist the current state to the storage.

  if Browser.supports? 'Storage.local'
    def reload
      replace `#@window.localStorage[#@name] || '{}'`
    end

    def save
      `#@window.localStorage[#@name] = #{JSON.dump(self)}`
    end
  elsif Browser.supports? 'Storage.global'
    def reload
      replace `#@window.globalStorage[#@window.location.hostname][#@name] || '{}'`
    end

    def save
      `#@window.globalStorage[#@window.location.hostname][#@name] = #{JSON.dump(self)}`
    end
  elsif Browser.supports? 'Element.addBehavior'
    def reload
      %x{
        #@element = #@window.document.createElement('link');
        #@element.addBehavior('#default#userData');

        #@window.document.getElementsByTagName('head')[0].appendChild(#@element);

        #@element.load(#@name);
      }

      replace `#@element.getAttribute(#@name) || '{}'`
    end

    def save
      %x{
        #@element.setAttribute(#@name, #{JSON.dump(self)});
        #@element.save(#@name);
      }
    end
  else
    def reload
      $document.cookies.options expires: 60 * 60 * 24 * 365

      replace $document.cookies[@name]
    end

    def save
      $document.cookies[@name] = JSON.dump(self)
    end
  end

  # Convert the storage to JSON.
  #
  # @return [String] the JSON representation
  def to_json
    io = StringIO.new("{")

    io << JSON.create_id.to_json << ":" << self.class.name.to_json << ","

    @data.each {|key, value|
      io << key.to_json.to_s << ":" << value.to_json << ","
    }

    io.seek(-1, IO::SEEK_CUR)
    io << "}"

    io.string
  end
end

# A {SessionStorage} allows you to store data across page reloads, as long as the session
# is active.
#
# @see https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Storage#sessionStorage
class SessionStorage < Storage
  def self.supported?
    Browser.supports? 'Storage.session'
  end

  def reload
    replace `#@window.sessionStorage[#@name] || '{}'`
  end

  def save
    `#@window.sessionStorage[#@name] = #{JSON.dump(self)}`
  end
end

class Window
  # Get a storage with the given name.
  #
  # @param name [Symbol] the name of the storage
  #
  # @return [Storage]
  def storage(name = :default)
    Storage.new(to_n, name)
  end

  # Get a session storage with the given name.
  #
  # @param name [Symbol] the name of the storage
  #
  # @return [SessionStorage]
  def session_storage(name = :default)
    SessionStorage.new(to_n, name)
  end
end

end


warn "`console' has been moved to Opal's stdlib, please `require 'console'` instead." if RUBY_ENGINE_VERSION.to_f >= 0.9

module Browser

# Manipulate the browser console.
#
# @see https://developer.mozilla.org/en-US/docs/Web/API/console
class Console
  include Native

  # Clear the console.
  def clear
    `#@native.clear()`
  end

  # Print a stacktrace from the call site.
  def trace
    `#@native.trace()`
  end

  # Log the passed objects based on an optional initial format.
  def log(*args)
    `#@native.log.apply(#@native, args)`
  end

  # Log the passed objects based on an optional initial format as informational
  # log.
  def info(*args)
    `#@native.info.apply(#@native, args)`
  end

  # Log the passed objects based on an optional initial format as warning.
  def warn(*args)
    `#@native.warn.apply(#@native, args)`
  end

  # Log the passed objects based on an optional initial format as error.
  def error(*args)
    `#@native.error.apply(#@native, args)`
  end

  # Time the given block with the given label.
  def time(label, &block)
    raise ArgumentError, "no block given" unless block

    `#@native.time(label)`

    begin
      if block.arity == 0
        instance_exec(&block)
      else
        block.call(self)
      end
    ensure
      `#@native.timeEnd()`
    end
  end

  # Group the given block.
  def group(*args, &block)
    raise ArgumentError, "no block given" unless block

    `#@native.group.apply(#@native, args)`

    begin
      if block.arity == 0
        instance_exec(&block)
      else
        block.call(self)
      end
    ensure
      `#@native.groupEnd()`
    end
  end

  # Group the given block but collapse it.
  def group!(*args, &block)
    return unless block_given?

    `#@native.groupCollapsed.apply(#@native, args)`

    begin
      if block.arity == 0
        instance_exec(&block)
      else
        block.call(self)
      end
    ensure
      `#@native.groupEnd()`
    end
  end
end

class Window
  # Get the {Console} for this window.
  #
  # @return [Console]
  def console
    Console.new(`#@native.console`)
  end
end

$console = $window.console

end


require 'stringio'

module Browser

# Allows manipulation of browser cookies.
#
# @see https://developer.mozilla.org/en-US/docs/Web/API/document.cookie
#
# Usage:
#
#   cookies = Browser::Cookies.new(`document`)
#   cookies["my-cookie"] = "monster"
#   cookies.delete("my-cookie")
#
class Cookies
  # Default cookie options.
  DEFAULT = {
    expires: Time.now + 60 * 60 * 24,
    secure:  false
  }

  include Enumerable

  attr_reader :options

  # Create a new {Cookies} wrapper.
  #
  # @param document [native] the native document object
  def initialize(document)
    @document = document
    @options  = DEFAULT.dup
  end

  # Access the cookie with the given name.
  #
  # @param name [String] the name of the cookie
  #
  # @return [Object]
  def [](name)
    matches = `#@document.cookie`.scan(/#{Regexp.escape(name.encode_uri_component)}=([^;]*)/)

    return if matches.empty?

    result = matches.flatten.map {|value|
      JSON.parse(value.decode_uri_component)
    }

    result.length == 1 ? result.first : result
  end

  # Set a cookie.
  #
  # @param name [String] the name of the cookie
  # @param value [Object] the data to set
  # @param options [Hash] the options for the cookie
  #
  # @option options [Integer] :max_age the max age of the cookie in seconds
  # @option options [Time]    :expires the expire date
  # @option options [String]  :path    the path the cookie is valid on
  # @option options [String]  :domain  the domain the cookie is valid on
  # @option options [Boolean] :secure  whether the cookie is secure or not
  def []=(name, value, options = {})
    string = value.is_a?(String) ? value : JSON.dump(value)
    encoded_value = encode(name, string, @options.merge(options))
    `#@document.cookie = #{encoded_value}`
  end

  # Delete a cookie.
  #
  # @param name [String] the name of the cookie
  def delete(name)
    `#@document.cookie = #{encode name, '', expires: Time.now}`
  end

  # @!attribute [r] keys
  # @return [Array<String>] all the cookie names
  def keys
    Array(`#@document.cookie.split(/; /)`).map {|cookie|
      cookie.split(/\s*=\s*/).first
    }
  end

  # @!attribute [r] values
  # @return [Array] all the cookie values
  def values
    keys.map {|key|
      self[key]
    }
  end

  # Enumerate the cookies.
  #
  # @yieldparam key [String] the name of the cookie
  # @yieldparam value [String] the value of the cookie
  #
  # @return [self]
  def each(&block)
    return enum_for :each unless block

    keys.each {|key|
      yield key, self[key]
    }

    self
  end

  # Delete all the cookies
  #
  # @return [self]
  def clear
    keys.each {|key|
      delete key
    }

    self
  end

protected
  def encode(key, value, options = {})
    io = StringIO.new

    io << key.encode_uri_component << ?= << value.encode_uri_component << '; '

    io << 'max-age=' << options[:max_age] << '; '        if options[:max_age]
    io << 'expires=' << options[:expires].utc << '; '    if options[:expires]
    io << 'path='    << options[:path] << '; '           if options[:path]
    io << 'domain='  << options[:domain] << '; '         if options[:domain]
    io << 'secure'                                       if options[:secure]

    io.string
  end
end

class DOM::Document < DOM::Element
  # @!attribute [r] cookies
  # @return [Cookies] the cookies for the document
  def cookies
    Cookies.new(@native) if defined?(`#@native.cookie`)
  end
end

end


require 'forwardable'

module RubyMarks
  # rubocop:disable Metrics/ClassLength
  class Recognizer
    extend Forwardable

    def_delegator :config, :configure
    attr_reader   :file, :raised_watchers, :groups, :watchers, :file_str, :original_file_str
    attr_accessor :config, :groups_detected, :groups_not_detected

    def initialize
      reset_document
      @groups = {}
      @groups_not_detected = []
      @config ||= Config.new(self)
    end

    def file=(file)
      reset_document
      @file = nil
      @file_str = nil
      @original_file = nil
      @original_file_str = nil

      @file = Magick::Image.read(file).first
      @file = @file.quantize(256, Magick::GRAYColorspace)
      @file = @file.threshold(@config.calculated_threshold_level)
      @original_file = @file
      @file = @file.edge(@config.edge_level)
      @groups_detected = false

      @groups.each_pair do |_label, group|
        group.marks = nil
        group.marks = Hash.new { |hash, key| hash[key] = [] }
      end
    end

    def reset_document
      @current_position = { x: 0, y: 0 }
      @clock_marks = []
      @raised_watchers = {}
      @watchers = {}
    end

    def filename
      @file&.filename
    end

    def add_group(group)
      @groups[group.label] = group if group
    end

    def add_watcher(watcher_name, &block)
      watcher = Watcher.new(watcher_name, self, &block)
      @watchers[watcher.name] = watcher if watcher
    end

    def raise_watcher(name, *args)
      watcher = @watchers[name]
      return unless watcher

      @raised_watchers[watcher.name] ||= 0
      @raised_watchers[watcher.name]  += 1
      watcher.run(*args)
    end

    # rubocop:disable Metrics/MethodLength
    # rubocop:disable Metrics/CyclomaticComplexity
    # rubocop:disable Metrics/PerceivedComplexity
    def scan
      raise IOError, "There's a invalid or missing file" if @file.nil?

      unmarked_group_found  = false
      multiple_marked_found = false

      result = Hash.new { |hash, key| hash[key] = [] }
      result.tap do |effect|
        begin
          Timeout.timeout(@config.scan_timeout) do
            detect_groups unless @groups_detected
          end
        rescue Timeout::Error
          raise_watcher :timed_out_watcher
          return effect
        end

        @groups.each_pair do |_label, group|
          marks = Hash.new { |hash, key| hash[key] = [] }
          group.marks.each_pair do |line, value|
            value.each do |mark|
              marks[line] << mark.value if mark.marked?(config.intensity_percentual) && mark.value
            end

            multiple_marked_found = true if marks[line].size > 1
            unmarked_group_found  = true if marks[line].empty?
          end

          effect[group.label.to_sym] = marks
        end

        raise_watcher :scan_unmarked_watcher, effect if unmarked_group_found

        raise_watcher :scan_multiple_marked_watcher, effect if multiple_marked_found

        if unmarked_group_found || multiple_marked_found
          raise_watcher :scan_mark_watcher, effect, unmarked_group_found, multiple_marked_found
        end
      end
    end
    # rubocop:enable Metrics/MethodLength
    # rubocop:enable Metrics/CyclomaticComplexity
    # rubocop:enable Metrics/PerceivedComplexity

    # rubocop:disable Metrics/MethodLength
    # rubocop:disable Metrics/CyclomaticComplexity
    # rubocop:disable Metrics/PerceivedComplexity
    # rubocop:disable Metrics/BlockNesting
    def detect_groups
      if @config.scan_mode == :grid
        scanner = FloodScan.new(@file.dup)
        @groups.each_pair do |_label, group|
          group_center = group.expected_coordinates.center
          x = group_center[:x]
          y = group_center[:y]
          width = group.expected_coordinates.width
          height = group.expected_coordinates.height
          block = scanner.scan(Magick::Point.new(x, y), width, height)
          if !block.empty?
            group.coordinates = Coordinates.new(block)
            marks_blocks = find_marks_grid(group)
            marks_blocks.each do |mark|
              mark_width  = ImageUtils.calc_width(mark[:x1], mark[:x2])
              mark_height = ImageUtils.calc_height(mark[:y1], mark[:y2])
              mark_file = @original_file.crop(mark[:x1], mark[:y1], mark_width, mark_height)
              o_mark = Mark.new group: group,
                                coordinates: coordinate(mark),
                                image_str: ImageUtils.export_file_to_str(mark_file),
                                line: mark[:line]

              group.marks[mark[:line]] << o_mark
            end
          else
            @groups_not_detected << group.label
          end
        end
      else
        file_str = ImageUtils.export_file_to_str(@file)
        original_file_str = ImageUtils.export_file_to_str(@original_file)
        incorrect_bubble_line_found = Hash.new { |hash, key| hash[key] = [] }
        bubbles_adjusted = []
        incorrect_expected_lines = false

        @groups.each_pair do |_label, group|
          next unless group.expected_coordinates.any?

          line = 0
          group_center = group.expected_coordinates.center

          block = find_block_marks(file_str, group_center[:x], group_center[:y], group)
          next unless block

          group.coordinates = Coordinates.new(block)
          marks_blocks = find_marks(original_file_str, group)
          marks_blocks.sort! { |a, b| a[:y1] <=> b[:y1] }
          mark_ant = nil
          marks_blocks.each do |mark|
            mark_width  = ImageUtils.calc_width(mark[:x1], mark[:x2])
            mark_height = ImageUtils.calc_height(mark[:y1], mark[:y2])

            next unless mark_width >= group.mark_width_with_down_tolerance &&
                        mark_height >= group.mark_height_with_down_tolerance

            mark_positions = mark[:y1] - 10..mark[:y1] + 10
            line += 1 unless mark_ant && mark_positions.include?(mark_ant[:y1])
            mark[:line] = line
            mark_ant = mark
          end

          marks_blocks.delete_if { |m| m[:line].nil? }
          marks_blocks.sort_by! { |a| [a[:line], a[:x1]] }

          mark_ant = nil
          marks_blocks.each do |mark|
            if mark_ant && mark_ant[:line] == mark[:line]
              mark_ant_center = ImageUtils.image_center(mark_ant)
              mark_center     = ImageUtils.image_center(mark)
              if (mark_ant_center[:x] - mark_center[:x]).abs < 10
                mark[:conflict] = true
                mark[:conflicting_mark] = mark_ant
              else
                mark_ant = mark
              end
            else
              mark_ant = mark
            end
          end
          marks_blocks.delete_if { |m| m[:conflict] }

          first_position = 0
          elements_position_count = 0
          marks_blocks.map { |m| m[:line] }.each do |dash|
            marks = marks_blocks.select { |m| m[:line] == dash }
            if marks.count == group.marks_options.count
              first_position += marks.first[:x1]
              elements_position_count += 1
            end
          end

          if elements_position_count.positive?
            first_position /= elements_position_count
            distance = group.distance_between_marks * (group.marks_options.count - 1)
            last_position = first_position + distance
            marks_blocks.delete_if do |mark|
              mark[:x1] < first_position - 10 ||
                mark[:x1] > last_position + 10
            end

            marks_blocks.map { |m| m[:line] }.each do |dash|
              loop do
                reprocess = false
                marks = marks_blocks.select { |m| m[:line] == dash }
                marks.each_with_index do |current_mark, index|
                  if index.zero?
                    first_mark_position = first_position - 5..first_position + 5
                    unless first_mark_position.include?(current_mark[:x1])
                      new_mark = { x1: first_position,
                                   x2: first_position + group.mark_width,
                                   y1: current_mark[:y1],
                                   y2: current_mark[:y1] + group.mark_height,
                                   line: dash }
                      marks_blocks << new_mark
                      marks_blocks.sort_by! { |a| [a[:line], a[:x1]] }
                      bubbles_adjusted << new_mark
                      reprocess = true
                      break
                    end
                  end
                  next_mark = marks[index + 1]
                  distance = 0
                  distance = next_mark[:x1] - current_mark[:x1] if next_mark
                  next unless distance > group.distance_between_marks + 10 ||
                              next_mark.nil? && index + 1 < group.marks_options.count

                  new_x1 = current_mark[:x1] + group.distance_between_marks
                  new_mark = { x1: new_x1,
                               x2: new_x1 + group.mark_width,
                               y1: current_mark[:y1],
                               y2: current_mark[:y1] + group.mark_height,
                               line: dash }
                  marks_blocks << new_mark
                  marks_blocks.sort_by! { |a| [a[:line], a[:x1]] }
                  bubbles_adjusted << new_mark
                  reprocess = true
                  break
                end
                break unless reprocess
              end
            end

          end

          marks_blocks.each do |mark|
            mark_width  = ImageUtils.calc_width(mark[:x1], mark[:x2])
            mark_height = ImageUtils.calc_height(mark[:y1], mark[:y2])
            mark_file = @original_file.crop(mark[:x1], mark[:y1], mark_width, mark_height)
            o_mark = Mark.new group: group,
                              coordinates: coordinate(mark),
                              image_str: ImageUtils.export_file_to_str(mark_file),
                              line: mark[:line]
            group.marks[mark[:line]] << o_mark if mark[:line] <= group.expected_lines
          end

          incorrect_expected_lines = group.incorrect_expected_lines

          group.marks.each_pair do |dash, marks|
            if marks.count != group.marks_options.count
              incorrect_bubble_line_found[group.label.to_sym] << dash
            end
          end
        end
        @groups_detected = true
        if incorrect_bubble_line_found.any? || bubbles_adjusted.any? || incorrect_expected_lines
          raise_watcher :incorrect_group_watcher,
                    incorrect_expected_lines, incorrect_bubble_line_found, bubbles_adjusted.flatten
        end
      end
    end
    # rubocop:enable Metrics/MethodLength
    # rubocop:enable Metrics/CyclomaticComplexity
    # rubocop:enable Metrics/PerceivedComplexity
    # rubocop:enable Metrics/BlockNesting

    # rubocop:disable Metrics/CyclomaticComplexity
    # rubocop:disable Metrics/MethodLength
    # rubocop:disable Metrics/PerceivedComplexity
    # rubocop:disable Metrics/BlockNesting
    def find_block_marks(image, x, y, group)
      expected_coordinates = group.expected_coordinates
      found_blocks = []
      expected_width  = expected_coordinates.width
      expected_height = expected_coordinates.height
      block = nil
      while x <= expected_coordinates.x2 && y <= expected_coordinates.y2
        if image[y] && image[y][x] == ' '
          block = find_in_blocks(found_blocks, x, y)
          unless block
            block = find_block(image, x, y)
            found_blocks << block

            block[:width]  = ImageUtils.calc_width(block[:x1], block[:x2])
            block[:height] = ImageUtils.calc_height(block[:y1], block[:y2])

            if @config.scan_mode == :grid
              unless block[:width] <= (expected_width + group.block_width_tolerance) &&
                     block[:width] >= (expected_width - group.block_width_tolerance)

                if block[:width] > expected_width + group.block_width_tolerance
                  ajust_width = block[:width] - expected_width
                  if @config.auto_ajust_block_width == :left
                    block[:x2] = (block[:x2] - ajust_width) + @config.edge_level
                    block[:width] = expected_width + @config.edge_level
                  elsif @config.auto_ajust_block_width == :right
                    block[:x1] = (block[:x1] + ajust_width) - @config.edge_level
                    block[:width] = expected_width + @config.edge_level
                  end
                else
                  block[:width] = 0
                end
              end
              unless block[:height] <= (expected_height + group.block_height_tolerance) &&
                     block[:height] >= (expected_height - group.block_height_tolerance)

                if block[:height] > expected_height + group.block_height_tolerance
                  ajust_width = block[:height] - expected_height
                  if @config.auto_ajust_block_height == :top
                    block[:y2] = (block[:y2] - ajust_height) + @config.edge_level
                    block[:height] = expected_height + @config.edge_level
                  elsif @config.auto_ajust_block_height == :bottom
                    block[:y1] = (block[:y1] + ajust_height) - @config.edge_level
                    block[:height] = expected_height + @config.edge_level
                  end
                else
                  block[:height] = 0
                end
              end
            end

            block_width_with_tolerance  = block[:width]  + group.block_width_tolerance
            block_height_with_tolerance = block[:height] + group.block_height_tolerance

            return block if block_width_with_tolerance >= expected_width &&
                            block_height_with_tolerance >= expected_height
          end
        end

        x += 1
        y += 1
      end
    end
    # rubocop:enable Metrics/CyclomaticComplexity
    # rubocop:enable Metrics/MethodLength
    # rubocop:enable Metrics/PerceivedComplexity
    # rubocop:enable Metrics/BlockNesting

    def find_marks_grid(group)
      block = group.coordinates
      blocks = []
      blocks.tap do |chunks|
        lines   = group.expected_lines
        columns = group.marks_options.size
        distance_lin = group.mark_height
        distance_col = group.mark_width
        lines.times do |lin|
          columns.times do |col|
            chunks << { x1: block.x1 + (col * distance_col),
                        y1: block.y1 + (lin * distance_lin),
                        x2: block.x1 + (col * distance_col) + distance_col,
                        y2: block.y1 + (lin * distance_lin) + distance_lin,
                        line: lin + 1 }
          end
        end
      end
    end

    # rubocop:disable Metrics/CyclomaticComplexity
    # rubocop:disable Metrics/MethodLength
    # rubocop:disable Metrics/PerceivedComplexity
    # rubocop:disable Metrics/BlockNesting
    def find_marks(image, group)
      block = group.coordinates
      y = block.y1
      blocks = []
      blocks.tap do |chunks|
        while y < block.y2
          x = block.x1
          while x < block.x2
            if image[y][x] == ' '
              x += 1
              next
            end

            result = find_in_blocks(chunks, x, y)
            unless result
              result = find_block(image, x, y, '.', block)

              mark_width  = ImageUtils.calc_width(*result.values_at(:x1, :x2))
              mark_height = ImageUtils.calc_height(*result.values_at(:y1, :y2))

              if mark_width > group.mark_width_with_up_tolerance
                distance_x1 = x - result[:x1]
                distance_x2 = result[:x2] - x
                if distance_x1 <= distance_x2
                  result[:x2] = result[:x1] + group.mark_width
                else
                  result[:x1] = result[:x2] - group.mark_width
                end
              end

              if mark_height > group.mark_height_with_up_tolerance
                distance_y1 = y - result[:y1]
                distance_y2 = result[:y2] - y
                if distance_y1 <= distance_y2
                  result[:y2] = result[:y1] + group.mark_height
                else
                  result[:y1] = result[:y2] - group.mark_height
                end
              end

              chunks << result unless chunks.any? { |b| b == result }

            end
            x += 1
          end
          y += 1
        end
      end
    end
    # rubocop:enable Metrics/CyclomaticComplexity
    # rubocop:enable Metrics/MethodLength
    # rubocop:enable Metrics/PerceivedComplexity
    # rubocop:enable Metrics/BlockNesting

    def flag_position(position)
      raise IOError, "There's a invalid or missing file" if @file.nil?

      files = @original_file.dup

      files.tap do |file|
        add_mark(file, position)
      end
    end

    # rubocop:disable Metrics/MethodLength
    def flag_all_marks
      raise IOError, "There's a invalid or missing file" if @file.nil?

      @original_file.dup.tap do |file|
        begin
          Timeout.timeout(@config.scan_timeout) do
            detect_groups unless @groups_detected
          end
        rescue Timeout::Error
          raise_watcher :timed_out_watcher
          return file
        end

        @groups.each_pair do |_label, group|
          dr = Magick::Draw.new
          dr.stroke_width = 5
          dr.stroke(COLORS[3])
          dr.line(*group.expected_coordinates.values_at(:x1, :y1, :x2, :y1))
          dr.line(*group.expected_coordinates.values_at(:x2, :y1, :x2, :y2))
          dr.line(*group.expected_coordinates.values_at(:x2, :y2, :x1, :y2))
          dr.line(*group.expected_coordinates.values_at(:x1, :y2, :x1, :y1))
          dr.draw(file)

          next unless group.coordinates

          dr = Magick::Draw.new
          dr.stroke_width = 5
          dr.stroke(COLORS[5])
          dr.line(*group.coordinates.values_at(:x1, :y1, :x2, :y1))
          dr.line(*group.coordinates.values_at(:x2, :y1, :x2, :y2))
          dr.line(*group.coordinates.values_at(:x2, :y2, :x1, :y2))
          dr.line(*group.coordinates.values_at(:x1, :y2, :x1, :y1))
          dr.draw(file)
        end
      end
    end
    # rubocop:enable Metrics/MethodLength

    private

    def coordinate(mark)
      {
        x1: mark[:x1],
        y1: mark[:y1],
        x2: mark[:x2],
        y2: mark[:y2]
      }
    end

    def find_block(image, x, y, character = ' ', coordinates = {})
      stack = ImageUtils.flood_scan(image, x, y, character, coordinates)

      x_elements = []
      y_elements = []
      stack.each do |k, _v|
        stack[k].inject(x_elements, :<<)
        y_elements << k
      end

      x_elements.sort!.uniq!
      y_elements.sort!.uniq!

      x1 = x_elements.first || 0
      x2 = x_elements.last  || 0
      y1 = y_elements.first || 0
      y2 = y_elements.last  || 0

      { x1: x1, x2: x2, y1: y1, y2: y2 }
    end

    def find_in_blocks(blocks, x, y)
      blocks.find do |result|
        result[:x1] <= x && result[:x2] >= x &&
          result[:y1] <= y && result[:y2] >= y
      end
    end

    # rubocop:disable Metrics/MethodLength
    def add_mark(file, position, mark = nil)
      dr = Magick::Draw.new
      if @config.scan_mode == :grid
        x = position[:x] - 9
        y = position[:y] + 5
        intensity = mark.intensity ? mark.intensity.ceil.to_s : '+'

        dr.annotate(file, 0, 0, x, y, intensity) do
          self.pointsize = 15
          self.fill = COLORS[2]
        end

        dr = Magick::Draw.new
        dr.stroke_width = 2
        dr.stroke(COLORS[1])

        dr.line(*mark.coordinates.values_at(:x1, :y1, :x2, :y1))
        dr.line(*mark.coordinates.values_at(:x2, :y1, :x2, :y2))
        dr.line(*mark.coordinates.values_at(:x2, :y2, :x1, :y2))
        dr.line(*mark.coordinates.values_at(:x1, :y2, :x1, :y1))
      else
        dr.annotate(file, 0, 0, position[:x] - 9, position[:y] + 11, '+') do
          self.pointsize = 30
          self.fill = '#900000'
        end

        dr = Magick::Draw.new
        dr.fill = '#FF0000'
        dr.point(position[:x], position[:y])
        dr.point(position[:x], position[:y] + 1)
        dr.point(position[:x] + 1, position[:y])
        dr.point(position[:x] + 1, position[:y] + 1)
      end

      dr.draw(file)
    end
    # rubocop:enable Metrics/MethodLength
  end
  # rubocop:enable Metrics/ClassLength
end


module Metaforce
  class AbstractClient
    class << self
      # Internal
      def endpoint(key)
        define_method :endpoint do; @options[key] end
      end

      # Internal
      def wsdl(wsdl)
        define_method :wsdl do; wsdl end
      end
    end

    # Public: Initialize a new client.
    #
    # options - A hash of options, which should have a :session_id key
    def initialize(options={})
      raise 'Please specify a hash of options' unless options.is_a?(Hash)
      @options = options
    end

  private

    # Internal: The Savon client to send SOAP requests with.
    def client
      @client ||= Savon.client(wsdl) do |wsdl|
        wsdl.endpoint = endpoint
      end.tap do |client|
        client.config.soap_header = soap_headers
        client.http.auth.ssl.verify_mode = :none
      end
    end

    # Internal: Performs a SOAP request. If the session is invalid, it will
    # attempt to reauthenticate by called the reauthentication handler if
    # present.
    def request(*args, &block)
      authenticate! unless session_id
      retries = authentication_retries
      begin
        perform_request(*args, &block)
      rescue Savon::SOAP::Fault => e
        if e.message =~ /INVALID_SESSION_ID/ && authentication_handler && retries > 0
          authenticate!
          retries -= 1
          retry
        end
        raise
      end
    end

    def perform_request(*args, &block)
      response = client.request(*args, &block)
      Hashie::Mash.new(response.body)[:"#{args[0]}_response"].result
    end

    # Internal Calls the authentication handler, which should set @options to a new
    # hash.
    def authenticate!
      options = authentication_handler.call(self, @options)
      @options.merge!(options)
      client.config.soap_header = soap_headers
    end

    # A proc object that gets called when the client needs to reauthenticate.
    def authentication_handler
      Metaforce.configuration.authentication_handler
    end

    def authentication_retries
      3
    end

    # Internal: Soap headers to set for authenticate.
    def soap_headers
      { 'ins0:SessionHeader' => { 'ins0:sessionId' => session_id } }
    end

    # Internal: The session id, which can be obtained by calling
    # Metaforce.login or through OAuth.
    def session_id
      @options[:session_id]
    end
  end
end


module Metaforce
  class Job::Retrieve < Job

    # Public: Instantiate a new retrieve job.
    #
    # Examples
    #
    #   job = Metaforce::Job::Retrieve.new(client)
    #   # => #<Metaforce::Job::Retrieve @id=nil>
    #
    # Returns self.
    def initialize(client, options={})
      super(client)
      @options = options
    end

    # Public: Perform the job.
    #
    # Examples
    #
    #   job = Metaforce::Job::Retrieve.new(client)
    #   job.perform
    #   # => #<Metaforce::Job::Retrieve @id='1234'>
    #
    # Returns self.
    def perform
      @id = client._retrieve(@options).id
      super
    end

    # Public: Get the detailed status of the retrieve.
    #
    # Examples
    #
    #   job.result
    #   # => { :id => '1234', :zip_file => '<base64 encoded content>', ... }
    #
    # Returns the RetrieveResult (http://www.salesforce.com/us/developer/docs/api_meta/Content/meta_retrieveresult.htm).
    def result
      @result ||= client.status(id, :retrieve)
    end

    # Public: Decodes the content of the returned zip file.
    #
    # Examples
    #
    #   job.zip_file
    #   # => '<binary content>'
    #
    # Returns the decoded content.
    def zip_file
      Base64.decode64(result.zip_file)
    end

    # Public: Unzips the returned zip file to the location.
    #
    # destination - Path to extract the contents to.
    #
    # Examples
    #
    #   job.extract_to('./path')
    #   # => #<Metaforce::Job::Retrieve @id='1234'>
    #
    # Returns self.
    def extract_to(destination)
      return on_complete { |job| job.extract_to(destination) } unless started?
      with_tmp_zip_file do |file|
        unzip(file, destination)
      end
      self
    end

  private

    # Internal: Unzips source to destination.
    def unzip(source, destination)
      Zip::File.open(source) do |zip|
        zip.each do |f|
          path = File.join(destination, f.name)
          FileUtils.mkdir_p(File.dirname(path))
          zip.extract(f, path) { true }
        end
      end
    end

    # Internal: Writes the zip file content to a temporary location so it can
    # be extracted.
    def with_tmp_zip_file
      file = Tempfile.new('retrieve')
      begin
        file.binmode
        file.write(zip_file)
        file.rewind
        yield file
      ensure
        file.close
        file.unlink
      end
    end

  end
end


require 'zip'
require 'base64'

module Metaforce
  class Job
    DELAY_START = 1
    DELAY_MULTIPLIER = 2

    autoload :Deploy,   'metaforce/job/deploy'
    autoload :Retrieve, 'metaforce/job/retrieve'
    autoload :CRUD,     'metaforce/job/crud'

    # Public: The id of the AsyncResult returned from Salesforce for
    # this job.
    attr_reader :id

    # Public: Instantiate a new job. Doesn't actually do anything until
    # .perform is called.
    #
    # Examples
    #
    #   job = Metaforce::Job.new(client)
    #   # => #<Metaforce::Job @id=nil>
    #
    # Returns self.
    def initialize(client)
      @_callbacks = Hash.new { |h,k| h[k] = [] }
      @client = client
    end

    # Public: Perform the job.
    #
    # Examples
    #
    #   job = Metaforce::Job.new
    #   job.perform
    #   # => #<Metaforce::Job @id=nil>
    #
    # Returns self.
    def perform
      start_heart_beat
      self
    end

    # Public: Utility method to determine if .perform has been called yet.
    #
    # Returns true if @id is set, false otherwise.
    def started?
      !!@id
    end

    # Public: Register a block to be called when an event occurs.
    #
    # Yields the job.
    #
    # &block - Proc or Lambda to be run when the event is triggered.
    #
    # Examples
    #
    #   job.on_complete do |job|
    #     puts "Job ##{job.id} completed!"
    #   end
    #
    #   job.on_error do |job|
    #     puts "Job failed!"
    #   end
    #
    #   job.on_poll do |job|
    #     puts "Polled status for #{job.id}"
    #   end
    #
    # Returns self.
    #
    # Signature
    #
    #   on_complete(&block)
    #   on_error(&block)
    #   on_poll(&block)
    %w[complete error poll].each do |type|
      define_method :"on_#{type}" do |&block|
        @_callbacks[:"on_#{type}"] << block
        self
      end
    end

    # Public: Queries the job status from the API.
    #
    # Examples
    #
    #   job.status
    #   # => { :id => '1234', :done => false, ... }
    #
    # Returns the AsyncResult (http://www.salesforce.com/us/developer/docs/api_meta/Content/meta_asyncresult.htm).
    def status
      @status ||= client.status(id)
    end

    # Public: Returns true if the job has completed.
    #
    # Examples
    #
    #   job.done
    #   # => true
    #
    # Returns true if the job has completed, false otherwise.
    def done?
      status.done
    end

    # Public: Returns the state if the job has finished processing.
    #
    # Examples
    #
    #   job.state
    #   # => 'Completed'
    #
    # Returns the state of the job.
    def state
      status.state
    end

    # Public: Check if the job is in a given state.
    #
    # Examples
    #
    #   job.queued?
    #   # => false
    #
    # Returns true or false.
    #
    # Signature
    #
    #   queued?
    #   in_progress?
    #   completed?
    #   error?
    %w[Queued InProgress Completed Error].each do |state|
      define_method :"#{state.underscore}?" do; self.state == state end
    end

    def inspect
      "#<#{self.class} @id=#{@id.inspect}>"
    end

    def self.disable_threading!
      ActiveSupport::Deprecation.warn <<-WARNING.strip_heredoc
        Metaforce::Job.disable_threading! is deprecated. Use Metaforce.configuration.threading = false instead.
      WARNING
      Metaforce.configuration.threading = false
    end

  private
    attr_reader :client

    # Internal: Starts a heart beat in a thread, which polls the job status
    # until it has completed or timed out.
    def start_heart_beat
      if threading?
        Thread.abort_on_exception = true
        @heart_beat ||= Thread.new &run_loop
      else
        run_loop.call
      end
    end

    # Internal: Starts the run loop, and blocks until the job has completed or
    # failed.
    def run_loop
      proc {
        delay = DELAY_START
        loop do
          @status = nil
          sleep (delay = delay * DELAY_MULTIPLIER)
          trigger :on_poll
          if completed? || error?
            trigger callback_type
            Thread.stop if threading?
            break
          end
        end
      }
    end

    def trigger(type)
      @_callbacks[type].each do |block|
        block.call(self)
      end
    end

    def callback_type
      if completed?
        :on_complete
      elsif error?
        :on_error
      end
    end

    def threading?
      Metaforce.configuration.threading
    end

  end
end


module Metaforce
  class Login
    def initialize(username, password, security_token=nil)
      @username, @password, @security_token = username, password, security_token
    end

    # Public: Perform the login request.
    #
    # Returns a hash with the session id and server urls.
    def login
      response = client.request(:login) do
        soap.body = {
          :username => username,
          :password => password
        }
      end
      response.body[:login_response][:result]
    end

  private

    # Internal: Savon client.
    def client
      @client ||= Savon.client(Metaforce.configuration.partner_wsdl) do |wsdl|
        wsdl.endpoint = Metaforce.configuration.endpoint
      end.tap { |client| client.http.auth.ssl.verify_mode = :none }
    end

    # Internal: Usernamed passed in from options.
    def username
      @username
    end

    # Internal: Password + Security Token combined.
    def password
      [@password, @security_token].join('')
    end
  end
end


require 'nokogiri'
require 'active_support/core_ext'

module Metaforce
  class Manifest < Hash

    # Public: Initializes a new instance of a manifest (package.xml) file.
    #
    # It can either take a hash:
    #   {
    #     :apex_class => [
    #       "TestController",
    #       "TestClass"
    #     ],
    #     :apex_component => [
    #       "SiteLogin"
    #     ]
    #   }
    #
    # Or an xml string containing the contents of a packge.xml file:
    #  <?xml version="1.0"?>
    #  <Package xmlns="http://soap.sforce.com/2006/04/metadata">
    #    <types>
    #      <members>TestClass</members>
    #      <members>AnotherClass</members>
    #      <name>ApexClass</name>
    #    </types>
    #    <types>
    #      <members>Component</members>
    #      <name>ApexComponent</name>
    #    </types>
    #    <types>
    #      <members>Assets</members>
    #      <name>StaticResource</name>
    #    </types>
    #    <version>23.0</version>
    #  </Package>
    #
    def initialize(components={})
      self.replace Hash.new { |h,k| h[k] = [] }
      if components.is_a?(Hash)
        self.merge!(components)
      elsif components.is_a?(String)
        self.parse(components)
      end
    end

    # Public: Returns a string containing a package.xml file
    #
    #  <?xml version="1.0"?>
    #  <Package xmlns="http://soap.sforce.com/2006/04/metadata">
    #    <types>
    #      <members>TestClass</members>
    #      <members>AnotherClass</members>
    #      <name>ApexClass</name>
    #    </types>
    #    <types>
    #      <members>Component</members>
    #      <name>ApexComponent</name>
    #    </types>
    #    <types>
    #      <members>Assets</members>
    #      <name>StaticResource</name>
    #    </types>
    #    <version>23.0</version>
    #  </Package>
    def to_xml
      xml_builder = Nokogiri::XML::Builder.new do |xml|
        xml.Package('xmlns' => 'http://soap.sforce.com/2006/04/metadata') {
          self.each do |key, members|
            xml.types {
              members.each do |member|
                xml.members member
              end
              xml.name key.to_s.camelize
            }
          end
          xml.version Metaforce.configuration.api_version
        }
      end
      xml_builder.to_xml
    end

    # Public: Converts the manifest into a format that can be used by the
    # metadata api.
    def to_package
      self.map do |type, members|
        { :members => members, :name => type.to_s.camelize }
      end
    end

    # Public: Parses a package.xml file
    def parse(file)
      document = Nokogiri::XML(file).remove_namespaces!
      document.xpath('//types').each do |type|
        name = type.xpath('name').first.content
        key = name.underscore.to_sym
        type.xpath('members').each do |member|
          self[key] << member.content
        end
      end
      self
    end

  end
end


module Metaforce
  class Job::Deploy < Job

    # Public: Instantiate a new deploy job.
    #
    # Examples
    #
    #   job = Metaforce::Job::Deploy.new(client, './path/to/deploy')
    #   # => #<Metaforce::Job::Deploy @id=nil>
    #
    # Returns self.
    def initialize(client, path, options={})
      super(client)
      @path, @options = path, options
    end

    # Public: Perform the job.
    #
    # Examples
    #
    #   job = Metaforce::Job::Deploy.new(client, './path/to/deploy')
    #   job.perform
    #   # => #<Metaforce::Job::Deploy @id='1234'>
    #
    # Returns self.
    def perform
      @id = client._deploy(payload, @options).id
      super
    end

    # Public: Get the detailed status of the deploy.
    #
    # Examples
    #
    #   job.result
    #   # => { :id => '1234', :success => true, ... }
    #
    # Returns the DeployResult (http://www.salesforce.com/us/developer/docs/api_meta/Content/meta_deployresult.htm).
    def result
      @result ||= client.status(id, :deploy)
    end

    # Public: Returns true if the deploy was successful.
    #
    # Examples
    #
    #   job.success?
    #   # => true
    #
    # Returns true or false based on the DeployResult.
    def success?
      result.success
    end

  private

    # Internal: Base64 encodes the contents of the zip file.
    #
    # Examples
    #
    #   job.payload
    #   # => '<lots of base64 encoded content>'
    #
    # Returns the content of the zip file encoded to base64.
    def payload
      Base64.encode64(File.open(file, 'rb').read)
    end

    # Internal: Returns the path to the zip file.
    def file
      File.file?(@path) ? @path : zip_file
    end

    # Internal: Creates a zip file with the contents of the directory.
    def zip_file
      path = Dir.mktmpdir
      File.join(path, 'deploy.zip').tap do |path|
        Zip::File.open(path, Zip::File::CREATE) do |zip|
          Dir["#{@path}/**/**"].each do |file|
            zip.add(file.sub("#{File.dirname(@path)}/", ''), file)
          end
        end
      end
    end

  end
end


module Xcodeproj

  class Project

    # Creates a new object with given UUID.
    #
    # @param [String] uuid UUID of the object.
    #
    def new_with_uuid(klass, uuid)
      if klass.is_a?(String)
        klass = Object.const_get(klass)
      end
      object = klass.new(self, uuid)
      object.initialize_defaults
      object
    end

    # Creates a new group with given UUID.
    #
    # @param [String] uuid UUID of the object.
    #
    def new_group_with_uuid(name, uuid, path = nil, source_tree = :group)
      main_group.new_group_with_uuid(name, uuid, path, source_tree)
    end

    # @param [String] name The name of target.
    # @return the target with given name
    #
    def target_named(name)
      self.targets.each do |target|
        if target.name == name.to_s
          return target
        end
      end
      nil
    end

  end

  def self.uuid_with_name(name)
      Digest::MD5.hexdigest(name).upcase[0,24]
  end
end

module Xcodeproj::Project::Object

  class PBXGroup

    # Creates a new group with given UUID.
    #
    # @param [String] uuid UUID of the object.
    #
    def new_group_with_uuid(name, uuid, path = nil, source_tree = :group)
      group = project.new_with_uuid(PBXGroup, uuid)
      children << group
      group.name = name
      group.set_source_tree(source_tree)
      group.set_path(path)
      group
    end

    # Creates a file reference with given UUID.
    #
    # @param [String] uuid UUID of the object.
    #
    def new_reference_with_uuid(path, uuid, source_tree = :group)
      # customize `FileReferencesFactory.new_file_reference`
      path = Pathname.new(path)
      ref = self.project.new_with_uuid(PBXFileReference, uuid)
      self.children << ref
      GroupableHelper.set_path_with_source_tree(ref, path, source_tree)
      ref.set_last_known_file_type

      # customize `FileReferencesFactory.configure_defaults_for_file_reference`
      if ref.path.include?('/')
        ref.name = ref.path.split('/').last
      end
      if File.extname(ref.path).downcase == '.framework'
        ref.include_in_index = nil
      end

      ref
    end
    alias_method :new_file_with_uuid, :new_reference_with_uuid
  end

  class PBXNativeTarget

    # @return 'Sources Build Phase' or `nil`
    #
    def sources_build_phase()
      self.build_phases.each do |phase|
        if phase.kind_of?(Xcodeproj::Project::Object::PBXSourcesBuildPhase)
          return phase
        end
      end
      nil
    end
  end

  class PBXLegacyTarget

    # @return 'Sources Build Phase' or `nil`
    #
    def sources_build_phase()
      self.build_phases.each do |phase|
        if phase.kind_of?(Xcodeproj::Project::Object::PBXSourcesBuildPhase)
          return phase
        end
      end
      nil
    end
  end

  class AbstractBuildPhase

    # Adds the file reference with given UUID.
    #
    # @param [String] uuid UUID of the object.
    #
    def add_file_reference_with_uuid(file_ref, uuid, avoid_duplicates = false)
      if avoid_duplicates && existing = build_file(file_ref)
        existing
      else
        build_file = project.new_with_uuid(PBXBuildFile, uuid)
        build_file.file_ref = file_ref
        files.insert(0, build_file)
        build_file
      end
    end

    # @return whether the file names match the pattern.
    # @param [Regexp] pattern The pattern of file name.
    #
    def include_filename?(pattern)
      self.file_display_names.each do |filename|
        return true if filename.match pattern
      end
      false
    end

  end
end


module Seeds
  class Core

    # @return [String] project folder path
    #
    attr_reader :root_path

    # @return [Boolean] whether display outputs
    #
    attr_accessor :mute

    # @return [String] Seedfile path
    #
    # @!visibility private
    #
    attr_reader :seedfile_path

    # @return [String] Seedfile.lock path
    #
    # @!visibility private
    #
    attr_reader :lockfile_path

    # @return [Xcodeproj::Project] Xcode project
    #
    # @!visibility private
    #
    attr_accessor :project

    # @return [String] content of Seedfile
    #
    # @!visibility private
    #
    attr_accessor :seedfile

    # @return [String] content of Seedfile.lock
    #
    # @!visibility private
    #
    attr_accessor :lockfile

    # @return [Hash{Sting => Seeds::Seed}] seeds by seed name
    #
    # @!visibility private
    #
    attr_reader :seeds

    # @return [Hash{Sting => Seeds::Seed}] locked dependencies by seed name
    #
    # @!visibility private
    #
    attr_reader :locks

    # @return [Hash{Sting => String}] target name by seed name
    #
    # @!visibility private
    #
    attr_reader :targets

    # @return [Hash{Sting => Seeds::Seed}] source file paths by seed name
    #
    # @!visibility private
    #
    attr_reader :source_files

    # @return [Array<Xcodeproj::Project::Object::PBXFileReference>]
    #         file references that will be added to project
    #
    # @!visibility private
    #
    attr_reader :file_references

    # @return [Boolean] whether append seed name as a prefix to swift files
    #
    # @!visibility private
    #
    attr_accessor :swift_seedname_prefix

    # @param  [String] root_path
    #         The path provided will be used for detecting Xcode project and
    #         Seedfile.
    #
    # @see #root_path
    #
    def initialize(root_path)
      @root_path = root_path
      @seedfile_path = File.join(root_path, "Seedfile")
      @lockfile_path = File.join(root_path, "Seeds", "Seedfile.lock")
      @seeds = {}
      @locks = {}
      @targets = {}
      @source_files = {}
      @file_references = []
    end

    # Read Seedfile and install dependencies. An exception will be raised if
    # there is no .xcodeproj file or Seedfile in the {#root_path}.
    #
    # @see #root_path
    #
    def install
      self.prepare_requirements
      self.analyze_dependencies
      self.execute_seedfile
      self.remove_seeds
      self.install_seeds
      self.configure_project
      self.configure_phase
      self.project.save
      self.build_lockfile
      @seeds = {}
      @locks = {}
      @targets = {}
      @source_files = {}
      @file_references = []
      @swift_seedname_prefix = false
    end

    # Read Xcode project, Seedfile and lockfile. An exception will be raised if
    # there is no .xcodeproj file or Seedfile in the {#root_path}.
    #
    # @see #root_path
    #
    # @!visibility private
    #
    def prepare_requirements
      # .xcodeproj
      project_filename = Dir.glob("#{root_path}/*.xcodeproj")[0]
      if project_filename
        self.project = Xcodeproj::Project.open(project_filename)
      end

      # Seedfile
      begin
        self.seedfile = File.read(self.seedfile_path)
      rescue Errno::ENOENT
        raise Seeds::Exception.new  "Couldn't find Seedfile."
      end

      # Seedfile.lock - optional
      begin
        self.lockfile = File.read(self.lockfile_path)
      rescue Errno::ENOENT
      end
    end

    # Parses Seedfile.lockfile into {#lockfile}.
    #
    # @see #lockfile
    #
    # @!visibility private
    #
    def analyze_dependencies
      say "Anaylizing dependencies"

      # Seedfile.lock
      if self.lockfile
        locks = YAML.load(self.lockfile)
        locks["SEEDS"].each do |lock|
          seed = Seeds::Seed.new
          seed.name = lock.split(' (')[0]
          seed.version = lock.split('(')[1].split(')')[0]
          if seed.version.start_with? '$'
            seed.commit = seed.version[1..-1]
            seed.version = nil
          end
          self.locks[seed.name] = seed
        end
      end
    end

    # Executes {#seedfile} using `eval`
    #
    # @!visibility private
    #
    def execute_seedfile
      @current_target_name = nil

      # Sets `@swift_seedname_prefix` as `true`.
      #
      # @!scope method
      # @!visibility private
      #
      def swift_seedname_prefix!()
        @swift_seedname_prefix = true
      end

      # Set current Xcode project with given path.
      #
      # @!scope method
      # @!visibility private
      #
      def xcodeproj(path)
        proejct_filename = File.join(self.root_path, path)
        self.project = Xcodeproj::Project.open(proejct_filename)
        self.validate_project
      end

      # Sets `@current_target_name` and executes code block.
      #
      # @param [String] names The name of target.
      #
      # @!scope method
      # @!visibility private
      #
      def target(*names, &code)
        self.validate_project
        names.each do |name|
          name = name.to_s  # use string instead of symbol
          target = self.project.target_named(name)
          if not target
            raise Seeds::Exception.new\
              "#{self.project.path.basename} doesn't have a target `#{name}`"
          end

          @current_target_name = name
          code.call()
        end
        @current_target_name = nil
      end

      def local(name, source_dir, options={})
        self.validate_project
        if not @current_target_name
          target *self.project.targets.map(&:name) do
            send(__callee__, name, source_dir, options)
          end
        else
          seed = Seeds::Seed::LocalSeed.new
          if not name
            raise Seeds::Exception.new\
            "Need a name to identifier."
          else
            seed.name = name
          end

          if not source_dir
            raise Seeds::Exception.new\
            "Need a source dir."
          else
            seed.source_dir = source_dir
          end

          seed.files = options[:files] ||  '**/*.{h,m,mm,swift}'
          if seed.files.kind_of? String
            seed.files = [seed.files]
          end
          seed.exclude_files = options[:exclude_files] || []
          self.seeds[seed.name] = seed
          self.targets[seed.name] ||= []
          self.targets[seed.name] << @current_target_name.to_s
        end
      end
      # Creates a new instance of {#Seeds::Seed::GitHub} and adds to {#seeds}.
      #
      # @see #Seeds::Seed::GitHub
      #
      # @!scope method
      # @!visibility private
      #
      def github(repo, tag, options={})
        self.validate_project
        if not @current_target_name  # apply to all targets
          target *self.project.targets.map(&:name) do
            send(__callee__, repo, tag, options)
          end
        elsif repo.split('/').count != 2
          raise Seeds::Exception.new\
          "#{repo}: GitHub should have both username and repo name.\n"\
          "    (e.g. `devxoul/JLToast`)"
        else
          seed = Seeds::Seed::GitHub.new
          seed.url = "https://github.com/#{repo}"
          seed.name = repo.split('/')[1]
          if tag.is_a?(String)
            if options[:commit]
              raise Seeds::Exception.new\
                "#{repo}: Version and commit are both specified."
            end
            seed.version = tag
            seed.files = options[:files] || '**/*.{h,m,mm,swift}'
            seed.exclude_files = options[:exclude_files] || []
          elsif tag.is_a?(Hash)
            options.merge!(tag)
            seed.commit = options[:commit][0..6]
            seed.files = options[:files] || '**/*.{h,m,mm,swift}'
            seed.exclude_files = options[:exclude_files] || []
          end
          if seed.files.kind_of?(String)
            seed.files = [seed.files]
          end
          if seed.exclude_files.kind_of?(String)
            seed.exclude_files = [seed.exclude_files]
          end
          self.seeds[seed.name] = seed
          self.targets[seed.name] ||= []
          self.targets[seed.name] << @current_target_name.to_s
        end
      end

      # Creates a new instance of {#Seeds::Seed::BitBucket} and adds to
      # {#seeds}.
      #
      # @see #Seeds::Seed::BitBucket
      #
      # @!scope method
      # @!visibility private
      #
      def bitbucket(repo, tag, options={})
        self.validate_project
        if not @current_target_name  # apply to all targets
          target *self.project.targets.map(&:name) do
            send(__callee__, repo, tag, options)
          end
        elsif repo.split('/').count != 2
          raise Seeds::Exception.new\
          "#{repo}: BitBucket should have both username and repo name.\n"\
          "    (e.g. `devxoul/JLToast`)"
        else
          seed = Seeds::Seed::BitBucket.new
          seed.url = "https://bitbucket.org/#{repo}"
          seed.name = repo.split('/')[1]
          if tag.is_a?(String)
            if options[:commit]
              raise Seeds::Exception.new\
                "#{repo}: Version and commit are both specified."
            end
            seed.version = tag
            seed.files = options[:files] || '**/*.{h,m,mm,swift}'
            seed.exclude_files = options[:exclude_files] || []
          elsif tag.is_a?(Hash)
            options.merge!(tag)
            seed.commit = options[:commit][0..6]
            seed.files = options[:files] || '**/*.{h,m,mm,swift}'
            seed.exclude_files = options[:exclude_files] || []
          end
          if seed.files.kind_of?(String)
            seed.files = [seed.files]
          end
          if seed.exclude_files.kind_of?(String)
            seed.exclude_files = [seed.exclude_files]
          end
          self.seeds[seed.name] = seed
          self.targets[seed.name] ||= []
          self.targets[seed.name] << @current_target_name.to_s
        end
      end

      def git(repo, tag, options={})
        self.validate_project
        if not @current_target_name
          target *self.project.targets.map(&:name) do
            send(__callee__, repo, tag, options)
          end
        elsif not repo.end_with? ".git"
          raise Seeds::Exception.new\
          "#{repo}: is not a valid git repo.\n"
        else
          seed = Seeds::Seed::CustomSeed.new
          seed.url = repo
          seed.name = repo.split('/').last.sub /.git$/, ''
          if tag.is_a? String
            if options[:commit]
              raise Seeds::Exception.new\
              "#{repo}: Version and commit are both specified."
            end
            seed.version = tag
            seed.files = options[:files] || '**/*.{h,m,mm,swift}'
            seed.exclude_files = options[:exclude_files] || []
          elsif tag.is_a? Hash
            seed.commit = tag[:commit][0..6]
            seed.files = tag[:files] || '**/*.{h,m,mm,swift}'
            seed.exclude_files = options[:exclude_files] || []
          end
          if seed.files.kind_of? String
            seed.files = [seed.files]
          end
          if seed.exclude_files.kind_of? String
            seed.exclude_files = [seed.exclude_files]
          end
          self.seeds[seed.name] = seed
          self.targets[seed.name] ||= []
          self.targets[seed.name] << @current_target_name.to_s
        end
      end

      eval seedfile
    end

    # Removes disused seeds.
    #
    # @!visibility private
    #
    def remove_seeds
      removings = self.locks.keys - self.seeds.keys
      removings.each do |name|
        say "Removing #{name} (#{self.locks[name].version})".red
        dirname = File.join(self.root_path, "Seeds", name)
        FileUtils.rm_rf(dirname)
      end
    end

    # Installs new seeds or updates existing seeds.
    #
    # @!visibility private
    #
    def install_seeds
      seed_dir = File.join self.root_path, "Seeds"
      if not Dir.exist? seed_dir
        Dir.mkdir seed_dir
      end

      self.seeds.sort.each do |name, seed|
        dirname = File.join(self.root_path, "Seeds", seed.name)
        if seed.instance_of? Seeds::Seed::LocalSeed
          self.install_local_seed(seed, dirname)
        else
          self.install_seed(seed, dirname)
        end

        next if not seed.files

        # add seed files to `source_files`
        self.source_files[name] = []
        seed.files.each do |file|
          paths = Dir.glob(File.join(dirname, file))

          # exclude files
          seed.exclude_files.each do |exclude_file|
            exclude_paths = Dir.glob(File.join(dirname, exclude_file))
            exclude_paths.each do |exclude_path|
              paths.delete(exclude_path)
            end
          end

          paths.each do |path|
            path = self.path_with_prefix(seed.name, path)
            self.source_files[name].push(path)
          end
        end
      end
    end

    # Installs new seed or updates existing seed in {#dirname}.
    #
    # @!visibility private
    #
    def install_seed(seed, dirname)
      # if remote url has changed, remove directory and clone again
      remote_url = `
        cd #{Shellwords.escape(dirname)} 2>&1 &&
        git remote show origin -n | grep Fetch | awk '{ print $3 }' 2>&1
      `.strip
      if remote_url != seed.url
        FileUtils.rm_rf(dirname)
      end

      # clone and return if not exists
      if not File.exist?(dirname)
        say "Installing #{seed.name} (#{seed.version or seed.commit})".green

        command = "git clone #{seed.url}"
        command += " -b #{seed.version}" if seed.version
        command += " #{Shellwords.escape(dirname)} 2>&1"
        output = `#{command}`

        unable_to_access = output.include?("unable to access")
        if unable_to_access and output.include?("Failed to connect to")
          raise Seeds::Exception.new\
            "#{seed.name}: Failed to connect to #{seed.url}. \n#{output}"
        end

        not_found = output.include?("not found")
        if not_found and output.include?("repository")
          raise Seeds::Exception.new\
            "#{seed.name}: Couldn't find the repository."
        elsif not_found and output.include?("upstream")
          raise Seeds::Exception.new\
            "#{seed.name}: Couldn't find the tag `#{seed.version}`."
        end

        if seed.commit and not seed.version # checkout to commit
          output = `cd #{Shellwords.escape(dirname)} 2>&1 &&\
                    git checkout #{seed.commit} 2>&1`
          if output.include?("did not match any")
            raise Seeds::Exception.new\
              "#{seed.name}: Couldn't find the commit `#{seed.commit}`."
          end
        end

        return
      end

      # discard local changes
      `cd #{Shellwords.escape(dirname)} 2>&1 &&\
       git reset HEAD --hard 2>&1 &&\
       git checkout . 2>&1 &&\
       git clean -fd 2>&1`

      if lock = self.locks[seed.name]
        lock_version = lock.version
        lock_commit = lock.commit
      end

      if seed.version == lock_version and seed.commit == lock_commit
        say "Using #{seed.name} (#{lock_version or lock_commit})"
        return
      end

      if seed.version
        say "Installing #{seed.name} #{seed.version}"\
            " (was #{lock_version or lock_commit})".green
        output = `cd #{Shellwords.escape(dirname)} 2>&1 &&\
                 git fetch origin #{seed.version} --tags 2>&1 &&\
                 git checkout #{seed.version} 2>&1`
        if output.include?("Couldn't find")
          raise Seeds::Exception.new\
            "#{seed.name}: Couldn't find the tag or branch `#{seed.version}`."
        end

      elsif seed.commit
        say "Installing #{seed.name} #{seed.commit}"\
            " (was #{lock_version or lock_commit})".green
        output = `cd #{Shellwords.escape(dirname)} 2>&1 &&
                  git checkout master 2>&1 &&
                  git pull 2>&1 &&
                  git checkout #{seed.commit} 2>&1`
        if output.include?("did not match any")
          raise Seeds::Exception.new\
            "#{seed.name}: Couldn't find the commit `#{seed.commit}`.".red
        end
      end

    end

    def install_local_seed(seed, dirname)
      FileUtils.rm_rf dirname
      if not File.exist? dirname
        say "Installing local seed: #{seed.name}"
        Dir.mkdir dirname
      end

      if seed.source_dir
        full_source_path = File.expand_path(seed.source_dir)
        command = "cp -R #{Shellwords.escape(full_source_path)}/* "\
                  "#{Shellwords.escape(self.root_path)}/Seeds/#{seed.name}"
        output = `#{command}`
      else
        raise Seeds::Exception.new\
        "Not found source dir."
      end
    end

    # Append seed name as a prefix to file name and returns the path.
    #
    # @!visibility private
    #
    def path_with_prefix(seedname, path)
      if @swift_seedname_prefix
        components = path.split("/")
        prefix = seedname + "_"  # Alamofire_
        filename = components[-1]  # Alamofire.swift
        extension = File.extname(filename)  # .swift

        # only swift files can have prefix in filename
        if extension == '.swift' and not filename.start_with? prefix
          filename = prefix + filename  # Alamofire_Alamofire.swift
          newpath = components[0...-1].join('/') + '/' + filename
          File.rename(path, newpath)  # rename real files
          path = newpath
        end
      end
      path
    end

    # Adds source files to the group 'Seeds' and save its reference to
    # {#file_references} and removes disused sources files,
    #
    # @see #file_references
    #
    # @!visibility private
    #
    def configure_project
      say "Configuring #{self.project.path.basename}"

      group = self.project["Seeds"]
      if group
        group.clear
      else
        uuid = Xcodeproj::uuid_with_name "Seeds"
        group = self.project.new_group_with_uuid("Seeds", uuid)
      end

      # remove existing group that doesn't have any file references
      group.groups.each do |seedgroup|
        valid_files = seedgroup.children.select do |child|
          File.exist?(child.real_path)
        end
        if valid_files.length == 0
          seedgroup.remove_from_project
        end
      end

      self.source_files.each do |seedname, filepaths|
        uuid = Xcodeproj::uuid_with_name "Seeds/#{seedname}"
        seedgroup = group[seedname] ||
                    group.new_group_with_uuid(seedname, uuid)
        filepaths.each do |path|
          filename = path.split('/')[-1]
          relpath = path[self.root_path.length..-1]
          uuid = Xcodeproj::uuid_with_name relpath
          file_reference = seedgroup[filename] ||
                           seedgroup.new_reference_with_uuid(path, uuid)
          self.file_references << file_reference
        end

        unusing_files = seedgroup.files - self.file_references
        unusing_files.each { |file| file.remove_from_project }
      end
    end

    # Adds file references to the 'Sources Build Phase'.
    #
    # @!visibility private
    #
    def configure_phase
      self.project.targets.each do |target|
        begin
          phase = target.sources_build_phase
          # support resources phase
          resource_phase = target.resources_build_phase
          next unless phase
        rescue NoMethodError
          next
        end

        # remove zombie build files
        phase.files_references.each do |file|
          begin
            file.real_path
          rescue
            phase.files.each do |build_file|
              phase.files.delete(build_file) if build_file.file_ref == file
            end
          end
        end

        resource_phase.files_references.each do |file|
          begin
            file.real_path
          rescue
            resource_phase.files.each do |build_file|
              resource_phase.files.delete(build_file) if build_file.file_ref == file
            end
          end
        end

        removings = [] # name of seeds going to be removed from the target
        addings = [] # name of seeds going to be added to the target

        self.targets.keys.sort.each do |seed_name|
          target_names = self.targets[seed_name]
          if not target_names.include?(target.name)
            removings << seed_name if not removings.include?(seed_name)
          else
            addings << seed_name if not addings.include?(seed_name)
          end
        end

        self.file_references.each do |file|
          removings.each do |seed_names|
            next if not seed_names.include?(file.parent.name)
            phase.files.each do |build_file|
              phase.files.delete(build_file) if build_file.file_ref == file
            end
            resource_phase.files.each do |build_file|
              resource_phase.files.delete(build_file) if build_file.file_ref == file
            end
          end

          addings.each do |seed_names|
            next if file.name.end_with? ".h"
            next if not seed_names.include?(file.parent.name)
            uuid = Xcodeproj::uuid_with_name "#{target.name}:#{file.name}"
            # Treat a file as resource file unless confirm it can be compiled.
            if self.valid_source_file?(file)
              phase.add_file_reference_with_uuid(file, uuid, true)
            else
              resource_phase.add_file_reference_with_uuid(file, uuid, true)
            end

          end
        end
      end
    end

    # Determines whether there's a source file.
    #
    # @!visibility private
    #
    def valid_source_file? filename
      suffixs = [".h", ".c", ".m", ".mm", ".swift", ".cpp"]
      suffixs.each do |suffix|
        return true if filename.name.end_with? suffix
      end
      return false
    end

    # Writes Seedfile.lock file.
    #
    # @!visibility private
    #
    def build_lockfile
      tree = { "SEEDS" => [] }
      self.seeds.each do |name, seed|
        if not seed.instance_of? Seeds::Seed::LocalSeed
          tree["SEEDS"] << "#{name} (#{seed.version or '$' + seed.commit})"
        end
      end
      File.write(self.lockfile_path, YAML.dump(tree))
    end

    def validate_project
      if self.project.nil?
        raise Seeds::Exception.new "Couldn't find .xcodeproj file."
      end
    end

    # Prints a message if {#mute} is `false`.
    #
    # @see #mute
    #
    def say(*strings)
      puts strings.join(" ") if not @mute
    end

  end
end


#--
# Yast rake
#
# Copyright (C) 2015 Novell, Inc.
#   This library is free software; you can redistribute it and/or modify
# it only under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
#
#   This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
# details.
#
#   You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#++
#
# Rake task for checking spelling in the documentation files.
# By default checks all *.md and *.html files.
#
# Supports custom dictionaries:
#
#  - global dictionary located in the tasks gem (lib/tasks/spell.yml)
#  - repository specific dictionary (.spell.yml in the root directory)
#
# The custom dictionaries are defined at the "dictionary" key.
#

require "rake"
require "rake/tasklib"

module Yast
  # Defines a spellcheck rake task
  class SpellcheckTask < Rake::TaskLib
    GLOBAL_SPELL_CONFIG_FILE = File.expand_path("../spell.yml", __FILE__)
    CUSTOM_SPELL_CONFIG_FILE = ".spell.yml".freeze

    # define the Rake task in the constructor
    def initialize
      namespace :check do
        desc "Run spell checker (by default for *.md and *.html files)"
        task :spelling do
          run_task
        end
      end
    end

  private

    # optionally colorize the misspelled words if the rainbow gem is present
    # @return [Boolean] true when the colorization support is present
    def colorize?
      return @colorize unless @colorize.nil?

      begin
        require "rainbow"
        @colorize = true
      rescue LoadError
        @colorize = false
      end
    end

    # create an Aspell speller object
    # @return [Aspell] the speller object
    def speller
      return @speller if @speller
      # raspell is an optional dependency, handle the missing case nicely
      begin
        require "raspell"
      rescue LoadError
        $stderr.puts "ERROR: Ruby gem \"raspell\" is not installed."
        exit 1
      end

      # initialize aspell
      @speller = Aspell.new("en_US")
      @speller.suggestion_mode = Aspell::NORMAL
      # ignore the HTML tags in the text
      @speller.set_option("mode", "html")

      @speller
    end

    # evaluate the files to check
    # @return [Array<String>] list of files
    def files_to_check
      files = config["check"].reduce([]) { |a, e| a + Dir[e] }
      config["ignore"].reduce(files) { |a, e| a - Dir[e] }
    end

    # read a Yaml config file
    def read_spell_config(file)
      return {} unless File.exist?(file)

      puts "Loading config file (#{file})..." if verbose == true
      require "yaml"
      YAML.load_file(file)
    end

    # print the duplicate dictionary entries
    # @param dict1 [Array<String>] the first dictionary
    # @param dict2 [Array<String>] the second dictionary
    def report_duplicates(dict1, dict2)
      duplicates = dict1 & dict2
      return if duplicates.empty?

      $stderr.puts "Warning: Found dictionary duplicates in the local dictionary " \
        "(#{CUSTOM_SPELL_CONFIG_FILE}):\n"
      duplicates.each { |duplicate| $stderr.puts "  #{duplicate}" }
      $stderr.puts
    end

    # return the merged global and the custom spell configs
    # @return [Hash] the merged configuration to use
    def config
      return @config if @config

      @config = read_spell_config(GLOBAL_SPELL_CONFIG_FILE)
      custom_config = read_spell_config(CUSTOM_SPELL_CONFIG_FILE)

      report_duplicates(config["dictionary"], custom_config["dictionary"].to_a)

      custom_config["dictionary"] = @config["dictionary"] + custom_config["dictionary"].to_a
      custom_config["dictionary"].uniq!

      # override the global values by the local if present
      @config.merge!(custom_config)

      @config
    end

    # check the file using the spellchecker
    # @param file [String] file name
    # @return [Boolean] true on success (no spelling error found)
    def check_file(file)
      puts "Checking #{file}..." if verbose == true
      # spell check each line separately so we can report error locations properly
      lines = File.read(file).split("\n")

      success = true
      lines.each_with_index do |text, index|
        misspelled = misspelled_on_line(text)
        next if misspelled.empty?

        success = false
        print_misspelled(misspelled, index, text)
      end

      success
    end

    def print_misspelled(list, index, text)
      list.each { |word| text.gsub!(word, Rainbow(word).red) } if colorize?
      puts "#{file}:#{index + 1}: \"#{text}\""

      list.each { |word| puts "    #{word.inspect} => #{speller.suggest(word)}" }
      puts
    end

    def misspelled_on_line(text)
      switch_block_tag if block_line?(text)
      return [] if inside_block
      speller.list_misspelled([text]) - config["dictionary"]
    end

    def block_line?(line)
      line =~ /^\s*```/
    end

    def switch_block_tag
      @inside_block = !@inside_block
    end

    attr_reader :inside_block

    # run the task
    def run_task
      if files_to_check.all? { |file| check_file(file) }
        puts "Spelling OK."
      else
        $stderr.puts "Spellcheck failed! (Fix it or add the words to " \
          "'#{CUSTOM_SPELL_CONFIG_FILE}' file if it is OK.)"
        exit 1
      end
    end
  end
end


# frozen_string_literal: true

module Dkdeploy
  # dsl api
  module DSL
    # Execute a rake/capistrano task only for given server
    #
    # @param server [Capistrano::Configuration::Server] Server to execute task
    # @param task [String] Name of rake/capistrano task
    # @param args [Array] Arguments of rake/capistrano task
    def invoke_for_server(server, task, *args)
      backup_filter = fetch :filter, {}
      new_server_filter = Marshal.load(Marshal.dump(backup_filter))
      new_server_filter[:host] = server.hostname
      set :filter, new_server_filter
      env.setup_filters
      info I18n.t('dsl.invoke_for_server.set_filter', task: task, host: server.hostname, scope: :dkdeploy)
      invoke! task, *args
    ensure
      set :filter, backup_filter
      env.setup_filters
    end
  end
end


# frozen_string_literal: true

module OptParseValidator
  # Implementation of the MultiChoices Option
  class OptMultiChoices < OptArray
    # @param [ Array ] option See OptBase#new
    # @param [ Hash ] attrs
    # @option attrs [ Hash ] :choices
    # @option attrs [ Array<Array> ] :incompatible
    # @options attrs [ String ] :separator See OptArray#new
    def initialize(option, attrs = {})
      raise Error, 'The :choices attribute is mandatory' unless attrs.key?(:choices)
      raise Error, 'The :choices attribute must be a hash' unless attrs[:choices].is_a?(Hash)

      super(option, attrs)
    end

    def append_help_messages
      option << 'Available Choices:'

      append_choices_help_messages

      super

      append_incompatible_help_messages
    end

    def append_choices_help_messages
      max_spaces = choices.keys.max_by(&:size).size

      choices.each do |key, opt|
        first_line_prefix  = " #{key} #{' ' * (max_spaces - key.length)}"
        other_lines_prefix = ' ' * first_line_prefix.size

        opt_help_messages(opt).each_with_index do |message, index|
          option << "#{index.zero? ? first_line_prefix : other_lines_prefix} #{message}"
        end
      end
    end

    def help_message_for_default
      msg = +''

      default.each do |key, value|
        msg << if value == true
                 key.to_s.titleize
               else
                 "#{key.to_s.titleize}: #{value}"
               end
        msg << ', '
      end

      msg.chomp(', ')
    end

    # @param [ OptBase ] opt
    #
    # @return [ Array<String> ]
    def opt_help_messages(opt)
      opt.help_messages.empty? ? [opt.to_s.humanize] : opt.help_messages
    end

    def append_incompatible_help_messages
      return if incompatible.empty?

      option << 'Incompatible choices (only one of each group/s can be used):'

      incompatible.each do |a|
        option << " - #{a.map(&:to_s).join(', ')}"
      end
    end

    # @param [ String ] value
    #
    # @return [ Hash ]
    def validate(value)
      results = {}

      super(value).each do |item|
        opt = choices[item.to_sym]

        if opt
          opt_value = opt.value_if_empty.nil? ? true : opt.value_if_empty
        else
          opt, opt_value = value_from_pattern(item)
        end

        results[opt.to_sym] = opt.normalize(opt.validate(opt_value))
      end

      verify_compatibility(results)
    end

    # @return [ Array ]
    def value_from_pattern(item)
      choices.each do |key, opt|
        next unless item =~ /\A#{key.to_s}(.*)\z/

        return [opt, Regexp.last_match[1]]
      end

      raise Error, "Unknown choice: #{item}"
    end

    # @return [ Array<Array<Symbol>> ]
    def incompatible
      [*attrs[:incompatible]]
    end

    # @param [ Hash ] values
    #
    # @return [ Hash ]
    def verify_compatibility(values)
      incompatible.each do |a|
        last_match = ''

        a.each do |key|
          sym = choices[key].to_sym

          next unless values.key?(sym)

          raise Error, "Incompatible choices detected: #{last_match}, #{key}" unless last_match.empty?

          last_match = key
        end
      end
      values
    end

    # No normalization
    def normalize(value)
      value
    end
  end
end


# frozen_string_literal: true

module OptParseValidator
  # Implementation of the Integer Range Option
  class OptIntegerRange < OptBase
    # @return [ Void ]
    def append_help_messages
      option << "Range separator to use: '#{separator}'"

      super
    end

    # @param [ String ] value
    #
    # @return [ Range ]
    def validate(value)
      a = super(value).split(separator)

      raise Error, "Incorrect number of ranges found: #{a.size}, should be 2" unless a.size == 2

      first_integer = a.first.to_i
      last_integer  = a.last.to_i

      raise Error, 'Argument is not a valid integer range' unless first_integer.to_s == a.first && last_integer.to_s == a.last

      (first_integer..last_integer)
    end

    # @return [ String ]
    def separator
      attrs[:separator] || '-'
    end
  end
end


# frozen_string_literal: true

module OptParseValidator
  # Implementation of the Array Option
  class OptArray < OptBase
    # @return [ Void ]
    def append_help_messages
      option << "Separator to use between the values: '#{separator}'"

      super
    end

    # @param [ String ] value
    #
    # @return [ Array ]
    def validate(value)
      super(value).split(separator)
    end

    # @return [ String ] The separator used to split the string into an array
    def separator
      attrs[:separator] || ','
    end

    # See OptBase#normalize
    # @param [ Array ] values
    def normalize(values)
      values.each_with_index do |value, index|
        values[index] = super(value)
      end
      values
    end
  end
end


# frozen_string_literal: true

module OptParseValidator
  # Implementation of the Path Option
  class OptPath < OptBase
    # Initialize attrs:
    #
    # :create     if set to true, will create the path
    #
    # :exists     if set to false, will ignore the file? and directory? checks
    #
    # :file       Check if the path is a file
    # :directory  Check if the path is a directory
    #
    # :executable
    # :readable
    # :writable
    #

    # @param [ String ] value
    #
    # @return [ String ]
    def validate(value)
      path = Pathname.new(value)

      allowed_attrs.each do |key|
        method = "check_#{key}"
        send(method, path) if respond_to?(method) && attrs[key]
      end

      path.to_s
    end

    def allowed_attrs
      %i[create file directory executable readable writable]
    end

    # check_create is implemented in the file_path and directory_path opts

    # @param [ Pathname ] path
    def check_file(path)
      raise Error, "'#{path}' is not a file" unless path.file? || attrs[:exists] == false
    end

    # @param [ Pathname ] path
    def check_directory(path)
      raise Error, "'#{path}' is not a directory" unless path.directory? || attrs[:exists] == false
    end

    # @param [ Pathname ] path
    def check_executable(path)
      raise Error, "'#{path}' is not executable" unless path.executable?
    end

    # @param [ Pathname ] path
    def check_readable(path)
      raise Error, "'#{path}' is not readable" unless path.readable?
    end

    # If the path does not exist, it will check for the parent
    # directory write permission
    #
    # @param [ Pathname ] path
    def check_writable(path)
      raise Error, "'#{path}' is not writable" if path.exist? && !path.writable? || !path.parent.writable?
    end
  end
end


# frozen_string_literal: true

module OptParseValidator
  # Base Option
  # This Option should not be called, children should be used.
  class OptBase
    attr_writer :required
    attr_reader :option, :attrs

    # @param [ Array ] option See OptionParser#on
    # @param [ Hash ] attrs
    # @option attrs  [ Boolean ] :required
    # @options attrs [ Array<Symbol>, Symbol ] :required_unless
    # @option attrs  [ Mixed ] :default The default value to use if the option is not supplied
    # @option attrs  [ Mixed ] :value_if_empty The value to use if no argument has been supplied
    # @option attrs  [ Array<Symbol> ] :normalize See #normalize
    #
    # @note The :default and :normalize 'logics' are done in OptParseValidator::OptParser#add_option
    def initialize(option, attrs = {})
      @option = option
      @attrs  = attrs

      # TODO: incompatible attributes, ie required and require_unless at the same time

      append_help_messages
    end

    # @return [ Void ]
    def append_help_messages
      option << "Default: #{help_message_for_default}" if default
      option << "Value if no argument supplied: #{value_if_empty}" if value_if_empty
      option << 'This option is mandatory' if required?
      option << "This option is mandatory unless #{required_unless.join(' or ')} is/are supplied" unless required_unless.empty?
    end

    def help_message_for_default
      default.to_s
    end

    # @return [ Boolean ]
    def required?
      @required ||= attrs[:required]
    end

    def required_unless
      @required_unless ||= [*attrs[:required_unless]]
    end

    # @return [ Mixed ]
    def default
      attrs[:default]
    end

    # @return [ Array<Mixed> ]
    def choices
      attrs[:choices]
    end

    # @return [ Mixed ]
    def value_if_empty
      attrs[:value_if_empty]
    end

    # @return [ Boolean ]
    def alias?
      false
    end

    # @return [ Boolean ]
    def advanced?
      attrs[:advanced] ? true : false
    end

    # @param [ String ] value
    def validate(value)
      if value.nil? || value.to_s.empty?
        raise Error, 'Empty option value supplied' if value_if_empty.nil?

        return value_if_empty
      end
      value
    end

    # Apply each methods from attrs[:normalize] to the value if possible
    # User input should not be used in this attrs[:normalize]
    #
    # e.g: normalize: :to_sym will return the symbol of the value
    #      normalize: [:to_sym, :upcase] Will return the upercased symbol
    #
    # @param [ Mixed ] value
    #
    # @return [ Mixed ]
    def normalize(value)
      [*attrs[:normalize]].each do |method|
        next unless method.is_a?(Symbol)

        value = value.send(method) if value.respond_to?(method)
      end

      value
    end

    # @return [ Symbol ]
    def to_sym
      unless @symbol
        long_option = to_long

        raise Error, "Could not find option symbol for #{option}" unless long_option

        @symbol = long_option.gsub(/^--/, '').tr('-', '_').to_sym
      end
      @symbol
    end

    # @return [ String ] The raw long option (e.g: --proxy)
    def to_long
      option.each do |option_attr|
        if /^--/.match?(option_attr)
          return option_attr.gsub(/ .*$/, '')
                            .gsub(/\[[^\]]+\]/, '')
        end
      end
      nil
    end

    # @return [ String ]
    def to_s
      to_sym.to_s
    end

    # @return [ Array<String> ]
    def help_messages
      first_message_index = option.index { |e| e[0] != '-' }

      return [] unless first_message_index

      option[first_message_index..-1]
    end
  end
end


# frozen_string_literal: true

# Gems
require 'addressable/uri'
require 'active_support/inflector'
require 'active_support/core_ext/hash'
# Standard Libs
require 'optparse'
require 'pathname'
# Custom Libs
require 'opt_parse_validator/errors'
require 'opt_parse_validator/hacks'
require 'opt_parse_validator/opts'
require 'opt_parse_validator/version'
require 'opt_parse_validator/config_files_loader_merger' # Could even create a gem out of it, as completely independent

# Gem namespace
module OptParseValidator
  # Validator
  class OptParser < OptionParser
    attr_reader :symbols_used, :opts

    def initialize(banner = nil, width = 32, indent = ' ' * 4)
      @results      = {}
      @symbols_used = []
      @opts         = []

      super(banner, width, indent)
    end

    # @return [ OptParseValidator::ConfigFilesLoaderMerger ]
    def options_files
      @options_files ||= ConfigFilesLoaderMerger.new
    end

    # @param [ Array<OptBase> ] options
    #
    # @return [ Self ] For chaining #new.add.results
    def add(*options)
      options.each do |option|
        check_option(option)

        @opts << option
        @symbols_used << option.to_sym

        # Set the default option value if it exists
        # The default value is not validated as it is provided by devs
        # and should be set to the correct format/value directly
        @results[option.to_sym] = option.default unless option.default.nil?

        register_callback(option)
      end

      self
    end

    # @return [ Hash ]
    def results(argv = default_argv)
      load_options_files
      parse!(argv)
      post_processing

      @results
    rescue StandardError => e
      # Raise it as an OptParseValidator::Error if not already one
      raise e.is_a?(Error) ? e.class : Error, e.message
    end

    # @return [ String ] The simplified help (without any of the advanced option/s listed)
    def simple_help
      help = to_s

      # Removes all advanced help messages
      @opts.select(&:advanced?).each do |opt|
        messages_pattern = //

        opt.help_messages.each do |msg|
          messages_pattern = /#{messages_pattern}\s*#{Regexp.escape(msg)}/
        end

        pattern = /\s*#{Regexp.escape(opt.option[0..1].select { |o| o[0] == '-' }.join(', '))}#{messages_pattern}/

        help.gsub!(pattern, '')
      end

      help
    end

    # @return [ String ] The full help, with the advanced option/s listed
    def full_help
      to_s
    end

    protected

    # Ensures the opt given is valid
    #
    # @param [ OptBase ] opt
    #
    # @return [ void ]
    def check_option(opt)
      raise Error, "The option is not an OptBase, #{opt.class} supplied" unless opt.is_a?(OptBase)
      raise Error, "The option #{opt.to_sym} is already used !" if @symbols_used.include?(opt.to_sym)
    end

    # @param [ OptBase ] opt
    #
    # @return [ void ]
    def register_callback(opt)
      on(*opt.option) do |arg|
        begin
          if opt.alias?
            parse!(opt.alias_for.split(' '))
          else
            @results[opt.to_sym] = opt.normalize(opt.validate(arg))
          end
        rescue StandardError => e
          # Adds the long option name to the message
          # And raises it as an OptParseValidator::Error if not already one
          # e.g --proxy Invalid Scheme format.
          raise e.is_a?(Error) ? e.class : Error, "#{opt.to_long} #{e}"
        end
      end
    end

    # @return [ Void ]
    def load_options_files
      files_data = options_files.parse(symbolize_keys: true)

      @opts.each do |opt|
        next unless files_data.key?(opt.to_sym)

        @results[opt.to_sym] = opt.normalize(opt.validate(files_data[opt.to_sym].to_s))
      end
    end

    # Ensure that all required options are supplied
    # Should be overriden to modify the behavior
    #
    # @return [ Void ]
    def post_processing
      @opts.each do |opt|
        raise NoRequiredOption, "The option #{opt} is required" if opt.required? && !@results.key?(opt.to_sym)

        next if opt.required_unless.empty? || @results.key?(opt.to_sym)

        fail_msg = "One of the following options is required: #{opt}, #{opt.required_unless.join(', ')}"

        raise NoRequiredOption, fail_msg unless opt.required_unless.any? do |sym|
          @results.key?(sym)
        end
      end
    end
  end
end


# frozen_string_literal: true

module OptParseValidator
  # Implementation of the Headers Option
  class OptHeaders < OptBase
    # @return [ Void ]
    def append_help_messages
      super

      option << "Separator to use between the headers: '; '"
      option << "Examples: 'X-Forwarded-For: 127.0.0.1', 'X-Forwarded-For: 127.0.0.1; Another: aaa'"
    end

    # @param [ String ] value
    #
    # @return [ Hash ] The parsed headers in a hash, with { 'key' => 'value' } format
    def validate(value)
      values = super(value).chomp(';').split('; ')

      headers = {}

      values.each do |header|
        raise Error, "Malformed header: '#{header}'" unless header.index(':')

        val = header.split(':', 2)

        headers[val[0].strip] = val[1].strip
      end

      headers
    end
  end
end


# frozen_string_literal: true

module OptParseValidator
  # Implementation of the URI Option
  class OptURI < OptBase
    # return [ Void ]
    def append_help_messages
      option << "Allowed Protocols: #{allowed_protocols.join(', ')}" unless allowed_protocols.empty?
      option << "Default Protocol if none provided: #{default_protocol}" if default_protocol

      super
    end

    # @return [ Array<String> ]
    def allowed_protocols
      @allowed_protocols ||= [*attrs[:protocols]].map(&:downcase)
    end

    # The default protocol (or scheme) to use if none was given
    def default_protocol
      @default_protocol ||= attrs[:default_protocol]&.downcase
    end

    # @param [ String ] value
    #
    # @return [ String ]
    def validate(value)
      uri = Addressable::URI.parse(value)

      uri = Addressable::URI.parse("#{default_protocol}://#{value}") if !uri.scheme && default_protocol

      unless allowed_protocols.empty? || allowed_protocols.include?(uri.scheme&.downcase)
        # For future refs: will have to check if the uri.scheme exists,
        # otherwise it means that the value was empty
        raise Addressable::URI::InvalidURIError
      end

      uri.to_s
    end
  end
end