import os.path

import aiohttp
import asyncio

from pokeretriever.ability import Ability
from pokeretriever.custom_exceptions import InvalidFileTypeError, FileExtensions
from pokeretriever.move import Move
from pokeretriever.stat import Stat
from pokeretriever.pokemon import Pokemon
from pathlib import Path

def read_file(path: str) -> list:
    """
    Read the input file line by line and store the values in a list that will be used for a Pokemon API call.

    :param path: string, location of where the file exists
    :return: string list.
    """
    try:
        # check file extension
        extension_suffix = Path(path).suffix
        if extension_suffix not in [file.value for file in FileExtensions]:
            raise InvalidFileTypeError(f'Invalid file type')

        # check file exits
        if not os.path.exists(path):
            raise FileNotFoundError()

        # read file and append value from each line into data_list
        data_list = []
        with open(path, 'r') as file:
            for line_value in file:
                data_list.append(line_value)

    except FileNotFoundError:
        print(f'Error message: File not found. Please try again.')
        exit()
    except ValueError:
        print(f'Error message: Your file is not of proper format. Please try again')
        exit()
    except InvalidFileTypeError:
        print(f'Error message: Cannot read invalid file type. Please try again')
        exit()

    else:
        return data_list

async def get_single_request(url: str, session: aiohttp.ClientSession) -> dict | None:
    """
    Method used when the input data argument is provided in the command line.

    :param url: str
    :param session: aiohttp.client.ClientSession
    :return: json payload from the Pokemon's API request.
    """
    response = await session.request(method='GET', url=url)

    if response.status == 200:
        json_response = await response.json()
        return json_response

    if response.status == 404:
        return

async def get_requests(all_url: list):
    """
    Method used when the input file argument is provided in the command line.

    :param all_url: string list
    :return: json payload for all Pokemon's API request.
    """
    # This functions takes in a list of urls and makes batch async calls. Returns a list of all responses
    async with aiohttp.ClientSession() as session:
        async_coroutines = [get_single_request(url, session) for url in all_url]
        all_responses = await asyncio.gather(*async_coroutines)
        all_responses_filtered = [response for response in all_responses if response is not None]  # filter out None values
        return all_responses_filtered

def convert_json_into_ability_object(json_list) -> list:
    """
    Take the payload JSON result from API get request calls and use it to create ability objects.

    :param json_list: json list
    :return: List of ability objects
    """
    if len(json_list) == 0:
        return []

    ability_objects = []
    for ability in json_list:
        single_ability = Ability(
            ability['generation']['name'],
            ability['effect_entries'] if ability['effect_entries'] else None,
            ability['pokemon'],
            ability['name'],
            ability['id']
        )
        ability_objects.append(single_ability)

    return ability_objects

def convert_json_into_stat_object(json_list) -> list:
    """
    Take the payload JSON result from API get request calls and use it to create stat objects.

    :param json_list: json list
    :return: List of stat objects
    """
    if len(json_list) == 0:
        return []

    stat_objects = []
    for stat in json_list:
        single_stat = Stat(
            stat['is_battle_only'],
            stat['name'],
            stat['id']
        )
        stat_objects.append(single_stat)

    return stat_objects

def convert_json_into_move_object(json_list) -> list:
    """
    Take the payload JSON result from API get request calls and use it to create move objects.

    :param json_list: json list
    :return: List of move objects
    """
    if len(json_list) == 0:
        return []

    move_objects = []
    for move in json_list:
        single_move = Move(
            move['generation']['name'],
            move['accuracy'],
            move['pp'],
            move['power'],
            move['type']['name'],
            move['damage_class']['name'],
            move['effect_entries'][0]['short_effect'] if move['effect_entries'] and move['effect_entries'][0][
                'short_effect'] else None,
            move['name'],
            move['id']
        )
        move_objects.append(single_move)

    return move_objects

async def get_moves(value_list) -> list:
    """
    Method that is a wrapper to call other functions for move mode.

    :param value_list: string list of input data of pokemon's name or id.
    :return: List of move objects
    """
    moves_url = [f"https://pokeapi.co/api/v2/move/{value}/" for value in value_list]
    moves_api_response = await get_requests(moves_url)
    move_objects = convert_json_into_move_object(moves_api_response)
    return move_objects

async def get_abilities(value_list) -> list:
    """
    Method that is a wrapper to call other functions for ability mode.

    :param value_list: string list of input data of pokemon's name or id.
    :return: List of ability objects
    """
    abilities_url = [f'https://pokeapi.co/api/v2/ability/{value}/' for value in value_list]
    abilities_api_response = await get_requests(abilities_url)
    abilities_objects = convert_json_into_ability_object(abilities_api_response)
    return abilities_objects

async def get_all_pokemon_api_data(value_list):
    """
    Method used to send get API requests for pokemon mode.

    :param value_list: input values for pokemon mode used for multiple get requests
    :return: list of JSON payload data from the api response.
    """
    pokemons_url = [f'https://pokeapi.co/api/v2/pokemon/{value}' for value in value_list]
    pokemons_data_api_response = await get_requests(pokemons_url)
    return pokemons_data_api_response

async def get_pokemon_abilities(data) -> list:
    """
    Method that is a wrapper to call other functions for pokemon mode to extract abilities.

    :param data: string list of input data of pokemon's name or id.
    :return: List of ability objects that is contained in pokemon API result.
    """
    # This function calls get_requests api to get all the json data of abilities, convert to obj Ability, return list of obj Ability

    abilities = data['abilities']
    abilities_url = [ability['ability']['url'] for ability in abilities]
    abilities_api_response = await get_requests(abilities_url)  # all the abilities response from PokeAPI
    ability_objects = convert_json_into_ability_object(abilities_api_response)
    return ability_objects

async def get_pokemon_stats(data) -> list:
    """
    Method that is a wrapper to call other functions for pokemon mode to extract stat.

    :param data: string list of input data of pokemon's name or id.
    :return: List of stat objects that is contained in pokemon API result.
    """
    # This function calls get_requests and then make a list of Stats objects, returns Stats obj list

    stats = data['stats']
    stats_url = [stat['stat']['url'] for stat in stats]
    stats_api_response = await get_requests(stats_url)  # all the stats response from PokeAPI
    stat_objects = convert_json_into_stat_object(stats_api_response)
    return stat_objects

async def get_pokemon_moves(data) -> list:
    """
    Method that is a wrapper to call other functions for pokemon mode to extract moves.

    :param data: string list of input data of pokemon's name or id.
    :return: List of move objects that is contained in pokemon API result.
    """
    # This function calls get_requests and then make a list of Move objects, returns Move obj list

    moves = data['moves']
    moves_url = [move['move']['url'] for move in moves]
    moves_api_response = await get_requests(moves_url)
    move_objects = convert_json_into_move_object(moves_api_response)
    return move_objects

def get_expanded_pokemon(data, moves: list, abilities: list, stats: list) -> Pokemon:
    """
    Method that deals when the expanded argument is mentioned in the command line terminal

    :param data: json payload data return from get pokemon API request
    :param moves: list of move objects
    :param abilities: list of ability objects
    :param stats: list of stats objects
    :return: pokemon object
    """
    ptypes = data['types']
    ptype_name = ", ".join([ptype['type']['name'] for ptype in ptypes])

    single_pokemon = Pokemon(
        data['height'],
        data['weight'],
        stats,
        ptype_name,
        abilities,
        moves,
        data['name'],
        data['id']
    )

    return single_pokemon

async def get_non_expanded_pokemon(data) -> Pokemon:
    """
    Method that deals when the expanded argument is not mentioned in the command line terminal

    :param data: json payload data return from get pokemon API request
    :return: pokemon object
    """
    stats = data['stats']
    stats_info = [(stat['stat']['name']).title() + ': ' + str(stat['base_stat']) for stat in stats]

    abilities = data['abilities']
    abilities_info = ['Name: ' + ability['ability']['name'] for ability in abilities]

    ptypes = data['types']
    ptype_name = ", ".join([ptype['type']['name'] for ptype in ptypes])

    moves = data['moves']
    moves_info = ["Move name: " + move["move"]["name"] + " | Level acquired: " + str(move["version_group_details"][0]["level_learned_at"]) for move in moves]

    single_pokemon = Pokemon(
        data['height'],
        data['weight'],
        stats_info,
        ptype_name,
        abilities_info,
        moves_info,
        data['name'],
        data['id']
    )

    return single_pokemon


from pygame.math import Vector2 as V2
from pygame.math import Vector3 as V3
import math
import random

def get_stl_lines(filename):
    f = open(filename, "r")
    lines = f.readlines()
    f.close()
    return [line.replace("\n","") for line in lines]

def get_vertex(line):
    if "vertex" in line:
        line = line.split(" ")
        x, y, z = line[-3],line[-2],line[-1]
        return V3(float(x),float(y),float(z)), "v"
    elif "normal" in line:
        line = line.split(" ")
        x, y, z = line[-3],line[-2],line[-1]
        return V3(float(x),float(y),float(z)), "n"
    return False

def get_triangles(lines):
    triangles = []
    k = 0
    vertices = [None, None, None]
    normal = None
    for line in lines:
        v = get_vertex(line)
        if v:
            if v[1] == "v":
                vertices[k] = v[0]
                k += 1
            elif v[1] == "n":
                normal = v[0]
        if k == 3:
            t = Triangle(vertices[0],vertices[1],vertices[2])
            t.n = normal
            triangles.append(t)
            assert t.n is not None
            k = 0
    assert k == 0
    return triangles

#les triangles font des references a des points, qui eux subissent les transfos!

class Triangle:

    def __init__(self, v1, v2, v3, color=None):
        self.v1 = v1
        self.v2 = v2
        self.v3 = v3
        self.n = None
        self.c = None
        self.color = color
        if color is None:
            self.color = V3(random.randint(0,255),random.randint(0,255),random.randint(0,255))
        self.ecolor = self.color
        self.neighs = []
        self.pd = None

    def compute_normal(self):
        e1 = self.v2 - self.v1
        e2 = self.v3 - self.v1
##        e3 = self.p3 - self.p2
        n1 = e1.cross(e2)
##        n2 = e1.cross(e3)
##        n3 = e2.cross(e3)
        #n1,n2,n3 yield the same result
        return n1

    def refresh_normal(self):
        self.n = self.compute_normal().normalize()

    def vertices(self):
        yield self.v1
        yield self.v2
        yield self.v3

##    def refresh_cd(self):
##        self.c = (self.v1 + self.v2 + self.v3)/3.
##        self.d = self.c.length_squared()

    def refresh_cd(self):
        self.c = (self.v1 + self.v2 + self.v3)/3.
        self.d = self.c.length_squared()

    def refresh_pd(self):
        self.pd = self.d
        counter = 1
        for buddy in self.neighs:
            a = self.n.angle_to(buddy.n)
            if a == 0:
                counter += 1
                self.pd += buddy.d
        self.pd /= counter

