module Git
  #
  # A class for git status
  #
  class Status
    include Enumerable

    def initialize(base)
      @base = base
      construct_status
    end

    #
    # Returns an Enumerable containing files that have changed from the
    # git base directory
    #
    # @return [Enumerable]
    def changed
      @files.select { |_k, f| f.type == 'M' }
    end

    #
    # Determines whether the given file has been changed.
    # File path starts at git base directory
    #
    # @param file [String] The name of the file.
    # @example Check if lib/git.rb has changed.
    #     changed?('lib/git.rb')
    # @return [Boolean]
    def changed?(file)
      changed.member?(file)
    end

    #
    # Returns an Enumerable containing files that have been added.
    # File path starts at git base directory
    #
    # @return [Enumerable]
    def added
      @files.select { |_k, f| f.type == 'A' }
    end

    #
    # Determines whether the given file has been added to the repository
    # File path starts at git base directory
    #
    # @param file [String] The name of the file.
    # @example Check if lib/git.rb is added.
    #     added?('lib/git.rb')
    # @return [Boolean]
    def added?(file)
      added.member?(file)
    end

    #
    # Returns an Enumerable containing files that have been deleted.
    # File path starts at git base directory
    #
    # @return [Enumerable]
    def deleted
      @files.select { |_k, f| f.type == 'D' }
    end

    #
    # Determines whether the given file has been deleted from the repository
    # File path starts at git base directory
    #
    # @param file [String] The name of the file.
    # @example Check if lib/git.rb is deleted.
    #     deleted?('lib/git.rb')
    # @return [Boolean]
    def deleted?(file)
      deleted.member?(file)
    end

    #
    # Returns an Enumerable containing files that are not tracked in git.
    # File path starts at git base directory
    #
    # @return [Enumerable]
    def untracked
      @files.select { |_k, f| f.untracked }
    end

    #
    # Determines whether the given file has is tracked by git.
    # File path starts at git base directory
    #
    # @param file [String] The name of the file.
    # @example Check if lib/git.rb is an untracked file.
    #     untracked?('lib/git.rb')
    # @return [Boolean]
    def untracked?(file)
      untracked.member?(file)
    end

    def pretty
      out = ''
      each do |file|
        out << pretty_file(file)
      end
      out << "\n"
      out
    end

    def pretty_file(file)
      <<~FILE
        #{file.path}
        \tsha(r) #{file.sha_repo} #{file.mode_repo}
        \tsha(i) #{file.sha_index} #{file.mode_index}
        \ttype   #{file.type}
        \tstage  #{file.stage}
        \tuntrac #{file.untracked}
      FILE
    end

    # enumerable method

    def [](file)
      @files[file]
    end

    def each(&block)
      @files.values.each(&block)
    end

    # subclass that does heavy lifting
    class StatusFile
      attr_accessor :path, :type, :stage, :untracked
      attr_accessor :mode_index, :mode_repo
      attr_accessor :sha_index, :sha_repo

      def initialize(base, hash)
        @base = base
        @path = hash[:path]
        @type = hash[:type]
        @stage = hash[:stage]
        @mode_index = hash[:mode_index]
        @mode_repo = hash[:mode_repo]
        @sha_index = hash[:sha_index]
        @sha_repo = hash[:sha_repo]
        @untracked = hash[:untracked]
      end

      def blob(type = :index)
        if type == :repo
          @base.object(@sha_repo)
        else
          begin
            @base.object(@sha_index)
          rescue
            @base.object(@sha_repo)
          end
        end
      end
    end

    private

    def construct_status
      @files = @base.lib.ls_files

      fetch_untracked
      fetch_modified
      fetch_added

      @files.each do |k, file_hash|
        @files[k] = StatusFile.new(@base, file_hash)
      end
    end

    def fetch_untracked
      ignore = @base.lib.ignored_files

      root_dir = @base.dir.path
      Dir.glob('**/*', File::FNM_DOTMATCH, base: root_dir) do |file|
        next if @files[file] || File.directory?(File.join(root_dir, file)) ||
                ignore.include?(file) || file =~ %r{^.git\/.+}

        @files[file] = { path: file, untracked: true }
      end
    end

    def fetch_modified
      # find modified in tree
      @base.lib.diff_files.each do |path, data|
        @files[path] ? @files[path].merge!(data) : @files[path] = data
      end
    end

    def fetch_added
      # find added but not committed - new files
      @base.lib.diff_index('HEAD').each do |path, data|
        @files[path] ? @files[path].merge!(data) : @files[path] = data
      end
    end
  end
end
