Add very large file support ( so it doesn't lag if nothing in the filter is on ( Displaying 8.4M+ strings might be a little strain on the CPU/GPU idk )
Fix settings not saving or doing anything in that matter.
Massively improve the UX/UI in a way that isnt emojis but more string info is displayed whilst also using the most efficient string dumping methods.

use eframe::egui;
use std::fs::File;
use std::io::{self, Read, Write};
use std::path::PathBuf;

// Additional imports for threading and serialization
use crossbeam_channel::{unbounded, Receiver, Sender};
use serde::{Deserialize, Serialize};

struct StringDumperApp {
    // Data related to the application
    strings: Vec<ExtractedString>,
    file_path: Option<PathBuf>,
    search_text: String,
    filtered_strings: Vec<ExtractedString>,

    // Settings
    min_length: usize,
    include_ascii: bool,
    include_utf16le: bool,
    include_utf16be: bool,
    settings_open: bool,

    // For asynchronous loading
    is_loading: bool,
    progress: f32,
    status_message: String,
    update_channel: Receiver<AppMessage>,
    sender_channel: Sender<AppMessage>,

    // For settings persistence
    settings: Settings,
}

#[derive(Clone)]
struct ExtractedString {
    value: String,
    offset: usize,
    encoding: String,
    selected: bool,
}

#[derive(Serialize, Deserialize)]
struct Settings {
    min_length: usize,
    include_ascii: bool,
    include_utf16le: bool,
    include_utf16be: bool,
}

enum AppMessage {
    StringsLoaded(io::Result<Vec<ExtractedString>>),
}

impl Default for StringDumperApp {
    fn default() -> Self {
        let (sender_channel, update_channel) = unbounded();

        let mut app = Self {
            strings: Vec::new(),
            file_path: None,
            search_text: String::new(),
            filtered_strings: Vec::new(),
            min_length: 4,
            include_ascii: true,
            include_utf16le: true,
            include_utf16be: true,
            settings_open: false,
            is_loading: false,
            progress: 0.0,
            status_message: String::new(),
            update_channel,
            sender_channel,
            settings: Settings {
                min_length: 4,
                include_ascii: true,
                include_utf16le: true,
                include_utf16be: true,
            },
        };

        // Load settings from file
        app.load_settings();

        app
    }
}

impl eframe::App for StringDumperApp {
    fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
        self.ui_menu(ctx, frame);

        if self.settings_open {
            self.open_settings(ctx);
        }

        if self.is_loading {
            self.show_loading(ctx);
        }

        egui::SidePanel::left("side_panel").show(ctx, |ui| {
            self.ui_side_panel(ui);
        });

        egui::CentralPanel::default().show(ctx, |ui| {
            self.ui_central_panel(ui);
        });

        egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
            self.ui_status_bar(ui);
        });

        // Handle messages from background threads
        while let Ok(message) = self.update_channel.try_recv() {
            match message {
                AppMessage::StringsLoaded(result) => {
                    self.is_loading = false;
                    match result {
                        Ok(strings) => {
                            self.strings = strings;
                            self.filter_strings();
                            self.status_message = "Strings loaded successfully.".to_string();
                        }
                        Err(e) => {
                            self.status_message = format!("Error loading strings: {}", e);
                        }
                    }
                }
            }
        }
    }
}

