# frozen-string-literal: true

require 'pre-commit/configuration/top_level'

module PreCommit
  module Utils
    module StagedFiles
      include PreCommit::Configuration::TopLevel

      BINARIES = [
        ".dmg", ".gz", ".img", ".iso", ".rar", ".tar", ".xz", ".zip"
      ].freeze
      IMAGES = [".gif", ".ico", ".jpeg", ".jpg", ".pdf", ".png"].freeze
      IGNORED_EXTENSIONS = BINARIES + IMAGES

      SOURCE_FILES = [
        ".coffee", ".css", ".erb", ".feature", ".html", ".js", ".json",
        ".liquid", ".md", ".rb", ".sass", ".scss", ".slim", ".yml"
      ].freeze

      def set_staged_files(*args)
        case args[0].to_s
        when "all" then staged_files_all
        when "git" then staged_files_git_all
        when "" then staged_files
        else self.staged_files=args
        end
      end

      def staged_files=(args)
        @staged_files = args
      end

      def staged_files
        @staged_files ||= filter_files(staged_from_git)
      end

      def staged_files_all
        @staged_files = filter_files(staged_from_dir)
      end

      def staged_files_git_all
        @staged_files = filter_files(staged_from_git_all)
      end

      # Definitely include this file in the checks.
      def source_file?(filename)
        SOURCE_FILES.include?(File.extname(filename))
      end

      # Try to bail out quickly based on filename.
      #
      # If the extension is `.jpg` this is likely not a source code file.
      # So let's not waste time checking to see if it's "binary" (as best we
      # can guess) and let's not run any checks on it.
      def ignore_extension?(filename)
        IGNORED_EXTENSIONS.include?(File.extname(filename))
      end

      # Make a best guess to determine if this is a binary file.
      # This is not an exact science ;)
      def appears_binary?(filename)
        size = File.size(filename)
        size > 1_000_000 || (size > 20 && binary?(filename))
      end

      def repo_ignored?(filename)
        repo_ignores.any? { |ignore| File.fnmatch?(ignore, filename) }
      end

    private
      # from https://github.com/djberg96/ptools/blob/master/lib/ptools.rb#L90
      def binary?(file)
        bytes = File.stat(file).blksize.to_i
        bytes = 4096 if bytes > 4096
        s = (File.read(file, bytes) || "").split(//)

        ((s.size - s.grep(" ".."~").size) / s.size.to_f) > 0.30
      end

      def filter_files(files)
        first_pass = files.reject do |file|
          repo_ignored?(file) ||
          ignore_extension?(file) ||
          File.directory?(file) ||
          !File.exist?(file)
        end

        # If it's a source file, definitely check it.
        # Otherwise, attempt to guess if the file is binary or not.
        first_pass.select do |file|
          source_file?(file) || !appears_binary?(file)
        end
      end

      def staged_from_git
        `git diff --cached --name-only --diff-filter=ACM`.split(/\n/)
      end

      def staged_from_git_all
        `git ls-files`.split(/\n/)
      end

      def staged_from_dir
        Dir["**/*"]
      end

      def repo_ignores_file
        File.join(top_level, ".pre_commit.ignore")
      end

      def repo_ignores
        @repo_ignores ||= File.read(repo_ignores_file).split("\n").compact
      rescue Errno::ENOENT
        @repo_ignores = []
      end

    end
  end
end