class Object3D:

    def __init__(self, filename):
        self.filename = filename
        self.lines = get_stl_lines(filename)
        self.triangles = get_triangles(self.lines)
        self.from_center = V3()
        vset = self.get_vertices_set()
        self.vertices = {val:V3(val) for val in vset}
        self.compactize()

    def compute_neighbours(self):
        for t1 in self.triangles:
            for t2 in self.triangles:
                if t1 != t2:
                    counter = 0
                    for v in t1.vertices():
                        if v == t2.v1:
                            counter += 1
                        elif v == t2.v2:
                            counter += 1
                        elif v == t2.v3:
                            counter += 1
                    if counter > 1:
                        t1.neighs.append(t2)

    def compactize(self):
        for t in self.triangles:
            t.v1 = self.vertices[tuple(t.v1)]
            t.v2 = self.vertices[tuple(t.v2)]
            t.v3 = self.vertices[tuple(t.v3)]

    def move(self, delta):
        for v in self.vertices.values():
            v += delta
        self.from_center += delta

    def rotate_x(self, angle, refresh=True):
        for v in self.vertices.values():
            v.rotate_x_ip(angle)
        if refresh:
            self.refresh_normals()

    def rotate_around_center_x(self, angle, refresh=True):
        tmp = V3(self.from_center)
        self.move(-tmp)
        self.rotate_x(angle, refresh)
        self.move(tmp)

    def rotate_y(self, angle, refresh=True):
        for v in self.vertices.values():
            v.rotate_y_ip(angle)
        if refresh:
            self.refresh_normals()

    def rotate_around_center_y(self, angle, refresh=True):
        tmp = V3(self.from_center)
        self.move(-tmp)
        self.rotate_y(angle, refresh)
        self.move(tmp)

    def rotate_z(self, angle, refresh=True):
        for v in self.vertices.values():
            v.rotate_z_ip(angle)
        if refresh:
            self.refresh_normals()

    def rotate_around_center_z(self, angle, refresh=True):
        tmp = V3(self.from_center)
        self.move(-tmp)
        self.rotate_z(angle, refresh)
        self.move(tmp)

##    def rotate_around_center(self, x,y,z):
##        tmp = V3(self.from_center)
##        self.move(-tmp)
##        for v in self.vertices.values():
##            v.rotate_x_ip(x)
##            v.rotate_y_ip(y)
##            v.rotate_z_ip(z)
##        self.move(tmp)
##        self.refresh_normals()

    def refresh_normals(self):
        for t in self.triangles:
            t.refresh_normal()

    def scale(self, factor): #account for this for rotations!
        for v in self.vertices.values():
            v *= factor
        self.refresh_normals()

    def refresh(self):
        for t in self.triangles:
            t.refresh_cd()
        for t in self.triangles:
            t.refresh_pd()
        self.triangles.sort(key=lambda x:x.pd, reverse=True)
##        self.triangles.sort(key=lambda x:x.d,reverse=True)

    def get_vertices_set(self):
        vset = set()
        for t in self.triangles:
            for v in[t.v1,t.v2,t.v3]:
                vset.add(tuple(v))
        return vset

class ManualObject3D(Object3D):

    def __init__(self, triangles):
        self.triangles = triangles
        self.from_center = V3()
        vset = self.get_vertices_set()
        self.vertices = {val:V3(val) for val in vset}
        self.compactize()


"""
    File 'data_import.py' used to load, preprocess, vectorize dataset
        and to find distributions across it.
"""
import pandas as pd
import numpy as np
import plotting

def loading_data():
    """
        Method to load initial Raiffeisenbank clients transactions dataset.
        return:
            data - pandas DataFrame of initial Raiffeisenbank clients
                transactions dataset
    """
    data = pd.read_csv("data/input/train_set.csv", delimiter=',',
                       index_col=False, dtype='unicode')

    return data

def preprocessing_data():
    """
        Method to preprocess and mapping data.
        return:
            1. data - pandas DataFrame of mapped data
            2. Numpy array (10000, ) with all customer ids
            3. Numpy array (82, ) with names of categories
    """
    # Data loading and picking only 3 feature that can be clustered
    data = loading_data()[['customer_id', 'amount', 'mcc']]

    # Loading of mapping category codes to mcc
    data_mapping = pd.read_csv("data/input/mcc2big.csv", delimiter=',',
                               index_col=False, dtype='unicode')
    data = pd.merge(data, data_mapping, on='mcc')

    # Loading of mapping category names on category codes
    data_mapping = pd.read_csv("data/input/87_categories.csv", delimiter=',',
                               index_col=False, dtype='unicode')
    data = pd.merge(data, data_mapping, on='category')

    # Dropping category codes and mcc
    data = data.drop(columns=['category', 'mcc'])

    return data, data['customer_id'].unique(), data['category_name'].unique()

def data_distributions(data, customer_ids, category_names):
    """
        Method to find and plot distributions across the dataset:
            1. Customers annual spendings
            2. Customers spendings
            3. Category transactions
            4. category average spendings
        param:
            1. data - pandas DataFrame of mapped data
            2. customer_ids - numpy array (10000, ) with all customer ids
            3. category_names - numpy array (82, ) with names of categories
    """
    annual_spendings, spendings, category_spendings = [], [], []
    category_numbers = np.zeros((len(category_names), ))

    # Find customers distributions
    for customer_id in customer_ids:
        customer_data = data[data['customer_id'] == customer_id]
        annual_spendings.append(pd.to_numeric(customer_data['amount']).sum())

        for i in range(len(customer_data.index)):
            spendings.append(pd.to_numeric(customer_data['amount'].values[i]))

    # Find categories distributions
    for i in range(len(category_names)):
        category_data = data[data['category_name'] == category_names[i]]
        category_numbers[i] += len(category_data.index)
        category_spendings.append(
            pd.to_numeric(category_data['amount']).mean())

    # Plot customers distributions
    plotting.hist_plotting(np.array(annual_spendings),
                           ["Annual spendings", "Frequency"],
                           "Customers annual spendings distribution")
    plotting.hist_plotting(np.array(spendings),
                           ["Spendings", "Frequency"],
                           "Customers spendings distribution")

    # Sort and plot category distributions
    category_numbers = np.array(category_numbers)
    sorted_indexes = np.argsort(category_numbers)

    plotting.bar_plotting([np.take_along_axis(category_numbers,
                                              sorted_indexes, axis=0),
                           np.take_along_axis(category_names,
                                              sorted_indexes, axis=0)],
                          ["Category", "Number of transactions"],
                          "Category transactions",
                          "data distributions")

    category_spendings = np.array(category_spendings)
    sorted_indexes = np.argsort(category_spendings)

    plotting.bar_plotting([np.take_along_axis(category_spendings,
                                              sorted_indexes, axis=0),
                           np.take_along_axis(category_names,
                                              sorted_indexes, axis=0)],
                          ["Category", "Average spendings"],
                          "Category average spendings",
                          "data distributions")

    return

def data_vectorization(data, customer_ids, category_names):
    """
        Method to vectorize dataset for topic modeling and clustering.
        param:
            1. data - pandas DataFrame of mapped data
            2. customer_ids - numpy array (10000, ) with all customer ids
            3. category_names - numpy array (82, ) with names of categories
        return:
            1. data_for_clustering - pandas DataFrame (10000, 82), where
                values are mean spendings of customers for every category
            2. data_for_lda - pandas DataFrame (10000, 82), where values are
                the transactions number of customers for every category
    """
    data_for_clustering = np.zeros((len(customer_ids), len(category_names)))
    data_for_lda = np.zeros((len(customer_ids), len(category_names)))

    # Compute customers mean spendings and number of transactions
    # for every category
    for i in range(len(customer_ids)):
        temp_id_data = data[data['customer_id'] == customer_ids[i]]
        for j in range(len(category_names)):
            temp_cat_data = temp_id_data[temp_id_data['category_name'] ==
                                         category_names[j]]
            data_for_clustering[i][j] = \
                pd.to_numeric(temp_cat_data['amount']).mean()
            data_for_lda[i][j] = temp_cat_data['amount'].count()

    # Create and save to csv DataFrame for clustering
    data_for_clustering = pd.DataFrame(data_for_clustering,
                                       columns=category_names,
                                       index=customer_ids)
    data_for_clustering.index.name = "customer_id"
    data_for_clustering = data_for_clustering.fillna(value=0)
    data_for_clustering.to_csv(
        path_or_buf="data/output/data_for_clustering.csv")

    # Create and save to csv DataFrame for topic modeling
    data_for_lda = pd.DataFrame(data_for_lda, columns=category_names,
                                index=customer_ids)
    data_for_lda.index.name = "customer_id"
    data_for_lda = data_for_lda.fillna(value=0)
    data_for_lda.to_csv(path_or_buf="data/output/data_for_lda.csv")

    return data_for_clustering, data_for_lda


"""Align multiple sequence FASTA files with MAFFT.

This script allows the user to concatenate multiple sequence files in FASTA format using the
concatenate_contigs() function and to align the multifasta file with the mafft_align() function.

"""

import os
import logging
import subprocess
from Bio import SeqIO
from Bio.Seq import Seq

def concatenate_contigs(contigs_list, out_file="all_mitogenomes.rotated.fa"):
    # create a list of BioSeqs that contains all contigs
    contigs = []
    for contig in contigs_list:
        with open(contig) as handle:
            for record in SeqIO.parse(handle, "fasta"):
                contigs.append(record)
    # write concatenated FASTA file
    with open(out_file, "w") as output_handle:
        SeqIO.write(contigs, output_handle, "fasta")

    return out_file

def mafft_align(multifasta_file, threads='1', out_file="all_mitogenomes.rotated.aligned.aln", clustal_format=False):
    if clustal_format:
        #logging.info("MAFFT output will be in clustal format")
        mafft = "mafft --quiet --clustalout --thread {} {} > {}".format(threads, multifasta_file, out_file)
    else:
        mafft = "mafft --quiet --thread {} {} > {}".format(threads, multifasta_file, out_file)
    logging.info("MAFFT alignment will be called with:\n" + mafft + "\n")
    subprocess.run(mafft, shell=True)

if __name__ == '__main__':
    contigs_files = []
    for curr_file in os.listdir('.'):
        if curr_file.endswith('mitogenome.rotated.fa'):
            contigs_files.append(curr_file)
    concatenate_contigs(contigs_files)


# *******************************************************
# @Fr4nc3
# 04/20/2013
# percolation module
# *******************************************************

import numpy as np
import matplotlib.pyplot as plt

def read_grid(input_file):
    """Create a site vacancy matrix from a text file."""
    N = input_file.readline().strip()
    b = []
    for line in input_file.readlines():
        b.append(list(map(int, line.rstrip())))

    # convert the array to numpy array
    a = np.array(b)
    return a

