use std::fs;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::env;

#[derive(serde::Serialize, Clone)]
struct ImageItem {
    path: String,
    name: String,
    thumbnail: String,
}

fn get_cache_dir() -> std::path::PathBuf {
    let mut path = env::temp_dir();
    path.push("watermark-wizard-thumbs");
    if !path.exists() {
        let _ = fs::create_dir_all(&path);
    }
    path
}

fn generate_thumbnail(original_path: &Path) -> Option<String> {
    let cache_dir = get_cache_dir();
    
    // Generate simple hash for filename
    let mut hasher = DefaultHasher::new();
    original_path.hash(&mut hasher);
    let hash = hasher.finish();
    let file_name = format!("{}.jpg", hash);
    let thumb_path = cache_dir.join(file_name);

    // Return if exists
    if thumb_path.exists() {
        return Some(thumb_path.to_string_lossy().to_string());
    }

    // Generate
    if let Ok(img) = image::open(original_path) {
        let thumb = img.thumbnail(u32::MAX, 200);
        let _file = fs::File::create(&thumb_path).ok()?;
        thumb.save_with_format(&thumb_path, image::ImageFormat::Jpeg).ok()?;
        
        return Some(thumb_path.to_string_lossy().to_string());
    }
    
    None
}

#[tauri::command]
async fn scan_dir(path: String) -> Result<Vec<ImageItem>, String> {
    let entries = fs::read_dir(&path).map_err(|e| e.to_string())?;
    
    // Collect valid paths first to avoid holding fs locks or iterators during parallel proc
    let mut valid_paths = Vec::new();
    for entry in entries {
        if let Ok(entry) = entry {
            let p = entry.path();
            if p.is_file() {
                if let Some(ext) = p.extension() {
                    let ext_str = ext.to_string_lossy().to_lowercase();
                    if ["png", "jpg", "jpeg", "webp"].contains(&ext_str.as_str()) {
                        valid_paths.push(p);
                    }
                }
            }
        }
    }

    // Process in parallel
    let mut images: Vec<ImageItem> = valid_paths.par_iter().filter_map(|path| {
        let name = path.file_name()?.to_string_lossy().to_string();
        let path_str = path.to_string_lossy().to_string();
        
        // Generate thumbnail
        let thumb = generate_thumbnail(path).unwrap_or_else(|| path_str.clone());

        Some(ImageItem {
            path: path_str,
            name,
            thumbnail: thumb,
        })
    }).collect();

    // Sort by name
    images.sort_by(|a, b| a.name.cmp(&b.name));
    Ok(images)
}

use image::GenericImageView;
use image::Pixel;
use rayon::prelude::*;
use std::path::Path;
use imageproc::drawing::draw_text_mut;
use ab_glyph::{FontRef, PxScale};
use tauri::{AppHandle, Manager};

mod lama;

// Embed the font to ensure it's always available without path issues
const FONT_DATA: &[u8] = include_bytes!("../assets/fonts/Roboto-Regular.ttf");

#[derive(serde::Serialize)]
struct ZcaResult {
    x: f64,
    y: f64,
    zone: String, 
}

#[derive(serde::Deserialize)]
struct ExportImageTask {
    path: String,
    manual_position: Option<ManualPosition>,
    scale: Option<f64>,
    opacity: Option<f64>,
    color: Option<String>,
}

#[derive(serde::Deserialize)]
struct WatermarkSettings {
    #[serde(rename = "type")]
    _w_type: String, // 'text' (image is deprecated for now per user request, but keeping struct flexible)
    text: String, // Was 'source'
    color: String, // Hex code e.g. "#FFFFFF"
    opacity: f64,
    scale: f64, // Font size relative to image height (e.g., 0.05 = 5% of height)
    // Global manual override is deprecated in favor of per-task control, but kept for struct compatibility if needed
    _manual_override: bool, 
    _manual_position: ManualPosition,
}

#[derive(serde::Deserialize)]
struct ManualPosition {
    x: f64,
    y: f64,
}

fn parse_hex_color(hex: &str) -> image::Rgba<u8> {
    let hex = hex.trim_start_matches('#');
    let r = u8::from_str_radix(&hex[0..2], 16).unwrap_or(255);
    let g = u8::from_str_radix(&hex[2..4], 16).unwrap_or(255);
    let b = u8::from_str_radix(&hex[4..6], 16).unwrap_or(255);
    image::Rgba([r, g, b, 255])
}

