module Amp
  
  class ManifestEntry < DelegateClass(Hash)
    
    ##
    # Initializes the dictionary. It can be empty, by initializing with no
    # arguments, or with more data by assigning them.
    # 
    # It is a hash of Filename => node_id
    # 
    # @param [Hash] mapping the initial settings of the dictionary
    # @param [Hash] flags the flag settings of the dictionary
    def initialize(mapping=nil, flags=nil)
      @source_hash = mapping || {}
      super(@source_hash || {})
      @flags = flags || {}
    end
    
    def inspect
      "#<ManifestEntry " + @source_hash.inspect + "\n" +
      "                " + @flags.inspect + ">"
    end
    
    def flags(file=nil)
      file ? @flags[file] : @flags
    end
    
    def files; keys; end
    
    def delete(*args)
      super(*args)
      flags.delete(*args)
    end
    
    ##
    # Clones the dictionary
    def clone
      self.class.new @source_hash.dup, @flags.dup
    end
    
    # @see clone
    alias_method :dup, :clone
    
    ##
    # Mark a file to be checked later on
    # 
    # @param [String] file the file to be marked for later checking
    # @param []
    def mark_for_later(file, node)
      self[file]  = nil # notice how we DIDN'T use `self.delete file`
      flags[file] = node.flags file
    end
    
  end

  
  ##
  # = Manifest
  # A Manifest is a special type of revision log. It stores lists of files
  # that are being tracked, with some flags associated with each one. The
  # manifest is where you can go to find what files a revision changed,
  # and any extra information about the file via its flags.
  class Manifest < Revlog
    
    attr_accessor :manifest_list
    
    ##
    # Parses a bunch of text and interprets it as a manifest entry.
    # It then maps them onto a ManifestEntry that stores the real
    # info.
    # 
    # @param [String] lines the string that contains the information
    #   we need to parse.
    def self.parse(lines)
      mf_dict = ManifestEntry.new
      
      lines.split("\n").each do |line|
        f, n = line.split("\0")
        if n.size > 40
          mf_dict.flags[f] = n[40..-1]
          mf_dict[f] = n[0..39].unhexlify
        else
          mf_dict[f] = n.unhexlify
        end
      end
      
      mf_dict
    end    
    
    def initialize(opener)
      @map_cache = nil
      @list_cache = nil
      super(opener, "00manifest.i")
    end
    
    ##
    # Reads the difference between the given node and the revision
    # before that.
    # 
    # @param [String] node the node_id of the revision to diff
    # @return [ManifestEntry] the dictionary with the info between
    #   the given revision and the one before that
    def read_delta(node)
      r = self.revision_index_for_node node
      return self.class.parse(Diffs::MercurialDiff.patch_text(self.revision_diff(r-1, r)))
    end
    
    ##
    # Parses the manifest's data at a given revision's node_id
    # 
    # @param [String, Symbol] node the node_id of the revision. If a symbol,
    #   it better be :tip or else shit will go down.
    # @return [ManifestEntry] the dictionary mapping the
    #   flags, filenames, digests, etc from the parsed data
    def read(node)
      node = tip if node == :tip
      
      return ManifestEntry.new if node == NULL_ID
      return @map_cache[1] if @map_cache && @map_cache[0] == node
      
      text = decompress_revision node
      
      @list_cache = text
      mapping = self.class.parse(text)
      @map_cache = [node, mapping]
      mapping
    end
    
    ##
    # Digs up the information about how a file changed in the revision
    # specified by the provided node_id.
    # 
    # @param [String] nodes the node_id of the revision we're interested in
    # @param [String] f the path to the file we're interested in
    # @return [[String, String], [nil, nil]] The data stored in the manifest about the
    #   file. The first String is a digest, the second String is the extra
    #   info stored alongside the file. Returns [nil, nil] if the node is not there
    def find(node, f)
      if @map_cache && node == @map_cache[0]
        return [@map_cache[1][f], @map_cache[1].flags[f]]
      end
      mapping = read(node)
      return [mapping[f],  (mapping.flags[f] || "")]
    end
    ##
    # Checks the list for files invalid characters that aren't allowed in
    # filenames.
    # 
    # @raise [RevlogSupport::RevlogError] if the path contains an invalid
    #   character, raise.
    def check_forbidden(list)
      list.each do |f|
        if f =~ /\n/ || f =~ /\r/
          raise RevlogSupport::RevlogError.new("\\r and \\n are disallowed in "+
                                               "filenames")
        end
      end
    end
    
    def encode_file(file, manifest)
      "#{file}\000#{manifest[file].hexlify}#{manifest.flags[file]}\n"
    end
    

    def add(map, journal, link, p1=nil, p2=nil, changed=nil)
      if changed || changed.empty? || @list_cache ||
          @list_cache.empty? || p1.nil? || @map_cache[0] != p1
        check_forbidden map
        @list_cache = map.map {|f,n| f}.sort.map {|f| encode_file f, map }.join
        
        n = add_revision(@list_cache, journal, link, p1, p2)
        @map_cache = [n, map]
        
        return n
      end
      
      check_forbidden changed[0] # added files, check if they're forbidden
      
      mapping = Manifest.parse(@list_cache)
      
      changed[0].each do |x| 
        mapping[x] = map[x].hexlify
        mapping.flags[x] = map.flags[x]
      end
      
      changed[1].each {|x| mapping.delete x }
      @list_cache = mapping.map {|k, v| k}.sort.map {|fn| encode_file(fn, mapping)}.join
      
      n = add_revision(@list_cache, journal, link, p1, p2)
      @map_cache = [n, map]
      
      n
    end
  end
end