use std::{
    io::{BufReader, Write},
    net::{SocketAddr, TcpStream},
    path::PathBuf,
    sync::{Arc, Mutex},
    thread,
};

use clap::{Parser, Subcommand};
#[cfg(feature = "speedy2d")]
use gui::GuiEvent;
#[cfg(feature = "playback")]
use musicdb_lib::player::Player;
use musicdb_lib::{
    data::{
        database::{ClientIo, Database},
        CoverId, SongId,
    },
    load::ToFromBytes,
    server::Command,
};
#[cfg(feature = "speedy2d")]
use speedy2d::color::Color;
#[cfg(feature = "speedy2d")]
mod gui;
#[cfg(feature = "speedy2d")]
mod gui_anim;
#[cfg(feature = "speedy2d")]
mod gui_base;
#[cfg(feature = "speedy2d")]
mod gui_edit_song;
#[cfg(feature = "speedy2d")]
mod gui_idle_display;
#[cfg(feature = "speedy2d")]
mod gui_library;
#[cfg(feature = "speedy2d")]
mod gui_notif;
#[cfg(feature = "speedy2d")]
mod gui_playback;
#[cfg(feature = "speedy2d")]
mod gui_playpause;
#[cfg(feature = "speedy2d")]
mod gui_queue;
#[cfg(feature = "speedy2d")]
mod gui_screen;
#[cfg(feature = "speedy2d")]
mod gui_settings;
#[cfg(feature = "speedy2d")]
mod gui_statusbar;
#[cfg(feature = "speedy2d")]
mod gui_text;
#[cfg(feature = "speedy2d")]
mod gui_wrappers;
#[cfg(feature = "merscfg")]
mod merscfg;
#[cfg(feature = "speedy2d")]
mod textcfg;

#[derive(Parser, Debug)]
struct Args {
    /// the address to be used for the tcp connection to the server
    addr: SocketAddr,
    /// what to do
    #[command(subcommand)]
    mode: Mode,
}

#[derive(Subcommand, Debug, Clone)]
enum Mode {
    #[cfg(feature = "speedy2d")]
    /// graphical user interface
    Gui,
    /// play in sync with the server, but load the songs from a local copy of the lib-dir
    #[cfg(feature = "playback")]
    SyncplayerLocal { lib_dir: PathBuf },
    /// play in sync with the server, and fetch the songs from it too. slower than the local variant for obvious reasons
    #[cfg(feature = "playback")]
    SyncplayerNetwork,
    #[cfg(feature = "mers")]
    RunMers { path: PathBuf },
}

fn get_config_file_path() -> PathBuf {
    directories::ProjectDirs::from("", "", "musicdb-client")
        .unwrap()
        .config_dir()
        .to_path_buf()
}

