from __future__ import absolute_import, division, print_function, unicode_literals

import argparse
import configparser
import datetime
import logging
import os
import sys

import cnes.common.service_config_file as service_config_file
import cnes.common.service_error as service_error
import cnes.common.service_logger as service_logger

import cnes.common.lib.my_timer as my_timer
import cnes.common.lib.my_tools as my_tools
import cnes.common.lib_lake.lake_db as lake_db
import cnes.common.lib_lake.locnes_filenames as locnes_filenames
import cnes.common.lib_lake.proc_lake as proc_lake
import cnes.common.lib_lake.proc_pixc_vec as proc_pixc_vec

import cnes.sas.lake_tile.sas_lake_tile as sas_lake_tile
import cnes.sas.lake_tile.proc_pixc as proc_pixc

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

class PGELakeTile():
    """
    Class PGELakeTile
    Pseudo PGE class to launch LakeTile SAS processing
    """

    def __init__(self, cmd_file):
        """
        Constructor of PGELakeTile

        :param cmd_file: command file full path
        :type cmd_file: string
        """

        # 0 - Init timer
        self.timer = my_timer.Timer()
        self.timer.start()

        # 1 - Load command file
        self.cmd_file = cmd_file
        my_tools.test_file(cmd_file, in_extent=".cfg")  # Test existance and extension
        my_params = self._read_cmd_file()  # Read parameters
        self.pixc_file = my_params["pixc_file"]
        self.pixcvec_river_file = my_params["pixcvec_river_file"]
        self.output_dir = my_params["output_dir"]

        # 2 - Load parameter file
        # 2.1 - Read value from command file
        file_config = my_params["param_file"]
        # 2.2 - Test existence
        if not os.path.exists(file_config):
            raise service_error.DirFileError(file_config)
        # 2.3 - Load parameters
        self.cfg = service_config_file.ServiceConfigFile(file_config)

        # 3 - Put command parameter inside cfg
        self._put_cmd_value(my_params)
        # Retrieve options which are optionnal
        self.flag_prod_shp = self.cfg.getboolean("OPTIONS", "Produce shp")
        self.flag_inc_file_counter = self.cfg.getboolean("OPTIONS", "Increment file counter")
        self.flag_write_full_path = self.cfg.getboolean("OPTIONS", "Write full path")

        # 4 - Init data object
        self.obj_pixcvec = None
        self.obj_pixc = None
        self.obj_lake_db = None
        self.obj_lake = None
        self.lake_tile_filenames = None

        # 5 - Initiate logging service
        service_logger.ServiceLogger()
        logger = logging.getLogger(self.__class__.__name__)

        # 6 - Print info
        logger.sigmsg("======================================")
        logger.sigmsg("===== LakeTileProcessing = BEGIN =====")
        logger.sigmsg("======================================")
        logger.info("> INPUT PIXC file = " + str(self.cfg.get("PATHS", "PIXC file")))
        logger.info("> INPUT PIXCVecRiver file = " + str(self.cfg.get("PATHS", "PIXCVecRiver file")))
        logger.info("> OUTPUT directory = " + str(self.cfg.get("PATHS", "Output directory")))
        logger.info("======================================")
        message = "> Command file: " + str(self.cmd_file)
        logger.info(message)
        message = "> " + str(self.cfg)
        logger.info(message)
        logger.info("======================================")

        # 7 - Test input parameters
        logger.info(">> Check config parameters type and value")
        self._check_config_parameters()

        # 8 - Form processing metadata dictionary
        self.proc_metadata = {}
        self.proc_metadata["history"] = "%sZ: Creation" % my_tools.swot_timeformat(datetime.datetime.utcnow(), in_format=3)
        if self.cfg.get("DATABASES", "LAKE_DB") is not None:
            self.proc_metadata["xref_prior_lake_db_file"] = self.cfg.get("DATABASES", "LAKE_DB")
        else:
            self.proc_metadata["xref_prior_lake_db_file"] = ""
        self.proc_metadata["xref_l2_hr_pixc_file"] = self.pixc_file
        self.proc_metadata["xref_l2_hr_pixcvecriver_file"] = self.pixcvec_river_file
        self.proc_metadata["xref_param_l2_hr_laketile_file"] = file_config

        logger.info("")

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

    def _read_cmd_file(self):
        """
        Read the parameter file and store parameters in a dictionary

        :return: dictionary containing parameters
        :rtype: dict
        """

        # 0 - Init output dictionary
        out_params = {}
        # Default values
        out_params["flag_prod_shp"] = "False"
        out_params["flag_inc_file_counter"] = "True"
        out_params["flag_write_full_path"] = "False"

        # 1 - Read parameter file
        config = configparser.ConfigParser()
        config.read(self.cmd_file)

        # 2 - Retrieve PATHS
        try:
            out_params["param_file"] = os.path.expandvars(config.get("PATHS", "param_file"))
        except:
            out_params["param_file"] = os.path.join(sys.path[0], "lake_tile_param.cfg")
            #out_params["param_file"] = os.path.join(os.getcwd(), "lake_tile_param.cfg")
        out_params["pixc_file"] = config.get("PATHS", "PIXC file")
        list_paths = config.options("PATHS")
        if ("PIXCVecRiver file" in list_paths) or ("pixcvecriver file" in list_paths):
            out_params["pixcvec_river_file"] = config.get("PATHS", "PIXCVecRiver file")
        else:
            out_params["pixcvec_river_file"] = ""
        out_params["output_dir"] = config.get("PATHS", "Output directory")

        # 3 - Retrieve DATABASES
        out_params["LAKE_DB"] = None
        out_params["LAKE_DB_ID"] = None
        # TODO : None in filename if no lake DATABASES or basins file
        if "DATABASES" in config.sections():
            list_db = config.options("DATABASES")
            # Prior lake database
            if "lake_db" in list_db:
                out_params["LAKE_DB"] = config.get("DATABASES", "LAKE_DB")
            if "lake_db_id" in list_db:
                out_params["LAKE_DB_ID"] = config.get("DATABASES", "LAKE_DB_ID")

        # 4 - Retrieve OPTIONS
        if "OPTIONS" in config.sections():
            list_options = config.options("OPTIONS")
            # Flag to also produce LakeTile_Edge and LakeTile_PIXCVec as shapefiles (=True); else=False (default)
            if "produce shp" in list_options:
                out_params["flag_prod_shp"] = config.get("OPTIONS", "Produce shp")
            # Flag to increment the file counter in the output filenames (=True, default); else=False
            if "increment file counter" in list_options:
                out_params["flag_inc_file_counter"] = config.get("OPTIONS", "Increment file counter")
            # To write full path in global attributes (=True); to write only basename=False
            if "write full path" in list_options:
                out_params["flag_write_full_path"] = config.get("OPTIONS", "Write full path")

        # 5 - Retrieve LOGGING
        error_file = config.get("LOGGING", "errorFile")
        out_params["errorFile"] = os.path.splitext(error_file)[0] + "_" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + os.path.splitext(error_file)[1]
        log_file = config.get("LOGGING", "logFile")
        out_params["logFile"] = os.path.splitext(log_file)[0] + "_" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + os.path.splitext(log_file)[1]
        out_params["logfilelevel"] = config.get("LOGGING", "logfilelevel")
        out_params["logConsole"] = config.get("LOGGING", "logConsole")
        out_params["logconsolelevel"] = config.get("LOGGING", "logconsolelevel")

        # 6 - Retrieve FILE_INFORMATION
        # Name of producing agency
        out_params["INSTITUTION"] = config.get("FILE_INFORMATION", "INSTITUTION")
        # Product version
        out_params["PRODUCT_VERSION"] = config.get("FILE_INFORMATION", "PRODUCT_VERSION")
        # Composite Release IDentifier for LakeTile processing
        out_params["CRID"] = config.get("FILE_INFORMATION", "CRID")
        # Version identifier of the product generation executable (PGE)
        out_params["PGE_VERSION"] = config.get("FILE_INFORMATION", "PGE_VERSION")
        # Contact
        out_params["CONTACT"] = config.get("FILE_INFORMATION", "CONTACT")

        return out_params

    def _put_cmd_value(self, param_list):
        """
        Add command parameters to the class.
        All parameters added here come from the command file.
        This is usefull to centralize all processing parameters.

        :param param_list: dictionary with command parameters
        :type param_list: dict
        """

        try:

            # Add LOGGING section and parameters
            section = "LOGGING"
            self.cfg.add_section(section)
            self.cfg.set(section, "errorFile", param_list["errorFile"])
            self.cfg.set(section, "logFile", param_list["logFile"])
            self.cfg.set(section, "logfilelevel", param_list["logfilelevel"])
            self.cfg.set(section, "logConsole", param_list["logConsole"])
            self.cfg.set(section, "logconsolelevel", param_list["logconsolelevel"])

            # Add PATHS section and parameters
            section = "PATHS"
            self.cfg.add_section(section)
            self.cfg.set(section, "PIXC file", param_list["pixc_file"])
            self.cfg.set(section, "PIXCVecRiver file", param_list["pixcvec_river_file"])
            self.cfg.set(section, "Output directory", param_list["output_dir"])

            # Add DATABASES section and parameters
            section = "DATABASES"
            self.cfg.add_section(section)
            self.cfg.set(section, "LAKE_DB", param_list["LAKE_DB"])
            self.cfg.set(section, "LAKE_DB_ID", param_list["LAKE_DB_ID"])

            # Add OPTIONS section and parameters
            section = "OPTIONS"
            self.cfg.add_section(section)
            self.cfg.set(section, "Produce shp", param_list["flag_prod_shp"])
            self.cfg.set(section, "Increment file counter", param_list["flag_inc_file_counter"])
            self.cfg.set(section, "Write full path", param_list["flag_write_full_path"])

            # Add section FILE_INFORMATION
            section = "FILE_INFORMATION"
            self.cfg.add_section(section)
            self.cfg.set(section, "INSTITUTION", param_list["INSTITUTION"])
            self.cfg.set(section, "PRODUCT_VERSION", param_list["PRODUCT_VERSION"])
            self.cfg.set(section, "CRID", param_list["CRID"])
            self.cfg.set(section, "PGE_VERSION", param_list["PGE_VERSION"])
            self.cfg.set(section, "CONTACT", param_list["CONTACT"])

        except Exception:
            print("Something wrong happened in _put_cmd_value !")
            raise

    def _check_config_parameters(self):
        """
        Check parameters coherence for LakeTile parameter file

        :return: True if OK
        :rtype: boolean
        """
        logger = logging.getLogger(self.__class__.__name__)
        logger.debug("- start -")

        try:

            # 1 - Config parameters from command file

            # 1.1 - PATH section
            # PIXC file
            self.cfg.test_var_config_file('PATHS', 'PIXC file', str, logger=logger)
            my_tools.test_file(self.cfg.get('PATHS', 'PIXC file'))
            logger.debug('OK - PIXC file = ' + str(self.cfg.get('PATHS', 'PIXC file')))
            # PIXCVecRiver file
            list_paths = self.cfg.options('PATHS')
            if ('PIXCVecRiver file' in list_paths) and (self.cfg.get('PATHS', 'PIXCVecRiver file') != ""):
                self.cfg.test_var_config_file('PATHS', 'PIXCVecRiver file', str, logger=logger)
                my_tools.test_file(self.cfg.get('PATHS', 'PIXCVecRiver file'))
                logger.debug('OK - PIXCVecRiver file = ' + str(self.cfg.get('PATHS', 'PIXCVecRiver file')))
            else:
                logger.debug("OK - NO PIXCVecRiver file in input")
            # Output directory
            self.cfg.test_var_config_file('PATHS', 'Output directory', str, logger=logger)
            my_tools.test_dir(self.cfg.get('PATHS', 'Output directory'))
            logger.debug('OK - Output directory = ' + str(self.cfg.get('PATHS', 'Output directory')))

            # 1.2 - DATABASES section
            # Lake database full path
            if self.cfg.get('DATABASES', 'LAKE_DB') is None:
                logger.debug('WARNING - LAKE_DB not filled => LakeTile product not linked to a lake database')
            elif self.cfg.get('DATABASES', 'LAKE_DB').endswith(".shp"):
                self.cfg.test_var_config_file('DATABASES', 'LAKE_DB', str, logger=logger)
                my_tools.test_file(self.cfg.get('DATABASES', 'LAKE_DB'))
                logger.debug('OK - LAKE_DB = ' + str(self.cfg.get('DATABASES', 'LAKE_DB')))
                # Lake identifier attribute name in the database
                if self.cfg.get('DATABASES', 'LAKE_DB_ID'):
                    self.cfg.test_var_config_file('DATABASES', 'LAKE_DB_ID', str, logger=logger)
                    logger.debug('OK - LAKE_DB_ID = ' + str(self.cfg.get('DATABASES', 'LAKE_DB_ID')))
                else :
                    logger.warning('WARNING - LAKE_DB file given but the lake_id fieldname is missing')
            elif self.cfg.get('DATABASES', 'LAKE_DB').endswith(".sqlite"):
                self.cfg.test_var_config_file('DATABASES', 'LAKE_DB', str, logger=logger)
                my_tools.test_file(self.cfg.get('DATABASES', 'LAKE_DB'))
                logger.debug('OK - LAKE_DB = ' + str(self.cfg.get('DATABASES', 'LAKE_DB')))
            elif os.path.isdir(self.cfg.get('DATABASES', 'LAKE_DB')):
                self.cfg.test_var_config_file('DATABASES', 'LAKE_DB', str, logger=logger)
                my_tools.test_dir(self.cfg.get('DATABASES', 'LAKE_DB'))
                logger.debug('OK - LAKE_DB directory = ' + str(self.cfg.get('DATABASES', 'LAKE_DB')))
            else:
                logger.debug('WARNING - Unknown LAKE_DB file format for file: %s => LakeTile product not linked to a lake database' % self.cfg.get('DATABASES', 'LAKE_DB'))

            # 1.3 - OPTIONS section
            # Shapefile production
            self.cfg.test_var_config_file('OPTIONS', 'Produce shp', bool, logger=logger)
            logger.debug('OK - Produce shp = ' + str(self.cfg.get('OPTIONS', 'Produce shp')))
            # Increment output file counter
            self.cfg.test_var_config_file('OPTIONS', 'Increment file counter', bool, logger=logger)
            logger.debug('OK - Increment file counter = ' + str(self.cfg.get('OPTIONS', 'Increment file counter')))
            # Write full path in global attributes
            self.cfg.test_var_config_file('OPTIONS', 'Write full path', bool, logger=logger)
            logger.debug('OK - Write full path = ' + str(self.cfg.get('OPTIONS', 'Write full path')))

            # 1.4 - FILE_INFORMATION section
            # Name of producing agency
            self.cfg.test_var_config_file('FILE_INFORMATION', 'INSTITUTION', str, logger=logger)
            logger.debug('OK - INSTITUTION = ' + str(self.cfg.get('FILE_INFORMATION', 'INSTITUTION')))
            # Product version
            self.cfg.test_var_config_file('FILE_INFORMATION', 'PRODUCT_VERSION', str, logger=logger)
            logger.debug('OK - PRODUCT_VERSION = ' + str(self.cfg.get('FILE_INFORMATION', 'PRODUCT_VERSION')))
            # Composite Release IDentifier for LakeTile processing
            self.cfg.test_var_config_file('FILE_INFORMATION', 'CRID', str, logger=logger)
            logger.debug('OK - CRID = ' + str(self.cfg.get('FILE_INFORMATION', 'CRID')))
            # Version identifier of the product generation executable (PGE)
            self.cfg.test_var_config_file('FILE_INFORMATION', 'PGE_VERSION', str, logger=logger)
            logger.debug('OK - PGE_VERSION = ' + str(self.cfg.get('FILE_INFORMATION', 'PGE_VERSION')))
            # Contact
            self.cfg.test_var_config_file('FILE_INFORMATION', 'CONTACT', str, logger=logger)
            logger.debug('OK - CONTACT = ' + str(self.cfg.get('FILE_INFORMATION', 'CONTACT')))

            # 2 - Config parameters from parameter file

            # 2.1 - CONFIG_PARAMS section

            # List of classif flags to keep for processing: 2=land_near_water  3=water_near_land   4=open_water  5=dark_water
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'CLASSIF_LIST', str, val_default="2;3;4;5", logger=logger)
            logger.debug('OK - CLASSIF_LIST = ' + str(self.cfg.get('CONFIG_PARAMS', 'CLASSIF_LIST')))
            # True/False list of classif flags which contain observed water (1-to-1 correspondance with CLASSIF_LIST)
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'CLASSIF_WATER', str, val_default="True;True;True;False", logger=logger)
            logger.debug('OK - CLASSIF_WATER = ' + str(self.cfg.get('CONFIG_PARAMS', 'CLASSIF_WATER')))
            # True/False list of classif flags to use for lake boundary construction (1-to-1 correspondance with CLASSIF_LIST)
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'CLASSIF_4HULL', str, val_default="True;True;True;True", logger=logger)
            logger.debug('OK - CLASSIF_4HULL = ' + str(self.cfg.get('CONFIG_PARAMS', 'CLASSIF_4HULL')))
            # True/False list of classif flags to use for WSE computation (1-to-1 correspondance with CLASSIF_LIST)
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'CLASSIF_4WSE', str, val_default="False;False;True;False", logger=logger)
            logger.debug('OK - CLASSIF_4WSE = ' + str(self.cfg.get('CONFIG_PARAMS', 'CLASSIF_4WSE')))
            # True/False list of classif flags to use for lake area computation (1-to-1 correspondance with CLASSIF_LIST)
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'CLASSIF_4AREA', str, val_default="True;True;True;True", logger=logger)
            logger.debug('OK - CLASSIF_4AREA = ' + str(self.cfg.get('CONFIG_PARAMS', 'CLASSIF_4AREA')))
            # True/False list of classif flags to use water fraction for inundated computation (1-to-1 correspondance with CLASSIF_LIST)
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'USE_FRACTIONAL_INUNDATION', str, val_default="True;True;False;False", logger=logger)
            logger.debug('OK - USE_FRACTIONAL_INUNDATION = ' + str(self.cfg.get('CONFIG_PARAMS', 'USE_FRACTIONAL_INUNDATION')))

            # List of quality flags to consider for PixC selection
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'PIXC_QUAL_LIST', str,
                                          val_default="interferogram_qual;classification_qual;geolocation_qual", logger=logger)
            logger.debug('OK - PIXC_QUAL_LIST = ' + str(self.cfg.get('CONFIG_PARAMS', 'PIXC_QUAL_LIST')))
            # Mask of quality flags to use for PixC selection (1-to-1 correspondance with PIXC_QUAL_LIST)
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'PIXC_QUAL_MASK', str, val_default="0;0;0", logger=logger)
            logger.debug('OK - PIXC_QUAL_MASK = ' + str(self.cfg.get('CONFIG_PARAMS', 'PIXC_QUAL_MASK')))
            # To exclude PixC having bright_land_flag=1 (=True) or not (=False, default)
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'EXCLUDE_BRIGHT_LAND', bool, val_default=False, logger=logger)
            logger.debug('OK - EXCLUDE_BRIGHT_LAND = ' + str(self.cfg.get('CONFIG_PARAMS', 'EXCLUDE_BRIGHT_LAND')))

            # Min cross-track distance to select PixC
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'MIN_XTRACK', float, val_default=-1.0, logger=logger)
            logger.debug('OK - MIN_XTRACK = ' + str(self.cfg.get('CONFIG_PARAMS', 'MIN_XTRACK')))
            # Max cross-track distance to select PixC
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'MAX_XTRACK', float, val_default=-1.0, logger=logger)
            logger.debug('OK - MAX_XTRACK = ' + str(self.cfg.get('CONFIG_PARAMS', 'MAX_XTRACK')))

            # Min size for a lake to generate a lake product (=polygon + attributes) for it
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'MIN_SIZE', float, val_default=0.01, logger=logger)
            logger.debug('OK - MIN_SIZE = ' + str(self.cfg.get('CONFIG_PARAMS', 'MIN_SIZE')))
            # Maximum number of azimuth tiles fully covered by a lake processed in LakeSP; -1 to disable (default)
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'MAX_NB_TILES_FULL_AZ', int, val_default=-1, logger=logger)
            logger.debug('OK - MAX_NB_TILES_FULL_AZ = ' + str(self.cfg.get('CONFIG_PARAMS', 'MAX_NB_TILES_FULL_AZ')))
            # Min percentage of overlapping area to consider PLD lake linked to an observed feature
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'MIN_OVERLAP', float, val_default=1.0, logger=logger)
            logger.debug('OK - MIN_OVERLAP = ' + str(self.cfg.get('CONFIG_PARAMS', 'MIN_OVERLAP')))
            # Method to compute area_total attribute
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'AREA_METHOD', str, val_default="pixc", logger=logger)
            logger.debug('OK - AREA_METHOD = ' + str(self.cfg.get('CONFIG_PARAMS', 'AREA_METHOD')))
            # Method to compute height-segmentation
            # 1 = Felzenszwalb
            # 2 = SLIC
            # 3 = Unsupervised thresholding
            # 4 = Watershed segmentation method
            # 5 = k-means clustering method
            # 6 = hierarchical clustering
            # 7 (default) = Otsu thresholding
            # 8 = MeanShift
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'SEGMENTATION_METHOD', int, val_default=7, logger=logger)
            logger.debug('OK - SEGMENTATION_METHOD = ' + str(self.cfg.get('CONFIG_PARAMS', 'SEGMENTATION_METHOD')))

            # To improve PixC geolocation (=True) or not (=False)
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'IMP_GEOLOC', bool, val_default=True, logger=logger)
            logger.debug('OK - IMP_GEOLOC = ' + str(self.cfg.get('CONFIG_PARAMS', 'IMP_GEOLOC')))
            # Method to compute lake boundary or polygon hull
            # 0 = convex hull
            # 1.0 = concave hull computed in ground geometry, based on Delaunay triangulation - using CGAL library
            # 1.1 = concave hull computed in ground geometry, based on Delaunay triangulation (time consuming)
            # 1.2 = concave hull computed in ground geometry, based on Delaunay triangulation with alpha parameter varying along range
            # 2.0 = edge computed in radar geometry, then converted in ground geometry by reordering lon lat (default)
            # 2.1 = edge computed in radar geometry, then converted in ground geometry by checking each segment
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'HULL_METHOD', float, valeurs=[0, 1.0, 1.1, 1.2, 2.0, 2.1], val_default=2.0, logger=logger)
            logger.debug('OK - HULL_METHOD = ' + str(self.cfg.get('CONFIG_PARAMS', 'HULL_METHOD')))
            # max number of pixel for hull computation 1
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'NB_PIX_MAX_DELAUNEY', int, val_default=100000, logger=logger)
            logger.debug('OK - NB_PIX_MAX_DELAUNEY = ' + str(self.cfg.get('CONFIG_PARAMS', 'NB_PIX_MAX_DELAUNEY')))

            # Big lakes parameters for improved geoloc
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'BIGLAKE_MODEL', str, valeurs=["polynomial", "no"], val_default="polynomial", logger=logger)
            logger.debug('OK - BIGLAKE_MODEL = ' + str(self.cfg.get('CONFIG_PARAMS', 'BIGLAKE_MODEL')))
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'BIGLAKE_MIN_SIZE', float, val_default=50.0, logger=logger)
            logger.debug('OK - BIGLAKE_MIN_SIZE = ' + str(self.cfg.get('CONFIG_PARAMS', 'BIGLAKE_MIN_SIZE')))
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'BIGLAKE_GRID_SPACING', float, val_default=4000, logger=logger)
            logger.debug('OK - BIGLAKE_GRID_SPACING = ' + str(self.cfg.get('CONFIG_PARAMS', 'BIGLAKE_GRID_SPACING')))
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'BIGLAKE_GRID_RES', float, val_default=8000, logger=logger)
            logger.debug('OK - BIGLAKE_GRID_RES = ' + str(self.cfg.get('CONFIG_PARAMS', 'BIGLAKE_GRID_RES')))

            # Input data for storage change computation
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'STOCC_INPUT', str, val_default="pld", logger=logger)
            logger.debug('OK - STOCC_INPUT = ' + str(self.cfg.get('CONFIG_PARAMS', 'STOCC_INPUT')))

            # Threshold of good pixels within a lake to set quality_f
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'THRESHOLD_4NOMINAL', float, val_default=0.7, logger=logger)
            logger.debug('OK - THRESHOLD_4NOMINAL = ' + str(self.cfg.get('CONFIG_PARAMS', 'THRESHOLD_4NOMINAL')))
            # Nb digits for counter of lakes in a tile or pass
            self.cfg.test_var_config_file('CONFIG_PARAMS', 'NB_DIGITS', str, val_default=6, logger=logger)
            logger.debug('OK - NB_DIGITS = ' + str(self.cfg.get('CONFIG_PARAMS', 'NB_DIGITS')))

        # Error managed
        except service_error.ConfigFileError:
            message = "Error in the configuration file " + self.cfg.path_conf
            logger.error(message, exc_info=True)
            raise

        # Warning error not managed !
        except Exception:
            logger.error("Something wrong happened during configuration file check!", exc_info=True)
            raise

        return True

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

    def _read_input_data(self):
        """
        This method prepares and init input data classes
        """
        logger = logging.getLogger(self.__class__.__name__)

        # 1 - Init PIXCVec object by retrieving data from the pixel cloud complementary file after river processing, if it exists
        logger.info("> 1.1 - Init pixel cloud complementary file...")
        self.obj_pixcvec = proc_pixc_vec.PixelCloudVec("TILE")
        if self.pixcvec_river_file != "":
            self.obj_pixcvec.set_from_pixcvec_file(self.pixcvec_river_file)
        else:
            logger.warning("NO PIXVecRiver file specified => ALL pixels of PIXC file are considered as part of a lake or an unassigned feature")
        logger.info("")

        # 2 - Init PIXC object by retrieving needed data from the pixel cloud
        logger.info("> 1.2 - Retrieving needed data from the pixel cloud...")
        self.obj_pixc = proc_pixc.PixelCloud()
        self.obj_pixc.set_from_pixc_file(self.pixc_file, self.obj_pixcvec.reject_index)
        logger.info("")

        # 3 - Retrieve orbit info from PIXC filename and compute output filenames
        logger.info("> 1.3 - Retrieving tile infos from PIXC filename...")
        self.lake_tile_filenames = locnes_filenames.LakeTileFilenames(self.pixc_file,
                                                                      self.pixcvec_river_file,
                                                                      self.output_dir,
                                                                      flag_inc=self.flag_inc_file_counter)

    def _read_lake_db(self):
        """
        This method prepares Lake DB class, and init with Lake DB data

        Possible combinations:
            - in_tile_ref=not None, others=None => LakeTile processing with operational PLD
            - in_tile_poly=not None, others=None
                - with in_az_0_line=not None, in_az_max_line=not None => LakeTile processing with user lake DB
        """
        logger = logging.getLogger(self.__class__.__name__)

        lake_db_file = self.cfg.get("DATABASES", "LAKE_DB")

        if (lake_db_file == "") or (lake_db_file is None):  # No PLD
            logger.warning("NO database specified -> NO link of SWOT obs with a priori lake")
            self.obj_lake_db = lake_db.LakeDb()

        else:  # Init PLD object wrt to file type

            type_db = lake_db_file.split('.')[-1]  # File type

            if type_db == "shp":  # User lake DB file in shapefile format
                logger.debug("Use of personal user lake database in shapefile format")
                self.obj_lake_db = lake_db.LakeDbShp(lake_db_file,
                                                     in_poly=self.obj_pixc.tile_poly,
                                                     in_az_0_line=self.obj_pixc.az_0_line,
                                                     in_az_max_line=self.obj_pixc.az_max_line)

            elif type_db == "sqlite":  # User or operational lake DB file in SQLite format
                if os.path.basename(lake_db_file).startswith(locnes_filenames.PLD_PREFIX_BASE):  # Operationnal PLD filename
                    logger.debug("Use of operational Prior Lake Database (specific filename given)")
                    self.obj_lake_db = lake_db.LakeDbSqlite(lake_db_file,
                                                            in_tile_ref=self.lake_tile_filenames.tile_ref)
                else:
                    logger.debug("Use of personal user lake database in SQLite format")
                    self.obj_lake_db = lake_db.LakeDbSqlite(lake_db_file,
                                                            in_poly=self.obj_pixc.tile_poly,
                                                            in_az_0_line=self.obj_pixc.az_0_line,
                                                            in_az_max_line=self.obj_pixc.az_max_line)

            elif os.path.isdir(lake_db_file):  # Directory of operational Prior Lake Database (PLD)
                logger.debug("Use of operational Prior Lake Database (upper directory given)")
                # Compute PLD filename
                lakedb_filenames = locnes_filenames.PldFilenames("TILE",
                                                                 lake_db_file,
                                                                 cycle_num=self.lake_tile_filenames.cycle_num,
                                                                 pass_num=self.lake_tile_filenames.pass_num)
                # Init PLD object
                if os.path.exists(lakedb_filenames.filename):
                    self.obj_lake_db = lake_db.LakeDbSqlite(lakedb_filenames.filename,
                                                            in_tile_ref=self.lake_tile_filenames.tile_ref)
                    self.proc_metadata["xref_prior_lake_db_file"] = lakedb_filenames.filename
                else:
                    logger.warning("NO related operational Prior Lake Database file found -> NO database specified")
                    self.obj_lake_db = lake_db.LakeDb()

            else:
                message = "Prior Lake Database format (%s) is unknown: must be .shp or .sqlite => set to None" % type_db
                logger.warning(message)
                self.obj_lake_db = lake_db.LakeDb()

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

    def _prepare_output_data(self):
        """
        This method creates output data classes
        """
        logger = logging.getLogger(self.__class__.__name__)

        # 1 - Initialize LakeTile product object
        logger.info("> 3.1 - Initialize LakeTile product object...")
        self.obj_lake = proc_lake.LakeProduct("TILE",
                                              self.obj_pixc,
                                              self.obj_pixcvec,
                                              self.obj_lake_db,
                                              os.path.basename(self.lake_tile_filenames.lake_tile_shp_file_obs).split(".")[0])

    def _write_output_data(self):
        """
        This method write output data
        """
        logger = logging.getLogger(self.__class__.__name__)

        # 1 - Write LakeTile shapefiles
        logger.info("> 8.1 - Writing LakeTile memory layer to shapefiles...")
        # 1.1 - Obs-oriented shapefile
        logger.info(". Obs-oriented shapefile = %s" % self.lake_tile_filenames.lake_tile_shp_file_obs)
        self.obj_lake.write_obs_file(self.lake_tile_filenames.lake_tile_shp_file_obs, self.proc_metadata)
        # 1.2 - PLD-oriented shapefile
        logger.info(". PLD-oriented shapefile = %s" % self.lake_tile_filenames.lake_tile_shp_file_prior)
        self.obj_lake.write_prior_file(self.lake_tile_filenames.lake_tile_shp_file_prior, self.proc_metadata)
        # 1.3 - Shapefile of unassigned water features
        logger.info(". Shapefile of unassigned water features = %s" % self.lake_tile_filenames.lake_tile_shp_file_unknown)
        self.obj_lake.write_unknown_file(self.lake_tile_filenames.lake_tile_shp_file_unknown, self.proc_metadata)
        # 1.4 - Close memory layer
        self.obj_lake.free_memory()
        logger.info("")

        # 2 - Write PIXCVec for objects entirely inside tile
        logger.info("> 8.2 - Writing LakeTile_PIXCVec file...")
        self.obj_pixcvec.write_file(self.lake_tile_filenames.lake_tile_pixcvec_file,
                                    self.proc_metadata)
        if self.flag_prod_shp:
            if self.obj_pixcvec.nb_water_pix == 0:
                logger.info("NO water pixel => NO LakeTile_PIXCVec shapefile produced")
            else:
                self.obj_pixcvec.write_file_as_shp(self.lake_tile_filenames.lake_tile_pixcvec_file.replace(".nc", ".shp"), self.obj_pixc)
        logger.info("")

        # 3 - Write intermediate NetCDF file with indices of pixels (and associated label) related to objects at the top/bottom edges of the tile
        logger.info("> 8.3 - Writing LakeTile_Edge file...")
        self.obj_pixc.write_edge_file(self.lake_tile_filenames.lake_tile_edge_file, self.proc_metadata)
        if self.flag_prod_shp:
            if self.obj_pixc.nb_edge_pix == 0:
                logger.info("NO edge pixel => NO LakeTile_Edge shapefile produced")
            else:
                self.obj_pixc.write_edge_file_as_shp(self.lake_tile_filenames.lake_tile_edge_file.replace(".nc", ".shp"))
        logger.info("")

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

    def start(self):
        """
        Main pseudoPGE method
        Start computation
        """
        logger = logging.getLogger(self.__class__.__name__)

        try:

            logger.info("***************************************************")
            logger.info("***** Read input data and prepare output data *****")
            logger.info("***************************************************")

            # 1 - Read input data
            logger.info("> 1 - Init and format input objects...")
            self._read_input_data()
            logger.info("")

            # 2 - Read lake db
            logger.info("> 2 - Init and read lake DB object...")
            self._read_lake_db()
            logger.info("")

            # 3 - Prepare output data
            logger.info("> 3 - Prepare output objects...")
            self._prepare_output_data()
            logger.info("")

            if self.obj_lake_db.flag_ocean:

                logger.warning("This is an OCEAN tile => generate an empty product")
                logger.info("")

            else:

                logger.info("****************************")
                logger.info("***** Run SAS LakeTile *****")
                logger.info("****************************")

                # 4 - Initialization
                logger.info("> 4 - Initialization of SASLakeTile class")
                my_lake_tile = sas_lake_tile.SASLakeTile(self.obj_pixc, self.obj_pixcvec, self.obj_lake_db, self.obj_lake)
                logger.info(self.timer.info(0))
                logger.info("")

                # 5 - Run pre-processing
                logger.info("> 5 - Run SASpre-processing")
                my_lake_tile.run_preprocessing()
                logger.info(self.timer.info(0))
                logger.info("")

                # 6 - Run processing
                logger.info("> 6 - Run SASprocessing")
                my_lake_tile.run_processing()
                logger.info(self.timer.info(0))
                logger.info("")

                # 7 - Run post-processing
                logger.info("> 7 - Run SASpost-processing")
                my_lake_tile.run_postprocessing()
                logger.info(self.timer.info(0))
                logger.info("")

            logger.info("*****************************")
            logger.info("***** Write output data *****")
            logger.info("*****************************")

            # 8 - Write output data
            logger.info("> 8 - Write output data")
            self._write_output_data()

        except service_error.SwotError:
            raise

        except Exception:
            message = "Fatal error catch in PGE LakeTile"
            logger.error(message, exc_info=True)
            raise

    def stop(self):
        """
        pseudoPGE method stop
        Close all services
        """
        logger = logging.getLogger(self.__class__.__name__)

        logger.info("")
        logger.info("******************************")
        logger.info("***** Close all services *****")
        logger.info("******************************")
        logger.info("")

        # 1 - Close lake database
        logger.info("> 1 - Closing lake database...")
        self.obj_lake_db.close_db()

        logger.info("")
        logger.info(self.timer.stop())
        logger.sigmsg("====================================")
        logger.sigmsg("===== LakeTileProcessing = END =====")
        logger.sigmsg("====================================")

        # 2 - Stop logger
        instance_logger = service_logger.get_instance()
        if instance_logger is not None:
            instance_logger.flush_log()
            instance_logger.close()

        # 3 - Clean configuration file
        service_config_file.clear_config()

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

