#!/usr/bin/env python3
## @file
#  @brief Add __str__ methods to financial objects so that @code print object @endcode leads to human readable results
""" @package str_methods.py -- Add __str__ methods to financial objects

   Import this module and str(Object) where Object is Transaction, Split, Invoice or Entry leads to
   human readable results. That is handy when using @code print object @endcode

   I chose to put these functions/methods in a separate file to develop them like this and maybe if
   they prove to be useful they can be put in gnucash_core.py.

   I am searching to find the best way to serialize these complex objects. Ideally this serialization
   would be configurable.

   If someone has suggestions how to beautify, purify or improve this code in any way please feel
   free to do so.

   This is written as a first approach to a shell-environment using ipython to interactively manipulate
   GnuCashs Data."""

#   @author Christoph Holtermann, c.holtermann@gmx.de
#   @ingroup python_bindings_examples
#   @date May 2011
#
#   ToDo :
#
#   * Testing for SWIGtypes
#   * dealing the cutting format in a bit more elegant way
#   * having setflag as a classmethod makes it probably impossible to have flags on instance level. Would changing that be useful ?
#   * It seems useful to have an object for each modification. That is because there is some Initialisation to be done.
#

import gnucash
from gnucash import function_class

# Default values for encoding of strings in GnuCashs Database
DEFAULT_ENCODING = "UTF-8"
DEFAULT_ERROR = "ignore"

def setflag(self, name, value):
    if not(name in self.OPTIONFLAGS_BY_NAME):
      self.register_optionflag(name)
    if value == True:
      self.optionflags |= self.OPTIONFLAGS_BY_NAME[name]
    else:
      self.optionflags &= ~self.OPTIONFLAGS_BY_NAME[name]

def getflag(self, name):
    if not(name in self.OPTIONFLAGS_BY_NAME):
      raise KeyError(str(name)+" is not a registered key.")
    return ((self.optionflags & self.OPTIONFLAGS_BY_NAME[name]) != 0)

def register_optionflag(self,name):
    """Taken from doctest.py"""
    # Create a new flag unless `name` is already known.
    return self.OPTIONFLAGS_BY_NAME.setdefault(name, 1 << len(self.OPTIONFLAGS_BY_NAME))

def ya_add_method(_class, function, method_name=None, clsmethod=False, noinstance=False):
    """Calls add_method from function_methods.py but makes it
    possible to use functions in this module. Also keeps the
    docstring"""

    if method_name == None:
      method_name = function.__name__

    setattr(gnucash.gnucash_core_c,function.__name__,function)
    if clsmethod:
      mf=_class.ya_add_classmethod(function.__name__,method_name)
    elif noinstance:
      mf=_class.add_method(function.__name__,method_name)
    else:
      mf=_class.ya_add_method(function.__name__,method_name)
    if function.__doc__ != None:
      setattr(mf, "__doc__", function.__doc__)

def infect(_class, function, method_name):
    if not getattr(_class, "OPTIONFLAGS_BY_NAME", None):
      _class.OPTIONFLAGS_BY_NAME={}
      _class.optionflags=0
      ya_add_method(_class,register_optionflag,clsmethod=True)
      ya_add_method(_class,setflag,clsmethod=True)
      ya_add_method(_class,getflag,clsmethod=True)
    ya_add_method(_class, function, method_name)

