#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2010       Craig J. Anderson
#
# 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
#

# $Id: SubstKeywords.py Version 2010-12-05

"""
Provide the SubstKeywords class that will replace keywords in a passed
string with information about the person/marriage/spouse. For sample:

foo = SubstKeywords(database, person_handle)
print foo.replace_and_clean(['$n was born on $b.'])

Will return a value such as:

Mary Smith was born on 3/28/1923.
"""

#------------------------------------------------------------------------
#
# Gramps modules
#
#------------------------------------------------------------------------
from gen.display.name import displayer as name_displayer
import DateHandler
import gen.lib
from gen.utils import get_birth_or_fallback, get_death_or_fallback

#------------------------------------------------------------------------
#
# Local constants
#
#------------------------------------------------------------------------
class TextTypes():
    """Four enumerations that are used to for the four main parts of a string.
    
    and used for states.  Separator is not used in states.
    text   -> remove or display
    remove -> display
    """
    separator, text, remove, display = range(4)
TXT = TextTypes()


#------------------------------------------------------------------------
#
# Formatting classes
#
#------------------------------------------------------------------------
class GenericFormat(object):
    """A Generic parsing class.  Will be subclassed by specific format strings
    """
    
    def __init__(self, string_in):
        self.string_in = string_in
    
    def _default_format(self, item):
        """ The default format if there is no format string """
        pass
    
    def is_blank(self, item):
        """ if the information is not known (item is None), remove the format
        string information from the input string if any.
        """
        if item is None:
            self.string_in.remove_start_end("(", ")")
            return True
        return False
    
    def generic_format(self, item, code, uppr, function):
        """the main parsing engine.
        
        Nedded ar the following:  the input string
        code - List of one character (string) codes (all lowercase)
        uppr - list of one character (string) codes that can be uppercased
            each needs to have a lowercase equivelant in code
        function - list of functions.
        there is a one to one relationship with character codes and functions.
        """
        if self.string_in.this != "(":
            return self._default_format(item)
        self.string_in.step()
        
        main = VarStringMain(TXT.remove)
        separator = SeparatorParse(self.string_in)
        #code given in args
        #function given in args
        
        while self.string_in.this and self.string_in.this != ")":
            #Check to see if _in.this is in code
            to_upper = False
            if uppr.find(self.string_in.this) != -1:
                #and the result should be uppercased.
                to_upper = True
                where = code.find(self.string_in.this.lower())
            else:
                where = code.find(self.string_in.this)
            if where != -1:
                self.string_in.step()
                tmp = function[where]()
                if to_upper:
                    tmp = tmp.upper()
                main.add_variable(tmp)
            elif separator.is_a():
                separator.parse_format(main)
            else:
                self.string_in.parse_format(main)
        
        if self.string_in.this == ")":
            self.string_in.step()
        
        if main.the_state == TXT.remove:
            return ""
        else:
            return main.get_string()[1]

#------------------------------------------------------------------------
# Name Format strings
#------------------------------------------------------------------------
class NameFormat(GenericFormat):
    """ The name format class.
    If no format string, the name is displayed as per preferance options
    otherwise, parse through a format string and put the name parts in
    """

    def get_name(self, person):
        """ A helper method for retreaving the persons name """
        if person:
            return person.get_primary_name()
        return None

    def _default_format(self, name):
        """ display the name as set in preferences """
        return name_displayer.sorted_name(name)
    
    def parse_format(self, name):
        """ Parse the name """
        if self.is_blank(name):
            return ""
        
        def common():
            """ return the common name of the person """
            return (name.get_call_name() or
                     name.get_first_name().split(' ')[0])

        code  = "tfcnxslg"
        upper = code.upper()
        function = [name.get_title,           #t
                    name.get_first_name,      #f
                    name.get_call_name,       #c
                    name.get_nick_name,       #n
                    common,                   #x
                    name.get_suffix,          #s
                    name.get_surname,         #l
                    name.get_family_nick_name #g
                    ]
        
        return self.generic_format(name, code, upper, function)

