# Spanakopita: a tool for merging and maintaining GEDCOM files.
# Copyright (C) Niko Matsakis 2007
# 
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or (at
# your option) any later version.
# 
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
# USA


"""
This file defines the data structures we use for storing GEDCOM
information.  In one sense, we describe a GEDCOM file as a tree of
records each of which has (tag, value) pairs.  However, we sometimes
use special sub-classes for particular tags such as Individuals and
Families.  These sub-classes then add convenient properties.

The base class for all data we read in is GEDCOMDataStructure.  It
defines generic methods that walk the list of sub-structures, add
children with a given tag, and things like that.

When a GEDCOM is parsed, the root of the GEDCOM tree is an instance of
GEDCOMDatabase.

Basic fields
------------

Every GEDCOMDataStructure offers a few useful fields:
  .database:     the GEDCOMDatabase to which this node belongs.
  .arg:          a text string containing the "argument" from the
                 GEDCOM file.
  .tag:          the tag that defined this entity
  .databaseId:   the unique id for this node in the db, or None

The last entry (databaseId) is (at least initially) assigned by the
parser.  The list of sub-elements is .fields, it should be
read/written with the appropriate accessors!

Parsing and Data Storage
------------------------

Any data structure we create while parsing is a descendant of the
GEDCOMDataStructure class and shares the same storage mechanism.  Each
one contains all the information about itself and a list of its
children.  Each object knows its own tag, argument, and database id.
Objects rarely interpret their own tags, that usually identifies the
object's purpose to its parent.

All of the parsing is basically table-driven.  Each class contains a
hashtable named 'parseTable'.  The table is keyed by a String
representing a valid label from the GEDCOM object; so, for example,
the object representing FAMilies has a parseTable with entries for
'HUSB', 'WIFE', and other tags.  The value of each entry is a tuple
with three pieces of information --- the class that can parse the
contents, and a min-max pair that indicates how many instances of that
label can be parsed.

References are lazilly handled, meaning that we simply store the text
of the label and look it up in a hashtable whenever we want to
deref it, as opposed to linking directly to the resulting
object.

If we ever see any tags that aren't in the parse table for a given
element, we just use Unparsed, which performs a very minimal
interpretation of the data and simply lets us pass it through
unchanged.

Most of the data structures amount to an transliteration of the
specification.  The classes all have counterparts to the structures in
the specification, though some of the python classes map to more than
one structure in the spec.  I usually named the python class the same
as the corresponding structure in the spec, so for example an
INDIVIDUAL_EVENT_STRUCTURE is called 'IndividualEvent'.
"""

import Foundation, objc
import re, sys, os.path

Unlimited = sys.maxint

def isRef(str):
    """ Returns true if 'str' is of the form for a GEDCOM cross-reference,
    which looks like @x111@ """
    if not len(str): return 0
    return str[0] == '@' and str[-1] == '@'

class GEDCOMError(Exception):
    pass

def makeNSMutableArray(lst):
    # Converts a Python list into an "NSMutableArrays"
    res = Foundation.NSMutableArray.array()
    for item in lst: res.append(item)
    return res

class GEDCOMDataStructure(Foundation.NSObject):

    """ A base class for all gedcom data structures.  Each gedcom data
    structure knows its tag, id, argument, and children.  The tag is
    the three or four letter GEDCOM string, like 'INDI', that created
    this structure.  The 'id' is the @xxx@ id (if any) assigned to
    this structure.  The argument is whatever text came after the tag,
    and the children are the other child tags.  The children are
    stored in a simple list.  This preserves the order that those
    structures were read in, and allows for more accurate output.  See
    the larger explanation at the top of the file for details on how
    the karpathos interface is implemented.

    The parser treats all GEDCOMDataStructures the same way.
    Basically, it calls 'consumeEvent()' with each parser event that
    belongs to it.  These routines process and add any data to the
    association list.

    This class contains the machinery for the assoc list."""

    def initWithDatabase_event_(self, database, event):
        # The assoc list is list of tuples.  The first element of each
        # tuple is the tag (a string), the second is an object from this file.
        self.setFields_([])
        self.database = database
        self.setArg_(event.arg if event else None)
        self.databaseId = None # initialized by the parser
        return self

    @objc.accessor
    def arg(self):
        return self.a_arg
    
    @objc.accessor
    def setArg_(self, arg):
        self.a_arg = arg

    @objc.accessor
    def fields(self):
        return self.a_fields

    @objc.accessor
    def setFields_(self, flds):
        self.a_fields = makeNSMutableArray(flds)

    def addField_(self, fld):
        self.willChangeValueForKey_("fields")
        self.a_fields.addObject_(fld)
        self.didChangeValueForKey_("fields")

    def allTagged(self, tags):
        """ Returns a list of all data objects with one of the given
        tags. """
        return [f.deref() for f in self.fields() if f.tag in tags]

    def first(self, tag, default=None):

        """ Returns the first data object with one the given tag, or
        default if none found"""
        
        for fdata in self.fields():
            if fdata.tag == tag: return fdata.deref()
        return default

    def add(self, tag, data):

        """ Adds a field with a given tag and data; sets the 'tag'
        field of the data, which is expected to be a
        GEDCOMDataStructure of the correct type as described in the
        parse table. """

        assert (
            (tag not in self.parseTable) or
            (isinstance(data, self.parseTable[tag][0])))

        data.tag = tag

        self.addField_(data)
        return

    def addRefTo(self, tag, data):

        """ Adds a field with a given tag and data; sets the 'tag' field
        of the data, which is expected to be a GEDCOMDataStructure """

        # First determine which kind of reference class to use:
        #   (for example, a FAMC link uses ChildToFamily)
        if tag in self.parseTable:
            ref_cls = self.parseTable[0]
        else:
            ref_cls = Reference

        rdata = ref_cls(self.database, None)
        rdata.arg = "@%s@" % data.databaseId
        rdata.tag = tag
        self.addField_(rdata)
        return

    def delTaggedWithData(self, tags, data):

        """ Removes the entry with tag in the list 'tags' and data 'data', or a
        link to to 'data' """

        self.setFields_([
            f for f in self.fields
            if f.tag not in tags or f.deref() != data ])

    def delTagged(self, tags):

        """ Removes any fields with tags from the given list """

        self.setFields_([
            f for f in self.fields
            if f.tag not in tags ])

    def deref(self):

        """ Typically returns self.  Some objects represent references,
        however, and in that case this returns the object we point at. """
        
        return self

    @objc.accessor
    def identity(self):
        if hasattr(self, 'databaseId'):
            return self.databaseId
        return None

    # -----------------------------------
    # Deleting an item
    #
    # Deleting an object is hard because it can
    # be referenced from all over the tree,
    # so we have to a DFS.

    def delete(self, obj):

        """
        Deletes the object 'obj' and any references to it from the
        descendent of this node.
        """
        
        if self == obj:
            return
        self.setFields_([f for f in self.fields if f.deref() != obj])
        for f in self.fields():
            f.delete()

    # -----------------------------------
    # Generic Parser Routines
    #
    # The parsing is all driven off an attribute
    # table which each class must define.  The
    # table should map a label to tuple (action,
    # min, max).  The action is a class constructor
    # for an appropriate handler.  The min and max
    # are the number of times that particular event may be
    # received.

    def getHandler(self, parser, event):

        """ Given an event that we received, constructs the object
        that should handle the event (and all its subevents).  The
        default impl simply reads from parseTable and almost never
        needs to be modified.  If the name of a substructure is
        unknown, it will fall back to Unparsed."""

        if event.label in self.parseTable:
            handlerClass = self.parseTable[event.label][0]
        else:
            handlerClass = Unparsed
        return handlerClass.alloc().initWithDatabase_event_(
            parser.database, event)

    def consumeEvent(self, parser, event):

        """ Called for all events that occur at our indentation level;
        the default impl calls getHandler() to receive a handler and
        pushes it onto the parser's stack """
        
        try:
            obj = self.getHandler(parser, event)
            self.add(event.label, obj)
            return obj
        except KeyError:
            raise GEDCOMError('Invalid label %s' % event.label)
        return

    def finalize(self):

        """ Called when all events for a particular element have been
        found; the default impl walks the handlers and verifies that their
        counts are appropriate.  This is purely a syntax correctness checking
        measure.  Others may override to do important work. """

        counts = {}
        for fdata in self.fields():
            try:
                counts[fdata.tag] += 1
            except KeyError:
                counts[fdata.tag] = 1

        # Now compare to what they are supposed to be:
        for fname, (func, min, max) in self.parseTable.items():
            try:
                count = counts[fname]
                if count < min or count > max:
                    raise GEDCOMError(('Label %s occurs %d times, '+
                                        'but should have occurred '
                                        '%d-%d times') %
                                       (fname, count, min, max))
            except KeyError:
                if min > 0:
                    raise GEDCOMError('Missing required tag %s' % fname)

    # Generic Output Routines
    # -----------------------

    def dump(self, out, level):

        """ Outputs the data for this element, including its tag, argument,
        id, and children. """

        if self.databaseId:
            out.write("%d @%s@ %s" % (level, self.databaseId, self.tag))
        else:
            out.write("%d %s" % (level, self.tag))

        if self.arg(): out.write(" %s" % self.arg())
        out.write("\n")

        for child in self.fields(): child.dump(out, level + 1)
        return

# Simple Structures ----------------------------------------------------
#
# These are basically strings with some form. 

class String(GEDCOMDataStructure):

    """ Expects simply one argument with no children """

    parseTable = {}

    def set(self, newval):
        self.setArg_(newval)
        return

    def __str__(self):
        return self.arg()

class Reference(GEDCOMDataStructure):

    """ Expects simply one argument with no children """

    parseTable = {}

    def deref(self):
        return self.database.get(self.arg())

class Name(String):

    """ GEDCOM names use a convention of highlighting the surname with
    slahes, like so: Niko /Matsakis/.  This class includes properties that
    access the surname separately from the other names. """

    namere = re.compile('(?P<before>[^/]*)/(?P<last>[^/]*)/(?P<after>[^/]*)')

    @objc.accessor
    def slashrep(self):
        return self.arg()

    @objc.accessor
    def setSlashrep_(self, arg):
        self.setArg_(arg)

    @objc.accessor
    def surname(self):
        match = self.namere.match(self.arg())
        if not match:
            return ""
        return match.group('last')

    @objc.accessor
    def otherNames(self):
        match = self.namere.match(self.arg())
        if not match:
            return ""
        otherNames = match.group('before')
        a = match.group('after')
        if a: otherNames += a
        return otherNames

    def __str__(self):
        return filter(lambda c: c != '/', self.arg())

PhoneNumber = String
Place = String
Age = String

class Date(String):

    """ TODO --- try to parse date strings, etc """

    @objc.accessor
    def year(self):
        return self.arg()[-4:] # lame

# Certain structures are merely recorded with absolutely no interpretation
# or parsing going on.  These are all versions of Unparsed; some of them
# have helpful names so we can fill in the details later. 

class Unparsed(GEDCOMDataStructure):

    parseTable = {}

    def deref(self):
        if isRef(self.arg()):
            return self.database.get(self.arg())
        return self
    
    def finalize(self):
        """ Unparsed does no validation """
        return
    
Address = Unparsed
IndividualAttribute = Unparsed
Change = Unparsed
Association = Unparsed
ResponsibleAgency = Unparsed
SourceCitation = Unparsed
HusbAge = Unparsed
WifeAge = Unparsed
AdoptionFamily = Unparsed

# Notes ----------------------------------------------------------------

class ContinuedString(String):
    def __str__(self):
        return "\n%s" % self.arg()

class Note(GEDCOMDataStructure):

    parseTable = { 'CONC':(String,0,Unlimited),
                    'CONT':(ContinuedString,0,Unlimited),
                    'CHAN':(Change,0,1),
                    }

    def __str__(self):
        # Construct the string by combining the argument with
        # any continuations.  Note that 'CONT' entries are a
        # ContinuedString, which automatically adds a '\n'
        res = self.arg()
        for additional in self.allTagged(('CONC', 'CONT')):
            res = res + str(additional)
        return res

# Multimedia ---------------------------------------------------------------
#
# Top-level multimedia objects correspond either to links, or data blobs.
# Right now we basically don't parse these.

MultimediaRecord = Unparsed

# MultimediaLink -----------------------------------------------------------
#
# See 55gcch2.htm#MULTIMEDIA_LINK
# 
# These may be either links to external data or inlined data.  A link
# to to internal data has no internal structure, but a link to
# external data has a few fields.  Therefore, in the constructor we
# choose which parse table to use.

class MultimediaLink(GEDCOMDataStructure):

    parseTable = {'FORM':(String,1,1),
                   'TITL':(String,0,1),
                   'FILE':(String,1,1),
                   'NOTE':(Note,0,Unlimited),
                   }

    empty_parseTable = {}

    def initWithDatabase_event_(self, database, event):
        GEDCOMDataStructure.initWithDatabase_event_(self, database, event)
        
        # If this is a link to an embedded multimedia object, then
        # it should have no internal fields, so overwrite the parse table
        # on this instance with an empty one:
        if self.arg():
            self.parseTable = self.empty_parseTable
            
        return self

    @objc.accessor
    def url(self):
        return str(self.first('FILE'))

    @objc.accessor
    def caption(self):
        # concatenate all NOTE fields into one caption
        notes = self.allTagged(('NOTE',))
        return " ".join(map(str, notes))

# Individual to Family Links(FAMS, FAMC) ----------------------------------

class IndToFamily(Reference):

    """ Base class for FAMC and FAMS links, which basically
    works the same way. """

    parseTable = { 'NOTE':(Note,0,Unlimited) }

class ChildToFamily(IndToFamily):
    parseTable = IndToFamily.parseTable
    parseTable['PEDI'] =(Unparsed,0,Unlimited)

class SpouseToFamily(IndToFamily):
    pass

# Events --------------------------------------------------------------

class Event(GEDCOMDataStructure):

    """ The contents of this correspond to EVENT_DETAIL.  At parse
    time, a subclass of this is always constructed, which should
    contain an event_types hashtable that can translation to a longer
    description based on the starting label. When building structures
    later, Events are constructed all on their own. """

    parseTable = { 'TYPE':(String, 0, 1),
                   'DATE':(Date, 0, 1),
                   'PLAC':(Place, 0, 1),
                   'ADDR':(Address, 0, 1),
                   'PHON':(PhoneNumber, 0, 3),
                   'AGE':(Age, 0, 1),
                   'AGNC':(ResponsibleAgency, 0, 1),
                   'CAUS':(String, 0, 1),
                   'SOUR':(SourceCitation, 0, Unlimited),
                   'OBJE':(MultimediaLink, 0, Unlimited),
                   'NOTE':(Note, 0, Unlimited) }

    def initWithDatabase_event_(self, database, event):
        GEDCOMDataStructure.initWithDatabase_event_(self, database, event)
        if event: self.type = self.event_types [event.label]
        return self

    @objc.accessor
    def description(self):
        return self.first('TYPE')

    @objc.accessor
    def date(self):
        return self.first('DATE', None)

    @objc.accessor
    def place(self):
        return self.first('PLAC', None)

    @objc.accessor
    def notes(self):
        return self.allTagged(('NOTE',))

    def _determine_type_label(self, label):
        for evtypeset in(IndividualEvent.event_types,
                          FamilyEvent.event_types):
            if res: break
            for key, val in evtypeset.items():
                if val == type:
                    return key
        return None

class FamilyEvent(Event):

    """ Family events look like events, but they can have additional
    fields indicating the age of the husb and wife at the time. """

    event_types = { 'ANUL': 'Anullment',
                    'CENS': 'Censoring',
                    'DIV':  'Divorce',
                    'DIVF': 'DIVF?',
                    'ENGA': 'Engaged',
                    'MARR': 'Married',
                    'MARB': 'MARB?',
                    'MARC': 'MARC?',
                    'MARL': 'MARL?',
                    'MARS': 'MARS?',
                    'EVEN': 'Other' }

    parseTable = dict(Event.parseTable)
    parseTable['HUSB'] = (HusbAge, 0, 1)
    parseTable['WIFE'] = (WifeAge, 0, 1)

class IndividualEvent(Event):

    """ Individual events are just like any other event """

    event_types = { 'BIRT': 'Birth',
                    'CHR':  'Christening',
                    'DEAT': 'Death',
                    'BURI': 'Burial',
                    'CREM': 'Cremation',
                    'ADOP': 'Adoption',
                    'BAPM': 'Baptism',
                    'BARM': 'Bar Mitzfah',
                    'BASM': 'Bats Mitzfah',
                    'BLES': 'Blessing',
                    'CHRA': 'CHRA?',
                    'CONF': 'Confirmation',
                    'FCOM': 'FCOM?',
                    'ORDN': 'Ordination',
                    'NATU': 'Naturalization',
                    'EMIG': 'Emigration',
                    'IMMI': 'Immigration',
                    'CENS': 'Censoring',
                    'PROB': 'Probation',
                    'WILL': 'Will',
                    'GRAD': 'Graduation',
                    'RETI': 'Retirement',
                    'EVEN': 'Other' }

def _new_individual_event(database, tag):
    event = IndividualEvent(database, None)
    event.type = IndividualEvent.event_types[tag]
    return event

class BirthEvent(IndividualEvent):

    """ Birth events can have a 'FAMC' pointer, such as BIRT and CHR """

    parseTable = dict(IndividualEvent.parseTable)
    parseTable['FAMC'] = (Reference, 0, 1)

    def initWithDatabase_tag_(self, database, tag):
        self.initWithDatabase_event_(database, None)
        self.type = IndividualEvent.event_types[tag]
        date = Date.alloc().initWithDatabase_event_(database, None)
        date.setArg_("?")
        self.add('DATE', date)
        return self

class AdoptionEvent(IndividualEvent):

    """ Adoption events can specify a family they were adopted into,
    and optionally a parent within that family.  This corresponds to
    IndividualEvent 'ADOP'. """

    parseTable = dict(IndividualEvent.parseTable)
    parseTable['FAMC'] =(AdoptionFamily, 0, 1)

# Records -----------------------------------------------

class Family(GEDCOMDataStructure):

    parseTable = { 'HUSB':(Reference,0,1),
                   'WIFE':(Reference,0,1),
                   'CHIL':(Reference,0,Unlimited),
                   'NCHI':(String,0,1), # basically ignored
                   'NOTE':(Note,0,Unlimited),
                   'OBJE':(MultimediaLink,0,Unlimited),
                   'CHAN':(Change,0,1),
                   
                   # Family Events
                   'ANUL':(FamilyEvent,0,Unlimited),
                   'CENS':(FamilyEvent,0,Unlimited),
                   'DIV':(FamilyEvent,0,Unlimited),
                   'DIVF':(FamilyEvent,0,Unlimited),
                   'ENGA':(FamilyEvent,0,Unlimited),
                   'MARR':(FamilyEvent,0,Unlimited),
                   'MARB':(FamilyEvent,0,Unlimited),
                   'MARC':(FamilyEvent,0,Unlimited),
                   'MARL':(FamilyEvent,0,Unlimited),
                   'MARS':(FamilyEvent,0,Unlimited),
                   'EVEN':(FamilyEvent,0,Unlimited)
                   
                   }
    
    @objc.accessor
    def parents(self):
        return self.allTagged(('HUSB', 'WIFE'))

    @objc.accessor
    def children(self):
        return self.allTagged(('CHIL',))

    @objc.accessor
    def events(self):
        return self.allTagged(('ANUL', 'CENS', 'DIV', 'DIVF',
                                'ENGA', 'MARR', 'MARB', 'MARC',
                                'MARL', 'MARS', 'EVEN'))

    @objc.accessor
    def notes(self):
        return self.allTagged(('NOTE',))

    def addChild(self, child):
        """ Adds an individual as a child of this family. """
        assert isinstance(child, Individual)

        # delete any existing links between this child/family
        #    (there may be a link in 1 direction but not the other!)
        child.delTaggedWithData(('FAMC',), self)
        self.delTaggedWithData(('CHIL',), self)

        # add links in both directions
        child.addRefTo('FAMC', self)
        self.addRefTo('CHIL', child)

    def addParent(self, parent):

        # delete any existing links between this spouse/family
        #    (there may be a link in 1 direction but not the other!)
        parent.delTaggedWithData(('FAMS',), self)
        self.delTaggedWithData(('HUSB','WIFE'), parent)

        # check whether we have space for an additional parent
        if len(self.parents) >= 2:
            raise GEDCOMError(
                ("Unable to add %r as a parent to family %r, " + 
                 "because it has 2 parents already %r") % (
                parent, self, self.parents))

        # create the FAMS link from parent to us
        parent.addRefTo('FAMS', self)

        # determine whether to fit this parent as a HUSB or WIFE
        #    (we prefer to find a spot with the correct gender,
        #     but if that doesn't work out we'll try to live with it)
        if parent.isMale():
            prefer, other = "HUSB", "WIFE"
        else:
            prefer, other = "WIFE", "HUSB"
        prev_value = self.first(prefer)

        # if the preferred slot is available, take it
        if not prev_value:
            self.addRefTo(prefer, parent)
            return

        # otherwise, see if the preferred slot has the incorrect sex
        # and boot them out of the preferred slot if so
        if parent.sex in ('M', 'F') and prev_value.sex != parent.sex:
            self.delTagged(("HUSB", "WIFE"))
            self.addRefTo(prefer, parent)
            self.addRefTo(other, prev_value)
            return

        # if not, just stick the parent in the non-preferred slot
        #    (Note: it must be vacant because there are < 2 parents)
        assert not self.first(other)
        self.addRefTo(other, parent)

class Individual(GEDCOMDataStructure):

    # Reference 55gcch2.htm#INDIVIDUAL_RECORD
    parseTable = { 'SEX':(String,0,1),
                    'ALIA':(Reference,0,Unlimited),
                    'NOTE':(Note,0,Unlimited),
                    'OBJE':(MultimediaLink,0,Unlimited),
                    'CHAN':(Change,0,1),
                    'FAMC':(ChildToFamily,0,Unlimited),
                    'FAMS':(SpouseToFamily,0,Unlimited),
                    'NAME':(Name,0,Unlimited),

                    # Individual Event Structure
                    # 55gcch2.htm#INDIVIDUAL_EVENT_STRUCTURE
                    'BIRT':(BirthEvent, 0, Unlimited),
                    'CHR':(BirthEvent, 0, Unlimited),
                    'DEAT':(IndividualEvent, 0, Unlimited),
                    'BURI':(IndividualEvent, 0, Unlimited),
                    'CREM':(IndividualEvent, 0, Unlimited),
                    'ADOP':(AdoptionEvent, 0, Unlimited),
                    'BAPM':(IndividualEvent, 0, Unlimited),
                    'BARM':(IndividualEvent, 0, Unlimited),
                    'BASM':(IndividualEvent, 0, Unlimited),
                    'BLES':(IndividualEvent, 0, Unlimited),
                    'CHRA':(IndividualEvent, 0, Unlimited),
                    'CONF':(IndividualEvent, 0, Unlimited),
                    'FCOM':(IndividualEvent, 0, Unlimited),
                    'ORDN':(IndividualEvent, 0, Unlimited),
                    'NATU':(IndividualEvent, 0, Unlimited),
                    'EMIG':(IndividualEvent, 0, Unlimited),
                    'IMMI':(IndividualEvent, 0, Unlimited),
                    'CENS':(IndividualEvent, 0, Unlimited),
                    'PROB':(IndividualEvent, 0, Unlimited),
                    'WILL':(IndividualEvent, 0, Unlimited),
                    'GRAD':(IndividualEvent, 0, Unlimited),
                    'RETI':(IndividualEvent, 0, Unlimited),
                    'EVEN':(IndividualEvent, 0, Unlimited),

                    # Individual Attribute Structure
                    # 55gcch2.htm#INDIVIDUAL_ATTRIBUTE_STRUCTURE
                    'CAST':(IndividualAttribute, 0, Unlimited),
                    'DSCR':(IndividualAttribute, 0, Unlimited),
                    'EDUC':(IndividualAttribute, 0, Unlimited),
                    'IDNO':(IndividualAttribute, 0, Unlimited),
                    'NATI':(IndividualAttribute, 0, Unlimited),
                    'NCHI':(IndividualAttribute, 0, Unlimited),
                    'NMR':(IndividualAttribute, 0, Unlimited),
                    'OCCU':(IndividualAttribute, 0, Unlimited),
                    'PROP':(IndividualAttribute, 0, Unlimited),
                    'RELI':(IndividualAttribute, 0, Unlimited),
                    'RESI':(IndividualAttribute, 0, Unlimited),
                    'SSN':(IndividualAttribute, 0, Unlimited),
                    'TITL':(IndividualAttribute, 0, Unlimited),
                    }
                    
    @objc.accessor
    def sex(self):
        return str(self.first('SEX', '?')).upper()

    @objc.accessor
    def isMale(self):
        return self.sex == "M"
    
    @objc.accessor
    def isFemale(self):
        return self.sex == "F"
    
    @objc.accessor
    def name(self):
        return self.first('NAME', 'Unknown')

    @objc.accessor
    def birthDate(self):
        event = self.first('BIRT')
        if not event:
            # add a birth day
            event = BirthEvent.alloc().initWithDatabase_tag_(
                self.database, 'BIRT')
            self.add('BIRT', event)
        return event.date()

    @objc.accessor
    def deathDate(self):
        event = self.first('DEAT')
        if not event: return None
        return event.date()

    @objc.accessor
    def spouses(self):
        fams = self.familiesWhereSpouse()
        if not fams: return makeNSMutableArray([])
        def find_spouse(fam):
            parents = fam.parents()
            otherParents = [p for p in parents if p is not self]
            if not otherParents: return None
            return otherParents[0]
        spouses = [find_spouse(f) for f in fams]
        return makeNSMutableArray([s for s in spouses if s])

    @objc.accessor
    def spousesNames(self):
        return ",".join([s.name().slashrep() for s in self.spouses()])

    @objc.accessor
    def isDead(self):
        return bool(self.first('DEAT'))

    @objc.accessor
    def events(self):
        return self.allTagged(('EVEN', 'CHR', 'DEAT', 'BURI',
                                'CREM', 'ADOP', 'BAPM', 'BARM',
                                'BASM', 'BLES', 'CHRA', 'CONF',
                                'FCOM', 'ORDN', 'NATU', 'EMIG',
                                'IMMI', 'CENS', 'PROB', 'WILL',
                                'GRAD', 'RETI', 'BIRT'))

    @objc.accessor
    def notes(self):
        return self.allTagged(('NOTE',))

    @objc.accessor
    def familiesWhereChild(self):
        """ All families where this individual is a child """
        return self.allTagged(('FAMC',))

    @objc.accessor
    def familiesWhereSpouse(self):
        """ All families where this individual is a spouse """
        return self.allTagged(('FAMS',))

    @objc.accessor
    def parents(self):
        res = []
        for fam in self.familiesWhereChild():
            res += fam.parents
        return res

# ----------------------------------------------------------------------
# Database

class GEDCOMDatabase(GEDCOMDataStructure):

    """ The GEDCOM version of a karpathos database.  It uses __getattr__
    to define the 'individuals' and 'families' lists, which are used
    internally by the karpathos database accessors.  Unlike the other
    """
    
    parseTable = { 'FAM':(Family,0,Unlimited),
                   'INDI':(Individual,0,Unlimited),
                   'NOTE':(Note,0,Unlimited),
                   'OBJE':(MultimediaRecord,0,Unlimited),
                   
                   # Terminate the parser
                   'TRLR':(String,0,1)
                   }

    def initWithParser_(self, parser):
        GEDCOMDataStructure.initWithDatabase_event_(self, self, None)
        self.refTable = parser.refTable
        self.nextpersonid = 0
        self.nextfamilyid = 0
        return self

    def get(self, id):
        """ Looks up a GEDCOM object by its ID.  Throws a GEDCOMError
        if no entity with that id exists.  Accepts the id either in
        raw form of in internal '@XXX@' form. """
        if not isRef(id):
            id = "@%s@" % (id,)
        try:
            return self.refTable[id]
        except KeyError:
            raise GEDCOMError("ID %s not found" % id)

    @objc.accessor
    def individuals(self):
        """ List of all individuals """
        return self.allTagged(('INDI',))

    @objc.accessor
    def families(self):
        """ List of all families """
        return self.allTagged(('FAM',))

    def finalize(self):
        """
        The GEDCOM spec is a bit ambiguous about certain things.  For example,
        there are links from a FAMILY to an INDIVIDUAL, AND reverse links,
        but it's unclear what is supposed to happen if one such link does
        not exist.  This method just iterates over and ensures that all
        reciprocal properties exist on both side.
        """

        for fam in self.families():
            for child in fam.children():
                if fam not in child.familiesWhereChild():
                    fam.addChild(child)   # reconstruct links in both dirs

            for parent in fam.parents():
                if fam not in parent.familiesWhereSpouse():
                    fam.addParent(parent) # reconstruct links in both dirs

        for ind in self.individual():
            for fam in ind.familiesWhereChild():
                if ind not in fam.children():
                    fam.addChild(ind)
                if ind not in fam.parents():
                    fam.addParent(ind)

    # Output
    # ------
    
    def output(self, out):
        for child in self.fields(): child.dump(out, 0)
