class CombinationGenerator < Generator
  def initialize(attack, use_evasions, ignore_evasions, depth)
    super(attack, use_evasions, ignore_evasions)
    @depth = depth
    @evasions, @all_options_by_evasion = get_evasions_and_options()

    i = 0
    @option_set_index = {}
    @evasion_index = {}

    good_starting_point = {}
    @evasions.each do |evasion|
      if not good_starting_point.has_key?(evasion)
        good_starting_point[evasion] = i
      end
      all_options = @all_options_by_evasion[evasion]
      all_options.each do |option_set|
        @option_set_index[i] = option_set
        @evasion_index[i] = evasion
        i += 1
      end
    end
    @options = @option_set_index.size

    values = []
    @depth.times do |j|
      values.push good_starting_point[@evasions[j]]
    end
    # jump ahead to the first valid
    @start_index = @index = get_index_from_values(values) - 1
    @max_index = (@options**@depth - 1)
  end

  def to_s
    return "Combination Generator (depth=#{@depth} options=#{@options} max_index=#{@max_index})"
  end

  def matrix(arrays, result = [], values = [])
    arrays.first.each do |value|
      if arrays.size > 1
        matrix(arrays[1..-1], result, values + [value])
      else
        result.push values + [value]
      end
    end
    return result
  end

  def get_unique_evasion_count_from_values(values)
    h = {}
    values.each do |index|
      h[@evasion_index[index]] = true
    end
    return h.size
  end

  def get_values_from_index(index)
    values = []
    (@depth-1).downto(0) do |j|
      val, index = index.divmod(@options**j)
      values.push val
    end
    return values
  end

  def get_index_from_values(values)
    index = 0
    @depth.times do |i|
      j = values.reverse[i]
      index += j * @options**i
    end
    return index
  end

  def generate(evindex)
    while true
      @index += 1
      if @index > @max_index # we are done
        return nil
      end

      values = get_values_from_index(@index)
      # they are ascending and non-equal and
      # they are from different evasions
      if values.uniq.sort == values and get_unique_evasion_count_from_values(values) == @depth
        valid = true
        configured_evasion = {}
        values.each do |index|
          evasion = @evasion_index[index]
          option_set = @option_set_index[index]
          option_hash = calculate_option_hash(evasion, option_set)
          if not valid_evasion?(evasion, option_hash)
            valid = false
            break
          else
            configured_evasion[evasion] = option_hash
          end
        end
        if valid
          if valid_combo?(configured_evasion)
            return configured_evasion
          end
        end
      end
    end
  end

  def get_evasions_and_options()
    all_options_by_evasion = {}
    evasions = []
    @attack.evasions.each do |evasion|
      next if @use_evasions.size > 0 and not @use_evasions.include?(evasion.name)
      next if @ignore_evasions.has_key?(evasion.name)
      evasions.push evasion
      arrays = []
      evasion.options.each do |option|
        arrays.push []
        option.get_enum_size.times do |i|
          arrays.last.push i
        end
      end
      if arrays.size == 0
        # this evasion has no options
        all_options = [true]
      else
        all_options = matrix(arrays)
      end
      all_options_by_evasion[evasion] = all_options
    end
    if @use_evasions.size == 0
      evasions.sort!{|x, y| all_options_by_evasion[x].size <=> all_options_by_evasion[y].size}
    else
      new_evasions = []
      @use_evasions_order.each do |evasion_name|
        evasions.each do |evasion|
          if evasion.name == evasion_name
            new_evasions.push evasion
          end
        end
      end
      evasions = new_evasions
    end
    return [evasions, all_options_by_evasion]
  end

  def calculate_option_hash(evasion, option_set)
    hash = {}
    evasion.options.size.times do |i|
      name = evasion.options[i].name
      index = option_set[i]
      value = evasion.options[i].get_value(index)
      hash[name] = value
    end
    return hash
  end

  def count
    i = 0
    time = 60
    begin
      timeout(time) do
        while generate(0) 
          i += 1
        end
      end
    rescue Timeout::Error
      puts "Could not calculate evasion space in #{time}sec"
      i = nil
    end
    # reset index
    @index = @start_index
    return i
  end
end