#------------------------------------------------------------------------
# Date Format strings
#------------------------------------------------------------------------
class DateFormat(GenericFormat):
    """ The date format class.
    If no format string, the date is displayed as per preferance options
    otherwise, parse through a format string and put the date parts in
    """

    def get_date(self, event):
        """ A helper method for retreaving a date from an event """
        if event:
            return event.get_date_object()
        return None

    def _default_format(self, date):
        return DateHandler.displayer.display(date)
    
    def __count_chars(self, char, max_amount):
        """ count the year/month/day codes """
        count = 1  #already have seen/passed one
        while count < max_amount and self.string_in.this == char:
            self.string_in.step()
            count = count +1
        return count
    
    def parse_format(self, date):
        """ Parse the name """
    
        if self.is_blank(date):
            return ""
        
        def year():
            """  The year part only """
            year = unicode(date.get_year())
            count = self.__count_chars("y", 4)
            if year == "0":
                return ""
            
            if count == 1:  #found 'y'
                if len(year) == 1:
                    return year
                elif year[-2] == "0":
                    return year[-1]
                else:
                    return year[-2:]
            elif count == 2:  #found 'yy'
                tmp = "0" + year
                return tmp[-2:]
            elif count == 3:  #found 'yyy'
                if len(year) > 2:
                    return year
                else:
                    tmp = "00" + year
                    return tmp[-3:]
            else:  #count == 4  #found 'yyyy'
                tmp = "000" + year
                return tmp[-4:]


        def month(char_found = "m"):
            """  The month part only """
            month = unicode(date.get_month())
            count = self.__count_chars(char_found, 4)
            if month == "0":
                return ""
                
            if count == 1:
                return month
            elif count == 2:  #found 'mm'
                tmp = "0" + month
                return tmp[-2:]
            elif count == 3:   #found 'mmm'
                return DateHandler.displayer.short_months[int(month)]
            else: #found 'mmmm'
                return DateHandler.displayer.long_months[int(month)]
        
        def month_up():
            return month("M").upper()
            

        def day():
            """  The day part only """
            day = unicode(date.get_day())
            count = self.__count_chars("d", 2)
            if day == "0":  #0 means not defined!
                return ""
            
            if count == 1: #found 'd'
                return day
            else:  #found 'dd'
                tmp = "0" + day
                return tmp[-2:]


        def modifier():
            #ui_mods taken from date.py def lookup_modifier(self, modifier):
            ui_mods = ["", _("before"), _("after"), _("about"), 
                       "", "", ""]
            return ui_mods[date.get_modifier()].capitalize()

        
        code  = "ymdMo"
        upper = "O"
        function = [year, month, day, month_up, modifier]
        
        return self.generic_format(date, code, upper, function)

#------------------------------------------------------------------------
# Place Format strings
#------------------------------------------------------------------------
class PlaceFormat(GenericFormat):
    """ The place format class.
    If no format string, the place is displayed as per preferance options
    otherwise, parse through a format string and put the place parts in
    """

    def get_place(self, database, event):
        """ A helper method for retreaving a place from an event """
        if event:
            bplace_handle = event.get_place_handle()
            if bplace_handle:
                return database.get_place_from_handle(bplace_handle)
        return None

    def _default_format(self, place):
        return place.get_title()
    
    def parse_format(self, place):
        """ Parse the place """

        if self.is_blank(place):
            return ""
        
        code = "elcuspnitxy"
        upper = code.upper()
        function = [place.get_main_location().get_street,
                    place.get_main_location().get_locality,
                    place.get_main_location().get_city,
                    place.get_main_location().get_county,
                    place.get_main_location().get_state,
                    place.get_main_location().get_postal_code,
                    place.get_main_location().get_country,
                    place.get_main_location().get_parish,
                    place.get_title,
                    place.get_longitude,
                    place.get_latitude
                    ]
        
        return self.generic_format(place, code, upper, function)

#------------------------------------------------------------------------
# Event Format strings
#------------------------------------------------------------------------
class EventFormat(GenericFormat):
    """ The event format class.
    If no format string, the event description is displayed
    otherwise, parse through the format string and put in the parts
        dates and places can have their own format strings
    """

    def __init__(self, database, _in):
        self.database = database
        GenericFormat.__init__(self, _in)
        
    def _default_format(self, event):
        if event is None:
            return ""
        else:
            return event.get_description()
    
    def __empty_format(self):
        """ clear out a sub format string """
        self.string_in.remove_start_end("(", ")")
        return ""
    
    def __empty_attrib(self):
        """ clear out an attribute name """
        self.string_in.remove_start_end("[", "]")
        return ""

    def parse_format(self, event):
        """ Parse the event format string.
        let the date or place classes handle any sub-format strings """

        if self.is_blank(event):
            return ""
        
        def format_date():
            """ start formatting a date in this event """
            date_format = DateFormat(self.string_in)
            return date_format.parse_format(date_format.get_date(event))
            
        def format_place():
            """ start formatting a place in this event """
            place_format = PlaceFormat(self.string_in)
            place = place_format.get_place(self.database, event)
            return place_format.parse_format(place)
                
        def format_attrib():
            """ Get the name and then get the attributes value """
            #Event's Atribute
            attrib_parse = AttributeParse(self.string_in)
            self.string_in.step()
            name = attrib_parse.get_name()
            if name:
                return attrib_parse.get_attribute(event.get_attribute_list(),
                                                  name)
            else:
                return ""
            
        code = "ndDia"
        upper = ""
        function = [event.get_description,
                    format_date,
                    format_place,
                    event.get_gramps_id,
                    format_attrib
                    ]
        
        return self.generic_format(event, code, upper, function)

    def parse_empty(self):
        """ remove the format string """
        
        code = "dDa"
        function = [self.__empty_format, self.__empty_format,
                    self.__empty_attrib]
        
        return self.generic_format(None, code, "", function)
        
