# Maks 1 linje mellomrom
# Rekkefølge skal ikke ha noe å si
# Trenger ikke skrive i all caps, den blir omgjort til .upper() automatisk
# INPUT() for inputs som kommer fra "ingensted"
# FLIPFLOP() for flipflops
# AND() eller OR() for gates
# Inne i parantesene putter du alle "variabler" som går som DIREKTE input

# NB!! Du trenger IKKE bruke NOT gates for input som kommer invertert, siden parseren skjønner det automatisk dersom du bruker '~' direkte foran variabelnavnet (se eksempel)
# Dersom du ser at det er en faktisk NOT gate tegnet på diagrammet, så kan du bare inverte variabelen selv (A -> ~A og omvendt) for å spare tid

input_string = """
A=INPUT()
B=INPUT()

G0=AND(~Q1, ~Q0, ~A, B)
G1=AND(~Q1, Q0, ~A, B)
G2=AND(Q1, ~Q0, A, B)
G3=AND(Q1, Q0, A, B)
G4=OR(G0, G1, G2, G3)

G5=AND(~Q1, Q0, A, ~B)
G6=AND(Q1, ~Q0, A, ~B)
G7=AND(Q1, ~Q0, A, B)
G8=AND(Q1, Q0, A, B)
G9=OR(G5, G6, G7, G8)

Q0=FLIPFLOP(G4)
Q1=FLIPFLOP(G9)

X=AND(Q0, ~Q1)
Y=AND(~Q0, Q1)
Z=AND(Q0, Q1)
"""




inputs = []
gates = []
flipflops = []

def find(x):
    for y in inputs:
        if y.name == x:
            return y

    for y in gates:
        if y.name == x:
            return y

    for y in flipflops:
        if y.name == x:
            return y

    return False

class input:
    def __init__(self, name):
        
        self.name = name
        self.value = True
        inputs.append(self)

    def evaluate(self):
        return self.value

class flipflop:
    def __init__(self, name):

        self.inputs = []
        self.q = False
        self.name = name

        flipflops.append(self)

    def evaluate(self):

        return self.q
    
    def print(self):
        s = f"{self.name}: "
        for input in self.inputs:
            s += f"{input.name}, "

        print(s[:-2])


class gate:
    def __init__(self, name, type):

        self.type = type
        self.name = name
        self.inputs = []
        gates.append(self)

    def evaluate(self):

        if self.type.lower() == "and":
            r = True
            for input in self.inputs:
                r = r and input.evaluate()
            return r
        
        elif self.type.lower() == "or":
            r = False
            for input in self.inputs:
                r = r or input.evaluate()
            return r
        
        elif self.type.lower() == "not":
            return not self.inputs[0].evaluate()

    def print(self):
        s = f"{self.name}: "
        for input in self.inputs:
            s += f"{input.name}, "

        print(s[:-2])


def clk():
    # Evaluate all d
    evaluations = []
    for flipflop in flipflops:
        evaluations.append([flipflop, flipflop.inputs[0].evaluate()])

    # Set all q
    for flipflop in evaluations:
        flipflop[0].q = flipflop[1]

def parse(string):
    string = string.upper()
    string = string.replace("\n\n", "\n")
    string = string.replace(" ", "")
    if string[0] == "\n":
        string = string[1:]
    if string[-1] == "\n":
        string = string[:-1]

    # FIRST PASS
    # Retrieve names and generate empty objects
    elements = string.split("\n")

    for element in elements:
        data = element.split("=")

        if data[1] == "INPUT()":
            input(data[0])

        elif data[1][:2] == "OR":
            gate(data[0], "or")
        
        elif data[1][:3] == "AND":
            gate(data[0], "and")
        
        elif data[1][:8] == "FLIPFLOP":
            flipflop(data[0])

    
    # SECOND PASS
    # Enter inputs
    for element in elements:
        data = element.split("=")
        if data[1] != "INPUT()":
            s0 = data[1].index("(")
            s1 = data[1].index(")")

            raw_inputs = data[1][s0+1:s1]
            unique_inputs = raw_inputs.split(",")

            e = find(data[0])
            for unique_input in unique_inputs:
                if unique_input[0] != "~":
                    e.inputs.append(find(unique_input))
                
                else:
                    unique_input = unique_input[1:]
                    temp = gate(f"~{unique_input}", "not")
                    temp.inputs.append(find(unique_input))
                    e.inputs.append(temp)