def write_grid(filename, sites):
    """Write a site vacancy matrix to a file."""
    N = str(len(sites))
    np.savetxt(filename, sites, fmt='%d', delimiter='', newline='\n', header=N,
               comments='')

def directed_flow(sites):
    """Returns a matrix of vacant/full sites (1=full, 0=vacant)."""
    n = len(sites)
    full = np.zeros((n, n), dtype='int32')
    for j in range(n):
        flow_from(sites, full, 0, j)
    return full

def flow_from(sites, full, i, j):
    """Adjusts the full array for flow from a single site."""
    n = len(sites)

    """ verify is the dimension are correct
    if not return and do nothing.
    """
    # if the range of i,j are offset return and do nothing
    if i < 0 or i >= n:
        return
    if j < 0 or j >= n:
        return

    # if the element i,j in sites is vacant do nothing
    if sites[i, j] == 0:
        return
    # if the element i,j in full matrix is full return and do nothing
    if full[i, j] == 1:
        return

    full[i, j] = 1  # set i, j  as full, in the full matrix

    # recursive call for down right left up flow.
    flow_from(sites, full, i + 1, j)
    flow_from(sites, full, i, j + 1)
    flow_from(sites, full, i, j - 1)
    flow_from(sites, full, i - 1, j)

def percolates(flow_matrix):
    """Returns a boolean if the flow_matrix exhibits percolation."""
    n = len(flow_matrix)
    full = directed_flow(flow_matrix)
    for j in range(n):
        if full[n - 1, j] == 1:
            return True
    return False

def generate(p):
    """return 1 / 0 in base of the random number."""
    return np.random.choice([0, 1], p=[1 - p, p])

def make_sites(n, p):
    """Returns an nxn site vacancy matrix
    Generates a numpy array representing an nxn site vacancy
    matrix with site vacancy probability p.
    """
    a = np.zeros((n, n), dtype='int32')
    for i in range(n):
        for j in range(n):
            a[i, j] = generate(p)

    return a

def show_perc(sites):
    """Displays a matrix using three colors for vacant, blocked, full
    Used to visualize directed flow on the matrix sites.
    """
    fig, ax = plt.subplots()
    full = directed_flow(sites)

    """ Meaning of the sum of the two matrix.
    # 2 (in blue) full
    # 1 (in light blue) open
    # 0 (in white) closed """
    matrix_flow = sites + full  # add two matrix

    ax.set_xticklabels([])  # remove labels
    ax.set_yticklabels([])

    ax.matshow(matrix_flow, cmap='Blues')  # show the matrix in blue colors.
    plt.savefig('percolation_visualization.pdf')
    plt.show()

def make_plot(trials, n):
    """generates and displays a graph of percolation p vs. vacancy p."""
    # initial variables
    p = 0  # probability start in 0
    vacancy_p = []
    percolation_p = []

    # runs until the probability is 1
    while p < 1:
        count_success = 0
        # loops of trials
        for j in range(trials):
            sites = make_sites(n, p)
            full = directed_flow(sites)

            if percolates(full):
                count_success += 1

        """percolation probability is the success number of percolation
        divided  by trials."""
        percolation_probability = count_success / trials

        # add values of p (probability) and percolation_probability
        vacancy_p.append(p)
        percolation_p.append(percolation_probability)

        # increases the probability p of percolation 0.04 per time
        p += 0.04

    # create plot x = vacancy_p and y = percolation_p
    plt.plot(vacancy_p, percolation_p, color="blue", linewidth=2, linestyle="-")

    # plot labels
    plt.xlabel('vacancy p')
    plt.ylabel('percolation p')
    plt.title('Percolation vs. Vacancy Probability')

    plt.savefig('percolation_plot.pdf')
    plt.show()


"""
Handle all of the file stuff.  Getting the files and saving files etc.
"""
from random import randint
import math
import os

class Files(object):
    def __init__(self):
        self.masterfile = "Vocab/vocab_master.txt"
        self.vocabfile = "Vocab/vocab.txt"
        self.checkForDuplicates()
        self.mergeFiles()
        self.fixVocabFiles()

    def openFile(self, filename, mode="r+"):
        return open(filename, mode, encoding='utf-8')

    def writeData(self, data, filename):
        with self.openFile(filename, "w+") as file:
            file.writelines(data)

    def appendData(self, line, filename):
        f = self.openFile(filename, "a+")
        f.write(line)
        f.close()

    def mergeFiles(self):
        '''Any new words we added to the master file we also want to add to the vocab file.'''
        masterdata = self.openFile(self.masterfile)
        data = self.openFile(self.vocabfile)
        words = []
        for line in data:
            words.append(line.split("::")[0])
        for line in masterdata:
            if line.split("::")[0] not in words:
                self.appendData(line, self.vocabfile)

    def checkForDuplicates(self):
        '''Go through the master vocab file and check for duplicate words (in english)'''
        data = self.readVocabFile(self.masterfile)
        newdata = []
        temp = []
        for line in data:
            word = line.split("::")[0]
            if word not in temp:
                temp.append(word)
                newdata.append(line)
        self.writeData(newdata, self.masterfile)

    def sortVocabFile(self):
        '''Sort the master vocab file in alphabetical (english)'''
        pass

    def fixVocabFiles(self):
        '''Make sure all lines have the 5 sections:  polish::english::right::wrong::last::streak::decks'''
        #f = open(self.vocabfile, "r+", encoding='utf-8')
        f = self.openFile(self.vocabfile)
        newlines = []
        for line in f:
            temp = line.split('\n')[0]
            newline = temp.split("::")
            if len(newline) == 2:
                temp += "::0::0::0::0::0::"
                newlines.append(temp)
            else:
                newlines.append(temp)

        f.close()
        #write changes back to file
        #f = open(self.vocabfile, "w+", encoding='utf-8')
        f = self.openFile(self.vocabfile, "w+")
        for line in newlines:
            f.write(line + "\n")
        f.close()

    def readVocabFile(self, vocabfile):
        data = []
        with self.openFile(vocabfile) as file:
        #with open(vocabfile, "r+", encoding='utf-8') as file:
            data = file.readlines()
        return data

    #def writeVocabFile(self, vocabfile, data):
    #    with self.openFile(vocabfile, "w+") as file:
    #    #with open(vocabfile, "w+", encoding='utf-8') as file:
    #        file.writelines(data)

    def generateRandomPack(self, num):
        '''Choose words randomly.'''
        words = self.readVocabFile(self.vocabfile)

        if len(words) < num:
            num = len(words)

        maxvalue = self.getMaxDeckValue(words)
        words, used = self.filterMostUsed(words, maxvalue, num)

        pack = []

        for i in list(range(num)):
           index = randint(0, len(words)-1)
           pack.append(words.pop(index))

        #self.writeVocabFile(self.vocabfile, words)

        self.writeData(words+used, self.vocabfile)

        return pack

    def getMaxDeckValue(self, words):
        '''return the maximum number of times a word has been added to a deck'''
        nums = []
        for word in words:
            nums.append(int(word.split("::")[6]))
        return max(nums)

    def filterMostUsed(self, words, maxvalue, num):
        temp = []
        used = []
        for word in words:
            val = int(word.split("::")[6])
            if val < maxvalue:
                temp.append(word)
            else:
                used.append(word)

        if len(temp) < num:
            return words, used
        return temp, used

    def fileWordsToFiles(self, pack):
        '''Send the temp words back to the vocab file'''
        #f = open(self.vocabfile, "a+", encoding='utf-8')
        f = self.openFile(self.vocabfile, "a+")
        for card in pack:
            card.regroup()
            f.write(card.raw)
        f.close()


# The Nature of Code
# Daniel Shiffman
# http://natureofcode.com

# A basic implementation of John Conway's Game of Life CA

import pygame
import random as rnd
import os.path

width = 640
height = 360
window = pygame.display.set_mode((width,height))
window.fill((0,0,0))  # Fills the screen with black

w = 8

class GOL:
    def __init__(self, r=None):
        global w
        self.w = w
        self.columns = int(width/self.w)
        self.rows = int(height/self.w)
        self.board = [[rnd.randint(0,2) for x in range(1, self.rows - 1)] for y in range(1, self.columns - 1)]

    def generate(self):
        nextGen = [[0 for x in range(1, self.rows - 1)] for y in range(1, self.columns - 1)]
        for x in range(1, self.columns - 3):
            for y in range(1, self.rows - 3):
                # Add up all the states in a 3x3 surrounding grid
                neighbors = 0
                for i in range(-1, 2):
                    for j in range(-1, 2):
                        neighbors += self.board[x + i][y + j]

                # A little trick to subtract the current cell's state since
                # we added it in the above loop
                neighbors -= self.board[x][y]

                # Rules of Life
                if ((self.board[x][y] == 1) and (neighbors < 2)): # Loneliness
                    nextGen[x][y] = 0
                elif ((self.board[x][y] == 1) and (neighbors > 3)): # Overpopulation
                    nextGen[x][y] = 0
                elif ((self.board[x][y] == 0) and (neighbors == 3)): # Reproduction
                    nextGen[x][y] = 1
                else:                                           # Stasis
                    nextGen[x][y] = self.board[x][y]

        self.board = nextGen # Next is now our board

    def display(self):
        window.fill((0,0,0))  # Fills the screen with black
        for i in range(0, self.columns - 3):
            for j in range(0, self.rows - 3):
                if(self.board[i][j] == 1):
                    pygame.draw.rect(window, pygame.Color('white'), (i * self.w, j * self.w, self.w, self.w))
                    #pygame.draw.rect(window, (255,255,255), (i * self.w, j * self.w, self.w, self.w))

        pygame.display.update()

gol = GOL()

def draw():
    global window
    global gol

    gol.generate()
    gol.display()

    # reset board when mouse is pressed
    for event in pygame.event.get():
        if event.type == pygame.MOUSEBUTTONDOWN:
            window.fill((0,0,0))  # Fills the screen with black
            gol = GOL()

while (True):
    draw()


import os
import signal
import time
import socket
import sys
from multiprocessing import Process

WATCHDOG_TIMEOUT = 10  # Timeout value in seconds
WATCHDOG_PORT = 3000  # Port number for the TCP connection

def handle_watchdog_signal(signum, frame):
    global server_ip
    print(f"Server {server_ip} cannot be reached.")
    sys.exit(0)

def keep_alive(tcp_socket):
    # Receive periodic "PING" messages to keep the TCP connection alive
    while True:
        message = tcp_socket.recv(1024)
        if not message:
            break

