#!/usr/bin/env ruby

$LOAD_PATH[0, 0] = File.join(File.dirname(__FILE__), '..', 'lib')

require 'linguist'
require 'rugged'
require 'optparse'
require 'json'
require 'tempfile'
require 'zlib'

class GitLinguist
  def initialize(path, commit_oid, incremental = true)
    @repo_path = path
    @commit_oid = commit_oid
    @incremental = incremental
  end

  def linguist
    if @commit_oid.nil?
      raise "git-linguist must be called with a specific commit OID to perform language computation"
    end
    repo = Linguist::Repository.new(rugged, @commit_oid)

    if @incremental && stats = load_language_stats
      old_commit_oid, old_stats = stats

      # A cache with NULL oid means that we want to freeze
      # these language stats in place and stop computing
      # them (for performance reasons)
      return old_stats if old_commit_oid == NULL_OID
      repo.load_existing_stats(old_commit_oid, old_stats)
    end

    result = yield repo

    save_language_stats(@commit_oid, repo.cache)
    result
  end

  def load_language_stats
    version, oid, stats = load_cache
    if version == LANGUAGE_STATS_CACHE_VERSION && oid && stats
      [oid, stats]
    end
  end

  def save_language_stats(oid, stats)
    cache = [LANGUAGE_STATS_CACHE_VERSION, oid, stats]
    write_cache(cache)
  end

  def clear_language_stats
    File.unlink(cache_file)
  rescue Errno::ENOENT
  end

  def disable_language_stats
    save_language_stats(NULL_OID, {})
  end

  protected
  NULL_OID = ("0" * 40).freeze

  LANGUAGE_STATS_CACHE = 'language-stats.cache'
  LANGUAGE_STATS_CACHE_VERSION = "v3:#{Linguist::VERSION}"

  def rugged
    @rugged ||= Rugged::Repository.bare(@repo_path)
  end

  def cache_file
    File.join(@repo_path, LANGUAGE_STATS_CACHE)
  end

  def write_cache(object)
    return unless File.directory? @repo_path

    Tempfile.open('cache_file', @repo_path) do |f|
      marshal = Marshal.dump(object)
      f.write(Zlib::Deflate.deflate(marshal))
      f.close
      File.rename(f.path, cache_file)
    end

    FileUtils.chmod 0644, cache_file
  end

  def load_cache
    marshal = File.open(cache_file, "rb") { |f| Zlib::Inflate.inflate(f.read) }
    Marshal.load(marshal)
  rescue SystemCallError, ::Zlib::DataError, ::Zlib::BufError, TypeError
    nil
  end
end


def git_linguist(args)
  incremental = true
  commit = nil

  parser = OptionParser.new do |opts|
    opts.banner = <<~HELP
      Linguist v#{Linguist::VERSION}
      Detect language type and determine language breakdown for a given Git repository.

      Usage:
      git-linguist [OPTIONS] stats|breakdown|dump-cache|clear|disable
    HELP

    opts.on("-f", "--force", "Force a full rescan") { incremental = false }
    opts.on("-c", "--commit=COMMIT", "Commit to index") { |v| commit = v}
  end

  parser.parse!(args)
  git_dir = `git rev-parse --git-dir`.strip
  raise "git-linguist must be run in a Git repository" unless $?.success?
  wrapper = GitLinguist.new(git_dir, commit, incremental)

  case args.pop
  when "stats"
    wrapper.linguist do |linguist|
      puts JSON.dump(linguist.languages)
    end
  when "breakdown"
    wrapper.linguist do |linguist|
      puts JSON.dump(linguist.breakdown_by_file)
    end
  when "dump-cache"
    puts JSON.dump(wrapper.load_language_stats)
  when "clear"
    wrapper.clear_language_stats
  when "disable"
    wrapper.disable_language_stats
  else
    $stderr.print(parser.help)
    exit 1
  end
rescue SystemExit
  exit 1
rescue Exception => e
  $stderr.puts e.message
  $stderr.puts e.backtrace
  exit 1
end

git_linguist(ARGV)
