File size: 24,847 Bytes
49aea16
 
 
 
 
 
 
 
 
 
 
7a73738
 
 
 
 
49aea16
 
 
 
 
 
 
 
 
7a73738
 
 
 
 
 
 
 
49aea16
7a73738
49aea16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7a73738
 
49aea16
 
 
 
 
 
7a73738
49aea16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7a73738
 
49aea16
 
e923d87
 
7a73738
49aea16
 
7a73738
49aea16
7a73738
49aea16
7a73738
49aea16
 
 
 
 
 
e923d87
 
 
49aea16
 
 
e923d87
 
 
7a73738
 
 
 
 
 
 
 
e923d87
7a73738
49aea16
 
e923d87
49aea16
 
 
 
 
7a73738
49aea16
 
 
 
 
 
 
 
 
 
 
 
e923d87
49aea16
 
7a73738
49aea16
 
 
 
7a73738
49aea16
 
 
e923d87
49aea16
 
 
 
 
e923d87
 
49aea16
e923d87
 
49aea16
 
 
 
 
 
 
 
 
 
e923d87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49aea16
e923d87
 
49aea16
 
 
 
7a73738
49aea16
 
 
 
e923d87
49aea16
e923d87
49aea16
e923d87
 
49aea16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e923d87
 
49aea16
 
 
 
 
 
 
 
e923d87
 
7a73738
49aea16
 
 
 
 
 
 
 
 
 
7a73738
49aea16
7a73738
49aea16
 
7a73738
49aea16
7a73738
 
 
 
 
 
 
 
 
e923d87
 
7a73738
 
 
 
 
e923d87
7a73738
 
e923d87
 
7a73738
e923d87
7a73738
e923d87
7a73738
 
 
e923d87
 
7a73738
 
 
 
 
 
e923d87
7a73738
 
 
 
 
e923d87
 
7a73738
e923d87
 
 
7a73738
 
 
e923d87
 
 
 
 
 
 
 
 
 
7a73738
 
 
 
 
 
e923d87
 
7a73738
 
e923d87
 
49aea16
 
 
 
 
 
 
7a73738
e923d87
 
 
 
 
49aea16
e923d87
7a73738
49aea16
 
 
 
7a73738
 
e923d87
49aea16
 
 
 
7a73738
 
49aea16
e923d87
49aea16
e923d87
7a73738
49aea16
 
 
e923d87
49aea16
 
e923d87
49aea16
 
 
 
 
 
 
 
e923d87
7a73738
49aea16
 
 
 
e923d87
49aea16
 
7a73738
49aea16
e923d87
49aea16
 
 
 
 
 
 
 
 
 
 
 
e923d87
49aea16
 
 
 
e923d87
49aea16
7a73738
e923d87
7a73738
 
e923d87
7a73738
 
 
 
 
 
e923d87
7a73738
 
e923d87
7a73738
 
e923d87
 
 
49aea16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7a73738
 
e923d87
 
 
 
 
 
7a73738
49aea16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
import gradio as gr
import matplotlib.pyplot as plt
from Bio import SeqIO
from Bio.Seq import Seq # Though not directly used in final logic, good for context
from Bio.Restriction import RestrictionBatch, AllEnzymes, Analysis
import os # For getting filename

# Ensure matplotlib uses a non-interactive backend for Gradio
import matplotlib
matplotlib.use('Agg')

# Define paths for example files
EXAMPLE_DIR = "eg_files"
EXAMPLE_PLASMID1_PATH = os.path.join(EXAMPLE_DIR, "plasmid1_example.gb")
EXAMPLE_PLASMID2_PATH = os.path.join(EXAMPLE_DIR, "plasmid2_example.gb")

# --- Core BioPython and Plotting Functions ---

