# coding: utf-8

# # Srategy Pattern
# 策略模式定义一系列算法并封装它们,这些算法可以互换。
# 策略模式还根据不同的对象使用不同的算法。

# ## class implement

# In[10]:

import abc
import collections

Customer = collections.namedtuple('Customer', 'name fidelity')

class LineItem(object):
    def __init__(self, product, quantity, price):
        self.product = product
        self.quantity = quantity
        self.price = price
    
    def total(self):
        return self.quantity * self.price
    
class Order(object):
    def __init__(self, customer, cart, promotion=None):
        self.customer = customer
        self.cart = cart
        self.promotion = promotion
    
    def total(self):
        if not hasattr(self, '__total'):
            self.__total = sum(lineitem.total() for lineitem in self.cart)
        return self.__total
    
    def due(self):
        discount = 0 if self.promotion is None else self.promotion.discount(self)
        return self.total() - discount
    
    def __repr__(self):
        return "<Order total:{:.2f} due:{:.2f}>".format(self.total(), self.due())


class Promotion(abc.ABC):
    """abstract class of Promotion"""
    @abc.abstractmethod
    def discount(self, Order):
        """return the discount vary different Order """
        pass
    

class FidelityPromo(Promotion):
    """if fidelity >= 1000 Give 5% discount"""
    
    def discount(self, order):
        """ return 0.05 of the total price of Order """
        return 0.05*order.total() if order.customer.fidelity >= 100 else 0.0

class BulkItemPromo(Promotion):
    """if the lineitem's quantity >= 20 give the lineitem'total price 10% discount"""
    def discount(self, order):
        discount = sum((0.1*lineitem.total() if lineitem.quantity >= 20 else 0.0 for lineitem in order.cart))
        return discount
        
class LargeOrderPromo(Promotion):
    """7% discount for the kind of lineitems in cart >10 """
    def discount(self, order):
        kinds = {lineitem.product for lineitem in order.cart}
        return 0.07*order.total() if len(kinds)>=10 else 0.0
    
        
    


# In[3]:

joe = Customer('John Doe', 0)


# In[4]:

ann = Customer('Ann Smith', 1100)


# In[5]:

cart = [LineItem('banana', 4, .5),LineItem('apple', 10, 1.5),LineItem('watermellon', 5, 5.0)]


# In[11]:

Order(joe, cart, FidelityPromo()) # joe's fidelity < 1000

# Out[11]: <Order total:42.00 due:42.00>

# In[12]:

Order(ann, cart, FidelityPromo()) # ann's fidelity > 1000

# Out[12]: <Order total:42.00 due:39.90>

# In[13]:

banana_cart = [LineItem('banana', 30, .5),LineItem('apple', 10, 1.5)]


# In[14]:

Order(joe, banana_cart, BulkItemPromo()) # number of 'banana' > 20

# Out[14]: <Order total:30.00 due:28.50>

# In[15]:

long_order = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)]


# In[16]:

Order(joe, long_order, LargeOrderPromo()) # number of line items >=10

# Out[16]: <Order total:10.00 due:9.30>



# In[17]:

Order(joe, cart, LargeOrderPromo())

# Out[17]: <Order total:42.00 due:42.00>

# ## function implement
# function is the first class in python.
# considering the subclasses of Promotion are very simple, i think implementation them as function is better.
# the defination of Customer, LineItem and Oder is no need to change.
# but change the due method in Oder is nesscery.

# In[18]:

import collections

Customer = collections.namedtuple('Customer', 'name fidelity')

class LineItem(object):
    def __init__(self, product, quantity, price):
        self.product = product
        self.quantity = quantity
        self.price = price
    
    def total(self):
        return self.quantity * self.price
    
class Order(object):
    def __init__(self, customer, cart, promotion=None):
        self.customer = customer
        self.cart = cart
        self.promotion = promotion
    
    def total(self):
        if not hasattr(self, '__total'):
            self.__total = sum(lineitem.total() for lineitem in self.cart)
        return self.__total
    
    def due(self):
        discount = 0 if self.promotion is None else self.promotion(self)
        return self.total() - discount
    
    def __repr__(self):
        return "<Order total:{:.2f} due:{:.2f}>".format(self.total(), self.due())

def fidelity_promo(order):
    return 0.05*order.total() if order.customer.fidelity >= 100 else 0.0
def bulk_promo(order):
    return sum((0.1*lineitem.total() if lineitem.quantity >= 20 else 0.0 for lineitem in order.cart))
def large_order_promo(order):
    kinds = {lineitem.product for lineitem in order.cart}
    return 0.07*order.total() if len(kinds)>=10 else 0.0


# In[19]:

joe = Customer('John Doe', 0)
ann = Customer('Ann Smith', 1100)
cart = [LineItem('banana', 4, .5),LineItem('apple', 10, 1.5),LineItem('watermellon', 5, 5.0)]


# In[20]:

Order(joe, cart, fidelity_promo) # joe's fidelity < 1000

# Out[20]: <Order total:42.00 due:42.00>

# In[21]:

Order(ann, cart, fidelity_promo) # ann's fidelity > 1000

# Out[21]:<Order total:42.00 due:39.90>

# In[22]:

banana_cart = [LineItem('banana', 30, .5),LineItem('apple', 10, 1.5)]


# In[24]:

Order(joe, banana_cart, bulk_promo) # number of 'banana' > 20

# Out[24]: <Order total:30.00 due:28.50>

# In[25]:

long_order = [LineItem(str(item_code), 1, 1.0) for item_code in range(10)]


# In[27]:

Order(joe, long_order, large_order_promo) # number of line items >=10

# Out[27]:<Order total:10.00 due:9.30>

# In[28]:

Order(joe, cart, large_order_promo)


# Out[28]: <Order total:42.00 due:42.00>

# ##  what is the best promotion

# In[31]:

promos = [fidelity_promo, bulk_promo, large_order_promo]
def best_promo(order):
    return max(promo(order) for promo in promos)


# In[32]:

Order(joe, long_order, best_promo)

# Out[32]:<Order total:10.00 due:9.30>


# In[33]:

Order(joe, banana_cart, best_promo)

# Out[33]: <Order total:30.00 due:28.50>

# In[34]:

Order(ann, cart, best_promo)

# Out[34]:<Order total:42.00 due:39.90>

# there is another way to find the all promotions in a module.
# use globals()
# globals() return a dict of all parameters: variable, function , class in current module.

# In[35]:

promos = [globals()[name] for name in globals() if name.endswith('_promo') and name != 'best_promo']


# also inspect module can inpect a module.
# for example, there is a module named promotions:

# In[ ]:

promos = [func for name, func in inspect.getmembers(promotions, inspect.isfunction)]