class ClassWithCutting__format__():
    """This class provides a __format__ method which cuts values to a certain width.

    If width is too big '...' will be put at the end of the resulting string."""

    def __init__(self,value):
      self.value = value

    def __format__(self, fmt):
        def get_width(fmt_spec):
            """Parse fmt_spec to obtain width"""

            def remove_alignment(fmt_spec):
                if fmt_spec[1] in ["<","^",">"]:
                    fmt_spec=fmt_spec[2:len(fmt_spec)]
                return fmt_spec

            def remove_sign(fmt_spec):
                if fmt_spec[0] in ["-","+"," "]:
                    fmt_spec=fmt_spec[1:len(fmt_spec)]
                return fmt_spec

            def remove_cross(fmt_spec):
                if fmt_spec[0] in ["#"]:
                    fmt_spec=fmt_spec[1:len(fmt_spec)]
                return fmt_spec

            def do_width(fmt_spec):
                n=""

                while len(fmt_spec)>0:
                    if fmt_spec[0].isdigit():
                      n+=fmt_spec[0]
                      fmt_spec=fmt_spec[1:len(fmt_spec)]
                    else:
                        break
                if n:
                    return int(n)
                else:
                    return None

            if len(fmt_spec)>=2:
                fmt_spec=remove_alignment(fmt_spec)
            if len(fmt_spec)>=1:
               fmt_spec=remove_sign(fmt_spec)
            if len(fmt_spec)>=1:
                fmt_spec=remove_cross(fmt_spec)
            width=do_width(fmt_spec)
            # Stop parsing here for we only need width

            return width

        def cut(s, width, replace_string="..."):
            """Cuts s to width and puts replace_string at it's end."""

            #s=s.decode('UTF-8', "replace")

            if len(s)>width:
                if len(replace_string)>width:
                    replace_string=replace_string[0:width]
                s=s[0:width-len(replace_string)]
                s=s+replace_string

            return s

        value=self.value

        # Replace Tabs and linebreaks
        #import types
        if isinstance(value, str):
            value = value.replace("\t","|")
            value = value.replace("\n","|")

        # Do regular formatting of object
        value = value.__format__(fmt)

        # Cut resulting value if longer than specified by width
        width = get_width(fmt)
        if width:
            value = cut(value, width, "...")

        return value

def all_as_classwithcutting__format__(*args):
    """Converts every argument to instance of ClassWithCutting__format__"""

    #import types
    l=[]
    for a in args:
        #if type(a) in [types.StringType, types.UnicodeType]:
        #  a=a.decode("UTF-8")
        l.append(ClassWithCutting__format__(a))

    return l

def all_as_classwithcutting__format__keys(encoding=None, error=None, **keys):
    """Converts every argument to instance of ClassWithCutting__format__"""

    #import types
    d={}
    if encoding==None:
      encoding=DEFAULT_ENCODING
    if error==None:
      error=DEFAULT_ERROR
    for a in keys:
        #if isinstance(keys[a], str):
        #  keys[a]=keys[a].decode(encoding,error)
        d[a]=ClassWithCutting__format__(keys[a])

    return d



# Split
def __split__str__(self, encoding=None, error=None):
    """__str__(self, encoding=None, error=None) -> object

    Serialize the Split object and return as a new Unicode object.

    Keyword arguments:
    encoding -- defaults to str_methods.default_encoding
    error -- defaults to str_methods.default_error
    See help(unicode) for more details or http://docs.python.org/howto/unicode.html.

    """

    from gnucash import Split
    import time
    #self=Split(instance=self)

    lot=self.GetLot()
    if lot:
        if type(lot).__name__ == 'SwigPyObject':
          lot=gnucash.GncLot(instance=lot)
        lot_str=lot.get_title()
    else:
        lot_str='---'

    transaction=self.GetParent()

    # This dict and the return statement can be changed according to individual needs
    fmt_dict={
        "account":self.GetAccount().name,
        "value":self.GetValue(),
        "memo":self.GetMemo(),
        "lot":lot_str}

    fmt_str= ("Account: {account:20} "+
            "Value: {value:>10} "+
            "Memo: {memo:30} ")

    if self.optionflags & self.OPTIONFLAGS_BY_NAME["PRINT_TRANSACTION"]:
        fmt_t_dict={
            "transaction_time":time.ctime(transaction.GetDate()),
            "transaction2":transaction.GetDescription()}
        fmt_t_str=(
            "Transaction: {transaction_time:30} "+
            "- {transaction2:30} "+
            "Lot: {lot:10}")
        fmt_dict.update(fmt_t_dict)
        fmt_str += fmt_t_str

    return fmt_str.format(**all_as_classwithcutting__format__keys(encoding,error,**fmt_dict))