#[tauri::command]
async fn export_batch(images: Vec<ExportImageTask>, watermark: WatermarkSettings, output_dir: String) -> Result<String, String> {
    let font = FontRef::try_from_slice(FONT_DATA).map_err(|e| format!("Font error: {}", e))?;
    
    // Note: Settings are now resolved per-task

    let results: Vec<Result<(), String>> = images.par_iter().map(|task| {
        let input_path = Path::new(&task.path);
        let img_result = image::open(input_path);
        
        if let Ok(dynamic_img) = img_result {
            let mut base_img = dynamic_img.to_rgba8();
            let (width, height) = base_img.dimensions();

            // Determine effective settings (Task > Global)
            let eff_scale = task.scale.unwrap_or(watermark.scale);
            let eff_opacity = task.opacity.unwrap_or(watermark.opacity);
            let eff_color_hex = task.color.as_ref().unwrap_or(&watermark.color);

            // Calculate final color
            let base_color = parse_hex_color(eff_color_hex);
            let alpha = (eff_opacity * 255.0) as u8;
            let text_color = image::Rgba([base_color[0], base_color[1], base_color[2], alpha]);

            // 1. Calculate Font Scale based on Image Height
            let mut scale_px = height as f32 * eff_scale as f32;
            
            // 2. Measure Text
            let scaled_font = PxScale::from(scale_px);
            let (t_width, _t_height) = imageproc::drawing::text_size(scaled_font, &font, &watermark.text);
            
            // 3. Ensure it fits width (Padding 15%)
            let max_width = (width as f32 * 0.85) as u32;
            if t_width > max_width {
                 let ratio = max_width as f32 / t_width as f32;
                 scale_px *= ratio;
            }
            let final_scale = PxScale::from(scale_px);
            let (final_t_width, final_t_height) = imageproc::drawing::text_size(final_scale, &font, &watermark.text);

            // 4. Determine Position (Task Specific > ZCA)
            // If task has manual_position, use it. Otherwise calculate ZCA.
            let (pos_x_pct, pos_y_pct) = if let Some(pos) = &task.manual_position {
                 (pos.x, pos.y)
            } else {
                 match calculate_zca_internal(&dynamic_img) {
                     Ok(res) => (res.x, res.y),
                     Err(_) => (0.5, 0.97),
                 }
            };

            // Calculate initial top-left based on center
            let center_x = width as f64 * pos_x_pct;
            let center_y = height as f64 * pos_y_pct;

            let mut x = (center_x - (final_t_width as f64 / 2.0)) as i32;
            let mut y = (center_y - (final_t_height as f64 / 2.0)) as i32;
            
            // 5. Strict Boundary Clamping
            // We ensure the text box (final_t_width, final_t_height) is always inside (0, 0, width, height)
            let min_padding = 2; // Absolute minimum pixels from edge
            
            if x < min_padding { x = min_padding; }
            if y < min_padding { y = min_padding; }
            if x + final_t_width as i32 > width as i32 - min_padding {
                x = width as i32 - final_t_width as i32 - min_padding;
            }
            if y + final_t_height as i32 > height as i32 - min_padding {
                y = height as i32 - final_t_height as i32 - min_padding;
            }
            
            // Re-clamp just in case of very small images where text is larger than image
            x = x.max(0);
            y = y.max(0);
            
            // 6. Draw Stroke (Simple 4-direction offset for black outline)
            // Stroke alpha should match text alpha
            let stroke_color = image::Rgba([0, 0, 0, text_color[3]]);
            for offset in [(-1, -1), (-1, 1), (1, -1), (1, 1)] {
                draw_text_mut(
                    &mut base_img,
                    stroke_color,
                    x + offset.0,
                    y + offset.1,
                    final_scale,
                    &font,
                    &watermark.text,
                );
            }

            // 7. Draw Main Text
            draw_text_mut(
                &mut base_img,
                text_color,
                x,
                y,
                final_scale,
                &font,
                &watermark.text,
            );

            // Save
            let file_name = input_path.file_name().unwrap_or_default();
            let output_path = Path::new(&output_dir).join(file_name);
            
            // Handle format specific saving
            // JPEG does not support Alpha channel. If we save Rgba8 to Jpeg, it might fail or look wrong.
            let ext = output_path.extension().and_then(|s| s.to_str()).unwrap_or("").to_lowercase();
            if ext == "jpg" || ext == "jpeg" {
                // Convert to RGB8 (dropping alpha)
                // Note: This simply drops alpha. If background was transparent, it becomes black.
                // For photos (JPEGs) this is usually fine as they don't have alpha.
                let rgb_img = image::DynamicImage::ImageRgba8(base_img).to_rgb8();
                rgb_img.save(&output_path).map_err(|e| e.to_string())?;
            } else {
                // For PNG/WebP etc, keep RGBA
                base_img.save(&output_path).map_err(|e| e.to_string())?;
            }
            
            Ok(())
        } else {
            Err(format!("Failed to open {}", task.path))
        }
    }).collect();

    let failures: Vec<String> = results.into_iter().filter_map(|r| r.err()).collect();
    
    if failures.is_empty() {
        Ok("All images processed successfully".to_string())
    } else {
        Err(format!("Completed with errors: {:?}", failures))
    }
}

