module DynportTools
  class Differ
    attr_accessor :diff_all, :use_return, :symbolize_keys
  
  
    def initialize(options = {})
      self.diff_all = options[:diff_all] != false
      self.use_return = options[:use_return] == true
    end
  
    def diff(a, b)
      if both?(a, b, Hash)
        diff_hash_values(a, b, a.keys + (self.diff_all ? b.keys : []))
      elsif both?(a, b, Array)
        diff_hash_values(a, b, all_array_indexes(a, b))
      else
        [a, b] if a != b
      end
    end
    
    def each_diff(the_diff, prefix = nil, &block)
      if the_diff.is_a?(Array)
        yield(prefix, the_diff.first, the_diff.at(1))
      elsif the_diff.is_a?(Hash)
        the_diff.each do |key, diff|
          if diff.is_a?(Array)
            yield([prefix, key].flatten.compact, diff.first, diff.at(1))
          else
            each_diff(diff, [prefix, key], &block)
          end
        end
      end
    end
  
    def diff_to_message_lines(the_diff)
      diffs = []
      each_diff(the_diff) do |keys, old_value, new_value|
        if keys.nil?
          diffs << "expected #{expected_value(old_value)} to be #{expected_value(new_value)}"
        else
          diffs << "expected #{keys_to_s(keys)} to #{use_return ? "return" : "be"} #{expected_value(old_value)} but #{use_return ? "did" : "was"} #{expected_value(new_value)}"
        end
      end
      diffs
    end
    
    def diff_strings(a, b)
      chunks = []
      last = 0
      Diff::LCS.diff(a, b).each do |group|
        old_s = []
        new_s = []
        removed_elements(group).each_with_index do |c, i|
          chunks << a[last..(c.position - 1)] if i == 0
          old_s << c.element 
          last = c.position + 1
        end
        added_elements(group).each_with_index do |c, i|
          if i == 0 && removed_elements(group).empty?
            chunks << a[last..(c.position - 1)]
            last = c.position
          end
          new_s << c.element
        end
        if (old_s.join("").length > 0 || new_s.join("").length > 0)
          chunks << Term::ANSIColor.bold("<#{Term::ANSIColor.red(old_s.join(""))}|#{Term::ANSIColor.green(new_s.join(""))}>")
        end
      end
      chunks.join("")
    end
  
  private
    def removed_elements(group)
      group.select { |c| c.action == "-" }
    end

    def added_elements(group)
      group.select { |c| c.action == "+" }
    end
    
    def expected_value(value)
      "<#{value.inspect}>"
    end
  
    def all_array_indexes(a, b)
      0.upto([a.length, b.length].max - 1).to_a
    end
  
    def diff_hash_values(a, b, keys)
      ret = keys.uniq.inject({}) do |hash, key|
        value_a = a[key]
        value_b = b[key]
        if symbolize_keys
          value_a ||= a[key.is_a?(Symbol) ? key.to_s : key.to_sym]
          value_b ||= b[key.is_a?(Symbol) ? key.to_s : key.to_sym]
        end
        if diff = diff(value_a, value_b)
          hash[key] = diff
        end
        hash
      end
      ret.empty? ? nil : ret
    end
  
    def both?(a, b, clazz)
      a.is_a?(clazz) && b.is_a?(clazz)
    end
    
    def keys_to_s(keys)
      keys.compact.map { |k| k.is_a?(Hash) ? k.inspect : k }.join("/")
    end
  
    def merge_prefixes(prefix, key)
      key_s = key.is_a?(Hash) ? key.inspect : key
      prefix ? "#{prefix}[#{key_s}]" : key_s
    end
  end
end