fn main() {
    #[cfg(not(feature = "speedy2d"))]
    #[cfg(not(feature = "mers"))]
    #[cfg(not(feature = "playback"))]
    compile_error!("None of the optional features are enabled. Without at least one of these, the application is useless! See Cargo.toml for info.");
    // parse args
    let args = Args::parse();
    // start
    let addr = args.addr;
    let mut con = TcpStream::connect(addr).unwrap();
    let mode = args.mode;
    writeln!(con, "main").unwrap();
    let database = Arc::new(Mutex::new(Database::new_clientside()));
    #[cfg(feature = "speedy2d")]
    let update_gui_sender: Arc<Mutex<Option<speedy2d::window::UserEventSender<GuiEvent>>>> =
        Arc::new(Mutex::new(None));
    #[cfg(feature = "speedy2d")]
    let sender = Arc::clone(&update_gui_sender);
    let con_thread = {
        let mode = mode.clone();
        let database = Arc::clone(&database);
        let mut con = con.try_clone().unwrap();
        // this is all you need to keep the db in sync
        thread::spawn(move || {
            #[cfg(feature = "playback")]
            let mut player =
                if matches!(mode, Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork) {
                    Some(Player::new().unwrap().without_sending_commands())
                } else {
                    None
                };
            #[allow(unused_labels)]
            'ifstatementworkaround: {
                // use if+break instead of if-else because we can't #[cfg(feature)] the if statement,
                // since we want the else part to run if the feature is disabled
                #[cfg(feature = "playback")]
                if let Mode::SyncplayerLocal { lib_dir } = mode {
                    let mut db = database.lock().unwrap();
                    db.lib_directory = lib_dir;
                    break 'ifstatementworkaround;
                }
                let mut db = database.lock().unwrap();
                let client_con: Box<dyn ClientIo> = Box::new(TcpStream::connect(addr).unwrap());
                db.remote_server_as_song_file_source = Some(Arc::new(Mutex::new(
                    musicdb_lib::server::get::Client::new(BufReader::new(client_con)).unwrap(),
                )));
            }
            loop {
                #[cfg(feature = "playback")]
                if let Some(player) = &mut player {
                    let mut db = database.lock().unwrap();
                    if db.is_client_init() {
                        // command_sender does nothing. if a song finishes, we don't want to move to the next song, we want to wait for the server to send the NextSong event.
                        player.update(&mut db, &Arc::new(|_| {}));
                    }
                }
                let update = Command::from_bytes(&mut con).unwrap();
                #[cfg(feature = "playback")]
                if let Some(player) = &mut player {
                    player.handle_command(&update);
                }
                database.lock().unwrap().apply_command(update);
                #[cfg(feature = "speedy2d")]
                if let Some(v) = &*update_gui_sender.lock().unwrap() {
                    v.send_event(GuiEvent::Refresh).unwrap();
                }
            }
        })
    };
    match mode {
        #[cfg(feature = "speedy2d")]
        Mode::Gui => {
            {
                let occasional_refresh_sender = Arc::clone(&sender);
                thread::spawn(move || loop {
                    std::thread::sleep(std::time::Duration::from_secs(1));
                    if let Some(v) = &*occasional_refresh_sender.lock().unwrap() {
                        v.send_event(GuiEvent::Refresh).unwrap();
                    }
                });
                gui::main(
                    database,
                    con,
                    musicdb_lib::server::get::Client::new(BufReader::new(
                        TcpStream::connect(addr).expect("opening get client connection"),
                    ))
                    .expect("initializing get client connection"),
                    sender,
                )
            };
        }
        #[cfg(feature = "playback")]
        Mode::SyncplayerLocal { .. } | Mode::SyncplayerNetwork => {
            con_thread.join().unwrap();
        }
        #[cfg(feature = "mers")]
        Mode::RunMers { path } => {
            let mut src = mers_lib::prelude_compile::Source::new_from_file(path).unwrap();
            let srca = Arc::new(src.clone());
            let con = Mutex::new(con);
            let (mut i1, mut i2, mut i3) = musicdb_mers::add(
                mers_lib::prelude_compile::Config::new().bundle_std(),
                &database,
                &Arc::new(move |cmd: Command| cmd.to_bytes(&mut *con.lock().unwrap()).unwrap()),
            )
            .infos();
            let program = mers_lib::prelude_compile::parse(&mut src, &srca)
                .unwrap()
                .compile(&mut i1, mers_lib::prelude_compile::CompInfo::default())
                .unwrap();
            match program.check(&mut i3, None) {
                Ok(_) => {}
                Err(e) => {
                    eprintln!("{e}");
                    std::process::exit(60);
                }
            };
            // wait until db is synced
            let dur = std::time::Duration::from_secs_f32(0.1);
            loop {
                std::thread::sleep(dur);
                if database.lock().unwrap().is_client_init() {
                    break;
                }
            }
            program.run(&mut i2);
        }
    }
}

pub fn accumulate<F: FnMut() -> Option<T>, T>(mut f: F) -> Vec<T> {
    let mut o = vec![];
    loop {
        if let Some(v) = f() {
            o.push(v);
        } else {
            break;
        }
    }
    o
}

fn get_cover(song: SongId, database: &Database) -> Option<CoverId> {
    let song = database.get_song(&song)?;
    if let Some(v) = song.cover {
        Some(v)
    } else {
        database.albums().get(song.album.as_ref()?)?.cover
    }
}

#[cfg(feature = "speedy2d")]
pub(crate) fn color_scale(c: Color, r: f32, g: f32, b: f32, new_alpha: Option<f32>) -> Color {
    Color::from_rgba(c.r() * r, c.g() * g, c.b() * b, new_alpha.unwrap_or(c.a()))
}