// Helper to reuse logic (adapted from command)
fn calculate_zca_internal(img: &image::DynamicImage) -> Result<ZcaResult, String> {
    let (width, height) = img.dimensions();
    
    // Greedy Layered Search
    // Priority: Bottom -> Up
    let y_levels = [0.97, 0.94, 0.91, 0.88];
    let x_cols = [1.0/6.0, 3.0/6.0, 5.0/6.0]; // Left, Center, Right centers
    let col_names = ["Left", "Center", "Right"];

    // Box Size for analysis (approx watermark size)
    let box_w = (width as f64 * 0.30) as u32; 
    let box_h = (height as f64 * 0.05) as u32; 
    let half_box_w = box_w / 2;
    let half_box_h = box_h / 2;

    let mut global_best_score = f64::MAX;
    let mut global_best_result = ZcaResult { x: 0.5, y: 0.97, zone: "Center".to_string() };

    for &y_pct in y_levels.iter() {
        let mut row_best_score = f64::MAX;
        let mut row_best_idx = 1; // Default Center
        let mut row_stats = Vec::new(); // (mean, std_dev)

        for (col_idx, &x_pct) in x_cols.iter().enumerate() {
            let cx = (width as f64 * x_pct) as u32;
            let cy = (height as f64 * y_pct) as u32;
            
            let start_x = if cx > half_box_w { cx - half_box_w } else { 0 };
            let start_y = if cy > half_box_h { cy - half_box_h } else { 0 };
            let end_x = (start_x + box_w).min(width);
            let end_y = (start_y + box_h).min(height);
            
            let mut luma_values = Vec::with_capacity((box_w * box_h) as usize);
            
            for y in start_y..end_y {
                for x in start_x..end_x {
                     let pixel = img.get_pixel(x, y);
                     let rgb = pixel.to_rgb();
                     let luma = 0.299 * rgb[0] as f64 + 0.587 * rgb[1] as f64 + 0.114 * rgb[2] as f64;
                     luma_values.push(luma);
                }
            }
            
            let count = luma_values.len() as f64;
            if count == 0.0 { 
                row_stats.push((0.0, f64::MAX));
                continue; 
            }

            let mean = luma_values.iter().sum::<f64>() / count;
            let variance = luma_values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / count;
            let std_dev = variance.sqrt();
            
            row_stats.push((mean, std_dev));

            // For choosing "Best in Row", we strictly prefer Flatness (StdDev)
            if std_dev < row_best_score {
                row_best_score = std_dev;
                row_best_idx = col_idx;
            }

            // Update Global Best (fallback)
            if std_dev < global_best_score {
                global_best_score = std_dev;
                global_best_result = ZcaResult {
                    x: x_pct,
                    y: y_pct,
                    zone: col_names[col_idx].to_string(),
                };
            }
        }

        // Analyze the Best Zone in this Row
        let (mean, std_dev) = row_stats[row_best_idx];

        // Safety Check: Is this zone "White Text"?
        // Condition: Mean > 180 (Bright-ish) AND StdDev > 20 (Busy/Text)
        let is_unsafe_white_text = mean > 180.0 && std_dev > 20.0;
        let is_unsafe_bright = mean > 230.0;

        if !is_unsafe_white_text && !is_unsafe_bright {
            // Safe!
            return Ok(ZcaResult {
                x: x_cols[row_best_idx],
                y: y_pct,
                zone: col_names[row_best_idx].to_string(),
            });
        }
    }

    Ok(global_best_result)
}

