import re
from math import log
from time import sleep
import subprocess
from io import StringIO
from pathlib import Path
from operator import itemgetter
from Bio import SearchIO, SeqIO
from Bio.Blast import NCBIXML, NCBIWWW
from RecBlast import print, merge_ranges
from RecBlast.WarningsExceptions import *

class Search(object):
    def __init__(self, search_type):
        self.search_type = search_type

    def __call__(self, seq_record, species, database, database_path, local,
                 indent, perc_ident, verbose, database_port=None,
                 expect=None, megablast=True, n_threads=1, write=False, filetype=None,
                 **kwargs):
        # query_length = len(seq_record)
        if isinstance(database, Path):
            return self.load(database)
        elif isinstance(database, str) and database != 'stop':
            return self.load(Path(database))
        elif database == 'stop':
            raise StopRecBlast()
        elif self.search_type in ["blastn", "blastp", "blastx", "tblastx", "tblastn"]:
            if verbose > 1:
                print(self.search_type, 'was selected.', indent=indent)
            dt = self.blast_prep(search_type=self.search_type, db_loc=database_path, database=database,
                                 species=species, verbose=verbose, indent=indent)
            return self.blast_run(seq_record=seq_record, species=species, database=dt.name, filetype=filetype,
                                  blast_type=self.search_type, local_blast=local, expect=expect, megablast=megablast,
                                  use_index=False, perc_ident=perc_ident, verbose=verbose, indent=indent,
                                  n_threads=n_threads, blastdb=database_path, outtype=5, return_raw=False,
                                  **kwargs)
        elif self.search_type in ['blat', 'tblat', 'blat-transcript', 'tblat-transcript']:
            if verbose > 1:
                print(self.search_type, 'was selected.', indent=indent)
            port = self.blat_prep(database_port=database_port, species=species, verbose=verbose, indent=indent)
            return self.blat_run(seq_record=seq_record, local=local, port=port,
                                 filetype=filetype, blat_type=self.search_type, perc_ident=perc_ident,
                                 verbose=verbose, indent=indent, blatdb=database_path, outtype='pslx')
        else:
            raise SearchEngineNotImplementedError('Invalid selection for search type!')

    @staticmethod
    def blast_run(seq_record, species, database, blast_type, filetype="fasta",
                  local_blast=False, expect=0.005, megablast=True, use_index=False, perc_ident=75,
                  verbose=True, indent=0, n_threads=1, blastdb='/usr/db/blastdb/', outtype=5,
                  return_raw=False, **kwargs):
        """A wrapper function for BLAST searches.
        :param seq_record: The record containing the query sequence for the search. Can be either a SeqIO.SeqRecord or
                           a string with the file loaction.
        :param str species: The species whose sequence database will be queried.
        :param Union[dict, str, Path] database: The name of the database to be used in the search.
        :param str blast_type: Type of BLAST search being performed
        :param str filetype: Filetype of seq_record (if seq_record is a SeqRecord object, leave as default.
                             [default: 'fasta']
        :param bool local_blast: Should the search be conducted locally or on remote servers? (BLAT searches are always
                                 local.) [Default: False]
        :param float expect: Highest expect value of BLAST results to be returned. [Default: 0.005]
        :param bool megablast: Should MegaBLAST be used for nucleotide searches? [Default: True]
        :param bool use_index: Should BLAST use indexes associated with the database files? [Default: False]
        :param int perc_ident: Minimum percent identity required of results to be returned [Default: 75]
        :param bool verbose: Verbose output? [Default: True]
        :param int indent: Indent level for pretty print. [Default: 0]
        :param int n_threads: Number of threads to allocate for BLAST [Default: 1]
        :param str blastdb: Path of databases for either BLAST or BLAT. [Default: '/usr/db/blastdb'
        :param int outtype: Output type. (see options for BLAST and BLAT) [Default: pslx]
        :param bool return_raw: Return raw output rather than processed BioBlastRecord? [Default: False]
        :param kwargs: Additional keyword arguments to pass on to BLAST/BLAT.
        :return: blast_record, blast_err
        """

        if isinstance(seq_record, SeqIO.SeqRecord):
            pass
        else:
            seq_record = SeqIO.read(seq_record, filetype)
        args = dict()
        if verbose:
            print("Now starting BLAST...", indent=indent)
        if local_blast:
            # build up the BLAST arguments:
            args.update({'-db': str(database), '-evalue': expect,
                         '-outfmt': str(outtype),
                         '-num_threads': n_threads})
            if blast_type == 'blastn':
                if megablast:
                    args['-task'] = 'megablast'
                if use_index:
                    args['-use_index'] = use_index
                args['-perc_identity'] = perc_ident
            args_expanded = list()
            [(args_expanded.append(j), args_expanded.append(k)) for j, k in args.items()]
            if verbose:
                print('Running BLAST locally...', indent=indent)
                print('Options:', indent=indent)
                print(args_expanded, indent=indent + 1)
            if blast_type in ["blastn", "blastp", "blastx", "tblastx", "tblastn"]:
                blast_cline = [blast_type] + args_expanded
                try:
                    blast_handle = subprocess.check_output([str(i) for i in blast_cline],
                                                           input=seq_record.format('fasta'),
                                                           universal_newlines=True, cwd=blastdb)
                    if isinstance(blast_handle, str):
                        blast_result = blast_handle
                        blast_err = None
                    else:
                        blast_result, blast_err = blast_handle

                except subprocess.CalledProcessError:
                    raise
            else:
                raise SearchError("Invalid blast choice!")
        else:
            args.update(dict(program=str(blast_type), database=str(database), sequence=seq_record.format('fasta'),
                             entrez_query='"{}"[ORGN]'.format(species), expect=expect, perc_ident=perc_ident))
            if megablast & (blast_type == 'blastn'):
                args['megablast'] = 'True'
            if kwargs:
                args.update(**kwargs)
            if verbose:
                print('Submitting Remote BLAST! Options passed:', indent=indent)
                for k, v in args.items():
                    print('{0}\t=\t{1}'.format(k, v), indent=indent + 1)
            try:
                blast_result = NCBIWWW.qblast(**args)
                blast_err = None
            except Exception as err:
                print(type(err), err)
                raise err

        if verbose:
            print('Done with Blast!', indent=indent)
        if return_raw:
            return blast_result, blast_err
        else:
            if isinstance(blast_result, StringIO):
                blast_record = NCBIXML.read(blast_result)
            else:
                try:
                    with StringIO(''.join(blast_result)) as fin:
                        blast_record = NCBIXML.read(fin)
                except Exception as err:
                    print('Error reading Blast Results! Aborting!', indent=indent)
                    print('Error details:\n', err, indent=indent)
                    raise err
            return blast_record, blast_err

    @staticmethod
    def blat_run(seq_record, port, local="localhost", filetype="fasta", blat_type='blat', perc_ident=None,
                 verbose=True, indent=0, blatdb='/usr/db/blastdb/', outtype='pslx'):
        """A wrapper function for BLAT searches.
        :param seq_record: The record containing the query sequence for the search. Can be either a SeqIO.SeqRecord or
                           a string with the file loaction.
        :param int port: Port of the gfServer to be queried
        :param str local: Host address.
        :param str filetype: Filetype of seq_record (if seq_record is a SeqRecord object, leave as default.
                             [default: 'fasta']
        :param str blat_type: Type of search to conduct. Can be a BLAST type (blastn, blastp, blastx, tblastn, tblastx)
                              or a BLAT type (blat, tblat). [Default: 'blastn']
        :param int perc_ident: Minimum percent identity required of results to be returned [Default: 75]
        :param bool verbose: Verbose output? [Default: True]
        :param int indent: Indent level for pretty print. [Default: 0]
        :param str blatdb: Path of databases for either BLAST or BLAT. [Default: '/usr/db/blastdb'
        :param str outtype: Output type. (see options for BLAST and BLAT) [Default: pslx]
        :return: blat_record, blat_err
        """
        if isinstance(seq_record, SeqIO.SeqRecord):
            pass
        elif isinstance(seq_record, str):
            seq_record = SeqIO.read(seq_record, filetype)
        else:
            raise TypeError('seq_record was of type {}, must be either '
                            'a str with filepath or a SeqRecord object!'.format(type(seq_record)))

        if verbose:
            print("Now starting BLAT...", indent=indent)
        if verbose > 1:
            print('Search Type: ', blat_type, indent=indent)

        args_expanded = ['gfClient', local, str(port), '/', '/dev/stdin', '/dev/stdout']
        args_expanded += ['-t=dnax', '-q=prot'] if blat_type.lower() == 'tblat' else []
        args_expanded += ['minIdentity={}'.format(perc_ident if perc_ident else 0),
                          '-out={}'.format(outtype)]

        try:
            if verbose > 1:
                print('BLAT command:', indent=indent)
                print(' '.join(args_expanded), indent=indent + 1)

            blat = subprocess.Popen(args_expanded, stdout=subprocess.PIPE,
                                    universal_newlines=True, cwd=blatdb,
                                    stdin=subprocess.PIPE, stderr=subprocess.PIPE)

            blat_raw, blat_raw_err = blat.communicate(input=seq_record.format('fasta'))
            if blat_raw_err:
                raise SearchError(blat_raw_err)

            head = subprocess.Popen(["head", "-n", "-1"], universal_newlines=True, stdin=subprocess.PIPE,
                                    stdout=subprocess.PIPE)

            blat_handle = head.communicate(input=blat_raw)
            if verbose > 2:
                print(blat_handle[0], indent=indent)
            if verbose:
                print('Done!', indent=indent)
            if isinstance(blat_handle, str):
                blat_result = blat_handle
                blat_err = None
            else:
                blat_result, blat_err = blat_handle
        except subprocess.CalledProcessError:
            raise
        blat_result, blast_err = blat_result, blat_err
        blat_record = None
        with StringIO(blat_result) as fin:
            try:
                if outtype == 'pslx':
                    blat_record = SearchIO.read(fin, format='blat-psl', pslx=True)
                elif outtype == 'psl':
                    blat_record = SearchIO.read(fin, format='blat-psl')
                elif outtype == 'blast8':
                    blat_record = SearchIO.read(fin, format='blast-tab')
                elif outtype == 'blast9':
                    blat_record = SearchIO.read(fin, format='blast-tab', comments=True)
                elif outtype == 'blast':
                    blat_record = SearchIO.read(fin, format='blast-xml')
                else:
                    raise SearchError('Invalid out type')
            except ValueError:
                if verbose:
                    print('No Query Results were found in handle for seq_record {}!'.format(seq_record.id))
                raise NoHitsError('No Query Results were found in handle for seq_record {}!'.format(seq_record.id))
            except Exception as err:
                print('Error reading BLAT results! Aborting!')
                print('Error details:\n')
                raise err
        return blat_record, blat_err

    @staticmethod
    def blat_prep(database_port, species, verbose, indent):
        if isinstance(database_port, dict):
            try:
                blat_port = database_port[species]
                if verbose > 1:
                    print('Using port {0} for gfServer of species {1}.'.format(blat_port, species), indent=indent)
            except KeyError:
                raise SearchError('No 2bit found for species {}!'.format(species))
        elif isinstance(database_port, int):
            blat_port = database_port
        elif isinstance(database_port, str):
            try:
                blat_port = int(database_port)
            except ValueError:
                raise SearchError('Invalid option "{}" was passed to database_port! database_port must be '
                                  'either a dictionary of species-port pairs or an integer!'.format(database_port))
        else:
            raise SearchError('Invalid option of type "{}" was passed to database_port! database_port must be '
                              'either a dictionary of species-port pairs or an '
                              'integer!'.format(str(type(database_port))))
        return blat_port

    @staticmethod
    def blast_prep(search_type, database, species, verbose, indent, db_loc):
        if database == 'auto' or database == 'auto-transcript':
            if verbose > 1:
                print('Blast type set to auto!', indent=indent)
            try:
                blast_db = get_searchdb(search_type=search_type, species=species, db_loc=db_loc,
                                        verbose=verbose, indent=indent + 1)
            except Exception:
                raise SearchError('No BLAST database was found for species {}!'.format(species))
        elif isinstance(database, dict):
            try:
                blast_db = database[species]
                if verbose > 1:
                    print('Using {} as BLAST database!'.format(blast_db), indent=indent)
            except KeyError:
                raise SearchError('No BLAST database was found for species {}!'.format(species))
        elif isinstance(database, str) or isinstance(database, Path):
            blast_db = database
        else:
            raise SearchError('Invalid type given for database!')
        return blast_db

    @staticmethod
    def load(database):
        try:
            if database.exists() and database.is_file():
                rec = None
                with database.open('r') as forward_blasthits:
                    if database.suffix == '.psl':
                        rec = SearchIO.read(forward_blasthits, 'blat-psl')
                    elif database.suffix == '.pslx':
                        rec = SearchIO.read(forward_blasthits, 'blat-psl', pslx=True)
                    elif database.suffix == '.xml':
                        rec = SearchIO.read(forward_blasthits, 'blast-xml')
                    else:
                        raise SearchError('Database file "{}" could not be loaded - '
                                          'Must be either a PSL, PSLX, or BLAST-XML file!'.format(str(database)))
            else:
                raise FileNotFoundError()
        except FileNotFoundError:
            raise SearchError('Database file "{}" was not found!'.format(str(database)))
        return rec

