#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2003-2004  Donald N. Allingham
#
# 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
#
# Statistics plugin (w) 2004 by Eero Tamminen.
# Partially based on code from the Timeline graph plugin.
#
# $Id: StatisticsChart.py,v 1.1.2.1 2004/10/07 19:25:20 eerot Exp $

"""
Statistics report
"""

#------------------------------------------------------------------------
#
# python modules
#
#------------------------------------------------------------------------
import os
import time
from gettext import gettext as _

#------------------------------------------------------------------------
#
# GNOME/gtk
#
#------------------------------------------------------------------------
import gtk

#------------------------------------------------------------------------
#
# GRAMPS modules
#
#------------------------------------------------------------------------
import const	# need gender names
import RelLib	# need Person internals for getting gender / gender name
import Utils
import Report
import BaseDoc
import GenericFilter
import Errors
import Date
import sort
from QuestionDialog import ErrorDialog

#------------------------------------------------------------------------
#
# Module globals
#
#------------------------------------------------------------------------

# sort type identifiers
_SORT_VALUE = 'VAL'
_SORT_KEY = 'KEY'

# needs to be global for the lookup_value_compare()
_lookup_items = {}

def lookup_value_compare(a, b):
    "compare given keys according to corresponding _lookup_items values"
    if _lookup_items[a] < _lookup_items[b]:
	return -1
    if _lookup_items[a] == _lookup_items[b]:
	return 0
    return 1

#------------------------------------------------------------------------
#
# point to centimeter convertion
#
#------------------------------------------------------------------------
def pt2cm(val):
    return (float(val)/28.3465)

#------------------------------------------------------------------------
#
# Statistics
#
#------------------------------------------------------------------------
class Statistics:

    def __init__(self, title, year_from, year_to, items, sort, reverse, document, output, newpage=0):
        """
        Creates the Statistics object that produces the report. This class
        is used by the StatisticsDialog class. The arguments are:

	title     - what data is collected
	year_from - year to start from
	year_to   - year to end to
        items     - dictionary of statistics items and their counts
	sort      - whether items are sorted by key or value
	reverse   - whether sorting happens in reverse order
        output    - name of the output file
        document  - BaseDoc instance for the output file. Any class derived
                   from BaseDoc may be used.
        """
	if title[1]:
	    self.title = "%s (%s): %04d-%04d" % (title[0], title[1], year_from, year_to)
	else:
	    self.title = "%s: %04d-%04d" % (title[0], year_from, year_to)
	self.items = items
	# generate item sorting index
	self.index_items(sort, reverse)
	
        self.d = document
        self.output = output
        self.newpage = newpage
        self.setup()
        if output:
            self.standalone = 1
            self.d.open(output)
            self.d.init()
        else:
            self.standalone = 0

	    
    def index_items(self, sort, reverse):
	"""creates & stores a sorted index for the items"""
	global _lookup_items

	# sort by item keys
	index = self.items.keys()
	index.sort()
	if reverse:
	    index.reverse()

	if sort == _SORT_VALUE:
	    # set for the sorting function
	    _lookup_items = self.items
	    
	    # then sort by value
	    index.sort(lookup_value_compare)
	    if reverse:
		index.reverse()

	self.index = index

	
    def setup(self):
        """
        Define the graphics styles used by the report. Paragraph definitions
        have already been defined in the document. The styles used are:

        SC-bar - A red bar with 0.5pt black line.
        SC-text  - Contains the SC-Name paragraph style used for
                the individual's name
        SC-title - Contains the SC-Title paragraph style used for
                the title of the document
        """
	g = BaseDoc.GraphicsStyle()
        g.set_line_width(0.8)
        g.set_color((0,0,0))
        g.set_fill_color((255,0,0))
        self.d.add_draw_style("SC-bar",g)

        g = BaseDoc.GraphicsStyle()
        g.set_paragraph_style("SC-Text")
        g.set_color((0,0,0))
        g.set_fill_color((255,255,255))
        g.set_line_width(0)
        self.d.add_draw_style("SC-text",g)

        g = BaseDoc.GraphicsStyle()
        g.set_paragraph_style("SC-Title")
        g.set_color((0,0,0))
        g.set_fill_color((255,255,255))
        g.set_line_width(0)
        g.set_width(self.d.get_usable_width())
        self.d.add_draw_style("SC-title",g)

	
    def write_report(self):
	"output the selected statistics..."

        font = self.d.style_list['SC-Text'].get_font()

	# set layout variables
	width = self.d.get_usable_width()
        row_h = pt2cm(font.get_size())
	max_y = self.d.get_usable_height() - row_h
        pad =  row_h * 0.5
        
	# calculate maximum key string size
        max_size = 0
	max_value = 0
        for key in self.index:
            max_size = max(self.d.string_width(font, key), max_size)
	    max_value = max(self.items[key], max_value)
	# horizontal area for the gfx bars
        start = pt2cm(max_size) + 1.0
        size = width - 1.5 - start

	# start page
        if self.newpage:
            self.d.page_break()
        self.d.start_page()

	# start output
	self.d.center_text('SC-title', self.title, width/2, 0)
	#print self.title

	yoffset = pt2cm(self.d.style_list['SC-Title'].get_font().get_size())
	for key in self.index:
	    yoffset += (row_h + pad)
	    if yoffset > max_y:
		# for graphical report, page_break() doesn't seem to work
		self.d.end_page()
		self.d.start_page()
		yoffset = 0

	    # right align the text to the value
	    x = start - pt2cm(self.d.string_width(font, key)) - 1.0
            self.d.draw_text('SC-text', key, x, yoffset)
	    #print key + ":",
	    
	    value = self.items[key]
	    stop = start + (size * value / max_value)
	    path = ((start, yoffset),
		    (stop, yoffset),
		    (stop, yoffset + row_h),
		    (start, yoffset + row_h))
	    self.d.draw_path('SC-bar', path)
            self.d.draw_text('SC-text', str(value), stop + 0.5, yoffset)
	    #print "%d/%d" % (value, max_value)
            
        self.d.end_page()    
        if self.standalone:
            self.d.close()

	return


