# This file Copyright Tim Coulter, 03/07/2007.
# 
# You are free to modify and use this file under the terms of the GNU LGPL.
# You can find the latest version of the LGPL here:
#      
#      http://www.gnu.org/licenses/lgpl.txt
# 
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.

require "svn/repos"
require "svn/core"
require "fileutils"

class SvnRepos

  attr_reader :repos_path
  
  # Translate SVN Constants to more programmer friendly terms.
  CONSTANTS = {:author => Svn::Core::PROP_REVISION_AUTHOR, :date => Svn::Core::PROP_REVISION_DATE}

  # Constructor. Either create a repository at the given location, or open it if
  # it already exists.
  def initialize(repos_path)
  
    @repos_path = repos_path
  
    begin
      File.stat(repos_path + "/format")
      @repos = Svn::Repos.open(@repos_path)
    rescue
      @repos = Svn::Repos.create(@repos_path)
    end
    
  end
  
  # Synonym for new.
  def self.open(repos_path)
    SvnRepos.new(repos_path)
  end
 
  # Commit a file. This function either takes in a hash with the :path and :data
  # keys set to their respective values, or it takes in an array of hashes with the
  # same keys set. Passing an array will let you commit multiple files at once.
  # 
  # The attributes hash is option, and as of now, is only available for setting the
  # author of a revisoin. An example would be {:author => "tcoulter"}.
  # 
  # Note that this function takes care of making sure files and directories are present
  # within the repository. You don't have to create them; this function will do it for you.
  def commit(hash, attributes={})
  
    if (hash.is_a? Array)
      multiple_commit(hash, attributes)
    else
      @repos.fs.transaction do |txn|
        write_file(txn, hash[:path], hash[:data])
        set_author_if_present(txn, attributes)
      end
    end
  
  end
 
  # Get the contents of a specific file for specific revision.
  # If not revision is passed, the most recent data is returned.
  def file_contents(path, revision_id = nil)
    
    if (revision_id != nil)
      revision_id = revision_id.to_i
    end
    
    begin
      stream = @repos.fs.root(revision_id).file_contents(path)
      stream.read
    rescue
      nil
    end
  end
  
  # Get the revision data for a specified path. This will return an array of hashes,
  # where each hash contains the :path, the :id, and the :date for a specific revision.
  def file_revs(path)
    make_revision_info_programmer_friendly(@repos.file_revs(path, 0, @repos.fs.youngest_rev))
  end
  
  def property(prop, rev=nil)
    @repos.prop(CONSTANTS[prop], rev)  
  end
  
  # Get the diffs for a specific revision. This function can be more programmer friendly,
  # but as of now, returns the exact output given from the repository.
  def diffs(rev)
  
    info = Svn::Info.new(@repos_path, rev)
    info.diffs
  
  end
  
  # Returns the most recent revision of the repository.
  def youngest_revision
    @repos.fs.youngest_rev
  end
  
  # Returns the most recent revision for a specified path within the repository.
  def youngest_revision(path)
    @repos.fs.root.stat(path).created_rev
  end
  
  # Alias the revision_count with the youngest revision. They happen to be the
  # same number all the time.
  alias_method :revision_count, :youngest_revision
  
  # Delete the repository from the filesystem and create a noew one.
  def format!
    FileUtils.remove_dir(@repos_path, false)
    Svn::Repos.create(@repos_path)
  end

private

  # A helper when commiting multiple files at once.
  def multiple_commit(array_of_hashes, attributes)
    
    if (array_of_hashes.length == 0)
      return
    end
    
    @repos.fs.transaction do |txn|
      array_of_hashes.each do |hash|
        write_file(txn, hash[:path], hash[:data])
      end  
      set_author_if_present(txn, attributes)
    end  
      
  end

  # Make a directory if it's not already present.
  def make_directory(txn, path)  
    if (txn.root.check_path(path) == 0)
      txn.root.make_dir(path)
    end
  end

  # Make a file if it's not already present.
  def make_file(txn, path)
    if (txn.root.check_path(path) == 0)
      txn.root.make_file(path)
    end
  end
  
  # Set the author if it's present in the attributes hash.
  def set_author_if_present(txn, attributes)
     
    if (attributes.has_key? :author)
      txn.set_prop(CONSTANTS[:author], attributes[:author])
    end
  end
  
  # Helper. This function does the actual writing.
  def write_file(txn, path, data)
    pieces = path.split("/").delete_if {|x| x == ""}
      
    dir_path = ""
    (0..pieces.length - 2).each do |index|
    
      dir_path += "/" + pieces[index]
      make_directory(txn, dir_path)
    end
    
    make_file(txn, path)
    
    checksum = MD5.new(data).hexdigest
    stream = txn.root.apply_text(path, checksum)
    stream.write(data)
    stream.close
  end
  
  # Helper to make file_revs friendlier. 
  def make_revision_info_programmer_friendly(file_revs)
  
    output_array = []
  
    file_revs.each do |rev|
      
      output_hash = {}
      output_hash[:path] = rev[0]
      output_hash[:id] = rev[1] 
      
      attribute_hash = rev[2]
      
      output_hash[:time] = Time.parse(attribute_hash[CONSTANTS[:date]])
      
      output_array.push(output_hash)
    end
  
    output_array
  
  end
  
end