def id_search(id_rec, id_type='brute', verbose=2, indent=0, custom_regex=None, regex_only=False):
    """

    EX:
    gi =
    refseq_accession = 'XP_010883249.1'
    scaffold = 'scaffold_145\t[:1033526-1034566](-)\t190
    id =
    chr = 'chrX[:3047971-3259961](-)119'
    seq_range =
    assembly1 = 'KN678312.1	[:9787-29116](+)	478'
    assembly2 = 'KN678312.1	[:9787-29116](+)	478'
    symbol = 'TP53'
    symbol = 'INS [:259-568](+) (161)'
    strand = '+'

    :param id_rec:
    :param id_type:
    :param custom_regex:
    :param regex_only:
    :param verbose:
    :param indent:
    :return:
    """
    # Define the regex functions
    p = dict(gi=re.compile('(\Agi[| _:]+[0-9.]+)'
                           '([| \t:_])?\[?(:?\d+-?\d+)?\]?([| \t:_])?(.*)'),
             accession=re.compile('(\A[AXNYZ][MWRCPGTZ][| _:]+[0-9.]+|\Aref[| _:]+[0-9.]+)'
                                  '([| \t:_])?\[?(:?\d+-?\d+)?\]?([| \t:_])?(.*)'),
             scaffold=re.compile('(\Ascaffold[| _:]+[0-9.]+)'
                                 '([| \t:_])?\[?(:?\d+-?\d+)?\]?([| \t:_])?(.*)'),
             id=re.compile('(\Aid[| _:]*[0-9.]+)'
                           '([| \t:_])?\[?(:?\d+-?\d+)?\]?([| \t:_])?(.*)'),
             chr=re.compile('(\Achr[| _:]*[A-Za-z0-9.]+)'
                            '([| \t:_])??\[?(:?\d+-?\d+)?\]?([| \t:_])?(.*)'),
             assembly=re.compile('(\A[A-Za-z]+[0-9.]+)'
                                 '([| \t:_])?\[?(:?\d+-?\d+)?\]?([| \t:_])?(.*)'),
             assembly_broad=re.compile('(\b[ALYB]+[0-9.]+)'
                                       '([| \t:_])?\[?(:?\d+-?\d+)?\]?([| \t:_])?(.*)'),
             symbol=re.compile('(\A\S+)([| \t:_])?\[?(:?\d+-?\d+)?\]?([| \t:_])?(.*)'),
             seq_range=re.compile(':?(\d+)-(\d+)'),
             strand=re.compile('(\([-+0N]\))'),
             score=re.compile('\d\d*')
             )
    if custom_regex is not None:
        p = {'custom': custom_regex}
        id_type = 'custom'

    # Begin search:
    if verbose > 1:
        print('ID Loaded, performing regex search for identifiers...', indent=indent)
        print('ID type: ', id_type, indent=indent)
    if id_type == 'brute':
        for tmp_type in ['accession', 'gi', 'scaffold', 'id', 'chr', 'assembly', 'assembly_broad', 'symbol']:
            if bool(p[tmp_type].findall(id_rec)):
                if verbose > 1:
                    print('Brute Force was set, tested strings for all pre-registered IDs.', indent=indent)
                    print('ID was selected as type {0}!'.format(tmp_type), indent=indent + 1)
                if regex_only:
                    return p[tmp_type]
                else:
                    return id_search(id_rec=id_rec, id_type=tmp_type, verbose=verbose, indent=indent)
        raise IDError('Couldn\'t identify the id type of line: {}!'.format(id_rec))
    else:
        try:
            item_parts = p[id_type].findall(id_rec)[0]
            if verbose > 1:
                print('Successfully found {0}, compiling list!'.format(id_type), indent=indent)
                print('Item:\t', '\t'.join(item_parts), indent=indent + 1)
        except IndexError:
            raise IDError('Could not identify patterns in {0} with id_type={1}, '
                          'is the id_search sequence correct?'.format(id_rec, id_type))
        try:

            item_parts = list(item_parts)
            item_parts[0] = item_parts[0] if not isinstance(item_parts[0], str) else ''.join(item_parts[0])

            if item_parts[2]:
                try:
                    sr_tuple = p['seq_range'].findall(item_parts[2])[0]
                    if verbose > 1:
                        print('Found sequence delimiters in IDs!', indent=indent)
                        print(sr_tuple, indent=indent + 1)
                except IndexError:
                    raise IDError('A positive match for a sequence range was found '
                                  '({0}), yet no hits were identified! Confirm that '
                                  'the regex is correct and try again!'.format(item_parts[2]))
            else:
                sr_tuple = (0, -1)
            if item_parts[4]:
                try:
                    strand = p['strand'].findall(item_parts[4])[0]
                except IndexError:
                    strand = '(N)'
                try:
                    score = p['score'].findall(item_parts[4])[0]
                except IndexError:
                    score = 0
            else:
                strand = '(N)'
                score = '0'
            if verbose > 1:
                if strand != '(N)':
                    print('Strand info found: {0}'.format(strand), indent=indent)
                if score != '0':
                    print('Score info found: {0}'.format(score), indent=indent)

            seq_range = (int(sr_tuple[0]), int(sr_tuple[1]), strand, int(score))
            return p, item_parts[0], seq_range, id_type

        except IndexError:
            raise IDError('Could not identify patterns in {0} with id_type={1}, '
                          'is the id_search sequence correct?'.format(id_rec, id_type))

def percent_identity_searchio(hit, is_protein=True):
    """Calculates percent identity based on entire hit. Adapted from UCSC BLAT FAQ and Biopython."""

    size_mul = 3 if is_protein else 1
    qali_size = size_mul * sum([i[-1] - i[0] for i in merge_ranges([(hsp.query_start, hsp.query_end) for hsp in hit])])
    tali_size = sum([i[-1] - i[0] for i in merge_ranges([(hsp.hit_start, hsp.hit_end) for hsp in hit])])
    ali_size = min(qali_size, tali_size)
    if ali_size <= 0:
        return 0
    size_dif = qali_size - tali_size
    size_dif = 0 if size_dif < 0 else size_dif
    sum_match = sum([i.match_num for i in hit])
    sum_rep = sum([i.match_rep_num for i in hit])
    sum_mismatch = sum([i.mismatch_num for i in hit])
    total = size_mul * (sum_match + sum_rep + sum_mismatch)
    if total != 0:
        millibad = (1000 * (sum([i.mismatch_num for i in hit]) * size_mul + sum([i.query_gap_num for i in hit]) +
                            round(3 * log(1 + size_dif)))) / total
    else:
        raise Exception('Somehow your total in the percent_identity function was 0, so you broke the script!')
    perc_ident = 100 - (millibad * 0.1)
    return perc_ident

def get_searchdb(search_type, species, db_loc, verbose=1, indent=0):
    """Finds and returns the appropriate search database for the given species and search type.

    This function automates the process of selecting the search database needed by the selected search program,
    like BLAST or BLAT, so that the user does not need to preoccupy themselves with providing said information
    for a large number of species. For BLAST* that depend on protein databases (BLASTP and BLASTX), the function
    searches for files matching the form 'Genus_species_protein.*' in the given directory; for BLAST* that depend
    on DNA databases (BLASTN, TBLASTN, and TBLASTX), it instead looks for files 'Genus_species_genome.*'.
    If '-transcript' is added to the end of any of the DNA-dependent BLAST*, then instead the function will
    search for files in the style of 'Genus_species_transcript.*'. In the case of BLAT searches, the program will
    similarly search for 'Genus_species*.2bit', or for 'Genus_species*transcript.2bit' if '-transcript' is added
    after the search type.
    In all usage cases, if the program does not find files matching the 'Genus_species' format, it will try to
    find the files using a case-insensitive search using the 6-letter abbreviated form of the species name.

    Usage::
    >>> get_searchdb('blastp', 'Homo sapiens', '/path/to/search/files')
    /path/to/search/files/Homo_Sapiens_protein.*
    >>> get_searchdb('tblastn', 'Homo sapiens', '/path/to/search/files')
    /path/to/search/files/HomSap_genome.*
    >>> get_searchdb('blastn-transcript', 'Homo sapiens', '/path/to/search/files')
    /path/to/search/files/HomSap_transcript.*
    >>> get_searchdb('blat', 'Homo sapiens', '/path/to/search/files')
    /path/to/search/files/HomSap.2bit
    >>> get_searchdb('blat-transcript', 'Homo sapiens', '/path/to/search/files')
    /path/to/search/files/HomSap_transcript.2bit

    Arguments::
    :param str search_type: The name of the search method (blast or blat, and sub-type: blastp, blastn, blat, tblat...)
    :param str species: Name of species associated with the database. If there is a space, it will be replaced with an
    underscore.
    :param str db_loc: Path to folder containing collection of search databases.
    :param int verbose: How verbose should the output be. Zero suppresses all output, 2 is max verbosity.
    :param int indent: Indent level for printed output.
    :return str:  Path to the identified search database.
    """
    if verbose:
        print('Search DB set to auto, choosing search_db...', indent=indent)
    species = species.replace(' ', '_')
    if verbose > 1:
        print('Search DB location set to: ', db_loc, indent=indent)
    db_type_dict = {
        'blastx': "protein",
        'blastp': "protein",
        'blastn': "genome",
        'tblastn': "genome",
        'tblastx': "genome",
        'blastn-transcript': "transcript",
        'tblastn-transcript': "transcript",
        'tblastx-transcript': "transcript",
        'blat': "blat",
        'tblat': "blat",
        'blat-transcript': 'blat-transcript',
        'tblat-transcript': 'tblat-transcript'
    }
    try:
        db_type = db_type_dict[search_type]
    except KeyError:
        print('Unable to determine search db type!', indent=indent)
        raise SearchError('Improper search type given ({})!'.format(search_type))
    if verbose > 1:
        print('DB type: ', db_type, indent=indent)
    db_path = Path(db_loc).absolute()
    if not db_path.exists():
        db_path = Path(db_loc)
    if db_path.exists() and db_path.is_dir():
        if db_type == 'blat':
            glob_path = [i for i in
                         db_path.glob('{0}*.2bit'.format(species.replace(' ', '_')))]  # Todo: generalize extension
        elif db_type in ['blat-transcript', 'tblat-transcript']:
            glob_path = [i for i in db_path.glob('{0}*transcript.2bit'.format(species.replace(' ', '_')))]
        else:
            glob_path = [i for i in db_path.glob('{0}_{1}*'.format(species.replace(' ', '_'), db_type))]
        if not glob_path:
            if verbose:
                print('No DB found! Trying again with abbreviated species name', indent=indent)
            species_abbv = ''.join([i[0:3] for i in species.title().split('_')])
            # making it insensitive to case for Glob
            species_abbv_insensitive = ''.join(['[{0}{1}]'.format(c.lower(),
                                                                  c.upper()) for c in species_abbv if c.isalpha()])
            if verbose:
                print('Abbreviated species name: ', species_abbv, indent=indent)
                print('RegEx species abbreviation: ', species_abbv_insensitive, indent=indent)
            if db_type == 'blat':
                glob_path = [i for i in db_path.glob('{0}*.2bit'.format(species_abbv_insensitive))]
            elif db_type in ['blat-transcript', 'tblat-transcript']:
                glob_path = [i for i in db_path.glob('{0}*transcript.2bit'.format(species_abbv_insensitive))]
            else:
                glob_path = [i for i in db_path.glob('{0}_{1}*'.format(species_abbv_insensitive, db_type))]
        try:
            if verbose:
                print(glob_path, indent=indent)
            if isinstance(glob_path, list):
                search_db = sorted(glob_path, reverse=True)[0]
            else:
                search_db = glob_path
        except IndexError:
            print('WARNING: COULD NOT FIND DATABASE! ABORTING!', indent=indent)
            raise DatabaseNotFoundError('', 'No databases were found!')
    else:
        raise DatabaseNotFoundError('DB_Path {} does not exist!'.format(str(db_path)))
    if verbose:
        print('{0} DB chosen: {1}'.format(search_type, str(search_db)), indent=indent)
    return search_db