#------------------------------------------------------------------------
#
# ConsumableString - Input string class
#
#------------------------------------------------------------------------
class ConsumableString(object):
    """
    A simple string implemplamentation with extras to help with parsing.
    
    This will contain the string to be parsed.  or string in.
    There will only be one of these for each processed line.
    """
    def __init__(self, string):
        self.__this_string = string
        self.__setup()
    
    def __setup(self):
        """ update class attributes this and next """
        if len(self.__this_string) > 0:
            self.this = self.__this_string[0]
        else:
            self.this = None
        if len(self.__this_string) > 1:
            self.next = self.__this_string[1]
        else:
            self.next = None
    
    def step(self):
        """ remove the first char from the string """
        self.__this_string = self.__this_string[1:]
        self.__setup()
        return self.this
    
    def step2(self):
        """ remove the first two chars from the string """
        self.__this_string = self.__this_string[2:]
        self.__setup()
        return self.this
    
    def remove_start_end(self, start, end):
        """ Removes a start, end block from the string if there """
        if self.this == start:
            self.text_to_next(end)
    
    def __get_a_char_of_text(self):
        """ Removes one char of TEXT from the string and returns it. """
        if self.this == "\\":
            if self.next == None:
                rtrn = "\\"
            else:
                rtrn = self.next
            self.step2()
        else:
            rtrn = self.this
            self.step()
        return rtrn 
    
    def text_to_next(self, char):
        """ remove a format strings from here """
        new_str = ""
        while self.this is not None and self.this != char:
            new_str += self.__get_a_char_of_text()
        if self.this == char:
            self.step()
        return new_str
    
    def is_a(self):
        return True
    
    def parse_format(self, _out):
        rtrn = self.__get_a_char_of_text()

        if rtrn:
            _out.add_text(rtrn)
            

#------------------------------------------------------------------------
#
# Output string classes
#
#------------------------------------------------------------------------
#------------------------------------------------------------------------
# VarStringBase classes
#------------------------------------------------------------------------
class VarStringBase(object):
    """
    A list to hold tuple object (integer from TextTypes, string)
    
    This will contain the string that will be displayed.  or string out.
    it is used for groups and format strings.
    """
    def __init__(self, init_state):
        self.the_string = []
        self.the_state = init_state
    
    def add_text(self, text):
        pass
    
    def add_display(self, text):
        pass
    
    def add_remove(self):
        pass
    
    def add_separator(self, text):
        pass
    
    def get_string(self):
        """ Get the final displayed string """
        if self.the_string == []:
            return self.the_state, ""
        return self.the_state, self.the_string[0][1]
        
    def _last_type(self):
        """ get the type of the last item added to the list """
        if self.the_string != []:
            return self.the_string[-1][0]
        else:
            return None

#------------------------------------------------------------------------
# VarStringMain classes
#------------------------------------------------------------------------
class VarStringMain(VarStringBase):
    """
    The main level string out (Not within a group or format string).
    
    This group differs from the others as it starts with
     TXT.text as the state.  (format strings will use TXT.remove)
     and everything is __combine[d] properly
    """
    def __init__(self, start_state = TXT.text):
        VarStringBase.__init__(self, start_state)
        self.level = self
    
    def merge(self, acquisition):
        """ Merge the content of acquisition into this place. """
        if acquisition.the_state != TXT.display:
            return
        
        if acquisition.the_state > self.the_state:
            self.the_state = acquisition.the_state
            
        for (adding, txt_to_add) in acquisition.the_string:
            if adding == TXT.text:
                self.add_text(txt_to_add)
            elif adding == TXT.display:
                self.add_display(txt_to_add)
            elif adding == TXT.remove:
                self.add_remove()
            elif adding == TXT.separator:
                self.add_separator(txt_to_add)
    
    def __add(self, text):
        """  Add a new piece of text to the existing (at end). or add new """
        if self.the_string == []:
            self.the_string.append((self.the_state, text))
        else:
            self.the_string[-1] = \
                (TXT.text, self.the_string[-1][1] + text)
        
    def __combine(self, text):
        """ combine the last two elements on the list and add text to it """
        new_str = self.the_string[-1][1]
        self.the_string.pop()
        self.__add(new_str + text)
        
    def add_text(self, text):
        """ a new piece of text to be added.
        remove a (non displaying) variable at the end if there is one.
        or accept a separator (at the end) if there is one.
        """
        to_the_left = self._last_type()
        if to_the_left == TXT.remove:
            self.the_string.pop()
            to_the_left = self._last_type()
        
        if to_the_left is None:
            self.the_string.append((TXT.text, text))
        elif to_the_left == TXT.text:
            self.the_string[-1] = \
                (TXT.text, self.the_string[-1][1] + text)
        elif to_the_left == TXT.separator:
            self.__combine(text)

        
    def add_separator(self, text):
        """ a new separator to be added.
        remove a (non displaying) variable to the end it there is one
        and drop me
        remove an existing separator at the end (if there)
        or add me to the end.
        """
        to_the_left = self._last_type()
        if to_the_left == TXT.remove:
            self.the_string.pop()
            return
        
        if self.the_string == []:
            return
        elif to_the_left == TXT.text:
            self.the_string.append((TXT.separator, text))
        elif to_the_left == TXT.separator:
            self.the_string.pop()
            self.the_string.append((TXT.separator, text))
        
    def add_display(self, text):
        """ add text to the end and update the state """
        self.the_state = TXT.display
        self.add_text(text)
        
    def add_remove(self):
        """ work with an empty variable.  either:
            bump up the state if needed
            remove an empty variable on the end if one
            remove a separator at the end if one and myself and stop
            and add a empty variable to the end.
        """
        if self.the_state != TXT.display:
            self.the_state = TXT.remove
        
        to_the_left = self._last_type()
        if to_the_left == TXT.separator:
            self.the_string.pop()
            return
        elif to_the_left == TXT.remove:
            self.the_string.pop()
        
        self.the_string.append((TXT.remove, ""))
    
    def add_variable(self, text):
        """ A helper fuction to either call:
            add_remove if the text string is blank
            otherwise add_display
        """
        if text == "":
            self.add_remove()
        else:
            self.add_display(text)
    
