# -*- coding: utf-8 -*-
# Copyright (C) 2009-2016 Mag. Christian Tanzer. All rights reserved
# Glasauergasse 32, A--1130 Wien, Austria. tanzer@swing.co.at
# ****************************************************************************
#
# This module is licensed under the terms of the BSD 3-Clause License
# <http://www.c-tanzer.at/license/bsd_3c.html>.
# ****************************************************************************
#
#++
# Name
#    TFL.Currency
#
# Purpose
#    Model a currency
#
# Revision Dates
#     7-Mar-2009 (CT) Creation
#    11-Mar-2009 (CT) `_massage_rhs` added
#    11-Mar-2009 (CT) `split` fixed (apply `abs` to cents)
#    15-Mar-2009 (CT) s/ROUND_05UP/ROUND_HALF_UP/ (Python 2.5.1 doesn't have
#                     ROUND_05UP, 2.5.2 does <Arrrrg>)
#     3-Nov-2009 (CT) `__hash__` added to avoid::
#                         DeprecationWarning: Overriding __eq__
#                         blocks inheritance of __hash__ in 3.x
#     7-Jan-2010 (CT) `__str__` moved from `Currency` to `_Currency_`
#    17-Jun-2010 (CT) `__unicode__` introduced
#    23-May-2013 (CT) Use `TFL.Meta.BaM` for Python-3 compatibility
#     6-Feb-2015 (CT) Add `ui_display.add_type`
#    16-Oct-2015 (CT) Add `__future__` imports
#    23-Sep-2016 (CT) Add `json_dump.default.add_type`
#    ««revision-date»»···
#--

from   __future__  import absolute_import
from   __future__  import division
from   __future__  import print_function
### importing `unicode_literals` from `__future__` wrecks 2.7 doctest

from   _TFL        import TFL
from   _TFL.pyk    import pyk

import _TFL._Meta.Object
import _TFL.Decorator
import _TFL.I18N
import _TFL.ui_display

import decimal
import re

### see Fri97, p.229, p.292
sep_1000_pat   = re.compile ("(\d{1,3}) (?= (?: \d\d\d)+ (?! \d) )", re.X)

@TFL.Decorator
def _binary_operator (f) :
    def _ (self, rhs) :
        C_Type = self.C_Type
        if isinstance (rhs, C_Type) :
            rhs = rhs.amount
        if self._massage_rhs :
            rhs = self._massage_rhs (rhs)
        return f (self, rhs)
    return _
# end def _binary_operator

@TFL.Decorator
def _binary_operator_currency (f) :
    def _ (self, rhs) :
        C_Type = self.C_Type
        if isinstance (rhs, C_Type) :
            rhs = rhs.amount
        if self._massage_rhs :
            rhs = self._massage_rhs (rhs)
        return C_Type (f (self, rhs))
    return _
# end def _binary_operator

@TFL.Decorator
def _binary_operator_inplace (f) :
    def _ (self, rhs) :
        C_Type = self.C_Type
        if isinstance (rhs, C_Type) :
            rhs = rhs.amount
        if self._massage_rhs :
            rhs = self._massage_rhs (rhs)
        f (self, rhs)
        self.amount = self.normalized_amount (self.amount)
        return self
    return _
# end def _binary_operator

class _M_Currency_ (TFL.Meta.Object.__class__) :

    def __repr__ (cls) :
        return cls.name
    # end def __repr__

# end class _M_Currency_