def main():
    global server_ip

    signal.signal(signal.SIGUSR1, handle_watchdog_signal)

    # Get the IP address of the server (localhost)
    server_ip = socket.gethostbyname("localhost")

    # Create a TCP socket
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # Set the SO_REUSEADDR and SO_REUSEPORT options
    tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    tcp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)

    # Bind the socket to the port
    server_address = ("localhost", WATCHDOG_PORT)
    tcp_socket.bind(server_address)

    # Listen for incoming connections
    tcp_socket.listen(1)

    # Accept a connection
    connection, client_address = tcp_socket.accept()

    # Run the keep_alive function in a separate process
    keep_alive_process = Process(target=keep_alive, args=(connection,))
    keep_alive_process.start()

    while True:
        start_time = time.time()
        time.sleep(WATCHDOG_TIMEOUT)
        elapsed_time = time.time() - start_time

        # Check if the elapsed time exceeds the timeout
        if elapsed_time >= WATCHDOG_TIMEOUT:
            handle_watchdog_signal(signal.SIGUSR1, None)

    # Terminate the keep_alive process before exiting
    keep_alive_process.terminate()

if __name__ == "__main__":
    main()


from .fuzzy_sets import FuzzySet, MF_object, Triangular_MF, SingletonsSet
from .rule_parsing import recursive_parse, preparse, postparse
from numpy import array, linspace, meshgrid, geomspace, log10, finfo, float64
from scipy.interpolate import interp1d
from copy import deepcopy
from collections import defaultdict, OrderedDict
import re
import string
from math import prod
try:
    from matplotlib.pyplot import figure, axes, plot, show, title, subplots, legend, colorbar
    matplotlib = True
except ImportError:
    matplotlib = False
try:
    import seaborn as sns
except:
    pass

# constant values
linestyles= ["-", "--", ":", "-."]

# for sanitization
valid_characters = string.ascii_letters + string.digits + "()_ "

class UndefinedUniverseOfDiscourseError(Exception):

    def __init__(self, message):
        self.message = message

class LinguisticVariable(object):
    """
        Creates a new linguistic variable.

        Args:
            FS_list: a list of FuzzySet instances.
            concept: a string providing a brief description of the concept represented by the linguistic variable (optional).
            universe_of_discourse: a list of two elements, specifying min and max of the universe of discourse. Optional, but it must be specified to exploit plotting facilities.
    """

    def __init__(self, FS_list=[], concept=None, universe_of_discourse=None):

        if FS_list==[]:
            raise Exception("ERROR: please specify at least one fuzzy set")
        self._universe_of_discourse = universe_of_discourse
        self._FSlist = FS_list
        self._concept = concept

    def get_values(self, v):
        result = {}
        for fs in self._FSlist:
            result[fs._term] = fs.get_value(v)
        return result

    def get_index(self, term):
        for n, fs in enumerate(self._FSlist):
            if fs._term == term: return n
        return -1

    def get_universe_of_discourse(self):
        """
        This method provides the leftmost and rightmost values of the universe of discourse of the linguistic variable.

        Returns:
            the two extreme values of the universe of discourse.
        """
        if self._universe_of_discourse is not None:
            return self._universe_of_discourse
        mins = []
        maxs = []
        try:
            for fs in self._FSlist:
                mins.append(min(fs._points.T[0]))
                maxs.append(max(fs._points.T[0]))
        except AttributeError:
            raise UndefinedUniverseOfDiscourseError("Cannot get the universe of discourse. Please, use point-based fuzzy sets or explicitly specify a universe of discourse")
        return min(mins), max(maxs)

    def draw(self, ax, TGT=None, element=None, highlight=None, xscale="linear"):
        """
        This method returns a matplotlib ax, representing all fuzzy sets contained in the liguistic variable.

        Args:
            ax: the matplotlib axis to plot to.
            TGT: (deprecated) show the memberships of a specific element of discourse TGT in the figure.
            element: show the memberships of a specific element of discourse in the figure.
            highlight: string, indicating the linguistic term/fuzzy set to highlight in the plot.
            xscale: default "linear", supported scales "log". Changes the scale of the xaxis.
        Returns:
            A matplotlib axis, representing all fuzzy sets contained in the liguistic variable.
        """
        if matplotlib == False:
            raise Exception("ERROR: please, install matplotlib for plotting facilities")
        if TGT is not None:
            print("Warning: argument 'TGT' is deprecated and will be discontinued in future relseases. Please use 'element'")
            element = TGT

        mi, ma = self.get_universe_of_discourse()
        if xscale == "linear":
            x = linspace(mi, ma, 10000)
        elif xscale == "log":
            if mi < 0 and ma > 0:
                x = geomspace(mi, -finfo(float64).eps, 5000) + geomspace(finfo(float64).eps, ma, 5000)
                # raise Exception("ERROR: cannot plot in log scale with negative universe of discourse")
            elif mi == 0:
                x = geomspace(finfo(float64).eps, ma, 10000)
            else:
                x = geomspace(mi, ma, 10000)
        else:
            raise Exception("ERROR: scale "+xscale+" not supported.")

        if highlight is None:
            linestyles= ["-", "--", ":", "-."]
        else:
            linestyles= ["-"]*4

        if element is not None:
            ax.plot([element, element], [0.0, 1], color="red", linestyle="--", linewidth=2.0, label="Value")

        for nn, fs in enumerate(self._FSlist):

            # singleton
            if isinstance(fs, SingletonsSet):
                xs = [pair[0] for pair in fs._funpointer._pairs]
                ys = [pair[1] for pair in fs._funpointer._pairs]
                ax.vlines(x=xs, ymin=0.0, ymax=ys, linestyles=linestyles[nn%4], color=next(ax._get_lines.prop_cycler)['color'], label=fs._term)

            elif fs._type == "function":
                y = [fs.get_value(xx) for xx in x]
                color = next(ax._get_lines.prop_cycler)['color']
                lw = 1

                if highlight==fs._term:
                    color="red"
                    lw =5
                elif highlight is not None:
                    color="lightgray"
                ax.plot(x,y, linestyles[nn%4], lw=lw, label=fs._term, color=color)

                # plot the membership degree on the fuzzy set
                if element is not None:
                    top = fs.get_value(element)
                    cut_y = [min(top, yy) for yy in y]
                    ax.fill([x[0]]+list(x)+[x[-1]],[0]+cut_y+[0] ,  color=color, alpha=0.5,)

            else:
                sns.regplot(x=fs._points.T[0], y=fs._points.T[1], marker="d", color="red", fit_reg=False, ax=ax)
                f = interp1d(fs._points.T[0], fs._points.T[1], bounds_error=False, fill_value=(fs.boundary_values[0], fs.boundary_values[1]))
                ax.plot(x, f(x), linestyles[nn%4], label=fs._term,)

        ax.set_xlabel(self._concept)
        ax.set_ylabel("Membership degree")
        ax.set_ylim(bottom=-0.05, top=1.05)
        if xscale == "log":
            ax.set_xscale("symlog", linthresh=10e-2)
            ax.set_xlim(x[0], x[-1])
        if highlight is None: ax.legend(loc="best")
        return ax

    def plot(self, outputfile="", TGT=None, element=None, highlight=None, xscale="linear"):
        """
        Shows a plot representing all fuzzy sets contained in the liguistic variable.

        Args:
            outputfile: path and filename where the plot must be saved.
            TGT: (deprecated) show the memberships of a specific element of discourse TGT in the figure.
            element: show the memberships of a specific element of discourse in the figure.
            highlight: string, indicating the linguistic term/fuzzy set to highlight in the plot.
            xscale: default "linear", supported scales "log". Changes the scale of the xaxis.
        """
        if matplotlib == False:
            raise Exception("ERROR: please, install matplotlib for plotting facilities")
        if TGT is not None:
            print("Warning: argument 'TGT' is deprecated and will be discontinued in future relseases. Please use 'element'")
            element = TGT

        fig, ax = subplots(1,1)
        self.draw(ax=ax, element=element, highlight=highlight, xscale=xscale)

        if outputfile != "":
            fig.savefig(outputfile)

        show()

    def __repr__(self):
        if self._concept is None:
            text = "N/A"
        else:
            text = self._concept
        return "<Linguistic variable '"+text+"', contains fuzzy sets %s, universe of discourse: %s>" % (str(self._FSlist), str(self._universe_of_discourse))

class AutoTriangle(LinguisticVariable):
    """
        Creates a new linguistic variable, whose universe of discourse is automatically divided in a given number of fuzzy sets.
        The sets are all symmetrical, normalized, and for each element of the universe their memberships sum up to 1.

        Args:
            n_sets: (integer) number of fuzzy sets in which the universe of discourse must be divided.
            terms: list of strings containing linguistic terms for the fuzzy sets (must be appropriate to the number of fuzzy sets).
            universe_of_discourse: a list of two elements, specifying min and max of the universe of discourse.
            verbose: True/False, toggles verbose mode.
    """

    def __init__(self, n_sets=3, terms=None, universe_of_discourse=[0,1], verbose=False):

        if n_sets<2:
            raise Exception("Cannot create linguistic variable with less than 2 fuzzy sets.")

        control_points = [x*1/(n_sets-1) for x in range(n_sets)]
        low = universe_of_discourse[0]
        high = universe_of_discourse[1]
        control_points = [low + (high-low)*x for x in control_points]

        if terms is None:
            terms = ['case %d' % (i+1) for i in range(n_sets)]

        FS_list = []

        FS_list.append(FuzzySet(function=Triangular_MF(low,low,control_points[1]), term=terms[0]))

        for n in range(1, n_sets-1):
            FS_list.append(
                FuzzySet(function=Triangular_MF(control_points[n-1], control_points[n], control_points[n+1]),
                    term=terms[n])
            )

        FS_list.append( FuzzySet(function=Triangular_MF(control_points[-2], high, high), term=terms[-1] ))

        super().__init__(FS_list, universe_of_discourse=universe_of_discourse)

        if verbose:
            for fs in FS_list:
                print(fs, fs.get_term())