#------------------------------------------------------------------------
# VarStringSecond classes
#------------------------------------------------------------------------
class VarStringSecond(VarStringBase):
    """The non-main level string out (within a group or format string).
    
    This group differs from main as it starts with
     TXT.remove as the state
     and everything is simply added to the end of a list.
     states are still updated properly
     it will be _combine(d) in the main level approprately.
    """
    def __init__(self, start_state = TXT.remove):
        VarStringBase.__init__(self, start_state)
    
    def merge(self, acquisition):
        """ Merge the content of acquisition into this place. """
        if acquisition.the_state != TXT.display:
            return
        
        if acquisition.the_state > self.the_state:
            self.the_state = acquisition.the_state
            
        self.the_string.extend(acquisition.the_string)
    
    def add_text(self, text):
        to_the_left = self._last_type()
        if to_the_left is None:
            self.the_string.append((TXT.text, text))
        elif to_the_left == TXT.text:
            self.the_string[-1] = \
                (TXT.text, self.the_string[-1][1] + text)
        else:
            self.the_string.append((TXT.text, text))
        
    def add_separator(self, text):
        self.the_string.append((TXT.separator, text))
        
    def add_display(self, text):
        self.the_state = TXT.display
        self.add_text(text)
        
    def add_remove(self):
        """ add a 'remove' tag in the list """
        if self.the_state != TXT.display:
            self.the_state = TXT.remove
        self.the_string.append((TXT.remove, ""))
    
    def add_variable(self, text):
        if text == "":
            self.add_remove()
        else:
            self.add_display(text)
    

#------------------------------------------------------------------------
#
# Parsers
#
#------------------------------------------------------------------------
#------------------------------------------------------------------------
# level_parse
#------------------------------------------------------------------------
class LevelParse(object):
    """The main string out class.  This class does two things
    
    provides text handling of {} and adds/removes/combines levels as needed.
    provides outside methods add_... to the last (innermost) level.
    """

    def __init__(self, consumer_in):
        self._in = consumer_in
        if consumer_in.this == "{":
            self.__levels = [VarStringMain(TXT.remove)]
        else:
            self.__levels = [VarStringMain(TXT.display)]
        self.level = self.__levels[-1]
    
    def is_a(self):
        return self._in.this == "{" or self._in.this == "}"
    
    def __can_combine(self):
        return len(self.__levels) > 1
    
    def __combine_level(self):
        """ If the last (inner most) level is not to be removed,
        combine it to the left"""
        if not self.__can_combine():
            return
        self.__levels[-2].merge(self.__levels[-1])
        self.__levels.pop()
        self.level = self.__levels[-1]
    
    def combine_all(self):
        while len(self.__levels) > 1:
            self.__combine_level()
            
    def add_text(self, text):
        self.level.add_text(text)
        
    def add_separator(self, text):
        self.level.add_separator(text)
        
    def add_display(self, text):
        self.level.add_display(text)
        
    def add_remove(self):
        self.level.add_remove()
    
    def add_variable(self, text):
        self.level.add_variable(text)

    def get_string(self):
        return self.level.get_string()
    
    def parse_format(self, _out):  #_out is not used
        """ Parse the text to see what to do
        Only handles {}
        """
        if self._in.this == "{":
            self.__levels.append(VarStringSecond())
            self.level = self.__levels[-1]
        
        elif self._in.this == "}":
            if self.__can_combine():
                self.__combine_level()
            else:
                self.level.add_text("}")
        self._in.step()
        
#------------------------------------------------------------------------
# SeparatorParse
#------------------------------------------------------------------------
class SeparatorParse(object):
    """ parse out a separator """
    def __init__(self, consumer_in):
        self._in = consumer_in
        
    def is_a(self):
        return self._in.this == "<"

    def parse_format(self, _out):
        """ get the text and pass it to string_out.separator """
        self._in.step()
        _out.add_separator(
            self._in.text_to_next(">"))
        
