Documentation
mod commands;
mod error;

use clap::Parser;
use regex::Regex;
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use commands::{create_glyph, install, play_glyph, uninstall};
use commands::{Cli, Commands};
pub use error::{CliError, Result};

fn extract_frame_number(filename: &str) -> Option<u32> {
    // Try to find a number sequence in the filename
    let re = Regex::new(r"(\d+)").unwrap();
    re.find_iter(filename)
        .last() // Take the last number sequence (usually the frame number)
        .and_then(|m| m.as_str().parse::<u32>().ok())
}

fn resolve_path(path: &Path) -> Result<PathBuf> {
    if path.is_absolute() {
        Ok(path.to_path_buf())
    } else {
        Ok(env::current_dir()?.join(path))
    }
}

fn find_sequence_frames(first_frame: &Path) -> Result<Vec<PathBuf>> {
    let first_frame = resolve_path(first_frame)?;

    // Check if path exists before proceeding
    if !first_frame.exists() {
        return Err(CliError::File(format!(
            "Path does not exist: {}",
            first_frame.display()
        )));
    }

    // If it's a directory, look for frame sequences inside
    if first_frame.is_dir() {
        let dir_entries = fs::read_dir(&first_frame)?;
        let mut frames = Vec::new();

        // Collect all potential frame files
        for entry in dir_entries {
            let entry = entry?;
            if entry.file_type()?.is_file() {
                frames.push(entry.path());
            }
        }

        if frames.is_empty() {
            return Err(CliError::File(format!(
                "No files found in directory {}",
                first_frame.display()
            )));
        }

        // Sort frames by name
        frames.sort();
        Ok(frames)
    } else {
        // If it's a file, try to find other files in the same directory with similar names
        let parent = first_frame
            .parent()
            .ok_or_else(|| CliError::File("Invalid path: no parent directory".to_string()))?;

        // Check if parent directory exists
        if !parent.exists() {
            return Err(CliError::File(format!(
                "Parent directory does not exist: {}",
                parent.display()
            )));
        }

        let first_frame_name = first_frame
            .file_name()
            .ok_or_else(|| CliError::File("Invalid filename".to_string()))?
            .to_string_lossy();

        // Get the base pattern by removing the number sequence
        let base_pattern = first_frame_name
            .split_once(char::is_numeric)
            .map(|(prefix, _)| prefix)
            .unwrap_or(&first_frame_name);

        let extension = first_frame
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or("");

        let mut frames = Vec::new();
        for entry in fs::read_dir(parent)? {
            let entry = entry?;
            let path = entry.path();

            if path.is_file() {
                if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
                    // Check if the file has the same extension and starts with the base pattern
                    if name.starts_with(base_pattern)
                        && path.extension().and_then(|ext| ext.to_str()) == Some(extension)
                    {
                        frames.push(path);
                    }
                }
            }
        }

        if frames.is_empty() {
            // If no sequence found and the file doesn't exist, return error
            if !first_frame.exists() {
                return Err(CliError::File(format!(
                    "File does not exist: {}",
                    first_frame.display()
                )));
            }
            // If file exists but no sequence found, just use the single frame
            Ok(vec![first_frame])
        } else {
            // Sort frames by their frame number
            frames.sort_by_key(|path| {
                path.file_name()
                    .and_then(|name| name.to_str())
                    .and_then(extract_frame_number)
                    .unwrap_or(0)
            });
            Ok(frames)
        }
    }
}