# This could be something like an __init__. Maybe we could call it virus because it infects the Split object which
# thereafter mutates to have better capabilities.
infect(gnucash.Split,__split__str__,"__str__")
gnucash.Split.register_optionflag("PRINT_TRANSACTION")
gnucash.Split.setflag("PRINT_TRANSACTION",True)

def __transaction__str__(self):
    """__str__ method for Transaction class"""
    from gnucash import Transaction
    import time
    self=Transaction(instance=self)

    fmt_tuple=('Date:',time.ctime(self.GetDate()),
          'Description:',self.GetDescription(),
          'Notes:',self.GetNotes())

    transaction_str = "{0:6}{1:25} {2:14}{3:40} {4:7}{5:40}".format(
          *all_as_classwithcutting__format__(*fmt_tuple))
    transaction_str += "\n"

    splits_str=""
    for n,split in enumerate(self.GetSplitList()):
        if not (type(split)==gnucash.Split):
            split=gnucash.Split(instance=split)

        transaction_flag = split.getflag("PRINT_TRANSACTION")
        split.setflag("PRINT_TRANSACTION",False)
        splits_str += "[{0:>2}] ".format(str(n))
        splits_str += str(split)
        splits_str += "\n"
        split.setflag("PRINT_TRANSACTION",transaction_flag)

    return transaction_str + splits_str

gnucash.gnucash_core_c.__transaction__str__=__transaction__str__
gnucash.Transaction.add_method("__transaction__str__","__str__")

def __invoice__str__(self):
    """__str__ method for Invoice"""

    from gnucash.gnucash_business import Invoice
    self=Invoice(instance=self)


    # This dict and the return statement can be changed according to individual needs
    fmt_dict={
        "id_name":"ID:",
        "id_value":self.GetID(),
        "notes_name":"Notes:",
        "notes_value":self.GetNotes(),
        "active_name":"Active:",
        "active_value":str(self.GetActive()),
        "owner_name":"Owner Name:",
        "owner_value":self.GetOwner().GetName(),
        "total_name":"Total:",
        "total_value":str(self.GetTotal()),
        "currency_mnemonic":self.GetCurrency().get_mnemonic()}

    ret_invoice= ("{id_name:4}{id_value:10} {notes_name:7}{notes_value:20} {active_name:8}{active_value:7} {owner_name:12}{owner_value:20}"+
                  "{total_name:8}{total_value:10}{currency_mnemonic:3}").\
                    format(**all_as_classwithcutting__format__keys(**fmt_dict))

    ret_entries=""
    entry_list = self.GetEntries()
    for entry in entry_list: # Type of entry has to be checked
      if not(type(entry)==Entry):
        entry=Entry(instance=entry)
      ret_entries += "  "+str(entry)+"\n"

    return ret_invoice+"\n"+ret_entries


from gnucash.gnucash_business import Invoice

gnucash.gnucash_core_c.__invoice__str__=__invoice__str__
gnucash.gnucash_business.Invoice.add_method("__invoice__str__","__str__")

def __entry__str__(self):
    """__str__ method for Entry"""

    from gnucash.gnucash_business import Entry
    self=Entry(instance=self)

    # This dict and the return statement can be changed according to individual needs
    fmt_dict={
        "date_name":"Date:",
        "date_value":str(self.GetDate()),
        "description_name":"Description:",
        "description_value":self.GetDescription(),
        "notes_name":"Notes:",
        "notes_value":self.GetNotes(),
        "quant_name":"Quantity:",
        "quant_value":str(self.GetQuantity()),
        "invprice_name":"InvPrice:",
        "invprice_value":str(self.GetInvPrice())}

    return ("{date_name:6}{date_value:15} {description_name:13}{description_value:20} {notes_name:7}{notes_value:20}"+
            "{quant_name:12}{quant_value:7} {invprice_name:10}{invprice_value:7}").\
                format(**all_as_classwithcutting__format__keys(**fmt_dict))

from gnucash.gnucash_business import Entry

gnucash.gnucash_core_c.__entry__str__=__entry__str__
gnucash.gnucash_business.Entry.add_method("__entry__str__","__str__")
