import tkinter as tk
from tkinter import messagebox, Tk, Label, Spinbox, Button, Text, END
from tkinter.scrolledtext import ScrolledText
import threading
import subprocess as sub
import os
import sys
import shutil
import queue
from datetime import datetime
import re

# Initialize a thread-safe queue for inter-thread communication
msg_queue = queue.Queue()

# Initialize the main window
mainwin = Tk()
mainwin.title('Codeforces Hack Assistant')

# GUI Layout
Label(mainwin, text="Your Solution (CPP)").grid(row=1, column=0, columnspan=15)
Label(mainwin, text="Solution to Hack (CPP)").grid(row=1, column=16, columnspan=15)

source = ScrolledText(mainwin, height=10, width=60)
source.grid(row=2, column=0, padx=20, pady=(0, 50), columnspan=15)

compare = ScrolledText(mainwin, height=10, width=60)
compare.grid(row=2, column=16, padx=20, pady=(0, 50), columnspan=15)

Label(mainwin, text="Test Case Generator (PYTHON)").grid(row=3, column=0, columnspan=15)
generator = ScrolledText(mainwin, height=10, width=60)
generator.grid(row=4, rowspan=3, column=0, padx=20, pady=(0, 50), columnspan=15)

Label(mainwin, text="Number of Threads: ").grid(row=4, column=17, columnspan=5)
spin = Spinbox(mainwin, from_=1, to=16)
spin.grid(row=4, column=22)

var1 = tk.IntVar()
checkbox = tk.Checkbutton(mainwin, text='Use Test Case Instead', variable=var1, onvalue=1, offvalue=0)
checkbox.grid(row=5, column=22)

# Initialize the log text area at startup to prevent AttributeError
tex = Text(mainwin, height=10, width=120)
tex.grid(row=7, padx=20, pady=(0, 20), column=0, columnspan=30)
tex.configure(state='disabled')

# Define a red tag for highlighting errors and mismatches
tex.tag_configure('red', foreground='red')

# Initialize global variables
execute = None
exit_btn = None
continue_executing = True
workers = []
test_num = 1

mutex = threading.Lock()
available = threading.Lock()

# NEW: Initialize a restart counter to prevent infinite automatic restarts
max_restarts = 5  # Maximum number of automatic restarts
current_restarts = 0

# Create 'usable' directory if it doesn't exist
if not os.path.exists("usable"):
    os.mkdir("usable")

def on_closing():
    if messagebox.askokcancel("Quit", "Do you want to quit?"):
        stop_all_workers()
        if os.path.exists("usable"):
            shutil.rmtree("usable")
        mainwin.destroy()

def printf(input_text, end_char="\n"):
    """
    Enqueue messages to be printed in the GUI with timestamps.
    """
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    msg_queue.put((f"[{timestamp}] {input_text}", end_char))

def passed():
    """
    Enqueue a message indicating a passed test.
    """
    global test_num
    with mutex:
        msg_queue.put((f"Test Passed: {test_num}", "\n"))
        test_num += 1

def stop_all_workers():
    """
    Signal all worker threads to stop and wait for them to finish.
    """
    global continue_executing, workers, test_num
    continue_executing = False
    current_thread = threading.current_thread()
    for i, worker in enumerate(workers.copy()):
        if worker != current_thread:
            worker.join(timeout=1)
            msg_queue.put((f"Closed Worker: {i + 1}", "\n"))
    workers.clear()
    test_num = 1
    continue_executing = True
    # Re-enable GUI elements through queue
    msg_queue.put(("ENABLE_GUI", ""))
    # NEW: Reset the restart counter when stopping all workers
    global current_restarts
    current_restarts = 0

def reset_gui_after_failure():
    """
    Reset GUI elements after a hack failure to allow re-execution.
    """
    # Re-enable GUI elements
    checkbox.configure(state='normal')
    execute.configure(state='normal')
    spin.configure(state='normal')
    source.configure(state='normal')
    compare.configure(state='normal')
    generator.configure(state='normal')
    exit_btn.configure(state='disabled')

def rewrite(path, data):
    """
    Write data to a file at the specified path.
    """
    with open(path, "w") as raw:
        raw.write(data)

