# Import signature tool to check if functions start with self or ga
from inspect import signature

# Import math for square root (ga.dist()) and ceil (crossover methods)
import math

import random
import sqlite3
from copy import deepcopy

# Import all the data structure prebuilt modules
from structure import Population as make_population
from structure import Chromosome as make_chromosome
from structure import Gene       as make_gene

# Misc. Methods
from examples import Fitness
from termination import Termination

# Parent/Survivor Selection Methods
from parent   import Parent
from survivor import Survivor

# Genetic Operator Methods
from crossover import Crossover
from mutation  import Mutation

# Database class
from database import sql_database
from sqlite3  import Error

# Graphing package
from database import matplotlib_graph
import matplotlib.pyplot as plt


class Attributes:
    """Default GA attributes can be found here. If any attributes have not
    been set then they will fall back onto the default attribute. All
    attributes have been catigorized to explain sections in the ga process."""

    #=====================#
    # Default GA methods: #
    #=====================#

    # Default EasyGA implimentation structure
    fitness_function_impl = Fitness.is_it_5
    make_population = make_population
    make_chromosome = make_chromosome
    make_gene = make_gene

    # Methods for accomplishing Parent-Selection -> Crossover -> Survivor_Selection -> Mutation -> Termination
    parent_selection_impl = Parent.Rank.tournament
    crossover_individual_impl = Crossover.Individual.single_point
    crossover_population_impl = Crossover.Population.sequential
    survivor_selection_impl = Survivor.fill_in_best
    mutation_individual_impl = Mutation.Individual.individual_genes
    mutation_population_impl = Mutation.Population.random_avoid_best
    termination_impl = Termination.fitness_generation_tolerance


    def dist(self, chromosome_1, chromosome_2):
        """Default distance lambda. Returns the square root of the difference in fitnesses."""
        return math.sqrt(abs(chromosome_1.fitness - chromosome_2.fitness))


    def weighted_random(self, weight):
        """Returns a random value between 0 and 1. Returns values between the weight and the
        nearest of 0 and 1 less frequently than between weight and the farthest of 0 and 1."""

        rand_num = random.random()
        if rand_num < weight:
            return (1-weight) * rand_num / weight
        else:
            return 1 - weight * (1-rand_num) / (1-weight)


    def gene_impl(self, *args, **kwargs):
        """Default gene implementation. Returns a random integer from 1 to 10."""
        return random.randint(1, 10)


    chromosome_impl = None


    #=====================================#
    # Special built-in class __methods__: #
    #=====================================#

    def __init__(
            self,
            *,
            # Attributes must be passed in using kwargs

            run  = 0,

            chromosome_length = 10,
            population_size = 10,
            population = None,
            target_fitness_type = 'max',
            update_fitness = False,

            parent_ratio = 0.10,
            selection_probability = 0.50,
            tournament_size_ratio = 0.10,

            current_generation = 0,
            current_fitness = 0,

            generation_goal   = 100,
            fitness_goal      = None,
            tolerance_goal    = None,
            percent_converged = 0.50,

            chromosome_mutation_rate = 0.15,
            gene_mutation_rate = 0.05,

            adapt_rate = 0.05,
            adapt_probability_rate = 0.05,
            adapt_population_flag  = True,

            max_selection_probability = 0.75,
            min_selection_probability = 0.25,
            max_chromosome_mutation_rate = None,
            min_chromosome_mutation_rate = None,
            max_gene_mutation_rate = 0.15,
            min_gene_mutation_rate = 0.01,

            Database = sql_database.SQL_Database,
            database_name = 'database.db',
            sql_create_data_structure = f"""
            CREATE TABLE IF NOT EXISTS data (
            id INTEGER PRIMARY KEY,
            config_id INTEGER DEFAULT NULL,
            generation INTEGER NOT NULL,
            fitness REAL,
            chromosome TEXT
            ); """,

            Graph = matplotlib_graph.Matplotlib_Graph,

            **kwargs
        ):

        # Keep track of the current run
        self.run = run

        # Initilization variables
        self.chromosome_length = chromosome_length
        self.population_size = population_size
        self.population = population
        self.target_fitness_type = target_fitness_type
        self.update_fitness = update_fitness

        # Selection variables
        self.parent_ratio = parent_ratio
        self.selection_probability = selection_probability
        self.tournament_size_ratio = tournament_size_ratio

        # Termination variables
        self.current_generation = current_generation
        self.current_fitness = current_fitness
        self.generation_goal = generation_goal
        self.fitness_goal = fitness_goal
        self.tolerance_goal = tolerance_goal
        self.percent_converged = percent_converged

        # Mutation variables
        self.chromosome_mutation_rate = chromosome_mutation_rate
        self.gene_mutation_rate = gene_mutation_rate

        # Adapt variables
        self.adapt_rate = adapt_rate
        self.adapt_probability_rate = adapt_probability_rate
        self.adapt_population_flag  = adapt_population_flag

        # Bounds on probabilities when adapting
        self.max_selection_probability = max_selection_probability
        self.min_selection_probability = min_selection_probability
        self.max_chromosome_mutation_rate = max_chromosome_mutation_rate
        self.min_chromosome_mutation_rate = min_chromosome_mutation_rate
        self.max_gene_mutation_rate = max_gene_mutation_rate
        self.min_gene_mutation_rate = min_gene_mutation_rate

        # Database varibles
        self.database = Database()
        self.database_name = database_name
        self.sql_create_data_structure = sql_create_data_structure

        # Graphing variables
        self.graph = Graph(self.database)

        # Any other custom kwargs?
        for name, value in kwargs.items():
            self.__setattr__(name, value)


    def __setattr__(self, name, value):
        """Custom setter for using

            self.name = value

        which follows the following guidelines:
        - if self.name is a property, the specific property setter is used
        - else if value is callable and the first parameter is either 'self' or 'ga', self is passed in as the first parameter
        - else if value is not None or self.name is not set, assign it like normal
        """

        # Check for property
        if hasattr(type(self), name) and isinstance(getattr(type(self), name), property):
            getattr(type(self), name).fset(self, value)

        # Check for function
        elif callable(value) and next(iter(signature(value).parameters), None) in ('self', 'ga'):
            foo = lambda *args, **kwargs: value(self, *args, **kwargs)
            # Reassign name and doc-string for documentation
            foo.__name__ = value.__name__
            foo.__doc__  = value.__doc__
            self.__dict__[name] = foo

        # Assign like normal unless None or undefined self.name
        elif value is not None or not hasattr(self, name):
            self.__dict__[name] = value


    #============================#
    # Built-in database methods: #
    #============================#


    def save_population(self):
        """Saves the current population to the database."""
        self.database.insert_current_population(self)


    def save_chromosome(self, chromosome):
        """Saves the given chromosome to the database."""
        self.database.insert_current_chromosome(self.current_generation, chromosome)


    #===================#
    # Built-in options: #
    #===================#


    def numeric_chromosomes(self):
        """Sets default numerical based methods"""

        # Adapt every 10th generation
        self.adapt_rate = 0.10

        # Use averaging for crossover
        self.crossover_individual_impl = Crossover.Individual.Arithmetic.average

        # Use averaging for mutation
        self.mutation_individual_impl = Mutation.Individual.individual_genes

        # Euclidean norm
        self.dist = lambda self, chromosome_1, chromosome_2:\
            math.sqrt(sum(
                (gene_1.value - gene_2.value) ** 2
                for gene_1, gene_2
                in zip(chromosome_1, chromosome_2)
            ))


    def permutation_chromosomes(self, cycle = True):
        """Sets default permutation based methods"""

        cycle = int(cycle)

        self.crossover_individual_impl = Crossover.Individual.Permutation.ox1
        self.mutation_individual_impl  = Mutation.Individual.Permutation.swap_genes

        def dist(self, chromosome_1, chromosome_2):
            """Count the number of gene pairs they don't have in common."""

            return sum(
                1
                for x, y
                in zip(chromosome_1, chromosome_2)
                if x != y
            )

        self.dist = dist


    #===========================#
    # Getter/setter properties: #
    #===========================#


    @property
    def run(self):
        """Getter function for the run counter."""
        return self._run


    @run.setter
    def run(self, value):
        """Setter function for the run counter."""
        if not(isinstance(value, int) and value >= 0):
            raise ValueError("ga.run counter must be an integer greater than or equal to 0.")
        self._run = value


    @property
    def current_generation(self):
        """Getter function for the current generation."""
        return self._current_generation


    @current_generation.setter
    def current_generation(self, generation):
        """Setter function for the current generation."""

        if not isinstance(generation, int) or generation < 0:
            raise ValueError("ga.current_generation must be an integer greater than or equal to 0")

        self._current_generation = generation


    @property
    def chromosome_length(self):
        """Getter function for chromosome length"""
        return self._chromosome_length


    @chromosome_length.setter
    def chromosome_length(self, length):
        """Setter function with error checking for chromosome length"""

        if(not isinstance(length, int) or length <= 0):
            raise ValueError("Chromosome length must be integer greater than 0")

        self._chromosome_length = length


    @property
    def population_size(self):
        """Getter function for population size"""

        return self._population_size


    @population_size.setter
    def population_size(self, size):
        """Setter function with error checking for population size"""

        if(not isinstance(size, int) or size <= 0):
            raise ValueError("Population size must be integer greater than 0")

        self._population_size = size


    @property
    def target_fitness_type(self):
        """Getter function for target fitness type."""

        return self._target_fitness_type


    @target_fitness_type.setter
    def target_fitness_type(self, target_fitness_type):
        """Setter function for target fitness type."""

        self._target_fitness_type = target_fitness_type


    @property
    def max_chromosome_mutation_rate(self):
        """Getter function for max chromosome mutation rate"""

        return self._max_chromosome_mutation_rate


    @max_chromosome_mutation_rate.setter
    def max_chromosome_mutation_rate(self, rate):
        """Setter function with error checking and default value for max chromosome mutation rate"""

        # Default value
        if rate is None:
            self._max_chromosome_mutation_rate = min(self.chromosome_mutation_rate*2, (1+self.chromosome_mutation_rate)/2)

        # Otherwise check value
        elif 0 <= rate <= 1:
            self._max_chromosome_mutation_rate = rate

        # Throw error
        else:
            raise ValueError("Max chromosome mutation rate must be between 0 and 1")


    @property
    def min_chromosome_mutation_rate(self):
        """Getter function for min chromosome mutation rate"""

        return self._min_chromosome_mutation_rate


    @min_chromosome_mutation_rate.setter
    def min_chromosome_mutation_rate(self, rate):
        """Setter function with error checking and default value for min chromosome mutation rate"""

        # Default value
        if rate is None:
            self._min_chromosome_mutation_rate = self.chromosome_mutation_rate/2

        # Otherwise check value
        elif 0 <= rate <= 1:
            self._min_chromosome_mutation_rate = rate

        # Throw error
        else:
            raise ValueError("Min chromosome mutation rate must be between 0 and 1")


    @property
    def database_name(self):
        """Getter function for the database name"""

        return self._database_name


    @database_name.setter
    def database_name(self, value_input):
        """Setter function with error checking for the database name"""

        # Update the database class of the name change
        self.database._database_name = value_input

        # Set the name in the ga attribute
        self._database_name = value_input
