# -*- coding: utf-8 -*-
'''
.. note::
    license: GNU Lesser General Public License v3.0 (see LICENSE)
'''
 
from __future__ import print_function
from __future__ import unicode_literals
from __future__ import absolute_import
 
from .account import Account
from ..utils import (check_strict,
                     raiseCsb43Exception,
                     DECIMAL,
                     IS_PY3,
                     m3_next,
                     m3_unicode,
                     m3_is_string,
                     m3_enc)
from ..utils import messages as msg
from ..i18n import tr as _
import sys
 
 
def showInfo(csb, fd=sys.stderr):
    '''
    :param csb: csb file object
    :type csb: :class:`File`
 
    :param fd: file descriptor
    :type fd: :class:`file`
    '''
    print("*",
          m3_enc((_("%d\taccount(s) read") % len(csb.accounts)), fd), file=fd)
    print("*",
          m3_enc((_("File properly closed:\t%s") % csb.is_closed()), fd),
          file=fd)
    print("*",
          m3_enc((_("%d\trecord(s) read") % csb.abstract.totalRecords), fd),
          file=fd)
    for ac in csb.accounts:
        print("*" * 60, file=fd)
        print("* +",
              m3_enc((_("Account:\t%s\t%s") % (ac.accountNumber,
                                               ac.shortName)), fd),
              file=fd)
        print("*  ",
              m3_enc((_(" From:\t%s") % ac.initialDate.strftime("%Y-%m-%d")),
                     fd),
              file=fd)
        print("*  ",
              m3_enc((_(" To:  \t%s") % ac.finalDate.strftime("%Y-%m-%d")),
                     fd),
              file=fd)
        print("*  ", file=fd)
        print("*  ",
              m3_enc((_("%d\ttransaction(s) read") % len(ac.transactions)),
                     fd),
              file=fd)
        print("*  ",
              m3_enc((_("Account properly closed:\t%s") % ac.is_closed()), fd),
              file=fd)
        print("*  ", file=fd)
        print("*  ",
              m3_enc((_("Previous amount:\t%14.2f\t%s") %
                      (ac.initialBalance, ac.currency.letter)), fd),
              file=fd)
        print("*  ",
              m3_enc((_(" Income:        \t%14.2f\t%s") %
                      (ac.abstract.income, ac.abstract.currency.letter)), fd),
              file=fd)
        print("*  ",
              m3_enc((_(" Expense:       \t%14.2f\t%s") %
                      (- ac.abstract.expense,
                       ac.abstract.currency.letter)), fd),
              file=fd)
        print("*  ",
              m3_enc((_("Balance:        \t%14.2f\t%s") %
                      (ac.abstract.balance,
                       ac.abstract.currency.letter)), fd),
              file=fd)
        print("*" * 60, file=fd)
 
 