def print_table(flipflops, output_gates, inputs):
    input_variation_length = 0
    string = "\n|"

    # top row
    string += f" {''.join(flipflops)} |"
    
    flipflop_name_length = len(''.join(flipflops))

    input_variations = []
    for i in range(2**len(inputs)):
        n = bin(i)[2:].rjust(len(inputs), "0")
        input_variations.append(n)
        input_variation_length = len(f"{''.join(inputs)}={n}")
        string += f" {''.join(inputs)}={n} |"

    flipflop_variations = []
    for i in range(2**len(flipflops)):
        n = bin(i)[2:].rjust(len(flipflops), "0")
        flipflop_variations.append(n)

    for gate in output_gates:
        string += f" {gate} |"

    spacer = f"\n{''.rjust(len(string)-1,'-')}\n"
    string += spacer

    # combination rows

    for flipflop_variation in flipflop_variations:
        string += f"| {flipflop_variation.rjust(flipflop_name_length, ' ')} |"

        for input_variation in input_variations:
            
            # make sure all paramters are set correctly
            for i in range(len(inputs)):
                if input_variation[i] == "0":
                    find(inputs[i]).value = False
                elif input_variation[i] == "1":
                    find(inputs[i]).value = True
                else:
                    print("FUCKSDLKFHKSLHDFLJHK")
            
            for i in range(len(flipflops)):
                if flipflop_variation[i] == "0":
                    find(flipflops[i]).q = False
                elif flipflop_variation[i] == "1":
                    find(flipflops[i]).q = True
                else:
                    print("AAAAAAAAAAAAAAAA")

            # start simulating
            # (needs to be done before clk)
            gate_string = ""
            for gate in output_gates:
                r = find(gate).evaluate()
                r = "1" if r else "0"

                gate_string += f" {r.rjust(len(gate), ' ')} |"

            evaluations = []
            
            clk()
            for flipflop in flipflops:
                evaluations.append(find(flipflop).evaluate())
            
            for i in range(len(evaluations)):
                if evaluations[i]:
                    evaluations[i] = "1"
                else:
                    evaluations[i] = "0"


            string += f" {''.join(evaluations).rjust(input_variation_length, ' ')} |"

        string += gate_string

        string += spacer


    print(string)

parse(input_string)
print_table(["Q1", "Q0"], ["X", "Y", "Z"], ["A", "B"])


# Maks 1 linje mellomrom
# Rekkefølge skal ikke ha noe å si
# Trenger ikke skrive i all caps, den blir omgjort til .upper() automatisk
# INPUT() for inputs som kommer fra "ingensted"
# FLIPFLOP() for flipflops
# AND() eller OR() for gates
# Inne i parantesene putter du alle "variabler" som går som DIREKTE input

# NB!! Du trenger IKKE bruke NOT gates for input som kommer invertert, siden parseren skjønner det automatisk dersom du bruker '~' direkte foran variabelnavnet (se eksempel)
# Dersom du ser at det er en faktisk NOT gate tegnet på diagrammet, så kan du bare inverte variabelen selv (A -> ~A og omvendt) for å spare tid

input_string = """
A=INPUT()
B=INPUT()

G0=AND(~Q1, ~Q0, ~A, B)
G1=AND(~Q1, Q0, ~A, B)
G2=AND(Q1, ~Q0, A, B)
G3=AND(Q1, Q0, A, B)
G4=OR(G0, G1, G2, G3)

G5=AND(~Q1, Q0, A, ~B)
G6=AND(Q1, ~Q0, A, ~B)
G7=AND(Q1, ~Q0, A, B)
G8=AND(Q1, Q0, A, B)
G9=OR(G5, G6, G7, G8)

Q0=FLIPFLOP(G4)
Q1=FLIPFLOP(G9)

X=AND(Q0, ~Q1)
Y=AND(~Q0, Q1)
Z=AND(Q0, Q1)
"""




inputs = []
gates = []
flipflops = []

def find(x):
    for y in inputs:
        if y.name == x:
            return y

    for y in gates:
        if y.name == x:
            return y

    for y in flipflops:
        if y.name == x:
            return y

    return False

class input:
    def __init__(self, name):
        
        self.name = name
        self.value = True
        inputs.append(self)

    def evaluate(self):
        return self.value

class flipflop:
    def __init__(self, name):

        self.inputs = []
        self.q = False
        self.name = name

        flipflops.append(self)

    def evaluate(self):

        return self.q
    
    def print(self):
        s = f"{self.name}: "
        for input in self.inputs:
            s += f"{input.name}, "

        print(s[:-2])


class gate:
    def __init__(self, name, type):

        self.type = type
        self.name = name
        self.inputs = []
        gates.append(self)

    def evaluate(self):

        if self.type.lower() == "and":
            r = True
            for input in self.inputs:
                r = r and input.evaluate()
            return r
        
        elif self.type.lower() == "or":
            r = False
            for input in self.inputs:
                r = r or input.evaluate()
            return r
        
        elif self.type.lower() == "not":
            return not self.inputs[0].evaluate()

    def print(self):
        s = f"{self.name}: "
        for input in self.inputs:
            s += f"{input.name}, "

        print(s[:-2])


def clk():
    # Evaluate all d
    evaluations = []
    for flipflop in flipflops:
        evaluations.append([flipflop, flipflop.inputs[0].evaluate()])

    # Set all q
    for flipflop in evaluations:
        flipflop[0].q = flipflop[1]