pub fn main() -> Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Some(Commands::Create {
            input,
            output,
            duration,
        }) => {
            let input_files = find_sequence_frames(&input).map_err(|e| {
                CliError::File(format!(
                    "Failed to find frame sequence in {}: {}",
                    input.display(),
                    e
                ))
            })?;

            if input_files.is_empty() {
                return Err(CliError::Input("No frame files found".to_string()));
            }

            println!("Found {} frames", input_files.len());
            for (i, path) in input_files.iter().enumerate() {
                if let Some(name) = path.file_name() {
                    println!("  Frame {}: {}", i, name.to_string_lossy());
                }
            }

            create_glyph(input_files, output, duration)
        }
        Some(Commands::Install) => {
            println!("Installing Glyph system-wide...");
            install::install()?;
            println!("Installation complete! Please restart your shell to use the new features.");
            Ok(())
        }
        Some(Commands::Uninstall) => {
            println!("Uninstalling Glyph...");
            uninstall::uninstall()?;
            println!("Uninstallation complete! Please restart your shell.");
            Ok(())
        }
        None => {
            // Default to play mode if a file is provided
            if let Some(input) = cli.input {
                if input.extension().and_then(|ext| ext.to_str()) == Some("glyph") {
                    play_glyph(input, cli.loops)
                } else {
                    Err(CliError::Input(
                        "Input file must have .glyph extension".to_string(),
                    ))
                }
            } else {
                Err(CliError::Input(
                    "Please provide a .glyph file to play or use a subcommand".to_string(),
                ))
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use std::io::Write;
    use tempfile::TempDir;

    fn create_test_file(dir: &TempDir, name: &str, content: &str) -> PathBuf {
        let path = dir.path().join(name);
        let mut file = File::create(&path).unwrap();
        writeln!(file, "{}", content).unwrap();
        path
    }

    #[test]
    fn test_extract_frame_number() {
        assert_eq!(extract_frame_number("frame_001.txt"), Some(1));
        assert_eq!(extract_frame_number("frame_1.txt"), Some(1));
        assert_eq!(extract_frame_number("frame.001.txt"), Some(1));
        assert_eq!(extract_frame_number("frame99.txt"), Some(99));
        assert_eq!(extract_frame_number("noframe.txt"), None);
    }

    #[test]
    fn test_find_sequence_simple_numbering() {
        let temp_dir = TempDir::new().unwrap();

        // Create test files
        let frame1 = create_test_file(&temp_dir, "frame_1.txt", "test");
        let _frame2 = create_test_file(&temp_dir, "frame_2.txt", "test");
        let _frame3 = create_test_file(&temp_dir, "frame_3.txt", "test");

        let frames = find_sequence_frames(&frame1).unwrap();
        assert_eq!(frames.len(), 3);
        assert!(frames.iter().all(|f| f.exists()));
    }

    #[test]
    fn test_find_sequence_padded_numbering() {
        let temp_dir = TempDir::new().unwrap();

        // Create test files
        let frame1 = create_test_file(&temp_dir, "frame_001.txt", "test");
        let _frame2 = create_test_file(&temp_dir, "frame_002.txt", "test");
        let _frame3 = create_test_file(&temp_dir, "frame_003.txt", "test");

        let frames = find_sequence_frames(&frame1).unwrap();
        assert_eq!(frames.len(), 3);

        // Verify correct ordering
        let names: Vec<_> = frames
            .iter()
            .map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
            .collect();
        assert_eq!(
            names,
            vec!["frame_001.txt", "frame_002.txt", "frame_003.txt"]
        );
    }

    #[test]
    fn test_find_sequence_mixed_files() {
        let temp_dir = TempDir::new().unwrap();

        // Create sequence files
        let frame1 = create_test_file(&temp_dir, "frame_001.txt", "test");
        create_test_file(&temp_dir, "frame_002.txt", "test");
        create_test_file(&temp_dir, "frame_003.txt", "test");

        // Create unrelated files
        create_test_file(&temp_dir, "other.txt", "test");
        create_test_file(&temp_dir, "unrelated_001.txt", "test");

        let frames = find_sequence_frames(&frame1).unwrap();
        assert_eq!(frames.len(), 3);

        // Verify only sequence files are included
        for frame in frames {
            assert!(frame
                .file_name()
                .unwrap()
                .to_string_lossy()
                .starts_with("frame_"));
        }
    }

    #[test]
    fn test_find_sequence_directory() {
        let temp_dir = TempDir::new().unwrap();

        // Create test files in directory
        create_test_file(&temp_dir, "frame_001.txt", "test");
        create_test_file(&temp_dir, "frame_002.txt", "test");
        create_test_file(&temp_dir, "other.txt", "test");

        let frames = find_sequence_frames(temp_dir.path()).unwrap();
        assert_eq!(frames.len(), 3); // All files in directory
    }

    #[test]
    fn test_find_sequence_single_file() {
        let temp_dir = TempDir::new().unwrap();
        let single_file = create_test_file(&temp_dir, "single.txt", "test");

        let frames = find_sequence_frames(&single_file).unwrap();
        assert_eq!(frames.len(), 1);
        assert_eq!(frames[0], single_file);
    }

    #[test]
    fn test_find_sequence_nonexistent() {
        let temp_dir = TempDir::new().unwrap();
        let nonexistent = temp_dir.path().join("definitely_nonexistent_file.txt");
        let result = find_sequence_frames(&nonexistent);
        assert!(result.is_err());
    }
}