if __name__ == '__main__':

    # 0 - Parse inline parameters
    PARSER = argparse.ArgumentParser(description="Compute SWOT LakeTile product\
            from one tile of PIXC product and its associated PIXCVecRiver product. ")
    PARSER.add_argument("command_file", help="command file (*.cfg)")
    ARGS = PARSER.parse_args()

    PGE = None

    try:
        # 1 - Instantiate PGE
        PGE = PGELakeTile(ARGS.command_file)

        # 2 - Start PGE LakeTile
        PGE.start()

    finally:
        if PGE is not None:
            # 3 - Stop PGE LakeTile
            PGE.stop()


#
#    --- Sparkplub B - Discovery application ---
#
#   The application will display information about all spB entities
#
import os
import time
import json
import paho.mqtt.client as mqtt
from mqtt_spb_wrapper import *

# APPLICATION default configuration parameters -----------------------------------------------
_DEBUG = False   # Enable debug messages

# Sparkplug B parameters
_config_spb_group_name = os.environ.get("SPB_GROUP", "*")

# MQTT Configuration
_config_mqtt_host = os.environ.get("MQTT_HOST", "localhost")
_config_mqtt_port = int(os.environ.get("MQTT_PORT", 1883))
_config_mqtt_user = os.environ.get("MQTT_USER", "")
_config_mqtt_pass = os.environ.get("MQTT_PASS", "")