def parse(string):
    string = string.upper()
    string = string.replace("\n\n", "\n")
    string = string.replace(" ", "")
    if string[0] == "\n":
        string = string[1:]
    if string[-1] == "\n":
        string = string[:-1]

    # FIRST PASS
    # Retrieve names and generate empty objects
    elements = string.split("\n")

    for element in elements:
        data = element.split("=")

        if data[1] == "INPUT()":
            input(data[0])

        elif data[1][:2] == "OR":
            gate(data[0], "or")
        
        elif data[1][:3] == "AND":
            gate(data[0], "and")
        
        elif data[1][:8] == "FLIPFLOP":
            flipflop(data[0])

    
    # SECOND PASS
    # Enter inputs
    for element in elements:
        data = element.split("=")
        if data[1] != "INPUT()":
            s0 = data[1].index("(")
            s1 = data[1].index(")")

            raw_inputs = data[1][s0+1:s1]
            unique_inputs = raw_inputs.split(",")

            e = find(data[0])
            for unique_input in unique_inputs:
                if unique_input[0] != "~":
                    e.inputs.append(find(unique_input))
                
                else:
                    unique_input = unique_input[1:]
                    temp = gate(f"~{unique_input}", "not")
                    temp.inputs.append(find(unique_input))
                    e.inputs.append(temp)



def print_table(flipflops, output_gates, inputs):
    input_variation_length = 0
    string = "\n|"

    # top row
    string += f" {''.join(flipflops)} |"
    
    flipflop_name_length = len(''.join(flipflops))

    input_variations = []
    for i in range(2**len(inputs)):
        n = bin(i)[2:].rjust(len(inputs), "0")
        input_variations.append(n)
        input_variation_length = len(f"{''.join(inputs)}={n}")
        string += f" {''.join(inputs)}={n} |"

    flipflop_variations = []
    for i in range(2**len(flipflops)):
        n = bin(i)[2:].rjust(len(flipflops), "0")
        flipflop_variations.append(n)

    for gate in output_gates:
        string += f" {gate} |"

    spacer = f"\n{''.rjust(len(string)-1,'-')}\n"
    string += spacer

    # combination rows

    for flipflop_variation in flipflop_variations:
        string += f"| {flipflop_variation.rjust(flipflop_name_length, ' ')} |"

        for input_variation in input_variations:
            
            # make sure all paramters are set correctly
            for i in range(len(inputs)):
                if input_variation[i] == "0":
                    find(inputs[i]).value = False
                elif input_variation[i] == "1":
                    find(inputs[i]).value = True
                else:
                    print("FUCKSDLKFHKSLHDFLJHK")
            
            for i in range(len(flipflops)):
                if flipflop_variation[i] == "0":
                    find(flipflops[i]).q = False
                elif flipflop_variation[i] == "1":
                    find(flipflops[i]).q = True
                else:
                    print("AAAAAAAAAAAAAAAA")

            # start simulating
            # (needs to be done before clk)
            gate_string = ""
            for gate in output_gates:
                r = find(gate).evaluate()
                r = "1" if r else "0"

                gate_string += f" {r.rjust(len(gate), ' ')} |"

            evaluations = []
            
            clk()
            for flipflop in flipflops:
                evaluations.append(find(flipflop).evaluate())
            
            for i in range(len(evaluations)):
                if evaluations[i]:
                    evaluations[i] = "1"
                else:
                    evaluations[i] = "0"


            string += f" {''.join(evaluations).rjust(input_variation_length, ' ')} |"

        string += gate_string

        string += spacer


    print(string)

parse(input_string)
print_table(["Q1", "Q0"], ["X", "Y", "Z"], ["A", "B"])


# Maks 1 linje mellomrom # Rekkefølge skal ikke ha noe å si # Trenger ikke skrive i all caps, den blir omgjort til .upper() automatisk # INPUT() for inputs som kommer fra "ingensted" # FLIPFLOP() for flipflops # AND() eller OR() for gates # Inne i parantesene putter du alle "variabler" som går som DIREKTE input # NB!! Du trenger IKKE bruke NOT gates for input som kommer invertert, siden parseren skjønner det automatisk dersom du bruker '~' direkte foran variabelnavnet (se eksempel) # Dersom du ser at det er en faktisk NOT gate tegnet på diagrammet, så kan du bare inverte variabelen selv (A -> ~A og omvendt) for å spare tid A=INPUT() B=INPUT()  G0=AND(~Q1, ~Q0, ~A, B) G1=AND(~Q1, Q0, ~A, B) G2=AND(Q1, ~Q0, A, B) G3=AND(Q1, Q0, A, B) G4=OR(G0, G1, G2, G3)  G5=AND(~Q1, Q0, A, ~B) G6=AND(Q1, ~Q0, A, ~B) G7=AND(Q1, ~Q0, A, B) G8=AND(Q1, Q0, A, B) G9=OR(G5, G6, G7, G8)  Q0=FLIPFLOP(G4) Q1=FLIPFLOP(G9)  X=AND(Q0, ~Q1) Y=AND(~Q0, Q1) Z=AND(Q0, Q1) # Evaluate all d # Set all q # FIRST PASS # Retrieve names and generate empty objects # SECOND PASS # Enter inputs # top row # combination rows # make sure all paramters are set correctly # start simulating # (needs to be done before clk)