require 'fileutils'
require 'pathname'
require 'digest'

require 'librarian/error'
require 'librarian/source/basic_api'
require 'librarian/source/git/repository'
require 'librarian/source/local'

module Librarian
  module Source
    class Git
      include BasicApi
      include Local

      lock_name 'GIT'
      spec_options [:ref, :branch, :tag, :commit, :path]

      DEFAULTS = {
        :ref => 'master'
      }

      attr_accessor :environment
      private :environment=

      attr_accessor :uri, :ref, :branch, :tag, :commit, :sha, :path
      private :uri=, :ref=, :branch=, :tag=, :commit=, :sha=, :path=

      def initialize(environment, uri, options)
        validate_options(uri, options)

        self.environment = environment
        self.uri = uri
        self.ref = options[:ref] || options[:branch] ||
                   options[:tag] || options[:commit] ||
                   DEFAULTS[:ref]
        self.sha = options[:sha]
        self.path = options[:path]

        @repository = nil
        @repository_cache_path = nil

        ref.kind_of?(String) or raise TypeError, "ref must be a String"
      end

      def to_s
        path ? "#{uri}##{ref}(#{path})" : "#{uri}##{ref}"
      end

      def ==(other)
        other &&
        self.class  == other.class  &&
        self.uri    == other.uri    &&
        self.ref    == other.ref    &&
        self.path   == other.path   &&
        (self.sha.nil? || other.sha.nil? || self.sha == other.sha)
      end

      alias :eql? :==

      def hash
        self.to_s.hash
      end

      def to_spec_args
        options = {}
        options.merge!(:ref => ref) if ref != DEFAULTS[:ref]
        options.merge!(:path => path) if path
        [uri, options]
      end

      def to_lock_options
        options = {:remote => uri, :ref => ref, :sha => sha}
        options.merge!(:path => path) if path
        options
      end

      def pinned?
        !!sha
      end

      def unpin!
        @sha = nil
      end

      def cache!
        repository_cached? and return or repository_cached!

        unless repository.git?
          repository.path.rmtree if repository.path.exist?
          repository.path.mkpath
          repository.clone!(uri)
          raise Error, "failed to clone #{uri}" unless repository.git?
        end

        # Probably unnecessary: nobody should be writing to our cache but us.
        # Just a precaution.
        repository_clean_once!

        unless sha
          repository_update_once!
          self.sha = fetch_sha_memo
        end

        unless repository.checked_out?(sha)
          repository_update_once! unless repository.has_commit?(sha)
          repository.checkout!(sha)
          # Probably unnecessary: if git fails to checkout, it should exit
          # nonzero, and we should expect Librarian::Posix::CommandFailure.
          raise Error, "failed to checkout #{sha}" unless repository.checked_out?(sha)
        end
      end

      # For tests
      def git_ops_count
        repository.git_ops_history.size
      end

    private

      attr_accessor :repository_cached
      alias repository_cached? repository_cached

      def repository_cached!
        self.repository_cached = true
      end

      def repository_cache_path
        @repository_cache_path ||= begin
          environment.cache_path + "source/git" + cache_key
        end
      end

      def repository
        @repository ||= begin
          Repository.new(environment, repository_cache_path)
        end
      end

      def filesystem_path
        @filesystem_path ||= path ? repository.path.join(path) : repository.path
      end

      def repository_clean_once!
        remote = repository.default_remote
        runtime_cache.once ['repository-clean', uri, ref].to_s do
          repository.reset_hard!
          repository.clean!
        end
      end

      def repository_update_once!
        remote = repository.default_remote
        runtime_cache.once ['repository-update', uri, remote, ref].to_s do
          repository.fetch! remote
          repository.fetch! remote, :tags => true
        end
      end

      def fetch_sha_memo
        remote = repository.default_remote
        runtime_cache.memo ['fetch-sha', uri, remote, ref].to_s do
          repository.hash_from(remote, ref)
        end
      end

      def cache_key
        @cache_key ||= begin
          uri_part = uri
          ref_part = "##{ref}"
          key_source = [uri_part, ref_part].join
          Digest::MD5.hexdigest(key_source)[0..15]
        end
      end

      def runtime_cache
        @runtime_cache ||= environment.runtime_cache.keyspace(self.class.name)
      end

      def validate_options(uri, options)
        found = 0; [:ref, :branch, :tag, :commit].each do |opt|
          found += 1 if options.has_key?(opt)
          found > 1 and raise Error,
            "at #{uri}, use only one of ref, branch, tag, or commit"
        end
      end
    end
  end
end
