#!/usr/bin/env python
# Christopher D. Leary (christopher.leary@cornell.edu)
# A starvation-vulnerable implementation of smokers and non-smokers
# entering/smoking in a bar. It's a bit silly, but gets the point across.


import threading
import itertools
import random

from monitor import Monitor





_notify = True


def notify(msg):
    if _notify:
        print msg





class Bar(Monitor):

    def __init__(self):
        Monitor.__init__(self)
        self.smoking_count = 0
        self.none_smoking = Monitor.Condition(self)

        self.non_smoker_count = 0
        self.all_smokers = Monitor.Condition(self)

    def enter_bar(self, is_smoker):
        if not is_smoker:
            name = threading.currentThread().getName()
            self.non_smoker_count += 1
            if self.smoking_count > 0:
                notify('%s waiting for everybody to stop smoking.' % name)
                self.none_smoking.wait()
            notify('NonSmoker %s has entered.' % name)

    def leave_bar(self, is_smoker):
        name = threading.currentThread().getName()
        if is_smoker:
            notify('Smoker %s has left.' % name)
        else:
            self.non_smoker_count -= 1
            notify('NonSmoker %s has left.' % name)
            if self.non_smoker_count <= 0:
                self.all_smokers.notifyAll()

    def want_smoke(self):
        name = threading.currentThread().getName()
        if self.non_smoker_count > 0:
            self.all_smokers.wait()
        notify('Smoker %s has lit up.' % name)
        self.smoking_count += 1

    def done_smoking(self):
        name = threading.currentThread().getName()
        self.smoking_count -= 1
        msg = 'Smoker %s has "quit" smoking; %d left.'
        notify(msg % (name, self.smoking_count))
        if self.smoking_count <= 0:
            self.none_smoking.notifyAll()





class Person(threading.Thread):

    person_id = itertools.count()

    def __init__(self, bar):
        threading.Thread.__init__(self, target=self.run)
        self.bar = bar
        self.setName('Person %d' % Person.person_id.next())


class Smoker(Person):

    def __init__(self, bar):
        Person.__init__(self, bar)
        self.nicotine_craving = threading.Event()
        self.bored = threading.Event()

    def run(self):
        bar = self.bar
        bar.enter_bar(True)
        neurotic_cycles = random.randint(1, 20)
        for i in range(neurotic_cycles):
            self.be_neurotic()
        bar.leave_bar(True)
        name = threading.currentThread().getName()
        notify('Smoker %s done.' % name)

    def get_craving(self):
        self.nicotine_craving.set()

    def get_bored(self):
        self.bored.set()

    def be_neurotic(self):
        craving_time = random.random()
        threading.Timer(craving_time, self.get_craving).start()
        self.nicotine_craving.wait()
        self.nicotine_craving.clear()
        self.bar.want_smoke()

        bored_time = random.random()
        threading.Timer(bored_time, self.get_bored).start()
        self.bored.wait()
        self.bored.clear()
        self.bar.done_smoking()


class NonSmoker(Person):

    def __init__(self, bar):
        Person.__init__(self, bar)
        self.go_outside = threading.Event()
        self.go_inside = threading.Event()

    def get_bored(self):
        self.go_outside.set()

    def get_lonely(self):
        self.go_inside.set()

    def run(self):
        bar = self.bar
        neurotic_cycles = random.randint(1, 5)
        for i in range(neurotic_cycles):
            self.be_neurotic()
        name = threading.currentThread().getName()
        notify('NonSmoker %s done' % name)

    def be_neurotic(self):
        lonely_time = random.random()
        threading.Timer(lonely_time, self.get_lonely).start()
        self.go_inside.wait()
        self.go_inside.clear()
        self.bar.enter_bar(False)

        bored_time = random.random()
        threading.Timer(bored_time, self.get_bored).start()
        self.go_outside.wait()
        self.go_outside.clear()
        self.bar.leave_bar(False)





def main():
    bar = Bar()
    people = []
    num_people = random.randrange(10, 30)
    for i in range(num_people):
        if i % 2 == 0:
            person = NonSmoker(bar)
        else:
            person = Smoker(bar)
        people.append(person)

    for person in people:
        person.start()


if __name__ == '__main__':
    main()