#[tauri::command]
fn get_zca_suggestion(path: String) -> Result<ZcaResult, String> {
    let img = image::open(&path).map_err(|e| e.to_string())?;
    calculate_zca_internal(&img)
}

#[derive(serde::Serialize)]
struct LayoutResult {
    x: f64,
    y: f64,
    scale: f64,
}

#[tauri::command]
async fn layout_watermark(path: String, text: String, base_scale: f64) -> Result<LayoutResult, String> {
    let img = image::open(&path).map_err(|e| e.to_string())?;
    let (width, height) = img.dimensions();
    let font = FontRef::try_from_slice(FONT_DATA).map_err(|e| format!("Font error: {}", e))?;

    // 1. Run ZCA to find best zone center (now with dark preference)
    let zca = calculate_zca_internal(&img)?;
    
    // 2. Calculate Text Dimensions at Base Scale
    let mut scale_val = base_scale;
    let mut scale_px = height as f32 * scale_val as f32;
    let mut font_scale = PxScale::from(scale_px);
    let (mut t_width, mut t_height) = imageproc::drawing::text_size(font_scale, &font, &text);

    // 3. Auto-Fit Width (Limit to 85% of image width)
    let max_width = (width as f32 * 0.85) as u32;
    if t_width > max_width {
        let ratio = max_width as f32 / t_width as f32;
        scale_val *= ratio as f64;
        scale_px *= ratio;
        font_scale = PxScale::from(scale_px);
        let dims = imageproc::drawing::text_size(font_scale, &font, &text);
        t_width = dims.0;
        t_height = dims.1;
    }

    // 4. Smart Clamping
    let center_x = zca.x * width as f64;
    let center_y = zca.y * height as f64;

    // Add safety margin to measured text size (Renderer mismatch buffer)
    let safe_t_width = t_width as f64 * 1.05;
    let safe_t_height = t_height as f64 * 1.05;

    let half_w = safe_t_width / 2.0;
    let half_h = safe_t_height / 2.0;
    
    // Increase edge padding to 4%
    let padding = width as f64 * 0.04;

    let min_x = half_w + padding;
    let max_x = width as f64 - half_w - padding;
    let final_x = center_x.clamp(min_x, max_x);

    let min_y = half_h + padding;
    let max_y = height as f64 - half_h - padding;
    let final_y = center_y.clamp(min_y, max_y);

    Ok(LayoutResult {
        x: final_x / width as f64,
        y: final_y / height as f64,
        scale: scale_val,
    })
}

#[derive(serde::Serialize)]
struct DetectionResult {
    rects: Vec<Rect>,
}

#[derive(serde::Serialize, Clone)]
struct Rect {
    x: f64,
    y: f64,
    width: f64,
    height: f64,
}