class FuzzySystem(object):

    """
        Creates a new fuzzy system.

        Args:
            operators: a list of strings, specifying fuzzy operators to be used instead of defaults. Currently supported operators: 'AND_PRODUCT'.
            show_banner: True/False, toggles display of banner.
            sanitize_input: sanitize variables' names to eliminate non-accepted characters (under development).
            verbose: True/False, toggles verbose mode.
    """

    def __init__(self, operators=None, show_banner=True, sanitize_input=False, verbose=True):

        self._rules = []
        self._lvs = OrderedDict()
        self._variables = OrderedDict()
        self._crispvalues = OrderedDict()
        self._outputfunctions = OrderedDict()
        self._outputfuzzysets = OrderedDict()

        self._constants = []

        self._operators = operators

        self._detected_type = None

        self._sanitize_input = sanitize_input
        if sanitize_input and verbose:
            print (" * Warning: Simpful rules sanitization is enabled, please pay attention to possible collisions of symbols.")

        if show_banner: self._banner()

    def _banner(self):
        import pkg_resources
        vrs = pkg_resources.get_distribution('simpful').version
        print("  ____  __  _  _  ____  ____  _  _  __   ")
        print(" / ___)(  )( \\/ )(  _ \\(  __)/ )( \\(  ) v%s " % vrs)
        print(" \\___ \\ )( / \\/ \\ ) __/ ) _) ) \\/ (/ (_/\\ ")
        print(" (____/(__)\\_)(_/(__)  (__)  \\____/\\____/")
        print()
        print(" Created by Marco S. Nobile (m.s.nobile@tue.nl)")
        print(" and Simone Spolaor (simone.spolaor@unimib.it)")
        print()

    def get_fuzzy_sets(self, variable_name):
        """
            Returns the list of FuzzySet objects associated to one linguistic variable.

            Args:
                variable_name: name of the linguistic variable.

            Returns:
                a list containing FuzzySet objects.
        """
        try:
            return self._lvs[variable_name]._FSlist
        except ValueError:
            raise Exception("ERROR: linguistic variable %s does not exist" % variable_name)

    def get_fuzzy_set(self, variable_name, fs_name):
        """
            Returns a FuzzySet object associated to a linguistic variable.

            Args:
                variable_name: name of the linguistic variable.
                fs_name: linguistic term associated to the fuzzy set.

            Returns:
                a FuzzySet object.
        """
        try:
            LV =  self._lvs[variable_name]
        except ValueError:
            raise Exception("ERROR: linguistic variable %s does not exist" % (variable_name))

        for fs in LV._FSlist:
            if fs._term==fs_name:       return fs

        raise Exception("ERROR: fuzzy set %s of linguistic variable %s does not exist" % (fs_name, variable_name))

    def set_variable(self, name, value, verbose=False):
        """
        Sets the numerical value of a linguistic variable.

        Args:
            name: name of the linguistic variables to be set.
            value: numerical value to be set.
            verbose: True/False, toggles verbose mode.
        """
        if self._sanitize_input: name = self._sanitize(name)
        try:
            value = float(value)
            self._variables[name] = value
            if verbose: print(" * Variable %s set to %f" % (name, value))
        except ValueError:
            raise Exception("ERROR: specified value for "+name+" is not an integer or float: "+value)

    def set_constant(self, name, value, verbose=False):
        """
        Sets the numerical value of a linguistic variable to a constant value (i.e. ignore fuzzy inference).

        Args:
            name: name of the linguistic variables to be set to a constant value.
            value: numerical value to be set.
            verbose: True/False, toggles verbose mode.
        """
        if self._sanitize_input: name = self._sanitize(name)
        try:
            value = float(value)
            self._variables[name] = value
            self._constants.append(name)
            if verbose: print(" * Variable %s set to a constant value %f" % (name, value))
        except ValueError:
            raise Exception("ERROR: specified value for "+name+" is not an integer or float: "+value)

    def add_rules_from_file(self, path, verbose=False):
        """
        Imports new fuzzy rules by reading the strings from a text file.

        Args:
            path: path to the file containing the rules.
            verbose: True/False, toggles verbose mode.
        """
        if path[-3:].lower()!=".xls" and path[-4:].lower()!=".xlsx":
            with open(path) as fi:
                rules_strings = fi.readlines()
            self.add_rules(rules_strings, verbose=verbose)
        else:
            raise NotImplementedError("Excel support not available.")

    def _sanitize(self, rule):
        new_rule = "".join(ch for ch in rule if ch in valid_characters)
        return new_rule

    def add_rules(self, rules, verbose=False):
        """
        Adds new fuzzy rules to the fuzzy system.

        Args:
            rules: list of fuzzy rules to be added. Rules must be specified as strings, respecting Simpful's syntax.
            verbose: True/False, toggles verbose mode.
        """
        for rule in rules:

            # optional: remove invalid symbols
            if self._sanitize_input: rule = self._sanitize(rule)

            parsed_antecedent = recursive_parse(preparse(rule), verbose=verbose, operators=self._operators)
            parsed_consequent = postparse(rule, verbose=verbose)
            self._rules.append( [parsed_antecedent, parsed_consequent] )
            if verbose:
                print(" * Added rule IF", parsed_antecedent, "THEN", parsed_consequent)
                print()
        if verbose: print(" * %d rules successfully added" % len(rules))

    def get_rules(self):
        """
        Returns the rule base of the fuzzy system.

        Returns:
                a list containing the fuzzy rules as strings, in the same order they were added.
        """
        rule_base = []

        for r in self._rules:
            ant = str(r[0])
            ant = ant.replace("c.(", "(")
            ant = ant.replace("f.(", "(")
            ant = "IF "+ant

            cons = r[1]
            cons = " THEN ("+cons[0]+" IS "+cons[1]+")"

            rule = ant + cons
            rule_base.append(rule)

        return rule_base

    def replace_rule(self, i, new_rule, verbose=False):
        """
        Replaces the i-th rule in the FuzzySystem object. Rules are stored in the same order they were added.

        Args:
            i: index of the rule to be replaced in the fuzzy system.
            new_rule: fuzzy rule to be used for the replacement.
            verbose: True/False, toggles verbose mode.
        """

        if len(self._rules)<i or i<0:
            raise Exception("ERROR: rule number %d does not exist")

        if self._sanitize_input:
            new_rule = self._sanitize(new_rule)
        parsed_antecedent = recursive_parse(preparse(new_rule), verbose=verbose, operators=self._operators)
        parsed_consequent = postparse(new_rule, verbose=verbose)
        self._rules[i]=[parsed_antecedent, parsed_consequent]

        if verbose:
            print( " * Rule", i, "replaced with new rule: IF", parsed_antecedent, "THEN", parsed_consequent)
            print()

    def add_linguistic_variable(self, name, LV, verbose=False):
        """
        Adds a new linguistic variable to the fuzzy system.

        Args:
            name: string containing the name of the linguistic variable.
            LV: linguistic variable object to be added to the fuzzy system.
            verbose: True/False, toggles verbose mode.
        """
        if self._sanitize_input: name = self._sanitize(name)
        self._lvs[name]=deepcopy(LV)
        if self._lvs[name]._concept is None:
            self._lvs[name]._concept = name
        # self._lvs[name]=deepcopy(LV)
        if verbose: print(" * Linguistic variable '%s' successfully added" % name)

    def set_crisp_output_value(self, name, value, verbose=False):
        """
        Adds a new crisp output value to the fuzzy system.

        Args:
            name: string containing the identifying name of the crisp output value.
            value: numerical value of the crisp output value to be added to the fuzzy system.
            verbose: True/False, toggles verbose mode.
        """
        if self._sanitize_input: name = self._sanitize(name)
        self._crispvalues[name]=value
        if verbose: print(" * Crisp output value for '%s' set to %f" % (name, value))
        self._set_model_type("Sugeno")

    def set_output_function(self, name, function, verbose=False):
        """
        Adds a new output function to the fuzzy system.

        Args:
            name: string containing the identifying name of the output function.
            function: string containing the output function to be added to the fuzzy system.
                The function specified in the string must use the names of linguistic variables contained in the fuzzy system object.
            verbose: True/False, toggles verbose mode.
        """
        if self._sanitize_input: name = self._sanitize(name)
        self._outputfunctions[name]=function
        if verbose: print(" * Output function for '%s' set to '%s'" % (name, function))
        self._set_model_type("Sugeno")

    def _set_model_type(self, model_type):
        if self._detected_type == "inconsistent": return
        if self._detected_type is  None:
            self._detected_type = model_type
            print (" * Detected %s model type" % model_type )
        elif self._detected_type != model_type:
            print("WARNING: model type is unclear (simpful detected %s, but a %s output was specified)" % (self._detected_type, model_type))
            self._detected_type = 'inconsistent'

    def get_firing_strengths(self, input_values=None):
        """
            If "input_values" is not provided, it returns a list of the firing strengths of the the rules,
            given the current state of input variables.
            "input_values" is an optional argument, in the form of a dictionary containing a list of input states for each variable.
            In this second case, it returns a 2D list of firing strengths of the given states.

            Args:
                input_values: dictionary where the keys are names of linguistic variables and the values are a list of input values for that variable.

            Returns:
                a list containing rules' firing strengths, or a 2D list containing rules' firing strengths for each given input state.
        """
        if input_values is None:
            results = [float(antecedent[0].evaluate(self)) for antecedent in self._rules]
            return results
        else:
            firings = []
            for i in range(0, len(list(input_values.values())[0])):
                for var in input_values.keys():
                    self.set_variable(var, input_values[var][i])
                res = self.get_firing_strengths()
                firings.append(res)
            return firings

    def mediate(self, outputs, antecedent, results, ignore_errors=False, ignore_warnings=False, verbose=False):

        final_result = {}

        list_crisp_values = [x[0] for x in self._crispvalues.items()]
        list_output_funs  = [x[0] for x in self._outputfunctions.items()]

        for output in outputs:
            if verbose:
                print(" * Processing output for variable '%s'" %  output)
                print("   whose universe of discourse is:", self._lvs[output].get_universe_of_discourse())
                print("   contains the following fuzzy sets:", self._lvs[output]._FSlist )

            num = 0
            den = 0

            for (ant, res) in zip(antecedent, results):
                outname = res[0]
                outterm = res[1]
                weight = float(res[2])
                crisp = True

                if verbose:
                    print(" ** Rule composition:", ant, "->", res, ", output variable: '%s'" % outname, "with term: '%s'" % outterm)

                if outname==output:
                    if outterm not in list_crisp_values:
                        crisp = False
                        if outterm not in list_output_funs:
                            raise Exception("ERROR: one rule calculates an output named '"
                                + outterm
                                + "', but I cannot find it among the output terms.\n"
                                + " --- PROBLEMATIC RULE:\n"
                                + "IF " + str(ant) + " THEN " + str(res))
                    if crisp:
                        crispvalue = self._crispvalues[outterm]
                    elif isinstance(self._outputfunctions[outterm], MF_object):
                        raise Exception("ERROR in consequent of rule %s.\nSugeno reasoning does not support output fuzzy sets." % ("IF " + str(ant) + " THEN " + str(res)))
                    else:
                        string_to_evaluate = self._outputfunctions[outterm]
                        for k,v in self._variables.items():
                            # old version
                            # string_to_evaluate = string_to_evaluate.replace(k,str(v))

                            # match a variable name preceeded or followed by non-alphanumeric and _ characters
                            # substitute it with its numerical value
                            string_to_evaluate = re.sub(r"(?P<front>\W|^)"+k+r"(?P<end>\W|$)", r"\g<front>"+str(v)+r"\g<end>", string_to_evaluate)
                        crispvalue = eval(string_to_evaluate)

                    try:
                        value = ant.evaluate(self)
                    except RuntimeError:
                        raise Exception("ERROR: one rule could not be evaluated\n"
                        + " --- PROBLEMATIC RULE:\n"
                        + "IF " + str(ant) + " THEN " + str(res) + "\n")

                    temp = value*crispvalue*weight
                    num += temp
                    den += value

            try:
                if den == 0.0:
                    final_result[output] = 0.0
                    if not ignore_warnings:
                        print("WARNING: the sum of rules' firing for variable '%s' is equal to 0. The result of the Sugeno inference was set to 0." % output)
                else:
                    final_result[output] = num / den

            except ArithmeticError:
                if ignore_errors:
                    print("WARNING: cannot perform Sugeno inference for variable '%s'. The variable appears only as antecedent in the rules or an arithmetic error occurred." % output)
                else:
                    raise Exception("ERROR: cannot perform Sugeno inference for variable '%s'. The variable appears only as antecedent in the rules or an arithmetic error occurred." % output)

        return final_result

    def mediate_Mamdani(self,
        outputs,
        antecedent,
        results,
        ignore_errors=False,
        ignore_warnings=False,
        verbose=False,
        subdivisions=1000,
        aggregation_function=max):

        final_result = {}

        for output in outputs:
            if verbose:
                print(" * Processing output for variable '%s'" %  output)
                print("   whose universe of discourse is:", self._lvs[output].get_universe_of_discourse())
                print("   contains the following fuzzy sets:", self._lvs[output]._FSlist )
            cuts_list = defaultdict(list)

            x0, x1 = self._lvs[output].get_universe_of_discourse()

            for (ant, res) in zip(antecedent, results):
                outname = res[0]
                outterm = res[1]
                weight = float(res[2])

                if verbose:
                    print(" ** Rule composition:", ant, "->", res, ", output variable: '%s'" % outname, "with term: '%s'" % outterm)

                if outname==output:
                    try:
                        value = ant.evaluate(self)
                    except RuntimeError:
                        raise Exception("ERROR: one rule could not be evaluated\n"
                        + " --- PROBLEMATIC RULE:\n"
                        + "IF " + str(ant) + " THEN " + str(res) + "\n")

                    #degree of satisfaction of the rule is multiplied by rule weight
                    cuts_list[outterm].append(value*weight)

            values = []
            weightedvalues = []
            integration_points = linspace(x0, x1, subdivisions)

            convenience_dict = {}
            for k in cuts_list.keys():
                convenience_dict[k] = self._lvs[output].get_index(k)
            if verbose: print ( " * Indices:", convenience_dict)

            for u in integration_points:
                comp_values = []
                for k, v_list in cuts_list.items():
                    for v in v_list:
                        n = convenience_dict[k]
                        fs_term = self._lvs[output]._FSlist[n]
                        result = float(fs_term.get_value_cut(u, cut=v))
                        comp_values.append(result)
                #keep = max(comp_values)
                keep = aggregation_function(comp_values)
                values.append(keep)
                weightedvalues.append(keep*u)

            sumwv = sum(weightedvalues)
            sumv = sum(values)

            try:
                if sumv == 0.0:
                    CoG = 0
                    if not ignore_warnings:
                        print("WARNING: the sum of rules' firing for variable '%s' is equal to 0. The result of the Mamdani inference was set to 0." % output)
                else:
                    CoG = sumwv/sumv

            except ArithmeticError:
                if ignore_errors:
                    print("WARNING: cannot perform Mamdani inference for variable '%s'. The variable appears only as antecedent in the rules or an arithmetic error occurred." % output)
                else:
                    raise Exception("ERROR: cannot perform Mamdani inference for variable '%s'. The variable appears only as antecedent in the rules or an arithmetic error occurred." % output)

            if verbose: print (" * Weighted values: %.2f\tValues: %.2f\tCoG: %.2f"% (sumwv, sumv, CoG))
            final_result[output] = CoG

        return final_result

    def Sugeno_inference(self, terms=None, ignore_errors=False, ignore_warnings=False, verbose=False):
        """
        Performs Sugeno fuzzy inference.

        Args:
            terms: list of the names of the variables on which inference must be performed. If empty, all variables appearing in the consequent of a fuzzy rule are inferred.
            ignore_errors: True/False, toggles the raising of errors during the inference.
            ignore_warnings: True/False, toggles the raising of warnings during the inference.
            verbose: True/False, toggles verbose mode.

        Returns:
            a dictionary, containing as keys the variables' names and as values their numerical inferred values.
        """
        if self._sanitize_input and terms is not None:
            terms = [self._sanitize(term) for term in terms]

        # default: inference on ALL rules/terms
        if terms == None:
            temp = [rule[1][0] for rule in self._rules]
            terms = list(set(temp))
        else:
            # get rid of duplicates in terms to infer
            terms = list(set(terms))
            for t in terms:
                if t not in set([rule[1][0] for rule in self._rules]):
                    raise Exception("ERROR: Variable "+t+" does not appear in any consequent.")

        array_rules = array(self._rules, dtype='object')
        # cheking if the rule base is weighted, if not add dummy weights
        for n, cons in enumerate(array_rules.T[1]):
            if len(cons)<3:
                cons = cons + ("1.0",)
                array_rules.T[1][n] = cons

        if len(self._constants)==0:
            result = self.mediate(terms, array_rules.T[0], array_rules.T[1], ignore_errors=ignore_errors, ignore_warnings=ignore_warnings, verbose=verbose)
        else:
            #remove constant variables from list of variables to infer
            ncost_terms = [t for t in terms if t not in self._constants]
            result = self.mediate(ncost_terms, array_rules.T[0], array_rules.T[1], ignore_errors=ignore_errors, ignore_warnings=ignore_warnings, verbose=verbose)
            #add values of constant variables
            cost_terms = [t for t in terms if t in self._constants]
            for name in cost_terms:
                result[name] = self._variables[name]

        return result

    def Mamdani_inference(self, terms=None, subdivisions=1000, aggregation_function=max, ignore_errors=False, ignore_warnings=False, verbose=False):
        """
        Performs Mamdani fuzzy inference.

        Args:
            terms: list of the names of the variables on which inference must be performed.If empty, all variables appearing in the consequent of a fuzzy rule are inferred.
            subdivisions: the number of integration steps to be performed for calculating fuzzy set area (default: 1000).
            aggregation_function: pointer to function used to aggregate fuzzy sets during Mamdani inference, default is max. Use Python sum function, or simpful's probor function for sum and probabilistic OR, respectively.
            ignore_errors: True/False, toggles the raising of errors during the inference.
            ignore_warnings: True/False, toggles the raising of warnings during the inference.
            verbose: True/False, toggles verbose mode.

        Returns:
            a dictionary, containing as keys the variables' names and as values their numerical inferred values.
        """
        if self._sanitize_input and terms is not None:
            terms = [self._sanitize(term) for term in terms]

        # default: inference on ALL rules/terms
        if terms == None:
            temp = [rule[1][0] for rule in self._rules]
            terms= list(set(temp))
        else:
            # get rid of duplicates in terms to infer
            terms = list(set(terms))
            for t in terms:
                if t not in set([rule[1][0] for rule in self._rules]):
                    raise Exception("ERROR: Variable "+t+" does not appear in any consequent.")

        array_rules = array(self._rules, dtype=object)
        # cheking if the rule base is weighted, if not add dummy weights
        for n, cons in enumerate(array_rules.T[1]):
            if len(cons)<3:
                cons = cons + ("1.0",)
                array_rules.T[1][n] = cons

        if len(self._constants)==0:
            result = self.mediate_Mamdani(terms, array_rules.T[0], array_rules.T[1], ignore_errors=ignore_errors, ignore_warnings=ignore_warnings, verbose=verbose, subdivisions=subdivisions, aggregation_function=aggregation_function)
        else:
            #remove constant variables from list of variables to infer
            ncost_terms = [t for t in terms if t not in self._constants]
            result = self.mediate_Mamdani(ncost_terms, array_rules.T[0], array_rules.T[1], ignore_errors=ignore_errors, ignore_warnings=ignore_warnings, verbose=verbose, subdivisions=subdivisions, aggregation_function=aggregation_function)
            #add values of constant variables
            cost_terms = [t for t in terms if t in self._constants]
            for name in cost_terms:
                result[name] = self._variables[name]

        return result

    def probabilistic_inference(self, terms=None, ignore_errors=False, ignore_warnings=False, verbose=False):
        raise NotImplementedError()

    def inference(self, terms=None, subdivisions=1000, aggregation_function=max, ignore_errors=False, ignore_warnings=False, verbose=False):
        """
        Performs the fuzzy inference, trying to automatically choose the correct inference engine.

        Args:
            terms: list of the names of the variables on which inference must be performed. If empty, all variables appearing in the consequent of a fuzzy rule are inferred.
            subdivisions: the number of integration steps to be performed for calculating fuzzy set area (default: 1000).
            aggregation_function: pointer to function used to aggregate fuzzy sets during Mamdani inference, default is max. Use Python sum function, or simpful's probor function for sum and probabilistic OR, respectively.
            ignore_errors: True/False, toggles the raising of errors during the inference.
            ignore_warnings: True/False, toggles the raising of warnings during the inference.
            verbose: True/False, toggles verbose mode.

        Returns:
            a dictionary, containing as keys the variables' names and as values their numerical inferred values.
        """
        if self._detected_type == "Sugeno":
            return self.Sugeno_inference(terms=terms, ignore_errors=ignore_errors, ignore_warnings=ignore_warnings, verbose=verbose)
        elif self._detected_type == "probabilistic":
            return self.probabilistic_inference(terms=terms, ignore_errors=ignore_errors, ignore_warnings=ignore_warnings, verbose=verbose)
        elif self._detected_type is None: # default
            return self.Mamdani_inference(terms=terms, subdivisions=subdivisions, ignore_errors=ignore_errors, ignore_warnings=ignore_warnings, verbose=verbose, aggregation_function=aggregation_function)
        else:
            raise Exception("ERROR: simpful could not detect the model type, please use either Sugeno_inference() or Mamdani_inference() methods.")

    def plot_variable(self, var_name, outputfile="", TGT=None, element=None, highlight=None, ax=None, xscale="linear"):
        """
        Plots all fuzzy sets contained in a liguistic variable. Options for saving the figure and draw on a matplotlib ax are provided.

        Args:
            var_name: string containing the name of the linguistic variable to plot.
            outputfile: string containing path and filename where the plot must be saved.
            TGT: (deprecated) show the memberships of a specific element of discourse TGT in the figure.
            element: show the memberships of a specific element of discourse in the figure.
            highlight: string, indicating the linguistic term/fuzzy set to highlight in the plot.
            ax: a matplotlib ax where the variable will be plotted.
            xscale: default "linear", supported scales "log". Changes the scale of the xaxis.
        """
        if matplotlib == False:
            raise Exception("ERROR: please, install matplotlib for plotting facilities")
        if TGT is not None:
            print("Warning: argument 'TGT' is deprecated and will be discontinued in future relseases. Please use 'element'")
            element = TGT

        if ax != None:
            ax = self._lvs[var_name].draw(ax=ax, element=element, highlight=highlight, xscale=xscale)
            return ax
        self._lvs[var_name].plot(outputfile=outputfile, element=element, highlight=highlight, xscale=xscale)

    def produce_figure(self, outputfile="", max_figures_per_row=4, element_dict=None):
        """
        Plots the membership functions of each linguistic variable contained in the fuzzy system.

        Args:
            outputfile: string containing path and filename where the plot must be saved.
            max_figures_per_row: maximum number of figures per row in the plot.
            element_dict: dictionary of elements of the universe of discourse whose membership must be plotted over the fuzzy sets.
        """
        if matplotlib == False:
            raise Exception("ERROR: please, install matplotlib for plotting facilities")

        # use a defaultdict in case no target was specified
        if element_dict is None:
            element_dict = defaultdict(lambda: None)

        num_ling_variables = len(self._lvs)
        if num_ling_variables == 0:
            raise Exception("ERROR: no linguistic variables added. Please add at least one variable to plot")
        #print(" * Detected %d linguistic variables" % num_ling_variables)
        columns = min(num_ling_variables, max_figures_per_row)
        if num_ling_variables>max_figures_per_row:
            if num_ling_variables%max_figures_per_row==0:
                rows = num_ling_variables//max_figures_per_row
            else:
                rows = num_ling_variables//max_figures_per_row + 1
        else:
            rows = 1

        fig, ax = subplots(rows, columns, figsize=(columns*5, rows*5))

        if rows==1: ax = [ax]
        if columns==1: ax= [ax]

        n = 0
        for k, v in self._lvs.items():
            r = n%max_figures_per_row
            c = n//max_figures_per_row
            v.draw(ax[c][r], element=element_dict[k])
            ax[c][r].set_ylim(0,1.05)
            n+=1

        for m in range(n, columns*rows):
            r = m%max_figures_per_row
            c = m//max_figures_per_row
            ax[c][r].axis('off')

        fig.tight_layout()

        if outputfile != "":
            fig.savefig(outputfile)
        else:
            show()

    def plot_surface(self, variables, output, detail=40, color_map="plasma"):
        """
        Plots the surface induced by the rules.

        Args:
            variables: a pair of linguistic variables for the x and y axis.
            output: the output variable to be computed.
            detail: number of subdivisions along each axis.
            color_map: the color map to be used for the plot.

        Returns:
            a matplotlib figure object.
        """

        if len(variables)!=2:
            print("ERROR: please specify the two variables for the surface plot")
            return None

        v1, v2 = variables

        min_v1, max_v1 = self._lvs[v1].get_universe_of_discourse()
        min_v2, max_v2 = self._lvs[v2].get_universe_of_discourse()

        A = linspace(min_v1, max_v1, detail)
        B = linspace(min_v2, max_v2, detail)
        C = []

        for a in A:
            temp = []
            for b in B:
                self.set_variable(self._lvs[v1]._concept, a)
                self.set_variable(self._lvs[v2]._concept, b)
                res = self.Sugeno_inference()[output]
                temp.append( res )
            C.append(temp)
        C = array(C)

        A,B = meshgrid(A,B)

        fig = figure(figsize=(8,6))
        ax = axes(projection='3d')

        v = ax.plot_surface(A,B,C, shade=True, cmap=color_map)
        ax.set_xlabel(self._lvs[v1]._concept)
        ax.set_ylabel(self._lvs[v2]._concept)
        ax.set_zlabel(output)
        colorbar(v, ax=ax)
        fig.tight_layout()
        return fig

    def aggregate(self, list_variables, function):
        """
        Performs a fuzzy aggregation of linguistic variables contained in a FuzzySystem object.

        Args:
            list_variables: list of linguistic variables names in the FuzzySystem object to aggregate.
            function: pointer to an aggregation function. The function must accept as an argument a list of membership values.

        Returns:
            the aggregated membership values.
        """
        memberships = []
        for variable, fuzzyset in list_variables.items():
            value = self._variables[variable]
            result = self._lvs[variable].get_values(value)[fuzzyset]
            memberships.append(result)
        return function(memberships)

