# -*- coding: utf-8 -*-

# Copyright(C) 2009-2012  Romain Bignon
#
# This file is part of weboob.
#
# weboob is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# weboob 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with weboob. If not, see <http://www.gnu.org/licenses/>.


from decimal import Decimal, InvalidOperation
import datetime
import re

from weboob.capabilities.bank import Transaction, Account
from weboob.capabilities import NotAvailable, NotLoaded
from weboob.tools.misc import to_unicode
from weboob.tools.log import getLogger

from weboob.exceptions import ParseError
from weboob.browser.elements import TableElement, ItemElement
from weboob.browser.filters.standard import Filter, CleanText, CleanDecimal, TableCell


__all__ = ['FrenchTransaction', 'AmericanTransaction']


class classproperty(object):
    def __init__(self, f):
        self.f = f

    def __get__(self, obj, owner):
        return self.f(owner)


class FrenchTransaction(Transaction):
    """
    Transaction with some helpers for french bank websites.
    """
    PATTERNS = []

    def __init__(self, id='', *args, **kwargs):
        super(FrenchTransaction, self).__init__(id, *args, **kwargs)
        self._logger = getLogger('FrenchTransaction')

    @classmethod
    def clean_amount(klass, text):
        """
        Clean a string containing an amount.
        """
        text = text.replace('.','').replace(',','.')
        return re.sub(u'[^\d\-\.]', '', text)

    def set_amount(self, credit='', debit=''):
        """
        Set an amount value from a string.

        Can take two strings if there are both credit and debit
        columns.
        """
        credit = self.clean_amount(credit)
        debit = self.clean_amount(debit)

        if len(debit) > 0:
            self.amount = - abs(Decimal(debit))
        elif len(credit) > 0:
            self.amount = Decimal(credit)
        else:
            self.amount = Decimal('0')

    def parse_date(self, date):
        if date is None:
            return NotAvailable

        if not isinstance(date, (datetime.date, datetime.datetime)):
            if date.isdigit() and len(date) == 8:
                date = datetime.date(int(date[4:8]), int(date[2:4]), int(date[0:2]))
            elif '/' in date:
                date = datetime.date(*reversed([int(x) for x in date.split('/')]))
        if not isinstance(date, (datetime.date, datetime.datetime)):
            self._logger.warning('Unable to parse date %r' % date)
            date = NotAvailable
        elif date.year < 100:
            date = date.replace(year=2000 + date.year)

        return date

    def parse(self, date, raw, vdate=None):
        """
        Parse date and raw strings to create datetime.date objects,
        determine the type of transaction, and create a simplified label

        When calling this method, you should have defined patterns (in the
        PATTERN class attribute) with a list containing tuples of regexp
        and the associated type, for example::

            PATTERNS = [(re.compile(r'^VIR(EMENT)? (?P<text>.*)'), FrenchTransaction.TYPE_TRANSFER),
                        (re.compile(r'^PRLV (?P<text>.*)'),        FrenchTransaction.TYPE_ORDER),
                        (re.compile(r'^(?P<text>.*) CARTE \d+ PAIEMENT CB (?P<dd>\d{2})(?P<mm>\d{2}) ?(.*)$'),
                                                                   FrenchTransaction.TYPE_CARD)
                       ]


        In regexps, you can define this patterns:

            * text: part of label to store in simplified label
            * category: part of label representing the category
            * yy, mm, dd, HH, MM: date and time parts
        """
        self.date = self.parse_date(date)
        self.vdate = self.parse_date(vdate)
        self.rdate = self.date
        self.raw = to_unicode(raw.replace(u'\n', u' ').strip())
        self.category = NotAvailable

        if '  ' in self.raw:
            self.category, _, self.label = [part.strip() for part in self.raw.partition('  ')]
        else:
            self.label = self.raw

        for pattern, _type in self.PATTERNS:
            m = pattern.match(self.raw)
            if m:
                args = m.groupdict()

                def inargs(key):
                    """
                    inner function to check if a key is in args,
                    and is not None.
                    """
                    return args.get(key, None) is not None

                self.type = _type
                labels = [args[name].strip() for name in ('text', 'text2') if inargs(name)]
                if labels:
                    self.label = ' '.join(labels)

                if inargs('category'):
                    self.category = args['category'].strip()

                # Set date from information in raw label.
                if inargs('dd') and inargs('mm'):
                    dd = int(args['dd'])
                    mm = int(args['mm'])

                    if inargs('yy'):
                        yy = int(args['yy'])
                    else:
                        d = self.date
                        try:
                            d = d.replace(month=mm, day=dd)
                        except ValueError:
                            d = d.replace(year=d.year-1, month=mm, day=dd)

                        yy = d.year
                        if d > self.date:
                            yy -= 1

                    if yy < 100:
                        yy += 2000

                    try:
                        if inargs('HH') and inargs('MM'):
                            self.rdate = datetime.datetime(yy, mm, dd, int(args['HH']), int(args['MM']))
                        else:
                            self.rdate = datetime.date(yy, mm, dd)
                    except ValueError as e:
                        self._logger.warning('Unable to date in label %r: %s' % (self.raw, e))

                return

    @classproperty
    def TransactionElement(k):
        class _TransactionElement(ItemElement):
            klass = k

            obj_date = klass.Date(TableCell('date'))
            obj_vdate = klass.Date(TableCell('vdate', 'date'))
            obj_raw = klass.Raw(TableCell('raw'))
            obj_amount = klass.Amount(TableCell('credit'), TableCell('debit', default=''))

        return _TransactionElement

    @classproperty
    def TransactionsElement(klass):
        class _TransactionsElement(TableElement):
            col_date =       [u'Date']
            col_vdate =      [u'Valeur']
            col_raw =        [u'Opération', u'Libellé', u'Intitulé opération']
            col_credit =     [u'Crédit', u'Montant']
            col_debit =      [u'Débit']

            item = klass.TransactionElement
        return _TransactionsElement

    class Date(CleanText):
        def __call__(self, item):
            date = super(FrenchTransaction.Date, self).__call__(item)
            return date

        def filter(self, date):
            date = super(FrenchTransaction.Date, self).filter(date)
            if date is None:
                return NotAvailable

            if not isinstance(date, (datetime.date, datetime.datetime)):
                if date.isdigit() and len(date) == 8:
                    date = datetime.date(int(date[4:8]), int(date[2:4]), int(date[0:2]))
                elif '/' in date:
                    date = datetime.date(*reversed([int(x) for x in date.split('/')]))
            if not isinstance(date, (datetime.date, datetime.datetime)):
                date = NotAvailable
            elif date.year < 100:
                date = date.replace(year=2000 + date.year)

            return date

    @classmethod
    def Raw(klass, *args, **kwargs):
        patterns = klass.PATTERNS

        class Filter(CleanText):
            def __call__(self, item):
                raw = super(Filter, self).__call__(item)
                if item.obj.rdate is NotLoaded:
                    item.obj.rdate = item.obj.date
                item.obj.category = NotAvailable
                if '  ' in raw:
                    item.obj.category, useless, item.obj.label = [part.strip() for part in raw.partition('  ')]
                else:
                    item.obj.label = raw

                for pattern, _type in patterns:
                    m = pattern.match(raw)
                    if m:
                        args = m.groupdict()

                        def inargs(key):
                            """
                            inner function to check if a key is in args,
                            and is not None.
                            """
                            return args.get(key, None) is not None

                        item.obj.type = _type
                        labels = [args[name].strip() for name in ('text', 'text2') if inargs(name)]
                        if labels:
                            item.obj.label = ' '.join(labels)

                        if inargs('category'):
                            item.obj.category = args['category'].strip()

                        # Set date from information in raw label.
                        if inargs('dd') and inargs('mm'):
                            dd = int(args['dd']) if args['dd'] != '00' else 1
                            mm = int(args['mm'])

                            if inargs('yy'):
                                yy = int(args['yy'])
                            else:
                                d = item.obj.date
                                try:
                                    d = d.replace(month=mm, day=dd)
                                except ValueError:
                                    d = d.replace(year=d.year-1, month=mm, day=dd)

                                yy = d.year
                                if d > item.obj.date:
                                    yy -= 1

                            if yy < 100:
                                yy += 2000

                            try:
                                if inargs('HH') and inargs('MM'):
                                    item.obj.rdate = datetime.datetime(yy, mm, dd, int(args['HH']), int(args['MM']))
                                else:
                                    item.obj.rdate = datetime.date(yy, mm, dd)
                            except ValueError as e:
                                raise ParseError('Unable to parse date in label %r: %s' % (raw, e))

                        break

                return raw

            def filter(self, text):
                text = super(Filter, self).filter(text)
                return to_unicode(text.replace(u'\n', u' ').strip())
        return Filter(*args, **kwargs)

    class Currency(CleanText):
        def filter(self, text):
            text = super(FrenchTransaction.Currency, self).filter(text)
            return Account.get_currency(text)

    class Amount(Filter):
        def __init__(self, credit, debit=None, replace_dots=True):
            self.credit_selector = credit
            self.debit_selector = debit
            self.replace_dots = replace_dots

        def __call__(self, item):
            if self.debit_selector:
                try:
                    return - abs(CleanDecimal(self.debit_selector, replace_dots=self.replace_dots)(item))
                except InvalidOperation:
                    pass

            if self.credit_selector:
                try:
                    return CleanDecimal(self.credit_selector, replace_dots=self.replace_dots)(item)
                except InvalidOperation:
                    pass

            return Decimal('0')


