extern crate crossterm;

use std::{
    fs::{
        File,
        OpenOptions
    },
    io::{
        self,
        BufWriter,
        Write
    },
    time::Duration
};

use crossterm::{
    cursor,
    event::{
        self,
        Event
    },
    style::{
        Colorize,
        Styler
    },
    terminal,
    ExecutableCommand,
};

use crate::evolearn::Population;

use super::Evolver;

pub const LOG_FILENAME: &str = "evolearn.csv";

impl Evolver {
    /// Evolves population until any key is pressed (`epochs` = 0) or for given number of `epochs`.
    /// Outputs to terminal and to log if logging is on.
    pub fn evol(&mut self, epochs: usize) -> Result<(), String> {
        // Some sanity checks...
        let (inp_size, outp_size, layer_size) = (self.bitran.inp_size(), self.bitran.outp_size(), self.population[0].size());
        if inp_size > layer_size {
            return Err(format!("Input size {} > layer size {}", inp_size, layer_size));
        }
        if outp_size > layer_size {
            return Err(format!("Output size {} > layer size {}", outp_size, layer_size));
        }

        let _ = io::stdout().execute(cursor::Hide); // ignore if it's not hideable

        let mut epochs_left: usize = 0;
        if epochs == 0 {
            println!("{}", "Press any key to stop...".dark_grey());
            terminal::enable_raw_mode().map_err(|e| e.to_string())?;
        } else {
            epochs_left = epochs;
        }

        // If logging is on, create logfile if it doesn't exist and write header, then get writer to log epochs' results to
        let mut log_writer = match self.settings.log {
            true => {
                let logfile_anew = File::open(LOG_FILENAME).is_err();
                let mut wr = BufWriter::new(OpenOptions::new()
                    .append(true)
                    .create(true)
                    .open(LOG_FILENAME).map_err(|e| e.to_string())?);
                if logfile_anew {
                    writeln!(&mut wr, "epoch;duration;min;average;max").map_err(|e| e.to_string())?;
                }
                Some(wr)
            },
            false => None
        };

        let mut best_max_sc = std::f64::NEG_INFINITY;

        'ext: loop {
            let time_start = std::time::Instant::now();

            self.population.evolearn(self.bitran.as_ref(), & self.parameters, self.settings.test_all);

            let duration = time_start.elapsed().as_nanos() / 1000000; // milsec
            let (min_sc, avg_sc, max_sc) = self.population.score_stats();

            self.counters.epoch += 1;

            // To screen...
            print!("{} {:8} ({:7} {}){} {} = {:9.4}{} = {:9.4}{} = {:9.4}",
                "Epoch".magenta().bold(), self.counters.epoch,
                duration, "ms".dark_blue(),
                ":".magenta().bold(),
                "min".magenta(), min_sc,
                ", average".magenta(), avg_sc,
                ", max".magenta(), max_sc);

            if self.settings.print_epochs_all {
                best_max_sc = std::f64::NEG_INFINITY; // "improvement at each epoch", for the following check
            }
            if max_sc > best_max_sc {
                best_max_sc = max_sc;
                // ...and to logfile
                if let Some(ref mut wr) = &mut log_writer {
                    writeln!(wr, "{};{};{:.4};{:.4};{:.4}", self.counters.epoch, duration, min_sc, avg_sc, max_sc).map_err(|e| e.to_string())?;
                }
                print!("\r\n");
            } else {
                print!("\r");
            }

            if epochs == 0 {
                while let Ok(true) = event::poll(Duration::from_secs(0)) {
                    if let Ok(Event::Key(..)) = event::read() {
                        break 'ext;
                    }
                }
            } else {
                epochs_left -= 1;
                if epochs_left == 0 {
                    break 'ext;
                }
            }
        }

        if let Some(ref mut wr) = &mut log_writer {
            wr.flush().map_err(|e| e.to_string())?;
        }

        if epochs == 0 {
            terminal::disable_raw_mode().map_err(|e| e.to_string())?;
        }

        println!("");

        let _ = io::stdout().execute(cursor::Show); // ignore if it's not showable

        Ok(())
    }

}