class File(object):
    '''
    A CSB43 file
 
    - Create a :class:`File` object from a file descriptor::
 
        >>> from csb43.csb43 import File
        >>> with open("csb_file.csb") as fd:
        ...     f = File(fd)
        ...     # do something with f
        ...
 
    - Create an empty :class:`File` object::
 
        >>> f = File()
 
    '''
 
    def __init__(self,
                 fd=None,
                 strict=True,
                 decimal=DECIMAL,
                 yearFirst=True):
        '''
        :param fd: a csb file
        :type fd: :class:`file`
 
        :param strict: treat warnings as exceptions when `True`
        :type strict: :class:`bool`
 
        :param decimal: number of digits to be considered as the decimal part \
        in money
        :type decimal: :class:`int`
 
        :param yearFirst: switch between YYMMD [`True`] and DDMMYY [`False`] \
        date formats
        :type yearFirst: :class:`bool`
 
        :raises: :class:`csb43.utils.Csb43Exception`
        '''
 
        self.__accounts = []
        self.__strict = strict
        self.__closing = None
        self.__decimal = decimal
        self.__yearFirst = yearFirst
        self.__numRecords = 0
        self.__fromFile = False
 
        if fd is not None:
 
            self.__fromFile = True
 
            def skip():
                pass
 
            launcher = {'00': skip,
                        '11': self.add_account,
                        '22': self.add_transaction,
                        '23': self.add_item,
                        '24': self.add_exchange,
                        '33': self.close_account,
                        '88': self.close_file}
 
            for line in fd:
                line = line.rstrip('\n\r')
                launcher.get(line[0:2], self.__unknownRecord)(line)
                self.__numRecords += 1
 
            self.__fromFile = False
        #else:
            #pass
 
    def __unknownRecord(self, line=''):
        raiseCsb43Exception(msg.BAD_RECORD(line), self.__strict)
 
    def _get_accounts(self):
        return self.__accounts
 
    def get_last_account(self):
        '''
        :rtype: the last added :class:`Account`
        '''
        return self.__accounts[-1]
 
    def add_account(self, record):
        '''
        Add a new account
 
        :param record: account record
        :type record: :class:`Account` or :class:`basestring`
 
        :raises: :class:`csb43.utils.Csb43Exception` if `record` is not valid
        '''
        if isinstance(record, Account):
            self.__accounts.append(record)
        else:
            self.__accounts.append(Account(record,
                                           self.__strict,
                                           decimal=self.__decimal,
                                           yearFirst=self.__yearFirst))
 
    def add_transaction(self, record):
        '''
        Add a new transaction to the last added account
 
        :param record: transaction record
        :type record: :class:`Transaction` or :class:`basestring`
 
        :raises: :class:`csb43.utils.Csb43Exception`
 
        .. seealso::
 
            :func:`Account.add_transaction`
        '''
        self.get_last_account().add_transaction(record)
 
    def add_item(self, record):
        '''
        Add a new additional item record to the last added transaction
 
        :param record: item record
        :type record: :class:`Item` or :class:`basestring`
 
        :raises: :class:`csb43.utils.Csb43Exception` when the record is \
        impossible to parse, or if the maximum number of complementary items \
        has been reached
 
        .. seealso::
 
            :func:`Transaction.add_item`
        '''
        self.get_last_account().add_item(record)
 
    def add_exchange(self, record, update=False):
        '''
        Add a new additional exchange record to the last added transaction
 
        :param record: csb exchange record or object
        :type record: :class:`Exchange` or :class:`basestring`
 
        :param update: update the current exchange object if it exists
        :type update: :class:`bool`
 
        :raises: :class:`csb43.utils.Csb43Exception`
 
        .. seealso::
 
            :func:`Transaction.add_exchange`
 
        '''
        self.get_last_account().add_exchange(record, update)
 
    def close_account(self, record=None):
        '''
        Close the current account
 
        :param record: csb record
        :type record: :class:`ClosingAccount` or :class:`basestring`
 
        :raises: :class:`csb43.utils.Csb43Exception` if `record` is not valid
 
        .. seealso::
 
            :func:`csb43.csb43.Account.close_account`
        '''
        self.get_last_account().close_account(record)
 
    def close_file(self, record=None):
        '''
        Close the file with a termination record
 
        :param record: csb record
        :type record: :class:`ClosingFile` or :class:`basestring`
 
        :raises: :class:`csb43.utils.Csb43Exception` if `record` is not valid
 
        If record is `None`, a new abstract is generated::
 
            >>> c = csb.File()
            >>> c.is_closed()
            False
            >>> c.close_file()
            >>> c.is_closed()
            True
            >>> c.abstract.totalRecords
            0
            >>> c.add_account(csb.Account())
            >>> c.abstract.totalRecords
            0
            >>> c.close_file()
            >>> c.abstract.totalRecords
            1
            >>> c.is_closed()
            True
 
        If record is not empty, the number of records of `File` must be
        coincident with the quantity given in `record`::
 
            >>> cf = csb.ClosingFile()
            >>> cf.totalRecords = 5
            >>> c.close_file(cf)
            Traceback (most recent call last):
            File "<stdin>", line 1, in <module>
            File "csb43/csb43/csb_file.py", line 200, in close_file
 
            File "csb43/utils/utils.py", line 25, in raiseCsb43Exception
                raise exc
            csb43.utils.utils.Csb43Exception: registro de cierre de fichero \
incongruente: total de registros 5 != 1
            >>>
        '''
        if self.__fromFile and (self.__closing is not None):
            raiseCsb43Exception(_("trying to close an already closed file"),
                                self.__strict)
 
        if record is not None:
            if isinstance(record, ClosingFile):
                self.__closing = record
            else:
                self.__closing = ClosingFile(record, self.__strict)
 
            n_r1 = int(self.__closing.totalRecords)
            n_r2 = self._get_num_records()
            if n_r1 != n_r2:
                raiseCsb43Exception(
                    _('incongruent closing record of file: '
                      'total records %d != %d') % (n_r1, n_r2),
                    self.__strict)
        else:
            self.__closing = ClosingFile(strict=self.__strict)
            self.__closing.totalRecords = self._get_num_records()
 
    def _get_num_records(self):
        if self.__fromFile:
            return self.__numRecords
        else:
            return sum([len([x for x in ac]) for ac in self.accounts])
 
    def is_closed(self):
        '''
        :rtype: `True` if this File has been properly closed
        '''
        return self.__closing is not None
 
    def _get_closing(self):
        return self.__closing
 
    def as_dict(self):
        '''
        :rtype: a representation of this object as a :class:`dict`. The keys \
        will be localised
 
        >>> import csb43.csb43 as csb
        >>> f = csb.File()
        >>> f.add_account(csb.Account())
        >>> f.add_transaction(csb.Transaction())
        >>> import pprint
        >>> pprint.pprint(f.as_dict())
        {u'cuentas': [{u'balance_inicial': None,
                    u'codigo_de_entidad': None,
                    u'codigo_de_sucursal': None,
                    u'divisa': None,
                    u'fecha_de_comienzo': None,
                    u'fecha_de_fin': None,
                    u'modalidad_de_informacion': None,
                    u'movimientos': [{u'cantidad': None,
                                        u'codigo_de_sucursal': None,
                                        u'concepto_comun': None,
                                        u'concepto_propio': None,
                                        u'fecha_de_operacion': None,
                                        u'fecha_valor': None,
                                        u'numero_del_documento': None,
                                        u'primera_referencia': None,
                                        u'segunda_referencia': None}],
                    u'nombre_abreviado': None,
                    u'numero_de_cuenta': None}]}
        '''
        return {
            _("accounts"): [x.as_dict() for x in self.accounts]
        }
 
    def __iter__(self):
        ''':rtype: iterator of all the `CSB43` records that this object \
        represents
 
        >>> import csb43.csb43 as csb
        >>> f = csb.File()
        >>> f.add_account(csb.Account())
        >>> f.add_transaction(csb.Transaction())
        >>> for x in f:
        ...     print x
        ...
        11                  000000000000000000000000000000
        22    0000000000000000000001000000000000000000000000000000000000
        88999999999999999999000002
        >>>
 
        '''
        if not self.__fromFile:
            self.close_file()
        return _FileIter(self)
 
    def __str__(self):
        ''':rtype: representation of this object as `CSB43` records \
        (using `\\\\n` as separator)
 
        >>> import csb43.csb43 as csb
        >>> f = csb.File()
        >>> f.add_account(csb.Account())
        >>> f.add_transaction(csb.Transaction())
        >>> print f
        11                  000000000000000000000000000000
        22    0000000000000000000001000000000000000000000000000000000000
        88999999999999999999000002
        '''
        return '\n'.join([x for x in self])
 
    #**** Properties ****
 
    accounts = property(_get_accounts, None, None,
                        ":rtype: :class:`list` of accounts")
    abstract = property(_get_closing, None, None,
                        ":rtype: :class:`ClosingFile` file abstract")
 
 