#[tauri::command]
async fn detect_watermark(path: String) -> Result<DetectionResult, String> {
    let img = image::open(&path).map_err(|e| e.to_string())?;
    let (width, height) = img.dimensions();
    let gray = img.to_luma8();
    
    // "Stroke Detection" Algorithm
    // Distinguishes "Text" (Thin White Strokes) from "Solid White Areas" (Walls, Sky)
    // Logic: A white text pixel must be "sandwiched" by dark pixels within a short distance.
    
    let cell_size = 10;
    let grid_w = (width + cell_size - 1) / cell_size;
    let grid_h = (height + cell_size - 1) / cell_size;
    let mut grid = vec![false; (grid_w * grid_h) as usize];

    // Focus Areas: Top 15%, Bottom 25%
    let top_limit = (height as f64 * 0.15) as u32;
    let bottom_start = (height as f64 * 0.75) as u32;
    
    let max_stroke_width = 15; // Max pixels for a text stroke thickness
    let contrast_threshold = 40; // How much darker the background must be
    let brightness_threshold = 200; // Text must be at least this white

    for y in 1..height-1 {
        if y > top_limit && y < bottom_start { continue; }

        for x in 1..width-1 {
            let p = gray.get_pixel(x, y)[0];
            
            // 1. Must be Bright
            if p < brightness_threshold { continue; }

            // 2. Stroke Check
            // We check for "Vertical Stroke" (Dark - Bright - Dark vertically)
            // OR "Horizontal Stroke" (Dark - Bright - Dark horizontally)
            
            let mut is_stroke = false;

            // Check Horizontal Stroke (Vertical boundaries? No, Vertical Stroke has Left/Right boundaries? 
            // Terminology: "Vertical Stroke" is like 'I'. It has Left/Right boundaries.
            // "Horizontal Stroke" is like '-', It has Up/Down boundaries.
            
            // Let's check Left/Right boundaries (Vertical Stroke)
            let mut left_bound = false;
            let mut right_bound = false;
            
            // Search Left
            for k in 1..=max_stroke_width {
                if x < k { break; }
                let neighbor = gray.get_pixel(x - k, y)[0];
                if p > neighbor && (p - neighbor) > contrast_threshold {
                    left_bound = true;
                    break;
                }
            }
            // Search Right
            if left_bound {
                for k in 1..=max_stroke_width {
                    if x + k >= width { break; }
                    let neighbor = gray.get_pixel(x + k, y)[0];
                    if p > neighbor && (p - neighbor) > contrast_threshold {
                        right_bound = true;
                        break;
                    }
                }
            }
            
            if left_bound && right_bound {
                is_stroke = true;
            } else {
                // Check Up/Down boundaries (Horizontal Stroke)
                let mut up_bound = false;
                let mut down_bound = false;
                
                // Search Up
                for k in 1..=max_stroke_width {
                    if y < k { break; }
                    let neighbor = gray.get_pixel(x, y - k)[0];
                    if p > neighbor && (p - neighbor) > contrast_threshold {
                        up_bound = true;
                        break;
                    }
                }
                // Search Down
                if up_bound {
                    for k in 1..=max_stroke_width {
                        if y + k >= height { break; }
                        let neighbor = gray.get_pixel(x, y + k)[0];
                        if p > neighbor && (p - neighbor) > contrast_threshold {
                            down_bound = true;
                            break;
                        }
                    }
                }

                if up_bound && down_bound {
                    is_stroke = true;
                }
            }

            if is_stroke {
                let gx = x / cell_size;
                let gy = y / cell_size;
                grid[(gy * grid_w + gx) as usize] = true;
            }
        }
    }

    // Connected Components on Grid (Simple merging)
    let mut rects = Vec::new();
    let mut visited = vec![false; grid.len()];

    for gy in 0..grid_h {
        for gx in 0..grid_w {
            let idx = (gy * grid_w + gx) as usize;
            if grid[idx] && !visited[idx] {
                // Start a new component
                let mut min_gx = gx;
                let mut max_gx = gx;
                let mut min_gy = gy;
                let mut max_gy = gy;
                
                let mut stack = vec![(gx, gy)];
                visited[idx] = true;

                while let Some((cx, cy)) = stack.pop() {
                    if cx < min_gx { min_gx = cx; }
                    if cx > max_gx { max_gx = cx; }
                    if cy < min_gy { min_gy = cy; }
                    if cy > max_gy { max_gy = cy; }

                    // Neighbors
                    let neighbors = [
                        (cx.wrapping_sub(1), cy), (cx + 1, cy),
                        (cx, cy.wrapping_sub(1)), (cx, cy + 1)
                    ];

                    for (nx, ny) in neighbors {
                        if nx < grid_w && ny < grid_h {
                            let nidx = (ny * grid_w + nx) as usize;
                            if grid[nidx] && !visited[nidx] {
                                visited[nidx] = true;
                                stack.push((nx, ny));
                            }
                        }
                    }
                }

                // Convert grid rect to normalized image rect
                // Add padding (1 cell)
                let px = (min_gx * cell_size) as f64;
                let py = (min_gy * cell_size) as f64;
                let pw = ((max_gx - min_gx + 1) * cell_size) as f64;
                let ph = ((max_gy - min_gy + 1) * cell_size) as f64;

                rects.push(Rect {
                    x: px / width as f64,
                    y: py / height as f64,
                    width: pw / width as f64,
                    height: ph / height as f64,
                });
            }
        }
    }

    Ok(DetectionResult { rects })
}

