module Amp
  module Repositories
    ##
    # = BranchManager
    # Michael Scott for Amp.
    #
    # More seriously, this class handles reading/writing to the branch cache
    # and figuring out what the head revisions are for each branch and such.
    module BranchManager
      include Amp::RevlogSupport::Node
      
      ##
      # Saves the branches with the given "partial" and the last_rev index.
      def save_branch_cache(partial, last_rev)
        tiprev = self.size - 1
        # If our cache is outdated, then update it and re-write it
        if last_rev != tiprev
          # search for new heads
          update_branches!(partial, last_rev+1, tiprev+1)
          # write our data out
          write_branches!(partial, self.changelog.tip, tiprev)
        end
        # return our mappings
        partial
      end
      
      ##
      # Loads the head revisions for each branch. Each branch has at least one, but
      # possible more than one, head.
      #
      # @return [Hash] a map, where the branch names are keys and the values
      #   are the heads of the branch
      def branch_heads
        # Gets the mapping of branch names to branch heads, but uses caching to avoid
        # doing IO and tedious computation over and over. As long as our tip doesn't
        # change, the cache will remain valid.
        
        # Check our current tip
        tip = self.changelog.tip
        # Do we have a cache, and if we do, is the saved cache == tip?
        if !@branch_cache.nil? && @branch_cache_tip == tip
          # if so, cache HIT
          return @branch_cache
        end
        
        # nope? cache miss
        # save the old tip
        oldtip = @branch_cache_tip
        # save the new tip
        @branch_cache_tip = tip
        
        # reset the branch cache
        @branch_cache = @branch_cache.nil? ? {} : @branch_cache.clear # returns same hash, but empty
        # if we didn't have an old cache, or it was invalid, read in the branches again
        if oldtip.nil? || self.changelog.node_map[oldtip].nil?
          partial, last, last_rev = read_branches!
        else
          # Otherwise, dig up the cached hash!
          last_rev = self.changelog.rev(oldtip)
          # Get the last branch cache
          partial = @u_branch_cache
        end
        # Save the branch cache (updating it if we have to)
        save_branch_cache(partial, last_rev)
        
        # Cache our saved hash
        @u_branch_cache = partial
        
        # Copy the partial into the branch_cache
        partial.each { |k, v| @branch_cache[k] = v }
        @branch_cache
      end
      
      # Returns a dict where branch names map to the tipmost head of
      # the branch, open heads come before closed
      def branch_tags
        tags = {}
        branch_heads.each do |branch_node, local_heads|
          head = nil
          local_heads.reverse_each do |h| # get the tip if its a closed
            if !(changelog.read(h)[5].include?("close"))
              head = h
              break
            end
          end
          head = local_heads.last if head.nil?
          tags[branch_node] = head # it's the tip
        end
        return tags
      end
      
      ##
      # Saves the branches, the tip-most node, and the tip-most revision
      # to the branch cache.
      #
      def save_branches(branches, tip, tip_revision)
        write_branches!(branches, tip, tip_revision)
      end
      
      private
      
      ##
      # Reads in the branches from the branch.cache file, stored in the root
      # of the repository's .hg folder. While the repository could figure out
      # what each branch's heads are each time the program is run, that would
      # be quite slow. So we cache them in a file, along with the tip of the
      # repository, so we know if our cache has become inaccurate.
      # The format is very simple:
      #     [tip_node_id] [tip_revision_number]
      #     [branch_head_node_id] [branch_name]
      #     [branch_head_node_id] [branch_name]
      #     [branch_head_node_id] [branch_name]
      #
      # Example:
      #     0abc3135810abc3135810abc3135810abc313581 603
      #     0abc3135810abc3135810abc3135810abc313581 default
      #     1234567890123456789012345678901234567890 other_branch
      #     0987654321098765432109876543210987654321 other_branch
      #
      # In the example, other_branch has 2 heads. This is acceptable. The tip of the
      # repository is node 0abc3135, revision 603, which is the only head of the default
      # branch.
      #
      # @return [[Hash, String, Integer]] The results are returned in the form of:
      #   [partial, tip_node_id, tip_rev_index], where +partial+ is a mapping of
      #   branch names to an array of their heads.
      def read_branches!
        partial, last, last_rev = {}, nil, nil
        lines = nil
        invalid = false
        
        begin
          # read in all the lines. This file should be short, so don't worry about
          # performance concerns of a File.read() call (this call is actually
          # Opener#read, which then calls File.read)
          lines = @hg_opener.read("branchheads.cache").split("\n")
        rescue SystemCallError # IO Errors, i.e. if there is no branch.cache file
          return {}, NULL_ID, NULL_REV
        end
        # use catch, not exceptions (exceptions are more costly)
        valid = catch(:invalid) do
          # Read in the tip node and tip revision #
          last, last_rev = lines.shift.split(" ", 2)
          last, last_rev = last.unhexlify, last_rev.to_i
          
          # if we aren't matching up with the current repo, then invalidate the cache
          if last_rev > self.size || self[last_rev].node != last
            throw :invalid, false
          end
          
          # Go through each next line and read in a head-branch pair
          lines.each do |line|
            # empty = useless line
            next if line.nil? || line.empty?
            # split on " ", only once so we can have a space in a branch name
            node, _label = line.split(" ", 2)
            # and assign to our "partial" i.e. our list of branch-heads
            partial[_label.strip] ||= []
            partial[_label.strip] << node.unhexlify
          end
        end
        
        # if invalid was thrown.... bail
        unless valid
          UI.puts("invalidating branch cache (tip different)")
          partial, last, last_rev = {}, NULL_ID, NULL_REV
        end
        
        # Return our results!
        [partial, last, last_rev]
      end
      
      ##
      # Invalidates the tag cache. Removes all ivars relating to tags.
      def invalidate_branch_cache!
        @branch_cache = nil
        @branch_cache_tip = nil
        @u_branch_cache = nil
      end
      
      ##
      # Goes through from revision +start+ to revision +stop+ and searches for
      # new branch heads for each branch. Annoying, yes. But necessary to keep the
      # cache up to date.
      #
      # @param [Hash] partial the current pairing of branch names to heads. Might
      #   be incomplete, which is why it's called "partial"
      # @param [Integer] start the revision # to start looking new branch heads
      # @param [Integer] stop the last revision in which to look for branch heads
      def update_branches!(partial, start, stop)
        (start..(stop-1)).each do |r|
          # get the changeset
          changeset = self[r]
          # look at its branch
          branch = changeset.branch
          # get that branch's partial list of heads
          
          branch_heads = (partial[branch] ||= [])
          # add this changeset
          branch_heads << changeset.node
          # remove our parents from this branch's list of heads if they're in there,
          # because if they have children, they aren't heads.
          changeset.parents.each do |parent|
            # get the node_id
            parent_node = parent.node
            # remove the parent
            branch_heads.delete parent_node if branch_heads.include? parent_node
          end
        end
      end
      
      ##
      # Writes the branches out to the branch cache. Simple as that. See #read_branches!
      # for the file format.
      #
      # @see read_branches!
      # @param [Hash] branches a mapping of branch names to arrays of their head node IDs
      # @param [String] tip the tip of this repository's node ID
      # @param [Integer] tip_revision the index # of this repository's tip
      def write_branches!(branches, tip, tip_revision)
        @hg_opener.open "branchheads.cache", "w" do |f|
          f << "#{tip.hexlify} #{tip_revision}\n"
          branches.each do |_label, nodes|
            nodes.each {|node| f << "#{node.hexlify} #{_label}\n" }
          end
        end
      rescue SystemCallError
        
      end
    end
    MichaelScott = BranchManager # hehehe
  end
end