#######################################################################
#                  Licensing Information                              #
#                                                                     #
#  The following code is a derivative work of the code from the       #
#  Mercurial project, which is licensed GPLv2. This code therefore    #
#  is also licensed under the terms of the GNU Public License,        #
#  verison 2.                                                         #
#                                                                     #
#  For information on the license of this code when distributed       #
#  with and used in conjunction with the other modules in the         #
#  Amp project, please see the root-level LICENSE file.               #
#                                                                     #
#  © Michael J. Edgar and Ari Brown, 2009-2010                        #
#                                                                     #
#######################################################################

module Amp
  module Repositories
    module Mercurial
      
      ##
      # = BundleRepository
      # This class represents a read-only repository that combines both local
      # repository data with a bundle file. The bundle file contains un-merged-in
      # changesets - this is useful for, say, previewing the results of a pull
      # action.
      #
      # A bundle is stored in the following manner:
      #  - Changelog entries
      #  - Manifest  entries
      #  - Modified File entry #1
      #  - Modified File entry #2
      #  - ...
      #  - Modified file entry #N
      class BundleRepository < LocalRepository
        def initialize(path="", config=nil, bundle_name="")
          @temp_parent = nil
          # Figure out what to do here - if there's no current local repository, that
          # takes some special work.
          begin
            super(path, false, config) # don't create, just look for a repository
          rescue
            # Ok, no local repository. Let's make one really quickly.
            @temp_parent = File.join(Dir.tmpdir, File.amp_make_tmpname("bundlerepo"))
            File.mkdir(@temp_parent)
            tmprepo = LocalRepository.new(@temp_parent, true, config) # true -> create
            super(@temp_parent, false, config) # and proceed as scheduled!
          end
          
          # Set up our URL variable, if anyone asks us what it is
          if path
            @url = "bundle:#{path}+#{bundle_name}"
          else
            @url = "bundle:#{bundle_name}"
          end
          
          @temp_file = nil
          @bundle_file = File.open(bundle_name, "r")
          
          @bundle_file.seek(0, IO::SEEK_END)
          Amp::UI.debug "Bundle File Size: #{@bundle_file.tell}"
          @bundle_file.seek(0, IO::SEEK_SET)
          
          # OK, now for the fun part - check the header to see if we're compressed.
          header = @bundle_file.read(6)
          # And switch based on that header
          if !header.start_with?("HG")
            # Not even an HG file. FML. Bail
            raise abort("#{bundle_name}: not a Mercurial bundle file")
          elsif not header.start_with?("HG10")
            # Not a version we understand, bail
            raise abort("#{bundle_name}: unknown bundle version")
          elsif header == "HG10BZ" || header == "HG10GZ"
            # Compressed! We'll have to save to a new file, because this could get messy.
            temp_file = Tempfile.new("hg-bundle-hg10un", @root)
            @temp_file_path = temp_file.path
            # Are we BZip, or GZip?
            case header
            when "HG10BZ"
              # fuck BZip. Seriously.
              headerio = StringIO.new "BZ", Support.binary_mode("w+")
              input = Amp::Support::MultiIO.new(headerio, @bundle_file)
              decomp = BZ2::Reader.new(input)
            when "HG10GZ"
              # Gzip is much nicer.
              decomp = Zlib::GzipReader.new(@bundle_file)
            end
            
            # We're writing this in an uncompressed fashion, of course.
            @temp_file.write("HG10UN")
            # While we can uncompressed....
            while !r.eof? do
              # Write the uncompressed data to our new file!
              @temp_file.write decomp.read(4096)
            end
            # and close 'er up
            @temp_file.close
            
            # Close the compressed bundle file
            @bundle_file.close
            # And re-open the uncompressed bundle file!
            @bundle_file = File.open(@temp_file_path, "r")
            # Skip the header.
            @bundle_file.seek(6)
          elsif header == "HG10UN"
            # uncompressed, do nothing
          else
            # We have no idae what's going on
            raise abort("#{bundle_name}: unknown bundle compression type")
          end
          # This hash stores pairs of {filename => position_in_bundle_file_of_this_file} 
          @bundle_files_positions = {}
        end
        
        ##
        # Gets the changelog of the repository. This is different from {LocalRepository#changelog}
        # in that it uses a {BundleChangeLog}. Also, since the manifest is stored in the bundle
        # directly after the changelog, by checking our position in the bundle file, we can save
        # where the bundle_file is stored.
        #
        # @return [BundleChangeLog] the changelog for this repository.
        def changelog
          @changelog      ||= Bundles::Mercurial::BundleChangeLog.new(@store.opener, @bundle_file)
          @manifest_start ||= @bundle_file.tell
          @changelog
        end
        
        ##
        # Gets the manifest of the repository. This is different from {LocalRepository#manifest}
        # in that it uses a {BundleManifest}. The file logs are stored in the bundle directly
        # after the manifest, so once we load the manifest, we save where the file logs start
        # when we are done loading the manifest.
        #
        # This has the side-effect of loading the changelog, if it hasn't been loaded already -#
        # this is necessary because the manifest changesets are stored after the changelog changesets,
        # and we must fully load the changelog changesets to know where to look for the manifest changesets.
        #
        # Don't look at me, I didn't design the file format.
        #
        # @return [BundleChangeLog] the changelog for this repository.
        def manifest
          return @manifest if @manifest
          @bundle_file.seek manifest_start
          @manifest   ||= Bundles::BundleManifest.new @store.opener, @bundle_file, proc {|n| changelog.rev(n) }
          @file_start ||= @bundle_file.tell
          @manifest
        end
        
        ##
        # Returns the position in the bundle file where the manifest changesets are located.
        # This involves loading the changelog first - see {#manifest}
        #
        # @return [Integer] the position in the bundle file where we can find the manifest
        #   changesets.
        def manifest_start
          changelog && @manifest_start
        end
        
        ##
        # Returns the position in the bundle file where the file log changesets are located.
        # This involves loading the changelog and the manifest first - see {#manifest}.
        #
        # @return [Integer] the position in the bundle file where we can find the file-log
        #   changesets.
        def file_start
          manifest && @file_start
        end
        
        ##
        # Gets the file-log for the given path, so we can look at an individual
        # file's history, for example. However, we need to be cognizant of files that
        # traverse the local repository's history as well as the bundle file.
        # 
        # @param [String] f the path to the file
        # @return [FileLog] a filelog (a type of revision log) for the given file
        def file(filename)
          
          # Load the file-log positions now - we didn't do this in the constructor for a reason
          # (if they don't ask for them, don't load them!)
          if @bundle_files_positions.empty?
            # Jump to the file position
            @bundle_file.seek file_start
            while true
              # get a changegroup chunk - it'll be the filename
              chunk = RevlogSupport::ChangeGroup.get_chunk @bundle_file
              # no filename? bail
              break if chunk.nil? || chunk.empty?
              
              # Now that we've read the filename, we're at the start of the changelogs for that
              # file. So let's save this position for later.
              @bundle_files_positions[chunk] = @bundle_file.tell
              # Then read chunks until we get to the next file!
              RevlogSupport::ChangeGroup.each_chunk(@bundle_file) {|c|}
            end
          end
          
          # Remove leading slash
          filename = filename.shift("/")
          
          # Does this file cross local history as well as the bundle?
          if @bundle_files_positions[filename]
            # If so, we'll need to make a BundleFileLog. Meh. 
            @bundle_file.seek @bundle_files_positions[filename]
            Bundles::BundleFileLog.new @store.opener, filename, @bundle_file, proc {|n| changelog.rev(n) }
          else
            # Nope? Make a normal FileLog!
            FileLog.new(@store.opener, filename)
          end
        end  
        
        ##
        # Gets the URL for this repository - unused, I believe.
        #
        # @return [String] the URL for the repository
        def url; @url; end
        
        ##
        # Closes the repository - in this case, it closes the bundle_file. Analogous to closing
        # an SSHRepository's socket.
        def close
          @bundle_file.close
        end
        
        # We can't copy files. Read-only.
        def can_copy?; false; end
        # Gets the current working directory. Not sure why we need this.
        def get_cwd; Dir.pwd; end
        
      end
    end
  end
end