# Experimental

require "cgi"
require "svn/ra"

module Svn
  class Synchronizer
    class Error < StandardError
    end

    class NotInitialized < Error
      attr_reader :destination_url
      def initialize(destination_url)
        @destination_url = destination_url
        super("#{@destination_url} is not initialized yet.")
      end
    end

    class ConsistencyError < Error
      attr_reader :destination_url, :destination_latest_revision, :next_revision
      def initialize(destination_url, destination_latest_revision, next_revision)
        @destination_url = destination_url
        @destination_latest_revision = destination_latest_revision
        @next_revision = next_revision
        super("the latest revision (#{@destination_latest_revision}) of " +
              "the destination repository (#{@destination_url}) " +
              "should be #{@next_revision - 1}.")
      end
    end

    module Property
      Svn::Ext::Core.constants.each do |name|
        case name.to_s
        when /\ASVNSYNC_PROP_/
          const_set($POSTMATCH, Svn::Ext::Core.const_get(name))
        end
      end
    end

    def initialize(destination_url, callbacks=nil)
      @destination = Ra::Session.open(destination_url, nil, callbacks)
      if @destination.repos_root != destination_url
        raise ArgumentError,
              "URL '#{destination_url}' is not repository root " +
              "(#{@destination.repos_root})."
      end
    end

    def source=(source)
      @destination[Property::FROM_URL, 0] = source.repos_root
      @destination[Property::FROM_UUID, 0] = source.uuid
      @destination[Property::LAST_MERGED_REV, 0] = "0"

      sync_property_re = /\A#{Property::PREFIX}/
      source.properties(0).each do |key, value|
        next if sync_property_re =~ key
        @destination[key, 0] = value
      end
    end

    def sync(callbacks=nil)
      last_merged_rev = @destination[Property::LAST_MERGED_REV, 0]
      raise NotInitialized.new(@destination.repos_root) if last_merged_rev.nil?

      last_merged_rev = Integer(last_merged_rev)
      source = Ra::Session.open(@destination[Property::FROM_URL, 0], nil,
                                callbacks)
      (last_merged_rev + 1).upto(source.latest_revision) do |revision|
        unless @destination.latest_revision + 1 == revision
          raise ConsistencyError.new(@destination.repos_root,
                                     @destination.latest_revision,
                                     revision)
        end
        editor = sync_editor(source, revision)
        source.replay(revision, 0, editor)
        editor.close_edit
        sync_svn_revision_properties(source, revision)
        @destination[Property::LAST_MERGED_REV, 0] = revision.to_s
      end
    end

    private
    def sync_editor(source, base_revision)
      properties = source.properties(base_revision)
      commit_editor = @destination.commit_editor2(properties)
      Editor.new(commit_editor, @destination.repos_root, base_revision)
    end

    def sync_svn_revision_properties(source, revision)
      [Core::PROP_REVISION_AUTHOR, Core::PROP_REVISION_DATE].each do |key|
        @destination[key, revision] = source[key, revision]
      end
    end

    class Editor < Delta::WrapEditor
      def initialize(wrapped_editor, dest_url, base_revision)
        super(wrapped_editor)
        @dest_url = dest_url
        @base_revision = base_revision
        @opened_root = false
      end

      def open_root(base_revision)
        @opened_root = true
        super
      end

      def add_directory(path, parent_baton, copy_from_path, copy_from_rev)
        if copy_from_path
          copy_from_path = "#{@dest_url}#{CGI.escape(copy_from_path)}"
        end
        super(path, parent_baton, copy_from_path, copy_from_rev)
      end

      def add_file(path, parent_baton, copy_from_path, copy_from_rev)
        if copy_from_path
          copy_from_path = "#{@dest_url}#{CGI.escape(copy_from_path)}"
        end
        super(path, parent_baton, copy_from_path, copy_from_rev)
      end

      def close_edit
        unless @opened_root
          baton = @wrapped_editor.open_root(@base_revision)
          @wrapped_editor.close_directory(baton)
        end
        super
      end
    end
  end
end
