from operator import attrgetter
from weakref import ref

class Cell(object):
    """Represents a single cell/value within a sudoku grid."""

    ### Accessors

    def _get_solved(self):
        """True iff this cell has been solved."""
        return len(self._values) == 1
    solved = property(_get_solved)

    def _get_value(self):
        """Returns this cell's value, if it has one known."""
        if self.solved:
            return self._values[0]
        return None
    value = property(_get_value)

    grid = property(lambda self: self._grid())
    constraints = property(attrgetter('_constraints'))

    def __init__(self, grid, row, column):
        self._grid = ref(grid)
        self._row = row
        self._col = column
        self._values = range(self.grid.size)
        self._constraints = []
        self._normalized = False

    def add_constraint(self, constraint):
        self._constraints.append(constraint)

    def set(self, value, normalize=True):
        """Sets the value of this cell.  If `normalize` is True or omitted, the
        grid will be updated accordingly.

        Returns the number of cell changes.
        """
        self._values = [value]
        if normalize:
            self._normalized = False
            return self.normalize() + 1
        
        return 1


    def normalize(self):
        """If this cell has been solved, eliminates its value as a candidate
        from every other cell in every group it's in.
        This method is exhaustive; repeated calls should have no effect.

        Returns the number of cell changes.
        """

        if self._normalized:
            # Already done
            return 0

        # Set this now just in case of infinite looping
        self._normalized = True

        if not self.solved:
            # Don't know the value yet
            return 0

        # Elimination time
        cell_changes = 0
        for constraint in self.constraints:
            for cell in constraint.cells:
                if cell == self:
                    continue
                cell_changes += cell.eliminate(self.value)

        return cell_changes

    def eliminate(self, value):
        """Eliminates the given value as a possibility for this cell.
        
        Returns the number of cell changes."""
        if value not in self._values:
            return 0

        self._values.remove(value)

        if len(self._values) == 0:
            # XXX give me a real exception here
            raise Exception

        self._normalized = False
        return self.normalize() + 1
