use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Instant;

use nsga2::as_algorithm_args;

use crate::algorithms::nsga2::crowding::{CrowdingDistanceOperator, DistanceData};
use crate::algorithms::nsga2::survival::{EpsilonData, NSGA2Survival};
use crate::algorithms::nsga2::{CoreNSGA2, CoreNSGA2Arg};
use crate::algorithms::Algorithm;
use crate::core::{OError, Population, Problem};
use crate::operators::{PolynomialMutationArgs, SimulatedBinaryCrossoverArgs};

#[cfg(feature = "python")]
use pyo3::prelude::*;

/// Input arguments for the R-NSGA2 algorithm.
#[as_algorithm_args]
#[cfg_attr(feature = "python", pyclass(get_all))]
pub struct RNSGA2Arg {
    /// The number of individuals to use in the population. This must be a multiple of `2`.
    pub number_of_individuals: usize,
    /// Find the Pareto-optimal solutions closest to the supplied reference points. This
    /// is a vector with size equals to the number of reference point to use. Each point
    /// must have as many coordinates as the number of problem objectives.
    pub reference_points: Vec<Vec<f64>>,
    /// This parameter controls the distance of the solutions close to the reference
    /// points on the Pareto-optimal front. The solution distances on the objective space
    /// are always normalised, therefore epsilon must be given in relative terms.
    /// A large value of epsilon leads to a large range of solutions on the front. If
    /// you want to obtain less solutions near the chosen reference points, you should
    /// choose a small value of epsilon.
    pub epsilon: u64,
    /// The weights to use for each objectives when the the normalised distance between
    /// the individuals is calculated during the survival step (i.e. when the
    /// individuals need to be selected from the last non-dominated front to create the
    /// new population). When `None` the weights are all the same and equal to `1/M`,
    /// where `M` is the number of objectives.
    pub weights: Option<Vec<f64>>,
    /// The options of the Simulated Binary Crossover (SBX) operator. This operator is used to
    /// generate new children by recombining the variables of parent solutions. This defaults to
    /// [`SimulatedBinaryCrossoverArgs::default()`].
    pub crossover_operator_options: Option<SimulatedBinaryCrossoverArgs>,
    /// The options to Polynomial Mutation (PM) operator used to mutate the variables of an
    /// individual. This defaults to [`PolynomialMutationArgs::default()`],
    /// with a distribution index or index parameter of `20` and variable probability equal `1`
    /// divided by the number of real variables in the problem (i.e., each variable will have the
    /// same probability of being mutated).
    pub mutation_operator_options: Option<PolynomialMutationArgs>,
    /// Instead of initialising the population with random variables, see the initial population
    /// with  the variable values from a JSON files exported with this tool. This option lets you
    /// restart the evolution from a previous generation; you can use any history file (exported
    /// when the field `export_history`) or the file exported when the stopping condition was reached.
    pub resume_from_file: Option<PathBuf>,
    /// The seed used in the random number generator (RNG). You can specify a seed in case you want
    /// to try to reproduce results. R-NSGA2 is a stochastic algorithm that relies on a RNG at
    /// different steps (when population is initially generated, during selection, crossover and
    /// mutation) and, as such, may lead to slightly different solutions. The seed is randomly
    /// picked if this is `None`.
    pub seed: Option<u64>,
}

#[cfg(feature = "python")]
#[pymethods]
impl RNSGA2Arg {
    #[new]
    #[pyo3(signature = (number_of_individuals, reference_points, epsilon, stopping_condition, weights=None, crossover_operator_options=None, mutation_operator_options=None, resume_from_file=None, parallel=None, export_history=None, seed=None))]
    fn py_new(
        number_of_individuals: usize,
        reference_points: Vec<Vec<f64>>,
        epsilon: f64,
        stopping_condition: StoppingCondition,
        weights: Option<Vec<f64>>,
        crossover_operator_options: Option<SimulatedBinaryCrossoverArgs>,
        mutation_operator_options: Option<PolynomialMutationArgs>,
        resume_from_file: Option<PathBuf>,
        parallel: Option<bool>,
        export_history: Option<ExportHistory>,
        seed: Option<u64>,
    ) -> PyResult<Self> {
        Ok(RNSGA2Arg {
            number_of_individuals,
            epsilon,
            weights,
            reference_points,
            crossover_operator_options,
            mutation_operator_options,
            resume_from_file,
            seed,
            stopping_condition,
            parallel,
            export_history,
        })
    }

    pub fn __repr__(&self) -> PyResult<String> {
        Ok(format!(
            "RNSGA2Arg(number_of_individuals={}, stopping_condition={})",
            self.number_of_individuals, self.stopping_condition
        ))
    }

    fn __str__(&self) -> String {
        self.__repr__().unwrap()
    }
}