#------------------------------------------------------------------------
#
# 
#
#------------------------------------------------------------------------
def _make_default_style(default_style):
    """Make the default output style for the Statistics report."""
    f = BaseDoc.FontStyle()
    f.set_size(10)
    f.set_type_face(BaseDoc.FONT_SERIF)
    p = BaseDoc.ParagraphStyle()
    p.set_font(f)
    p.set_alignment(BaseDoc.PARA_ALIGN_RIGHT)
    p.set_description(_("The style used for the items and values."))
    default_style.add_style("SC-Text",p)

    f = BaseDoc.FontStyle()
    f.set_size(14)
    f.set_type_face(BaseDoc.FONT_SANS_SERIF)
    p = BaseDoc.ParagraphStyle()
    p.set_font(f)
    p.set_alignment(BaseDoc.PARA_ALIGN_CENTER)
    p.set_description(_("The style used for the title of the page."))
    default_style.add_style("SC-Title",p)

#------------------------------------------------------------------------
#
# Builds filter list for this report
#
#------------------------------------------------------------------------
def _get_report_filters(person):
    """Set up the list of possible content filters."""

    name = person.getPrimaryName().getName()
        
    all = GenericFilter.GenericFilter()
    all.set_name(_("Entire Database"))
    all.add_rule(GenericFilter.Everyone([]))

    des = GenericFilter.GenericFilter()
    des.set_name(_("Descendants of %s") % name)
    des.add_rule(GenericFilter.IsDescendantOf([person.getId(), 1]))

    ans = GenericFilter.GenericFilter()
    ans.set_name(_("Ancestors of %s") % name)
    ans.add_rule(GenericFilter.IsAncestorOf([person.getId(), 1]))

    com = GenericFilter.GenericFilter()
    com.set_name(_("People with common ancestor with %s") % name)
    com.add_rule(GenericFilter.HasCommonAncestorWith([person.getId()]))

    return [all, des, ans, com]