@pyk.adapt__bool__
@pyk.adapt__div__
@pyk.adapt__str__
class _Currency_ (TFL.Meta.BaM (TFL.Meta.Object, metaclass = _M_Currency_)) :

    name            = "EUR"
    sloppy_name     = "EUR"
    symbol          = property (lambda s : s._symbol or s.sloppy_name)

    decimal_sign    = "."
    sep_1000        = ","

    C_Type          = property (lambda s : s.__class__)
    _currency       = property (lambda s : s.amount)
    _symbol         = None

    _massage_rhs    = None

    def as_string_s (self, round = 0) :
        result = self.as_string   (round)
        result = sep_1000_pat.sub (r"\g<1>%s" % self.sep_1000, result)
        return result
    # end def as_string_s

    ### binary operators

    @_binary_operator_currency
    def __add__ (self, rhs) :
        return self.amount + rhs
    # end def __add__

    __radd__ = __add__

    @_binary_operator_currency
    def __sub__ (self, rhs) :
        return self.amount - rhs
    # end def __sub__

    __rsub__ = __sub__

    @_binary_operator_currency
    def __mul__ (self, rhs) :
        return self.amount * rhs
    # end def __mul__

    __rmul__ = __mul__

    @_binary_operator_currency
    def __truediv__ (self, rhs) :
        return self.amount / rhs
    # end def __truediv__

    __rdiv__    = __truediv__

    @_binary_operator_currency
    def __floordiv__ (self, rhs) :
        return self.amount // rhs
    # end def __floordiv__

    __rfloordiv__ = __floordiv__

    @_binary_operator_currency
    def __mod__ (self, rhs) :
        return self.amount % rhs
    # end def __mod__

    __rmod__ = __mod__

    @_binary_operator
    def __divmod__ (self, rhs) :
        return tuple (self.C_Type (x) for x in divmod (self.amount, rhs))
    # end def __divmod__

    __rdivmod__ = __divmod__

    ### binary operators in-place (aka, augmented assignment operators)

    @_binary_operator_inplace
    def __iadd__ (self, rhs) :
        self.amount += rhs
    # end def __iadd__

    @_binary_operator_inplace
    def __isub__ (self, rhs) :
        self.amount -= rhs
    # end def __isub__

    @_binary_operator_inplace
    def __imul__ (self, rhs) :
        self.amount *= rhs
    # end def __imul__

    @_binary_operator_inplace
    def __idiv__ (self, rhs) :
        self.amount /= rhs
    # end def __idiv__

    @_binary_operator_inplace
    def __ifloordiv__ (self, rhs) :
        self.amount //= rhs
    # end def __ifloordiv__

    ### unary operators

    def __abs__ (self) :
        return self.C_Type (abs (self.amount))
    # end def __abs__

    def __float__ (self) :
        return float (self.amount)
    # end def __float__

    def __hash__ (self) :
        return hash (self.amount)
    # end def __hash__

    def __int__ (self) :
        return int (self.amount)
    # end def __int__

    def __neg__ (self) :
        return self.C_Type (- self.amount)
    # end def __neg__

    def __bool__ (self) :
        return bool (self.amount)
    # end def __bool__

    def __pos__ (self) :
        return self.C_Type (self.amount)
    # end def __pos__

    ### comparison operators

    @_binary_operator
    def __eq__ (self, rhs) :
        return self.amount == rhs
    # end def __eq__

    @_binary_operator
    def __ne__ (self, rhs) :
        return self.amount != rhs
    # end def __ne__

    @_binary_operator
    def __ge__ (self, rhs) :
        return self.amount >= rhs
    # end def __ge__

    @_binary_operator
    def __gt__ (self, rhs) :
        return self.amount > rhs
    # end def __gt__

    @_binary_operator
    def __le__ (self, rhs) :
        return self.amount <= rhs
    # end def __le__

    @_binary_operator
    def __lt__ (self, rhs) :
        return self.amount < rhs
    # end def __lt__

    ### other operators

    def __repr__ (self) :
        return """%s ("%s")""" % (self.C_Type.__name__, self.amount)
    # end def __repr__

    def __str__ (self) :
        return u"%s %s" % (self.as_string (), self.symbol or self.name)
    # end def __str__

# end class _Currency_

class Currency (_Currency_) :
    """Model a currency using Decimal for representation.

       You can subclass this to parameterize

       - `name`         : default "Eur"
       - `sloppy_name`  : default "€"
       - `decimal_sign` : default "."
       - `sep_1000`     : default ","

       - `C`: context used to create Decimal instances
       - `Q`: a Decimal used for `quantize` (default Decimal ("0.01"))

       >>> Currency (1), Currency (1) * 2, Currency (4) / 3
       (Currency ("1.00"), Currency ("2.00"), Currency ("1.33"))
       >>> c = Currency ("12345.67")
       >>> c, c.as_string (), c.as_string (round = True)
       (Currency ("12345.67"), '12345.67', '12346')
       >>> c.as_string_s (), c.as_string_s (round = True)
       ('12,345.67', '12,346')
       >>> vat = Currency ("1.20")
       >>> c * vat, c / vat
       (Currency ("14814.80"), Currency ("10288.06"))
       >>> c - c / vat
       Currency ("2057.61")
       >>> divmod (c, 5)
       (Currency ("2469.00"), Currency ("0.67"))
       >>> c *= vat
       >>> c
       Currency ("14814.80")
       >>> c /= vat
       >>> c
       Currency ("12345.67")
       >>> print (c)
       12345.67 €

       >>> c = Currency ("12345.67")
       >>> d = Currency ("12345.67")
       >>> c == d, c != d, c > 12345, d < 12345
       (True, False, True, False)
    """

    _symbol         = u"€"

    C     = decimal.Context (prec = 12, rounding = decimal.ROUND_HALF_UP)
    D     = decimal.Decimal
    Q     = decimal.Decimal ("0.01")
    Q_inv = Q ** -1
    U     = decimal.Decimal ("1.")

    def __init__ (self, amount = 0) :
        if isinstance (amount, self.C_Type) :
            amount = amount.amount
        elif self._massage_rhs :
            amount = self._massage_rhs (amount)
        self.amount = self.normalized_amount (self.D (amount, self.C))
    # end def __init__

    def as_string (self, round = False) :
        if round :
            return "%s" % (int (self.quantize (self.U)), )
        else :
            a, c = self.split ()
            return "%d%s%02d" % (a, self.decimal_sign, c)
    # end def as_string

    def normalized_amount (self, amount) :
        return amount.quantize (self.Q)
    # end def normalized_amount

    def quantize (self, * args, ** kw) :
        return self.__class__ (self.amount.quantize (* args, ** kw))
    # end def quantize

    def split (self) :
        amount = self.amount
        a      = int (amount)
        return a, int (abs (amount - a) * self.Q_inv)
    # end def split

    def _massage_rhs_float (self, rhs) :
        if isinstance (rhs, float) :
            rhs = self.D (str (rhs))
        return rhs
    # end def _massage_rhs_float

# end class Currency

@TFL._Add_Import_Callback ("_TFL.json_dump")
def _import_cb_json_dump (module) :
    @module.default.add_type (_Currency_)
    def json_encode_range (o) :
        return pyk.text_type (o)
# end def _import_cb_json_dump

@TFL.ui_display.add_type (_Currency_)
def _ui_display_date (obj) :
    return pyk.text_type (obj)
# end def _ui_display_date

if __name__ != "__main__" :
    TFL._Export ("*")
### __END__ TFL.Currency