# useful pre-implemented functions for aggregation or implication during fuzzy inference
def prod(m_list):
    """
        Performs aggregation of membership values using the product operation.

        Args:
            m_list: list of membership values to aggregate.

        Returns:
            the aggregated membership value.
    """
    return prod(m_list)

def probor(m_list):
    """
        Performs aggregation of membership values using the probabilistic OR operation.

        Args:
            m_list: list of membership values to aggregate.

        Returns:
            the aggregated membership value.
    """
    res = m_list[0]
    if len(m_list) == 1:
        return m_list[0]
    for i in range(1, len(m_list)):
        res = res + m_list[i] - res * m_list[i]
    return res

if __name__ == '__main__':
    pass


import json
import logging
import os

accuracy = 2

# Set up logging configuration
logging.basicConfig(level=logging.WARNING, format='%(levelname)s - %(message)s')

def delete_files_in_folder(folder_path):
    i = 0
    # Iterate over all the files in the folder
    for filename in os.listdir(folder_path):
        file_path = os.path.join(folder_path, filename)

        # Check if the path is a file (not a directory)
        if os.path.isfile(file_path):
            # Delete the file
            os.remove(file_path)
            i += 1
    print(f"Deleted {i} files")

def write_json(element, json_folder):
    guid = element.guid
    height = round(element.element_planes[0].height, accuracy)
    width = 0
    delivery_number = element.delivery_number
    for plane in element.element_planes:
        width += plane.width
    width = round(width, accuracy)
    plane_count = element.plane_count

    planes = []

    def get_children_data(opening):
        opening_dict = {}
        opening_dict["HEIGHT"] = round(opening.height, accuracy)
        opening_dict["WIDTH"] = round(opening.width, accuracy)
        opening_plane = opening.local_opening_plane
        opening_dict["LOCATION"] = {"ORIGIN": opening_plane.get_origin_str(),
                                    "X_VECTOR": opening_plane.get_x_vec_str(),
                                    "Y_VECTOR": opening_plane.get_y_vec_str()}

        opening_dict["LEVEL"] = opening.level

        if opening.top is not None:
            opening_dict["TOP"] = {"PROFILE": opening.top.profile, "GUID": opening.top.guid}
        if opening.bottom is not None:
            opening_dict["BOTTOM"] = {"PROFILE": opening.bottom.profile, "GUID": opening.bottom.guid}
        if opening.left is not None:
            opening_dict["LEFT"] = {"PROFILE": opening.left.profile, "GUID": opening.left.guid}
        if opening.right is not None:
            opening_dict["RIGHT"] = {"PROFILE": opening.right.profile, "GUID": opening.right.guid}

        opening_dict["SPLIT DIRECTION"] = opening.split_direction

        opening_dict["CHILDREN"] = []

        if len(opening.children) == 0:
            opening_dict["TYPE"] = opening.type

        for child_opening in opening.children:
            opening_dict["CHILDREN"].append(get_children_data(child_opening))

        return opening_dict

    for plane in element.element_planes:
        plane_dict = {}
        plane_dict["HEIGHT"] = round(plane.height, accuracy)
        plane_dict["WIDTH"] = round(plane.width, accuracy)

        opening = plane.opening

        opening_dict = {}

        opening_dict["HEIGHT"] = round(opening.height, accuracy)
        opening_dict["WIDTH"] = round(opening.width, accuracy)

        opening_plane = opening.plane
        opening_dict["LOCATION"] = {"ORIGIN": opening_plane.get_origin_str(),
                                    "X_VECTOR": opening_plane.get_x_vec_str(),
                                    "Y_VECTOR": opening_plane.get_y_vec_str()}

        opening_dict["LEVEL"] = opening.level

        if opening.top is not None:
            opening_dict["TOP"] = {"PROFILE": opening.top.profile, "GUID": opening.top.guid}
        if opening.bottom is not None:
            opening_dict["BOTTOM"] = {"PROFILE": opening.bottom.profile, "GUID": opening.bottom.guid}
        if opening.left is not None:
            opening_dict["LEFT"] = {"PROFILE": opening.left.profile, "GUID": opening.left.guid}
        if opening.right is not None:
            opening_dict["RIGHT"] = {"PROFILE": opening.right.profile, "GUID": opening.right.guid}

        opening_dict["SPLIT DIRECTION"] = opening.split_direction

        opening_dict["CHILDREN"] = []

        if len(opening.children) == 0:
            opening_dict["TYPE"] = opening.type

        for child_opening in opening.children:
            opening_dict["CHILDREN"].append(get_children_data(child_opening))

        plane_dict["OPENING"] = opening_dict

        planes.append(plane_dict)

    data = {
        "GUID": guid,
        "HEIGHT": height,
        "WIDTH": width,
        "PLANE COUNT": plane_count,
        "PLANES": planes,
        "DELIVERY_NUMBER": delivery_number
    }

    # serialize the data to JSON format

    json_data = json.dumps(data, indent=4)

    # create a new file and write the JSON data to it
    file_path = json_folder + '/' + guid + '.json'

    with open(file_path, "w") as file:
        file.write(json_data)