class _FileIter(object):
 
    def __init__(self, f):
        self.__output = []
        self.__output.extend(f.accounts)
        if f.abstract:
            self.__output.append(f.abstract)
 
        self.__iter = iter(self.__output)
        self.__acc = None
 
    def next(self):
        if self.__acc:
            try:
                return m3_next(self.__acc)
            except StopIteration:
                self.__acc = None
        now = m3_next(self.__iter)
 
        if isinstance(now, Account):
            self.__acc = iter(now)
            return self.next()
        else:
            return str(now)
 
    if IS_PY3:
        def __next__(self):
            return self.next()
 
 
class ClosingFile(object):
    '''
    A File abstract, given by a termination record
 
    Create a :class:`ClosingFile` object from a `CSB43` string record::
 
        >>> from csb43.csb43 import ClosingFile
        >>> c = ClosingFile(record)
 
    From an empty object to a `CSB43` string record::
 
        >>> c = ClosingFile()
        >>> c.totalRecords = 123
        >>> str(c)
        '8899999999999999999900012\
3                                                      '
 
    '''
 
    def __init__(self, record=None, strict=True):
        '''
        :param record: csb record
        :type record: :class:`basestring` or `None`
 
        :param strict: treat warnings as exceptions when `True`
        :type strict: :class:`bool`
 
        :raises: :class:`csb43.utils.Csb43Exception`
        '''
        self.__strict = strict
 
        self.__totalRecords = None
        self.__padding = None
 
        if record is not None:
            if not ClosingFile.is_valid(record):
                raiseCsb43Exception(msg.BAD_RECORD(record), self.__strict)
 
            self._check_nines(record[2:20], self.__strict)
            self._set_total_records(record[20:26], self.__strict)
            self._set_padding(record[26:80], self.__strict)
 
    def _get_total_records(self):
        if self.__totalRecords is None:
            return None
        return int(self.__totalRecords)
 
    def _get_external_total_records(self):
        if self.__totalRecords is None:
            return None
        return int(self.__totalRecords)
 
    def _get_padding(self):
        return self.__padding
 
    @check_strict(r'^\d{6}$')
    def _set_total_records(self, value, strict=True):
        self.__totalRecords = value
 
    def _set_external_total_records(self, value):
        self._set_total_records(m3_unicode(value).rjust(6, '0'))
 
    @check_strict(r'^.{0,54}$')
    def _set_padding(self, value, strict=True):
        self.__padding = value
 
    @check_strict(r'^9{18}$')
    def _check_nines(self, value, strict=True):
        pass
 
    @staticmethod
    def is_valid(record):
        return m3_is_string(record)\
            and (27 <= len(record) <= 80) and (record[0:2] == '88')
 
    def __str__(self):
        ''':rtype: representation of this object as `CSB43` records \
        (using `\\\\n` as separator)'''
        return "88{nines}{records:0>6}{padding: <54}".format(
            nines='9' * 18,
            records=self._get_total_records() or '',
            padding=self._get_padding() or ''
        )
 
    #**** Properties ****
 
    totalRecords = property(_get_external_total_records,
                            _set_external_total_records, None,
                            """total number of entries
 
>>> c.totalRecords = 34
>>> c.totalRecords
34
>>> c.totalRecords = '115'
>>> c.totalRecords
115
 
""")
    padding = property(_get_padding, _set_padding, None, "padding")