#------------------------------------------------------------------------
#
# Data extraction methods from the database
#
#------------------------------------------------------------------------

class Extract:
    def __init__(self):
        """Methods for extracting statistical data from the database"""

    def estimate_age(self, person, date):
	"""Utility method to estimate person's age at given date:
	person -- person whose age is to be estimated
	date -- date at which the age should be estimated
	This expects that Person's birth and the date argument are
	using the same calendar and that between those two dates
	there haven't been any calendar discontinuations."""
	birth = person.getBirth().getDateObj()
	if not (date.getYearValid() and birth.getYearValid()):
	    return _("Missing date(s)")
	age = date.getYear() - birth.getYear()
	if date.getMonthValid() and birth.getMonthValid():
	    if date.getMonth() < birth.getMonth():
		age -= 1
	    else:
		if (date.getMonth() == birth.getMonth() and
		date.getDayValid() and birth.getDayValid() and
		date.getDay() < birth.getDay()):
		    age -= 1
	if age >= 0:
	    return str(age)
	else:
	    return _("Invalid date(s)")

    def methods(self):
        """returns extraction methods and tuple of their names"""
        return [
	    (self.title, _("Titles")),
            (self.forename, _("Forenames")),
            (self.birth_year, _("Birth years")),
            (self.death_year, _("Death years")),
            (self.birth_month, _("Birth months")),
            (self.death_month, _("Death months")),
            (self.death_age, _("Estimated ages at death")),
            #(self.marriage_age, _("TODO: Estimated (first) marriage ages")),
            #(self.first_child_age, _("TODO: Estimated ages for bearing the first child")),
            #(self.last_child_age, _("TODO: Estimated Ages for bearing the last child")),
	    #(self.child_count, _("TODO: Number of children")),
	    #(self.death_cause, _("TODO: Cause of death")),
            (self.gender, _("Genders"))
        ]

    def title(self, person):
	title = person.getPrimaryName().getTitle()
	if title:
	    return [title]
	else:
	    return [_("Person's missing (preferred) title")]
	
    def forename(self, person):
	# because this returns list, other methods return list too
	firstnames = person.getPrimaryName().getFirstName().strip()
	if firstnames:
	    return [name.capitalize() for name in firstnames.split()]
	else:
	    return [_("Person's missing (preferred) forename")]

    def birth_year(self, person):
	year = person.getBirth().getDateObj().getYear()
	if year != Date.UNDEF:
	    return [str(year)]
	else:
	    return [_("Person's missing birth year")]

    def death_year(self, person):
	year = person.getDeath().getDateObj().getYear()
	if year != Date.UNDEF:
	    return [str(year)]
	else:
	    return [_("Person's missing death year")]
        
    def birth_month(self, person):
	month = person.getBirth().getDateObj().start
	if month.getMonthValid():
	    return [month.getMonthStr()]
	else:
	    return [_("Person's missing birth month")]

    def death_month(self, person):
	month = person.getDeath().getDateObj().start
	if month.getMonthValid():
	    return [month.getMonthStr()]
	else:
	    return [_("Person's missing death month")]

    def death_age(self, person):
	return [self.estimate_age(person, person.getDeath().getDateObj())]

    def marriage_age(self, person):
	return "Marriage age stat unimplemented"

    def first_child_age(self, person):
	return "First child bearing age stat unimplemented"

    def last_child_age(self, person):
	return "Last child bearing age stat unimplemented"

    def child_count(self, person):
	return "Child count stat unimplemented"

    def death_cause(self, person):
	return "Death cause stat unimplemented"
	
    def gender(self, person):
	# TODO: why there's no Person.getGenderName?
	# It could be used by getDisplayInfo & this...
	Person = RelLib.Person
	if person.gender == Person.male:
	    gender = const.male
	elif person.gender == Person.female:
	    gender = const.female
	else:
	    gender = const.unknown
	return [gender]

    def collect_data(self, db, filter_func, extract_func, genders,
                     year_from, year_to, no_years):
        """goes through the database and collects the selected personal
	data persons fitting the filter and birth year criteria. The
	arguments are:
	db           - the GRAMPS database
        filter_func  - filtering function selected by the StatisticsDialog
	extract_func - extraction method selected by the StatisticsDialog
	genders      - which gender(s) to include into statistics
	year_from    - use only persons who've born this year of after
	year_to      - use only persons who've born this year or before
	no_years     - use also people without any birth year
	"""
	Person = RelLib.Person
	items = {}
	# go through the people and collect data
	for person in filter_func.apply(db, db.getPersonMap().values()):

	    # check whether person has suitable gender
	    if person.gender != genders and genders != Person.unknown:
		continue
	    
	    # check whether birth year is within required range
	    birth = person.getBirth().getDateObj()
	    if birth.getYearValid():
		year = birth.getYear()
		if not (year >= year_from and year <= year_to):
		    continue
	    else:
		# if death before range, person's out of range too...
		death = person.getDeath().getDateObj()
		if death.getYearValid() and death.getYear() < year_from:
		    continue
		if not no_years:
		    # do not accept people who are not known to be in range
		    continue

	    # get the information
	    value = extract_func(person)
	    # list of information found
	    for key in value:
		if key in items.keys():
		    items[key] += 1
		else:
		    items[key] = 1
	return items