def simulate_digest_and_plot_gradio(plasmid_seq_record, enzyme_name, plasmid_label):
    """
    Simulates restriction digest and plots a virtual agarose gel.
    Uses enzyme.catalyse() for robust fragment generation.
    """
    fig, ax = plt.subplots(figsize=(6, 8)) # Adjusted size for better readability

    if plasmid_seq_record is None:
        ax.text(0.5, 0.5, f"Error: Plasmid data for '{plasmid_label}' is missing.",
                ha='center', va='center', wrap=True, color='red')
        ax.set_xticks([]); ax.set_yticks([])
        ax.set_title(f"Virtual Gel: {plasmid_label} - Error", fontsize=10)
        plt.tight_layout()
        return fig

    try:
        enzyme = AllEnzymes.get(str(enzyme_name))
        if not enzyme:
            raise ValueError(f"Enzyme '{enzyme_name}' not found in Biopython's AllEnzymes.")
    except Exception as e:
        ax.text(0.5, 0.5, f"Error: Could not load enzyme '{enzyme_name}'.\n{e}",
                ha='center', va='center', wrap=True, color='red')
        ax.set_xticks([]); ax.set_yticks([])
        ax.set_title(f"Virtual Gel: {plasmid_label} - Error", fontsize=10)
        plt.tight_layout()
        return fig

    fragments_seqs = enzyme.catalyse(plasmid_seq_record.seq)
    
    is_uncut = False
    if len(fragments_seqs) == 1 and len(fragments_seqs[0]) == len(plasmid_seq_record.seq):
        if not enzyme.search(plasmid_seq_record.seq):
            is_uncut = True

    if is_uncut:
        ax.text(0.5, 0.5, f"Enzyme {enzyme_name} does not cut {plasmid_label}",
                ha='center', va='center', wrap=True)
        ax.set_title(f"Virtual Gel: {plasmid_label} + {enzyme_name} (No Sites)", fontsize=10)
        lengths = [len(plasmid_seq_record.seq)]
    else:
        lengths = sorted([len(f) for f in fragments_seqs], reverse=True)

    ax.set_yscale("log")
    min_display_size = 10 
    plasmid_len_for_scale = max(len(plasmid_seq_record.seq), min_display_size * 10) 
    max_display_size = max(plasmid_len_for_scale * 1.1, min_display_size * 2) 
    ax.set_ylim(min_display_size, max_display_size)
    
    band_width = 0.6
    lane_center = 0.5

    if not lengths:
        ax.text(0.5, 0.5, "No fragments to display.", ha='center', va='center')
    else:
        for i, size in enumerate(lengths):
            if size < min_display_size:
                ax.text(lane_center, min_display_size * 1.1 , f"(+ {len(lengths) - i} small fragments < {min_display_size}bp not shown)", 
                        ha='center', va='top', fontsize=7, color='gray')
                break 
            ax.plot([lane_center - band_width/2, lane_center + band_width/2], [size, size], 
                    linewidth=6, color='royalblue', solid_capstyle='butt')
            ax.text(lane_center + band_width/2 + 0.05, size, f"{size} bp", 
                    va='center', ha='left', fontsize=8)

    ax.invert_yaxis()
    ax.set_title(f"Virtual Gel: {plasmid_label} digested with {enzyme_name}", fontsize=10)
    ax.set_ylabel("Fragment Size (bp)", fontsize=9)
    ax.set_xlabel("Lane 1", fontsize=9)
    ax.set_xticks([])
    ax.tick_params(axis='y', labelsize=8)

    well_top_y = ax.get_ylim()[0] 
    well_line_y = well_top_y * 1.01 
    well_depth_y = well_top_y * 0.98

    ax.plot([lane_center - band_width/1.5, lane_center + band_width/1.5], [well_line_y, well_line_y], 
            linewidth=1.5, color='black')
    ax.plot([lane_center - band_width/1.5, lane_center - band_width/1.5], [well_line_y, well_depth_y], 
            linewidth=1.5, color='black')
    ax.plot([lane_center + band_width/1.5, lane_center + band_width/1.5], [well_line_y, well_depth_y], 
            linewidth=1.5, color='black')

    plt.tight_layout(pad=1.5)
    return fig

