mod input;
mod update;
mod views;

use anyhow::{Result, anyhow};
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
use egui::{Context, FontData, FontDefinitions, FontFamily, FontTweak, Vec2, ViewportBuilder};
use itertools::Itertools;
use pwsp_lib::{
    types::{
        audio_player::PlayerState,
        config::GuiConfig,
        config::HotkeyConfig,
        gui::{AppState, AudioPlayerState},
        socket::Request,
    },
    utils::{
        daemon::get_daemon_config,
        gui::{get_gui_config, make_request_async, make_request_sync, start_app_state_thread},
    },
};
use rfd::FileDialog;
use std::{
    cmp::Ordering,
    fs,
    path::{Path, PathBuf},
    sync::{Arc, Mutex},
};
use system_fonts::{FontStyle, FoundFontSource, find_for_locale};

const SUPPORTED_EXTENSIONS: [&str; 13] = [
    "mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi", "opus",
];

struct SoundpadGui {
    pub app_state: AppState,
    pub config: GuiConfig,
    pub audio_player_state: AudioPlayerState,
    pub audio_player_state_shared: Arc<Mutex<AudioPlayerState>>,
}

impl SoundpadGui {
    fn new(ctx: &Context) -> Self {
        let audio_player_state = Arc::new(Mutex::new(AudioPlayerState::default()));
        start_app_state_thread(audio_player_state.clone());

        let config = get_gui_config();

        ctx.set_zoom_factor(config.scale_factor);

        let mut soundpad_gui = SoundpadGui {
            app_state: AppState::default(),
            config: config.clone(),
            audio_player_state: AudioPlayerState::default(),
            audio_player_state_shared: audio_player_state.clone(),
        };

        soundpad_gui.app_state.dirs = config.dirs;
        soundpad_gui.app_state.hotkey_config = HotkeyConfig::load().unwrap_or_default();

        soundpad_gui
    }

    pub fn play_toggle(&mut self) {
        let (new_state, request) = {
            let guard = self
                .audio_player_state_shared
                .lock()
                .unwrap_or_else(|e| e.into_inner());
            match guard.state {
                PlayerState::Playing => (Some(PlayerState::Paused), Some(Request::pause(None))),
                PlayerState::Paused => (Some(PlayerState::Playing), Some(Request::resume(None))),
                PlayerState::Stopped => (None, None),
            }
        };

        if let Some(req) = request {
            make_request_async(req);
        }

        if let Some(state) = new_state {
            let mut guard = self
                .audio_player_state_shared
                .lock()
                .unwrap_or_else(|e| e.into_inner());
            guard.new_state = Some(state.clone());
            guard.state = state;
        }
    }

    pub fn open_file(&mut self) {
        let file_dialog = FileDialog::new().add_filter("Audio File", &SUPPORTED_EXTENSIONS);
        if let Some(path) = file_dialog.pick_file() {
            self.play_file(&path, false);
        }
    }

    pub fn add_dirs(&mut self) {
        let file_dialog = FileDialog::new();
        if let Some(paths) = file_dialog.pick_folders() {
            for path in paths {
                self.app_state.dirs.push(path);
            }
            self.app_state.dirs = self.app_state.dirs.iter().unique().cloned().collect();
            self.config.dirs = self.app_state.dirs.clone();
            self.config.save_to_file().ok();
        }
    }

    pub fn open_dir(&mut self, path: &PathBuf) {
        self.app_state.current_dir = Some(path.clone());
        match path.read_dir() {
            Ok(read_dir) => {
                self.app_state.listed_files = read_dir
                    .filter_map(|res| res.ok())
                    .map(|entry| entry.path())
                    .collect();
            }
            Err(e) => {
                eprintln!("Failed to read directory {:?}: {}", path, e);
                self.app_state.listed_files.clear();
            }
        }
    }

    pub fn play_file(&mut self, path: &Path, concurrent: bool) {
        make_request_async(Request::play(&path.to_string_lossy(), concurrent));
    }

    pub fn set_input(&mut self, name: String) {
        make_request_async(Request::set_input(&name));

        if self.config.save_input {
            let mut daemon_config = get_daemon_config();
            daemon_config.default_input_name = Some(name);
            daemon_config.save_to_file().ok();
        }
    }

    pub fn toggle_loop(&mut self, id: Option<u32>) {
        make_request_async(Request::toggle_loop(id));
    }

    pub fn pause(&mut self, id: Option<u32>) {
        make_request_async(Request::pause(id));
    }

    pub fn resume(&mut self, id: Option<u32>) {
        make_request_async(Request::resume(id));
    }

    pub fn stop(&mut self, id: Option<u32>) {
        make_request_async(Request::stop(id));
    }

    pub fn play_hotkey_slot(&mut self, slot: &str) {
        make_request_async(Request::play_hotkey(slot));
    }

    pub fn get_filtered_files(&self) -> Vec<PathBuf> {
        let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect();
        let sort_order = self
            .app_state
            .current_dir
            .as_ref()
            .map(|d| self.config.get_sort_order(d))
            .unwrap_or_default();

        files.sort_by(|a, b| {
            let a_is_dir = a.is_dir();
            let b_is_dir = b.is_dir();
            if a_is_dir && !b_is_dir {
                Ordering::Less
            } else if !a_is_dir && b_is_dir {
                Ordering::Greater
            } else {
                sort_order.compare(a, b)
            }
        });

        let search_query = self.app_state.search_query.to_lowercase();
        let search_query = search_query.trim();

        files
            .into_iter()
            .filter(|entry_path| {
                if entry_path.is_dir() {
                    return true;
                }

                if !SUPPORTED_EXTENSIONS.contains(
                    &entry_path
                        .extension()
                        .unwrap_or_default()
                        .to_str()
                        .unwrap_or_default(),
                ) {
                    return false;
                }

                if !search_query.is_empty() {
                    let file_name = entry_path
                        .file_name()
                        .unwrap_or_default()
                        .to_string_lossy()
                        .to_string();

                    if !file_name.to_lowercase().contains(search_query) {
                        return false;
                    }
                }

                true
            })
            .collect()
    }
}

