require 'diffy'
require 'rubygems/comparator/base'
require 'rubygems/comparator/dir_utils'
require 'rubygems/comparator/monitor'

class Gem::Comparator

  ##
  # Gem::Comparator::FileListComparator can
  # compare file lists from gem's specs
  # based on the given Gem::Package objects
  #
  # To compare the files it needs to extract
  # gem packages to +options[:output]+

  class FileListComparator < Gem::Comparator::Base

    def initialize(ignore_group_writable=true)
      expect(:packages)

      # We need diff
      begin
        IO.popen('diff --version')
      rescue Exception
        error('Calling `diff` command failed. Do you have it installed?')
      end

      @ignore_group_writable = ignore_group_writable
    end

    ##
    # Compare file lists for gem's Gem::Package objects
    # in +packages+ and writes the changes to the +report+
    #
    # If +options[:param]+ is set, it compares only
    # that file list

    def compare(packages, report, options = {})
      info 'Checking file lists...'

      @packages = packages

      # Check file lists from older versions to newer
      filter_params(SPEC_FILES_PARAMS, options[:param]).each do |param|
        all_same = true

        packages.each_with_index do |pkg, index|
          unpacked_gem_dirs[packages[index].spec.version] = extract_gem(pkg, options[:output])
          next if index == 0

          # File lists as arrays
          previous = value_from_spec(param, packages[index-1].spec)
          current = value_from_spec(param, pkg.spec)
          next unless (previous && current)

          vers = "#{packages[index-1].spec.version}->#{packages[index].spec.version}"

          deleted = previous - current
          added = current - previous
          same = current - added

          if options[:brief]
            deleted, dirs_added = dir_changed(previous, current)
          end

          report[param].set_header "#{different} #{param}:"

          report[param][vers].section do
            set_header "#{Rainbow(packages[index-1].spec.version).cyan}->" +
                       "#{Rainbow(packages[index].spec.version).cyan}:"
            nest('deleted').section do
              set_header '* Deleted:'
              puts deleted unless deleted.empty?
            end

            nest('added').section do
              set_header '* Added:'
              puts dirs_added if options[:brief]
            end
          end
          # Add information about permissions, shebangs etc.
          report = check_added_files(param, vers, index, added, report, options[:brief], options[:diff])

          report[param][vers]['changed'].set_header '* Changed:'
          report = check_same_files(param, vers, index, same, report, options[:brief], options[:diff])
          same_files = report[param][vers]['changed'].messages.empty?
          all_same = false unless same_files

          if previous == current && same_files && !all_same
            report[param][vers] << "#{Rainbow(packages[index-1].spec.version).cyan}->" + \
                                   "#{Rainbow(packages[index].spec.version).cyan}: No change"
          end

        end

        if all_same && options[:log_all]
          report[param].set_header "#{same} #{param}:"
          value = value_from_spec(param, @packages[0].spec)
          value = '[]' if value.empty?
          report[param] << value
        end
      end
      report
    end

    private

      ##
      # Access @unpacked_gem_dirs hash that stores
      # paths to the unpacked gem dirs
      #
      # Keys of the hash are gem's versions

      def unpacked_gem_dirs
        @unpacked_gem_dirs ||= {}
      end

      ##
      # This returns [deleted, added] directories between
      # +previous+ and +current+ file lists
      #
      # For top level (.) it compares files themselves

      def dir_changed(previous, current)
        prev_dirs = DirUtils.dirs_of_files(previous)
        curr_dirs = DirUtils.dirs_of_files(current)
        deleted = DirUtils.remove_subdirs(prev_dirs - curr_dirs)
        added = DirUtils.remove_subdirs(curr_dirs - prev_dirs)
        [deleted, added]
      end

      def check_added_files(param, vers, index, files, report, brief_mode, diff_mode)
        files.each do |file|
          added_file = File.join(unpacked_gem_dirs[@packages[index].spec.version], file)

          line_changes = if diff_mode
            Monitor.files_diff(nil, added_file)
          else
            Monitor.lines_changed(nil, added_file)
          end

          changes = Monitor.new_file_permissions(added_file, @ignore_group_writable),
                    Monitor.new_file_executability(added_file),
                    Monitor.new_file_shebang(added_file)

          if(!changes.join.empty? || (!brief_mode && !line_changes.empty?))
            if diff_mode
              report[param][vers]['added'][file].set_header file
              report[param][vers]['added'][file] << line_changes.split("\n")
            else
              report[param][vers]['added'] << "#{file} #{line_changes}"
            end
          end

          changes.each do |change|
            report[param][vers]['added'] << change unless change.empty?
          end
        end
        report
      end

      def check_same_files(param, vers, index, files, report, brief_mode, diff_mode)
        files.each do |file|
          prev_file = File.join(unpacked_gem_dirs[@packages[index-1].spec.version], file)
          curr_file = File.join(unpacked_gem_dirs[@packages[index].spec.version], file)

          next unless check_files([prev_file, curr_file])

          line_changes = if diff_mode
            Monitor.files_diff(prev_file, curr_file)
          else
            Monitor.lines_changed(prev_file, curr_file)
          end

          changes = Monitor.files_permissions_changes(prev_file, curr_file, @ignore_group_writable),
                    Monitor.files_executability_changes(prev_file, curr_file),
                    Monitor.files_shebang_changes(prev_file, curr_file)

          if(!changes.join.empty? || (!brief_mode && !line_changes.empty?))
            if diff_mode
              report[param][vers]['changed'][file].set_header file
              report[param][vers]['changed'][file] << line_changes.split("\n")
            else
              report[param][vers]['changed'] << "#{file} #{line_changes}"
            end
          end

          changes.each do |change|
            report[param][vers]['changed'] << change unless change.empty?
          end
        end
        report
      end

      ##
      # Check that files exist

      def check_files(files)
        files.each do |file|
          unless File.exist? file
            warn "#{file} mentioned in spec does not exist " +
                 "in the gem package, skipping check"
            return false
          end
        end
        true
      end

  end
end
