import random

def dice():
    return random.randint(1,6)

def roll_dice(n):
    for i in range(n):
        print(dice())
        
roll_dice(5)

"""
Explanation: Lecture 3
Warmup

Write a function that simulates a dice roll every time the function is called.
End of explanation
"""


balls = ['r', 'r', 'b', 'b', 'b']

def ball_game(num_red, num_blue, select_k):
    balls = ['r']*num_red + ['b']*num_blue
    res = []
    for k in range(select_k):
        random.shuffle(balls)
        res.append(balls.pop())
        print('ratio of blue to red in sack', balls.count('b')/balls.count('r') )
    
    assert len(balls) + len(res) == num_red + num_blue
    
    return ("balls = ", balls,"result = ", res)

#ball_game(1000,100, 1000)


balls.count('r')

"""
Explanation: Rewrite your function to take in an int n and simulate n dice rolls.
Write a function that takes in a list of red and blue balls and on each call, pulls a ball randomly out of the list and updates the list.
End of explanation
"""


def coin(n):
    res = []
    for i in range(n):
        res.append(random.randint(0,1))
    return res
            
        


# %load ../examples/compressor
    
def groupby_char(lst):
    """Returns a list of strings containing identical characters.
    
    Takes a list of characters produced by running split on a string. 
    Groups runs (in order sequences) of identical
    characters into string elements in the list.
    
    Parameters
    ---------
    Input:
    lst: list
    A list of single character strings.
    
    Output:
    grouped: list
    A list of strings containing grouped characters."""

    new_lst = []

    count = 1

    for i in range(len(lst) - 1): # we range to the second to last index since we're checking if lst[i] == lst[i + 1]. 
        if lst[i] == lst[i + 1]:
            count += 1        
        else:
            new_lst.append([lst[i],count]) # Create a lst of lists. Each list contains a character and the count of adjacent identical characters.
            count = 1
            
    new_lst.append((lst[-1],count)) # Return the last character (we didn't reach it with our for loop since indexing until second to last).


    grouped = [char*count for [char, count] in new_lst] 

    return grouped

def compress_group(string):
    """Returns a compressed two character string containing a character and a number.
    
    Takes in a string of identical characters and returns the compressed string consisting of the character and the length of the original string.
    
    Example
    -------
    "AAA"-->"A3"

    Parameters:
    -----------
    Input:
    string: str
    A string of identical characters.
    
    Output:
    ------
    compressed_str: str
    A compressed string of length two containing a character and a number.
    """
    

    return str(string[0]) + str(len(string))


def compress(string):
    """Returns a compressed representation of a string.
    
    Compresses the string by mapping each run of identical characters to a 
    single character and a count. 
    
    Ex.
    --
    compress('AAABBCDDD')--> 'A3B2C1D3'.
    
    Only compresses string if the compression is shorter than the original string.

    Ex.
    --
    compress('A')--> 'A' # not 'A1'.

    Parameters
    ----------

    Input:
    string: str
    The string to compress

    Output:
    compressed: str
    The compressed representation of the string.
    """

    try:
        split_str = [char for char in string] # Create list of single characters.
        grouped = groupby_char(split_str) # Group characters if characters are identical.
        compressed = ''.join( # Compress each element of the grouped list and join to a string.
                [compress_group(elem) for elem in grouped])

        if len(compressed) < len(string): # Only return compressed if compressed is actually shorter.
            return compressed
        else:
            return string

    except IndexError: # If our input string is empty, return an empty string.
        return ""

    except TypeError: # If we get something that's not compressible (including NoneType) return None.
        return None


if __name__ == "__main__":
    import sys
    print(sys.argv[0])
    string = sys.argv[1]
    print("string is", string)
    print("compression is", compress(string))


lst = [0,0,0,0,1,1,1,1,1, 0, 0, 0, 0, 0]
lst_str = [str(elem) for elem in lst]
lst_str

grouped = groupby_char(lst_str)
grouped


lengths = [len(elem) for elem in grouped]
lengths

max(lengths)

def get_max_run(n):
    """Generates n coin flips and returns the max run over all the coin flips"""
    lst = coin(n)
    print(lst)
    lst_str = [str(elem) for elem in lst]

    grouped = groupby_char(lst_str)
    lengths = [len(elem) for elem in grouped]
    return max(lengths)

get_max_run(10)

"""
Explanation: Simulate n coin flips for n = 10, 100, 1000, 10000. Are the ratios of heads to tails what you would expect?
End of explanation
"""


class Customer(object):
    """A customer of ABC Bank with a checking account. Customers have the
    following properties:

    Attributes:
        name: A string representing the customer's name.
        balance: A float tracking the current balance of the customer's account.
    """

    def __init__(self, name, balance=0.0):
        """Return a Customer object whose name is *name* and starting
        balance is *balance*."""
        self.name = name
        self.balance = balance

    def withdraw(self, amount):
        """Return the balance remaining after withdrawing *amount*
        dollars."""
        if amount > self.balance:
            raise RuntimeError('Amount greater than available balance.')
        self.balance -= amount
        return self.balance

    def deposit(self, amount):
        """Return the balance remaining after depositing *amount*
        dollars."""
        self.balance += amount
        return self.balance
    


help(jeff)