def blat_server(twobit, order='start', host='localhost', port=20000, type='blat', log='/dev/null', species=None,
                search_db_loc='/usr/db/blat', verbose=1, indent=0, try_limit=10, **kwargs):
    """Convenience function that controls a gfServer. Still in alpha.

    This function serves as a python wrapper for the Bash gfServer command. The user can either provide a .2bit file,
    or else can provide a species and set 'twobit="auto"' to have the function use 'get_searchdb()' to find a .2bit file
    automatically. By default, the function is set to start up a new gfServer instance, but using the 'order' parameter,
    the user can execute any of the standard gfServer commands such as 'stop' and 'status'.
    To start a gfServer, the function first probes the selected port (default is 20000) to ensure its unused; if it is
    currently in use, the program then goes port-by-port in ascending order until it finds an empty port to use for the
    server. Then, it simply calls the gfServer command with all the keyword arguments required, as well as with any
    extra arguments provided by the user.

    Usage::
    >>>blat_server(twobit='hg38.2bit', port=20000, verbose=3)
    gfServer start localhost 20001 -canStop -stepSize=5 hg38.2bit
    # Waits 30 seconds, then starts calling 'gfServer status localhost 20001' every 30 seconds for 5 minutes
    # If at any point 'gfServer status' returns something that is not an error or "Couldn't connect...", it
    # returns the port where the server was opened.
    20001
    >>>blat_server(twobit='auto', port=20000, species='Homo sapiens', verbose=3)
    # Calls get_searchdb('blat', 'Homo sapiens', db_loc=BLATDB)
    # Internally, will return a .2bit file such as 'Homo_sapiens.2bit'
    20001
    >>>blat_server(twobit='hg38.2bit', port=20000, order='status', verbose=3)
    # If the server is active:
    1
    >>>blat_server(twobit='hg38.2bit', port=20000, order='status', verbose=3)
    # If the server either has not been started or is not yet active:
    0
    >>>blat_server(twobit='hg38.2bit', port=20000, order='status', verbose=3)
    # If the server returns an error
    Exception(...)

    :param str twobit: A path to the .2bit file to be used for the server. Can also be set to 'auto'.
    :param str order: A command for gfServer. Can be one of the following: start, stop, status, files, query (requires
    a nucleotide sequence in fasta format), protQuery (requires a protein sequence in fasta format), transQuery
    (requires a nucleotide sequence in fasta format), pcr (requires arguments fPrimer, rPrimer, maxDistance), direct
    (requires probe.fa, file(s).nib), or pcrDirect (requires fPrimer, rPrimer, file(s).nib).
    :param str host: Address at which to host the server.
    :param int port: Port number that will be assigned to server. If in use, will test new port number in increments of
    1 until a free port is found.
    :param str type: Type of server to be hosted. 'blat' will start a DNA server, 'tblat' will start a DNAX server for
    protein queries.
    :param str log: Path and name of log file to be written.
    :param str species: Species name that get_searchdb() will use to find .2bit file when twobit='auto'.
    :param str search_db_loc: Path to the folder containing .2bit file.
    :param int verbose: Level of verbosity of function output. 0 suppresses all output, 3 is max verbosity.
    :param int indent: Indentation level of print output.
    :param int try_limit: Number of tries at 30-second intervals that function should probe the gfServer before timeout.
    :param kwargs: keyword arguments to be passed on to gfServer.
    :return: if order='start', returns the port of the new gfServer; if order='status', returns 0 if there was no
    connection, or 1 if the server is active and responding.
    """
    # Regular: gfServer start localhost portX -stepSize=5 -log=untrans.log database.2bit
    # Prot>DNAX:  gfServer start localhost portY -trans -mask -log=trans.log database.2bit
    gfserver_suppl_args = list()
    if twobit == 'auto' and order != 'stop':
        if verbose:
            print('2bit set to auto: searching for 2bit file for species ', species, indent=indent)
        twobit = get_searchdb(search_type='blat', species=species, db_loc=search_db_loc,
                              verbose=verbose, indent=indent + 1)
        if twobit.exists() and twobit.is_file():
            twobit = twobit.name
        else:
            raise BLATServerError('Invalid 2bit file!')
    for key, item in kwargs.items():
        if key == 'order':
            order = item
        elif key == 'host':
            host = item
        elif key == 'port':
            port = item
        else:
            gfserver_suppl_args.append('-{0}={1}'.format(key, item))
    if order == 'status':
        gfcheck = subprocess.Popen('gfServer status {0} {1}'.format(str(host), str(port)), stdout=subprocess.PIPE,
                                   stderr=subprocess.STDOUT, universal_newlines=True, shell=True,
                                   executable='/bin/bash')
        out, _ = gfcheck.communicate()
        if "couldn't connect to localhost" in out.lower():
            return 0
        elif "error" in out.lower():
            raise BLATServerError(out)
        else:
            return 1
    elif order == 'stop':
        subprocess.check_call('gfServer stop {0} {1}'.format(str(host), str(port)), stdout=subprocess.PIPE,
                              stderr=subprocess.STDOUT, universal_newlines=True, shell=True,
                              executable='/bin/bash')
        return
    else:
        print(order)
        # Todo: make the portsniffer its own function and make sure it works properly.
        portfinder = subprocess.check_output('/home/manny/Scripts/oneshot/checkifportisopen.sh {}'.format(str(port)),
                                             universal_newlines=True, shell=True, executable='/bin/bash')
        port = portfinder.rstrip()

        gfserver_cmd = ['gfServer', str(order), str(host), str(port), '-canStop']
        if type == 'blat':
            gfserver_cmd.append('-stepSize=5')
        elif type == 'tblat':
            gfserver_cmd += ['-trans', '-mask']
        if gfserver_suppl_args:
            gfserver_cmd += gfserver_suppl_args
        gfserver_cmd_str = ' '.join(gfserver_cmd + [twobit])
        if verbose > 2:
            print(gfserver_cmd_str, indent=indent)
        subprocess.Popen(gfserver_cmd_str, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
                         universal_newlines=True, shell=True, executable='/bin/bash')
        tries = 0
        while tries <= try_limit:
            sleep(30)
            gfcheck = subprocess.Popen('gfServer status {0} {1}'.format(str(host), str(port)), stdout=subprocess.PIPE,
                                       stderr=subprocess.STDOUT, universal_newlines=True, shell=True,
                                       executable='/bin/bash')
            out, _ = gfcheck.communicate()
            if verbose > 2:
                print(out)
            if "couldn't connect to localhost" in out.lower():
                tries += 1
            elif "error" in out.lower():
                raise BLATServerError(out)
            else:
                if verbose:
                    print(out)
                return port
        if tries > try_limit:
            raise TimeoutError('Timed out!')

def id_ranker(record, perc_score, perc_query_span, perc_ident, expect=None,
              indent=0, verbose=1, same_strand=True, return_only=None):
    """Filters results based on score, expectation value, length, percent identity, and span; returns a sorted list.

    :param query_record record: Either a SearchIO.QueryResult or a Bio.Blast.Record.
    :param float perc_score: Minimum percentage of top score for a hit.
    :param float expect: Maximum e-value for a hit (BLAST-only).
    :param float perc_query_span: Minimum percent of the longest hit by query coverage for a hit.
    :param int perc_ident: Minimum percent identity of a hit.
    :param int indent: Indent level for pretty print. [Default: 0]
    :param int verbose: Level of verbose output? [Default: 1]
    :param bool same_strand: Should the function filter hits with HSPs on different strands? [Default:True]
    :param return_only: Should all or only one id be returned?
    :return list: Returns a list of tuples containing the final hit data in BED6 format.
    """
    id_list = []
    if verbose:
        print('Beginning ID_Ranker...', indent=indent)
    if record.program == 'blat':
        if verbose > 2:
            print('Results obtained from BLAT run.', indent=indent + 1)
    elif 'blast' in record.program:
        if verbose > 2:
            print('Results obtained from BLAST run.', indent=indent + 1)
    else:
        raise NotImplementedError('Sorry, your program {} is not yet '
                                  'implemented for RecBlast!'.format(record.program))

    # Create filter functions:
    def hsp_minscores(hsp):
        return hsp.score >= int(perc_score * top_score)

    def hsp_min_query_span(hsp):
        return hsp.query_span >= perc_query_span * top_length

    def hsp_perc_ident(hsp):
        return hsp.ident_pct >= perc_ident

    def hsp_same_strand(hsp):
        if same_strand:
            return all([i == hsp.hit_strand_all[0] for i in hsp.hit_strand_all])
        else:
            return True

    def hit_sort_scores(hit):
        return sum([hsp.score for hsp in hit.hsps])

    def hsp_sort_scores(hsp):
        return hsp.score

    # Get top stats:
    top_score = max([max([hsp.score for hsp in hit.hsps]) for hit in record])
    if verbose > 1:
        print('Top score for {}:\t'.format(record.id), top_score, indent=indent)
    top_length = max([max([hsp.query_span for hsp in hit]) for hit in record])
    if verbose > 1:
        print('Longest hit for {}:\t'.format(record.id), top_length, indent=indent)

    if verbose > 2:
        print("ALL HITS STATS:")
        print('|\tHit Name:\t|\t# HSPs\t|\tScore:\t|\tLength:\t|\tP.Ident\t|')
        print("==========================================================")
        for hit in record:
            name = hit.id
            n_hsp = len(hit.hsps)
            print('|\t{HitName}\t|\t{HSP}\t|'.format(HitName=name, HSP=n_hsp))
            print("------------------------------------------------------")
            for hsp in hit:
                print('|\t{id}\t|\t{hf}\t|\t{score}\t|\t{length}\t|\t{ident}\t|'.format(id=hsp.hit_id,
                                                                                hf=len(hsp),
                                                                                score=hsp.score,
                                                                                length=hsp.hit_span,
                                                                                ident=hsp.ident_pct))

    # Execute filters:
    # query_span
    if verbose > 1:
        print('Number of HSPs for {}:\t'.format(record.id), sum([len(i.hsps) for i in record]), indent=indent)
        print('Filtering out all HSPs shorter than {}...'.format(perc_query_span * top_length), indent=indent)
    record = record.hsp_filter(hsp_min_query_span) if perc_query_span else record
    if not record:
        text = ('No hits in Query Results match a stretch of the query sequence longer than '
                '{0}!').format((top_length * perc_query_span))
        raise NoHitsError(text)
    # Score
    if verbose > 1:
        print('Number of HSPs for {}:\t'.format(record.id), sum([len(i.hsps) for i in record]), indent=indent)
        print('Filtering out all HSPs with scores less than {}...'.format(top_score * perc_score), indent=indent)
    record = record.hsp_filter(hsp_minscores) if perc_score else record
    if not record:
        text = 'No hits in Query Results have a score above the minimum of {0}!'.format((top_score * perc_score))
        raise NoHitsError(text)
    if verbose > 1:
        print('Number of HSPs for {}:\t'.format(record.id), sum([len(i.hsps) for i in record]), indent=indent)
        print('Filtering out all HSPs with percent identity below {}...'.format(perc_ident), indent=indent)
    record = record.hsp_filter(hsp_perc_ident) if perc_ident else record
    if not record:
        text = 'No hits in Query Results have a percent identity above {}%!'.format(round(perc_ident * 100, 2))
        raise NoHitsError(text)
    if verbose > 1:
        print('Number of HSPs for {}:\t'.format(record.id), sum([len(i.hsps) for i in record]), indent=indent)
        if same_strand:
            print('Filtering out all HSPs that have fragments on opposite strands...')
    record = record.hsp_filter(hsp_same_strand) if same_strand else record
    if not record:
        text = 'No hits in Query Results are on the same strand!'
        raise NoHitsError(text)
    # Sorting them for good measure
    if verbose > 1:
        print('Sorting all hits by descending scores!', indent=indent)
    record.sort(key=hit_sort_scores, reverse=True, in_place=True)
    for hit in record:
        hit.sort(key=hsp_sort_scores, reverse=True, in_place=True)
    if verbose > 1:
        print('Done!', indent=indent)
    # Add items to id_list
    # Big note: think in HSPs, not Hits
    n = 1
    for hit in record:
        for hsp in hit:
            # some quick strand math:
            if hsp._has_hit_strand:
                strands = set(hsp.hit_strand_all)
                if len(strands) == 1:
                    strand = "+" if strands == {1} else "-"
                else:
                    strand = "."
            else:
                strand = "."
            if verbose > 2:
                print("Adding hit {chr}:{s}-{e}({st}) to id list".format(chr=hsp.hit_id,
                                                                         s=str(hsp.hit_range[0]),
                                                                         e=str(hsp.hit_range[1]),
                                                                         st=strand),
                      indent=indent)
            # A little witchcraft before we do though
            # turns out hsp.hit_start_all won't necessarily start with the starting point of the hit...
            # That means we need to zip hit_start_all and hit_span_all, sort by the first one, then de-zip.
            block_starts, block_spans = zip(*sorted(zip(hsp.hit_start_all, hsp.hit_span_all), key=itemgetter(0)))

            # chr (start,end) id score strand thickStart thickEnd rgb blockcount blockspans blockstarts query_span
            id_list.append([hsp.hit_id, hsp.hit_range, hsp.query_id, hsp.score, strand, hsp.hit_range[0],
                            hsp.hit_range[1], "255,0,0", len(hsp.hit_start_all),
                            ",".join([str(i) for i in block_spans]),
                            ",".join([str(i - hsp.hit_range[0]) for i in block_starts]), hsp.query_range])

            if return_only and n == return_only:
                print('Returning only the top {} hits, ending here!'.format(return_only),
                      indent=indent)
                return id_list
            n += 1
    return id_list