# Global Variables -------------------------------------------------------
spb_groups = {}    # Variable to store the discord groups and entities

topic = MqttSpbTopic()  # spB Topic parser

def on_connect(client, userdata, flags, rc):
    """
        MQTT Callback function for connect events
    """
    if rc == 0:
        if _config_spb_group_name == "*":
            topic = "spBv1.0/#"
        else:
            topic = "spBv1.0/%s/#" % _config_spb_group_name

        # Subscribe to the specific topic
        client.subscribe(topic)
        print("MQTT Client connected and subscribed to topic: " + topic)

    else:
        print("MQTT Client failed to connect with result code " + str(rc))

def on_message(client, userdata, msg):
    """
        MQTT Callback function for received messages events
    """

    global topic, spb_groups

    # print(msg.topic)

    # Parse the topic
    topic.parse_topic(msg.topic)

    group_name = topic.group_name
    eon_name = topic.eon_name
    eon_device_name = topic.eon_device_name
    entity_name = topic.entity_name

    # Check if the group is present in the ones detected
    if group_name not in spb_groups.keys():
        spb_groups[group_name] = {}

    # Check if the eon is present in the group
    if eon_name not in spb_groups[group_name].keys():
        spb_groups[group_name][eon_name] = {}

    # Check if the entity is present in the group and eon
    if entity_name not in spb_groups[group_name][eon_name].keys():
        entity = MqttSpbEntity( spb_group_name= group_name,
                                spb_eon_name= eon_name,
                                spb_eon_device_name=eon_device_name,
                                debug_info=_DEBUG)
        spb_groups[group_name][eon_name][entity_name] = entity     # Save the reference
    else:
        entity = spb_groups[group_name][eon_name][entity_name]  # Get the reference

    # BIRTH,  parse the data into the entity object, for later display
    if "BIRTH" in topic.message_type:

        # Parse payload
        entity.deserialize_payload_birth(msg.payload)