def parse_cpp_inputs(cpp_code):
    """
    Parses C++ code to extract input variables and their types.
    """
    # Regex to capture variable declarations (supports basic types)
    var_decl_regex = re.compile(r'\b(int|long_long|long|double|float|string)\s+(\w+);')
    
    # Regex to capture cin >> lines
    cin_regex = re.compile(r'cin\s*>>\s*(.+?);')
    
    # Map to store variable types
    var_types = {}
    
    # Find all variable declarations
    for match in var_decl_regex.finditer(cpp_code):
        var_type = match.group(1)
        var_name = match.group(2)
        var_types[var_name] = var_type
    
    # Find all cin >> lines and extract variables
    inputs = []
    for match in cin_regex.finditer(cpp_code):
        vars_part = match.group(1)
        # Split by '>>' and possible multiple variables
        vars = [v.strip() for v in vars_part.split('>>')]
        inputs.extend(vars)
    
    # Remove duplicates while preserving order
    seen = set()
    inputs = [x for x in inputs if not (x in seen or seen.add(x))]
    
    # Get types for input variables
    input_vars = []
    for var in inputs:
        var_type = var_types.get(var, 'int')  # default to int if type not found
        input_vars.append((var, var_type))
    
    return input_vars

def generate_test_case_generator(input_vars, test_cases=10):
    """
    Generates Python test case generator code based on input variables and their types.
    """
    generator_code = """import random

def generate_test_input():
    t = random.randint(1, {test_cases})
    print(t)
    for _ in range(t):
""".format(test_cases=test_cases)
    
    for var, var_type in input_vars:
        indent = ' ' * 8
        if var_type == 'int':
            generator_code += f"{indent}{var} = random.randint(1, 10**9)\n"
        elif var_type in ['long_long', 'long']:
            generator_code += f"{indent}{var} = random.randint(1, 10**18)\n"
        elif var_type in ['double', 'float']:
            generator_code += f"{indent}{var} = random.uniform(1.0, 10**9)\n"
        elif var_type == 'string':
            generator_code += f"{indent}{var} = ''.join(random.choices('abcdefghijklmnopqrstuvwxyz', k=10))\n"
        else:
            generator_code += f"{indent}{var} = 0  # Unknown type: {var_type}\n"
        generator_code += f"{indent}print({var})\n"
    
    generator_code += "\n# Example usage:\n# generate_test_input()\n"
    
    return generator_code

def auto_generate_generator():
    """
    Automatically generates the test case generator based on the C++ solution code.
    Allows further editing after generation.
    """
    cpp_code = source.get(1.0, END)
    input_vars = parse_cpp_inputs(cpp_code)
    
    if not input_vars:
        printf("No input variables found. Please ensure your C++ code contains input reading via 'cin'.", "")
        return
    
    generator_code = generate_test_case_generator(input_vars, test_cases=10)
    
    # Populate the generator text area with the generated code
    generator.configure(state='normal')
    generator.delete(1.0, END)
    generator.insert(END, generator_code)
    generator.configure(state='normal')  # Keep it editable after auto-generation
    
    # Optionally, write the generator code to the 'usable' directory
    rewrite("usable/generator.py", generator_code)
    
    printf("Test case generator auto-generated successfully.", "")

def execut(name, input_data, type_, num=0):
    """
    Execute a program (Python or C) and return its output and errors.
    """
    if type_ == "Python":
        # Execute Python script using the current Python interpreter
        command = [sys.executable, f'{name}.py']
        try:
            p = sub.Popen(command, stdout=sub.PIPE, stderr=sub.PIPE, cwd='usable')
            output, errors = p.communicate(timeout=10)
        except sub.TimeoutExpired:
            p.kill()
            output, errors = p.communicate()
        except FileNotFoundError:
            msg_queue.put(("RED:Python interpreter not found.", ""))
            return "", "Python interpreter not found."
        return output.decode('utf-8'), errors.decode('utf-8')

    elif type_ == "C":
        # Prepare input file
        input_file = f"usable/input{num}.txt"
        rewrite(input_file, str(input_data))
    
        # Execute C program
        executable = f"./{name}" if os.name != 'nt' else f"{name}.exe"
        command = [executable]
    
        try:
            with open(input_file, 'r') as f:
                p = sub.Popen(command, stdout=sub.PIPE, stderr=sub.PIPE, cwd='usable', stdin=f)
                output, errors = p.communicate(timeout=10)
        except FileNotFoundError:
            msg_queue.put((f"RED:Executable {name} not found in 'usable' directory.", ""))
            return "", "Executable not found."
        except sub.TimeoutExpired:
            p.kill()
            output, errors = p.communicate()
        return output.decode('utf-8'), errors.decode('utf-8')

    else:
        msg_queue.put((f"RED:Unknown type: {type_}", ""))
        return "", f"Unknown type: {type_}"