#------------------------------------------------------------------------
# AttributeParse
#------------------------------------------------------------------------
class AttributeParse(object):
    """  Parse attributes """
    
    def __init__(self, consumer_in):
        self._in = consumer_in
        
    def get_name(self):
        """ Gets a name inside a [] block """
        if self._in.this != "[":
            return ""
        self._in.step()
        return self._in.text_to_next("]")
        
    def get_attribute(self, attrib_list, attrib_name):
        """ Get an attribute by name """
        if attrib_name == "":
            return ""
        for attr in attrib_list:
            if str(attr.get_type()) == attrib_name:
                return str(attr.get_value())
        return ""
    
    def is_a(self):
        """ check """
        return self._in.this == "a"
    
    def parse_format(self, _out, attrib_list):
        """ Get the attribute and add it to the string out """
        name = self.get_name()
        _out.add_variable(
            self.get_attribute(attrib_list, name))
        
#------------------------------------------------------------------------
# VariableParse
#------------------------------------------------------------------------
class VariableParse(object):
    """ Parse the individual variables """
    
    def __init__(self, friend, database, consumer_in):
        self.friend = friend
        self.database = database
        self._in = consumer_in
    
    def is_a(self):
        """ check """
        return self._in.this == "$" and self._in.next is not None and \
                              "nsijbBdDmMvVauetT".find(self._in.next) != -1

    def get_event_by_type(self, marrage, e_type):
        """ get an event from a type """
        if marrage is None:
            return None
        for e_ref in marrage.get_event_ref_list():
            if not e_ref:
                continue
            event = self.friend.database.get_event_from_handle(e_ref.ref)
            if event.get_type() == e_type:
                return event
        return None
    
    def get_event_by_name(self, person, event_name):
        """ get an event from a name. """
        for e_ref in person.get_event_ref_list():
            if not e_ref:
                continue
            event = self.friend.database.get_event_from_handle(e_ref.ref)
            if event.get_type().is_type(event_name):
                return event
        return None
    
    def empty_item(self, item, _out):
        """ return false if there is a valid item(date or place).
        Otherwise
            add a TXT.remove marker in the output string
            remove any format strings from the input string
        """
        if item is not None:
            return False
        
        _out.add_remove()
        self._in.remove_start_end("(", ")")
        return True
    
    def empty_attribute(self, person, _out):
        """ return false if there is a valid person.
        Otherwise
            add a TXT.remove marker in the output string
            remove any attribute name from the input string
        """
        if person:
            return False
        
        _out.add_remove()
        self._in.remove_start_end("[", "]")
        return True
    
    def __parse_date(self, event, _out):
        """ sub to process a date
        Given an event, get the date object, process the format,
        pass the result to string_out"""
        date_f = DateFormat(self._in)
        date = date_f.get_date(event)
        if self.empty_item(date, _out):
            return
        _out.add_variable( date_f.parse_format(date) )
    
    def __parse_place(self, event, _out):
        """ sub to process a date
        Given an event, get the place object, process the format,
        pass the result to string_out"""
        place_f = PlaceFormat(self._in)
        place = place_f.get_place(self.database, event)
        if self.empty_item(place, _out):
            return
        _out.add_variable( place_f.parse_format(place) )
    
    def __parse_name(self, person):
        name_format = NameFormat(self._in)
        name = name_format.get_name(person)
        return name_format.parse_format(name)
    
    def __parse_id(self, first_class_object):
        if first_class_object is not None:
            return first_class_object.get_gramps_id()
        else:
            return ""
    
    def __parse_event(self, person):
        event = self.get_event_by_name(person, attrib_parse.get_name())
        event_f = EventFormat(self.database, self._in)
        if event:
            return event_f.parse_format(event)
        else:
            event_f.parse_empty()
            return ""
    
    def parse_format(self, _out):
        """Parse the $ variables. """
        if not self.is_a():
            return
        
        attrib_parse = AttributeParse(self._in)
        next_char = self._in.next
        self._in.step2()
        
        if next_char == "n":
            #Person's name
            _out.add_variable(
                self.__parse_name(self.friend.person))
        elif next_char == "s":
            #Souses name
            _out.add_variable(
                self.__parse_name(self.friend.spouse))
        
        elif next_char == "i":
            #Person's Id
            _out.add_variable(
                self.__parse_id(self.friend.person))
        elif next_char == "j":
            #Marriage Id
            _out.add_variable(
                self.__parse_id(self.friend.family))

        elif next_char == "b":
            #Person's Birth date
            if self.empty_item(self.friend.person, _out):
                return
            self.__parse_date(
                get_birth_or_fallback(self.friend.database, self.friend.person),
                _out)
        elif next_char == "d":
            #Person's Death date
            if self.empty_item(self.friend.person, _out):
                return
            self.__parse_date(
                get_death_or_fallback(self.friend.database, self.friend.person),
                _out)
        elif next_char == "m":
            #Marriage date
            if self.empty_item(self.friend.family, _out):
                return
            self.__parse_date(
                self.get_event_by_type(self.friend.family,
                                       gen.lib.EventType.MARRIAGE),
                _out)
        elif next_char == "v":
            #Divorce date
            if self.empty_item(self.friend.family, _out):
                return
            self.__parse_date(
                self.get_event_by_type(self.friend.family,
                                       gen.lib.EventType.DIVORCE),
                _out)
        elif next_char == "T":
            #Todays date
            date_f = DateFormat(self._in)
            from gen.lib.date import Today
            date = Today()
            if self.empty_item(date, _out):
                return
            _out.add_variable( date_f.parse_format(date) )

        elif next_char == "B":
            #Person's birth place
            if self.empty_item(self.friend.person, _out):
                return
            self.__parse_place(
                get_birth_or_fallback(self.friend.database, self.friend.person),
                _out)
        elif next_char == "D":
            #Person's death place
            if self.empty_item(self.friend.person, _out):
                return
            self.__parse_place(
                get_death_or_fallback(self.friend.database, self.friend.person),
                _out)
        elif next_char == "M":
            #Marriage place
            if self.empty_item(self.friend.family, _out):
                return
            self.__parse_place(
                self.get_event_by_type(self.friend.family,
                                       gen.lib.EventType.MARRIAGE),
                _out)
        elif next_char == "V":
            #Divorce place
            if self.empty_item(self.friend.family, _out):
                return
            self.__parse_place(
                self.get_event_by_type(self.friend.family,
                                       gen.lib.EventType.DIVORCE),
                _out)

        elif next_char == "a":
            #Person's Atribute
            if self.empty_attribute(self.friend.person, _out):
                return
            attrib_parse.parse_format(_out,
                                      self.friend.person.get_attribute_list())
        elif next_char == "u":
            #Marriage Atribute
            if self.empty_attribute(self.friend.family, _out):
                return
            attrib_parse.parse_format(_out,
                                      self.friend.family.get_attribute_list())
        
        elif next_char == "e":
            #person event
            _out.add_variable(
                self.__parse_event(self.friend.person))
        elif next_char == "t":
            #person event
            _out.add_variable(
                self.__parse_event(self.friend.family))
        