/// The reference-point-based NSGA-II algorithm (R-NSGA2).
///
/// Implemented based on:
/// > Kalyanmoy Deb and J. Sundar. 2006. Reference point based multi-objective
/// > optimization using evolutionary algorithms. In Proceedings of the 8th annual
/// > conference on Genetic and evolutionary computation (GECCO '06). Association
/// > for Computing Machinery, New York, NY, USA, 635–642.
///
/// See: <https://doi.org/10.1145/1143997.1144112>.
pub struct RNSGA2 {
    /// The reference points.
    reference_points: Vec<Vec<f64>>,
    /// The core RSGA2 algorithm.
    core: CoreNSGA2,
    /// The options sued to initialise the algorithm
    args: RNSGA2Arg,
}

impl RNSGA2 {
    /// Initialise the RNSGA2 algorithm.
    ///
    /// # Arguments
    ///
    /// * `problem`: The problem being solved.
    /// * `args`: The [`RNSGA2Arg`] arguments to customise the algorithm behaviour.
    ///
    /// returns: `RNSGA2`.
    pub fn new(problem: Problem, options: RNSGA2Arg) -> Result<Self, OError> {
        let name = "R-NSGA2".to_string();
        let nsga2_options = options.clone();

        // check the size of the reference points
        if options.reference_points.is_empty() {
            return Err(OError::AlgorithmInit(
                name,
                "The vector of reference point is empty".to_string(),
            ));
        }
        if options.reference_points.get(0).unwrap().len() != problem.number_of_objectives() {
            return Err(OError::AlgorithmInit(
                name,
                format!(
                    "The number of coordinates of the reference points must match the number of objectives ({})", 
                    problem.number_of_objectives()
                ),
            ));
        }
        // check epsilon
        if options.epsilon == 0 {
            return Err(OError::AlgorithmInit(
                name,
                "Epsilon must be strictly larger than zero".to_string(),
            ));
        }
        // check weights
        let weights = options.weights.unwrap_or(vec![
            1.0 / problem.number_of_objectives() as f64;
            problem.number_of_objectives()
        ]);
        if weights.len() == problem.number_of_objectives() {
            return Err(OError::AlgorithmInit(
                name,
                format!(
                    "The number of weights coordinates must match the number of objectives ({})",
                    problem.number_of_objectives()
                ),
            ));
        }

        let core_options = CoreNSGA2Arg {
            stopping_condition: options.stopping_condition,
            number_of_individuals: options.number_of_individuals,
            crossover_operator_options: options.crossover_operator_options,
            mutation_operator_options: options.mutation_operator_options,
            resume_from_file: options.resume_from_file,
            seed: options.seed,
            survival_operator: NSGA2Survival::Epsilon(EpsilonData {
                epsilon: nsga2_options.epsilon.clone(),
                reference_points: nsga2_options.reference_points.clone(),
                weights: weights.clone(),
            }),
            crowding_operator: CrowdingDistanceOperator::Rank(DistanceData {
                reference_points: nsga2_options.reference_points.clone(),
                weights,
            }),
            parallel: options.parallel,
            export_history: options.export_history,
        };

        Ok(Self {
            reference_points: options.reference_points,
            args: nsga2_options,
            core: CoreNSGA2::new(problem, core_options)?,
        })
    }
}

impl Algorithm<RNSGA2Arg> for RNSGA2 {
    /// This assesses the initial random population and sets the individual's ranks and crowding
    /// distance needed in [`self.evolve`].
    ///
    /// return: `Result<(), OError>`
    fn initialise(&mut self) -> Result<(), OError> {
        self.core.initialise()
    }

    fn evolve(&mut self) -> Result<(), OError> {
        self.core.evolve()
    }

    fn stopping_condition(&self) -> &StoppingCondition {
        &self.core.stopping_condition
    }

    fn name(&self) -> String {
        "R-NSGA2".to_string()
    }

    fn problem(&self) -> Arc<Problem> {
        self.core.problem.clone()
    }

    fn population(&self) -> &Population {
        &self.core.population
    }

    fn export_history(&self) -> Option<&ExportHistory> {
        self.core.export_history.as_ref()
    }

    fn generation(&self) -> u32 {
        self.core.generation
    }

    fn number_of_function_evaluations(&self) -> u32 {
        self.core.nfe
    }

    fn algorithm_options(&self) -> RNSGA2Arg {
        self.args.clone()
    }

    fn start_time(&self) -> &Instant {
        &self.core.start_time
    }
}

impl RNSGA2 {
    /// Get the reference points used in the optimisation.
    pub fn reference_points(&self) -> Vec<Vec<f64>> {
        self.args.reference_points.clone()
    }
}

impl Display for RNSGA2 {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.name().as_str())
    }
}