class AmericanTransaction(Transaction):
    """
    Transaction with some helpers for american bank websites.
    """
    @classmethod
    def clean_amount(klass, text):
        """
        Clean a string containing an amount.
        """
        # Convert "American" UUU.CC format to "French" UUU,CC format
        if re.search(r'\d\.\d\d(?: [A-Z]+)?$', text):
            text = text.replace(',', ' ').replace('.', ',')
        return FrenchTransaction.clean_amount(text)

    @classmethod
    def decimal_amount(klass, text):
        """
        Convert a string containing an amount to Decimal.
        """
        amnt = AmericanTransaction.clean_amount(text)
        return Decimal(amnt) if amnt else Decimal('0')


def sorted_transactions(iterable):
    """Sort an iterable of transactions in reverse chronological order"""
    return sorted(iterable, reverse=True, key=lambda tr: (tr.date, tr.rdate))


def test():
    clean_amount = AmericanTransaction.clean_amount
    assert clean_amount('42') == '42'
    assert clean_amount('42,12') == '42.12'
    assert clean_amount('42.12') == '42.12'
    assert clean_amount('$42.12 USD') == '42.12'
    assert clean_amount('$12.442,12 USD') == '12442.12'
    assert clean_amount('$12,442.12 USD') == '12442.12'

    decimal_amount = AmericanTransaction.decimal_amount
    assert decimal_amount('$12,442.12 USD') == Decimal('12442.12')
    assert decimal_amount('') == Decimal('0')