def analyze_plasmids_gradio(file1_path, file2_path, current_plasmid_choice_for_plot):
    """
    Analyzes two plasmid files to find unique restriction enzymes and
    enzymes that cut both plasmids but with different fragmentation patterns.
    Returns status messages, plasmid data, lists of enzyme names,
    and an update for the enzyme selection dropdown.
    """
    initial_enzyme_dd_update = gr.update(choices=["Analyze plasmids first"], value="Analyze plasmids first", interactive=False)
    empty_return_for_error = ["", "", "", None, None, [], [], [], initial_enzyme_dd_update]


    # Check if example files exist if paths match example paths
    example_file_error_msg = ""
    if file1_path == EXAMPLE_PLASMID1_PATH and not os.path.exists(EXAMPLE_PLASMID1_PATH):
        example_file_error_msg += f"Example file not found: {EXAMPLE_PLASMID1_PATH}. Please create it in the '{EXAMPLE_DIR}' directory.\n"
    if file2_path == EXAMPLE_PLASMID2_PATH and not os.path.exists(EXAMPLE_PLASMID2_PATH):
        example_file_error_msg += f"Example file not found: {EXAMPLE_PLASMID2_PATH}. Please create it in the '{EXAMPLE_DIR}' directory.\n"
    
    if example_file_error_msg:
          return example_file_error_msg, *empty_return_for_error


    if file1_path is None or file2_path is None:
        return "Error: Please upload or load both plasmid files.", *empty_return_for_error

    try:
        def read_plasmid(filepath, filename_for_error):
            try:
                return SeqIO.read(filepath, "genbank")
            except Exception:
                try:
                    return SeqIO.read(filepath, "fasta")
                except Exception as e_fasta:
                    raise ValueError(f"Could not parse '{filename_for_error}'. Ensure it's a valid GenBank or FASTA file. Last error: {e_fasta}")
        
        p1_orig_filename = os.path.basename(file1_path)
        p2_orig_filename = os.path.basename(file2_path)

        plasmid1_seq_rec = read_plasmid(file1_path, p1_orig_filename)
        plasmid2_seq_rec = read_plasmid(file2_path, p2_orig_filename)

    except Exception as e:
        return str(e), *empty_return_for_error

    valid_enzyme_objects = []
    for enz_name in AllEnzymes.elements():
        enzyme_obj = AllEnzymes.get(enz_name)
        if enzyme_obj and hasattr(enzyme_obj, 'site') and enzyme_obj.site is not None:
            if hasattr(enzyme_obj, 'is_restriction') and enzyme_obj.is_restriction():
                valid_enzyme_objects.append(enzyme_obj)
            elif not hasattr(enzyme_obj, 'is_restriction'):
                 valid_enzyme_objects.append(enzyme_obj)

    if not valid_enzyme_objects:
        return "Error: Could not load any restriction enzymes from Biopython.", *empty_return_for_error

    enzymes_batch = RestrictionBatch(valid_enzyme_objects)
    analysis1 = Analysis(enzymes_batch, plasmid1_seq_rec.seq, linear=False)
    analysis2 = Analysis(enzymes_batch, plasmid2_seq_rec.seq, linear=False)

    enzymes_cutting_p1_obj = set(analysis1.with_sites().keys())
    enzymes_cutting_p2_obj = set(analysis2.with_sites().keys())

    unique_to_1_obj = sorted(list(enzymes_cutting_p1_obj - enzymes_cutting_p2_obj), key=lambda e: str(e))
    unique_to_2_obj = sorted(list(enzymes_cutting_p2_obj - enzymes_cutting_p1_obj), key=lambda e: str(e))

    unique_to_1_names = [str(e) for e in unique_to_1_obj]
    unique_to_2_names = [str(e) for e in unique_to_2_obj]
    
    p1_display_label = f"Plasmid 1 ({p1_orig_filename})"
    p2_display_label = f"Plasmid 2 ({p2_orig_filename})"

    msg1 = f"Enzymes cutting only {p1_display_label} ({len(unique_to_1_names)}):\n" + ", ".join(unique_to_1_names) if unique_to_1_names else f"No unique enzymes found for {p1_display_label}."
    msg2 = f"Enzymes cutting only {p2_display_label} ({len(unique_to_2_names)}):\n" + ", ".join(unique_to_2_names) if unique_to_2_names else f"No unique enzymes found for {p2_display_label}."
    
    # New: Find enzymes cutting both but with different fragments
    common_enzymes_obj = enzymes_cutting_p1_obj.intersection(enzymes_cutting_p2_obj)
    common_diff_fragments_enzymes_names = []
    for enzyme_obj in common_enzymes_obj:
        try:
            fragments1_seqs = enzyme_obj.catalyse(plasmid1_seq_rec.seq)
            fragments2_seqs = enzyme_obj.catalyse(plasmid2_seq_rec.seq)
            
            lengths1 = sorted([len(f) for f in fragments1_seqs])
            lengths2 = sorted([len(f) for f in fragments2_seqs])
            
            if lengths1 != lengths2:
                common_diff_fragments_enzymes_names.append(str(enzyme_obj))
        except Exception as e_cat_common:
            print(f"Warning: Error during catalysis comparison for common enzyme {str(enzyme_obj)}: {e_cat_common}")
            # Optionally skip this enzyme or log more formally
            
    common_diff_fragments_enzymes_names = sorted(list(set(common_diff_fragments_enzymes_names)))
    
    msg_common_diff = f"Enzymes cutting BOTH plasmids with DIFFERENT fragments ({len(common_diff_fragments_enzymes_names)}):\n" + \
                      ", ".join(common_diff_fragments_enzymes_names) if common_diff_fragments_enzymes_names \
                      else "No enzymes found that cut both plasmids with different fragment patterns."

    status = "Analysis complete."
    if not unique_to_1_names and not unique_to_2_names and not common_diff_fragments_enzymes_names:
        status += " No differentiating enzymes found."
    
    dd_choices = []
    if current_plasmid_choice_for_plot == "Plasmid 1":
        dd_choices = unique_to_1_names if unique_to_1_names else [f"No unique enzymes for {p1_display_label}"]
    else: 
        dd_choices = unique_to_2_names if unique_to_2_names else [f"No unique enzymes for {p2_display_label}"]

    if (current_plasmid_choice_for_plot == "Plasmid 1" and unique_to_1_names) or \
       (current_plasmid_choice_for_plot == "Plasmid 2" and unique_to_2_names):
        current_dd_update = gr.update(choices=["Select an enzyme"] + dd_choices, value="Select an enzyme", interactive=True)
    else:
        current_dd_update = gr.update(choices=dd_choices, value=dd_choices[0], interactive=False if not dd_choices or "No unique" in dd_choices[0] else True)
        
    return status, msg1, msg2, msg_common_diff, plasmid1_seq_rec, plasmid2_seq_rec, \
           unique_to_1_names, unique_to_2_names, common_diff_fragments_enzymes_names, current_dd_update

