'''
### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###
                   308-304 - Object-Oriented Software Design
                                 ASSIGNMENT  4

   SSheetDICT.py ---
      class SpreadsheetData implementation, based on Python dictionaries.
      REVIEWED FOR ASSIGNMENT 4

   last modified: 04/04/02
                       ===============================
                             Copyright (c)  2002
                            Jean-Sébastien BOLDUC
                             (jseb@cs.mcgill.ca)

                        McGill University  (Montréal)
                       ===============================

### ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ###
'''

# Import CellData and CellCoordinate implementations
from CData  import *
from CCoord import *

#                                                  *** MODIFIED FOR ASSGT 4 ***
# Also need some material from module SSGridView:
from SSGridView import *

#                                                  *** MODIFIED FOR ASSGT 4 ***
# Custom exception:
class AbstractError(Exception):
  """Abstract class cannot be instanciated."""
  pass

#                                                  *** MODIFIED FOR ASSGT 4 ***
# Define class AbstractSubject:
class AbstractSubject:
  '''
  Abstract subject class: class SpreadsheetData inherits from this.
  '''
  def __init__(self):
    ''' AbstractSubject constructor.
    '''
    # Prevent this class from being instantiated (abstract):
    if self.__class__ == AbstractSubject:
       raise AbstractError, "cannot instantiate abstract class"
       
    # self.__observers is a list of tuple. each entry (subtuple) contains:
    #       - a handle to the observer object (a GridView),
    #       - dataOffsetHor and dataOffsetVer for the corresponding GridView
    #         (ALWAYS 0 IN THE CURRENT PROTOTYPE),
    #       - height and width for the corresponding GridView.
    self.__observers = []
    
  def attach(self, obs, doffH = 0, doffV = 0, height = 0, width = 0):
    ''' obs:AbstractObserver, doffH:Integer, doffV:Integer,
              height:Integer, width:Integer -> Boolean
              
    Attach an observer to the subject. The method returns 0 if the observer is
    already attached to the subject, 1 otherwise.
    A TypeError is raised on bad argument.
    '''
    # Do a little typechecking:
    if not isinstance(obs, AbstractObserver):
      raise TypeError, "Unsupported observer type"
    if not(type(doffH) == type(doffV) == type(height)                         \
          == type(width) == types.IntType):
      raise TypeError, "Invalid argument(s)"
    # Check whether obs is already attached:
    if obs in map(lambda x : x[0], self.__observers):
      return 0
    # Attach the observer:
    self.__observers.append((obs, doffH, doffV, height, width))
    return 1
    
  def detach(self, obs):
    ''' obs:AbstractObserver ->
              
    Detach an observer from the subject.
    '''
    for i in range(len(self.__observers)):
      if self.__observers[i][0] == obs:
        del self.__observers[i]
        break
        
  def notify(self, cell):
    ''' cell:CellCoordinate ->

    Send an update message to the observers that are concerned (i.e., those
    whose view area is affected by the modification). We adopt a "push"
    strategy, whereby the relevant information is sent with the update message.
    '''
    data = self[cell]  # data is a CellData object
    # We subtract 1 because the lowest indexes for a SpreadsheetData are 1,
    # whereas they are 0 for a SpreadsheetGridView...
    X = cell.getColumn() - 1
    Y = cell.getRow() - 1
    
    for o in self.__observers:
      # Send an update message to the observer O only if:
      #       O.dataOffsetHor <= X < O.dataOffsetHor + width
      #       AND
      #       O.dataOffsetVer <= Y < O.dataOffsetVer + height
      # Note that we send an update message even to the observer that triggered
      # the notify.
      if ( X in range(o[1], o[1]+o[4]) ) and ( Y in range(o[2], o[2]+o[3]) ):
        o[0].update(Y, X, data)