#------------------------------------------------------------------------
#
# StatisticsDialog
#
#------------------------------------------------------------------------
class StatisticsDialog(Report.DrawReportDialog):

    report_options = {}

    def __init__(self, database, person):
	self.extract = Extract()
        Report.DrawReportDialog.__init__(self, database, person, self.report_options)

    def get_title(self):
        """The window title for this dialog"""
        return "%s - %s - GRAMPS" % (_("Statistics Graph"),
                                     _("Graphical Reports"))

    def get_header(self, name):
        """The header line at the top of the dialog contents."""
        return _("Statistics Graph for %s") % name

    def get_stylesheet_savefile(self):
        """Where to save user defined styles for this report."""
        return 'statistics.xml'

    def get_target_browser_title(self):
        """The title of the window created when the 'browse' button is
        clicked in the 'Save As' frame."""
        return _("Statistics File")

    def get_report_generations(self):
        """No generation options."""
        return (0, 0)
    
    def add_user_options(self):
        """
        Override the base class add_user_options task to add
	report specific options
        """

	# what data to extract from database
        self.extract_menu = gtk.Menu()
	idx = 0
	for item in self.extract.methods():
            menuitem = gtk.MenuItem(item[1])
            menuitem.set_data('extract', idx)
            self.extract_menu.append(menuitem)
	    idx += 1
	self.extract_menu.show_all()

	tip = _("Select which data is collected and which statistics is shown.")
	self.extract_style = gtk.OptionMenu()
        self.extract_style.set_menu(self.extract_menu)
        self.add_option(_('Data to show'), self.extract_style, tip)

	# how to sort the data
        self.sort_menu = gtk.Menu()
	for item in [(_SORT_VALUE, _("Item count")), (_SORT_KEY, _("Item name"))]:
            menuitem = gtk.MenuItem(item[1])
            menuitem.set_data('sort', item[0])
            self.sort_menu.append(menuitem)
	self.sort_menu.show_all()

	tip = _("Select how the statistical data is sorted.")
	self.sort_style = gtk.OptionMenu()
        self.sort_style.set_menu(self.sort_menu)
        self.add_option(_('Sorted by'), self.sort_style, tip)

	# sorting order
	tip = _("Check to reverse the sorting order.")
        self.reverse = gtk.CheckButton(_("Sort in reverse order"))
        self.reverse.set_active(0)
        self.add_option(None, self.reverse, tip)
        self.reverse.show()

	# year range
        self.from_box = gtk.Entry(4)
        self.from_box.set_text('1700')
        self.to_box = gtk.Entry(4)
        self.to_box.set_text(str(time.localtime()[0]))

	box = gtk.HBox()
	box.add(self.from_box)
	box.add(gtk.Label("-"))
	box.add(self.to_box)
	tip = _("Select year range within which people need to be born to be selected for statistics.")
        self.add_option(_('People born between'), box, tip)
        box.show_all()

	# include people without birth year?
	tip = _("Check this if you want people who have no birth date or year to be accounted also in the statistics.")
        self.no_years = gtk.CheckButton(_("Include people without birth years"))
        self.no_years.set_active(0)
        self.add_option(None, self.no_years, tip)
        self.no_years.show()

	# gender selection
	Person = RelLib.Person
        self.gender_menu = gtk.Menu()
	for item in [(Person.unknown, _("Both")), (Person.male, _("Men")), (Person.female, _("Women"))]:
            menuitem = gtk.MenuItem(item[1])
            menuitem.set_data('gender', item[0])
            self.gender_menu.append(menuitem)
	self.gender_menu.show_all()

	tip = _("Select which genders are included into statistics.")
	self.genders = gtk.OptionMenu()
        self.genders.set_menu(self.gender_menu)
        self.add_option(_('Genders included'), self.genders, tip)

    def get_report_filters(self):
        return _get_report_filters(self.person)

    def make_default_style(self):
        _make_default_style(self.default_style)

    def make_report(self):

	year_to = int(self.to_box.get_text())
        year_from = int(self.from_box.get_text())
	no_years = self.no_years.get_active()
        genders = self.gender_menu.get_active().get_data('gender')
        extract = self.extract.methods()[self.extract_menu.get_active().get_data('extract')]
	items = self.extract.collect_data(self.db, self.filter,
		extract[0], genders, year_from, year_to, no_years)

	# title needs both data extraction method name + gender name
	Person = RelLib.Person
	if genders == Person.male:
	    gender = _("men")
	elif genders == Person.female:
	    gender = _("women")
	else:
	    gender = None
	title = (extract[1], gender)
	sort = self.sort_menu.get_active().get_data('sort')
	reverse = self.reverse.get_active()

        try:
            MyReport = Statistics(title, year_from, year_to, items, sort, reverse, self.doc, self.target_path)
            MyReport.write_report()
        except Errors.FilterError, msg:
            (m1,m2) = msg.messages()
            ErrorDialog(m1,m2)
        except Errors.ReportError, msg:
            (m1,m2) = msg.messages()
            ErrorDialog(m1, m2)
        except IOError, msg:
            ErrorDialog(_("Could not create %s" % self.target_path), msg)
        except:
            import DisplayTrace
            DisplayTrace.DisplayTrace()

#------------------------------------------------------------------------
#
# entry point
#
#------------------------------------------------------------------------
def report(database, person):
    """
    report - task starts the report. The plugin system requires that the
    task be in the format of task that takes a database and a person as
    its arguments.
    """
    StatisticsDialog(database, person)


#------------------------------------------------------------------------
#
# Register the Statistics report with the plugin system. The register_report
# task of the Plugins module takes the following arguments.
#
# task - function that starts the task
# name - Name of the report
# status - alpha/beta/production
# category - Category entry in the menu system.
# author_name - Name of the author
# author_email - Author's email address
# description - function that returns the description of the report
#
#------------------------------------------------------------------------
from Plugins import register_report

register_report(
    task=report,
    name=_("Statistics Graph"),
    status=(_("Alpha")),
    category=_("Graphical Reports"),
    author_name="Eero Tamminen",
    author_email="",
    description= _("Generates statistical bar graphs.")
    )
