from abc import ABCMeta, abstractmethod
import math


class SurrogateModel(metaclass=ABCMeta):
    def __init__(self, problem):
        # self.name = name
        self.problem = problem

        # surrogate model
        self.regressor = None

        self.x_data = []
        self.y_data = []

        self.trained = False

        # distance in percentage, taking into account bounds
        self.distance_threshold = 0.0

        # stats
        self.eval_counter = 0
        self.predict_counter = 0
        self.eval_stats = True

    def add_data(self, x, y):
        self.x_data.append(x)
        self.y_data.append(y)

    def read_from_data_store(self):
        for individual in self.problem.individuals:
            self.add_data(individual.vector, individual.costs)

    def init_default_regressor(self):
        pass

    @abstractmethod
    def train(self):
        pass

    @abstractmethod
    def predict(self, x, *args):
        pass

    def compute(self, individual):
        return self.problem.evaluate(individual)

    def compute_distance(self, x):
        # n = len(limits)
        dist = float("inf")
        for vector in self.x_data:
            dist_tmp = 0.0
            for k in range(len(vector)):
                d = 100.0 * abs(vector[k] - x[k]) / (self.problem.parameters[k]['bounds'][1] - self.problem.parameters[k]['bounds'][0])
                # print("parameter {0:11.6e}, \tvector {1:11.6e}, \tdiff {2:11.6e}, \tlimits {3:11.6e} \tdistance: {4:4.1f} %".format(x[k], vector[k], abs(x[k] - vector[k]), self.problem.parameters[k]['bounds'][1] - self.problem.parameters[k]['bounds'][0], d))
                # dist_tmp = dist_tmp + d
                dist_tmp = max(dist_tmp, d)
            # average value
            # dist_tmp = dist_tmp / n

            if dist_tmp < dist:
                vec = vector
            dist = min(dist, dist_tmp)

            # for k in range(len(vector)):
            #     d = 100.0 * math.fabs(vec[k] - x[k]) / (self.problem.parameters[k]['bounds'][1] - self.problem.parameters[k]['bounds'][0])
            #     print("parameter {0:11.6e}, \tvector {1:11.6e}, \tdiff {2:11.6e}, \tlimits {3:11.6e} \tdistance: {4:4.1f} %".format(x[k], vector[k], abs(x[k] - vector[k]), self.problem.parameters[k]['bounds'][1] - self.problem.parameters[k]['bounds'][0], d))
            # print("dist min: {0:4.1f} %".format(dist))

        return dist

    @abstractmethod
    def evaluate(self, individual):
        pass


class SurrogateModelPredict(SurrogateModel):
    def __init__(self, problem):
        super().__init__(problem)

    def evaluate(self, individual):
        values = None
        if self.trained and "predict" in dir(self.problem):
            values = self.problem.predict(individual)
            if values is not None:
                # count prediction
                self.problem.surrogate.predict_counter += 1

        if values is None:
            # evaluate model
            values = self.evaluate_individual(individual)

        # self.problem.logger.info("surrogate: predict / eval counter: {0:5.0f} / {1:5.0f}, total: {2:5.0f}".format(
        #     self.problem.surrogate.predict_counter,
        #     self.problem.surrogate.eval_counter,
        #     self.problem.surrogate.predict_counter + self.problem.surrogate.eval_counter))

        return values

    def evaluate_individual(self, individual):
        # evaluate problem
        value = self.problem.evaluate(individual)
        # increase counter
        self.eval_counter += 1
        # add training date to surrogate model
        self.add_data(individual.vector, value)

        if self.train_step != -1:
            if self.eval_counter % self.train_step == 0:
                # init default regressor
                if self.regressor is None:
                    self.init_default_regressor()

                # train model
                self.train()
        return value


class SurrogateModelEval(SurrogateModel):
    def __init__(self, problem):
        super().__init__(problem)
        self.trained = True

    def train(self):
        pass

    def predict(self, individual):
        return self.problem.evaluate(individual)

    def evaluate(self, individual):
        self.eval_counter += 1
        return self.problem.evaluate(individual)

