import tkinter as tk
from tkinter import ttk, scrolledtext
import time
import threading
import random
import string
from typing import List, Tuple
from concurrent.futures import ThreadPoolExecutor, as_completed

class BrainfuckGeneratorGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Brainfuck String Generator")
        self.root.geometry("900x750")
        
        # Variables
        self.current_best_code = ""
        self.last_input = ""
        self.generation_lock = threading.Lock()
        self.stop_generation = False
        self.init_factors = []
        self.generation_start_time = 0
        self.tests_performed = 0
        self.total_combinations = 0
        self.used_branch_distance = 0
        self.used_init_factor = 0
        self.benchmark_mode = False
        self.benchmark_data = ""
        self.current_generation_id = 0  # Track current generation session
        
        # Thread pool for parallel execution
        self.thread_pool = ThreadPoolExecutor(max_workers=4)
        
        # Detail level
        self.detail_level = tk.StringVar(value="Normal")
        
        self.setup_ui()
        
    def setup_ui(self):
        # Main frame
        main_frame = ttk.Frame(self.root, padding="5")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # Top controls frame
        top_frame = ttk.Frame(main_frame)
        top_frame.pack(fill=tk.X, pady=(0, 5))
        
        # Detail level
        ttk.Label(top_frame, text="Detail:").pack(side=tk.LEFT, padx=(0, 5))
        detail_combo = ttk.Combobox(top_frame, textvariable=self.detail_level, 
                                   values=["None", "Normal", "Detailed"], 
                                   state="readonly", width=10)
        detail_combo.pack(side=tk.LEFT, padx=(0, 15))
        detail_combo.bind('<<ComboboxSelected>>', self.on_detail_level_change)
        
        # Benchmark button
        ttk.Button(top_frame, text="🚀 Benchmark", command=self.start_benchmark, 
                  width=12).pack(side=tk.LEFT, padx=(0, 15))
        
        # Clear and Copy buttons
        ttk.Button(top_frame, text="Clear", command=self.clear_text, width=8).pack(side=tk.RIGHT, padx=(5, 0))
        ttk.Button(top_frame, text="Copy Code", command=self.copy_code, width=10).pack(side=tk.RIGHT)
        
        # Main content area (2 columns)
        content_frame = ttk.Frame(main_frame)
        content_frame.pack(fill=tk.BOTH, expand=True)
        
        # Left column - Input and Output
        left_frame = ttk.Frame(content_frame)
        left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 5))
        
        # Input section (collapsible)
        self.input_section = CollapsibleSection(left_frame, "Input Text", initially_collapsed=False)
        self.input_section.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
        
        self.input_text = scrolledtext.ScrolledText(self.input_section.content_frame, height=8, wrap=tk.WORD)
        self.input_text.pack(fill=tk.BOTH, expand=True)
        self.input_text.bind('<KeyRelease>', self.on_input_change)
        
        # Output section (collapsible)
        self.output_section = CollapsibleSection(left_frame, "Brainfuck Output", initially_collapsed=False)
        self.output_section.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
        
        self.output_text = scrolledtext.ScrolledText(self.output_section.content_frame, height=10, wrap=tk.WORD, state=tk.DISABLED)
        self.output_text.pack(fill=tk.BOTH, expand=True)
        
        # Status bar
        status_frame = ttk.Frame(left_frame)
        status_frame.pack(fill=tk.X)
        
        self.status_var = tk.StringVar(value="Ready")
        status_label = ttk.Label(status_frame, textvariable=self.status_var, relief=tk.SUNKEN, padding="2")
        status_label.pack(side=tk.LEFT, fill=tk.X, expand=True)
        
        self.length_var = tk.StringVar(value="Length: 0")
        length_label = ttk.Label(status_frame, textvariable=self.length_var, relief=tk.SUNKEN, padding="2")
        length_label.pack(side=tk.RIGHT)
        
        # Right column - Options and Statistics
        right_frame = ttk.Frame(content_frame, width=350)
        right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, padx=(5, 0))
        right_frame.pack_propagate(False)
        
        # Options section (collapsible)
        self.options_section = CollapsibleSection(right_frame, "Optimization Options", initially_collapsed=True)
        self.options_section.pack(fill=tk.X, pady=(0, 5))
        
        # Branch distance
        branch_frame = ttk.Frame(self.options_section.content_frame)
        branch_frame.pack(fill=tk.X, pady=2)
        
        ttk.Label(branch_frame, text="Branch Distance:").pack(side=tk.LEFT)
        self.branch_var = tk.StringVar(value="5-15")
        branch_combo = ttk.Combobox(branch_frame, textvariable=self.branch_var, 
                                   values=["1-10", "5-15", "10-20", "1-20", "5-25", "1-50", "Custom"], 
                                   state="readonly", width=8)
        branch_combo.pack(side=tk.RIGHT)
        branch_combo.bind('<<ComboboxSelected>>', self.on_branch_selection_change)
        
        # Custom branch controls
        self.custom_branch_frame = ttk.Frame(self.options_section.content_frame)
        self.custom_branch_frame.pack(fill=tk.X, pady=2)
        self.custom_branch_frame.pack_forget()
        
        ttk.Label(self.custom_branch_frame, text="Min:").pack(side=tk.LEFT)
        self.min_branch_var = tk.StringVar(value="1")
        min_branch_spin = ttk.Spinbox(self.custom_branch_frame, from_=1, to=50, width=4, 
                                     textvariable=self.min_branch_var)
        min_branch_spin.pack(side=tk.LEFT, padx=(2, 8))
        min_branch_spin.bind('<KeyRelease>', self.on_options_change)
        
        ttk.Label(self.custom_branch_frame, text="Max:").pack(side=tk.LEFT)
        self.max_branch_var = tk.StringVar(value="20")
        max_branch_spin = ttk.Spinbox(self.custom_branch_frame, from_=1, to=50, width=4,
                                     textvariable=self.max_branch_var)
        max_branch_spin.pack(side=tk.LEFT, padx=(2, 0))
        max_branch_spin.bind('<KeyRelease>', self.on_options_change)
        
        # Factors section
        factors_frame = ttk.LabelFrame(self.options_section.content_frame, text="Initialization Factors", padding="3")
        factors_frame.pack(fill=tk.X, pady=2)
        
        # Compact factors grid
        factors_grid = ttk.Frame(factors_frame)
        factors_grid.pack(fill=tk.X)
        
        self.init_vars = {}
        # Create 5 rows of 5 factors each (1-25)
        for i in range(5):
            for j in range(5):
                factor = i * 5 + j + 1
                var = tk.BooleanVar(value=(factor in {1, 5, 10, 15, 20, 25}))
                self.init_vars[factor] = var
                
                cb = ttk.Checkbutton(factors_grid, text=str(factor), variable=var, 
                                   command=self.on_options_change, width=3)
                cb.grid(row=i, column=j, sticky=tk.W, padx=2, pady=1)
        
        # Additional factors and control buttons
        factors_controls = ttk.Frame(factors_frame)
        factors_controls.pack(fill=tk.X, pady=(3, 0))
        
        self.additional_factor_var = tk.BooleanVar(value=False)
        ttk.Checkbutton(factors_controls, text="26-100", variable=self.additional_factor_var, 
                       command=self.on_options_change).pack(side=tk.LEFT)
        
        ttk.Button(factors_controls, text="All", command=self.select_all_factors, width=5).pack(side=tk.RIGHT, padx=(2, 0))
        ttk.Button(factors_controls, text="None", command=self.deselect_all_factors, width=5).pack(side=tk.RIGHT, padx=(2, 0))
        ttk.Button(factors_controls, text="Default", command=self.default_factors, width=6).pack(side=tk.RIGHT, padx=(2, 0))
        
        # Statistics section (collapsible)
        self.stats_section = CollapsibleSection(right_frame, "Generation Statistics", initially_collapsed=True)
        self.stats_section.pack(fill=tk.BOTH, expand=True)
        
        # Stats content
        stats_content = self.stats_section.content_frame
        
        # Basic stats
        basic_frame = ttk.Frame(stats_content)
        basic_frame.pack(fill=tk.X, pady=2)
        
        self.basic_stats_vars = {}
        basic_stats = [
            ("Code Size:", "code_size", "0 bytes"),
            ("Input Size:", "input_size", "0 bytes"), 
            ("Ratio:", "ratio", "0%"),
            ("Tests:", "tests", "0/0"),
            ("Best Size:", "best_size", "0 bytes"),
            ("Worst Size:", "worst_size", "0 bytes"),
            ("Unicode:", "unicode", "No")
        ]
        
        for i, (label, key, default) in enumerate(basic_stats):
            frame = ttk.Frame(basic_frame)
            frame.pack(fill=tk.X, pady=1)
            ttk.Label(frame, text=label, width=12).pack(side=tk.LEFT)
            self.basic_stats_vars[key] = tk.StringVar(value=default)
            ttk.Label(frame, textvariable=self.basic_stats_vars[key], 
                     font="TkDefaultFont 9 bold").pack(side=tk.LEFT)
        
        # Performance stats
        perf_frame = ttk.Frame(stats_content)
        perf_frame.pack(fill=tk.X, pady=2)
        
        self.perf_stats_vars = {}
        perf_stats = [
            ("Tests/Sec:", "tests_sec", "0/s"),
            ("Data/Sec:", "data_sec", "0 B/s"),
            ("ETA:", "eta", "-"),
            ("Used Branch:", "used_branch", "-"),
            ("Used Factor:", "used_factor", "-")
        ]
        
        for i, (label, key, default) in enumerate(perf_stats):
            frame = ttk.Frame(perf_frame)
            frame.pack(fill=tk.X, pady=1)
            ttk.Label(frame, text=label, width=12).pack(side=tk.LEFT)
            self.perf_stats_vars[key] = tk.StringVar(value=default)
            ttk.Label(frame, textvariable=self.perf_stats_vars[key], 
                     font="TkDefaultFont 9 bold").pack(side=tk.LEFT)
        
        # Advanced stats (shown only in Detailed mode)
        self.advanced_frame = ttk.LabelFrame(stats_content, text="Advanced Stats", padding="3")
        self.advanced_frame.pack(fill=tk.X, pady=(5, 0))
        
        self.advanced_stats_vars = {}
        advanced_stats = [
            ("Compression:", "compression", "0x"),
            ("Max +:", "max_plus", "0"),
            ("Max -:", "max_minus", "0"), 
            ("Max .:", "max_dot", "0"),
            ("Minified:", "minified", "NO")
        ]
        
        for i, (label, key, default) in enumerate(advanced_stats):
            frame = ttk.Frame(self.advanced_frame)
            frame.pack(fill=tk.X, pady=1)
            ttk.Label(frame, text=label, width=12).pack(side=tk.LEFT)
            self.advanced_stats_vars[key] = tk.StringVar(value=default)
            ttk.Label(frame, textvariable=self.advanced_stats_vars[key],
                     font="TkDefaultFont 9 bold").pack(side=tk.LEFT)
        
        # Benchmark results (hidden by default)
        self.benchmark_frame = ttk.LabelFrame(stats_content, text="Benchmark Results", padding="3")
        self.benchmark_frame.pack(fill=tk.X, pady=(5, 0))
        self.benchmark_frame.pack_forget()
        
        self.benchmark_vars = {}
        benchmark_stats = [
            ("Test Data:", "test_data", "0 bytes"),
            ("Total Time:", "total_time", "0.00s"),
            ("Avg Time/Test:", "avg_time", "0.00s"),
            ("Performance:", "performance", "0 tests/s"),
            ("Best Compress:", "best_compress", "0x")
        ]
        
        for i, (label, key, default) in enumerate(benchmark_stats):
            frame = ttk.Frame(self.benchmark_frame)
            frame.pack(fill=tk.X, pady=1)
            ttk.Label(frame, text=label, width=14).pack(side=tk.LEFT)
            self.benchmark_vars[key] = tk.StringVar(value=default)
            ttk.Label(frame, textvariable=self.benchmark_vars[key],
                     font="TkDefaultFont 9 bold").pack(side=tk.LEFT)
        
        # Initialize
        self.update_init_factors()
        self.update_details_visibility()
        
    def generate_random_data(self, size_kb: int = 10) -> str:
        """Generate random test data of specified size in KB"""
        size_bytes = size_kb * 1024
        # Use a mix of printable characters to simulate real text data
        chars = string.ascii_letters + string.digits + string.punctuation + ' ' * 10
        chunks = []
        chunk_size = 1000  # Generate in chunks to avoid memory issues
        
        while len(b''.join(chunk.encode('utf-8') for chunk in chunks)) < size_bytes:
            chunk = ''.join(random.choice(chars) for _ in range(chunk_size))
            chunks.append(chunk)
        
        # Combine and trim to exact size
        result = ''.join(chunks)
        result_bytes = result.encode('utf-8')
        if len(result_bytes) > size_bytes:
            # Trim to exact size
            result = result_bytes[:size_bytes].decode('utf-8', errors='ignore')
        
        return result

    def start_benchmark(self):
        """Start benchmark with current settings"""
        self.benchmark_mode = True
        self.status_var.set("Generating 10KB test data...")
        
        # Generate test data in thread to avoid blocking UI
        def generate_and_benchmark():
            # Generate 10KB of random data
            self.benchmark_data = self.generate_random_data(10)
            self.root.after(0, self.run_benchmark)
        
        threading.Thread(target=generate_and_benchmark, daemon=True).start()

    def run_benchmark(self):
        """Run the benchmark with generated data"""
        if not self.benchmark_data:
            self.status_var.set("Benchmark failed: no data generated")
            return
            
        self.status_var.set("Running benchmark with 10KB test data...")
        self.benchmark_frame.pack(fill=tk.X, pady=(5, 0))  # Show benchmark results
        
        # Store original input and replace with benchmark data
        self.original_input = self.last_input
        self.last_input = self.benchmark_data
        
        # Update input display to show benchmark info (but not the actual data)
        self.input_text.config(state=tk.NORMAL)
        self.input_text.delete("1.0", tk.END)
        self.input_text.insert("1.0", f"[BENCHMARK MODE - 10KB RANDOM TEST DATA]\n\nData size: {len(self.benchmark_data.encode('utf-8')):,} bytes\nCharacter range: All printable ASCII\n\nActual data hidden for performance...")
        self.input_text.config(state=tk.NORMAL)
        
        # Clear output
        self.output_text.config(state=tk.NORMAL)
        self.output_text.delete("1.0", tk.END)
        self.output_text.insert("1.0", "[BENCHMARK RESULTS WILL APPEAR HERE]")
        self.output_text.config(state=tk.DISABLED)
        
        # Start benchmark generation
        threading.Thread(target=self.benchmark_generation_thread, daemon=True).start()

    def benchmark_generation_thread(self):
        """Thread function for benchmark generation"""
        with self.generation_lock:
            self.stop_generation = True
            time.sleep(0.1)
            self.stop_generation = False
            
            start_time = time.time()
            best_code, best_length, used_branch, used_factor = self.optimize_brainfuck_parallel(self.benchmark_data)
            total_time = time.time() - start_time
            
            if self.stop_generation:
                return
                
            # Calculate benchmark statistics
            total_tests = self.total_combinations
            avg_time_per_test = total_time / total_tests if total_tests > 0 else 0
            tests_per_second = total_tests / total_time if total_time > 0 else 0
            
            # Calculate compression ratio for benchmark
            input_size = len(self.benchmark_data.encode('utf-8'))
            compression_ratio = input_size / best_length if best_length > 0 else 0
            
            # Update benchmark results
            self.root.after(0, self.update_benchmark_results, total_time, avg_time_per_test, 
                          tests_per_second, compression_ratio, input_size)
            
            # Update output with benchmark summary
            benchmark_summary = (
                f"=== BENCHMARK COMPLETED ===\n\n"
                f"Test Data: 10KB random data\n"
                f"Input Size: {input_size:,} bytes\n"
                f"Best Code Size: {best_length:,} bytes\n"
                f"Compression Ratio: {compression_ratio:.2f}x\n"
                f"Total Tests: {total_tests:,}\n"
                f"Total Time: {total_time:.2f}s\n"
                f"Tests/Second: {tests_per_second:.1f}\n"
                f"Optimal Branch: {used_branch}\n"
                f"Optimal Factor: {used_factor}\n\n"
                f"Generated Brainfuck Code ({best_length:,} bytes):\n"
                f"----------------------------------------\n"
            )
            
            if best_code:
                # Show only first 500 chars of code in benchmark mode
                code_preview = best_code[:500] + ("..." if len(best_code) > 500 else "")
                full_output = benchmark_summary + code_preview
            else:
                full_output = benchmark_summary + "No code generated"
            
            self.root.after(0, self.update_benchmark_output, full_output, best_length)
            
            # Restore original input
            self.last_input = getattr(self, 'original_input', "")
            self.benchmark_mode = False

    def update_benchmark_results(self, total_time: float, avg_time: float, 
                               tests_per_second: float, compression_ratio: float, input_size: int):
        """Update benchmark results display"""
        self.benchmark_vars['test_data'].set(f"{input_size:,} bytes")
        self.benchmark_vars['total_time'].set(f"{total_time:.2f}s")
        self.benchmark_vars['avg_time'].set(f"{avg_time:.4f}s")
        self.benchmark_vars['performance'].set(f"{tests_per_second:.1f} tests/s")
        self.benchmark_vars['best_compress'].set(f"{compression_ratio:.2f}x")

    def update_benchmark_output(self, output_text: str, length: int):
        """Update output with benchmark results"""
        self.output_text.config(state=tk.NORMAL)
        self.output_text.delete("1.0", tk.END)
        self.output_text.insert("1.0", output_text)
        self.output_text.config(state=tk.DISABLED)
        self.length_var.set(f"Length: {length}")
        self.status_var.set("Benchmark completed!")

    def on_branch_selection_change(self, event=None):
        if self.branch_var.get() == "Custom":
            self.custom_branch_frame.pack()
        else:
            self.custom_branch_frame.pack_forget()
        self.on_options_change()
        
    def on_detail_level_change(self, event=None):
        self.update_details_visibility()
        if self.last_input and not self.benchmark_mode:
            self.schedule_generation()
            
    def update_details_visibility(self):
        detail_level = self.detail_level.get()
        if detail_level == "Detailed":
            self.advanced_frame.pack()
        else:
            self.advanced_frame.pack_forget()
    
    def select_all_factors(self):
        for var in self.init_vars.values():
            var.set(True)
        self.on_options_change()
    
    def deselect_all_factors(self):
        for var in self.init_vars.values():
            var.set(False)
        self.on_options_change()
    
    def default_factors(self):
        default_factors = {1, 5, 10, 15, 20, 25}
        for factor, var in self.init_vars.items():
            var.set(factor in default_factors)
        self.on_options_change()
    
    def update_init_factors(self):
        self.init_factors = [factor for factor, var in self.init_vars.items() if var.get()]
        if self.additional_factor_var.get():
            self.init_factors.extend(range(26, 101))
        self.init_factors.sort()
    
    def on_options_change(self, event=None):
        self.update_init_factors()
        if self.last_input and not self.benchmark_mode:
            self.schedule_generation()
    
    def schedule_generation(self):
        """Schedule generation with debouncing to avoid repeated processing"""
        if hasattr(self, '_gen_schedule_id'):
            self.root.after_cancel(self._gen_schedule_id)
        self._gen_schedule_id = self.root.after(500, self.start_generation)
    
    def start_generation(self):
        """Start generation in thread"""
        if not self.benchmark_mode:
            # Cancel any ongoing generation
            self.stop_generation = True
            self.current_generation_id += 1  # Invalidate previous generation
            threading.Thread(target=self.generate_brainfuck_thread, daemon=True).start()
        
    def string_to_utf8_bytes(self, input_string: str) -> List[int]:
        return list(input_string.encode('utf-8'))
    
    def generate_path(self, byte_values: List[int], max_branch_distance: int):
        if not byte_values:
            return [], ()
            
        branch_list = []
        sequence = []
        for byte_value in byte_values:
            min_branch_distance = max_branch_distance
            closest_branch = None
            
            for branch_idx, branch in enumerate(branch_list):
                if branch:
                    branch_distance = abs(branch[-1] - byte_value)
                    if branch_distance < min_branch_distance:
                        min_branch_distance = branch_distance
                        closest_branch = branch_idx
            
            if closest_branch is not None:
                sequence.append(closest_branch)
                branch_list[closest_branch].append(byte_value)
            else:
                sequence.append(len(branch_list))
                branch_list.append([byte_value])
                
        return (branch_list, tuple(sequence))

    def shift_by(self, num: int):
        if not num:
            return ''
        return '>' * num if num > 0 else '<' * -num

    def change_by(self, num: int):
        if not num:
            return ''
        return '+' * num if num > 0 else '-' * -num

    def format_data_size(self, bytes_per_sec: float) -> str:
        """Format data size in B/KB/MB/GB/TB"""
        if bytes_per_sec == 0:
            return "0 B/s"
        
        units = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s']
        size = bytes_per_sec
        unit_index = 0
        
        while size >= 1024 and unit_index < len(units) - 1:
            size /= 1024
            unit_index += 1
            
        return f"{size:.1f} {units[unit_index]}"

    def generate_code_with_init(self, input_string: str, max_branch_distance: int, init_factor: int):
        """Generate Brainfuck code for given parameters - thread safe"""
        if not input_string and input_string != "":
            return ""
            
        try:
            byte_values = self.string_to_utf8_bytes(input_string)
            
            if not byte_values:
                return ""
            
            init_sequence = '+' * init_factor
            final_code = f'{init_sequence}['
            appr = []
            branches, seq = self.generate_path(byte_values, max_branch_distance)
            
            if not branches:
                return ""
                
            for branch in branches:
                coefficient = round(branch[0] / init_factor)
                final_code += ">" + self.change_by(coefficient)
                appr.append(coefficient * init_factor)
                
            final_code += self.shift_by(-len(branches)) + '-]>'
            
            indexes = [0] * len(branches)
            for step, branch_index in enumerate(seq):
                index = indexes[branch_index]
                branch = branches[branch_index]
                
                if step > 0:
                    final_code += self.shift_by(branch_index - seq[step - 1])
                    
                if index > 0:
                    final_code += self.change_by(branch[index] - branch[index - 1])
                else:
                    final_code += self.change_by(branch[0] - appr[branch_index])
                    
                indexes[branch_index] += 1
                final_code += '.'
                
            return final_code
        except Exception as e:
            return f"Error generating code: {str(e)}"

    def analyze_code_stats(self, code: str) -> dict:
        """Analyze Brainfuck code statistics"""
        if not code:
            return {
                'compression_ratio': 0,
                'highest_plus_repeats': 0,
                'highest_minus_repeats': 0,
                'highest_dot_repeats': 0,
                'well_minified': 'NO'
            }
            
        stats = {
            'compression_ratio': 0,
            'highest_plus_repeats': 0,
            'highest_minus_repeats': 0,
            'highest_dot_repeats': 0,
            'well_minified': 'NO'
        }
        
        # Calculate terrible code size
        if self.last_input:
            byte_values = self.string_to_utf8_bytes(self.last_input)
            terrible_size = sum(abs(val) + 1 for val in byte_values)
            
            if terrible_size > 0 and len(code) > 0:
                stats['compression_ratio'] = terrible_size / len(code)
        
        # Find highest repetitions
        current_char = ''
        current_count = 0
        
        for char in code:
            if char == current_char:
                current_count += 1
            else:
                if current_char == '+' and current_count > stats['highest_plus_repeats']:
                    stats['highest_plus_repeats'] = current_count
                elif current_char == '-' and current_count > stats['highest_minus_repeats']:
                    stats['highest_minus_repeats'] = current_count
                elif current_char == '.' and current_count > stats['highest_dot_repeats']:
                    stats['highest_dot_repeats'] = current_count
                    
                current_char = char
                current_count = 1
        
        # Check for last sequence
        if current_char == '+' and current_count > stats['highest_plus_repeats']:
            stats['highest_plus_repeats'] = current_count
        elif current_char == '-' and current_count > stats['highest_minus_repeats']:
            stats['highest_minus_repeats'] = current_count
        elif current_char == '.' and current_count > stats['highest_dot_repeats']:
            stats['highest_dot_repeats'] = current_count
        
        # Check if well-minified
        valid_chars = set('+-<>[].')
        has_non_bf_chars = any(char not in valid_chars for char in code)
        has_leading_brackets = code.startswith('[]')
        has_bracket_comments = '[-]' in code and len([c for c in code if c in '[]']) > 10  # Excessive brackets
        has_adjacent_opposites = ('<>' in code or '><' in code or '+-' in code or '-+' in code)
        
        stats['well_minified'] = 'YES' if (not has_non_bf_chars and not has_leading_brackets and 
                                         not has_bracket_comments and not has_adjacent_opposites) else 'NO'
        
        return stats

    def optimize_brainfuck_parallel(self, input_text: str):
        """Parallel optimization using thread pool"""
        if input_text is None:
            return "", 0, 0, 0
            
        # Parse branch distance range
        branch_distances = self.get_branch_distances()
        self.total_combinations = len(branch_distances) * len(self.init_factors)
        
        best_code = None
        best_length = float('inf')
        used_branch = 0
        used_factor = 0
        
        input_bytes = len(input_text.encode('utf-8'))
        self.generation_start_time = time.time()
        current_generation_id = self.current_generation_id
        
        # Create tasks for parallel execution
        tasks = []
        for distance in branch_distances:
            for init_factor in self.init_factors:
                tasks.append((distance, init_factor))
        
        # Process tasks in parallel with cancellation support
        completed = 0
        futures = []
        
        # Submit tasks to thread pool
        for distance, init_factor in tasks:
            if self.stop_generation or self.current_generation_id != current_generation_id:
                break
            future = self.thread_pool.submit(self.generate_code_with_init, input_text, distance, init_factor)
            futures.append((future, distance, init_factor))
        
        # Process results as they complete
        for future, distance, init_factor in futures:
            if self.stop_generation or self.current_generation_id != current_generation_id:
                break
                
            try:
                code = future.result(timeout=30)  # 30 second timeout per task
                completed += 1
                self.tests_performed = completed
                
                # Update real-time stats
                elapsed_time = time.time() - self.generation_start_time
                tests_per_second = completed / elapsed_time if elapsed_time > 0 else 0
                data_processed = input_bytes * completed
                data_per_second = data_processed / elapsed_time if elapsed_time > 0 else 0
                
                if completed > 1:
                    eta_seconds = (elapsed_time / completed) * (self.total_combinations - completed)
                    eta_str = f"{eta_seconds:.1f}s"
                    percent_done = (completed / self.total_combinations) * 100
                else:
                    eta_str = "-"
                    percent_done = 0
                
                progress = f"Testing {completed}/{self.total_combinations} ({percent_done:.1f}%)"
                if self.benchmark_mode:
                    progress = f"Benchmark: {progress}"
                self.root.after(0, self.update_status, progress)
                self.root.after(0, self.update_real_time_stats, input_text, best_length, 0, 
                              completed, self.total_combinations, tests_per_second, data_per_second, eta_str)
                
                if code and not code.startswith("Error"):
                    code_length = len(code)
                    
                    if code_length < best_length:
                        best_code = code
                        best_length = code_length
                        used_branch = distance
                        used_factor = init_factor
                        self.root.after(0, self.update_best_so_far, best_length)
                        
            except Exception as e:
                print(f"Task failed: {e}")
                continue
        
        return best_code, best_length, used_branch, used_factor

    def get_branch_distances(self):
        """Get branch distances based on current selection"""
        branch_range = self.branch_var.get()
        if branch_range == "Custom":
            try:
                min_dist = int(self.min_branch_var.get())
                max_dist = int(self.max_branch_var.get())
                return list(range(min_dist, max_dist + 1))
            except ValueError:
                return list(range(5, 16))
        elif branch_range == "1-10":
            return list(range(1, 11))
        elif branch_range == "5-15":
            return list(range(5, 16))
        elif branch_range == "10-20":
            return list(range(10, 21))
        elif branch_range == "1-20":
            return list(range(1, 21))
        elif branch_range == "5-25":
            return list(range(5, 26))
        elif branch_range == "1-50":
            return list(range(1, 51))
        else:
            return list(range(5, 16))

    def update_real_time_stats(self, input_text: str, best_length: int, worst_length: int, 
                             tests_done: int, total_tests: int, tests_per_second: float, 
                             data_per_second: float, eta_str: str):
        """Update real-time statistics display with proper values"""
        if self.detail_level.get() == "None":
            return
            
        # Basic stats
        input_size = len(input_text.encode('utf-8'))
        has_unicode = any(ord(char) > 127 for char in input_text)
        unicode_info = "Yes" if has_unicode else "No"
        ratio = (best_length / input_size) * 100 if input_size > 0 and best_length < float('inf') else 0
        
        self.basic_stats_vars['code_size'].set(f"{best_length if best_length < float('inf') else 0} bytes")
        self.basic_stats_vars['input_size'].set(f"{input_size} bytes")
        self.basic_stats_vars['ratio'].set(f"{ratio:.1f}%")
        self.basic_stats_vars['tests'].set(f"{tests_done}/{total_tests}")
        self.basic_stats_vars['best_size'].set(f"{best_length if best_length < float('inf') else 0} bytes")
        self.basic_stats_vars['worst_size'].set(f"{worst_length} bytes")
        self.basic_stats_vars['unicode'].set(unicode_info)
        
        # Performance stats
        self.perf_stats_vars['tests_sec'].set(f"{tests_per_second:.1f}/s")
        self.perf_stats_vars['data_sec'].set(self.format_data_size(data_per_second))
        self.perf_stats_vars['eta'].set(eta_str)
        self.perf_stats_vars['used_branch'].set(str(self.used_branch_distance))
        self.perf_stats_vars['used_factor'].set(str(self.used_init_factor))
        
        # Advanced stats (only for Detailed mode with valid code)
        if self.detail_level.get() == "Detailed" and self.current_best_code:
            stats = self.analyze_code_stats(self.current_best_code)
            self.advanced_stats_vars['compression'].set(f"{stats.get('compression_ratio', 0):.2f}x")
            self.advanced_stats_vars['max_plus'].set(f"{stats.get('highest_plus_repeats', 0)}")
            self.advanced_stats_vars['max_minus'].set(f"{stats.get('highest_minus_repeats', 0)}")
            self.advanced_stats_vars['max_dot'].set(f"{stats.get('highest_dot_repeats', 0)}")
            self.advanced_stats_vars['minified'].set(f"{stats.get('well_minified', 'NO')}")

    def update_best_so_far(self, best_length: int):
        """Update the display with the best code length found so far"""
        self.length_var.set(f"Best: {best_length}")

    def on_input_change(self, event=None):
        """Handle input text changes - instantly cancel generation if input changes"""
        if self.benchmark_mode:
            return
            
        current_input = self.input_text.get("1.0", tk.END)
        
        if current_input.endswith('\n'):
            current_input = current_input[:-1]
        
        if current_input == self.last_input:
            return
            
        # Input changed - cancel any ongoing generation immediately
        self.stop_generation = True
        self.current_generation_id += 1  # Invalidate current generation
        self.last_input = current_input
        
        # Clear previous results
        self.output_text.config(state=tk.NORMAL)
        self.output_text.delete("1.0", tk.END)
        self.output_text.config(state=tk.DISABLED)
        self.length_var.set("Length: 0")
        self.status_var.set("Input changed - restarting...")
        
        # Start new generation
        self.schedule_generation()

    def generate_brainfuck_thread(self):
        """Thread function for generating Brainfuck code"""
        with self.generation_lock:
            self.stop_generation = True
            time.sleep(0.1)  # Allow previous thread to stop
            self.stop_generation = False
            
            input_text = self.last_input
            current_generation_id = self.current_generation_id
            
            if input_text == "":
                self.root.after(0, self.update_output, "", 0, 0, 0, "Ready - Empty input")
                self.root.after(0, self.clear_stats)
                return
                
            self.root.after(0, self.update_status, "Starting optimization...")
            
            start_time = time.time()
            best_code, best_length, used_branch, used_factor = self.optimize_brainfuck_parallel(input_text)
            generation_time = time.time() - start_time
            
            # Check if generation was cancelled or input changed
            if self.stop_generation or self.current_generation_id != current_generation_id:
                return
                
            if best_code is None:
                best_code = ""
                best_length = 0
                used_branch = 0
                used_factor = 0
                
            self.used_branch_distance = used_branch
            self.used_init_factor = used_factor
            
            status_text = f"Ready - Generated in {generation_time:.2f}s (branch: {used_branch}, factor: {used_factor})"
            self.root.after(0, self.update_output, best_code, best_length, used_branch, used_factor, status_text)

    def clear_stats(self):
        """Clear statistics display"""
        for var in self.basic_stats_vars.values():
            var.set("0")
        for var in self.perf_stats_vars.values():
            var.set("0")
        for var in self.advanced_stats_vars.values():
            var.set("0")
        # Hide benchmark results
        self.benchmark_frame.pack_forget()

    def update_status(self, message):
        """Update status label from main thread"""
        self.status_var.set(message)

    def update_output(self, code, length, used_branch, used_factor, status):
        """Update output text area from main thread"""
        self.output_text.config(state=tk.NORMAL)
        self.output_text.delete("1.0", tk.END)
        self.output_text.insert("1.0", code)
        self.output_text.config(state=tk.DISABLED)
        
        self.length_var.set(f"Length: {length}")
        self.status_var.set(status)
        
        self.current_best_code = code
        self.used_branch_distance = used_branch
        self.used_init_factor = used_factor

    def clear_text(self):
        """Clear both input and output text areas"""
        self.stop_generation = True
        self.current_generation_id += 1
        
        self.input_text.config(state=tk.NORMAL)
        self.input_text.delete("1.0", tk.END)
        self.input_text.config(state=tk.NORMAL)
        self.output_text.config(state=tk.NORMAL)
        self.output_text.delete("1.0", tk.END)
        self.output_text.config(state=tk.DISABLED)
        self.length_var.set("Length: 0")
        self.status_var.set("Ready")
        self.current_best_code = ""
        self.last_input = ""
        self.benchmark_mode = False
        self.clear_stats()

    def copy_code(self):
        """Copy the current best code to clipboard"""
        if self.current_best_code:
            self.root.clipboard_clear()
            self.root.clipboard_append(self.current_best_code)
            self.status_var.set("Code copied to clipboard!")
            
    def __del__(self):
        """Cleanup thread pool"""
        if hasattr(self, 'thread_pool'):
            self.thread_pool.shutdown(wait=False)