#[derive(serde::Deserialize)]
struct StrokePoint {
    x: f64,
    y: f64,
}

#[derive(serde::Deserialize)]
struct StrokeRect {
    x: f64,
    y: f64,
    w: f64,
    h: f64,
}

#[derive(serde::Deserialize)]
#[serde(tag = "type")]
enum MaskStroke {
    #[serde(rename = "path")]
    Path {
        points: Vec<StrokePoint>,
        width: f64,
    },
    #[serde(rename = "rect")]
    Rect {
        rect: StrokeRect,
    },
}

#[tauri::command]
async fn run_inpainting(app: AppHandle, path: String, strokes: Vec<MaskStroke>) -> Result<String, String> {
    let img = image::open(&path).map_err(|e| e.to_string())?.to_rgba8();
    let (width, height) = img.dimensions();
    
    // 1. Create Gray Mask (0 = keep, 255 = remove)
    let mut mask = image::GrayImage::new(width, height);
    
    for stroke in strokes {
        match stroke {
            MaskStroke::Rect { rect } => {
                let x1 = (rect.x * width as f64) as i32;
                let y1 = (rect.y * height as f64) as i32;
                let w = (rect.w * width as f64) as i32;
                let h = (rect.h * height as f64) as i32;
                
                // Draw 255 on mask
                for y in y1..(y1 + h) {
                    for x in x1..(x1 + w) {
                        if x >= 0 && x < width as i32 && y >= 0 && y < height as i32 {
                            mask.put_pixel(x as u32, y as u32, image::Luma([255]));
                        }
                    }
                }
            },
            MaskStroke::Path { points, width: stroke_w_pct } => {
                if points.is_empty() { continue; }
                let r = (stroke_w_pct * width as f64 / 2.0).ceil() as i32;
                let r2 = r * r;

                for i in 0..points.len() - 1 {
                    let p1 = &points[i];
                    let p2 = &points[i+1];
                    
                    let x1 = p1.x * width as f64;
                    let y1 = p1.y * height as f64;
                    let x2 = p2.x * width as f64;
                    let y2 = p2.y * height as f64;
                    
                    // Simple line interpolation
                    let dist = ((x2-x1).powi(2) + (y2-y1).powi(2)).sqrt();
                    let steps = dist.max(1.0) as i32;
                    
                    for s in 0..=steps {
                        let t = s as f64 / steps as f64;
                        let cx = (x1 + (x2 - x1) * t) as i32;
                        let cy = (y1 + (y2 - y1) * t) as i32;
                        
                        // Draw circle at cx, cy
                        for dy in -r..=r {
                            for dx in -r..=r {
                                if dx*dx + dy*dy <= r2 {
                                    let nx = cx + dx;
                                    let ny = cy + dy;
                                    if nx >= 0 && nx < width as i32 && ny >= 0 && ny < height as i32 {
                                        mask.put_pixel(nx as u32, ny as u32, image::Luma([255]));
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    // 2. Resolve Model Path
    let model_path = app.path().resource_dir()
        .map_err(|e| e.to_string())?
        .join("resources")
        .join("lama_fp32.onnx");

    if !model_path.exists() {
        return Err("Model file 'lama_fp32.onnx' not found in resources.".to_string());
    }

    // 3. Run Inference
    // This is computationally heavy, maybe run in thread? Tauri async commands are already threaded.
    let result_img = lama::run_lama_inpainting(&model_path, &img, &mask)?;
    
    // Save to temp
    let cache_dir = get_cache_dir();
    let file_name = format!("inpainted_{}.png", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis());
    let out_path = cache_dir.join(file_name);
    
    result_img.save(&out_path).map_err(|e| e.to_string())?;
    
    Ok(out_path.to_string_lossy().to_string())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![scan_dir, get_zca_suggestion, export_batch, detect_watermark, layout_watermark, run_inpainting])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