import argparse
import sys
import datetime

from lib.data_holder import *
from lib.models import *
from lib.extremums import *

###
### GLOBALS
###

CACHE_DIR = 'cache'

data_holder = None

def lazy_data_holder():
    global data_holder
    if data_holder is None:
        data_holder = DataHolder()

    return data_holder

###
### PLOT POINT CLOUD
###

def plot_point_cloud():
    """Plot point cloud for protein-ligand pair."""

    options_parser = argparse.ArgumentParser(add_help=True)
    options_parser.add_argument('--protein', help='protein index', default=0, type=int)
    options_parser.add_argument('--ligand', help='ligand index', default=5, type=int)
    options = options_parser.parse_args()

    cache_file_name = '{}/{}_{}.points'.format(CACHE_DIR, options.protein, options.ligand)
    arr = create_cached(cache_file_name, lambda: lazy_data_holder().get_point_array(options.protein, options.ligand))
    print('Have {} points for pair ({}, {})'.format(len(arr.spherical), options.protein, options.ligand))

    subarr = arr.subarray_for_distance_range(4.5, 5.5)
    print(len(subarr.spherical))

    scatter_plot_point_array(subarr)
    plt.savefig('test.png')

###
### PLOT DENSITY
###

from matplotlib.backends.backend_pdf import PdfPages

def plot_density_with_params(protein, ligand, r_from, r_to):
    cache_file_name = '{}/{}_{}.points'.format(CACHE_DIR, protein, ligand)
    arr = create_cached(cache_file_name, lambda: lazy_data_holder().get_point_array(protein, ligand))
    print('Have {} points for pair ({}, {})'.format(len(arr.spherical), protein, ligand))

    # 0, 5, 4.75-5.25 ok
    # 0, 6, 4.75-5.25 ok
    # 1, 1, 4.25-5.75 ok

    pdf_output = PdfPages('plot_{}_{}.pdf'.format(protein, ligand))

    #subarr = arr.subarray_for_distance_range(4.75, 5.25)
    subarr = arr.subarray_for_distance_range(r_from, r_to)
    print(len(subarr.spherical))

    scatter_plot_point_array(subarr)
    plt.xlabel(r'$\varphi$')
    plt.ylabel(r'$\theta$')
    plt.savefig('plots/points.png')
    plt.title('Source points')
    pdf_output.savefig(plt.gcf())
    plt.close()

    def make_model_density_plot(model, output_file, title):
        dist_point_array = get_point_array_for_distance(5)
        density = model.get_density_for_points(dist_point_array)
        density = density.reshape(100, 100)

        imshow_plot_density(density)

        extremums = kernel_extremums_2d(density)
        plot_extremums(extremums, density)

        plt.xlabel(r'$\varphi$')
        plt.ylabel(r'$\theta$')
        plt.title(title)
        plt.savefig(output_file)
        pdf_output.savefig(plt.gcf())
        plt.close()

    if True:
        kernel_density_model = KernelDensityModel(subarr)
        make_model_density_plot(kernel_density_model, 'plots/density_kernel.png', 'Parzen')

    if True:
        histogram_density_model = HistogramDensityModel(subarr, r_steps=3, theta_steps=25, phi_steps=25)
        make_model_density_plot(histogram_density_model, 'plots/density_histogram.png', 'Histogram')

    if True:
        gm_density_model = GaussianMixtureDensityModel(subarr)
        make_model_density_plot(gm_density_model, 'plots/density_gmm.png', 'Gaussian mixture')

    def compare_models(models):
        dist_point_array = union_point_arrays([get_point_array_for_distance(5), get_point_array_for_distance(10), get_point_array_for_distance(15)])
        volumes = get_point_array_approx_volumes(dist_point_array, n_r=3, n_phi=100, n_theta=100)

        densities = [model.get_density_for_points(dist_point_array) for model in models]

        def get_average_diff_for_pair(i, j):
            # MAPE
            #return (np.abs(densities[i] - densities[j]) / (np.minimum(densities[i], densities[j]) + 1e-6) * volumes).sum() / volumes.sum()

            # MSE
            return ((densities[i] - densities[j]) ** 2 * volumes).sum() / volumes.sum()

        for i in range(len(models)):
            for j in range(len(models)):
                print('pair {} - {}: {}'.format(i, j, get_average_diff_for_pair(i, j)))

    compare_models([kernel_density_model, histogram_density_model, gm_density_model])
    pdf_output.close()

    if False:
        def make_model_density_plots_multiple(model, output_file):
            dist_point_array = get_point_array_for_distance(5)
            densities = model.get_density_for_points(dist_point_array)

            for index, density in enumerate(densities):
                imshow_plot_density(density.reshape(100, 100))
                plt.savefig(output_file.format(index))
                plt.close()

        N = 10
        model = NeuralNetworkModel(subarr, num_iterations=N)
        for t in range(300):
            make_model_density_plots_multiple(model, 'plots/nn/density_nn_{}_' + str(model.total_iterations) + '.png')
            model.optimize_for(N)

def plot_density():
    """Plot point cloud for protein-ligand pair."""

    options_parser = argparse.ArgumentParser(add_help=True)
    options_parser.add_argument('--protein', help='protein index', default=1, type=int)
    options_parser.add_argument('--ligand', help='ligand index', default=1, type=int)
    options = options_parser.parse_args()

    plot_density_with_params(options.protein, options.ligand, 4.25, 5.75)

def plot_density_preset():
    plot_density_with_params(0, 5, 4.75, 5.25)
    plot_density_with_params(0, 0, 4.75, 5.25)
    plot_density_with_params(1, 1, 4.25, 5.75)

###
### MOD CHOOSER
###

def mod_chooser_main(available_modes):
    available_mode_names = [
        mode.__name__ for mode in available_modes
    ]

    is_known_mode = len(sys.argv) >= 2 and sys.argv[1] in available_mode_names
    need_help = len(sys.argv) >= 2 and sys.argv[1] == '--help'

    if not need_help and not is_known_mode and len(sys.argv) >= 2:
        print('Unknown mode: "{}"\n'.format(sys.argv[1], available_mode_names))

    if not is_known_mode or need_help:
        print('Usage: {} <mode> <options>'.format(sys.argv[0]))
        print('')
        print('Available modes:')
        for mode in available_modes:
            print('{} - {}'.format(mode.__name__, mode.__doc__))
        sys.exit(1)

    mode = sys.argv[1]
    mode_func = available_modes[available_mode_names.index(mode)]
    del sys.argv[1]
    mode_func()

###
### MAIN
###

def main():
    available_modes = [
        plot_point_cloud,
        plot_density,
        plot_density_preset
    ]

    mod_chooser_main(available_modes)

if __name__ == '__main__':
    main()


# Imports from FindRoots
from FindRoots import fixPoint
from FindRoots import bisection
from FindRoots import falsPos
from FindRoots import newRap
from FindRoots import secant
from FindRoots import multipleRoot
from FindRoots import intervals
from FindRoots import derivatives

# Imports from Integrals
from Integrals import Riemann
from Integrals import trapecio
from Integrals import Simpson
from Integrals import Romberg
from Integrals import gaussSeidel
from Integrals import jacobi
from Integrals import jacobiMatriz

# Imports from Regressions
from Regressions import linear
from Regressions import polinomial

# Imports from Interpolations
from Interpolations import newtonInter
from Interpolations import lagrannge

# imports for operations
import math
import matplotlib.pyplot as plt
import numpy as np

# Menus
def mainMenu():
    print("-------------------------Menú Principal-------------------------")
    print(" 1. Raíces y Derivadas\n",
          "2. Integrales\n",
          "3. Matirces\n",
          "4. Regresiones\n",
          "5. Interpolaciones\n")

def derivativesAndRootsMenu():
    print("----------------------------------------------------------------")
    print(" 1. Encontrar derivadas\n",
          "2. Buscar Raíces\n")

def rootsMenu():
    print("--------------------------Menú Raíces---------------------------")
    print(" 1. Método de Punto Fijo (Modificar para g(x))\n",
          "2. Método de Bisección\n",
          "3. Método de Falsa Posición\n",
          "4. Método de Newton-Raphson\n",
          "5. Método de la Secante\n",
          "6. Método de Raíces Múltiples (Newton-Raphson modificado)\n")

def integralsMenu():
    print("-------------------------Menú Integrales------------------------")
    print(" 1. Integral por Riemann\n",
          "2. Integral por Trapecios\n",
          "3. Integral por Simpson\n",
          "4. Integral por Romberg\n")