def compares(input_text, pp):
    """
    Compare the outputs of the source and hack solutions.
    """
    a_output, a_errors = execut(f'source{pp}', input_text, "C", pp)
    b_output, b_errors = execut(f'compare{pp}', input_text, "C", pp)

    if a_errors.strip():
        msg_queue.put((f"RED:Runtime Error on Source!\nInput:\n{input_text}\n\nError:\n{a_errors}\nExiting Thread..", ""))
        stop_all_workers()
        # NEW: Enqueue a message to start a new session
        msg_queue.put(("START_NEW_SESSION", ""))
        return False
    if b_errors.strip():
        msg_queue.put((f"RED:Runtime Error on Hack!\nInput:\n{input_text}\n\nError:\n{b_errors}\nExiting Thread..", ""))
        stop_all_workers()
        # NEW: Enqueue a message to start a new session
        msg_queue.put(("START_NEW_SESSION", ""))
        return False
    if a_output.strip() != b_output.strip():
        mismatch_message = (
            f"RED:Output Mismatch Detected!\n\n"
            f"Input:\n{input_text}\n\n"
            f"Your Solution Output:\n{a_output}\n\n"
            f"Hack Solution Output:\n{b_output}\n\n"
            f"Exiting Thread.."
        )
        msg_queue.put((mismatch_message, ""))
        stop_all_workers()
        # NEW: Enqueue a message to start a new session
        msg_queue.put(("START_NEW_SESSION", ""))
        return False
    return True

def run(pp, single=False):
    """
    Worker thread function to compile and execute code.
    """
    global continue_executing
    with available:
        if not continue_executing:
            return

        # Compile the source.cpp
        compile_source = ['clang++', 'source.cpp', '-o', f'source{pp}']
        try:
            p = sub.Popen(compile_source, stdout=sub.PIPE, stderr=sub.PIPE, cwd='usable')
            output, errors = p.communicate()
            errors = errors.decode('utf-8')
            if errors.strip():
                msg_queue.put(("RED:Source Compilation Error!\nExiting..", ""))
                stop_all_workers()
                # NEW: Enqueue a message to start a new session
                msg_queue.put(("START_NEW_SESSION", ""))
                return
            # Set executable permissions (Unix-based systems)
            if os.name != 'nt':
                os.chmod(f'usable/source{pp}', 0o755)
        except FileNotFoundError:
            msg_queue.put(("RED:clang++ not found. Please ensure it is installed and added to PATH.", ""))
            stop_all_workers()
            # NEW: Enqueue a message to start a new session
            msg_queue.put(("START_NEW_SESSION", ""))
            return

        # Compile the compare.cpp
        compile_compare = ['clang++', 'compare.cpp', '-o', f'compare{pp}']
        try:
            p = sub.Popen(compile_compare, stdout=sub.PIPE, stderr=sub.PIPE, cwd='usable')
            output, errors = p.communicate()
            errors = errors.decode('utf-8')
            if errors.strip():
                msg_queue.put(("RED:Solution to Hack Compilation Error!\nExiting..", ""))
                stop_all_workers()
                # NEW: Enqueue a message to start a new session
                msg_queue.put(("START_NEW_SESSION", ""))
                return
            # Set executable permissions (Unix-based systems)
            if os.name != 'nt':
                os.chmod(f'usable/compare{pp}', 0o755)
        except FileNotFoundError:
            msg_queue.put(("RED:clang++ not found. Please ensure it is installed and added to PATH.", ""))
            stop_all_workers()
            # NEW: Enqueue a message to start a new session
            msg_queue.put(("START_NEW_SESSION", ""))
            return

        # If not running in single mode, update the generator script
        if not single:
            rewrite(f"usable/generator{pp}.py", generator.get(1.0, END))

    while continue_executing:
        if single:
            # Use the provided test case instead of generating one
            test_case = generator.get(1.0, END).strip()
            if not test_case:
                msg_queue.put(("RED:No test case provided.\nExiting..", ""))
                stop_all_workers()
                # NEW: Enqueue a message to start a new session
                msg_queue.put(("START_NEW_SESSION", ""))
                return
            result = compares(test_case, pp)
            if result:
                passed()
            # After a single test case, stop execution to allow user modification
            stop_all_workers()
            return

        # Execute the generator to produce a test case
        generate_output, generate_errors = execut(f'generator{pp}', "", "Python")
        if generate_errors.strip():
            msg_queue.put((f"RED:Generator Error!\n{generate_errors}\nExiting..", ""))
            stop_all_workers()
            # NEW: Enqueue a message to start a new session
            msg_queue.put(("START_NEW_SESSION", ""))
            return

        # Compare the outputs of the source and hack solutions
        result = compares(generate_output, pp)
        if result:
            passed()
        else:
            # If mismatch, stop and allow user to edit generator
            return

