# (Be in -*- python -*- mode.)
#
# ====================================================================
# Copyright (c) 2006-2008 CollabNet.  All rights reserved.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution.  The terms
# are also available at http://subversion.tigris.org/license-1.html.
# If newer versions of this license are posted there, you may use a
# newer version instead, at your option.
#
# This software consists of voluntary contributions made by many
# individuals.  For exact contribution history, see the revision
# history and logs, available at http://cvs2svn.tigris.org/.
# ====================================================================

"""Keep track of counts of different types of changeset links."""



# A cvs_item doesn't depend on any cvs_items in either pred or succ:
LINK_NONE = 0

# A cvs_item depends on one or more cvs_items in pred but none in succ:
LINK_PRED = 1

# A cvs_item depends on one or more cvs_items in succ but none in pred:
LINK_SUCC = 2

# A cvs_item depends on one or more cvs_items in both pred and succ:
LINK_PASSTHRU = LINK_PRED | LINK_SUCC


class ChangesetGraphLink(object):
    def __init__(self, pred, changeset, succ):
        """Represent a link in a loop in a changeset graph.

        This is the link that goes from PRED -> CHANGESET -> SUCC.

        We are mainly concerned with how many CVSItems have LINK_PRED,
        LINK_SUCC, and LINK_PASSTHRU type links to the neighboring
        commitsets.  If necessary, this class can also break up CHANGESET
        into multiple changesets."""

        self.pred = pred
        self.pred_ids = set(pred.cvs_item_ids)

        self.changeset = changeset

        self.succ_ids = set(succ.cvs_item_ids)
        self.succ = succ

        # A count of each type of link for cvs_items in changeset
        # (indexed by LINK_* constants):
        link_counts = [0] * 4

        for cvs_item in list(changeset.iter_cvs_items()):
            link_counts[self.get_link_type(cvs_item)] += 1

        [self.pred_links, self.succ_links, self.passthru_links] = link_counts[1:]

    def get_link_type(self, cvs_item):
        """Return the type of links from CVS_ITEM to self.PRED and self.SUCC.

        The return value is one of LINK_NONE, LINK_PRED, LINK_SUCC, or
        LINK_PASSTHRU."""

        retval = LINK_NONE

        if cvs_item.get_pred_ids() & self.pred_ids:
            retval |= LINK_PRED
        if cvs_item.get_succ_ids() & self.succ_ids:
            retval |= LINK_SUCC

        return retval

    def get_links_to_move(self):
        """Return the number of items that would be moved to split changeset."""

        return min(self.pred_links, self.succ_links) \
               or max(self.pred_links, self.succ_links)

    def is_breakable(self):
        """Return True iff breaking the changeset will do any good."""

        return self.pred_links != 0 or self.succ_links != 0

    def __cmp__(self, other):
        """Compare SELF with OTHER in terms of which would be better to break.

        The one that is better to break is considered the lesser."""

        return (
            - cmp(int(self.is_breakable()), int(other.is_breakable()))
            or cmp(self.passthru_links, other.passthru_links)
            or cmp(self.get_links_to_move(), other.get_links_to_move())
            )

    def break_changeset(self, changeset_key_generator):
        """Break up self.changeset and return the fragments.

        Break it up in such a way that the link is weakened as efficiently
        as possible."""

        if not self.is_breakable():
            raise ValueError('Changeset is not breakable: %r' % self.changeset)

        pred_items = []
        succ_items = []

        # For each link type, should such CVSItems be moved to the
        # changeset containing the predecessor items or the one containing
        # the successor items?
        destination = {
            LINK_PRED : pred_items,
            LINK_SUCC : succ_items,
            }

        if self.pred_links == 0:
            destination[LINK_NONE] = pred_items
            destination[LINK_PASSTHRU] = pred_items
        elif self.succ_links == 0:
            destination[LINK_NONE] = succ_items
            destination[LINK_PASSTHRU] = succ_items
        elif self.pred_links < self.succ_links:
            destination[LINK_NONE] = succ_items
            destination[LINK_PASSTHRU] = succ_items
        else:
            destination[LINK_NONE] = pred_items
            destination[LINK_PASSTHRU] = pred_items

        for cvs_item in self.changeset.iter_cvs_items():
            link_type = self.get_link_type(cvs_item)
            destination[link_type].append(cvs_item.id)

        # Create new changesets of the same type as the old one:
        return [
            self.changeset.create_split_changeset(
                changeset_key_generator.gen_id(), pred_items),
            self.changeset.create_split_changeset(
                changeset_key_generator.gen_id(), succ_items),
            ]

    def __str__(self):
        return 'Link<%x>(%d, %d, %d)' % (
            self.changeset.id,
            self.pred_links, self.succ_links, self.passthru_links)