def matrixsMenu():
    print("-------------------------Menú Matrices--------------------------")
    print(" 1. Matriz de GaussSeidel\n",
          "2. Matriz de Jacobi (Lambdas)\n",
          "3. Matriz de Jacobi (Algebra Matricial)\n",
          "4. Matriz identidad")

def regressionsMenu():
    print("------------------------Menú Regresiones------------------------")
    print(" 1. Regresión Lineal\n",
          "2. Regresión Polinomial\n")

def interpolationsMenu():
    print("----------------------Menú Interpolaciones----------------------")
    print(" 1. Interpolación por Newton\n",
          "2. Interpolación por Lagrannge\n")

# Grafic Function
def graphic_function(f : callable, x_range = (-20, 20), step = 0.1):
    x = np.arange(x_range[0], x_range[1], step)
    y = []

    for val in x:
        try:
            y_val = f(val)
            y.append(y_val)
        except:
            y.append(float('nan'))

    plt.figure(figsize = (8,6))
    plt.plot(x, y)
    plt.title('Gráfico de la función')
    plt.xlabel('x')
    plt.ylabel('y')
    plt.grid(True)
    plt.show()

# Function
def f(x):
    #return x**2
    #return math.exp(-x**2)
    #return math.cos(x)
    #return math.sin(x)
    return math.log(x)

mainMenu()
mainMenu = int(input("Qué acción quiere realizar?\n"))

if mainMenu == 1: # Derivatives and Find roots
    derivativesAndRootsMenu()
    menuRaizDeri = int(input("Qué acción quiere realizar?\n"))

    if menuRaizDeri== 1:
        valueX = float(input("En que valor de X quiere la aproximación?\n"))

        derivatives.mainDerivates(f, valueX)
        graphic_function(f)

    elif menuRaizDeri == 2:
        rootsMenu()
        rootMetod = int(input("Qué método desea usar?\n")) # Options to select metod

        # Fixed Point Metod
        if rootMetod == 1:
            x0 = 1
            fixPoint.fix_point(x0, f)
            graphic_function(f)

        # Bisection Metod
        elif rootMetod == 2:
            intervals = intervals.Search_Interval(f, -100, 100)
            if intervals:
                x0, x1 = intervals[-1]
                bisection(x0, x1, f)
            else:
                print("No se encontraron intervalos para aplicar el método de bisección.")
            graphic_function(f)

        # False Position Metod
        elif rootMetod == 3:
            intervals = intervals.Search_Interval(f, -100, 100)
            if intervals:
                x0, x1 = intervals[-1]
                falsPos.false_position(x0, x1, f)
            else:
                print("No se encontraron intervalos para aplicar el método de falsa posición.")
            graphic_function(f)

        # Newton-Raphson Metod
        elif rootMetod == 4:
            x0 = 0
            newRap.newton_raphson(x0, f)
            graphic_function(f)

        # Secant Metod
        elif rootMetod == 5:
            intervals = intervals.Search_Interval(f, -100, 100)
            if intervals:
                x0, x1 = intervals[-1]
                secant.sec(x0, x1, f)
            else:
                print("No se encontraron intervalos para aplicar el método de la secante.")
            graphic_function(f)

        # Multiple Roots
        elif rootMetod == 6:
            x0 = 0
            multipleRoot.mul_New_Rap(x0, f)
            graphic_function(f)

        else:
            print("La opción elegida no existe")

    else:
        print("La opción elegida no existe")

elif mainMenu == 2: # Integrals
    integralsMenu()
    integralMetod = int(input("Qué método desea usar?\n")) # Options to select metod

    # Riemann Metod
    if integralMetod == 1:
        Riemann.RiemannLeftExt(0, 3, 5, f)
        Riemann.RiemannMid(0, 3, 5, f)
        Riemann.RiemannRightExt(0, 3, 5, f)

    # Trapeze Metod
    elif integralMetod == 2:
        trapecio.trapeze(0, 20000, 5, f)
        trapecio.ntrapeze(0, 20000, 5, f)

    # Simpson Metod
    elif integralMetod == 3:
        Simpson.Simpson(0, 20000, 3, f)
        Simpson.nSimpson(0, 20000, 8, f)

    # Romberg Metod
    elif integralMetod == 4:
        Romberg.Romberg(0,20000,4,f)

    else:
        print("La opción elegida no existe")

elif mainMenu == 3: # Matrixs
    matrixsMenu()
    matrixMetod = int(input("Qué método desea usar?\n")) # Options to select metod

    # Gauss-Seidel Matrix
    if matrixMetod == 1:
        A = np.array([[6, 2, 1],
                      [2, 3, 1],
                      [2, 1, 4]])
        b = np.array(
            [1, 0, 0]
        )
        gaussSeidel.gaussSeidel(A, b, 10, 2, tol= 0.001)

    # Jacobi Matrix (With Lambdas)
    elif matrixMetod == 2:
        f1 = lambda x, y ,z: (4-y-2*z)/3 #3x, f1--> x
        f2 = lambda x, y ,z: (6-2*x-z)/1 #y, f2--> y
        f3 = lambda x, y ,z: (2-x-4*y)/6 #6z, f3--> z

        jacobi.jacobi(f1, f2, f3)

    # Jacobi Matrix (Matrix Algebra)
    elif matrixMetod == 3:
        A = np.array([[3, 1, 2],
                      [2, 1, 1],
                      [1, 4, 6]])
        b = np.array(
            [4, 6, 2]
        )
        jacobiMatriz.jacobiMatriz(A, b, 10, 2)

    #Matriz inversa e identidad
    elif matrixMetod == 4:

        A = np.array([[6, 2, 1, 0],
                      [2, 3, 1, 0],
                      [2, 1, 4, 2],
                      [1, 0, 0, 3]])

        B = np.linalg.inv(A)

        C = np.dot(A,B)

        print("Matriz")
        print(A)

        print("\nInversa")
        print(B)

        print("\nIdentidad")
        print (C)

    else:
        print("La opción elegida no existe")

elif mainMenu == 4: # Regressions
    regressionsMenu()
    regressionType = int(input("Qué tipo de regresión desea usar?\n")) # Options to select metod

    # Linear Regression
    if regressionType == 1:
        dataX = np.array([1.1, 2, 3.01, 4, 4.98, 6, 7.02, 8])
        dataY = np.array([2.5, 5.1, 8, 9.6, 10.8, 14, 15.1, 18])
        a0, a1 = linear.minSqr(dataX, dataY)

        # Graphic Linear Regression
        resol = 20
        xx = np.linspace(-1,12, resol)
        yy = a0 +a1*xx
        fig, ax = plt.subplots()
        ax.plot(xx, yy, 'b')
        ax.plot(dataX, dataY, 'o')
        plt.grid()
        plt.show()

    # Polinomial Regression
    elif regressionType == 2:
        dataX = np.array([1.1, 2.1, 3.01, 4, 4.98, 6.1, 7.02, 8, 9, 10])
        dataY = np.array([4.1, 5.2, 12.2, 19, 31, 43, 52, 71, 84.6, 104])
        m = 2; n = len(dataX)
        if n < m + 1:
            print('Pocos datos (n < m + 1)')
        else:
            a0, a1, a2 = polinomial.minSqr(dataX, dataY, m, n)

        # Graphic Polinomial Regression
        resol = 100
        xx = np.linspace(-2, 12, resol)
        yy = a0 + a1*xx + a2*xx**2

        fig, ax = plt.subplots()
        ax.plot(xx, yy, 'r')
        ax.plot(dataX, dataY, 'o')
        plt.grid()
        plt.show()

    else:
        print("La opción elegida no existe")

elif mainMenu == 5: # Interpolations
    interpolationsMenu()
    interpolMetod = int(input("Qué método desea usar?\n")) # Options to select metod

    # Interpolation for Newton
    if interpolMetod == 1:
        x = 2
        interpolation = newtonInter.Newton(1,5,5,f,x)
        error = abs(interpolation - f(2))/(f(2))
        print("Interpolación por Newton : ", interpolation,"\nError: ",error*100)

    elif interpolMetod == 2:
        x = 2
        interpolation = lagrannge.Lagrannge(1,5,5,f,x)
        error = abs(interpolation - f(2))/(f(2))
        print("Interpolación por Lagrannge : ", interpolation,"\nError: ",error*100)

    else:
        print("La opción elegida no existe")

else:
        print("La opción elegida no existe")


import urllib3
from kivy.clock import Clock
from functools import partial

class Ping():

	# Выводим статус подключения к сети на главной странице и странице Настроек
	def getPing(self, ti):
		try:
		    http=urllib3.PoolManager()
		    response = http.request('GET', 'http://google.com')
		    self.screens[0].ids.net_label.text = 'Online'
		    self.screens[3].ids.net_bottom.text = 'Online'

		except:
			self.screens[0].ids.net_label.text = 'Offline'
			self.screens[3].ids.net_bottom.text = 'Offline'

	def callPing(self):
		Clock.schedule_interval(partial(Ping.getPing, self), 15)


import math
import json
import argparse

import cv2
import numpy as np
from shapely.geometry import Point
from PIL import Image, ImageDraw, ImageFont

SIZE                        = 60, 60
INNER_CIRCLE_DIAM           = 28

HOLE_SPACING                = 0
HOLE_SIZE                   = 0.800     # only used for debug output

SPHERICAL_DOME_MODE         = True
SPHERE_RADIUS               = 60/2
SPHERE_DIST                 = 25.055 # measured from work position zero

SPIRAL_SORT                 = False

DEBUG_IMAGE                 = "debug.png"
DEBUG_SCALE                 = 20
IMAGE_SIZE                  = (SIZE[0] * DEBUG_SCALE, SIZE[1] * DEBUG_SCALE) # mm

SVG_SCALE                   = 3.78
SVG_OFFSET                  = [-SIZE[0]/2*SVG_SCALE, -SIZE[1]/2*SVG_SCALE]

GCODE_FILE                  = "output.gcode"
SVG_FILE                    = "output.svg"

TRAVEL_SPEED                = 600
RAISE_SPEED                 = 400
DRILL_SPEED                 = 80

DRILL_START                 = -2
DRILL_DEPTH                 = -3 -6 -1

SAFE_HEIGHT                 = 6        # at the start
SAFE_HEIGHT_LOW             = 1        # used within the area

# ---

# TORUS

TORUS_FILE                  = "debruijn_torus_16_32_3_3.npy"
HOLE_SPACING                = 1.60
# TORUS_FILTER_INVERT         = False # RED
TORUS_FILTER_INVERT         = True # GREEN

# HEXMAP

HEXMAP_FILE                 = "2678_data_3_i-1725881.json"
# HEXMAP_FILE                 = "2633_data_0_i-1303443.json"
HOLE_SPACING                = 2.00
# HEXMAP_FILTER_VALUE         = 0
HEXMAP_FILTER_VALUE         = 1

# ---

MODE_HEXMAP                 = "hexmap"
MODE_TORUS                  = "torus"
MODE_GRID                   = "grid"

# ---

START_CMD       = """
G90                     (abs coords)
G21                     (units: mm)
G1 F{travel_speed}
G1 Z{z:.4f}             (move to safe height)
G1 X0 Y0 Z{z:.4f}       (move to zero)
G92 X0 Y0 Z{z:.4f} A0   (reset extruder axis)
"""

# MOVE_CMD        = """
# M03 S{power}
# G4 P0
# G1 F{move_speed}
# G1  X{x} Y{y}
# """

MOVE_CMD        = """
G1 F{travel_speed}
G1 X{x:.4f} Y{y:.4f} Z{z:.4f}
"""

END_CMD         = """
G4 P0
G1 F{travel_speed}
G1 Z{z:.4f}
G1 X0 Y0 Z{z:.4f}
"""

