// Standard library
use std::error::Error;
use std::fs::File;
use std::io::BufReader;
use std::path::Path;
use std::time::{Duration,Instant};

// External crates
use clap::Parser;
use log::{info, debug};
use indicatif::{ProgressBar, ProgressStyle};
use humantime;

// Internal modules
mod types;
mod config;
mod simulator;

// Specific uses from modules
use crate::simulator::Simulation;
use crate::types::{norm_time, real_body};


#[derive(Parser, Debug)]
#[command(
    version, 
    about="Orbital mechanics simulator", 
    long_about = "Given initial conditions you provide to --config, \
this program will numerically integrate and determinate their \
paths based off Newton's law of gravity.")]
struct Args {
    ///Config file for initial conditions
    #[arg(short, long)]
    config: String,

    /// Time to run simulation for (e.g. 10s, 5m, 2h, 100d)
    #[arg(short, long, value_parser = humantime::parse_duration)]
    time: Duration,

    ///Step size for simulation (seconds)
    #[arg(short, long, default_value_t = 10.0)]
    step_size: f64,

    ///How often to update progress bar and save (seconds)
    #[arg(short = 'P', long, default_value_t = 1000)]
    steps_per_save: usize,

    /// Filename to save trajectory to
    #[arg(short, long)]
    output_file: String,
}

fn read_config<P: AsRef<Path>>(path: P) -> Result<config::ConfigFile, Box<dyn Error>> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);

    let u = serde_json::from_reader(reader)?;

    Ok(u)
}
//fn parse_time(arg: &str) 

fn format_duration_single_unit(dur: Duration) -> String {
    let secs = dur.as_secs_f64();

    const MINUTE: f64 = 60.0;
    const HOUR: f64 = 60.0 * MINUTE;
    const DAY: f64 = 24.0 * HOUR;
    const YEAR: f64 = 365.25 * DAY;

    if secs >= YEAR {
        format!("{:.0} years", (secs / YEAR).round())
    } else if secs >= DAY {
        format!("{:.0} days", (secs / DAY).round())
    } else if secs >= HOUR {
        format!("{:.0} hours", (secs / HOUR).round())
    } else if secs >= MINUTE {
        format!("{:.0} minutes", (secs / MINUTE).round())
    } else {
        format!("{:.0} seconds", secs.round())
    }
}

fn main() {
    env_logger::init();
    let args = Args::parse();

    info!("Loading initial parameters from {}", args.config);

    let conf = read_config(args.config).unwrap();
    for body in &conf.bodies {
        info!("Loaded {} with mass {:.3e} kg", body.name, body.mass);
        debug!("R_i = {:?}, V_i = {:?}", body.position, body.velocity);
    }

    let n_steps = (args.time.as_secs() as f64 / args.step_size) as usize;


    let pb = ProgressBar::new(n_steps.try_into().unwrap());
    pb.set_style(
    ProgressStyle::with_template("[{elapsed_precise}] {bar:40.cyan/blue} {percent_precise}% \n\
    Time remaining: {eta} \n\
    Current simulation speed: {msg}")
        .unwrap()
        .progress_chars("=>-"),
    );

    let mut sim = Simulation::new(
        conf.bodies, 
        norm_time(args.step_size), 
        args.steps_per_save,
        args.output_file,
    );
    let start = Instant::now();
    sim.run(n_steps, Some(|| {
            let elapsed = start.elapsed().as_secs() as f64 + 1.0;
            let speed = Duration::from_secs_f64(pb.position() as f64 * args.step_size / elapsed);
            pb.set_message(format!("{} /sec", format_duration_single_unit(speed)));
            pb.inc(args.steps_per_save as u64);
        })
    );
}