print("--- Sparkplug B entity discovery application ---")

# Set up the MQTT client connection that will listen to all Sparkplug B messages
_mqtt_client = mqtt.Client()
if _config_mqtt_user != "":
    _mqtt_client.username_pw_set(_config_mqtt_user, _config_mqtt_pass)
_mqtt_client.on_connect = on_connect
_mqtt_client.on_message = on_message
_mqtt_client.connect(_config_mqtt_host, _config_mqtt_port)  # Connect to MQTT
print("Connecting to MQTT broker server - %s:%d" % (_config_mqtt_host, _config_mqtt_port))

# Start the mqtt client thread
_mqtt_client.loop_start()

# Wait some time to receive the persisted messages in the broker.
time.sleep(4)

# Stop the mqtt client
_mqtt_client.loop_stop()

# Let's show the result
PIPE = "│  "
ELBOW = "└──"
TEE = "├──"
SPACE = "   "

out_dict = {}   # output data

for group_name in spb_groups.keys():

    print("\nspBv1.0 / %s" % group_name)

    if group_name not in out_dict.keys():    # Create if it doesn't exist
        out_dict[group_name] = {}

    for eon_name in spb_groups[group_name].keys():

        # Check if last item
        L1_last = (eon_name == list(spb_groups[group_name].keys())[-1])
        if L1_last:
            L1 = ELBOW
        else:
            L1 = TEE

        # ├── EON
        print("  %s %s" % (L1, eon_name))

        # Create if it doesn't exist
        if eon_name not in out_dict[group_name].keys():
            out_dict[group_name][eon_name] = []

        for entity_name in spb_groups[group_name][eon_name].keys():

            # ├── EON
            # │    ├── ENTITY
            L2_last = (entity_name == list(spb_groups[group_name][eon_name].keys())[-1])
            if L1_last:
                L1 = SPACE
            else:
                L1 = PIPE

            if L2_last:  # Check if last item
                L2 = ELBOW
            else:
                L2 = TEE

            entity = spb_groups[group_name][eon_name][entity_name]  # Get entity object

            print("  %s %s %s" % (L1, L2, entity_name))

            # Add the entity information
            out_dict[group_name][eon_name].append(entity.get_dictionary())

            if L2_last:
                L2 = SPACE
            else:
                L2 = PIPE

            MAX_STR_LEN = 40    # Display only a maximum of characters

            # Print entity Attributes, commands, data
            print("  %s %s %s ATTR/" % (L1, L2, TEE))
            attributes = entity.attribures.get_dictionary()
            if len(attributes) != 0:
                for field in attributes:
                    _value = field['value']
                    if isinstance(_value, str):
                        if len(_value) > MAX_STR_LEN:
                            _value = _value[:MAX_STR_LEN].replace("\n", "").replace("\r", "") + " ..."
                        else:
                            _value = _value
                    else:
                        _value = str(_value)
                    print("  %s %s %s - %s : %s (%s)" % (L1, L2, PIPE, field['name'], _value, type(field['value'])))

            print("  %s %s %s CMD/" % (L1, L2, TEE))
            commands = entity.commands.get_dictionary()
            if len(commands) != 0:
                for field in commands:
                    _value = field['value']
                    if isinstance(_value, str):
                        if len(_value) > MAX_STR_LEN:
                            _value = _value[:MAX_STR_LEN].replace("\n", "").replace("\r", "") + " ..."
                        else:
                            _value = _value
                    else:
                        _value = str(_value)
                    print("  %s %s %s - %s : %s (%s)" % (L1, L2, PIPE, field['name'], _value, type(field['value'])))

            print("  %s %s %s DATA/" % (L1, L2, ELBOW))
            data = entity.data.get_dictionary()
            if len(data) != 0:
                for field in data:
                    _value = field['value']
                    if isinstance(_value, str):
                        if len(_value) > MAX_STR_LEN:
                            _value = _value[:MAX_STR_LEN].replace("\n", "").replace("\r", "") + " ..."
                        else:
                            _value = _value
                    else:
                        _value = str(_value)
                    print("  %s %s %s - %s : %s (%s)" % (L1, L2, SPACE, field['name'], _value, type(field['value'])))