def printer():
    """
    Start the execution process by compiling code and initiating worker threads.
    """
    global execute, exit_btn
    global current_restarts  # NEW: Access the restart counter

    # Clear previous logs
    tex.configure(state='normal')
    tex.delete(1.0, END)
    tex.configure(state='disabled')
    
    # Write the source and compare C++ codes to the 'usable' directory
    rewrite("usable/source.cpp", source.get(1.0, END))
    rewrite("usable/compare.cpp", compare.get(1.0, END))

    # Reset restart counter when manually starting a new session
    current_restarts = 0  # NEW:

    # Disable GUI elements during execution
    checkbox.configure(state='disabled')
    execute.configure(state='disabled')
    spin.configure(state='disabled')
    source.configure(state='disabled')
    compare.configure(state='disabled')
    generator.configure(state='disabled')
    exit_btn.configure(state='normal')

    if var1.get() == 1:
        # Single-threaded mode using provided test case
        p = threading.Thread(target=run, args=(str(0), True), name='0', daemon=True)
        p.start()
        workers.append(p)
        printf("Started Thread: 1")
    else:
        # Multi-threaded mode with generated test cases
        for i in range(int(spin.get())):
            p = threading.Thread(target=run, args=(str(i),), name=str(i), daemon=True)
            p.start()
            workers.append(p)
            printf(f"Started Thread: {i + 1}")
    return

def process_queue():
    """
    Periodically check the queue and update the GUI.
    """
    global current_restarts  # NEW: Access the restart counter
    try:
        while True:
            message, end_char = msg_queue.get_nowait()
            if message.startswith("RED:"):
                # Remove the 'RED:' prefix
                clean_message = message[4:]
                tex.configure(state='normal')
                tex.insert(tk.END, clean_message + end_char, 'red')
                tex.see(tk.END)
                tex.configure(state='disabled')
            elif message == "ENABLE_GUI":
                # Re-enable GUI elements
                reset_gui_after_failure()
                printf("Ready for new execution.", "")
            elif message == "START_NEW_SESSION":
                # NEW: Handle automatic restart
                if current_restarts < max_restarts:
                    current_restarts += 1
                    printf(f"Starting new testing session automatically ({current_restarts}/{max_restarts})...", "")
                    mainwin.after(1000, printer)  # Restart after 1 second
                else:
                    printf("Maximum number of automatic restarts reached.", "")
            else:
                tex.configure(state='normal')
                tex.insert(tk.END, message + end_char)
                tex.see(tk.END)
                tex.configure(state='disabled')
    except queue.Empty:
        pass
    finally:
        mainwin.after(100, process_queue)  # Check the queue every 100 ms

# Initialize Execute, Stop, and Auto-Generate Generator buttons
execute = Button(mainwin, text="Execute", command=printer)
execute.grid(row=6, column=18, columnspan=5, sticky="EW", pady=(0, 25))

exit_btn = Button(mainwin, text="Stop", command=stop_all_workers)
exit_btn.grid(row=6, column=25, columnspan=3, sticky="EW", pady=(0, 25))
exit_btn.configure(state='disabled')

# Initialize Auto-Generate Generator button
auto_gen_btn = Button(mainwin, text="Auto-Generate Generator", command=auto_generate_generator)
auto_gen_btn.grid(row=3, column=16, columnspan=15, sticky="EW", padx=20, pady=(10, 0))

# Handle window closing event
mainwin.protocol("WM_DELETE_WINDOW", on_closing)

# Start processing the queue
mainwin.after(100, process_queue)

# Start the Tkinter event loop
mainwin.mainloop(). A new session should only be started on hitting execute button only not automatically restart after finding a mismatch!