import time
import os

from utils.logging import get_logger

logger = get_logger(__name__)


class Stats:
    """Statistics which are collected during the optimization process.
    """

    def __init__(self, config: dict, trajectory_file: str) -> None:
        self.target_function_walltime_used = 0.0
        self.config = config
        self.reset()

        self.mip_calls = 0
        self.bp_calls = 0
        self.trajectory_file = trajectory_file

    def reset(self) -> None:
        """Resets the internal variables."""
        self.size_changes = 0
        self.exact_runs = 0
        self.rre_runs = 0
        self.rrd_runs = 0
        self.tours_runs = 0
        self.rtw_runs = 0
        self.wre_runs = 0
        self.wtw_runs = 0
        self.incumbent_changed = 0
        self.cutoff_runs = 0
        self.optimal_runs_mip = 0
        self.optimal_runs_bp = 0
        self.feasible_runs_mip = 0
        self.feasible_runs_bp = 0
        self.infeasible_runs_mip = 0
        self.infeasible_runs_bp = 0
        self.error_runs_bp = 0
        self.no_solution_runs_mip = 0
        self.time_best_solution = 0
        self.time_used = [0.01]
        self.time_used_mip = [0.01]
        self.time_used_bp = [0.01]
        self.success_destroy = [0 for _ in range(3)]
        self.name_destroy = []
        self.runs_destroy = []
        self.duration_destroy = []
        self.start_time = time.perf_counter()

    def save(self, value: int) -> None:
        """Save the trajectory of the best solution found
            
            Parameters
            ----------
            value : int
                value of the best solution found
        """
        with open(os.path.join(f'{os.environ["HOME"]}', 'busdriverschedulingproblem',self.trajectory_file), 'a') as f:
            f.write(f'{self.get_used_walltime()},{value}\n')

    def get_used_walltime(self) -> float:
        """Returns used wallclock time."""
        return time.perf_counter() - self.start_time

    def get_remaing_walltime(self) -> float:
        """Subtracts the runtime configuration budget
            with the used wallclock time.
        """
        return self.config['walltime_limit'] - (time.perf_counter() - self.start_time)

    def is_budget_exhausted(self) -> bool:
        """Check whether the the remaining walltime was exceeded."""
        return self.get_remaing_walltime() < 0

    def update_repairer(self, name: str, status: str, time_used: float) -> None:
        """Update stats regarding the status

        Parameters
        ----------
        status : str
            optimization status, which can be
            OPTIMAL(0),
            ERROR(-1),
            INFEASIBLE(1),
            UNBOUNDED(2),
            FEASIBLE(3) for the case when a feasible solution
                was found but optimality was not proved,
            INT_INFEASIBLE(4) for the case when the lp relaxation is
                feasible but no feasible integer solution,
            NO_SOLUTION_FOUND(5) for the case when an integer solution was not
                found in the optimization.
        time_used : _type_
            time used for MIP
        """
        self.time_used.append(time_used)
        if name == 'CG':
            self.bp_calls += 1
            self.time_used_bp.append(time_used)
            if status == 'INFEASIBLE':
                self.infeasible_runs_bp += 1
            elif status == 'OPTIMAL':
                self.optimal_runs_bp += 1
            elif status == 'ERROR':
                self.error_runs_bp += 1
        elif name == 'MIP':
            self.mip_calls += 1
            self.time_used_mip.append(time_used)
            if status == 'FEASIBLE':
                self.feasible_runs_mip += 1
            elif status == 'INFEASIBLE':
                self.infeasible_runs_mip += 1
            elif status == 'NO_SOLUTION_FOUND':
                self.no_solution_runs_mip += 1
            elif status == 'OPTIMAL':
                self.optimal_runs_mip += 1
        self.exact_runs += 1
        
    def print(self) -> None:
        """Prints all statistics."""
        self.stats_runs = ''.join(f'--- {self.name_destroy[i]}: {self.success_destroy[i]}/{self.runs_destroy[i]}\n' for i in range(len(self.name_destroy)))

        self.stats_runs = self.stats_runs[:-1]
        try:
            logger.info(
                "\n"
                f"---DESTROYERS -------------------------------------\n"
                f"{self.stats_runs}\n"
                f"---COLUMN GENERATION\n"
                f"--- Optimal: {self.optimal_runs_bp}/{self.bp_calls} \n"
                f"--- Infeasible: {self.infeasible_runs_bp}/{self.bp_calls} \n"
                f"--- Error: {self.error_runs_bp}/{self.bp_calls} \n"
                f"---STATISTICS -------------------------------------\n"
                f"--- Best solutions changed: {self.incumbent_changed}\n"
                f"--- CG calls: {self.bp_calls}\n"
                f"--- Last solution found after {self.time_best_solution:.2f} s\n"
                f"--- Size changes: {self.size_changes}\n"
                f"--- Average time used to repair: {sum(self.time_used)/len(self.time_used):.2f} s\n"
                f"--- Max time used to repair: {max(self.time_used):.2f} s\n"
                f"--- Used wallclock time: {round(self.get_used_walltime())}/{self.config['walltime_limit']} s\n"
                f"----------------------------------------------------"
            )
        except Exception as e:
            logger.error(e)
