"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/dulcinea/lib/category.py $
$Id: category.py 22945 2003-11-02 23:43:01Z dbinger $

Provides the Category class.
"""

import re
from dulcinea.base import DulcineaPersistent
from dulcinea.persistent_list import PersistentList
from dulcinea.typeutils import typecheck, typecheck_seq
from dulcinea.spec import any, instance, sequence, set

class Category (DulcineaPersistent):

    """
    A category is a node in a partial ordering.

    Instance attributes:
      name : string
        unique, URL-friendly identifier for this category
      label : string
        human-readable string that describes this category; probably but
        not necessarily unique
      description : string
        paragraph-length, human-readable prose description of this
        category
      children : PersistentList [Category]
      parents :  PersistentList [Category]
    """

    name_is = any(str, None)
    label_is = any(str, None)
    description_is = any(str, None)
    children_is = sequence(instance('Category'), PersistentList)
    parents_is = sequence(instance('Category'), PersistentList)

    def __init__ (self, name=None):
        self.name = name
        self.label = None
        self.description = None
        self.children = PersistentList()
        self.parents = PersistentList()

    def __str__ (self):
        return self.name or "*unnamed*"

    def check_name (self, name):
        """(string) -> bool

        All group names must match 'name_re' because they must be URL
        friendly.
        """
        typecheck(name, str)
        name_re = re.compile(r'[a-zA-Z0-9_]*$')
        return name_re.match(name)

    def set_name (self, name):
        if not self.check_name(name):
            raise ValueError, "invalid name '%s'" % name
        self.name = name

    def set_label (self, label):
        typecheck(label, str, allow_none=1)
        self.label = label

    def set_description (self, description):
        typecheck(description, str, allow_none=1)
        self.description = description

    def add_child (self, child):
        typecheck(child, Category)
        if child.is_ancestor_of(self):
            raise ValueError, "Attempt to create circular relationship"
        if child not in self.children:
            self.children.append(child)
            child.parents.append(self)
        self._update_expansion()

    def remove_child (self, child):
        typecheck(child, Category)
        self.children.remove(child)
        child.parents.remove(self)
        self._update_expansion()

    def set_children (self, children):
        typecheck(children, list)
        for child in self.children[:]: # remove_child() modifies self.children
            self.remove_child(child)
        for child in children:
            self.add_child(child)

    def get_children (self):
        return self.children

    def set_parents (self, parents):
        typecheck(parents, list)
        for parent in self.parents[:]: # remove_child() modifies self.parents
            parent.remove_child(self)
        for parent in parents or []:
            parent.add_child(self)

    def get_parents (self):
        return self.parents

    def _update_expansion (self):
        if hasattr(self, '_v_expansion'):
            del self._v_expansion
        self._v_expansion = self.expand()
        for parent in self.parents:
            parent._update_expansion()

    def expand (self):
        """() -> { Category : bool }

        Return a set containing this category and its (recursive) children.
        """
        if not hasattr(self, "_v_expansion"):
            expansion = {self: 1}
            for child in self.children:
                if child not in expansion:
                    expansion.update(child.expand())
            self._v_expansion = expansion
        return self._v_expansion

    def get_descendants (self):
        """() -> [ Category ]

        Return list containing this category's (recursive) children.
        """
        descendants = []
        for child in self.children:
            if child not in descendants:
                descendants.append(child)
                descendants += [descendant
                                for descendant in child.get_descendants()
                                if descendant not in descendants]
        return descendants

    def get_leaves (self):
        """() -> [ Category ]

        Return list containing this category or this category's
        (recursive) children that have no children.
        """
        if self.children:
            leaves = {}
            for child in self.children:
                for leaf in child.get_leaves():
                    leaves[leaf] = 1
            return leaves.keys()
        else:
            return [self]

    def is_descendant_of (self, other_category):
        """(Category) -> bool

        Is the other category the same as this one or one of the
        descendant categories?
        """
        return self in other_category.expand()

    def is_ancestor_of (self, other_category):
        """(Category) -> bool

        Is this category the same as or a proper descendant of
        other_category?
        """
        return other_category in self.expand()

    def get_name (self):
        return self.name

    def get_label (self):
        return self.label or self.name

    def get_description (self):
        return self.description


class Grouped:
    """
    Instance attributes:
      groups : PersistentList [Category]
    """

    groups_is = sequence(Category, PersistentList)

    def __init__(self):
        self.groups = PersistentList()

    def add_group(self, group):
        self.set_groups(self.groups + [group])

    def set_groups(self, groups):
        set(self, groups=PersistentList(groups))

    def get_groups(self):
        """() -> [Category]
        Not a copy, don't mangle.
        """
        return self.groups

    def get_descendants_of(self, ancestor):
        return [group for group in self.groups
                if ancestor.is_ancestor_of(group)]

    def in_group(self, other_group):
        if other_group is None:
            return 0
        typecheck(other_group, Category)
        other_groups = other_group.expand()
        for group in self.groups:
            if group in other_groups:
                return 1
        return 0

    def in_groups(self, groups):
        """Is self in all of the groups?
        """
        typecheck_seq(groups, Category, allow_none=1)
        for group in groups:
            if not self.in_group(group):
                return 0
        return 1