impl StringDumperApp {
    fn ui_menu(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
        use egui::menu;

        egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
            menu::bar(ui, |ui| {
                ui.menu_button("File", |ui| {
                    if ui.button("📂 Open File...").clicked() {
                        self.open_file();
                        ui.close_menu();
                    }
                    if ui.button("💾 Save Strings...").clicked() {
                        self.save_strings_dialog();
                        ui.close_menu();
                    }
                });

                ui.menu_button("Edit", |ui| {
                    if ui.button("⚙ Settings").clicked() {
                        self.settings_open = true;
                        ui.close_menu();
                    }
                });

               
            });
        });
    }

    fn ui_side_panel(&mut self, ui: &mut egui::Ui) {
        ui.heading("Controls");
        ui.separator();
        if ui.button("📂 Open File...").clicked() {
            self.open_file();
        }
        if ui.button("💾 Save Strings...").clicked() {
            self.save_strings_dialog();
        }
        ui.separator();
        if ui.button("⚙ Settings").clicked() {
            self.settings_open = true;
        }
    }

    fn ui_central_panel(&mut self, ui: &mut egui::Ui) {
        ui.vertical(|ui| {
            ui.heading("Advanced String Dumper");
            ui.separator();

            if let Some(path) = &self.file_path {
                ui.horizontal_wrapped(|ui| {
                    ui.label("📄 File:");
                    ui.monospace(format!("{}", path.display()));
                });
            } else {
                ui.label("No file loaded.");
            }

            ui.horizontal(|ui| {
                ui.label("🔍 Search:");
                if ui
                    .add_sized(
                        [ui.available_width() * 0.5, 25.0],
                        egui::TextEdit::singleline(&mut self.search_text)
                            .hint_text("Type to search..."),
                    )
                    .changed()
                {
                    self.filter_strings();
                }
                if ui.button("❌ Clear").clicked() {
                    self.search_text.clear();
                    self.filter_strings();
                }
            });

            ui.separator();

            egui::ScrollArea::vertical().show(ui, |ui| {
                for s in &mut self.filtered_strings {
                    ui.horizontal(|ui| {
                        ui.checkbox(&mut s.selected, "");
                        ui.label(format!("0x{:08X}", s.offset))
                            .on_hover_text("Offset");
                        ui.label(&s.encoding).on_hover_text("Encoding");
                        ui.monospace(&s.value);
                    });
                    ui.separator();
                }
            });
        });
    }

    fn ui_status_bar(&mut self, ui: &mut egui::Ui) {
        ui.horizontal(|ui| {
            if self.is_loading {
                ui.add(egui::ProgressBar::new(self.progress).show_percentage());
                ui.label(&self.status_message);
            } else {
                ui.label(&self.status_message);
            }
        });
    }

    fn open_file(&mut self) {
        if let Some(path) = rfd::FileDialog::new().pick_file() {
            self.file_path = Some(path.clone());
            self.load_strings_async(path);
        }
    }

    fn save_strings_dialog(&mut self) {
        if let Some(path) = rfd::FileDialog::new().save_file() {
            self.save_strings(&path);
            self.status_message = "Strings saved successfully.".to_string();
        }
    }

    fn load_strings_async(&mut self, path: PathBuf) {
        let min_length = self.min_length;
        let include_ascii = self.include_ascii;
        let include_utf16le = self.include_utf16le;
        let include_utf16be = self.include_utf16be;
        let sender = self.sender_channel.clone();
        self.is_loading = true;
        self.progress = 0.0;
        self.status_message = "Loading strings...".to_string();

        std::thread::spawn(move || {
            let result = extract_strings_from_file(
                &path,
                min_length,
                include_ascii,
                include_utf16le,
                include_utf16be,
                sender.clone(),
            );
            sender
                .send(AppMessage::StringsLoaded(result))
                .expect("Failed to send message");
        });
    }

    fn filter_strings(&mut self) {
        if self.search_text.is_empty() {
            self.filtered_strings = self.strings.clone();
        } else {
            let search_lower = self.search_text.to_lowercase();
            self.filtered_strings = self
                .strings
                .iter()
                .filter(|s| s.value.to_lowercase().contains(&search_lower))
                .cloned()
                .collect();
        }
    }

    fn save_strings(&self, path: &std::path::Path) {
        if let Ok(mut file) = File::create(path) {
            for s in &self.filtered_strings {
                if s.selected {
                    if let Err(e) = writeln!(file, "{}", s.value) {
                        break;
                    }
                }
            }
        } else {
        }
    }

    fn open_settings(&mut self, ctx: &egui::Context) {
        // Clone the settings to local variables
        let mut min_length = self.min_length;
        let mut include_ascii = self.include_ascii;
        let mut include_utf16le = self.include_utf16le;
        let mut include_utf16be = self.include_utf16be;
        let mut apply_changes = false;
    
        egui::Window::new("Settings")
            .open(&mut self.settings_open)
            .show(ctx, |ui| {
                ui.label("Minimum String Length:");
                ui.add(egui::Slider::new(&mut min_length, 1..=20));
    
                ui.separator();
                ui.label("String Encodings to Include:");
                ui.checkbox(&mut include_ascii, "ASCII")
                    .on_hover_text("Include ASCII encoded strings.");
                ui.checkbox(&mut include_utf16le, "UTF-16 Little Endian")
                    .on_hover_text("Include UTF-16 LE strings.");
                ui.checkbox(&mut include_utf16be, "UTF-16 Big Endian")
                    .on_hover_text("Include UTF-16 BE strings.");
    
                ui.separator();
    
                if ui.button("Apply").clicked() {
                    apply_changes = true;
                }
                if ui.button("Cancel").clicked() {
                    // No changes applied
                }
            });
    
        if apply_changes {
            self.min_length = min_length;
            self.include_ascii = include_ascii;
            self.include_utf16le = include_utf16le;
            self.include_utf16be = include_utf16be;
    
            // Save settings to file
            self.settings.min_length = min_length;
            self.settings.include_ascii = include_ascii;
            self.settings.include_utf16le = include_utf16le;
            self.settings.include_utf16be = include_utf16be;
            self.save_settings();
    
            // Reload strings if a file is loaded
            if let Some(path) = &self.file_path {
                self.load_strings_async(path.clone());
            }
    
            self.settings_open = false;
        }
    }

    fn show_loading(&mut self, ctx: &egui::Context) {
        egui::Window::new("Loading")
            .anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
            .collapsible(false)
            .show(ctx, |ui| {
                ui.label("Please wait while the strings are being loaded...");
                ui.add(
                    egui::ProgressBar::new(self.progress)
                        .desired_width(300.0)
                        .show_percentage(),
                );
            });
    }

    fn save_settings(&self) {
        if let Ok(json) = serde_json::to_string(&self.settings) {
            std::fs::write("settings.json", json).ok();
        }
    }

    fn load_settings(&mut self) {
        if let Ok(json) = std::fs::read_to_string("settings.json") {
            if let Ok(settings) = serde_json::from_str::<Settings>(&json) {
                self.min_length = settings.min_length;
                self.include_ascii = settings.include_ascii;
                self.include_utf16le = settings.include_utf16le;
                self.include_utf16be = settings.include_utf16be;
                self.settings = settings;
            }
        }
    }
}

