require 'dandelion/git'

module Dandelion
  module Deployment
    class RemoteRevisionError < StandardError; end
    class FastForwardError < StandardError; end

    class Deployment
      class << self
        def create(repo, backend, options)
          begin
            DiffDeployment.new(repo, backend, options)
          rescue RemoteRevisionError
            FullDeployment.new(repo, backend, options)
          end
        end
      end

      def initialize(repo, backend, options = {})
        @repo = repo
        @backend = backend
        @options = { :exclude => [], :additional => [], :revision => 'HEAD', :revision_file => '.revision', :local_path => '' }.merge(options)
        @tree = Git::Tree.new(@repo, @options[:revision], @options[:local_path])

        if @options[:dry]
          # Stub out the destructive backend methods
          def @backend.write(file, data); end
          def @backend.delete(file); end
        end
      end

      def local_revision
        @tree.revision
      end

      def remote_revision
        nil
      end

      def write_revision
        @backend.write(@options[:revision_file], local_revision)
      end

      def validate
        begin
          raise FastForwardError if fast_forwardable
        rescue Grit::Git::CommandFailed
        end
      end

      def log
        Dandelion.logger
      end

      def deploy_additional
        if @options[:additional].nil? || @options[:additional].empty?
          log.debug("No additional files to deploy")
          return
        end

        @options[:additional].each do |file|
          log.debug("Uploading additional file: #{file}")
          @backend.write(file, IO.read(file))
        end
      end

      protected

      def exclude_file?(file)
        @options[:exclude].map { |e| file.start_with?(e) }.any? unless @options[:exclude].nil?
      end

      private

      def fast_forwardable
        !@repo.git.native(:cherry, {:raise => true, :timeout => false}).empty?
      end
    end

    class DiffDeployment < Deployment
      def initialize(repo, backend, options = {})
        super(repo, backend, options)
        @diff = Git::Diff.new(@repo, read_remote_revision, @options[:revision], @options[:local_path])
      end

      def remote_revision
        @diff.from_revision
      end

      def deploy
        if !revisions_match? && any?
          deploy_changed
          deploy_deleted
        else
          log.debug("No changes to deploy")
        end

        deploy_additional

        unless revisions_match?
          write_revision
        end
      end

      def deploy_changed
        @diff.changed.each do |file|
          if exclude_file?(file)
            log.debug("Skipping file: #{file}")
          else
            if data = @tree.show(file)
              log.debug("Uploading file: #{file}")
              @backend.write(file, data)
            end
          end
        end
      end

      def deploy_deleted
        @diff.deleted.each do |file|
          if exclude_file?(file)
            log.debug("Skipping file: #{file}")
          else
            log.debug("Deleting file: #{file}")
            @backend.delete(file)
          end
        end
      end

      def any?
        @diff.changed.any? || @diff.deleted.any?
      end

      def revisions_match?
        remote_revision == local_revision
      end

      private

      def read_remote_revision
        begin
          @backend.read(@options[:revision_file]).chomp
        rescue Backend::MissingFileError
          raise RemoteRevisionError
        end
      end
    end

    class FullDeployment < Deployment
      def deploy
        @tree.files.each do |file|
          if exclude_file?(file)
            log.debug("Skipping file: #{file}")
          else
            if data = @tree.show(file)
              log.debug("Uploading file: #{file}")
              @backend.write(file, data)
            end
          end
        end

        deploy_additional
        write_revision
      end
    end
  end
end