"""
Explanation: Write a function that pulls the text from http://www.py4inf.com/code/romeo-full.txt and displays all the lines containing the word 'love'. Use the requests library.


Find a built in python library that you haven't heard of before. Learn how some of the functions work. Write a small script testing out the functions.


Introducing classes
Python: everything is an object
So everything has a class???
Classes: a blueprint for creating objects
End of explanation
"""


class Customer(object):
    """A customer of ABC Bank with a checking account. Customers have the
    following properties:

    Attributes:
        name: A string representing the customer's name.
        balance: A float tracking the current balance of the customer's account.
    """

    def __init__(self, name):
        """Return a Customer object whose name is *name*.""" 
        self.name = name

    def set_balance(self, balance=0.0):
        """Set the customer's starting balance."""
        self.balance = balance

    def withdraw(self, amount):
        """Return the balance remaining after withdrawing *amount*
        dollars."""
        if amount > self.balance:
            raise RuntimeError('Amount greater than available balance.')
        self.balance -= amount
        return self.balance

    def deposit(self, amount):
        """Return the balance remaining after depositing *amount*
        dollars."""
        self.balance += amount
        return self.balance

"""
Explanation: Be careful what you __init__
After init has finished, the caller can rightly assume that the object is ready to use. That is, after jeff = Customer('Jeff Knupp', 1000.0), we can start making deposit and withdraw calls on jeff; jeff is a fully-initialized object.
End of explanation
"""


class SMS_store(object):
    
    def __init__(self):
        self.inbox = []
    
    def add_new_arrival(self, from_number, time_arrived, text_of_SMS, has_been_viewed=False):
        # Makes new SMS tuple, inserts it after other messages
        # in the store. When creating this message, its
        # has_been_viewed status is set False.
        
        msg = (has_been_viewed, from_number, time_arrived, text_of_SMS)
        self.inbox.append(msg)
      

    def message_count(self):
        # Returns the number of sms messages in my_inbox
        return len(self.inbox)
  
    def get_unread_indexes(self):
        # Returns list of indexes of all not-yet-viewed SMS messages
        return [i for i, elem in enumerate(self.inbox) if not elem[0]]
"""
    my_inbox.get_message(i)
        # Return (from_number, time_arrived, text_of_sms) for message[i]
        # Also change its state to "has been viewed".
        # If there is no message at position i, return None
        

    my_inbox.delete(i)     # Delete the message at index i
    my_inbox.clear()       # Delete all messages from inbox

"""

my_inbox = SMS_store()

my_inbox.add_new_arrival('adasf', 'asdf', 'asdf')
my_inbox.inbox

my_inbox.get_unread_indexes()

class WhatDoesThe(object):
    
    def cow_say():
        return "MOOO"
    
    def elephant_say():
        return "PFHARGLE"
    
    def seal_say():
        return "AUGHAUGHAUGH"
    
    def fox_say():
        return "tingalingalingalinga"
    
    @staticmethod
    def FoxSay(self):
        print(cow_say())
        print(elephant_say())
        print(seal_say())
        print(fox_say())


WhatDoesThe.FoxSay()

what_does_the.FoxSay()

"""
Explanation: A change in perspective
Objects can have state
Exercise
With your partner: 
Create a new class, SMS_store. The class will instantiate SMS_store objects, similar to an inbox or outbox on a cellphone:
my_inbox = SMS_store()
This store can hold multiple SMS messages (i.e. its internal state will just be a list of messages). Each message will be represented as a tuple:
(has_been_viewed, from_number, time_arrived, text_of_SMS)
The inbox object should provide these methods:
```
my_inbox.add_new_arrival(from_number, time_arrived, text_of_SMS)
    # Makes new SMS tuple, inserts it after other messages
    # in the store. When creating this message, its
    # has_been_viewed status is set False.
my_inbox.message_count()
    # Returns the number of sms messages in my_inbox
my_inbox.get_unread_indexes()
    # Returns list of indexes of all not-yet-viewed SMS messages
my_inbox.get_message(i)
    # Return (from_number, time_arrived, text_of_sms) for message[i]
    # Also change its state to "has been viewed".
    # If there is no message at position i, return None
my_inbox.delete(i)     # Delete the message at index i
my_inbox.clear()       # Delete all messages from inbox
```
Write the class, create a message store object, write tests for these methods, and implement the methods.
End of explanation
"""


class Car(object):

    wheels = 4

    def __init__(self, make, model):
        self.make = make
        self.model = model



mustang = Car('Ford', 'Mustang')
print(mustang.wheels)

print(Car.wheels)


"""
Explanation: Class attributes
Class attributes are attributes that are set at the class-level, as opposed to the instance-level. Normal attributes are introduced in the init method, but some attributes of a class hold for all instances in all cases. For example, consider the following definition of a Car object:
End of explanation
"""


class Car(object):

    wheels = 4
    
    def make_car_sound():
        print('VRooooommmm!')

    def __init__(self, make, model):
        self.make = make
        self.model = model


my_car = Car('ford', 'mustang')

# my_car.make_car_sound() # This will break
Car.make_car_sound()


class Car(object):

    wheels = 4
    
    @staticmethod
    def make_car_sound():
        print('VRooooommmm!')

    def __init__(self, make, model):
        self.make = make
        self.model = model

my_car = Car('ford', 'mustang')

my_car.make_car_sound()

"""
Explanation: Static methods
End of explanation
"""