def cut_poly(f, poly):

    coords = poly.exterior.coords

    # move to start of polygon
    f.write(MOVE_CMD.format(
        x=coords[0][0], y=coords[0][1],
        travel_speed=TRAVEL_SPEED))

    f.write(CUT_CMD.format(
        x=coords[0][0], y=coords[0][1],
        power=LASER_CUT,
        cut_speed=CUT_SPEED))

    # cut
    for i in range(1, len(coords)):
        p = coords[i]
        f.write(CUT_CMD.format(
            x=p[0], y=p[1],
            power=LASER_CUT,
            cut_speed=CUT_SPEED))

    # close last segment
    f.write(CUT_CMD.format(
        x=coords[0][0], y=coords[0][1],
        power=LASER_CUT,
        cut_speed=CUT_SPEED))

def calculate_dome_offset(x, y, distance, radius, center=(SIZE[0]/2, SIZE[1]/2)):
    xy_dist = math.sqrt((center[0]-x)**2 + (center[0]-y)**2)
    return math.sqrt(radius**2 - xy_dist**2) - distance

def pointy_hex_to_pixel(q, r, s, center=[0, 0], hex_size=HOLE_SPACING/2):
    x = hex_size * (math.sqrt(3) * q + math.sqrt(3)/2 * r)
    y = hex_size * (3./2 * r)
    return (x + center[0], y + center[1])

# ------------------------------------------------------------------------------------------

font             = ImageFont.load_default()
font_large       = ImageFont.truetype("FiraMono-Regular.ttf", 16)
font_large_bold  = ImageFont.truetype("FiraMono-Bold.ttf", 16)

circle = Point(SIZE[0]/2, SIZE[1]/2).buffer(INNER_CIRCLE_DIAM/2)
points = []

ap = argparse.ArgumentParser()

ap.add_argument(
    "mode",
    default=MODE_HEXMAP,
    choices=[MODE_HEXMAP, MODE_TORUS, MODE_GRID],
    help=""
)

args = vars(ap.parse_args())

if SPHERICAL_DOME_MODE:
    print("SPHERICAL DOME MODE")

if args["mode"] == MODE_HEXMAP:

    data = None
    with open(HEXMAP_FILE, "r") as f:
        data = json.load(f)

    for key in data["data"]:

        if not data["data"][key] == HEXMAP_FILTER_VALUE:
            continue

        q, r, s = [int(c) for c in key.split("|")]

        x, y = pointy_hex_to_pixel(q, r, s, center=[SIZE[0]/2, SIZE[1]/2])

        # mirror Y axis so it can be observed through the lens on the back
        y = SIZE[1] - y

        p = Point(x, y)

        if not circle.intersection(p):
            continue

        points.append(p)

    print("generated {}/{} hexmap points".format(len(points), len(data["data"].keys())))

elif args["mode"] == MODE_TORUS:
    print("TORUS MODE")

    torus = np.load(TORUS_FILE)

    # mirror torus vertically so it can be observed through the lens on the back
    torus = np.flip(torus, axis=1)

    offset = [torus.shape[1]*HOLE_SPACING/2-HOLE_SPACING/2, torus.shape[0]*HOLE_SPACING/2-HOLE_SPACING/2]

    for i in range(torus.shape[0]):
        line = []
        for j in range(torus.shape[1]):

            if TORUS_FILTER_INVERT:
                if not torus[i, j]:
                    continue
            else:
                if torus[i, j]:
                    continue

            p = Point(SIZE[0]/2 + j*HOLE_SPACING - offset[0], SIZE[1]/2 + i*HOLE_SPACING - offset[1])

            if not circle.intersection(p):
                continue

            line.append(p)

        if i % 2 == 0:
            points += line
        else:
            points += reversed(line)

    print("generated {}/{} torus points".format(len(points), torus.shape[0]*torus.shape[1]))

elif args["mode"] == MODE_GRID:

    # regular grid

    # for x in np.linspace(0, SIZE[0], math.floor(SIZE[0]/HOLE_SPACING)):
    #     for y in np.linspace(0, SIZE[1], math.floor(SIZE[1]/HOLE_SPACING)):
    #         p = Point(x, y)
    #         if circle.intersection(p):
    #             points.append(Point(x, y))

    # hex grid

    print("GRID HEXAGON MODE")

    size = HOLE_SPACING
    w = math.sqrt(3) * size
    h = 2 * size
    num_x = int(SIZE[0]/w)
    num_y = int(SIZE[1]/(0.75*h))
    offset = [0, 0]
    if num_y % 2 == 0:
        offset = [SIZE[0]-num_x*w, SIZE[1]-num_y*(0.75*h)]
    else:
        offset = [SIZE[0]-num_x*w, SIZE[1]-num_y*(0.75*h)+0.75*h]

    print("hexagons - horizontal: {} | vertical: {}".format(num_x, num_y))
    print("hex offsets: {:6.3f} {:6.3f}".format(*offset))

    for y in range(0, num_y):
        line = []
        for x in range(0, num_x):

            p = None
            if y % 2 == 0:
                p = Point(offset[0]/2 + x*w,              offset[1]/2 + 0.75*h*y) # + 0.5*h)
            else:
                p = Point(offset[0]/2 + 0.5*w + x*w,      offset[1]/2 + 0.75*h*y) # + 0.5*h)

            if not circle.intersection(p):
                continue

            line.append(p)

            # if x == 0:
            #     print("{} {}".format(y, p))

        if y % 2 == 0:
            points += line
        else:
            points += reversed(line)

else:
    print("mode missing")
    sys.exit(-1)

if SPIRAL_SORT:
    # spiral traversal order, naive implementation
    points_sorted = []
    center = [SIZE[0]/2, SIZE[1]/2]
    points = sorted(points, key=lambda p: math.sqrt(math.pow(center[0] - p.x, 2) + math.pow(center[1] - p.y, 2)), reverse=False)
    points_sorted.append(points[0])
    points = points[1:]

    # weighting
    while len(points) > 0:
        points = sorted(
            points,
            key=lambda p:
                0.45 * math.sqrt(math.pow(center[0] - p.x, 2) + math.pow(center[1] - p.y, 2)) +                      # distance to center
                0.55 * math.sqrt(math.pow(points_sorted[-1].x - p.x, 2) + math.pow(points_sorted[-1].y - p.y, 2)),   # distance to last point
            reverse=False
        )
        points_sorted.append(points[0])
        points = points[1:]

    points = list(reversed(points_sorted))

# ---

print("num points: {}".format(len(points)))

