The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# ====================================================================
#    Licensed to the Apache Software Foundation (ASF) under one
#    or more contributor license agreements.  See the NOTICE file
#    distributed with this work for additional information
#    regarding copyright ownership.  The ASF licenses this file
#    to you under the Apache License, Version 2.0 (the
#    "License"); you may not use this file except in compliance
#    with the License.  You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing,
#    software distributed under the License is distributed on an
#    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
#    KIND, either express or implied.  See the License for the
#    specific language governing permissions and limitations
#    under the License.
# ====================================================================

require "English"
require "forwardable"

require "svn/error"
require "svn/util"
require "svn/core"
require "svn/ext/delta"

module Svn
  module Delta
    Util.set_constants(Ext::Delta, self)
    Util.set_methods(Ext::Delta, self)

    class << self
      alias _path_driver path_driver
    end
    alias _path_driver path_driver

    module_function
    def svndiff_handler(output, version=nil)
      args = [output, version || 0]
      handler, handler_baton = Delta.txdelta_to_svndiff2(*args)
      handler.baton = handler_baton
      handler
    end

    def read_svndiff_window(stream, version)
      Delta.txdelta_read_svndiff_window(stream, version)
    end

    def skip_svndiff_window(file, version)
      Delta.txdelta_skip_svndiff_window(file, version)
    end

    def path_driver(editor, revision, paths, &callback_func)
      Delta._path_driver(editor, revision, paths, callback_func)
    end

    def send(string_or_stream, handler=nil)
      if handler.nil? and block_given?
        handler = Proc.new {|window| yield(window)}
        Delta.setup_handler_wrapper(handler)
      end
      if string_or_stream.is_a?(TextDeltaStream)
        string_or_stream.send(handler)
      elsif string_or_stream.is_a?(String)
        Delta.txdelta_send_string(string_or_stream, handler)
      else
        Delta.txdelta_send_stream(string_or_stream, handler)
      end
    end

    def apply(source, target, error_info=nil)
      result = Delta.txdelta_apply_wrapper(source, target, error_info)
      handler, handler_baton = result
      handler.baton = handler_baton
      [handler,nil]
    end

    def parse_svndiff(error_on_early_close=true, &handler)
      Delta.txdelta_parse_svndiff(handler, error_on_early_close)
    end

    def setup_handler_wrapper(wrapper)
      Proc.new do |window|
        Delta.txdelta_invoke_window_handler_wrapper(wrapper, window)
      end
    end

    TextDeltaStream = SWIG::TYPE_p_svn_txdelta_stream_t

    class TextDeltaStream
      class << self
        def new(source, target)
          Delta.txdelta(source, target)
        end

        def push_target(source, &handler)
          Delta.txdelta_target_push(handler, source)
        end
      end

      def md5_digest
        Delta.txdelta_md5_digest_as_cstring(self)
      end

      def next_window
        Delta.txdelta_next_window(self, Svn::Core::Pool.new)
      end

      def each
        while window = next_window
          yield(window)
        end
      end

      def send(handler=nil)
        if handler.nil? and block_given?
          handler = Proc.new {|window| yield(window)}
          Delta.setup_handler_wrapper(handler)
        end
        Delta.txdelta_send_txstream(self, handler)
      end
    end

    TextDeltaWindow = TxdeltaWindow

    class TextDeltaWindow
      def compose(other_window)
        Delta.txdelta_compose_windows(other_window, self)
      end

      def ops
        Delta.txdelta_window_t_ops_get(self)
      end

      def apply_instructions(source_buffer)
        Delta.swig_rb_txdelta_apply_instructions(self, source_buffer)
      end
    end

    TextDeltaWindowHandler =
      SWIG::TYPE_p_f_p_svn_txdelta_window_t_p_void__p_svn_error_t

    class TextDeltaWindowHandler
      attr_accessor :baton

      def call(window)
        Delta.txdelta_invoke_window_handler(self, window, @baton)
      end

      def send(string_or_stream)
        if string_or_stream.is_a?(TextDeltaStream)
          Delta.txdelta_send_txstream(string_or_stream, self, @baton)
        elsif string_or_stream.is_a?(String)
          Delta.txdelta_send_string(string_or_stream, self, @baton)
        else
          Delta.txdelta_send_stream(string_or_stream, self, @baton)
        end
      end
    end

    class Editor

      %w(set_target_revision open_root delete_entry
         add_directory open_directory change_dir_prop
         close_directory absent_directory add_file open_file
         apply_textdelta change_file_prop close_file
         absent_file close_edit abort_edit).each do |name|
        alias_method("_#{name}", name)
        undef_method("#{name}=")
      end

      attr_accessor :baton
      attr_writer :target_revision_address
      private :target_revision_address=

      def target_revision
        Svn::Delta.swig_rb_delta_editor_get_target_revision(self)
      end

      def set_target_revision(target_revision)
        args = [self, @baton, target_revision]
        Svn::Delta.editor_invoke_set_target_revision(*args)
      end

      def open_root(base_revision)
        args = [self, @baton, base_revision]
        Svn::Delta.editor_invoke_open_root_wrapper(*args)
      end

      def delete_entry(path, revision, parent_baton)
        args = [self, path, revision, parent_baton]
        Svn::Delta.editor_invoke_delete_entry(*args)
      end

      def add_directory(path, parent_baton,
                        copyfrom_path, copyfrom_revision)
        args = [self, path, parent_baton, copyfrom_path, copyfrom_revision]
        Svn::Delta.editor_invoke_add_directory_wrapper(*args)
      end

      def open_directory(path, parent_baton, base_revision)
        args = [self, path, parent_baton, base_revision]
        Svn::Delta.editor_invoke_open_directory_wrapper(*args)
      end

      def change_dir_prop(dir_baton, name, value)
        args = [self, dir_baton, name, value]
        Svn::Delta.editor_invoke_change_dir_prop(*args)
      end

      def close_directory(dir_baton)
        args = [self, dir_baton]
        Svn::Delta.editor_invoke_close_directory(*args)
      end

      def absent_directory(path, parent_baton)
        args = [self, path, parent_baton]
        Svn::Delta.editor_invoke_absent_directory(*args)
      end

      def add_file(path, parent_baton,
                   copyfrom_path, copyfrom_revision)
        args = [self, path, parent_baton, copyfrom_path, copyfrom_revision]
        Svn::Delta.editor_invoke_add_file_wrapper(*args)
      end

      def open_file(path, parent_baton, base_revision)
        args = [self, path, parent_baton, base_revision]
        Svn::Delta.editor_invoke_open_file_wrapper(*args)
      end

      def apply_textdelta(file_baton, base_checksum)
        args = [self, file_baton, base_checksum]
        handler, handler_baton =
          Svn::Delta.editor_invoke_apply_textdelta_wrapper(*args)
        handler.baton = handler_baton
        handler
      end

      def change_file_prop(file_baton, name, value)
        args = [self, file_baton, name, value]
        Svn::Delta.editor_invoke_change_file_prop(*args)
      end

      def close_file(file_baton, text_checksum)
        args = [self, file_baton, text_checksum]
        Svn::Delta.editor_invoke_close_file(*args)
      end

      def absent_file(path, parent_baton)
        args = [self, path, parent_baton]
        Svn::Delta.editor_invoke_absent_file(*args)
      end

      def close_edit
        args = [self, @baton]
        Svn::Delta.editor_invoke_close_edit(*args)
      ensure
        Core::Pool.destroy(self)
      end

      def abort_edit
        args = [self, @baton]
        Svn::Delta.editor_invoke_abort_edit(*args)
      ensure
        Core::Pool.destroy(self)
      end
    end

    class BaseEditor
      # open_root -> add_directory -> open_directory -> add_file -> open_file
      def set_target_revision(target_revision)
      end

      def open_root(base_revision)
      end

      def delete_entry(path, revision, parent_baton)
      end

      def add_directory(path, parent_baton,
                        copyfrom_path, copyfrom_revision)
      end

      def open_directory(path, parent_baton, base_revision)
      end

      def change_dir_prop(dir_baton, name, value)
      end

      def close_directory(dir_baton)
      end

      def absent_directory(path, parent_baton)
      end

      def add_file(path, parent_baton,
                   copyfrom_path, copyfrom_revision)
      end

      def open_file(path, parent_baton, base_revision)
      end

      # return nil or object which has `call' method.
      def apply_textdelta(file_baton, base_checksum)
      end

      def change_file_prop(file_baton, name, value)
      end

      def close_file(file_baton, text_checksum)
      end

      def absent_file(path, parent_baton)
      end

      def close_edit(baton)
      end

      def abort_edit(baton)
      end
    end

    class CopyDetectableEditor < BaseEditor
      def add_directory(path, parent_baton,
                        copyfrom_path, copyfrom_revision)
      end

      def add_file(path, parent_baton,
                   copyfrom_path, copyfrom_revision)
      end
    end

    class ChangedDirsEditor < BaseEditor
      attr_reader :changed_dirs

      def initialize
        @changed_dirs = []
      end

      def open_root(base_revision)
        [true, '']
      end

      def delete_entry(path, revision, parent_baton)
        dir_changed(parent_baton)
      end

      def add_directory(path, parent_baton,
                        copyfrom_path, copyfrom_revision)
        dir_changed(parent_baton)
        [true, path]
      end

      def open_directory(path, parent_baton, base_revision)
        [true, path]
      end

      def change_dir_prop(dir_baton, name, value)
        dir_changed(dir_baton)
      end

      def add_file(path, parent_baton,
                   copyfrom_path, copyfrom_revision)
        dir_changed(parent_baton)
      end

      def open_file(path, parent_baton, base_revision)
        dir_changed(parent_baton)
      end

      def close_edit(baton)
        @changed_dirs.sort!
      end

      private
      def dir_changed(baton)
        if baton[0]
          # the directory hasn't been printed yet. do it.
          @changed_dirs << "#{baton[1]}/"
          baton[0] = nil
        end
      end
    end

    class ChangedEditor < BaseEditor

      attr_reader :copied_files, :copied_dirs
      attr_reader :added_files, :added_dirs
      attr_reader :deleted_files, :deleted_dirs
      attr_reader :updated_files, :updated_dirs

      def initialize(root, base_root)
        @root = root
        @base_root = base_root
        @in_copied_dir = []
        @copied_files = []
        @copied_dirs = []
        @added_files = []
        @added_dirs = []
        @deleted_files = []
        @deleted_dirs = []
        @updated_files = []
        @updated_dirs = []
      end

      def open_root(base_revision)
        [true, '']
      end

      def delete_entry(path, revision, parent_baton)
        if @base_root.dir?("/#{path}")
          @deleted_dirs << "#{path}/"
        else
          @deleted_files << path
        end
      end

      def add_directory(path, parent_baton,
                        copyfrom_path, copyfrom_revision)
        copyfrom_rev, copyfrom_path = @root.copied_from(path)
        if in_copied_dir?
          @in_copied_dir.push(true)
        elsif Util.copy?(copyfrom_path, copyfrom_rev)
          @copied_dirs << ["#{path}/", "#{copyfrom_path.sub(/^\//, '')}/", copyfrom_rev]
          @in_copied_dir.push(true)
        else
          @added_dirs << "#{path}/"
          @in_copied_dir.push(false)
        end
        [false, path]
      end

      def open_directory(path, parent_baton, base_revision)
        [true, path]
      end

      def change_dir_prop(dir_baton, name, value)
        if dir_baton[0]
          @updated_dirs << "#{dir_baton[1]}/"
          dir_baton[0] = false
        end
        dir_baton
      end

      def close_directory(dir_baton)
        unless dir_baton[0]
          @in_copied_dir.pop
        end
      end

      def add_file(path, parent_baton,
                   copyfrom_path, copyfrom_revision)
        copyfrom_rev, copyfrom_path = @root.copied_from(path)
        if in_copied_dir?
          # do nothing
        elsif Util.copy?(copyfrom_path, copyfrom_rev)
          @copied_files << [path, copyfrom_path.sub(/^\//, ''), copyfrom_rev]
        else
          @added_files << path
        end
        [nil, nil, nil]
      end

      def open_file(path, parent_baton, base_revision)
        [nil, nil, path]
      end

      def apply_textdelta(file_baton, base_checksum)
        file_baton[0] = :update
        nil
      end

      def change_file_prop(file_baton, name, value)
        file_baton[1] = :update
      end

      def close_file(file_baton, text_checksum)
        text_mod, prop_mod, path = file_baton
        # test the path. it will be nil if we added this file.
        if path
          if [text_mod, prop_mod] != [nil, nil]
            @updated_files << path
          end
        end
      end

      def close_edit(baton)
        @copied_files.sort! {|a, b| a[0] <=> b[0]}
        @copied_dirs.sort! {|a, b| a[0] <=> b[0]}
        @added_files.sort!
        @added_dirs.sort!
        @deleted_files.sort!
        @deleted_dirs.sort!
        @updated_files.sort!
        @updated_dirs.sort!
      end

      private
      def in_copied_dir?
        @in_copied_dir.last
      end
    end

    class WrapEditor
      extend Forwardable

      def_delegators :@wrapped_editor, :set_target_revision, :open_root
      def_delegators :@wrapped_editor, :delete_entry, :add_directory
      def_delegators :@wrapped_editor, :open_directory, :change_dir_prop
      def_delegators :@wrapped_editor, :close_directory, :absent_directory
      def_delegators :@wrapped_editor, :add_file, :open_file, :apply_textdelta
      def_delegators :@wrapped_editor, :change_file_prop, :close_file
      def_delegators :@wrapped_editor, :absent_file, :close_edit, :abort_edit

      def initialize(wrapped_editor)
        @wrapped_editor = wrapped_editor
      end
    end
  end
end