#
# @lc app=leetcode.cn id=1446 lang=python3
#
# [1446] 连续字符
#

# @lc code=start
class Solution:
    def maxPower(self, s: str) -> int:
        i,j = 0,1
        res = 1
        tmp = -1
        while j < len(s):
            if s[i] == s[j]:
                j += 1
                res += 1

            elif s[i] != s[j]:
                if tmp < res:
                    tmp = res
                res = 1
                i = j
                j += 1
        return max(res,tmp)
# @lc code=end

print(Solution().maxPower(s = "cc"))


import sys, time, argparse
import subprocess, os.path, pathlib
import math, shutil
from xml.etree.ElementInclude import default_loader

Description = """FASTQ file compression tool
For example
    {exe} example.txt

--------------------------
Command line options:
--------------------------
""".format(exe=sys.argv[0])

cmd7z = "7z a -mm=PPMd"
cmdbsc = "external/libbsc/bsc e"
cmdreorder = "reorder.py"
sap = "./sap"
lex = "./lex"

def main():
    parser = argparse.ArgumentParser(description=Description, formatter_class=argparse.RawTextHelpFormatter)
    parser.add_argument('input',           help='input file name', type=str, nargs='+')
    parser.add_argument('-o', '--output',  help='output base name (def. input base name)', default="", type=str)
    parser.add_argument('--7z',            help='compression 7 zip: compression of the file with 7 zip', action='store_true', default = False)
    parser.add_argument('--bsc',           help='compression bsc: compression of the file with bsc', action='store_true', default = False)
    parser.add_argument('-1', '--c1',      help='compression 1: compression of the input file', action='store_true', default = False)
    parser.add_argument('-2', '--c2',      help='compression 2: compression of the file in partitions', action='store_true', default = False)
    parser.add_argument('-3', '--c3',      help='compression 3: compression of the file in 3-components and partitions', action='store_true', default = False)
    parser.add_argument('-r', '--reorder', help='reorder option: 1 (lexicographical order), 2 (colexicographical order) or 3(random order)', default=False, type=int)
    parser.add_argument('-p', '--part',    help='size of the part for file division in MB', default=False, type=int)
    parser.add_argument('-t', '--test',    help='compression test: original file', default="", type=str)
    parser.add_argument('-v',              help='verbose: extra info in the log file',action='store_true')
    args = parser.parse_args()

    define_basename(args)

    path = os.path.split(sys.argv[0])[0]
    path_inp = os.path.split(sys.argv[1])[0]
    inp = os.path.splitext(sys.argv[1])[0]

    if(args.bsc): #if the compression is with bsc
        op_comp = 0
    else: #if the compression is with 7zip
        op_comp = 1

    file_inp = sys.argv[1]
    if(args.reorder):
        op = args.reorder
        reorder(sys.argv[1], inp, op)
        if(op == 1):
            r = '_lex'
            file_inp = inp + "_lex.fastq"
        elif(op == 2):
            r = '_colex'
            file_inp = inp + "_colex.fastq"
        elif(op == 3):
            r = '_random'
            file_inp = inp + "_random.fastq"

    if(args.output): #all output files named args.argv[1]
        inp_aux = os.path.join(path_inp, args.output)
        if(args.reorder):
            inp_aux = inp_aux + r

    else: #all output files named --output
        inp_aux = inp
        if(args.reorder):
            inp_aux = inp + r

    if(op_comp == 1):
        logfile_name = inp_aux + ".zip.log"
        file_out = inp_aux + ".zip"
        file_outh = inp_aux + "_header.zip"
        file_outd = inp_aux + "_dna.zip"
        file_outq = inp_aux + "_qs.zip"
    else:
        logfile_name = inp_aux + ".bsc.log"
        file_out = inp_aux + ".bsc"
        file_outh = inp_aux + "_header.bsc"
        file_outd = inp_aux + "_dna.bsc"
        file_outq = inp_aux + "_qs.bsc"

    file_header = inp_aux + ".header"
    file_dna = inp_aux + ".dna"
    file_qs = inp_aux + ".qs"
    file_aux = inp_aux + "_"

    if(args.c1): #if the compression is total
        aux = 1
    if(args.c2): #if the compression is in partitions
        aux = 2
    if(args.c3): #if the compression is in 3-components + partitions
        aux = 3

    print("Sending logging messages to file:", logfile_name)

    with open(logfile_name,"a") as logfile:
        print(">>> Begin computation",file=logfile)
        show_command_line(logfile)
        logfile.flush()

        if(args.part):
            part = args.part*(2**20) # X MB
        else:
            part = 10*(2**20) # 10 MB

        sarq = os.path.getsize(file_inp)
        if(args.test):
            inp_original = os.path.splitext(args.test)[0]
            file_data = inp_original + "_" + str(aux) + "_data.txt" #create the document with the compression and time data
            f_data = open(file_data, 'a') #open file data

        ###### print data from the compression ######
        print("\n\n###### COMPRESSION ######\n")
        print("Size .fastq: {:.6f} bytes\n".format(sarq))

        if(args.c1): #if the compression is total

            t = compression(file_out, file_inp, logfile_name, op_comp ) #return of the time compression
            sarq_c = os.path.getsize(file_out)
            ratio = (sarq_c/sarq)*100

            print("Size .zip: {:.6f} bytes".format(sarq_c))
            print("Compression 1 (FASTQ): {:.6f}".format(sarq_c/sarq))
            print("Time 1: {:.6f}".format(t))
            if(args.test):
                f_data.write("{:.6f} {:.6f} {:.2f}\n".format(sarq_c/sarq, t, ratio))

        if(args.c2): #if the compression is in partitions (size set in the input command)

            parts = partition(file_aux, part, file_inp)
            cont = 1
            sarq_c = 0
            t = 0
            while(cont <= parts):
                file_part = file_aux + str(cont) + ".part"
                if(op_comp == 1):
                    file_outp = file_aux + str(cont) + ".zip"
                else:
                    file_outp = file_aux + str(cont) + ".bsc"

                t = t + compression(file_outp, file_part, logfile_name, op_comp) #return of the time compression, sum of the times
                sarq_c = sarq_c + os.path.getsize(file_outp)
                cont = cont + 1
            ratio = (sarq_c/sarq)*100

            print("Compression 3 (sum parts): {:.6f}".format(sarq_c/sarq))
            print("Time: {:.6f}".format(t))
            if(args.test):
                f_data.write("{:.6f} {:.6f} {:.2f}\n".format(sarq_c/sarq, t, ratio))

        if(args.c3): #if the compression is in 3-components (header, dna and qs)

            t = 0
            components(file_header, file_dna, file_qs, file_inp)

            file_h = os.path.splitext(file_outh)[0] + "_"
            file_d = os.path.splitext(file_outd)[0] + "_"
            file_q = os.path.splitext(file_outq)[0] + "_"

            parts_h = partition(file_h, part, file_header)
            parts_d = partition(file_d, part, file_dna)
            parts_q = partition(file_q, part, file_qs)

            cont = 1
            sarq_c = 0
            t = 0
            while(cont <= parts_h or cont <= parts_d or cont <= parts_q):

                if(cont <= parts_h):
                    file_part = file_h + str(cont) + ".part"
                    if(op_comp == 1):
                        file_outp = file_h + str(cont) + ".zip"
                    else:
                        file_outp = file_h + str(cont) + ".bsc"

                    t = t + compression(file_outp, file_part, logfile_name, op_comp) #return of the time compression, sum of the times
                    sarq_c = sarq_c + os.path.getsize(file_outp)

                if(cont <= parts_d):
                    file_part = file_d + str(cont) + ".part"
                    if(op_comp == 1):
                        file_outp = file_d + str(cont) + ".zip"
                    else:
                        file_outp = file_d + str(cont) + ".bsc"
                    t = t + compression(file_outp, file_part, logfile_name, op_comp) #return of the time compression, sum of the times
                    sarq_c = sarq_c + os.path.getsize(file_outp)

                if(cont <= parts_q):
                    file_part = file_q + str(cont) + ".part"
                    if(op_comp == 1):
                        file_outp = file_q + str(cont) + ".zip"
                    else:
                        file_outp = file_q + str(cont) + ".bsc"

                    t = t + compression(file_outp, file_part, logfile_name, op_comp) #return of the time compression, sum of the times
                    sarq_c = sarq_c + os.path.getsize(file_outp)
                cont = cont + 1
            ratio = (sarq_c/sarq)*100

            print("Compression 3-components with partitions (sum parts): {:.6f}".format(sarq_c/sarq))
            print("Time: {:.6f}".format(t))
            if(args.test):
                f_data.write("{:.6f} {:.6f} {:.2f}\n".format(sarq_c/sarq, t, ratio))

        if(args.test):
            f_data.close() #close data file