fn extract_strings_from_file(
    path: &std::path::Path,
    min_length: usize,
    include_ascii: bool,
    include_utf16le: bool,
    include_utf16be: bool,
    sender: Sender<AppMessage>,
) -> io::Result<Vec<ExtractedString>> {
    let mut file = File::open(path)?;
    let metadata = file.metadata()?;
    let file_size = metadata.len();
    let mut buffer = Vec::new();
    file.read_to_end(&mut buffer)?;

    let strings = extract_strings(
        &buffer,
        min_length,
        include_ascii,
        include_utf16le,
        include_utf16be,
        file_size,
        sender,
    );

    Ok(strings)
}

fn extract_strings(
    data: &[u8],
    min_length: usize,
    include_ascii: bool,
    include_utf16le: bool,
    include_utf16be: bool,
    file_size: u64,
    sender: Sender<AppMessage>,
) -> Vec<ExtractedString> {
    let mut strings = Vec::new();
    let mut processed: u64 = 0;

    if include_ascii {
        strings.extend(extract_ascii_strings(
            data,
            min_length,
            &mut processed,
            file_size,
            &sender,
        ));
    }

    if include_utf16le {
        strings.extend(extract_unicode_strings(
            data,
            min_length,
            true,
            &mut processed,
            file_size,
            &sender,
        ));
    }

    if include_utf16be {
        strings.extend(extract_unicode_strings(
            data,
            min_length,
            false,
            &mut processed,
            file_size,
            &sender,
        ));
    }

    strings
}