fn add_font(font_name: &str, font_bytes: &[u8], fonts: &mut FontDefinitions) -> Result<()> {
    let font_data = FontData::from_owned(font_bytes.to_vec()).tweak(FontTweak {
        scale: 1.0,
        hinting_override: Some(true),
        ..Default::default()
    });

    fonts
        .font_data
        .insert(font_name.to_owned(), font_data.into());

    fonts
        .families
        .entry(FontFamily::Proportional)
        .or_default()
        .insert(0, font_name.to_owned());
    fonts
        .families
        .entry(FontFamily::Monospace)
        .or_default()
        .insert(0, font_name.to_owned());

    Ok(())
}

fn load_system_fonts(fonts: &mut FontDefinitions) -> Result<()> {
    let (_, en_sans) = find_for_locale("en", FontStyle::Sans);
    let (_, en_serif) = find_for_locale("en", FontStyle::Serif);
    let (_, ja_sans) = find_for_locale("ja", FontStyle::Sans);
    let (_, ar_sans) = find_for_locale("ar", FontStyle::Sans);

    let system_fonts = [en_sans, en_serif, ja_sans, ar_sans].concat();

    for font in system_fonts.iter().rev() {
        let font_bytes = match &font.source {
            FoundFontSource::Path(path) => fs::read(path)?,
            FoundFontSource::Bytes(bytes) => bytes.to_vec(),
        };

        add_font(&font.key, &font_bytes, fonts)?;
    }

    Ok(())
}

pub async fn run() -> Result<()> {
    const ICON: &[u8] = include_bytes!("../../assets/icon.png");

    let options = NativeOptions {
        vsync: true,
        centered: true,
        hardware_acceleration: HardwareAcceleration::Preferred,

        viewport: ViewportBuilder::default()
            .with_app_id("ru.arabianq.pwsp")
            .with_inner_size(Vec2::new(1200.0, 800.0))
            .with_min_inner_size(Vec2::new(800.0, 600.0))
            .with_icon(from_png_bytes(ICON)?),

        ..Default::default()
    };

    match run_native(
        "Pipewire Soundpad",
        options,
        Box::new(|cc| {
            egui_material_icons::initialize(&cc.egui_ctx);

            let mut fonts = FontDefinitions::default();
            load_system_fonts(&mut fonts).ok();
            cc.egui_ctx.set_fonts(fonts);

            Ok(Box::new(SoundpadGui::new(&cc.egui_ctx)))
        }),
    ) {
        Ok(_) => {
            let config = get_gui_config();
            if config.pause_on_exit {
                make_request_sync(Request::pause(None)).ok();
            }
            Ok(())
        }
        Err(e) => Err(anyhow!(e.to_string())),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_filtered_files() {
        let mut gui = SoundpadGui {
            app_state: AppState::default(),
            config: GuiConfig::default(),
            audio_player_state: AudioPlayerState::default(),
            audio_player_state_shared: Arc::new(Mutex::new(AudioPlayerState::default())),
        };

        // Create some dummy paths
        // We will mock path properties using standard Rust PathBuf
        let dir_a = PathBuf::from("a_dir");
        let file_b = PathBuf::from("b_file.mp3");
        let file_c = PathBuf::from("c_file.wav");
        let file_txt = PathBuf::from("invalid.txt");

        gui.app_state.listed_files.insert(dir_a.clone());
        gui.app_state.listed_files.insert(file_b.clone());
        gui.app_state.listed_files.insert(file_c.clone());
        gui.app_state.listed_files.insert(file_txt.clone());

        // Note: is_dir() check in get_filtered_files relies on physical filesystem properties.
        // On the real OS filesystem, these paths don't exist, so they are treated as files.
        // Unsupported extensions (like .txt) will be filtered out.
        // So we expect only file_b and file_c, sorted alphabetically.
        let filtered = gui.get_filtered_files();
        assert_eq!(filtered.len(), 2);
        assert_eq!(filtered[0], file_b);
        assert_eq!(filtered[1], file_c);

        // Test search query
        gui.app_state.search_query = "c_fi".to_string();
        let filtered_search = gui.get_filtered_files();
        assert_eq!(filtered_search.len(), 1);
        assert_eq!(filtered_search[0], file_c);

        // Test sort order descending
        gui.app_state.current_dir = Some(PathBuf::from("dummy_dir"));
        gui.config.dirs_settings.insert(
            PathBuf::from("dummy_dir"),
            pwsp_lib::types::config::DirSettings {
                sort_order: pwsp_lib::types::config::SortOrder::AlphabeticalDesc,
            },
        );
        gui.app_state.search_query = String::new();
        let filtered_desc = gui.get_filtered_files();
        assert_eq!(filtered_desc.len(), 2);
        assert_eq!(filtered_desc[0], file_c);
        assert_eq!(filtered_desc[1], file_b);
    }
}