##########################################################################################################
###### function of the reordered ######
##########################################################################################################
def reorder(file_inp, file_output, op):
    command = "python3 {reorder} {input} {output} {op}".format(reorder=cmdreorder, input=file_inp, output=file_output, op=op)
    execute_command(command)

##########################################################################################################
###### function of the compression ######
##########################################################################################################
def compression(file_out, file_input, logfile, op_comp):
    if(op_comp == 1): #compression with 7zip
        cmd = cmd7z
        command =  cmd + " {zip} {input} >> {log}".format(zip=file_out, input=file_input, log=logfile)
    else: #compression with bsc
        cmd = cmdbsc
        command =  cmd + " {input} {zip} >> {log}".format(zip=file_out, input=file_input, log=logfile)

    #execution of the command
    t = execute_command(command)
    return t #return time de compression

##########################################################################################################
###### function of the 3-components division ######
##########################################################################################################
def components(file_header, file_dna, file_qs, file_inp):

    f = open(file_inp, 'r')
    f_header = open(file_header, 'w') #file with header
    f_dna = open(file_dna, 'w') #file with dna
    f_qs = open(file_qs, 'w') #file with qs

    s = None
    while(s != ""):
        s = f.readline()
        f_header.write(s)
        s = f.readline()
        f_dna.write(s)
        #+
        s = f.readline()
        s = f.readline()
        f_qs.write(s)

    f_header.close()
    f_dna.close()
    f_qs.close()
    f.close()

##########################################################################################################
###### function of the partition ######
##########################################################################################################
def partition(file_aux, part, file_inp):

    cont = 1 #count the number of parts

    file_part = file_aux + str(cont) + ".part"
    f_part = open(file_part, 'w')
    f = open(file_inp, 'r')
    sum = 0
    s = None
    while(s != ""):
        for i in range(4):
            s = f.readline()
            sum += len(s)
            f_part.write(s) #header, dna, +, qs

        if(s == "" and sum < part):
            cont = cont + 1
        elif(sum >= part):
            cont = cont + 1
            sum = 0
            f_part.close()
            file_part = file_aux + str(cont) + ".part"
            f_part = open(file_part, 'w')

    f_part.close()
    f.close()
    return cont-1

##########################################################################################################

def define_basename(args):
    if len(args.output)==0:
        args.basename = args.input[0]
    else:
        args.basename = args.output

##########################################################################################################
###### function of the command execution #######
##########################################################################################################
def execute_command(command):
    try:
        start = time.time()
        subprocess.call(command, shell = True)
        t = time.time()-start
    except subprocess.CalledProcessError:
        print("Error to executing command line: {command}".format(command = command))
        return False
    return t #return time of the execution

##########################################################################################################

def show_command_line(f):
    f.write("Python command line: ")
    for x in sys.argv:
        f.write(x+" ")
    f.write("\n")

##########################################################################################################

if __name__ == '__main__':
    main()


# 4) [Programming] Write a program that search for the integer 120 in the
# following list of integers using the binary search algorithm.
#
# 12 34 37 45 57 82 99 120 134

searchValue = 12
arr = [12, 34, 37, 45, 57, 82, 99, 120, 134]
unsearchedIndex = [0, len(arr)-1]

while True:
    centerIndex = int((unsearchedIndex[0] + unsearchedIndex[1]) / 2)
    centerValue = arr[centerIndex]
    if unsearchedIndex[0] > unsearchedIndex[1]:
        print(f"There is no {searchValue} in this array")
        break
    elif centerValue == searchValue:
        print(f"{searchValue}'s index is {centerIndex}")
        break
    elif centerValue > searchValue:
        unsearchedIndex[1] = centerIndex - 1
    elif centerValue < searchValue:
        unsearchedIndex[0] = centerIndex + 1