#
#  $Id: Blocks.py,v 1.6 1999/11/29 06:57:17 rob Exp $
#
#  Copyright 1999 Rob Tillotson <robt@debian.org>
#  All Rights Reserved
#
#  Permission to use, copy, modify, and distribute this software and
#  its documentation for any purpose and without fee or royalty is
#  hereby granted, provided that the above copyright notice appear in
#  all copies and that both the copyright notice and this permission
#  notice appear in supporting documentation or portions thereof,
#  including modifications, that you you make.
#
#  THE AUTHOR ROB TILLOTSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
#  THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
#  AND FITNESS.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
#  SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
#  RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
#  CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
#  CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE!
#
"""Blocks and Records.
"""

__version__ = '$Id: Blocks.py,v 1.6 1999/11/29 06:57:17 rob Exp $'

__copyright__ = 'Copyright 1999 Rob Tillotson <robt@debian.org>'

import string, operator, struct, time, sys

import debug

#######################################################################
#  Records, Blocks, Appinfo, and friends
#######################################################################

# Field type specifiers (in Block.fields) are tuples of
# (type, default, [validation, ...])

FLD_UNKNOWN = None
FLD_ANY = '*'
FLD_BOOLEAN = 'b'
FLD_STRING = 's'  # validation: max length
FLD_INT = 'i'     # validation: min, max values
FLD_FLOAT = 'f'   # validation: min, max values
FLD_TIME = 't'    # validation: min, max date range
FLD_TIME_T = 'T'  # validation: min, max date range (as an integer/float time!)
FLD_LIST = '['    # validation: min, max length

# Blocks of all types are now expected to be accessed as dictionaries!

def palm_date_to_tuple(d, hour=0, minute=0, second=0):
    """Convert a PalmOS 2-byte date to a time tuple."""
    if d == 0xffff: return None
    else:
	d = int(d)
	return time.localtime(time.mktime(((d >> 9) + 1904,
					   ((d >> 5) & 15),
					   d & 31,
					   int(hour), int(minute), int(second),
					   0, 0, -1)))

def tuple_to_palm_date(t):
    """Convert the date part of a time tuple to a PalmOS integer date."""
    if t[0] > 1900:
	d = (t[0] - 1904) << 9
    else:
	d = (t[0] - 4) << 9

    d = d | (t[1] << 5) | t[2]
    return d


class Block:
    """Generic superclass for 'Things Which Can Be Packed And Unpacked',
    including but not limited to database records, appblocks, sortblocks,
    etc."""
    def __init__(self, raw=''):
	self.data = {}
	if hasattr(self, 'fields'):
	    for x in self.fields.keys():
		self.data[x] = self.fields[x][1]
	else:
	    self.fields = {}
	self.raw = raw
	if raw: self.unpack(raw)

    def __repr__(self):
	return '<%s len %s>' % (self.__class__.__name__,
				self.raw and len(self.raw) or 0)
    
    def unpack(self, raw):
	self.raw = raw

    def pack(self):
	return self.raw

    # programming aids
    def packfields(self, fmt, names):
	r = apply(struct.pack,
		  (fmt,)+tuple(map(operator.getitem,
				   [self.data]*len(names),
				   names)))
	return r

    def unpackfields(self, fmt, names, r=''):
	map(operator.setitem, [self.data]*len(names), names,
	    struct.unpack(fmt, r))
	
    def __getitem__(self, k): return self.data[k]
    def __setitem__(self, k, v): self.data[k] = v
    def get(self, k, d): return self.data.get(k, d)
    def keys(self): return self.data.keys()
    def values(self): return self.data.values()
    def items(self): return self.data.items()
    def has_key(self, k): return self.data.has_key(k)

    def __len__(self): return len(self.pack())
    
    def dump(self, f=sys.stdout, verbose=0):
	debug.dump_raw(self.raw, f)

	if verbose and self.fields:
	    f.write('\nfields:\n')
	    fld = self.fields.keys()
	    fld.sort()
	    for k in fld:
		f.write('  %s: %s\n' % (k, self.data[k]))
		
	
# Packing and unpacking aids
def trim_null(s):
    return string.split(s, '\0')[0]

def pad_null(s, l):
    if len(s) > l - 1:
	s = s[:l-1]
    s = s + '\0'
    if len(s) < l: s = s + '\0' * (l - len(s))
    return s

def unpack_category_appinfo(dict, raw):
    d = {'categoryRenamed': [0] * 16,
	 'categoryName': [''] * 16,
	 'categoryID': [0] * 16,
	 'categoryLastUniqueID': 0
	 }
    r = struct.unpack('>H', raw[:2])[0]
    for i in range(0, 16):
	if r & (1 << i):
	    d['categoryRenamed'][i] = 1

	d['categoryName'][i] = trim_null(raw[2+i*16:18+i*16])
	d['categoryID'][i] = int(struct.unpack('>B', raw[258+i])[0])

    d['categoryLastUniqueID'] = int(struct.unpack('>B', raw[274])[0])
    dict.update(d)
    return 278