fn extract_ascii_strings(
    data: &[u8],
    min_length: usize,
    processed: &mut u64,
    file_size: u64,
    sender: &Sender<AppMessage>,
) -> Vec<ExtractedString> {
    let mut strings = Vec::new();
    let mut current_string = Vec::new();
    let mut offset = 0;

    for &byte in data {
        if (byte.is_ascii_graphic() || byte == b' ') {
            if current_string.is_empty() {
                offset = *processed as usize;
            }
            current_string.push(byte);
        } else {
            if current_string.len() >= min_length {
                if let Ok(s) = String::from_utf8(current_string.clone()) {
                    strings.push(ExtractedString {
                        value: s,
                        offset,
                        encoding: "ASCII".to_string(),
                        selected: false,
                    });
                }
            }
            current_string.clear();
        }
        *processed += 1;
        if *processed % 10000 == 0 {
            let progress = *processed as f32 / file_size as f32;
            sender
                .send(AppMessage::StringsLoaded(Ok(Vec::new())))
                .ok();
        }
    }

    // Check if there's a string at the end
    if current_string.len() >= min_length {
        if let Ok(s) = String::from_utf8(current_string.clone()) {
            strings.push(ExtractedString {
                value: s,
                offset,
                encoding: "ASCII".to_string(),
                selected: false,
            });
        }
    }

    strings
}
fn extract_unicode_strings(
    data: &[u8],
    min_length: usize,
    little_endian: bool,
    processed: &mut u64,
    file_size: u64,
    sender: &Sender<AppMessage>,
) -> Vec<ExtractedString> {
    let mut strings = Vec::new();
    let mut current_string = Vec::new();
    let mut offset = 0;
    let mut i = 0;

    while i + 1 < data.len() {
        let b1 = data[i];
        let b2 = data[i + 1];

        let code_unit = if little_endian {
            u16::from_le_bytes([b1, b2])
        } else {
            u16::from_be_bytes([b1, b2])
        };

        if code_unit == 0 {
            if current_string.len() >= min_length {
                let s = String::from_utf16_lossy(&current_string);
                strings.push(ExtractedString {
                    value: s,
                    offset,
                    encoding: if little_endian {
                        "UTF-16 LE".to_string()
                    } else {
                        "UTF-16 BE".to_string()
                    },
                    selected: false,
                });
            }
            current_string.clear();
        } else if (code_unit >= 0x21 && code_unit <= 0x7E) || code_unit == 32 {
            if current_string.is_empty() {
                offset = i;
            }
            current_string.push(code_unit);
        } else {
            if current_string.len() >= min_length {
                let s = String::from_utf16_lossy(&current_string);
                strings.push(ExtractedString {
                    value: s,
                    offset,
                    encoding: if little_endian {
                        "UTF-16 LE".to_string()
                    } else {
                        "UTF-16 BE".to_string()
                    },
                    selected: false,
                });
            }
            current_string.clear();
        }

        i += 2;
        *processed += 2;
        if *processed % 10000 == 0 {
            let progress = *processed as f32 / file_size as f32;
            sender
                .send(AppMessage::StringsLoaded(Ok(Vec::new())))
                .ok();
        }
    }

    // Check if there's a string at the end
    if current_string.len() >= min_length {
        let s = String::from_utf16_lossy(&current_string);
        strings.push(ExtractedString {
            value: s,
            offset,
            encoding: if little_endian {
                "UTF-16 LE".to_string()
            } else {
                "UTF-16 BE".to_string()
            },
            selected: false,
        });
    }

    strings
}

fn main() {
    let app = StringDumperApp::default();
    let native_options = eframe::NativeOptions::default();
    eframe::run_native("Advanced String Dumper", native_options, Box::new(|_cc| {
        Ok(Box::new(app))
    }))
    .unwrap();
}