with Image.new(mode="RGB", size=IMAGE_SIZE) as im:
    draw = ImageDraw.Draw(im, "RGBA")

    for x in range(1, IMAGE_SIZE[0]//100):
        draw.line([x*100, 0, x*100, IMAGE_SIZE[1]], width=1, fill=(40, 40, 40))

    for y in range(1, IMAGE_SIZE[1]//100):
        draw.line([0, y*100, IMAGE_SIZE[0], y*100], width=1, fill=(40, 40, 40))

    draw.text((25, 5+20),           "HOLE SPACING:", (255, 255, 255), font=font_large)
    draw.text((25+170, 5+20),       " {:2.3f} mm".format(HOLE_SPACING), (255, 255, 255), font=font_large_bold)

    draw.text((25, 5+20*2),         "HOLE SIZE:", (255, 255, 255), font=font_large)
    draw.text((25+170, 5+20*2),     " {:2.3f} mm".format(HOLE_SIZE), (255, 255, 255), font=font_large_bold)

    draw.text((25, 5+20*3),         "INNER CIRCLE:", (255, 255, 255), font=font_large)
    draw.text((25+170, 5+20*3),     " {:2.2f} mm".format(INNER_CIRCLE_DIAM), (255, 255, 255), font=font_large_bold)

    draw.line([25, 10+20*4, 270, 10+20*4], width=1, fill=(80, 80, 80))

    draw.text((25, 20+20*4),         "total points:", (255, 255, 255), font=font_large)
    draw.text((25+170, 20+20*4),     " {}".format(len(points)), (255, 255, 255), font=font_large_bold)

    val = " - "
    if args["mode"] == MODE_HEXMAP:
        val = str(HEXMAP_FILTER_VALUE)
    elif args["mode"] == MODE_TORUS:
        val = str(TORUS_FILTER_INVERT)
    draw.text((25, IMAGE_SIZE[1]-30),       "FILTER VALUE:", (255, 255, 255), font=font_large)
    draw.text((25+170, IMAGE_SIZE[1]-30),   val, (255, 255, 255), font=font_large_bold)

    draw.line([0, IMAGE_SIZE[1]/2, IMAGE_SIZE[0], IMAGE_SIZE[1]/2], width=1, fill=(40, 0, 0))
    draw.line([IMAGE_SIZE[0]/2, 0, IMAGE_SIZE[0]/2, IMAGE_SIZE[1]], width=1, fill=(40, 0, 0))

    for i in range(0, len(points)-1):
        cur = points[i]
        nxt = points[i+1]

        draw.line([
            int(cur.x * DEBUG_SCALE), int(cur.y * DEBUG_SCALE),
            int(nxt.x * DEBUG_SCALE), int(nxt.y * DEBUG_SCALE)],
            width=4, fill=(0, 0, 150))

    for point in points:
        coords = [(int(x * DEBUG_SCALE), int(y * DEBUG_SCALE)) for x, y in point.buffer(HOLE_SIZE/2).exterior.coords]
        draw.polygon(coords, fill="white")

    coords = [(int(x * DEBUG_SCALE), int(y * DEBUG_SCALE)) for x, y in circle.exterior.coords]
    draw.polygon(coords, outline="red")

    im.save(DEBUG_IMAGE)

with open(GCODE_FILE, "w") as f:
    f.write(START_CMD.format(
        travel_speed=TRAVEL_SPEED,
        z=SAFE_HEIGHT
    ))

    # move to initial position
    f.write(MOVE_CMD.format(
            x=0, y=0, z=SAFE_HEIGHT,
            travel_speed=TRAVEL_SPEED))

    # move to first point
    coords = list(points[0].coords)[0]
    f.write(MOVE_CMD.format(
            x=coords[0], y=coords[1], z=SAFE_HEIGHT,
            travel_speed=TRAVEL_SPEED))

    spherical_offset = 0

    for i in range(0, len(points)):

        p = list(points[i].coords)[0]

        if SPHERICAL_DOME_MODE:
            spherical_offset = calculate_dome_offset(p[0], p[1], SPHERE_DIST, SPHERE_RADIUS)

        p = [p[0], p[1]*-1+SIZE[1]] # flip Y coordinate to convert top-left coordinate system (numpy matrix, PIL image) to bottom-left system (gcode)

        # move
        f.write(MOVE_CMD.format(
            x=p[0], y=p[1], z=SAFE_HEIGHT_LOW,
            travel_speed=TRAVEL_SPEED))

        # lower
        f.write(MOVE_CMD.format(
            x=p[0], y=p[1], z=DRILL_START,
            travel_speed=TRAVEL_SPEED))

        # drill
        f.write(MOVE_CMD.format(
            x=p[0], y=p[1], z=DRILL_DEPTH + spherical_offset,
            travel_speed=DRILL_SPEED))

        # print(DRILL_DEPTH + spherical_offset)

        # raise
        f.write(MOVE_CMD.format(
            x=p[0], y=p[1], z=SAFE_HEIGHT_LOW,
            travel_speed=RAISE_SPEED))

    f.write(
        END_CMD.format(
            travel_speed=TRAVEL_SPEED,
            z=SAFE_HEIGHT
    ))

    print("written to file: {}".format(GCODE_FILE))

with open(SVG_FILE, "w") as f:

    f.write("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n")
    f.write("<?xml-stylesheet href=\"style.css\" type=\"text/css\" title=\"main_stylesheet\" alternate=\"no\" media=\"screen\" ?>\n")
    f.write("<svg baseProfile=\"tiny\" version=\"1.2\" width=\"{}{}\" height=\"{}{}\"\n".format(SIZE[0]*SVG_SCALE, "mm", SIZE[1]*SVG_SCALE, "mm"))
    f.write("xmlns=\"http://www.w3.org/2000/svg\" xmlns:ev=\"http://www.w3.org/2001/xml-events\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n")
    f.write("<defs />\n")

    f.write("<circle cx=\"{}\" cy=\"{}\" r=\"{}\" fill=\"none\" stroke=\"black\" />\n".format(
        SIZE[0]/2*SVG_SCALE+SVG_OFFSET[0],
        SIZE[1]/2*SVG_SCALE+SVG_OFFSET[1],
        INNER_CIRCLE_DIAM/2*SVG_SCALE))

    for i in range(0, len(points)):
        p = list(points[i].coords)[0]

        f.write("<circle cx=\"{}\" cy=\"{}\" r=\"{}\" />\n".format(
            p[0]*SVG_SCALE+SVG_OFFSET[0],
            p[1]*SVG_SCALE+SVG_OFFSET[1],
            HOLE_SIZE/2*SVG_SCALE))

    f.write("</svg>\n")

    print("written to file: {}".format(SVG_FILE))


from ast import literal_eval
import os, sys
import customtkinter as ctk
from PIL import Image

ctk.set_appearance_mode("dark")

ctk.set_default_color_theme("blue")

from numpy import number
sys.path.insert(0, os.path.dirname("algorithms"))

import random
import copy
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox
import csv
from pathlib import Path
import pandas as pd
from tkinter import Menu
from tkinter import font
from tkinter import filedialog
import numpy as np

from csv_cleaner import Csv_Cleaner
from csv_editor import Application
from algorithms.naive_classification import NaiveClassification
from algorithms.metrics import Metrics
from algorithms.knn import KNN
from algorithms.naive_bayes import NaiveBayes

####
# useful dictionnary
classes_labels = {
    4 : 'positive',
    2 : 'neutral',
    0 : 'negative'
}
###

"""
Initializes the splash window to display the XSA logo
"""
splash_root     = tk.Tk()
splash_root.title('XSA - GUI')
splash_root.geometry("500x500")
path            = Path(__file__).parent / "."
logo_path       = (path / "./assets/black_logo.png").resolve()
photo           = tk.PhotoImage(file=logo_path)
image_label     = ttk.Label(
    splash_root,
    image=photo,
    text='XSA',
    compound='top'
)
image_label.pack()

####################
# GLOBAL VARIABLES #
####################
single_input_classification = None
number_of_k_value           = None
distance_value              = None
active_dataset              = None
variante1                   = None
variante2                   = None
variante3                   = None

def main():
    """
    Defining all the necessary functions
    """

    def set_active_dataset(df):
        """Sets the active dataset global variable to the dataset given in parameter.
        Tha active dataset is the one opened in the CSV Viewer

        Args:
            df (pd.dataframe): the dataset opened in the csv viewer
        """
        global active_dataset
        active_dataset = df

    def get_active_dataset():
        """Retrieves the value stored in the active dataset global variables

        Returns:
            pd.dataframe: the active dataset
        """
        global active_dataset
        return active_dataset

    def open_csv_file():
        """Opens a filedialog asking for the user to choose a csv file, then call the display_csv_data function to display it on the csv viewer in the app.
        """
        file_path = filedialog.askopenfilename(title="Open CSV File", filetypes=[("CSV files", "*.csv")])
        if file_path:
            display_csv_data(file_path)

    def ask(question):
        """Asks a yes/no question to the user. Useful for basic input

        Args:
            question (str): the question asked to the user

        Returns:
            bool: litteraly yes or no
        """
        response = messagebox.askyesno("XSA - User input",
                          question,
                          icon ='question')
        if response:    # If the answer was "Yes" response is True
            return True
        else:           # If the answer was "No" response is False
            return False

    def display_csv_data(file_path):
        """This function displays a csv in the csv viewer, the central panel in the application.
        Upon opening, the function asks the user of he wants his data to be cleaned, then is the file has a header, if not, default header will be applied.

        Args:
            file_path (str): path to the file that is to be displayed

        Returns:
            None: if the user decides to cancel the operation
        """
        clean_data = ask("Would you like to clean your data ?")
        file_name = ""
        df = pd.read_csv(file_path, on_bad_lines='skip')
        set_active_dataset(df)

        if clean_data:
                df = pd.read_csv(file_path, on_bad_lines='skip', header=None)
                cleaner = Csv_Cleaner(file_path)
                df = cleaner.clean()

                set_active_dataset(df)

                f = filedialog.asksaveasfile(mode='w', defaultextension=".csv")
                if f is None: # asksaveasfile return `None` if dialog closed with "cancel".
                    return
                f.write(active_dataset.to_csv(index=False, sep=",", header=True, quotechar='"', lineterminator="\r"))
                file_name = f.name
                f.close()
        else:
            file_name = file_path

        header_prsent = ask("Does the file have a header ?")
        with open(file_name, 'r', newline='') as file:
            csv_reader = csv.reader(file, quoting=csv.QUOTE_ALL)
            if header_prsent:
                header = next(csv_reader)
            else:
                header=[f"col_{x}" for x in range(count(df.columns))]

            tree.delete(*tree.get_children())  # Clear the current data

            tree["columns"] = header
            for col in header:
                tree.heading(col, text=col)
                tree.column(col, width=300, stretch=True)

            for row in csv_reader:
                tree.insert('', "end", values=row)

    def user_selection_model_parameter(model):
        """Allows the user to select the hyperparameters of his model before training the model.

        Args:
            model (str): the str equivalent of the model (e.g : 'knn', 'naive_classification', 'naive_bayes' ...)
        """
        global number_of_k_value
        global distance_value
        global vote_value
        global variante1
        global variante2
        global variante3

        number_of_k_value = tk.StringVar()
        distance_value = tk.StringVar()
        vote_value = tk.StringVar()

        variante1 = tk.StringVar()
        variante2 = tk.StringVar()
        variante3 = tk.StringVar()

        if model == 'knn':
            new= ctk.CTkToplevel(master=root)
            new.geometry("400x600")
            new.title("User Input KNN")

            my_font = ctk.CTkFont(family="Helvetica", size=20, weight="bold")

            ctk.CTkLabel(new, text="Number of K", font=my_font).pack(pady=20)
            ctk.CTkEntry(new, textvariable=number_of_k_value).pack(pady=20, padx=10)

            ctk.CTkLabel(new,text="Distance", font=my_font).pack(pady=20)
            choices = ["naive", "levenshtein", "lcs", "damerau_levenshtein", "hamming", "jaro", "cosine", "jaccard", "sorensen_dice", "qgram_dice"]
            ctk.CTkComboBox(new, variable=distance_value, values=choices).pack(pady=40, padx=10)

            ctk.CTkLabel(new, text="Vote", font=my_font).pack(pady=20)
            votes = ["majoritaire", "pondéré"]
            ctk.CTkComboBox(new, variable=vote_value, values=votes).pack(pady=20, padx=10)

        if model == "naive_bayes":
            new= ctk.CTkToplevel(master=root)
            new.geometry("400x600")
            new.title("User Input NB")

            my_font = ctk.CTkFont(family="Helvetica", size=20, weight="bold")

            ctk.CTkLabel(new,text="Variante 1", font=my_font).pack(pady=20)
            choices = ["fréquence", "présence"]
            ctk.CTkComboBox(new, variable=variante1, values=choices).pack(pady=40, padx=10)

            ctk.CTkLabel(new, text="Variante 2 (stopwords)", font=my_font).pack(pady=20)
            votes = ["avec", "sans"]
            ctk.CTkComboBox(new, variable=variante2, values=votes).pack(pady=20, padx=10)

            ctk.CTkLabel(new, text="Variante 3", font=my_font).pack(pady=20)
            votes = ["uni-gramme", "bi-gramme", "both"]
            ctk.CTkComboBox(new, variable=variante3, values=votes).pack(pady=20, padx=10)

        ctk.CTkButton(new, text="Validate Parameters and exit", command=new.destroy).pack(pady=30)
        root.wait_window(new)

    def test_model_dataset():
        """tests the model based on the parameters that were input by the user.
        """
        selection = algo_var.get()

        if selection == 'naive_bayes':
            if isinstance(get_active_dataset(), type(None)):
                messagebox.showwarning(title="Warning", message="Please load a training dataset in the CSV viewer")
            else:

                df_train = copy.deepcopy(active_dataset)
                messagebox.showinfo(title="Info Naive Bayes", message="Load your testing data")
                open_csv_file()
                df_test = copy.deepcopy(active_dataset)
                user_selection_model_parameter("naive_bayes")
                nb_model = NaiveBayes(df_train, variante1.get(), variante2.get(), variante3.get())
                classifications = []
                for tweet_token in df_test["Tweet_Tokenized"]:
                    tweet_a_categoriser = " ".join(literal_eval(tweet_token))
                    classifications.append(nb_model.classification(tweet_a_categoriser))
                df_test["model_class"] = classifications
                set_active_dataset(df_test)
                f = filedialog.asksaveasfile(mode='w', defaultextension=".csv")
                if f is None: # asksaveasfile return `None` if dialog closed with "cancel".
                    return

                f.write(active_dataset.to_csv(index=False, sep=",", header=True, quotechar='"', lineterminator="\r"))
                file_name = f.name
                f.close()
                display_csv_data(file_name)

        elif selection == 'knn':
            if isinstance(get_active_dataset(), type(None)):
                messagebox.showwarning(title="Warning", message="Please load a training dataset in the CSV viewer")
            else:

                df_train = copy.deepcopy(active_dataset)
                messagebox.showinfo(title="Info KNN", message="Load your testing data")
                open_csv_file()
                df_test = copy.deepcopy(active_dataset)
                user_selection_model_parameter("knn")
                knn_model = KNN(df_train ,number_of_k_value.get(), distance_value.get(), vote_value.get())

                classifications = []
                for tweet_token in df_test["Tweet_Tokenized"]:
                    tweet_a_categoriser = " ".join(literal_eval(tweet_token))
                    classifications.append(knn_model.classification(tweet_a_categoriser))
                df_test["model_class"] = classifications
                set_active_dataset(df_test)
                f = filedialog.asksaveasfile(mode='w', defaultextension=".csv")
                if f is None: # asksaveasfile return `None` if dialog closed with "cancel".
                    return

                f.write(active_dataset.to_csv(index=False, sep=",", header=True, quotechar='"', lineterminator="\r"))
                file_name = f.name
                f.close()
                display_csv_data(file_name)

        elif selection == 'naive_classification':
            if isinstance(get_active_dataset(), type(None)):
                messagebox.showwarning(title="Warning", message="Please load a dataset in the CSV viewer")
            else:
                nc = NaiveClassification(active_dataset)
                classified_df = nc.get_classified()
                set_active_dataset(classified_df)
                f = filedialog.asksaveasfile(mode='w', defaultextension=".csv")
                if f is None: # asksaveasfile return `None` if dialog closed with "cancel".
                    return

                f.write(active_dataset.to_csv(index=False, sep=",", header=True, quotechar='"', lineterminator="\r"))
                file_name = f.name
                f.close()
                display_csv_data(file_name)

    def get_user_input_for_single_classification():
        """Asks the user for a single input to be classified. Mostly used as a demo example to avoid spending too much time loading the datasets.
        """
        global single_input_classification
        single_input_classification = tk.StringVar()

        new= ctk.CTkToplevel(master=root)
        new.geometry("300x300")
        new.title("User Input")

        my_font = ctk.CTkFont(family="Helvetica", size=20, weight="bold")

        ctk.CTkLabel(new, text="Tweet Input", font=my_font).pack(pady=20)
        ctk.CTkEntry(new, width=200,textvariable=single_input_classification).pack(pady=20, padx=10)

        ctk.CTkButton(new, text="Validate input and exit", command=new.destroy).pack(pady=30)
        root.wait_window(new)

    def test_model_single_input():
        """Tests the model if the mode is single_input_classification
        """
        selection = algo_var.get()

        if selection == 'naive_bayes':
            if isinstance(get_active_dataset(), type(None)):
                messagebox.showwarning(title="Warning", message="Please load a training dataset in the CSV viewer")
            else:
                user_selection_model_parameter("naive_bayes")
                get_user_input_for_single_classification()
                tweet_a_categoriser = single_input_classification.get()
                cleaner = Csv_Cleaner(is_single_input=True, single_input=tweet_a_categoriser)
                tweet_a_categoriser_clean = cleaner.clean()
                nb_model = NaiveBayes(active_dataset, variante1.get(), variante2.get(), variante3.get())

                classification = nb_model.classification(" ".join((tweet_a_categoriser_clean)), single_input_classification=True)
                messagebox.showinfo(title="Info", message=f"Your input '{tweet_a_categoriser}' has been classsified as : {classes_labels[int(classification)]}")

        if selection == 'knn':
            if isinstance(get_active_dataset(), type(None)):
                messagebox.showwarning(title="Warning", message="Please load a training dataset in the CSV viewer")
            else:
                user_selection_model_parameter("knn")
                get_user_input_for_single_classification()
                tweet_a_categoriser = single_input_classification.get()
                cleaner = Csv_Cleaner(is_single_input=True, single_input=tweet_a_categoriser)
                tweet_a_categoriser_clean = cleaner.clean()
                knn_model = KNN(active_dataset ,number_of_k_value.get(), distance_value.get(), vote_value.get())

                classification = knn_model.classification(" ".join((tweet_a_categoriser_clean)))
                messagebox.showinfo(title="Info", message=f"Your input '{tweet_a_categoriser}' has been classsified as : {classes_labels[int(classification)]}")

        if selection == 'naive_classification':
            get_user_input_for_single_classification()
            tweet_a_categoriser = single_input_classification.get()
            cleaner = Csv_Cleaner(is_single_input=True, single_input=tweet_a_categoriser)
            tweet_a_categoriser_clean = cleaner.clean()
            nc = NaiveClassification(tweet_a_categoriser_clean, True)
            classification = nc.classify()
            print(classification)
            messagebox.showinfo(title="Info", message=f"Your input '{tweet_a_categoriser}' has been classsified as : {classes_labels[int(classification)]}")

    def kfold_indices(data, k=5):
        data = data.sample(frac=1)
        fold_size = len(data) // k
        indices = np.arange(len(data))
        folds = []

        for i in range(k):
            test_indices = indices[i * fold_size: (i + 1) * fold_size]
            train_indices = np.concatenate([indices[:i * fold_size], indices[(i + 1) * fold_size:]])
            validation_set = data.loc[test_indices].reset_index(drop=True)
            training_set = data.loc[train_indices].reset_index(drop=True)
            folds.append((training_set, validation_set))
        return folds

    def train_test_split(data, test_size=0.2, random_seed=None):

        if random_seed is not None:
            random.seed(random_seed)

        test_size = int(len(data) * test_size)

        shuffled_data = random.sample(data, len(data))

        test_set = shuffled_data[:test_size]
        train_set = shuffled_data[test_size:]

        return train_set, test_set

    def train_model():
        """tests the model based on the parameters that were input by the user.
        """
        selection = algo_var.get()

        if selection == 'naive_bayes':
            if isinstance(get_active_dataset(), type(None)):
                messagebox.showwarning(title="Warning", message="Please load a training dataset in the CSV viewer")
            else:

                classifications_validation = []
                df_train = copy.deepcopy(active_dataset)

                ## selection du modele
                #user_selection_model_parameter(algo_var.get())
                ## cross validation
                fold_indices = kfold_indices(df_train, 3)
                variante1 = ["présence", "fréquence"]
                variante2 = ["avec", "sans"]
                variante3 = ["uni-gramme", "bi-gramme", "both"]
                cross_val_scores = {}
                for j in range(len(variante1)):
                    for k in range(len(variante2)):
                        for l in range(len(variante3)):
                            for i, (train_set, val_set) in enumerate(fold_indices):
                                score_folds = []
                                classifications_validation = []
                                nb_model = NaiveBayes(train_set, variante1[j], variante2[k], variante3[l]) # model fitted on the training set
                                for tweet_token in val_set["Tweet_Tokenized"]:
                                    tweet_a_categoriser = " ".join(literal_eval(tweet_token)) # model evaluated on the validation set
                                    classifications_validation.append(nb_model.classification(tweet_a_categoriser))
                                val_set["model_class"] = classifications_validation
                                metric = Metrics(val_set, root, algo_var.get())
                                score_folds.append(metric.get_accuracy())
                            cross_val_scores[f"[{variante1[j]}, {variante2[k]}, {variante3[l]}]"]=(np.mean(score_folds))
                print(cross_val_scores)
                max_key = max(cross_val_scores, key=cross_val_scores.get)
                max_value = cross_val_scores[max_key]

                metric.display(f"Model : {max_key} -> {max_value}")

        elif selection == 'knn':
            if isinstance(get_active_dataset(), type(None)):
                messagebox.showwarning(title="Warning", message="Please load a training dataset in the CSV viewer")
            else:
                classifications_validation = []
                df_train = copy.deepcopy(active_dataset)

                ## cross validation
                cross_val_scores = {}
                fold_indices = kfold_indices(df_train, k=2)
                variante1 = [1,2,3]
                variante2 = ["jaro"]#, "lcs", "damerau_levenshtein", "jaro", "cosine", "jaccard", "sorensen_dice", "qgram_dice"]
                variante3 = ["pondéré", "majoritaire"]
                cross_val_scores = {}
                for j in range(len(variante1)):
                    for k in range(len(variante2)):
                        for l in range(len(variante3)):
                            for i, (train_set, val_set) in enumerate(fold_indices):
                                score_folds = []
                                classifications_validation = []
                                knn = KNN(train_set, variante1[j], variante2[k], variante3[l]) # model fitted on the training set
                                for tweet_token in val_set["Tweet_Tokenized"]:
                                    tweet_a_categoriser = " ".join(literal_eval(tweet_token)) # model evaluated on the validation set
                                    classifications_validation.append(knn.classification(tweet_a_categoriser))
                                val_set["model_class"] = classifications_validation
                                metric = Metrics(val_set, root, algo_var.get())
                                score_folds.append(metric.get_accuracy())
                            cross_val_scores[f"[{variante1[j]}, {variante2[k]}, {variante3[l]}]"]=(np.mean(score_folds))
                print(cross_val_scores)
                max_key = max(cross_val_scores, key=cross_val_scores.get)
                max_value = cross_val_scores[max_key]
                metric.display(f"Model : {max_key} -> {max_value}")

        elif selection == 'naive_classification':
            if isinstance(get_active_dataset(), type(None)):
                messagebox.showwarning(title="Warning", message="Please load a dataset in the CSV viewer")
            else:
                nc = NaiveClassification(active_dataset)
                classified_df = nc.get_classified()
                set_active_dataset(classified_df)
                metric = Metrics(classified_df, root, algo_var.get())
                metric.display(train=False)

    def show_model_stats():
        """Displays metrics computed on the active dataset
        """
        selection = algo_var.get()
        if selection=="knn":
            metrics = Metrics(active_dataset, parent=root, model=selection)
            metrics.display(train=False)
        if selection=="naive_bayes":
            metrics = Metrics(active_dataset, parent=root, model=selection)
            metrics.display(train=False)
        if selection=="naive_classification":
            metrics = Metrics(active_dataset, parent=root, model=selection)
            metrics.display(train=False)

    # ENTRY POINT OF THE GUI IMPLEMENTATION
    splash_root.destroy()
    root = ctk.CTk()
    root.geometry("1000x900")
    root.title("X(Twitter) Sentiment Analysis - GUI")

    paned_window = tk.PanedWindow(root, orient="vertical", borderwidth=0)
    paned_window.pack(fill='both', expand=True)

    upper_frame = ctk.CTkFrame(paned_window, width=600, height=600,  border_width=0)
    middle_frame = ctk.CTkFrame(paned_window, width=600, height=600,  border_width=0)

    ###########
    # STYLES #
    ###########
    # Create style Object
    style = ttk.Style(upper_frame)
    # set ttk theme to "clam" which support the fieldbackground option
    style.theme_use("clam")
    style.configure("Treeview", background="white",
                    fieldbackground="#1e1b24", foreground="black")
    style.configure("TPanedwindow", background="black")

    ############
    # FRAMES ###
    ############
    #upper frame
    tree = ttk.Treeview(upper_frame, show="headings")
    tree.pack(padx=20, pady=20, fill="both", expand=True)

    status_label = ctk.CTkLabel(upper_frame, text="", padx=20, pady=10)
    status_label.pack()

    open_button = ctk.CTkButton(upper_frame, text="Open CSV file",command=open_csv_file)
    open_button.pack(padx=20, pady=10)

    #middle frame
    csv_editor = Application(middle_frame)
    csv_editor.pack()
    open_button = ctk.CTkButton(middle_frame, text="Edit CSV file",command=csv_editor.loadCells)
    open_button.pack(padx=50, pady=10)

    menubar = Menu(root, background='#1E1B24', fg='white')

    filemenu = Menu(menubar, tearoff=0, background='#1E1B24', fg='white')
    filemenu.add_command(label="New", command=csv_editor.newCells)     # add save dialog
    # add save dialog
    filemenu.add_command(label="Open", command=csv_editor.loadCells)
    filemenu.add_command(label="Save as", command=csv_editor.saveCells)
    filemenu.add_command(label="Exit", command=csv_editor.quit)

    menubar.add_cascade(label="File", menu=filemenu)
    menubar.add_command(label="Exit", command=csv_editor.quit)

    root.config(menu=menubar, background="#1E1B24")

    default_font = font.nametofont("TkTextFont")
    default_font.configure(family="Helvetica")

    root.option_add("*Font", default_font)

    # down frame
    algo_var = tk.StringVar()

    algoFrame = ctk.CTkFrame(paned_window, width=600, height=600,  border_width=0)
    algoFrame.grid(column=0, row=0, padx=20, pady=20)

    # create a radio button
    naive_classif = ctk.CTkRadioButton(algoFrame, text='Dictionnary', value='naive_classification', variable=algo_var)
    naive_classif.grid(column=0, row=0, ipadx=10, ipady=10)
    knn = ctk.CTkRadioButton(algoFrame, text='KNN', value='knn', variable=algo_var)
    knn.grid(column=1, row=0, ipadx=10, ipady=10, sticky=tk.E)
    naive_bayes = ctk.CTkRadioButton(algoFrame, text='Naive Bayes', value='naive_bayes', variable=algo_var)
    naive_bayes.grid(column=2, row=0, ipadx=10, ipady=10, sticky=tk.NS)

    ###########
    buttonFrame = ctk.CTkFrame(paned_window, width=600, height=600, border_width=0)
    buttonFrame.grid(column=0, row=0, padx=20, pady=20)

    train = ctk.CTkButton(buttonFrame, text="Train model", command=train_model)
    train.grid(column=0, row=0, ipadx=10, ipady=10)
    test = ctk.CTkButton(buttonFrame, text="Test model on dataset", command=test_model_dataset)
    test.grid(column=1, row=0, ipadx=10, ipady=10)
    test_single_sentence = ctk.CTkButton(buttonFrame, text="Test on single input", command=test_model_single_input)
    test_single_sentence.grid(column=2, row=0, ipadx=10, ipady=10)
    stats = ctk.CTkButton(buttonFrame, text="Stats", command=show_model_stats)
    stats.grid(column=3, row=0, ipadx=10, ipady=10)

    paned_window.paneconfig(middle_frame, width=1000, height=450)
    paned_window.paneconfig(upper_frame, width=1000, height=400)
    paned_window.paneconfig(algoFrame, width=1000, height=50)
    paned_window.paneconfig(buttonFrame, width=1000, height=50)

    paned_window.add(middle_frame)
    paned_window.add(upper_frame)
    paned_window.add(algoFrame)
    paned_window.add(buttonFrame)

    root.mainloop()

# Set Interval
splash_root.after(1000, main)
# Execute tkinter
splash_root.mainloop()