def pack_category_appinfo(d):
    r = 0
    names = ''
    ids = ''
    for i in range(0, 16):
	if d['categoryRenamed'][i]:
	    r = r | (1 << i)
	names = names + pad_null(d['categoryName'][i], 16)
	ids = ids + chr(d['categoryID'][i])

    return struct.pack('>H',r)+names+ids+struct.pack('>B',d['categoryLastUniqueID'])+'\0\0\0'

	

class Record(Block):
    """Generic database record type."""
    def __init__(self, raw='', index=0, id=0, attr=0, category=0):
	Block.__init__(self, raw)
	self.deleted = (attr & 0x80) != 0
	self.modified = (attr & 0x40) != 0
	self.busy = (attr & 0x20) != 0
	self.secret = (attr & 0x10) != 0
	self.archived = (attr & 0x08) != 0
	self.category = category
	self.index = index
	self.id = id

    def attributes(self):
	a = 0
	if self.deleted: a = a | 0x80
	if self.modified: a = a | 0x40
	if self.busy: a = a | 0x20
	if self.secret: a = a | 0x10
	if self.archived: a = a | 0x08
	return a

    def __repr__(self):
	s = '<%s #%s id %06x cat %s' % (self.__class__.__name__,
					self.index, self.id, self.category)
	if self.deleted: s = s + ' del'
	if self.modified: s = s + ' mod'
	if self.busy: s = s + ' busy'
	if self.secret: s = s + ' sec'
	if self.archived: s = s + ' arc'
	s = s + 'len %s>' % (self.raw and len(self.raw) or 0)
	return s
	    
    def dump(self, f=sys.stdout, verbose=0):
	self.pack()
	a = self.attributes()
	f.write('#%d, %d bytes, cat %d, id 0x%06x, attr 0x%02x' % (self.index,
						      len(self.raw),
						      self.category,
						      self.id,
						      a))
	if a:
	    an = []
	    if self.deleted: an.append('deleted')
	    if self.modified: an.append('modified')
	    if self.busy: an.append('busy')
	    if self.secret: an.append('secret')
	    if self.archived: an.append('archived')
	    f.write(' ('+string.join(an)+')')
	f.write('\n')
	Block.dump(self, f, verbose)

	      
class AppBlock(Block):
    """Generic AppBlock type.  By default, an AppBlock is treated as
    just a hunk of raw data; this class is only provided for the sake
    of consistency."""
    def __repr__(self):
	return '<%s len %s>' % (self.__class__.__name__,
				self.raw and len(self.raw) or 0)
    
    def dump(self, f=sys.stdout, verbose=0):
	self.pack()
	f.write('appinfo: %d bytes\n' % len(self.raw))
	Block.dump(self, f, verbose)

class CategoryAppBlock(AppBlock):
    """An AppBlock with built-in category support."""
    def __init__(self, *a, **kw):
	if not hasattr(self, 'fields'):
	    self.fields = {}

	# need to figure out Palm defaults for this
	self.fields.update({'categoryName': (FLD_LIST, ['']*16, 16, 16),
			    'categoryID': (FLD_LIST, [0]*16, 16, 16),
			    'categoryRenamed': (FLD_LIST, [0]*16, 16, 16),
			    'categoryLastUniqueID': (FLD_BOOLEAN, 0)
			    })
	apply(AppBlock.__init__, (self,)+a, kw)

    def unpack(self, raw):
	self.raw = raw
	l = unpack_category_appinfo(self.data, raw)
	return raw[l:]

    def pack(self):
	return pack_category_appinfo(self.data)
    

class SortBlock(Block):
    """Generic SortBlock type.  By default, a SortBlock is treated as
    just a hunk of raw data; this class is only provided for the sake
    of consistency."""
    def dump(self, f=sys.stdout, verbose=0):
	self.pack()
	f.write('sortinfo: %d bytes\n' % len(self.raw))
	Block.dump(self, f, verbose)

class Resource(Block):
    """Generic resource type."""
    def __init__(self, raw=None, type='    ', id=0):
	Block.__init__(self, raw)
	self.type = type
	self.id = id

    def __repr__(self):
	return "<%s %s %s len %s>" % (self.__class__.__name__,
				      repr(self.type), self.id,
				      self.raw and len(self.raw) or 0)
    
    def dump(self, f=sys.stdout, verbose=0):
	self.pack()
	f.write("resource type %s, id %s, %d bytes\n" % (repr(self.type),
							   self.id,
							   len(self.raw)))
	Block.dump(self, f, verbose)

class PrefBlock(Block):
    """Generic AppPref block."""
    def __init__(self, raw='', creator='    ', id=0, version=0, backup=0):
	Block.__init__(self, raw)
	self.creator = creator
	self.id = id
	self.version = version
	self.backup = backup

    def dump(self, f=sys.stdout, verbose=0):
	self.pack()
	f.write("preference creator %s, id %d, version %d" % (repr(self.creator),
							      self.id,
							      self.version))
	# this is a flag, right???
	if self.backup:
	    f.write(", backup")
	f.write('\n')
	Block.dump(self, f, verbose)
	
