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

   SSheetDICT.py ---
      class SpreadsheetData implementation, based on Python dictionaries.

   last modified: 01/24/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 *

class SpreadsheetData:
  '''
  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.'''
    # 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

  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

  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.
    '''
    # This method prints the smallest rectangular area of the spreadsheet that
    # contains all data. It doesn't do anything special if the width does not
    # fit the display.
    # The formatting is adapted CellData objects containing only integers
    # between -9999 and 9999 and row/column indexes <= 9999
    # (some fine-tuning here).

    S = '' # the string to be returned

    luCoord = self.getLU()
    if luCoord == None: # This means the spreadsheet is empty
      return S
    rbCoord = self.getRB()

    # Build spreadsheet heading (column indexes)
    S = S + " "*6
    for c in range(luCoord.getColumn(), rbCoord.getColumn()+1):
      S = S + "C%-5d" % c
    S = S + "\n"

    for row in range(luCoord.getRow(), rbCoord.getRow()+1):
      # Build row heading (row indexes)
      S = S + "R%-5d" % row
      # display data:
      for col in range(luCoord.getColumn(), rbCoord.getColumn()+1):
        data = self[CellCoordinate(row, col)]
        if data == None:
          S = S + " "*6
        else:
          S = S + "%- 6d" % (data.getValue())
      S = S + "\n"

    return S