#                                                  *** MODIFIED FOR ASSGT 4 ***
# SpreadsheetData is now a concrete subject:
class SpreadsheetData(AbstractSubject):
  '''
  Encapsulates a dynamically sized spreadsheet structure containing CellData
  data and indexed by CellCoordinate coordinates.
  THIS VERSION BASED ON PYTHON DICTIONARIES
  '''

  ### -------------------------------------------------------------  API -- ###

  def __init__(self):
    ''' ->  -- SpreadsheetData constructor.'''
    #                                              *** MODIFIED FOR ASSGT 4 ***
    # Call parent class' constructor FIRST:
    AbstractSubject.__init__(self)   
    # Data stored in private variable __dict, a dictionary, where the keys are
    # tuples (row, column). A nonexisting key means that the corresponding cell
    # is empty (contains None).
    self.__dict = {}

  def __setitem__(self, coord, data):
    '''coord:CellCoordinate, data:CellData ->

    Update the content of cell indexed by coord with data.
    A KeyError is raised on bad coordinate, and a TypeError is raised on
    bad value. Example use:
          sd[CellCoordinate(3,4)] = CellData(33)
    '''
    # First make sure that coord and data are the right type:
    if not isinstance(coord, CellCoordinate):
      raise KeyError, 'coordinate must be CellCoordinate object'
    if not isinstance(data, CellData):
      raise TypeError, 'data must be CellData object'

    # Store the data
    # If __dict[(i, j)] already contained a CellData object, we don't need to
    # explicitely delete that object, thanks to garbage collection.
    self.__dict[(coord.getRow(), coord.getColumn())] = data
    #                                              *** MODIFIED FOR ASSGT 4 ***
    # Notification:
    self.notify(coord)

  def __getitem__(self, coord):
    '''coord:CellCoordinate -> :CellData | None

    Return the content of a cell indexed by coord (return None if the cell is
    empty).
    A KeyError is raised on bad coordinate. Example use:
          sd[CellCoordinate(3,4)]
    '''
    # Make sure coord is the right type:
    if not isinstance(coord, CellCoordinate):
      raise KeyError, 'coordinate must be CellCoordinate object'

    try:
      return self.__dict[(coord.getRow(), coord.getColumn())]
    except KeyError: # The cell is empty
      return None

  def __delitem__(self, coord):
    '''coord:CellCoordinate ->

    Empty the cell indexed by coord'.
    A KeyError is raised on bad coordinate. Example use:
          del sd[CellCoordinate(3,4)]
    '''
    # Make sure coord is the right type:
    if not isinstance(coord, CellCoordinate):
      raise KeyError, 'coordinate must be CellCoordinate object'

    # Delete the CellData object (if any)
    # If __dict[(i, j)] actually contained a CellData object (which would
    # usually be the case), we don't need to explicitely delete the object,
    # thanks to garbage collection.
    try:
      del self.__dict[(coord.getRow(), coord.getColumn())]
    except KeyError: # The cell was already empty
      pass
    #                                              *** MODIFIED FOR ASSGT 4 ***
    # Notification:
    self.notify(coord)

  def getLU(self):
    ''' -> :CellCoordinate | None

    Return a CellCoordinate containing the Left-most non-empty column,
    and Upper-most non-emtpy row.
    Return None in case of an empty spreadsheet
    '''
    # Use of the built-in min and map functions is not optimal, since on each
    # call to getLU, a list of size n (the number of nonempty cells in the
    # spreadsheet) has to be scanned twice.
    try:
      row    = min( map(lambda x : x[0], self.__dict.keys()) )
      column = min( map(lambda x : x[1], self.__dict.keys()) )
    except ValueError: # happens when __dict is empty (empty spreadsheet)
      return None
    return CellCoordinate(row, column)

  def getRB(self):
    ''' -> :CellCoordinate | None

    Return a CellCoordinate containing the Right-most non-empty column,
    and Bottom-most non-emtpy row.
    Return None in case of an empty spreadsheet.
    '''
    # See note in method getLU.
    try:
      row    = max( map(lambda x : x[0], self.__dict.keys()) )
      column = max( map(lambda x : x[1], self.__dict.keys()) )
    except ValueError: # happens when __dict is empty (empty spreadsheet)
      return None
    return CellCoordinate(row, column)

  def __str__(self):
    ''' -> :String

    Return the string representation of the SpreadSheet.
    This looks like a table of values with spaces for empty cells.
    The row and column indexes are also shown.
    NOT USED IN ASSIGNMENT 4
    '''
    return "Not Implemented"