class CollapsibleSection(ttk.Frame):
    """A collapsible section frame with title and toggle button"""
    def __init__(self, parent, title, initially_collapsed=False, **kwargs):
        super().__init__(parent, **kwargs)
        self.parent = parent
        self.is_collapsed = initially_collapsed
        
        # Title frame with toggle button
        self.title_frame = ttk.Frame(self)
        self.title_frame.pack(fill=tk.X)
        
        self.toggle_btn = ttk.Button(self.title_frame, text="▼" if initially_collapsed else "▲", 
                                   width=2, command=self.toggle)
        self.toggle_btn.pack(side=tk.LEFT)
        
        self.title_label = ttk.Label(self.title_frame, text=title, font="TkDefaultFont 9 bold")
        self.title_label.pack(side=tk.LEFT, padx=(2, 0))
        
        # Content frame
        self.content_frame = ttk.Frame(self)
        if not initially_collapsed:
            self.content_frame.pack(fill=tk.BOTH, expand=True)
        
    def toggle(self):
        self.is_collapsed = not self.is_collapsed
        if self.is_collapsed:
            self.content_frame.pack_forget()
            self.toggle_btn.config(text="▼")
        else:
            self.content_frame.pack(fill=tk.BOTH, expand=True)
            self.toggle_btn.config(text="▲")


def main():
    root = tk.Tk()
    app = BrainfuckGeneratorGUI(root)
    root.mainloop()

if __name__ == "__main__":
    main()