require "rugged"
require "rfix/file"
require "rfix/file_cache"
require "rfix/untracked"
require "rfix/tracked"

class Rfix::Repository
  include Rfix::Log
  attr_reader :files, :repo

  def initialize(root_path:, load_untracked: false, reference: Rfix::Branch::HEAD, paths: [])
    unless File.exist?(root_path)
      raise Rfix::Error, "#{root_path} does not exist"
    end

    unless Pathname.new(root_path).absolute?
      raise Rfix::Error, "#{root_path} is not absolute"
    end

    unless reference.is_a?(Rfix::Branch::Base)
      raise Rfix::Error.new("Need Branch::Base, got {{error:#{reference.class}}}")
    end

    @files          = FileCache.new(root_path)
    @repo           = Rugged::Repository.new(root_path)
    @paths          = paths
    @reference      = reference
    @load_untracked = load_untracked

    load!
  end

  def load_untracked?
    @load_untracked
  end

  def load_tracked?
    !! @reference
  end

  def reference
    @reference
  end

  def refresh!(path)
    @files.get(path).refresh!
  end

  def include?(path, line)
    say_debug "Checking #{path}:#{line}"

    if file = @files.get(path)
      return file.include?(line)
    end

    say_debug "\tSkip file (return false)"
    return false
  end

  def set_root(_path_path)
    using_path(root_path)
  end

  def paths
    files.pluck(&:absolute_path)
  end

  def current_branch
    repo.head.name
  end

  def has_reference?(reference)
    repo.rev_parse(reference)
  rescue Rugged::ReferenceError
    return false
  end

  def local_branches
    repo.branches.each_name(:local).to_a
  end

  def git_path
    repo.workdir
  end

  def head
    @head ||= repo.rev_parse("HEAD")
  end

  def upstream
    @upstream ||= reference.resolve(with: repo)
  end

  private

  def load_tracked!
    params = {
      # ignore_whitespace_change: true,
      include_untracked_content: true,
      recurse_untracked_dirs: true,
      # ignore_whitespace_eol: true,
      include_unmodified: false,
      include_untracked: true,
      ignore_submodules: true,
      # ignore_whitespace: true,
      include_ignored: false,
      context_lines: 0
    }

    unless @paths.empty?
      say_debug("Use @paths #{@paths.join(", ")}")
      params[:disable_pathspec_match] = false
      params[:paths] = @paths
    end

    say_debug("Run diff on #{reference}")
    upstream.diff(head, **params).tap do |diff|
      diff.find_similar!(
        renames_from_rewrites: true,
        renames: true,
        copies: true
      )
    end.each_delta do |delta|
      path = delta.new_file.fetch(:path)
      say_debug("Found #{path} while diff")
      try_store(path, [delta.status])
    end
  rescue Rugged::ReferenceError
    abort_box($ERROR_INFO.to_s) do
      prt "Reference {{error:#{reference}}} cannot be found in repository"
    end
  rescue Rugged::ConfigError
    abort_box($ERROR_INFO.to_s) do
      prt "No upstream branch set for {{error:#{current_branch}}}"
    end
  rescue TypeError
    abort_box($ERROR_INFO.to_s) do
      prt "Reference {{error:#{reference}}} is not pointing to a tree or commit"
    end
  end

  def load!
    load_tracked!
    load_untracked!
  end

  # https://github.com/libgit2/rugged/blob/35102c0ca10ab87c4c4ffe2e25221d26993c069c/test/status_test.rb
  # - +:index_new+: the file is new in the index
  # - +:index_modified+: the file has been modified in the index
  # - +:index_deleted+: the file has been deleted from the index
  # - +:worktree_new+: the file is new in the working directory
  # - +:worktree_modified+: the file has been modified in the working directory
  # - +:worktree_deleted+: the file has been deleted from the working directory

  MODIFIED  = [:modified, :worktree_modified, :index_modified].freeze
  IGNORED   = [:ignored].freeze
  STAGED    = [:added, :index_new].freeze
  UNTRACKED = [:worktree_new, :untracked].freeze
  COPIED    = [:copied].freeze
  DELETED   = [:deleted, :worktree_deleted, :index_deleted].freeze
  RENAMED   = [:renamed].freeze

  SKIP = [*DELETED, *RENAMED, *COPIED, *IGNORED].freeze
  ACCEPT = [*MODIFIED].freeze

  def load_untracked!
    repo.status do |path, status|
      try_store(path, status)
    end
  end

  def store(file)
    say_debug("Trying to add #{file.absolute_path}")
    if File.exist?(file.absolute_path)
      @files.add(file)
    else
      say_debug "#{file} does not exist"
    end
  end

  def try_store(path, status)
    if SKIP.any?(&status.method(:include?))
      return say_debug("Ignored {{warning:#{status.join(', ')}}} #{path}")
    end

    if STAGED.any?(&status.method(:include?))
      return store(Rfix::Untracked.new(path, repo, nil))
    end

    if UNTRACKED.any?(&status.method(:include?))
      unless load_untracked?
        return say_debug("Ignore #{path} as untracked files are ignored: #{status}")
      end

      return store(Rfix::Untracked.new(path, repo, nil))
    end

    if ACCEPT.any?(&status.method(:include?))
      return store(Rfix::Tracked.new(path, repo, reference))
    end

    say_debug "Status not found {{error:#{status.join(', ')}}} for {{italic:#{path}}}"
  end
end