def plot_selected_digest_controller(plasmid_choice_label, enzyme_name, p1_data, p2_data):
    """
    Controller to select the correct plasmid data and call the plotting function.
    """
    fig_placeholder, ax_placeholder = plt.subplots(figsize=(6, 8))
    ax_placeholder.text(0.5, 0.5, "Plot will appear here.", ha='center', va='center')
    ax_placeholder.set_xticks([]); ax_placeholder.set_yticks([])
    plt.tight_layout()

    if not enzyme_name or enzyme_name == "Select an enzyme" or "No unique enzymes" in enzyme_name or "Analyze plasmids first" in enzyme_name:
        ax_placeholder.clear()
        ax_placeholder.text(0.5, 0.5, "Please select a valid plasmid and enzyme after analysis.", ha='center', va='center', wrap=True)
        plt.tight_layout()
        return fig_placeholder
    
    target_plasmid_rec = None
    target_label = ""

    if plasmid_choice_label == "Plasmid 1":
        if p1_data is None:
            ax_placeholder.clear()
            ax_placeholder.text(0.5, 0.5, "Plasmid 1 data not loaded. Please re-analyze.", ha='center', va='center', wrap=True, color='red')
            plt.tight_layout()
            return fig_placeholder
        target_plasmid_rec = p1_data
        target_label = "Plasmid 1"
        if hasattr(p1_data, 'name') and p1_data.name and p1_data.name !="<unknown name>": target_label += f" ({p1_data.name})"
        elif hasattr(p1_data, 'id') and p1_data.id and p1_data.id !="<unknown id>": target_label += f" ({p1_data.id})"
    elif plasmid_choice_label == "Plasmid 2":
        if p2_data is None:
            ax_placeholder.clear()
            ax_placeholder.text(0.5, 0.5, "Plasmid 2 data not loaded. Please re-analyze.", ha='center', va='center', wrap=True, color='red')
            plt.tight_layout()
            return fig_placeholder
        target_plasmid_rec = p2_data
        target_label = "Plasmid 2"
        if hasattr(p2_data, 'name') and p2_data.name and p2_data.name !="<unknown name>": target_label += f" ({p2_data.name})"
        elif hasattr(p2_data, 'id') and p2_data.id and p2_data.id !="<unknown id>": target_label += f" ({p2_data.id})"
    else:
        ax_placeholder.clear()
        ax_placeholder.text(0.5, 0.5, "Invalid plasmid selection.", ha='center', va='center', wrap=True, color='red')
        plt.tight_layout()
        return fig_placeholder

    return simulate_digest_and_plot_gradio(target_plasmid_rec, enzyme_name, target_label)