#------------------------------------------------------------------------
#
# SubstKeywords
#
#------------------------------------------------------------------------
class SubstKeywords(object):
    """Accepts a person/family with format lines and returns a new set of lines
    using variable substitution to make it.
    
    The individual variables are defined with the classes that look for them.
    
    Needed:
        Database object
        person_handle
            This will be the center person for the display
        family_handle
            this will specify the specific family/spouse to work with.
            If none given, then the first/prefered family/spouse is used
    """
    def __init__(self, database, person_handle, family_handle=None):
        """get the person and find the family/spouse to use for this display"""
        
        self.database = database
        self.person = database.get_person_from_handle(person_handle)
        self.family = None
        self.spouse = None
        self.line = None   #Consumable_string - set below
        
        if self.person is None:
            return
        
        fam_hand_list = self.person.get_family_handle_list()
        if fam_hand_list:
            if family_handle in fam_hand_list:
                self.family = database.get_family_from_handle(family_handle)
            else: 
                #Error.  [0] may give wrong marriage info.
                #only here because of OLD specifications.  Specs read:
                # * $S/%S 
                #   Displays the name of the person's preferred ...
                # 'preferred' means FIRST.  
                #The first might not be the correct marriage to display.
                #else: clause SHOULD be removed.
                self.family = database.get_family_from_handle(fam_hand_list[0])
            
            father_handle = self.family.get_father_handle()
            mother_handle = self.family.get_mother_handle()
            self.spouse = None
            if father_handle == person_handle:
                if mother_handle:
                    self.spouse = database.get_person_from_handle(mother_handle)
            else:
                if father_handle:
                    self.spouse = database.get_person_from_handle(father_handle)
    
    def __main_level(self):
        """parse each line of text and return the new displayable line
        
        There are four things we can find here
            A {} group which will make/end as needed.
            A <> separator
            A !  variable - Handlded seprately
            or text
        """
        #This is the upper most level of text
        #main = var_string_out()
        main = LevelParse(self.line)
        check = [main,
                 SeparatorParse(self.line),
                 VariableParse(self, self.database, self.line),
                 self.line]
        
        while self.line.this:
            for tmp in check:
                if tmp.is_a():
                    tmp.parse_format(main)
                    break
           
        main.combine_all()

        state, line = main.get_string()
        if state is TXT.remove:
            return None
        else:
            return line
    
    def replace_and_clean(self, lines):
        """
        return a new array of lines with all of the substitutions done
        """
        new = []
        for this_line in lines:
            if this_line == "":
                new.append(this_line)
                continue
            #print "- ", this_line
            self.line = ConsumableString(this_line)
            new_line = self.__main_level()
            #print "+ ", new_line
            if new_line is not None:
                new.append(new_line)
        
        if new == []:
            new = [u""]
        return new


#Acts 20:35 (New International Version)
#In everything I did, I showed you that by this kind of hard work
#we must help the weak, remembering the words the Lord Jesus himself
#said: 'It is more blessed to give than to receive.'