# Save discovery results into json file
filename = "spb_discovery_results.json"
print("\nSaving results to file: " + filename)
with open(filename, "w") as fw:
    fw.write( json.dumps(out_dict))


__license__ = "2019, Romain Graziani"
__docformat__ = 'reStructuredText'
__author__ = 'Romain Graziani <romain.graziani@clermont.in2p3.fr>'
__date__ = '2020/10/02 10:46:12'
__adv__ = 'plots.py'

import matplotlib.pyplot as mpl
from matplotlib.gridspec import GridSpec
from scipy.stats import binned_statistic_2d
import numpy as np

def make_focal_plane():
    fig = mpl.figure(figsize=(8,7.75))
    gs = GridSpec(4, 5, figure=fig,wspace=0.1,hspace=0.1, width_ratios = [1,1,1,1,0.1], height_ratios=[1,1,1,1])
    return fig, gs

def get_ax_cbar(fig, gs):
    ax = fig.add_subplot(gs[:,-1])
    return ax

def get_ax_ccd(fig, gs, ccd):
    j = ((ccd-1) % 4)
    i = 3- ((ccd-1) // 4)
    ax = fig.add_subplot(gs[i,j])
    return ax, i, j

def vminvmax_parser(data, vmin, vmax):
    """ """
    if vmin is None:
        vmin="0"
    if vmax is None:
        vmax = "100"
    if type(vmin) == str:
        vmin = np.percentile(data, float(vmin))
    if type(vmax) == str:
        vmax = np.percentile(data, float(vmax))
    return vmin, vmax

def get_threeplot_axes(fig=None, scalex=1, scaley=1, globalbottom=0, globalleft=0,
                        left= 0.05, width=0.25, bottom=0.15, heigth=0.75,
                        hspan=0.015, hxspan=0.1,
                        cwidth = 0.01):
    """ """
    if fig is None:
        fig = mpl.figure(figsize=[9,3])

    left, width = globalleft+left*scalex, width*scalex
    bottom, heigth = globalbottom+bottom*scaley, heigth*scaley
    hspan, hxspan = hspan*scalex,hxspan*scalex
    cwidth = cwidth*scalex

    axd = fig.add_axes([left+0*(width+hspan)     ,bottom,width,heigth])
    axm = fig.add_axes([left+1*(width+hspan)     ,bottom,width,heigth])
    cax = fig.add_axes([left+2*(width+hspan)     ,bottom,cwidth,heigth])
    axr = fig.add_axes([left+2*(width+hspan) +hxspan,bottom,width,heigth])
    caxr = fig.add_axes([left+3*(width+hspan)+hxspan,bottom,cwidth,heigth])
    axes = [axd,axm,axr]
    caxes = [cax, caxr]

    return fig, axes, caxes

def display_binned2d(ax, binneddata, xbins=None, ybins=None, vmin="5", vmax="95", cax=None,
                         transpose=False, cmap=None, **kwargs):
    """ """
    from ziff.plots import vminvmax_parser
    vmin_, vmax_ = vminvmax_parser(binneddata, vmin, vmax)
    if xbins is not None and ybins is not None:
        extent = (xbins[0],xbins[-1],ybins[0],ybins[-1])
    else:
        extent = None

    prop = dict(origin="lower", extent=extent)
    im = ax.imshow(binneddata.T if transpose else binneddata, cmap=cmap,
                    vmin=vmin_, vmax=vmax_, **{**prop,**kwargs})

    if cax is not None:
        ax.figure.colorbar(im, cax=cax)

    return im

def show_shapebinned(dataframe, nbins=50,
                     which="sigma", vmin="2", vmax="98", cmap='RdBu_r', normres=True,
                     tight_layout=True, rmflagout=True, cvmin=None, cvmax=None, suffix=["_stars","_model"],
                     normed_by=["ccdid", "obsjd"], ref="_model",normstat="median",
                     rvmin=-0.05, rvmax=0.05,
              ):
    """ """
    import matplotlib.pyplot as mpl
    from ziff.utils import vminvmax_parser
    from scipy.stats import binned_statistic_2d
    #
    # - Input
    if cvmax is None:
        cvmax = vmax
    if cvmin is None:
        cvmin = vmin
    # - end: Input
    #

    #
    if rmflagout:
        dataframe = dataframe[~(dataframe[f"flagout{suffix[0]}"] | dataframe[f"flagout{suffix[0]}"])]

    u,v, data, model = dataframe[["u_model","v_stars",f"{which}{suffix[0]}",f"{which}{suffix[1]}"]].values.T
    residual = (data-model)/model

    if normed_by is not None:
        grouped = dataframe.groupby(normed_by)
        normalisation = grouped[f"{which}{ref}"].transform(normstat)
        data  = data/normalisation
        model = model/normalisation

    #

    fig = mpl.figure(figsize=[10,3])
    left,width = 0.05, 0.25
    hspan, hxspan = 0.01,0.08

    axd = fig.add_axes([left+0*(width+hspan)     ,0.1,width,0.8])
    axm = fig.add_axes([left+1*(width+hspan)     ,0.1,width,0.8])
    axc = fig.add_axes([left+2*(width+hspan)     ,0.1,0.01,0.8])
    axr = fig.add_axes([left+2*(width+hspan) +hxspan,0.1,width,0.8])
    axcr = fig.add_axes([left+3*(width+hspan)+hxspan,0.1,0.01,0.8])
    axes = [axd,axm,axr]

    # Data to show

    vmin_, vmax_ = vminvmax_parser(model, vmin, vmax)

    bins_u = np.linspace(np.min(u),np.max(u),nbins)
    bins_v =  np.linspace(np.min(v),np.max(v),nbins)

    data_bstat = binned_statistic_2d(u, v, data, statistic="median", bins=[bins_u,bins_v]).statistic
    model_bstat = binned_statistic_2d(u, v, model, statistic="median", bins=[bins_u,bins_v]).statistic
    res_bstat = binned_statistic_2d(u, v, residual, statistic="median", bins=[bins_u,bins_v]).statistic

    default_imshow_kwargs =  {'cmap' : cmap, 'origin':'lower',
                              'extent' : (bins_u[0],bins_u[-1],bins_v[0],bins_v[-1]),
                              "vmin":vmin_, "vmax":vmax_}

    im = axd.imshow(data_bstat.T,  **default_imshow_kwargs)
    im = axm.imshow(model_bstat.T, **default_imshow_kwargs)
    fig.colorbar(im,cax=axc)
    im = axr.imshow(res_bstat.T, **{**default_imshow_kwargs,**{"vmin":rvmin, "vmax":rvmax}})
    fig.colorbar(im,cax=axcr)

    # Main axes
    [ax_.tick_params(labelsize="small", labelcolor="0.5") for ax_ in [axm, axd, axr]]
    # Color bars
    [ax_.tick_params(labelsize="medium", labelcolor="k") for ax_ in [axc, axcr]]
    axm.set_yticks([])
    axr.set_yticks([])
#    [[ax_.set_xticks([]),ax_.set_yticks([])] for ax_ in axes]

#
#   Plotting
#
def show_psfresults(psf, nstars=9, index=None, savefile=None, **kwargs):
    """
    if index given, nstars is ignored.
    """
    all_indexes = np.arange(len(psf.stars))
    if index is not None:
        nstars = len(index)
    elif nstars is not None:
        index = np.random.choice(all_indexes, nstars, replace=False)
    else:
        raise ValueError("you must provide easer nstars or index ; both are None")

    print(index)

    fig = mpl.figure(figsize=[9,3*nstars])
    for i, index_ in enumerate(index):
        _ = show_single_psfstar(psf, index_, fig=fig,
                            globalbottom=i/nstars, scaley=1/nstars, **kwargs)
        if i ==0:
            fig, axes, cax = _

    axes[0].set_xlabel("Data", weight="medium")
    axes[1].set_xlabel("Model", weight="medium")
    axes[2].set_xlabel("Residual", weight="medium")

    if savefile:
        fig.savefig(savefile)

    return fig

def show_single_psfstar(psf, index, fig=None,
                        scalex=1, scaley=1, globalbottom=0, globalleft=0,
                       vmin="1", vmax="99",cvmin="1", cvmax="99",
                        show_text=True, **kwargs):
    """ """
    fig, axes, caxes= get_threeplot_axes(fig=fig, scalex=scalex, scaley=scaley,
                                             globalbottom=globalbottom, globalleft=globalleft)
    [axd,axm,axr] = axes
    [cax, caxr] = caxes

    # Star and Model
    star_plotted  = psf.stars[index]
    model_plotted = psf.drawStar(star_plotted)

    data = star_plotted.image.array
    model = model_plotted.image.array
    residual = data-model

    vmin_, vmax_ = vminvmax_parser(data, vmin, vmax)
    cvmin_, cvmax_ = vminvmax_parser(residual, cvmin, cvmax)

    prop = dict(origin="lower", cmap="cividis", vmin=vmin_, vmax=vmax_)

    # Data
    scd = axd.imshow(data, **{**prop,**kwargs})
    fig.colorbar(scd, cax=cax)
    # Model
    scm = axm.imshow(model, **prop)
    fig.colorbar(scm, cax=cax)
    # Dif
    scr = axr.imshow(residual, **{**prop,**{"vmin":cvmin_, "vmax":cvmax_}, **kwargs})
    fig.colorbar(scr, cax=caxr)

    [[ax_.set_xticks([]),ax_.set_yticks([])] for ax_ in axes]
    [ax_.tick_params(labelsize="small") for ax_ in caxes]
    # - Text
    if show_text:
        axd.text(-0.02, 0.5, f"index: {index} (x={star_plotted.image.center.x}, y={star_plotted.image.center.y})",
                 transform=axd.transAxes, rotation=90, va="center", ha="right", fontsize="x-small", color="0.3")

    return fig, axes, caxes

# End of plots.py ========================================================


import logging
import re
import time
import urllib.parse
import uuid
from datetime import datetime

from odoo import _, api, fields, models, tools
from odoo.exceptions import AccessError
from odoo.tools import email_split

_logger = logging.getLogger(__name__)

EVENT_OPEN_DELTA = 10  # seconds
EVENT_CLICK_DELTA = 5  # seconds

class MailTrackingEmail(models.Model):
    _name = "mail.tracking.email"
    _order = "time desc"
    _rec_name = "display_name"
    _description = "MailTracking email"

    # This table is going to grow fast and to infinite, so we index:
    # - name: Search in tree view
    # - time: default order fields
    # - recipient_address: Used for email_store calculation (non-store)
    # - state: Search and group_by in tree view
    name = fields.Char(string="Subject", readonly=True, index=True)
    display_name = fields.Char(
        string="Display name",
        readonly=True,
        store=True,
        compute="_compute_tracking_display_name",
    )
    timestamp = fields.Float(
        string="UTC timestamp", readonly=True, digits="MailTracking Timestamp"
    )
    time = fields.Datetime(string="Time", readonly=True, index=True)
    date = fields.Date(
        string="Date", readonly=True, compute="_compute_date", store=True
    )
    mail_message_id = fields.Many2one(
        string="Message", comodel_name="mail.message", readonly=True, index=True
    )
    message_id = fields.Char(compute="_compute_message_id")
    mail_id = fields.Many2one(string="Email", comodel_name="mail.mail", readonly=True)
    partner_id = fields.Many2one(
        string="Partner", comodel_name="res.partner", readonly=True
    )
    recipient = fields.Char(string="Recipient email", readonly=True)
    recipient_address = fields.Char(
        string="Recipient email address",
        readonly=True,
        store=True,
        compute="_compute_recipient_address",
        index=True,
    )
    sender = fields.Char(string="Sender email", readonly=True)
    state = fields.Selection(
        [
            ("error", "Error"),
            ("deferred", "Deferred"),
            ("sent", "Sent"),
            ("delivered", "Delivered"),
            ("opened", "Opened"),
            ("rejected", "Rejected"),
            ("spam", "Spam"),
            ("unsub", "Unsubscribed"),
            ("bounced", "Bounced"),
            ("soft-bounced", "Soft bounced"),
        ],
        string="State",
        index=True,
        readonly=True,
        default=False,
        help=" * The 'Error' status indicates that there was an error "
        "when trying to sent the email, for example, "
        "'No valid recipient'\n"
        " * The 'Sent' status indicates that message was succesfully "
        "sent via outgoing email server (SMTP).\n"
        " * The 'Delivered' status indicates that message was "
        "succesfully delivered to recipient Mail Exchange (MX) server.\n"
        " * The 'Opened' status indicates that message was opened or "
        "clicked by recipient.\n"
        " * The 'Rejected' status indicates that recipient email "
        "address is blacklisted by outgoing email server (SMTP). "
        "It is recomended to delete this email address.\n"
        " * The 'Spam' status indicates that outgoing email "
        "server (SMTP) consider this message as spam.\n"
        " * The 'Unsubscribed' status indicates that recipient has "
        "requested to be unsubscribed from this message.\n"
        " * The 'Bounced' status indicates that message was bounced "
        "by recipient Mail Exchange (MX) server.\n"
        " * The 'Soft bounced' status indicates that message was soft "
        "bounced by recipient Mail Exchange (MX) server.\n",
    )
    error_smtp_server = fields.Char(string="Error SMTP server", readonly=True)
    error_type = fields.Char(string="Error type", readonly=True)
    error_description = fields.Char(string="Error description", readonly=True)
    bounce_type = fields.Char(string="Bounce type", readonly=True)
    bounce_description = fields.Char(string="Bounce description", readonly=True)
    tracking_event_ids = fields.One2many(
        string="Tracking events",
        comodel_name="mail.tracking.event",
        inverse_name="tracking_email_id",
        readonly=True,
    )
    # Token isn't generated here to have compatibility with older trackings.
    # New trackings have token and older not
    token = fields.Char(
        string="Security Token",
        readonly=True,
        default=lambda s: uuid.uuid4().hex,
        groups="base.group_system",
    )

    @api.depends("mail_message_id")
    def _compute_message_id(self):
        """This helper field will allow us to map the message_id from either the linked
        mail.message or a mass.mailing mail.trace.
        """
        self.message_id = False
        for tracking in self.filtered("mail_message_id"):
            tracking.message_id = tracking.mail_message_id.message_id

    @api.model_create_multi
    def create(self, vals_list):
        records = super().create(vals_list)
        failed_states = self.env["mail.message"].get_failed_states()
        records.filtered(lambda one: one.state in failed_states).mapped(
            "mail_message_id"
        ).write({"mail_tracking_needs_action": True})
        return records

    def write(self, vals):
        super().write(vals)
        state = vals.get("state")
        if state and state in self.env["mail.message"].get_failed_states():
            self.mapped("mail_message_id").write({"mail_tracking_needs_action": True})

    def _find_allowed_tracking_ids(self):
        """Filter trackings based on related records ACLs"""
        # Admins passby this filter
        if not self or self.env.user.has_group("base.group_system"):
            return self.ids
        # Override ORM to get the values directly
        self._cr.execute(
            """
            SELECT id, mail_message_id, partner_id
            FROM mail_tracking_email WHERE id IN %s
            """,
            (tuple(self.ids),),
        )
        msg_linked = self._cr.fetchall()
        if not msg_linked:
            return []
        _, msg_ids, partner_ids = zip(*msg_linked)
        # Filter messages with their ACL rules avoiding False values fetched in the set
        msg_ids = self.env["mail.message"]._search(
            [("id", "in", [x for x in msg_ids if x])]
        )
        partner_ids = self.env["res.partner"]._search(
            [("id", "in", [x for x in partner_ids if x])]
        )
        return [
            track_id
            for track_id, mail_msg_id, partner_id in msg_linked
            if (mail_msg_id in msg_ids)  # We can read the linked message
            or (
                not mail_msg_id and partner_id in partner_ids
            )  # No linked mail.message but we can read the linked partner
            or (not any({mail_msg_id, partner_id}))  # No linked record
        ]

    @api.model
    def _search(
        self,
        args,
        offset=0,
        limit=None,
        order=None,
        count=False,
        access_rights_uid=None,
    ):
        """Filter ids based on related records ACLs"""
        ids = super()._search(
            args, offset, limit, order, count=count, access_rights_uid=access_rights_uid
        )
        if not self.env.user.has_group("base.group_system") and not count:
            ids = self.browse(ids)._find_allowed_tracking_ids()
        return ids

    def check_access_rule(self, operation):
        """Rely on related messages ACLs"""
        super().check_access_rule(operation)
        allowed_ids = self._search([("id", "in", self._find_allowed_tracking_ids())])
        disallowed_ids = set(self.exists().ids).difference(set(allowed_ids))
        if not disallowed_ids:
            return
        raise AccessError(
            _(
                "The requested operation cannot be completed due to security "
                "restrictions. Please contact your system administrator.\n\n"
                "(Document type: %s, Operation: %s)"
            )
            % (self._description, operation)
            + " - ({} {}, {} {})".format(
                _("Records:"), list(disallowed_ids), _("User:"), self._uid
            )
        )

    def read(self, fields=None, load="_classic_read"):
        """Override to explicitly call check_access_rule, that is not called
        by the ORM. It instead directly fetches ir.rules and apply them.
        """
        if not self.env.user.has_group("base.group_system"):
            self.check_access_rule("read")
        return super().read(fields=fields, load=load)

    @api.model
    def email_is_bounced(self, email):
        if not email:
            return False
        res = self.sudo()._email_last_tracking_state(email)
        return res and res[0].get("state", "") in {
            "rejected",
            "error",
            "spam",
            "bounced",
        }

    @api.model
    def _email_last_tracking_state(self, email):
        return self.search_read(
            [("recipient_address", "=", email.lower())],
            ["state"],
            limit=1,
            order="time DESC",
        )

    @api.model
    def email_score_from_email(self, email):
        if not email:
            return 0.0
        data = self.sudo().read_group(
            [("recipient_address", "=", email.lower())],
            ["recipient_address", "state"],
            ["state"],
        )
        mapped_data = {state["state"]: state["state_count"] for state in data}
        return self.with_context(mt_states=mapped_data).sudo().email_score()

    @api.model
    def _email_score_weights(self):
        """Default email score weights. Ready to be inherited"""
        return {
            "error": -50.0,
            "rejected": -25.0,
            "spam": -25.0,
            "bounced": -25.0,
            "soft-bounced": -10.0,
            "unsub": -10.0,
            "delivered": 1.0,
            "opened": 5.0,
        }

    def email_score(self):
        """Default email score algorimth. Ready to be inherited
        It can receive a recordset or mapped states dictionary via context.
        Must return a value beetwen 0.0 and 100.0
        - Bad reputation: Value between 0 and 50.0
        - Unknown reputation: Value 50.0
        - Good reputation: Value between 50.0 and 100.0
        """
        weights = self._email_score_weights()
        score = 50.0
        states = self.env.context.get("mt_states", False)
        if states:
            for state in states.keys():
                score += weights.get(state, 0.0) * states[state]
        else:
            for tracking in self:
                score += weights.get(tracking.state, 0.0)
        if score > 100.0:
            score = 100.0
        elif score < 0.0:
            score = 0.0
        return score

    @api.depends("recipient")
    def _compute_recipient_address(self):
        for email in self:
            email.recipient_address = False
            recipient_email = email_split(email.recipient)
            if recipient_email:
                email.recipient_address = recipient_email[0].lower()

    @api.depends("name", "recipient")
    def _compute_tracking_display_name(self):
        for email in self:
            parts = [email.name or ""]
            if email.recipient:
                parts.append(email.recipient)
            email.display_name = " - ".join(parts)

    @api.depends("time")
    def _compute_date(self):
        for email in self:
            email.date = fields.Date.to_string(fields.Date.from_string(email.time))

    def _get_mail_tracking_img(self):
        m_config = self.env["ir.config_parameter"]
        base_url = m_config.get_param("mail_tracking.base.url") or m_config.get_param(
            "web.base.url"
        )
        if self.token:
            path_url = (
                "mail/tracking/open/%(db)s/%(tracking_email_id)s/%(token)s/"
                "blank.gif"
                % {
                    "db": self.env.cr.dbname,
                    "tracking_email_id": self.id,
                    "token": self.token,
                }
            )
        else:
            # This is here for compatibility with older records
            path_url = "mail/tracking/open/{db}/{tracking_email_id}/blank.gif".format(
                db=self.env.cr.dbname, tracking_email_id=self.id
            )
        track_url = urllib.parse.urljoin(base_url, path_url)
        return (
            '<img src="%(url)s" alt="" '
            'data-odoo-tracking-email="%(tracking_email_id)s"/>'
            % {"url": track_url, "tracking_email_id": self.id}
        )

    def _partners_email_bounced_set(self, reason, event=None):
        recipients = []
        if event and event.recipient_address:
            recipients.append(event.recipient_address)
        else:
            recipients = [x for x in self.mapped("recipient_address") if x]
        for recipient in recipients:
            self.env["res.partner"].search(
                [("email", "=ilike", recipient)]
            ).email_bounced_set(self, reason, event=event)

    def smtp_error(self, mail_server, smtp_server, exception):
        values = {"state": "error"}
        IrMailServer = self.env["ir.mail_server"]
        if (
            str(exception) == IrMailServer.NO_VALID_RECIPIENT
            and not self.recipient_address
        ):
            values.update(
                {
                    "error_type": "no_recipient",
                    "error_description": "The partner doesn't have a defined email",
                }
            )
        else:
            values.update(
                {
                    "error_smtp_server": tools.ustr(smtp_server),
                    "error_type": exception.__class__.__name__,
                    "error_description": tools.ustr(exception),
                }
            )
            self.sudo()._partners_email_bounced_set("error")
        self.sudo().write(values)

    def tracking_img_add(self, email):
        self.ensure_one()
        tracking_url = self._get_mail_tracking_img()
        if tracking_url:
            content = email.get("body", "")
            content = re.sub(
                r'<img[^>]*data-odoo-tracking-email=["\'][0-9]*["\'][^>]*>', "", content
            )
            body = tools.append_content_to_html(
                content, tracking_url, plaintext=False, container_tag="div"
            )
            email["body"] = body
        return email

    def _message_partners_check(self, message, message_id):
        if not self.mail_message_id.exists():  # pragma: no cover
            return True
        mail_message = self.mail_message_id
        partners = mail_message.notified_partner_ids | mail_message.partner_ids
        if self.partner_id and self.partner_id not in partners:
            # If mail_message haven't tracking partner, then
            # add it in order to see his tracking status in chatter
            if mail_message.subtype_id:
                mail_message.sudo().write(
                    {"notified_partner_ids": [(4, self.partner_id.id)]}
                )
            else:
                mail_message.sudo().write({"partner_ids": [(4, self.partner_id.id)]})
        return True

    def _tracking_sent_prepare(self, mail_server, smtp_server, message, message_id):
        self.ensure_one()
        ts = time.time()
        dt = datetime.utcfromtimestamp(ts)
        self._message_partners_check(message, message_id)
        self.sudo().write({"state": "sent"})
        return {
            "recipient": message["To"],
            "timestamp": "%.6f" % ts,
            "time": fields.Datetime.to_string(dt),
            "tracking_email_id": self.id,
            "event_type": "sent",
            "smtp_server": smtp_server,
        }

    def _event_prepare(self, event_type, metadata):
        self.ensure_one()
        m_event = self.env["mail.tracking.event"]
        method = getattr(m_event, "process_" + event_type, None)
        if method and callable(method):
            return method(self, metadata)
        else:  # pragma: no cover
            _logger.info("Unknown event type: %s" % event_type)
        return False

    def _concurrent_events(self, event_type, metadata):
        m_event = self.env["mail.tracking.event"]
        self.ensure_one()
        concurrent_event_ids = False
        if event_type in {"open", "click"}:
            ts = metadata.get("timestamp", time.time())
            delta = EVENT_OPEN_DELTA if event_type == "open" else EVENT_CLICK_DELTA
            domain = [
                ("timestamp", ">=", ts - delta),
                ("timestamp", "<=", ts + delta),
                ("tracking_email_id", "=", self.id),
                ("event_type", "=", event_type),
            ]
            if event_type == "click":
                domain.append(("url", "=", metadata.get("url", False)))
            concurrent_event_ids = m_event.search(domain)
        return concurrent_event_ids

    def event_create(self, event_type, metadata):
        event_ids = self.env["mail.tracking.event"]
        for tracking_email in self:
            other_ids = tracking_email._concurrent_events(event_type, metadata)
            if not other_ids:
                vals = tracking_email._event_prepare(event_type, metadata)
                if vals:
                    events = event_ids.sudo().create(vals)
                    if event_type in {"hard_bounce", "spam", "reject"}:
                        for event in events:
                            self.sudo()._partners_email_bounced_set(
                                event_type, event=event
                            )
                    event_ids += events
            else:
                _logger.debug("Concurrent event '%s' discarded", event_type)
        return event_ids

    # TODO Remove useless method
    @api.model
    def event_process(self, request, post, metadata, event_type=None):
        # Generic event process hook, inherit it and
        # - return 'OK' if processed
        # - return 'NONE' if this request is not for you
        # - return 'ERROR' if any error
        return "NONE"  # pragma: no cover


#! /usr/bin/env python

# import core.config
import config
import logging
import os
import sys

from flask import Flask, send_file, request, jsonify
from flask_sqlalchemy import SQLAlchemy
from whitenoise import WhiteNoise
from datetime import date
from dateutil.parser import parse as parse_date
from dateutil.relativedelta import *

# init flask app
app = Flask(__name__)

# Custom or Default Config
if 'APP_SETTINGS' in os.environ:
    app.config.from_object(os.environ['APP_SETTINGS'])
else:
    app.config.from_object(config.Config)

# WhiteNoise for static file delivery
noiseapp = WhiteNoise(app, root='./static/')
db = SQLAlchemy(app)

# Turn on logging
app.logger.addHandler(logging.StreamHandler(sys.stdout))
app.logger.setLevel(logging.ERROR)

# EXTRA IMPORT
# need to suppress PEP8 E402 warning here
from core.model import User

####################
# BEGINNING ROUTES #
####################

@app.route('/')
def index():
    return send_file(open('index.html'))

@app.route('/anmelden', methods=['POST'])
def register():
    # should do more validation here!
    try:
        vorname = request.form['vorname']
        nachname = request.form['nachname']
        geb = parse_date(request.form['geburtstag'])

    except Exception as e:
        raise e
        return "Unable to parse form...", 500

    # check date in range
    if geb.year not in range(1890, 2020):
        return "Unrealistisches Geburtsjahr", 400

    # look for unique username in db
    requested_usrname = vorname + nachname + unicode(geb.date())
    usr = User.query.filter_by(username=requested_usrname).first()

    if usr is None:
        # register new user
        try:
            new_user = User(vorname, nachname, geb)
            db.session.add(new_user)
            db.session.commit()
        except:
            return "Unable to save " + unicode(new_user) + " to database...", 503
        # initial score of 0
        new_user.score = 0
        return new_user.make_json_data(), 201
    else:
        # return existing user info
        return usr.make_json_data(), 200

@app.route('/savegame', methods=['POST'])
def save_game():
    """Save a game state to the database and return the user it belongs to or 404"""
    data = request.get_json(force=True)

    username = data['username']
    usr = User.query.filter_by(username=username).first_or_404()

    # do i need to call json.dumps(data) ???
    usr.savegame = data

    if 'score' in data:
        score = data['score']
        usr.update_highscore(score)

    db.session.commit()
    return usr.make_json_data(), 200

@app.route('/loadgame/<int:userid>')
def load_game(userid):
    """Tries to load a savegame from the database and returns it or 404"""
    # this should be a dict of unicode strings...
    sg = User.query.get_or_404(userid).savegame
    if sg is None:
        return "Dieser User hat noch keinen Spielstand gespeichert. ", 404
    else:
        return jsonify(sg), 200

@app.route('/bestenliste')
def bestenliste():
    """Sends Top 10 List to client"""
    best_users = User.query.add_columns(User.vorname, User.nachname, User.geb, User.highscore).order_by(User.highscore.desc()).limit(10).all()
    res = []
    # get today once
    today = date.today()

    for usr in best_users:
        # get age
        age = relativedelta(today, usr.geb).years
        age = age if age > 0 else 0
        res.append({
            'vorname': usr.vorname,
            'nachname': usr.nachname,
            'alter': age,
            'highscore': usr.highscore
        })
    return jsonify(res), 200

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

# Do this with username instead, can be computed in JS
# @app.route('/retrieve/<userid>')
# def retrieve(userid):
#     return User.get_user(userid).vorname


from django.db.backends.mysql.base import DatabaseCreation as MySQLCreation
from django.db.backends.mysql.base import DatabaseIntrospection as MySQLIntrospection
from django.db.backends.mysql.base import DatabaseOperations as MySQLOperations
from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper

try:
    import MySQLdb as Database
except ImportError as e:
    from django.core.exceptions import ImproperlyConfigured
    raise ImproperlyConfigured("Error loading MySQLdb module: %s" % e)

class DatabaseOperations(MySQLOperations):
    compiler_module = "autotest.frontend.db.backends.afe.compiler"

class DatabaseWrapper(MySQLDatabaseWrapper):

    def __init__(self, *args, **kwargs):
        self.connection = None
        super(DatabaseWrapper, self).__init__(*args, **kwargs)
        self.creation = MySQLCreation(self)
        try:
            self.ops = DatabaseOperations()
        except TypeError:
            self.ops = DatabaseOperations(connection=kwargs.get('connection'))
        self.introspection = MySQLIntrospection(self)

    def _valid_connection(self):
        if self.connection is not None:
            if self.connection.open:
                try:
                    self.connection.ping()
                    return True
                except Database.DatabaseError:
                    self.connection.close()
                    self.connection = None
        return False


from  random import *
import matplotlib.pyplot as plt
import stats
import numpy
import math
import os

def generoValoresNormales(mu,sigma):
    sum=0
    for i in range(0,12):
        r=random()
        sum=sum+r
    return(sigma*(sum-6)+mu)

def muestroEspeYVar(valores,mu,sigma):
    print("\nEl calculo de la Esperanza de la muestra es:", stats.mean(valores))
    print("Teoricamente es =", mu)
    print("El calculo de la Varianza de la muestra es =",numpy.var(valores) )
    print("Teoricamente es:",  sigma**2)
    print("El Desvio Estandar de la muestra es = ",math.sqrt(numpy.var(valores)))
    print("Teoricamente es =",sigma)

def creoHistogramas(valores,mu,sigma):
    plt.hist(valores1,edgecolor = 'black',linewidth=1,density=1, label=(f'Media 1 = {mu} , Sigma 1= {sigma}'))
    plt.xlabel("Valores")
    plt.ylim(0, 0.2)
    y=[(1/(sigma*(math.sqrt(2*math.pi))))*math.exp((-1/2)*((x-mu)/sigma)**2) for x in valores]
    plt.plot(valores,y,'.')
    plt.legend(title="Funcion densidad")
    plt.grid(True)
    plt.title("Grafica de Distribuciones Exponencial")
    plt.show()

cantNum = 1000
mu=[25,30]
sigma=[3,5]
print(math.pi)
for i in range(0,2):
    valores1 = []
    for j in range(0,cantNum):
        valores1.append(generoValoresNormales(mu[i],sigma[i]))
    print("\nMuestra numero ",i+1)
    muestroEspeYVar(valores1,mu[i],sigma[i])
    creoHistogramas(valores1,mu[i],sigma[i])


#!/usr/bin/env python3
from subprocess import CalledProcessError
from pytest import CaptureFixture, raises

from typer_scripts.core import run, RunMode

class TestRun:
    @staticmethod
    def test_run_executes_command(capfd: CaptureFixture[str]) -> None:
        run(['echo', 'value'], RunMode.DEFAULT)

        assert 'value' in capfd.readouterr().out

    @staticmethod
    def test_run_captures_output(capfd: CaptureFixture[str]) -> None:
        result = run(['echo', 'value'], RunMode.DEFAULT,
                     capture_output=True)

        assert 'value' in result.stdout.decode()

    @staticmethod
    def test_run_prints_args_with_dry_run(capfd: CaptureFixture[str]) -> None:
        run(['echo', 'value'], RunMode.DRY_RUN)

        assert "('echo', 'value')" in capfd.readouterr().out

    @staticmethod
    def test_run_returns_expected_completed_process_with_dry_run() -> None:
        result = run(['echo', 'value'], RunMode.DRY_RUN)

        assert result.args == ('echo', 'value')
        assert result.returncode == 0
        assert result.stdout == b"('echo', 'value')"

    @staticmethod
    def test_run_raises_exception_with_invalid_command(
            capfd: CaptureFixture[str],
    ) -> None:
        with raises(CalledProcessError):
            run(['ls', '--unknown-flag'], RunMode.DEFAULT)