def update_enzyme_dropdown_choices_on_radio_change(plasmid_choice_label, p1_enzyme_names, p2_enzyme_names):
    if plasmid_choice_label == "Plasmid 1":
        choices = p1_enzyme_names if p1_enzyme_names else ["No unique enzymes for P1"]
        if p1_enzyme_names:
            return gr.update(choices=["Select an enzyme"] + choices, value="Select an enzyme", interactive=True)
        return gr.update(choices=choices, value=choices[0], interactive=False)
    elif plasmid_choice_label == "Plasmid 2":
        choices = p2_enzyme_names if p2_enzyme_names else ["No unique enzymes for P2"]
        if p2_enzyme_names:
            return gr.update(choices=["Select an enzyme"] + choices, value="Select an enzyme", interactive=True)
        return gr.update(choices=choices, value=choices[0], interactive=False)
    return gr.update(choices=[], value=None, interactive=False)


def load_examples_and_auto_process():
    """
    Loads example files, triggers analysis, and then attempts to auto-plot.
    """
    # Step 1: Perform analysis with example files
    status, msg1, msg2, msg_common_diff, p1_rec, p2_rec, p1_enz_names, p2_enz_names, \
    _common_diff_list_ignore, enz_dd_update_from_analysis = \
        analyze_plasmids_gradio(EXAMPLE_PLASMID1_PATH, EXAMPLE_PLASMID2_PATH, "Plasmid 1")

    # If analysis failed (e.g., files not found), p1_rec or p2_rec might be None
    if p1_rec is None or p2_rec is None :
        fig_error, ax_error = plt.subplots(figsize=(6,8))
        ax_error.text(0.5, 0.5, f"Error during example analysis:\n{status}", ha='center', va='center', color='red', wrap=True) # Display actual error
        ax_error.set_xticks([]); ax_error.set_yticks([])
        plt.tight_layout()
        # Ensure all expected output values are provided
        return status, msg1, msg2, msg_common_diff, None, None, [], [], \
               gr.update(choices=["Error"], value="Error", interactive=False), \
               gr.update(value="Plasmid 1"), fig_error

    # Step 2: Determine auto-plot parameters and update dropdown based on analysis
    auto_plot_plasmid_label = None
    auto_plot_enzyme_name = None
    auto_plot_plasmid_data = None
    final_radio_choice = "Plasmid 1" # Default
    final_enz_dd_update = enz_dd_update_from_analysis # Use directly from analysis initially

    if p1_enz_names:
        auto_plot_plasmid_label = "Plasmid 1"
        auto_plot_enzyme_name = p1_enz_names[0]
        auto_plot_plasmid_data = p1_rec
        final_radio_choice = "Plasmid 1"
        final_enz_dd_update = gr.update(choices=["Select an enzyme"] + p1_enz_names, value=auto_plot_enzyme_name, interactive=True)
    elif p2_enz_names:
        auto_plot_plasmid_label = "Plasmid 2"
        auto_plot_enzyme_name = p2_enz_names[0]
        auto_plot_plasmid_data = p2_rec
        final_radio_choice = "Plasmid 2"
        final_enz_dd_update = gr.update(choices=["Select an enzyme"] + p2_enz_names, value=auto_plot_enzyme_name, interactive=True)
    else: # No unique enzymes for either, dropdown should reflect current radio choice (P1 default from analyze call)
        if final_radio_choice == "Plasmid 1":
             final_enz_dd_update = gr.update(choices=[f"No unique enzymes for P1 ({os.path.basename(EXAMPLE_PLASMID1_PATH)})"], 
                                             value=f"No unique enzymes for P1 ({os.path.basename(EXAMPLE_PLASMID1_PATH)})", interactive=False)
        # No explicit else for P2 needed here as P1 is the default for initial dropdown population

    # Step 3: Generate plot if possible
    if auto_plot_enzyme_name and auto_plot_plasmid_data:
        # Use the actual name from p1_data or p2_data for the label if available
        plot_label_detail = ""
        if auto_plot_plasmid_label == "Plasmid 1":
            if hasattr(p1_rec, 'name') and p1_rec.name and p1_rec.name != "<unknown name>": plot_label_detail = f" ({p1_rec.name})"
            elif hasattr(p1_rec, 'id') and p1_rec.id and p1_rec.id != "<unknown id>": plot_label_detail = f" ({p1_rec.id})"
        elif auto_plot_plasmid_label == "Plasmid 2":
            if hasattr(p2_rec, 'name') and p2_rec.name and p2_rec.name != "<unknown name>": plot_label_detail = f" ({p2_rec.name})"
            elif hasattr(p2_rec, 'id') and p2_rec.id and p2_rec.id != "<unknown id>": plot_label_detail = f" ({p2_rec.id})"
        
        gel_fig = simulate_digest_and_plot_gradio(auto_plot_plasmid_data, auto_plot_enzyme_name, f"{auto_plot_plasmid_label}{plot_label_detail}")
    else:
        fig_placeholder, ax_placeholder = plt.subplots(figsize=(6, 8))
        ax_placeholder.text(0.5, 0.5, "No unique enzymes found for automatic plotting.", ha='center', va='center', wrap=True)
        ax_placeholder.set_xticks([]); ax_placeholder.set_yticks([])
        plt.tight_layout()
        gel_fig = fig_placeholder
        if not p1_enz_names and not p2_enz_names:
             final_enz_dd_update = gr.update(choices=["No unique enzymes found"], value="No unique enzymes found", interactive=False)

    # Return all updates
    return status, msg1, msg2, msg_common_diff, p1_rec, p2_rec, p1_enz_names, p2_enz_names, \
           final_enz_dd_update, gr.update(value=final_radio_choice), gel_fig