if __name__ == '__main__':
#-------------------------------------------------------------------------
#
# For Testing everything except VariableParse, SubstKeywords and EventFormat
# apply it as a script:
#
#     ==> in command line do "PYTHONPATH=??? python libsubstkeyword.py"
#
# You will need to put in your own path to the src directory
#
#-------------------------------------------------------------------------
    # pylint: disable-msg=C0103

    def combinations(c, r):
        # combinations('ABCD', 2) --> AB AC AD BC BD CD
        # combinations(range(4), 3) --> 012 013 023 123
        pool = tuple(range(c))
        n = len(pool)
        if r > n:
            return
        indices = range(r)
        yield tuple(pool[i] for i in indices)
        while True:
            for i in reversed(range(r)):
                if indices[i] != i + n - r:
                    break
            else:
                return
            indices[i] += 1
            for j in range(i+1, r):
                indices[j] = indices[j-1] + 1
            yield tuple(pool[i] for i in indices)

    def main_level_test(_in, testing_class, testing_what):
        """This is a mini def __main_level(self):
        """
        main = LevelParse(_in)
        sepa = SeparatorParse(_in)
        test = testing_class(_in)
        
        while _in.this:
            if main.is_a():
                main.parse_format(_in)
            elif sepa.is_a():
                sepa.parse_format(main)
            elif _in.this == "$":
                _in.step()
                main.add_variable(
                    test.parse_format(testing_what))
            else:
                _in.parse_format(main)

        main.combine_all()

        state, line = main.get_string()
        if state is TXT.remove:
            return None
        else:
            return line


    from gen.lib.date import Date
    y_or_n = ()
    date_to_test = Date()
    def date_set():
        date_to_test.set_yr_mon_day(
            1970 if 0 in y_or_n else 0,
            9 if 1 in y_or_n else 0,
            3 if 2 in y_or_n else 0
            )
        #print date_to_test
    
    line_in = "<Z>$(yyy) <a>$(<Z>Mm)<b>$(mm){<c>$(d)}{<d>$(yyyy)<e>}<f>$(yy)"
    consume_str = ConsumableString(line_in)
    
    print line_in
    print "#None are known"
    tmp = main_level_test(consume_str, DateFormat, date_to_test)
    print tmp
    print "Good" if tmp == " " else "!! bad !!"

    
    print
    print
    print "#One is known"
    answer = []
    for y_or_n in combinations(3, 1):
        date_set()
        consume_str = ConsumableString(line_in)
        tmp = main_level_test(consume_str, DateFormat, date_to_test)
        print tmp
        answer.append(tmp)
    print "Good" if answer == [
        "1970 d1970f70",
        " a99b09",
        " c3"
        ] else "!! bad !!"


    print
    print
    print "#Two are known"
    answer = []
    for y_or_n in combinations(3, 2):
        date_set()
        consume_str = ConsumableString(line_in)
        tmp = main_level_test(consume_str, DateFormat, date_to_test)
        print tmp
        answer.append(tmp)
    print "Good" if answer == [
        "1970 a99b09d1970f70",
        "1970 c3d1970f70",
        " a99b09c3"
        ] else "!! bad !!"


    print
    print
    print "#All are known"
    answer = []
    y_or_n = (0, 1, 2)
    date_set()
    consume_str = ConsumableString(line_in)
    tmp = main_level_test(consume_str, DateFormat, date_to_test)
    print tmp
    answer.append(tmp)
    print "Good" if answer == ["1970 a99b09c3d1970f70"
        ] else "!! bad !!"

    import sys
    sys.exit()
    print
    print
    print "============="
    print "============="

    from gen.lib.name import Name
    y_or_n = ()
    name_to_test = Name()
    def name_set():
        #code  = "tfcnxslg"
        name_to_test.set_call_name("Bob" if 0 in y_or_n else "")
        name_to_test.set_title("Dr." if 1 in y_or_n else "")
        name_to_test.set_first_name("Billy" if 2 in y_or_n else "")
        name_to_test.set_nick_name("Buck" if 3 in y_or_n else "")
        name_to_test.set_suffix("IV" if 4 in y_or_n else "")
        #now can we put something in for the last name?
        name_to_test.set_family_nick_name("The Clubs" if 5 in y_or_n else "")
    
    line_in = "{$(c)$(t)<1>{<2>$(f)}{<3>$(n){<0> <0>}<4>$(x)}$(s)<5>$(l)<6>$(g)<0>"
    consume_str = ConsumableString(line_in)
    
    print
    print
    print line_in
    print "#None are known"
    tmp = main_level_test(consume_str, NameFormat, name_to_test)
    print tmp
    print "Good" if tmp == None else "!! bad !!"


    print
    print
    print "#Two are known"
    answer = []
    for y_or_n in combinations(6, 2):
        name_set()
        consume_str = ConsumableString(line_in)
        tmp = main_level_test(consume_str, NameFormat, name_to_test)
        print tmp
        answer.append(tmp)
    print "Good" if answer == [
        "BobDr.4Bob",
        "Bob2Billy4Bob",
        "Bob3Buck4Bob",
        "Bob4BobIV",
        "Bob4BobThe Clubs",
        "Dr.2Billy4Billy",
        "Dr.3Buck",
        "Dr.1IV",
        "Dr.6The Clubs",
        "Billy3Buck4Billy",
        "Billy4BillyIV",
        "Billy4BillyThe Clubs",
        "BuckIV",
        "BuckThe Clubs",
        "IV6The Clubs"
        ] else "!! bad !!"


    print
    print
    print "#All are known"
    y_or_n = (0, 1, 2, 3, 4, 5)
    name_set()
    consume_str = ConsumableString(line_in)
    answer = main_level_test(consume_str, NameFormat, name_to_test)
    print answer
    print "Good" if answer == "BobDr.2Billy3Buck4BobIV6The Clubs" \
                            else "!! bad !!"


    print
    print
    print "============="
    print "============="

    from gen.lib.place import Place
    y_or_n = ()
    place_to_test = Place()
    def place_set():
        #code = "elcuspnitxy"
        main_loc = place_to_test.get_main_location()
        main_loc.set_street(
            "Lost River Ave." if 0 in y_or_n else ""
        )
        main_loc.set_locality(
            "Second district" if 1 in y_or_n else ""
        )
        main_loc.set_city(
            "Arco" if 2 in y_or_n else ""
        )
        main_loc.set_county(
            "Butte" if 3 in y_or_n else ""
        )
        main_loc.set_state(
            "Idaho" if 4 in y_or_n else ""
        )
        main_loc.set_postal_code(
            "83213" if 5 in y_or_n else ""
        )
        main_loc.set_country(
            "USA" if 6 in y_or_n else ""
        )
        main_loc.set_parish(
            "St Anns" if 7 in y_or_n else ""
        )
        place_to_test.set_title(
            "Atomic City" if 8 in y_or_n else ""
        )
        place_to_test.set_longitude(
            "N43H38'5\"N" if 9 in y_or_n else ""
        )
        place_to_test.set_latitude(
            "W113H18'5\"W" if 10 in y_or_n else ""
        )
    
    #code = "txy"
    line_in = "$(e)<1>{<2>$(l) <3> $(c)<4><0><5>{$(s)<6>$(p)<7>" + \
              "{<1>$(n)<2>}<3>$(i<0>)<4>}<5>$(t)<6>$(x)<7>}<8>$(y)"
    consume_str = ConsumableString(line_in)
    
    print
    print
    print line_in
    print "#None are known"
    tmp = main_level_test(consume_str, PlaceFormat, place_to_test)
    print tmp
    print "Good" if tmp == "" else "!! bad !!"


    print
    print
    print "#Three are known (string lengths only)"
    answer = []
    for y_or_n in combinations(11, 4):
        place_set()
        consume_str = ConsumableString(line_in)
        tmp = main_level_test(consume_str, PlaceFormat, place_to_test)
        #print tmp
        answer.append(len(tmp))
    print answer
    print "Good" if answer == [38, 44, 44, 42, 46, 50, 49, 50, 40, 40, 38, 42,
        46, 45, 46, 46, 44, 48, 52, 51, 52, 44, 48, 52, 51, 52, 46, 50, 49, 50,
        54, 53, 54, 57, 58, 57, 28, 28, 26, 30, 34, 33, 34, 34, 32, 36, 40, 39,
        40, 32, 36, 40, 39, 40, 34, 38, 37, 38, 42, 41, 42, 45, 46, 45, 30, 28,
        32, 36, 35, 36, 28, 32, 36, 35, 36, 30, 34, 33, 34, 38, 37, 38, 41, 42,
        41, 34, 38, 42, 41, 42, 36, 40, 39, 40, 44, 43, 44, 47, 48, 47, 36, 40,
        39, 40, 44, 43, 44, 47, 48, 47, 42, 41, 42, 45, 46, 45, 49, 50, 49, 53,
        28, 28, 26, 30, 34, 33, 34, 34, 32, 36, 40, 39, 40, 32, 36, 40, 39, 40,
        34, 38, 37, 38, 42, 41, 42, 45, 46, 45, 30, 28, 32, 36, 35, 36, 28, 32,
        36, 35, 36, 30, 34, 33, 34, 38, 37, 38, 41, 42, 41, 34, 38, 42, 41, 42,
        36, 40, 39, 40, 44, 43, 44, 47, 48, 47, 36, 40, 39, 40, 44, 43, 44, 47,
        48, 47, 42, 41, 42, 45, 46, 45, 49, 50, 49, 53, 19, 17, 21, 25, 24, 25,
        17, 21, 25, 24, 25, 19, 23, 22, 23, 27, 26, 27, 30, 31, 30, 23, 27, 31,
        30, 31, 25, 29, 28, 29, 33, 32, 33, 36, 37, 36, 25, 29, 28, 29, 33, 32,
        33, 36, 37, 36, 31, 30, 31, 34, 35, 34, 38, 39, 38, 42, 19, 23, 27, 26,
        27, 21, 25, 24, 25, 29, 28, 29, 32, 33, 32, 21, 25, 24, 25, 29, 28, 29,
        32, 33, 32, 27, 26, 27, 30, 31, 30, 34, 35, 34, 38, 27, 31, 30, 31, 35,
        34, 35, 38, 39, 38, 33, 32, 33, 36, 37, 36, 40, 41, 40, 44, 33, 32, 33,
        36, 37, 36, 40, 41, 40, 44, 38, 39, 38, 42, 46] else "!! bad !!"

