import random
import threading
import math
import os

class UnaryExpression:
    def __init__(self, inner):
        self.inner = inner
    
    def eval(self): raise Exception("Should be implemented")
    def __str__(self): raise Exception("Should be implemented")

class ConstExpression(UnaryExpression):
    def eval(self):
        return self.inner
    
    def __str__(self):
        return str(self.inner)

class CosExpression(UnaryExpression):
    def eval(self):
        return math.cos(math.radians(self.inner.eval()))
    
    def __str__(self):
        return f"cos({str(self.inner)})"

class SinExpression(UnaryExpression):
    def eval(self):
        return math.sin(math.radians(self.inner.eval()))

    def __str__(self):
        return f"sin({str(self.inner)})"

class SqrtExpression(UnaryExpression):
    def eval(self):
        return math.sqrt(abs(self.inner.eval()))

    def __str__(self):
        return f"sqrt(|{str(self.inner)}|)"

class Expression:
    def __init__(self, left, right):
        self.left = left
        self.right = right

    def eval(self): pass
    def __str__(self): pass

class AddExpression(Expression):
    def eval(self):
        return self.left.eval() + self.right.eval()
    
    def __str__(self):
        return f"({self.left} + {self.right})"

class SubExpression(Expression):
    def eval(self):
        return self.left.eval() - self.right.eval()
    
    def __str__(self):
        return f"({self.left} - {self.right})"
    
class MulExpression(Expression):
    def eval(self):
        return self.left.eval() * self.right.eval()
    
    def __str__(self):
        return f"({self.left} * {self.right})"
    
class DivExpression(Expression):
    def eval(self):
        return self.left.eval() / self.right.eval()
    
    def __str__(self):
        return f"({self.left} / {self.right})"

flag = "IGCTFBR0M4NCE"
hardness_max = len(flag)

suffixes = ["The Second", "Von Frankensmith", "Henry Annabel",
            "The Third", "Von Snickerdoodle", "Isabella Grace",
            "Reginald Fitzgibbons", "Elizabeth Thornfield", 
            "Percival Montague", "Amelia Penelope", 
            "The Fourth", "Bartholomew Featherstone", 
            "Penelope Wainwright", "Archibald Blackwood", 
            "Josephine Marigold", "Tobias Waverley", 
            "Matilda Beauregard", "Algernon Pickleswick", 
            "Gwendolyn Whipple", "Ferdinand Honeydew", 
            "Octavia Larkspur", "Thaddeus Pumpernickel", 
            "Prudence Wilkins", "Barnabas Smudgebottom", 
            "Clementine Pumpernickel", "Maximilian Thistledown",
            "Genevieve Pendleton", "Horace Wiggledyke", 
            "Cordelia Thistledown", "Percival Thistlewhisk", 
            "Lavinia Honeyblossom", "Reginald Whifflebottom", 
            "Arabella Featherington"]

def random_unary(hardness):
    prob = hardness / hardness_max
    is_const = random.choices([True, False], [1 - prob, prob])[0]
    
    if is_const:
        return ConstExpression(random.randrange(-1000, 1000))
    
    next_hardness = hardness - 1
    if next_hardness == -1: 
        next_hardness = 0
    
    unaries = [
        CosExpression, 
        SinExpression,
        SqrtExpression
    ]
    return random.choice(unaries)(random_puzzle(hardness))

def random_puzzle(hardness, unary_allowed = True):
    prob = hardness / hardness_max
    is_unary = random.choices([True, False], [1 - prob, prob])[0]

    if is_unary and unary_allowed:
        return random_unary(hardness)

    expressions = [
        AddExpression,
        SubExpression,
        MulExpression,
        DivExpression,
    ]

    next_hardness = hardness - 1
    if next_hardness == -1:
        next_hardness = 0

    return random.choice(expressions)(random_puzzle(next_hardness), random_puzzle(next_hardness))

def random_suffix():
    return random.choice(suffixes)

def timer_end():
    print("Sorry time is up :(\nMaybe i'll see you around")
    
    # os._exit kills the entire program regardless of threads running
    os._exit(0)

def start_timer():
    threading.Timer(10, timer_end).start()

print("Hey I am going to introduce you to a bunch of friends of mine.")
print("They would love to be your friend too but they are a bit weird they want you to solve a puzzle first to test if you are smart enough to be their friend.")
print("I know they are a bit pretentious like that, anyway they also want you to solve it in 10 seconds because otherwise it might be too easy, their words not mine.")

print("Are you ready?")
inp = input("[yes] > ")
if inp != 'yes':
    print("Okay, come back a later time when you are ready.")
    exit(0)

print("Okay here we go!")

start_timer()

for i, c in enumerate(flag):
    name = c + " " + random_suffix()
    print(f"Hello my name is {name}, I would love to be your friend but I really need you to solve this puzzle first: ")

    puzzle = random_puzzle(i + 1, unary_allowed=False)
    print(puzzle)
    answer = input("What do you think the answer is > ")
    try:
        if math.isclose(puzzle.eval(), float(answer), rel_tol=0.1):
            print("That is right! Thank you very much, we are friends now.")
        else:
            print("Nope that is not right, sorry, maybe we'll see you around")
            exit(0)
    except ValueError as e:
        print(e)
        print("Nope that is not even a number, sorry, maybe we'll see you around")
        exit(0)

print("Thanks, I think we can now call each other friends :)")