# --- Gradio Interface Definition ---
with gr.Blocks(theme=gr.themes.Default()) as demo:
    gr.Markdown("# Plasmid Restriction Digest Analyzer & Virtual Gel")
    gr.Markdown(
        "**Instructions:**\n"
        "1. Upload two plasmid sequence files (GenBank `.gb`/`.gbk` or FASTA `.fasta`/`.fna`/`.fa` format) OR click 'Load Example Files'.\n"
        "2. If uploading manually, click `Analyze Uploaded Plasmids`. Results will show:\n"
        "    a. Enzymes that uniquely cut only Plasmid 1.\n"
        "    b. Enzymes that uniquely cut only Plasmid 2.\n"
        "    c. Enzymes that cut **both** plasmids but produce different fragment patterns.\n"
        "3. Select which plasmid's **unique** enzymes you want to consider for plotting.\n"
        "4. Choose a specific enzyme from the dropdown list.\n"
        "5. Click `Generate Gel Plot` to visualize the digestion pattern for the selected plasmid and enzyme.\n"
        f"Note: For 'Load Example Files', ensure `plasmid1_example.gb` and `plasmid2_example.gb` are in a folder named `{EXAMPLE_DIR}` next to this script."
    )

    plasmid1_data_state = gr.State()
    plasmid2_data_state = gr.State()
    p1_unique_enzymes_list_state = gr.State([])
    p2_unique_enzymes_list_state = gr.State([])
    # common_diff_enzymes_list_state = gr.State([]) # Not strictly needed as state if only displayed

    with gr.Row():
        with gr.Column(scale=1):
            gr.Markdown("### 1. Upload Plasmids & Analyze")
            file_p1 = gr.File(label="Plasmid 1 File (e.g., .gb, .fasta)", type="filepath", file_types=[".gb", ".gbk", ".fasta", ".fna", ".fa"])
            file_p2 = gr.File(label="Plasmid 2 File (e.g., .gb, .fasta)", type="filepath", file_types=[".gb", ".gbk", ".fasta", ".fna", ".fa"])
            
            _current_plasmid_choice_for_plot_hidden = gr.Textbox(value="Plasmid 1", visible=False) # For analysis logic with dropdown

            analyze_btn = gr.Button("Analyze Uploaded Plasmids", variant="secondary")
            example_btn = gr.Button("Load Example Files & Auto-Analyze/Plot", variant="primary", elem_id="example_button")
        
        with gr.Column(scale=2):
            gr.Markdown("### Analysis Results")
            status_message_txt = gr.Textbox(label="Status", interactive=False, lines=1, max_lines=3)
            unique_enzymes_p1_txt = gr.Textbox(label="Enzymes cutting only Plasmid 1", interactive=False, lines=3, max_lines=6)
            unique_enzymes_p2_txt = gr.Textbox(label="Enzymes cutting only Plasmid 2", interactive=False, lines=3, max_lines=6)
            common_diff_enzymes_txt = gr.Textbox(label="Enzymes cutting BOTH plasmids (different fragments)", interactive=False, lines=3, max_lines=6) # New Textbox

    gr.Markdown("---")
    gr.Markdown("### 2. Visualize Digestion on Virtual Gel")
    
    with gr.Row():
        with gr.Column(scale=1):
            plasmid_to_plot_choice_radio = gr.Radio(
                choices=["Plasmid 1", "Plasmid 2"], 
                label="Select Plasmid for Gel Visualization (of its unique enzymes)",
                value="Plasmid 1",
                interactive=True
            )
            
            enzyme_for_plot_dropdown = gr.Dropdown(
                label="Select Unique Enzyme for Chosen Plasmid", 
                choices=["Analyze plasmids first"], 
                value="Analyze plasmids first",
                interactive=False
            )
            plot_btn = gr.Button("Generate Gel Plot for Selection", variant="secondary", elem_id="plot_button")

        with gr.Column(scale=2):
            gel_plot_output = gr.Plot(label="Virtual Agarose Gel")
    
    gr.Markdown("---")
    gr.Markdown("Developed using Biopython, Matplotlib, and Gradio.")
    gr.Markdown("Note: Large plasmid files or complex analyses might take a few moments.")

    # --- Event Handlers ---
    plasmid_to_plot_choice_radio.change(
        fn=lambda x: x, 
        inputs=[plasmid_to_plot_choice_radio], 
        outputs=[_current_plasmid_choice_for_plot_hidden] # Keep this to inform analyze_plasmids_gradio logic for initial dd
    )
    
    analyze_btn.click(
        fn=analyze_plasmids_gradio,
        inputs=[file_p1, file_p2, _current_plasmid_choice_for_plot_hidden], # Pass the hidden value
        outputs=[
            status_message_txt, unique_enzymes_p1_txt, unique_enzymes_p2_txt,
            common_diff_enzymes_txt, # New output for the new textbox
            plasmid1_data_state, plasmid2_data_state,
            p1_unique_enzymes_list_state, p2_unique_enzymes_list_state, 
            gr.State(), # Placeholder for common_diff_fragments_enzymes_names list (returned but not stored in state)
            enzyme_for_plot_dropdown
        ]
    )

    example_btn.click(
        fn=load_examples_and_auto_process,
        inputs=[], 
        outputs=[
            status_message_txt, unique_enzymes_p1_txt, unique_enzymes_p2_txt,
            common_diff_enzymes_txt, # New output
            plasmid1_data_state, plasmid2_data_state,
            p1_unique_enzymes_list_state, p2_unique_enzymes_list_state,
            enzyme_for_plot_dropdown, 
            plasmid_to_plot_choice_radio, 
            gel_plot_output 
        ]
    )

    plasmid_to_plot_choice_radio.change(
        fn=update_enzyme_dropdown_choices_on_radio_change,
        inputs=[plasmid_to_plot_choice_radio, p1_unique_enzymes_list_state, p2_unique_enzymes_list_state],
        outputs=[enzyme_for_plot_dropdown]
    )

    plot_btn.click(
        fn=plot_selected_digest_controller,
        inputs=[plasmid_to_plot_choice_radio, enzyme_for_plot_dropdown, plasmid1_data_state, plasmid2_data_state],
        outputs=[gel_plot_output]
    )

if __name__ == '__main__':
    if not os.path.exists(EXAMPLE_DIR):
        os.makedirs(EXAMPLE_DIR)
        print(f"Created directory: {EXAMPLE_DIR}. Please add example plasmid files (plasmid1_example.gb, plasmid2_example.gb) to it for the example button to work.")
    
    # Check for example files and print a message if they are missing
    if not os.path.exists(EXAMPLE_PLASMID1_PATH) or not os.path.exists(EXAMPLE_PLASMID2_PATH):
        print(f"Warning: Example files (plasmid1_example.gb, plasmid2_example.gb) not found in '{EXAMPLE_DIR}'. The 'Load Example Files' button might not work as expected.")
        print("You can create dummy GenBank or FASTA files with these names for testing if needed.")

    demo.launch()