import sys
# TODO: figure out how to do this correctly before code review
sys.path.insert(0, '../../') # to have access to NDNT

import copy # needed to handle annoying python pass by reference
import pprint
import networkx as nx
import numpy as np
from enum import Enum

# NDN imports
import NDNT.NDNT as NDN
from NDNT.modules.layers import *
from NDNT.networks import *

# enums to make things easier to remember
class Norm(Enum):
    none = 0
    unit = 1
    max = 2

class NL(Enum):
    linear = 'lin'
    relu = 'relu'
    elu = 'elu'
    square = 'square'
    softplus = 'softplus'
    tanh = 'tanh'
    sigmoid = 'sigmoid'

class LayerType(Enum):
    normal = 'normal'
    conv = 'conv'
    divnorm = 'divnorm'
    tconv = 'tconv'
    stconv = 'stconv'
    biconv = 'biconv'
    readout = 'readout'
    fixation = 'fixation'
    lag = 'lag'
    time = 'time'
    dim0 = 'dim0'
    dimSP = 'dimSP'
    channel = 'channel'
    LVlayer = 'LVlayer'

class NetworkType(Enum):
    normal = 'normal'
    add = 'add'
    mult = 'mult'
    readout = 'readout'
    scaffold = 'scaffold'

class RegVal(Enum):
    d2xt = 'd2xt',
    d2x = 'd2x',
    d2t = 'd2t',
    l1 = 'l1',
    l2 = 'l2',
    norm2 = 'norm2',
    norm = 'norm',
    pos = 'pos',
    neg = 'neg',
    orth = 'orth',
    bi_t = 'bi_t',
    glocalx = 'glocalx',
    glocalt = 'glocalt',
    localx = 'localx',
    localt = 'localt',
    trd = 'trd',
    local = 'local',
    glocal = 'glocal',
    max = 'max',
    max_filt = 'max_filt',
    max_space = 'max_space',
    center = 'center',
    edge_t = 'edge_t',
    edge_x = 'edge_x'

# TODO: make output_norm an enum
# TODO: make window an enum

# layer
# need to deepcopy the params
# to get around weird python inheritance junk
# where subclasses overwrite the superclass state
# https://stackoverflow.com/questions/15469579/when-i-instantiate-a-python-subclass-it-overwrites-base-class-attribute
# defines superset of all possible params
# converts params that are provided to the value required by the NDN
def _convert_params(internal_layer_type,
                    internal_freeze_weights=False,
                    internal_weights=None,
                    filter_dims=None,
                    filter_width=None,
                    window=None,
                    padding=None,
                    num_filters=None,
                    num_inh_percent=None,
                    bias=None,
                    norm_type=None,
                    NLtype=None,
                    initialize_center=None,
                    reg_vals=None,
                    output_norm=None,
                    pos_constraint=None,
                    temporal_tent_spacing=None,
                    num_iter=None,
                    output_config=None,
                    res_layer=None,
                    num_lags=None):
    params = {
        'internal_layer_type': internal_layer_type,
        'internal_freeze_weights': internal_freeze_weights,
        'internal_weights': internal_weights
    }

    if filter_dims is not None: params['filter_dims'] = filter_dims
    if filter_width is not None: params['filter_width'] = filter_width
    if window is not None: params['window'] = window
    if padding is not None: params['padding'] = padding
    if num_filters is not None: params['num_filters'] = num_filters
    if num_inh_percent is not None: params['num_inh_percent'] = num_inh_percent
    if bias is not None: params['bias'] = bias
    if norm_type is not None: params['norm_type'] = norm_type.value
    if NLtype is not None: params['NLtype'] = NLtype.value
    if initialize_center is not None: params['initialize_center'] = initialize_center
    if reg_vals is not None: params['reg_vals'] = reg_vals
    if output_norm is not None: params['output_norm'] = output_norm
    if pos_constraint is not None: params['pos_constraint'] = pos_constraint
    if temporal_tent_spacing is not None: params['temporal_tent_spacing'] = temporal_tent_spacing
    if num_iter is not None: params['num_iter'] = num_iter
    if output_config is not None: params['output_config'] = output_config
    if res_layer is not None: params['res_layer'] = res_layer
    if num_lags is not None: params['num_lags'] = num_lags

    return params

class Layer:
    def __init__(self,
                 num_filters:int=None,
                 num_inh_percent:float=None,
                 bias:bool=None,
                 norm_type:Norm=None,
                 NLtype:NL=None,
                 initialize_center:bool=None,
                 reg_vals:dict=None,
                 output_norm:bool=None,
                 pos_constraint:bool=None,
                 temporal_tent_spacing:int=None,
                 freeze_weights:bool=False,
                 weights=None):
        self.params = _convert_params(internal_layer_type=NDNLayer,
                                      internal_freeze_weights=freeze_weights,
                                      internal_weights=weights,
                                      num_filters=num_filters,
                                      num_inh_percent=num_inh_percent,
                                      bias=bias,
                                      norm_type=norm_type,
                                      NLtype=NLtype,
                                      initialize_center=initialize_center,
                                      reg_vals=reg_vals,
                                      output_norm=output_norm,
                                      pos_constraint=pos_constraint,
                                      temporal_tent_spacing=temporal_tent_spacing)
        self.network = None # to be able to point to the network we are a part of
        self.index = None # to be able to point to the layer we are a part of in the Network
        # TODO: be able to set this layer's weights as the
        #       weights of a previous layer that maybe we can access by name
        #       from the Model API
        # like Layer().use_weights(prev_layer.get_layer('drift'))
        # or something like this...

    def _get_weights(self):
        return self.network.model.NDN.networks[self.network.index].layers[self.index].get_weights()

    def _subunit_weights(self):
        weights = self.network.model.NDN.networks[self.network.index].layers[self.index].get_weights()
        # reshape weights by the number of filters in this layer
        return np.reshape(weights, (self.params['num_filters'], -1))

    # define property to make it easier to remember
    weights = property(_get_weights)
    subunit_weights = property(_subunit_weights)

class TemporalLayer:
    def __init__(self,
                 num_filters:int=None,
                 num_inh_percent:float=None,
                 num_lags:int=None,
                 bias:bool=None,
                 norm_type:Norm=None,
                 NLtype:NL=None,
                 initialize_center:bool=None,
                 reg_vals:dict=None,
                 output_norm:bool=None,
                 pos_constraint:bool=None,
                 temporal_tent_spacing:int=None,
                 freeze_weights:bool=False,
                 weights=None):
        self.params = _convert_params(internal_layer_type=Tlayer,
                                      internal_freeze_weights=freeze_weights,
                                      internal_weights=weights,
                                      num_filters=num_filters,
                                      num_inh_percent=num_inh_percent,
                                      num_lags=num_lags,
                                      bias=bias,
                                      norm_type=norm_type,
                                      NLtype=NLtype,
                                      initialize_center=initialize_center,
                                      reg_vals=reg_vals,
                                      output_norm=output_norm,
                                      pos_constraint=pos_constraint,
                                      temporal_tent_spacing=temporal_tent_spacing)
        self.network = None # to be able to point to the network we are a part of
        self.index = None # to be able to point to the layer we are a part of in the Network
        # TODO: be able to set this layer's weights as the
        #       weights of a previous layer that maybe we can access by name
        #       from the Model API
        # like Layer().use_weights(prev_layer.get_layer('drift'))
        # or something like this...
    def _get_weights(self):
        return self.network.model.NDN.networks[self.network.index].layers[self.index].get_weights()

    def _subunit_weights(self):
        weights = self.network.model.NDN.networks[self.network.index].layers[self.index].get_weights()
        # reshape weights by the number of filters in this layer
        return np.reshape(weights, (self.params['num_filters'], -1))

    # define property to make it easier to remember
    weights = property(_get_weights)
    subunit_weights = property(_subunit_weights)

# layer subclasses
class PassthroughLayer:
    def __init__(self,
                 num_filters=None,
                 bias=None,
                 NLtype=None,
                 freeze_weights=False):
        self.params = _convert_params(internal_layer_type = ChannelLayer,
                                      internal_freeze_weights=freeze_weights,
                                      num_filters = num_filters,
                                      bias = bias,
                                      NLtype = NLtype)

        self.network = None # to be able to point to the network we are a part of
        self.index = 0 # to be able to point to the layer we are a part of in the NDN

    def _get_weights(self):
        return self.network.model.NDN.networks[self.network.index].layers[self.index].get_weights()

    # define property to make it easier to remember
    weights = property(_get_weights)

class ConvolutionalLayer:
    def __init__(self,
                 filter_dims=None,
                 window=None,
                 padding=None,
                 num_filters=None,
                 num_inh_percent=None,
                 bias=None,
                 norm_type=None,
                 NLtype=None,
                 initialize_center=None,
                 reg_vals=None,
                 output_norm=None,
                 pos_constraint=None,
                 temporal_tent_spacing:int=None):
        self.params = _convert_params(internal_layer_type=ConvLayer,
                                      filter_dims=filter_dims,
                                      window=window,
                                      padding=padding,
                                      num_filters=num_filters,
                                      num_inh_percent=num_inh_percent,
                                      bias=bias,
                                      norm_type=norm_type,
                                      NLtype=NLtype,
                                      initialize_center=initialize_center,
                                      reg_vals=reg_vals,
                                      output_norm=output_norm,
                                      pos_constraint=pos_constraint,
                                      temporal_tent_spacing=temporal_tent_spacing)
        self.network = None # to be able to point to the network we are a part of
        self.index = None # to be able to point to the layer we are a part of in the NDN

    def _get_weights(self):
        return self.network.model.NDN.networks[self.network.index].layers[self.index].get_weights()

    # define property to make it easier to remember
    weights = property(_get_weights)

class IterativeConvolutionalLayer:
    def __init__(self,
                 filter_dims=None,
                 window=None,
                 padding='same', # default in the NDN
                 num_filters=None,
                 num_inh_percent=0.0,
                 bias=False,
                 norm_type=Norm.none,
                 NLtype=NL.linear,
                 initialize_center=False,
                 reg_vals=None,
                 output_norm=None,
                 pos_constraint=False,
                 temporal_tent_spacing:int=None,
                 num_iter=1,
                 output_config='last',
                 res_layer=True):
        self.params = _convert_params(internal_layer_type=IterLayer,
                                      filter_width=filter_dims, # Dan change filter_dims to filter_width here
                                      window=window,
                                      padding=padding,
                                      num_filters=num_filters,
                                      num_inh_percent=num_inh_percent,
                                      bias=bias,
                                      norm_type=norm_type,
                                      NLtype=NLtype,
                                      initialize_center=initialize_center,
                                      reg_vals=reg_vals,
                                      output_norm=output_norm,
                                      pos_constraint=pos_constraint,
                                      temporal_tent_spacing=temporal_tent_spacing,
                                      num_iter=num_iter,
                                      output_config=output_config,
                                      res_layer=res_layer)
        self.network = None # to be able to point to the network we are a part of
        self.index = None # to be able to point to the layer we are a part of in the NDN

    def _get_weights(self):
        return self.network.model.NDN.networks[self.network.index].layers[self.index].get_weights()

    # define property to make it easier to remember
    weights = property(_get_weights)

class TemporalConvolutionalLayer:
    def __init__(self,
                 filter_width=None,
                 num_lags=None,
                 window=None,
                 padding='spatial', # default in the NDN
                 num_filters=None,
                 num_inh_percent=0.0,
                 bias=False,
                 norm_type=Norm.none,
                 NLtype=NL.linear,
                 initialize_center=False,
                 reg_vals=None,
                 output_norm=None,
                 pos_constraint=False,
                 temporal_tent_spacing:int=None):

        self.params = _convert_params(internal_layer_type=TconvLayer,
                                      filter_width=filter_width,
                                      num_lags=num_lags,
                                      window=window,
                                      padding=padding,
                                      num_filters=num_filters,
                                      num_inh_percent=num_inh_percent,
                                      bias=bias,
                                      norm_type=norm_type,
                                      NLtype=NLtype,
                                      initialize_center=initialize_center,
                                      reg_vals=reg_vals,
                                      output_norm=output_norm,
                                      pos_constraint=pos_constraint,
                                      temporal_tent_spacing=temporal_tent_spacing)
        self.network = None # to be able to point to the network we are a part of
        self.index = None # to be able to point to the layer we are a part of in the NDN

    def _get_weights(self):
        return self.network.model.NDN.networks[self.network.index].layers[self.index].get_weights()

    # define property to make it easier to remember
    weights = property(_get_weights)

class IterativeTemporalConvolutionalLayer:
    def __init__(self,
                 num_lags=None,
                 filter_width=None,
                 window=None,
                 num_filters=None,
                 padding='spatial', # default in the NDN
                 num_inh_percent=0.0,
                 bias=False,
                 norm_type=Norm.none,
                 NLtype=NL.linear,
                 initialize_center=False,
                 reg_vals=None,
                 output_norm=None,
                 pos_constraint=False,
                 temporal_tent_spacing:int=None,
                 num_iter=1,
                 output_config='last',
                 res_layer=True,):
        self.params = _convert_params(internal_layer_type=IterTlayer,
                                      filter_width=filter_width,
                                      window=window,
                                      padding=padding,
                                      num_filters=num_filters,
                                      num_inh_percent=num_inh_percent,
                                      bias=bias,
                                      norm_type=norm_type,
                                      NLtype=NLtype,
                                      initialize_center=initialize_center,
                                      reg_vals=reg_vals,
                                      output_norm=output_norm,
                                      pos_constraint=pos_constraint,
                                      temporal_tent_spacing=temporal_tent_spacing,
                                      num_iter=num_iter,
                                      output_config=output_config,
                                      res_layer=res_layer,
                                      num_lags=num_lags)
        self.network = None # to be able to point to the network we are a part of
        self.index = None # to be able to point to the layer we are a part of in the NDN

    def _get_weights(self):
        return self.network.model.NDN.networks[self.network.index].layers[self.index].get_weights()

    # define property to make it easier to remember
    weights = property(_get_weights)

# utility "networks"
class Input: # holds the input info
    def __init__(self, covariate, input_dims):
        self.name = covariate
        self.index = -1 # index of network in the list of networks
        self.covariate = covariate
        self.input_dims = input_dims
        self.inputs = [] # this should always be empty for an Input
        self.output = None

    def to(self, network):
        # set the input_dims of the network to be the desired Input.input_dims
        network.input_covariate = self.covariate
        network.layers[0].params['input_dims'] = self.input_dims
        # if the first layer is a Tlayer, then we need to change the last dimension to be 1
        if network.layers[0].params['internal_layer_type'] is Tlayer:
            print('changing last dimension of input_dims to be 1')
            network.layers[0].params['input_dims'][-1] = 1
        self.output = network
        network.inputs.append(self)

    def __str__(self):
        return 'Input name='+self.name+', covariate='+self.covariate+', input_dims='+','.join([str(d) for d in self.input_dims])

class Output: # holds the output info
    def __init__(self, num_neurons):
        self.name = 'output'
        self.index = -1 # index of network in the list of networks
        self.num_neurons = num_neurons
        self.inputs = []
        self.output = None

    def update_num_neurons(self, num_neurons):
        # this will iterate over its inputs,
        # and update their last layer.num_filters to be num_neurons
        assert len(self.inputs) == 1, 'Output can only have one input'
        self.num_neurons = num_neurons
        self.inputs[0].layers[-1].params['num_filters'] = num_neurons

    def __str__(self):
        return 'Output name='+self.name+', num_neurons='+str(self.num_neurons)

class Concat:
    def __init__(self,
                 NLtype,
                 networks=None,
                 bias=None,
                 freeze_weights=False):
        # TODO: this is a little hacky
        num_filters = None
        if networks is not None:
            num_filters = networks[0].layers[-1].params['num_filters']

        self.name = '.'
        if networks is not None: # TODO: kind of a hack
            self.name = '.'.join(network.name for network in networks)

        self.index = -1 # index of network in the list of networks
        self.inputs = networks
        self.output = None

        # NDN params
        self.input_covariate = None # this should always be None for an Operator
        self.ffnet_type = NetworkType.normal.value # normal is used for concatenation
        self.layers = [PassthroughLayer(num_filters=num_filters, NLtype=NLtype, bias=bias, freeze_weights=freeze_weights)]

        # points its parents (the things to be summed) to this node
        if networks is not None:
            for network in networks:
                network.output = self
    def to(self, network):
        # if we are going to an output, update our num_filters to be the num_neurons
        if isinstance(network, Output):
            self.layers[-1].params['num_filters'] = network.num_neurons
        self.output = network
        network.inputs.append(self)

    def __str__(self):
        if self.inputs is not None:
            return 'Concat name='+self.name+' '+str(self.index)+', inputs='+','.join([str([inp]) for inp in self.inputs])
        else:
            return 'Concat name='+self.name+' '+str(self.index)

class Add:
    def __init__(self,
                 NLtype,
                 networks=None,
                 bias=None):
        # TODO: this is a little hacky
        num_filters = None
        if networks is not None:
            num_filters = networks[0].layers[-1].params['num_filters']
            # make sure that the networks all have the same num_filters in their output
            for network in networks:
                assert network.layers[-1].params['num_filters'] == num_filters, "input networks must all have the same num_filters"

        self.name = '+'
        if networks is not None: # TODO: kind of a hack
            self.name = '+'.join(network.name for network in networks)

        self.index = -1 # index of network in the list of networks
        self.inputs = networks
        self.output = None

        # NDN params
        self.input_covariate = None # this should always be None for an Operator
        self.ffnet_type = NetworkType.add.value
        self.layers = [Layer(num_filters=num_filters, NLtype=NLtype, bias=bias)]

        # points its parents (the things to be summed) to this node
        if networks is not None:
            for network in networks:
                network.output = self

    def to(self, network):
        # if we are going to an output, update our num_filters to be the num_neurons
        if isinstance(network, Output):
            self.layers[-1].params['num_filters'] = network.num_neurons
        self.output = network
        network.inputs.append(self)

    def __str__(self):
        if self.inputs is not None:
            return 'Add name='+self.name+' '+str(self.index)+', inputs='+','.join([str([inp]) for inp in self.inputs])
        else:
            return 'Add name='+self.name+' '+str(self.index)

class Mult:
    def __init__(self,
                 NLtype,
                 networks=None,
                 bias=None):
        num_filters = None
        # TODO: this is a little hacky
        if networks is not None:
            num_filters = networks[0].layers[-1].params['num_filters']
            # make sure that the networks all have the same num_filters in their output
            for network in networks:
                assert network.layers[-1].params['num_filters'] == num_filters, "input networks must all have the same num_filters"

        self.name = '*'
        if networks is not None: # TODO: kind of a hack
            self.name = '*'.join(network.name for network in networks)

        self.index = -1 # index of network in the list of networks
        self.inputs = networks
        self.output = None

        # NDN params
        self.input_covariate = None # this should always be None for an Operator
        self.ffnet_type = NetworkType.mult.value
        self.layers = [Layer(num_filters=num_filters, NLtype=NLtype, bias=bias)]

        # points its parents (the things to be multiplied) to this node
        if networks is not None:
            for network in networks:
                network.output = self

    def to(self, network):
        # if we are going to an output, update our num_filters to be the num_neurons
        if isinstance(network, Output):
            self.layers[-1].params['num_filters'] = network.num_neurons
        self.output = network
        network.inputs.append(self)

    def __str__(self):
        if self.inputs is not None:
            return 'Mult name='+self.name+' '+str(self.index)+', inputs='+','.join([str([inp]) for inp in self.inputs])
        else:
            return 'Mult name='+self.name+' '+str(self.index)

# network
class Network:
    def __init__(self, layers, name, network_type=NetworkType.normal):
        # internal params
        self.model = None # to be able to point to the model we are a part of
        self.name = name
        self.network_type = network_type
        self.index = -1 # index of network in the list of networks
        self.inputs = []
        self.output = None

        # NDNLayer params
        self.input_covariate = None # the covariate that goes to this network
        self.ffnet_type = network_type.value # get the value out of the network_type
        self.layers = layers
        for li, layer in enumerate(self.layers):
            layer.index = li
            layer.network = self # point the layer back to the network it is a part of

    def to(self, network):
        network.inputs.append(self)
        self.output = network
        if isinstance(network, Output):
            assert 'num_filters' not in self.layers[-1].params, 'num_filters should not be set on the last layer going to the Output'
            # TODO: maybe it is gross,
            # but update this output layer num_filters to match the num_neurons
            network.update_num_neurons(network.num_neurons)

    def __str__(self):
        return 'Network name='+self.name+' '+str(self.index)+', len(layers)='+str(len(self.layers))+', inputs='+','.join([str([inp]) for inp in self.inputs])

# model
def _Network_to_FFnetwork(network):
    # add layers to the network
    NDNLayers = []
    for li, layer in enumerate(network.layers):
        # get the layer_type
        layer_type = layer.params['internal_layer_type']

        # get params to pass
        sanitized_layer_params = {}
        for k,v in layer.params.items():
            # skip internal params
            if not 'internal' in k:
                sanitized_layer_params[k] = v

        # convert the num_inh_percent to num_inh
        if 'num_inh_percent' in sanitized_layer_params \
                and sanitized_layer_params['num_inh_percent'] is not None \
                and sanitized_layer_params['num_inh_percent'] > 0:
            num_inh = int(sanitized_layer_params['num_inh_percent'] * sanitized_layer_params['num_filters'])
            sanitized_layer_params['num_inh'] = num_inh
            del sanitized_layer_params['num_inh_percent']

        # populate the filter_dims parameter for the TemporalConvolutionalLayer
        if layer_type == TconvLayer:
            sanitized_layer_params['filter_dims'] = [None, sanitized_layer_params['filter_width'], 1, sanitized_layer_params['num_lags']]
            del sanitized_layer_params['filter_width']
            del sanitized_layer_params['num_lags']

            if li == 0:
                sanitized_layer_params['filter_dims'][0] = 1
            else:
                # set the filter_dims num_channels to the previous layer's num_filters
                num_iter = 1
                if 'num_iter' in NDNLayers[-1]:
                    num_iter = NDNLayers[-1]['num_iter']
                sanitized_layer_params['filter_dims'][0] = NDNLayers[-1]['num_filters']*num_iter
            layer_dict = layer_type.layer_dict(**sanitized_layer_params)
        elif isinstance(network, Add) or isinstance(network, Mult) or isinstance(network, Concat):
            layer_dict = layer_type.layer_dict(**sanitized_layer_params)
            layer_dict['weights_initializer'] = 'ones'
        else:
            layer_dict = layer_type.layer_dict(**sanitized_layer_params)

        # NDN has a bug where certain parameters don't get copied over from the constructor
        # we need to set them separately from the constructor
        if 'reg_vals' in sanitized_layer_params:
            layer_dict['reg_vals'] = sanitized_layer_params['reg_vals']
        if 'output_norm' in sanitized_layer_params:
            layer_dict['output_norm'] = sanitized_layer_params['output_norm']
        if 'window' in sanitized_layer_params:
            layer_dict['window'] = sanitized_layer_params['window']

        NDNLayers.append(layer_dict)

    # if the network gets input from an Input (e.g. has input_covariate)
    if network.input_covariate is not None:
        return FFnetwork.ffnet_dict(
            xstim_n=network.input_covariate,
            ffnet_n=None,
            layer_list=NDNLayers,
            ffnet_type=network.ffnet_type)
    else: # if the network gets inputs from other Networks
        return FFnetwork.ffnet_dict(
            xstim_n=None,
            ffnet_n=[inp.index for inp in network.inputs],
            layer_list=NDNLayers,
            ffnet_type=network.ffnet_type)

def _Model_to_NDN(model, verbose):
    ffnets = []
    for network in model.networks:
        ffnets.append(_Network_to_FFnetwork(network))
    if verbose:
        print("=== MODEL ===")
        for i in range(len(model.networks)):
            print('---', model.networks[i].name, '---')
            pprint.pprint(ffnets[i])
    ndn_model = NDN.NDN(ffnet_list=ffnets, loss_type='poisson')
    for ni in range(len(ndn_model.networks)):
        network = model.networks[ni]
        to_network = network.output

        for li in range(len(ndn_model.networks[ni].layers)):
            layer = model.networks[ni].layers[li]
            weights = layer.params['internal_weights']
            if weights is not None:
                ndn_model.networks[ni].layers[li].weight.data = copy.deepcopy(weights.data)

            freeze_weights = layer.params['internal_freeze_weights']
            if freeze_weights: # set the weights to be frozen on the NDN model
                # turn off the parameters on the current layer
                ndn_model.networks[ni].layers[li].set_parameters(val=False)
                # if this is not the last layer in the network,
                # turn off the weights on the next layer
                if li < len(ndn_model.networks[ni].layers)-1:
                    print('turning off weights on layer', li+1, 'of network', ni)
                    ndn_model.networks[ni].layers[li+1].set_parameters(val=False,name='weight')
                else:
                    # if this is the last layer in the network,
                    # turn off the weights on the to_network
                    if ni < len(ndn_model.networks)-1:
                        to_network_idx = to_network.index
                        print('turning off weights on network', to_network, to_network_idx)
                        ndn_model.networks[to_network_idx].layers[0].set_parameters(val=False,name='weight')
    return ndn_model

class Model:
    def __init__(self, output, name='', create_NDN=True, verbose=False):
        self.name = name
        self.NDN = None # set this Model's NDN to None to start
        self.inputs = []
        self.networks = []
        self.output = output

        # network.index --> network map
        self.netidx_to_model = {}
        # network.name --> network map
        self.networks_by_name = {}

        for network in self.traverse():
            # create groups
            if isinstance(network, Output):
                continue
            if isinstance(network, Input):
                self.inputs.append(network)
            else: # if it is a Network, Add, Mult or otherwise
                network.model = self # point the network back to the model
                self.networks.append(network)

        # b/c the NDNT requires that earlier networks have lower indices,
        # we need to reverse the network list and the associated indices
        self.networks.reverse()
        for idx, network in enumerate(self.networks):
            self.netidx_to_model[idx] = network
            assert network.name not in self.networks_by_name, "networks must have unique names"
            self.networks_by_name[network.name] = network
            network.index = idx # set the reversed depth-first index

        # create the NDN now
        if create_NDN:
            self.NDN = _Model_to_NDN(self, verbose)

    def update_NDN(self, verbose=False):
        self.NDN = _Model_to_NDN(self, verbose)

    def update_num_neurons(self, num_neurons, verbose=False):
        self.output.update_num_neurons(num_neurons)
        # update the NDN as well
        self.update_NDN()

    # prevent the weights from being updated on forward
    def freeze_weights(self, network_names=None):
        for network in self.networks:
            # skip networks we don't want
            if network_names is not None and network.name not in network_names:
                continue
            self.NDN.set_parameters(name='weight', val=False,
                                    ffnet_target=network.index)

    # initialize the weights using a new random initialization
    def reinitialize_weights(self, verbose=False):
        # call the NDN create function again to reinitialize the weights
        self.update_NDN()

    # copy the weights from the previous model into this model
    def use_weights_from(self, other_model, network_names=None):
        # validate the models have the same structure before copying
        # throw Exception if they do not have same structure
        # https://docs.python.org/3/library/exceptions.html#exception-hierarchy

        # use all the network names if none are set
        if network_names is None:
            network_names = list(self.networks_by_name.keys())

        filtered_self_networks = [net for net in self.networks if net.name in network_names]
        filtered_other_networks = [net for net in other_model.networks if net.name in network_names]
        if len(filtered_self_networks) != len(filtered_other_networks):
            raise TypeError("models must have the same number of networks")
        for ni in range(len(other_model.NDN.networks)):
            # skip networks we don't want
            if self.networks[ni].name not in network_names:
                continue
            if len(other_model.NDN.networks[ni].layers) != len(self.NDN.networks[ni].layers):
                raise TypeError("networks must have the same number of layers")
            for li in range(len(other_model.NDN.networks[ni].layers)):
                prev_weight = other_model.NDN.networks[ni].layers[li].weight
                curr_weight = self.NDN.networks[ni].layers[li].weight
                if prev_weight.shape != curr_weight.shape:
                    raise TypeError("weights must have the same shape")

        for ni in range(len(other_model.NDN.networks)):
            # skip networks we don't want
            if self.networks[ni].name not in network_names:
                continue
            for li in range(len(other_model.NDN.networks[ni].layers)):
                prev_layer = other_model.NDN.networks[ni].layers[li]
                curr_layer = self.NDN.networks[ni].layers[li]
                # have to make the tensors temporarily not require grad to do this
                with torch.no_grad():
                    curr_layer.weight = copy.deepcopy(prev_layer.weight)

    def __str__(self):
        return 'Model len(inputs)'+str(len(self.inputs))+ \
            ', len(networks)='+str(len(self.networks))+ \
            ', output='+str(self.output.num_neurons)

    # recursively traverse, depth-first, from the output to the inputs
    def _traverse(self, inp, out, networks, verbose=False):
        networks.append(inp)
        if verbose:
            if out is not None:
                print(inp.name, '-->', out.name)
            else:
                print(inp.name, '--> None')
        # base case
        if isinstance(inp, Input):
            return inp

        for prev_inp in inp.inputs:
            self._traverse(prev_inp, inp, networks, verbose)

    def traverse(self, verbose=False):
        # traverse starting from the output
        # this is the default behavior
        networks = []
        self._traverse(self.output, None, networks, verbose)
        return networks

    def draw(self, verbose=False):
        g = nx.DiGraph()

        for net in self.traverse(verbose):
            # add the node to the graph
            for inp in net.inputs:
                g.add_edge(inp.name, net.name)

        # draw graph
        nx.draw_networkx(g, with_labels=True)

    def print_params(self, key):
        for network in self.networks:
            print(network.name)
            for li, layer in enumerate(network.layers):
                if key in layer.params:
                    print(li, layer.params[key])
                else:
                    print(li, 'None')


from setuptools import setup, find_packages

VERSION = '1.0.1'
DESCRIPTION = 'GONG GONG'
LONG_DESCRIPTION = 'GONG GONG GONG GONG'

setup(
    name='py_gong_gong',
    version=VERSION,
    description=DESCRIPTION,
    url='https://github.com/Arkenbrien/py_gong_gong',
    author='Rhett Huston',
    packages=find_packages(),
    install_requires=['playsound'],
    classifiers=[
        'Development Status :: 6 - Mature'
    ],
)


from absl import app
from absl import flags
from absl import logging

import os
from os.path import join
import shutil

import cv2
import numpy as np
from tqdm import tqdm

def imread(path, mode="bgr"):
    """
    Helper function to read images
    Mode could be one of 'bgr' or 'rgb'
    """
    img = cv2.imread(path)
    if mode == "rgb":
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

    return img

class DirectoryTree:
    """
    A DirectoryTree class using composite pattern.
    A directory tree has a path for itself, a parent
    directory, and child directories. The parent and
    child directories are instances of the DirectoryTree class.
    """

    def __init__(self, path=None, parent=None, depth=0):
        self.parent = parent
        self.path = path
        self.directories = {}
        self.depth = depth
        if depth == 0:
            self.name = self.path
        else:
            self.name = self.path.split('/')[-1]

        if os.path.isfile(self.path):
            raise OSError('Please specify a directory not a file!')

        if not os.path.exists(self.path):
            os.makedirs(self.path)
        else:
            # Iterate through all directories in self.path, and add to self.directories
            for dir in os.listdir(self.path):
                if os.path.isdir(join(self.path, dir)):
                    self.add(dir)

    def add(self, *names):
        """ Add folders to current tree."""
        if not self.exists():
            raise OSError('This directory tree is no longer valid.')
        for name in names:
            if hasattr(self, name):
                raise OSError(
                    'path <%s> already exists in this file structure' % join(self.path, name))

            setattr(self, name, DirectoryTree(
                path=join(self.path, name), parent=self, depth=self.depth + 1))
            self.directories[name] = getattr(self, name)

    def print_all(self):
        """
        A debug function. Prints all directories
        in the tree in an understandable format.
        """
        if not self.exists():
            raise OSError('This directory tree is no longer valid.')
        cur_path = self.path.split('/')[-1]
        s = ''
        if self.depth != 0:
            s = '|'
            for i in range(self.depth):
                s += '___'

        logging.info("%s%s" % (s, cur_path))
        for name, d in self.directories.items():
            d.logging.info_all()

    def remove(self, force=False):
        """
        Delete a directory tree.
        If the directory is not empty
        use the force option for force
        removal.
        """
        if not self.exists():
            raise OSError('This directory tree is no longer valid.')
        if force:
            shutil.rmtree(self.path)
        else:
            os.rmdir(self.path)

        if self.parent is not None:
            delattr(self.parent, self.name)
            del self.parent.directories[self.name]

    def exists(self):
        return os.path.isdir(self.path)

    def get_file_paths(self, exts=[""]):
        """
        Find all files with given extensions.
        This is particularly useful to query
        sepcific files scattered in a directory
        tree of unknown structure.
        """
        res = []
        # Add files in current directory
        for f in os.listdir(self.path):
            file_path = os.path.join(self.path, f)
            if not os.path.isfile(file_path):
                continue
            for ext in exts:
                size_ext = len(ext)
                if size_ext > len(file_path):
                    continue
                if file_path[-size_ext:] == ext:
                    res.append(file_path)

        # Add files in sub directories
        for dt in self.directories.values():
            res.extend(dt.get_file_paths(exts=exts))
        return res

    def get_image_file_paths(self):
        """
        Helper function to all images in
        this directory tree.
        """
        exts = ['.JPG', '.jpg', '.png', '.PNG']
        return self.get_file_paths(exts=exts)

class FeatureExtractor:
    SIFT = 1
    AKAZE = 2
    ORB = 3

class FeatureMatcher:
    BRUTE_FORCE = 1
    BRUTE_FORCE_HAMMING = 2

class Encoder:
    """
    Base class to encode anchor or candidate images.
    """
    feature_extractors = {
        FeatureExtractor.SIFT: cv2.SIFT_create(),
        FeatureExtractor.AKAZE: cv2.AKAZE_create(),
        FeatureExtractor.ORB: cv2.ORB_create()
    }

    def __init__(self, category, path, feature_extractor=FeatureExtractor.ORB):
        self.category = category
        self.path = path
        self.id = hash(self.path)
        self.kp = None
        self.des = None
        self.img = None

        assert feature_extractor in [
            FeatureExtractor.SIFT, FeatureExtractor.AKAZE, FeatureExtractor.ORB], "Feature extractor should be either be sift, orb, or akaze."
        self.feature_extractor = feature_extractor

    def read(self, mode="bgr"):
        """
        Read img in self.path to self.img
        """
        if self.img is None:
            img = imread(self.path, mode=mode)
            np_img = np.array(img)
            self.img = np_img

    def get_image(self):
        """
        Returns self.img if not None
        """
        if not self.img:
            self.read()
        return self.img

    def extract_features(self):
        self.read()
        if (not self.kp) or (not self.des):
            kp, des = Encoder.feature_extractors[self.feature_extractor].detectAndCompute(
                self.img, None)
            self.kp = kp
            self.des = des

    def get_keypoints(self):
        if not self.kp:
            self.extract_features()
        return self.kp

    def get_descriptors(self):
        if not self.des:
            self.extract_features()
        return self.des

class Candidate(Encoder):
    """
    Class to encode candidate images.
    """

    def __init__(self, category, path, feature_extractor=FeatureExtractor.ORB):
        super().__init__(category, path, feature_extractor)

class Anchor(Encoder):
    """
    Class to encode anchor images.
    """

    def __init__(self, category, path, feature_extractor=FeatureExtractor.ORB):
        super().__init__(category, path, feature_extractor)

class Match:
    """
    Class that represents a match between anchors and candidates.
    """

    feature_matchers = {
        FeatureMatcher.BRUTE_FORCE: cv2.BFMatcher(),
        FeatureMatcher.BRUTE_FORCE_HAMMING: cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=False),
    }

    def __init__(self, anchor, candidate, feature_matcher=FeatureMatcher.BRUTE_FORCE_HAMMING):
        self.anchor = anchor
        self.candidate = candidate
        self.matches = None
        self.good_matches = None
        self.feature_matcher = feature_matcher

    def match_keypoints(self):
        """Match keypoints using the nearest neightbor approach."""
        self.matches = Match.feature_matchers[self.feature_matcher].knnMatch(
            self.anchor.des, self.candidate.des, k=2)

    def ratio_test(self, r=0.5):
        """
        Find good matches with a ratio test.
        If the ratio of two neighboring keypoints
        (in the feature space) is below a specified
        threshold then this is considered to be a
        good match.

        Args:
            r (float, optional): ratio between 0 and 1. Defaults to 0.5.
        """
        assert self.matches, "keypoints should be matched before doing ratio test"
        # Ratio test
        assert r > 0 and r < 1, "ratio should be between 0 and 1 but is %.2f" % r
        good = []
        for m, n in self.matches:
            if m.distance < r * n.distance:
                good.append([m])
        self.good_matches = good

    def drawMatchesKnn(self):
        """A debugging method to visualize matches.

        Returns:
            array: image with matched keypoints from reference and current
        """
        matches = self.good_matches if self.good_matches is not None else self.matches
        drawing = cv2.drawMatchesKnn(
            self.anchor.img,
            self.anchor.kp,
            self.candidate.img,
            self.candidate.kp,
            matches,
            None, flags=2)
        return drawing

def encode(
        root_path: str,
        read_images=False,
        compute_features=False,
        match_keypoints=False,
        feature_extractor=FeatureExtractor.ORB):
    """Helper function to encode images. This
    function takes the data folder structure
    into consideration. The data should be organized
    in the following manner:
        root
        ---- [category name]
        --------------- actual (holds anchor images)
        --------------- proof (holds matching images to the anchor)

    Args:
        root_path (str): root data path
        read_images (bool, optional): Defaults to False.
        compute_features (bool, optional): Defaults to False.
        match_keypoints (bool, optional): Defaults to False.
        feature_extractor (str, optional): . Defaults to "sift".

    All optional arguments support the felxibility of the processing pipeline.
    These options are used to optimize calls to the encode function.
    Ine might make several calls to encode in different stages of processing
    the images.

    Returns:
        tuple: A tuple of anchors, candidates, matches in that order.
    """
    # Select feature matcher based on extractor
    feature_matcher = FeatureMatcher.BRUTE_FORCE
    if feature_extractor in [FeatureExtractor.ORB, FeatureExtractor.AKAZE]:
        feature_matcher = FeatureMatcher.BRUTE_FORCE_HAMMING

    anchors, candidates = [], []
    root_dt = DirectoryTree(root_path)
    logging.info("Encoding anchors and candidates..")
    for dt in tqdm(root_dt.directories.values()):
        category = dt.name

        # Find & encode anchor
        tmp = dt.actual.get_image_file_paths()
        assert len(tmp) == 1, "There should be only single anchor image but found %i in %s" % (
            len(tmp), dt.actual.path)
        anchor_path = tmp[0]
        anchor = Anchor(category, anchor_path, feature_extractor)
        if read_images:
            anchor.read()
        if compute_features:
            anchor.extract_features()
        anchors.append(anchor)

        # Find & encode candidates
        tmp = dt.proof.get_image_file_paths()
        assert len(tmp) > 0, "There should more than zero candidates"
        candidate_paths = tmp
        for candidate_path in candidate_paths:
            candidate = Candidate(category, candidate_path, feature_extractor)
            if read_images:
                candidate.read()
            if compute_features:
                candidate.extract_features()
            candidates.append(candidate)

    # Create a list of possible matches
    # This is a anchor x candidate pairing operation
    # with O(anchors * candidates)
    matches = []
    for anchor in anchors:
        for candidate in candidates:
            matches.append(
                Match(anchor, candidate, feature_matcher=feature_matcher))

    logging.info("Matching anchors and candidates..")
    # Match keypoints in every match
    if match_keypoints:
        assert compute_features, "To do matching use compute_features option."
        for match in tqdm(matches):
            match.match_keypoints()

    return anchors, candidates, matches


#! /usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-only
#
# Issue 152
#
# Create a package 'a' which depends on 'p', where 'p' is provided by packages
# 'b' and 'c'. Ensure that 'a' can be installed and either 'b' or 'c' is
# installed.
#
# This ensures that opkg can select a provider when both providers are
# effectively equal. Neither is preferred, neither is held, neither matches the
# abstract package name, etc.
#
# In opkg v0.2.3 this failed: neither provider was selected and so no provider
# of 'p' was returned.
#

import os
import opk, cfg, opkgcl

opk.regress_init()

o = opk.OpkGroup()
o.add(Package="a", Depends="p")
o.add(Package="b", Provides="p")
o.add(Package="c", Provides="p")
o.write_opk()
o.write_list()

opkgcl.update()

opkgcl.install("a")
if not opkgcl.is_installed("a"):
    opk.fail("Package 'a' failed to install.")
if not (opkgcl.is_installed("b") or opkgcl.is_installed("c")):
    opk.fail("No provider of 'p' was installed.")


import sequential
import utils
import time
import threaded

def test_battery(op, size):
    print(f"*Op: {op} | Size: {size}*")

    # size of the matrices from method call
    matrix_a = utils.Matrix(size=size)
    matrix_b = utils.Matrix(size=size)

    #-- sequential
    i0 = time.time()
    eval(f"sequential.{op}(matrix_a, matrix_b)")  # op is the operation (add, sub, mult)
    i1 = time.time()

    seq = i1 - i0  # time in seconds it took to run the sequential version of the operation
    print(f"Sequential execution time: {seq} s")

    #-- threaded
    i0 = time.time()
    eval(f"threaded.{op}(matrix_a, matrix_b, threads=2)")
    i1 = time.time()

    thr = i1 - i0  # time in seconds it took to run the threaded version of the operation
    print(f"Threaded execution time: {thr} s")

    print(f"Using threads is {((seq/thr - 1) * 100):.2f}% faster.\n")

test_battery("add", 10)
test_battery("sub", 10)
test_battery("mult", 10)
test_battery("add", 100)
test_battery("sub", 100)
test_battery("mult", 100)
test_battery("add", 1000)
test_battery("sub", 1000)
test_battery("mult", 1000)

# test battery on a generic operation that takes 1 second per iteration
i0 = time.time()
sequential.generic_op(iterations=4)
i1 = time.time()
seq = i1 - i0
print(f"Sequential execution time: {seq} s\n")

# running the same 4 ops on 2 threads should cut the time in half
i0 = time.time()
threaded.generic_op(iterations=4)  # default number of threads is 2
i1 = time.time()
thr = i1 - i0
print(f"Threaded execution time: {thr} s")

print(f"Using threads is {((seq/thr - 1) * 100):.2f}% faster.\n")

# 4 iterations on 4 threads should be the same as running 1 iteration since they all run concurrently
i0 = time.time()
threaded.generic_op(iterations=4, threads=4)  # running with 4 threads this time
i1 = time.time()
thr = i1 - i0
print(f"Threaded execution time: {thr} s")

print(f"Using threads is {((seq/thr - 1) * 100):.2f}% faster.\n")


import streamlit as st
from st_functions import st_button, load_css
from PIL import Image

st.set_page_config(
    page_title="Linkquarium",
    page_icon="./images/favicon.PNG",
)

load_css()

# st.write("[![Star](https://img.shields.io/github/stars/dev-rakeshshaw/links.svg?logo=github&style=social)](https://github.com/dev-rakeshshaw/Linkquarium)")

col1, col2, col3 = st.columns(3)
col1.image(Image.open('./images/logo1.png'), use_column_width='always')
col2.image(Image.open('./images/dp.png'))

st.header('Rakesh Shaw, Open-Source Enthusiast.')
st.info('  ðŸ‘‹Upcoming DevðŸ‘‹ | Success requires persistent practice, guided by patience.')

icon_size = 20

st_button('resume','https://drive.google.com/file/d/1aBuysfNWHlq4BqacD-DVKsfDqglh6Drl/view?usp=share_link', 'View my Resume', icon_size)
st_button('github', 'https://github.com/dev-rakeshshaw', 'Follow me on Github', icon_size)
st_button('twitter', 'https://twitter.com/dev_rakeshshaw', 'Follow me on Twitter', icon_size)
st_button('telegram', 'https://t.me/dev_rakeshshaw', 'Chat with me on Telegram', icon_size)
st_button('linkedin', 'https://www.linkedin.com/in/rakesh-shaw-937514263/', 'Connect with me on LinkedIn', icon_size)


# https://leetcode.com/problems/minimum-cost-to-move-chips-to-the-same-position/

class Solution:
    def minCostToMoveChips(self, position):
        min_cost = float("inf")

        for pos in position:
            cur_cost = 0
            isOdd = pos%2
            for v in position:
                if v%2 == isOdd:
                    continue
                cur_cost += 1
            min_cost = min(min_cost, cur_cost)
        return min_cost


from torch.autograd.grad_mode import F
from torch.nn.functional import sigmoid
from torch.nn.modules.loss import CrossEntropyLoss
from torch.optim import SGD, Adam, lr_scheduler
from tqdm import tqdm
import math
from torch.cuda import amp
import torch
from utils.loss import DBLoss
import torch.nn as nn
import yaml
from basemodel import TextDetector
from utils.db_utils import SegDetectorRepresenter, QuadMetric
import numpy as np
from datetime import datetime
from torchsummary import summary
import numexpr
import os
import shutil
os.environ['NUMEXPR_MAX_THREADS'] = str(numexpr.detect_number_of_cores())

from db_dataset import create_dataloader
from utils.general import LOGGER, Loggers, CUDA, DEVICE
import time
import random

torch.random.manual_seed(0)
random.seed(0)
np.random.seed(0)

def one_cycle(y1=0.0, y2=1.0, steps=100):
    return lambda x: ((1 - math.cos(x * math.pi / steps)) / 2) * (y2 - y1) + y1

def eval_model(model: nn.Module, val_loader, post_process, metric_cls):
    # global DEVICE
    raw_metrics = []
    total_frame = 0.0
    total_time = 0.0
    model.eval()
    for i, batch in tqdm(enumerate(val_loader), total=len(val_loader), desc='test model'):
        with torch.no_grad():
            # 数据进行转换和丢到gpu
            for key, value in batch.items():
                if value is not None:
                    if isinstance(value, torch.Tensor):
                        batch[key] = value.to(DEVICE)
            start = time.time()
            with amp.autocast():
                preds = model(batch['imgs'])
            boxes, scores = post_process(batch, preds,is_output_polygon=False)
            total_frame += batch['imgs'].size()[0]
            total_time += time.time() - start
            raw_metric = metric_cls.validate_measure(batch, (boxes, scores))
            raw_metrics.append(raw_metric)
    metrics = metric_cls.gather_measure(raw_metrics)
    LOGGER.info('FPS:{}'.format(total_frame / total_time))
    return metrics['recall'].avg, metrics['precision'].avg, metrics['fmeasure'].avg

def train(hyp):
    start_epoch = 0
    hyp_train, hyp_data, hyp_model, hyp_logger, hyp_resume = hyp['train'], hyp['data'], hyp['model'], hyp['logger'], hyp['resume']
    epochs = hyp_train['epochs']
    batch_size = hyp_train['batch_size']

    scaler = amp.GradScaler(enabled=CUDA)
    criterion = DBLoss()
    use_bce = False
    if hyp_train['loss'] == 'bce':
        use_bce = True
    shrink_with_sigmoid = not use_bce

    model = TextDetector(hyp_model['weights'], map_location='cpu', act=hyp_model['act'])
    model.initialize_db(hyp_model['unet_weights'])
    model.dbnet.shrink_with_sigmoid = shrink_with_sigmoid
    model.train_db()
    model.to(DEVICE)

    if hyp_model['db_weights'] != '':
        model.dbnet.load_state_dict(torch.load(hyp_model['db_weights'])['weights'])
    if hyp_train['optimizer'] == 'adam':
        optimizer = Adam(model.dbnet.parameters(), lr=hyp_train['lr0'], betas=(0.937, 0.999), weight_decay=0.00002)  # adjust beta1 to momentum
    else:
        optimizer = SGD(model.dbnet.parameters(), lr=hyp_train['lr0'], momentum=hyp_train['momentum'], nesterov=True, weight_decay=hyp_train['weight_decay'])

    if hyp_train['linear_lr']:
        lf = lambda x: (1 - x / (epochs - 1)) * (1.0 - hyp_train['lrf']) + hyp_train['lrf']  # linear
    else:
        lf = one_cycle(1, hyp_train['lrf'], epochs)  # cosine 1->hyp['lrf']

    if hyp_train['linear_lr']:
        lf = lambda x: (1 - x / (epochs - 1)) * (1.0 - hyp_train['lrf']) + hyp_train['lrf']  # linear
    else:
        lf = one_cycle(1, hyp_train['lrf'], epochs)  # cosine 1->hyp['lrf']
    scheduler = lr_scheduler.LambdaLR(optimizer, lr_lambda=lf)  # plot_lr_scheduler(optimizer, scheduler, epochs)

    logger = None
    if hyp_resume['resume_training']:
        LOGGER.info(f'resume traning ... ')
        ckpt = torch.load(hyp_resume['ckpt'], map_location=DEVICE)
        model.dbnet.load_state_dict(ckpt['weights'])
        optimizer.load_state_dict(ckpt['optimizer'])
        scheduler.load_state_dict(ckpt['scheduler'])
        scheduler.step()
        start_epoch = ckpt['epoch'] + 1
        hyp_logger['run_id'] = ckpt['run_id']
        logger = Loggers(hyp)

    else:
        # if hyp_logger['type'] == 'wandb':
        logger = Loggers(hyp)

    train_img_dir, train_mask_dir, imgsz, augment, aug_param = hyp_data['train_img_dir'], hyp_data['train_mask_dir'], hyp_data['imgsz'], hyp_data['augment'], hyp_data['aug_param']
    val_img_dir, val_mask_dir = hyp_data['val_img_dir'], hyp_data['val_mask_dir']
    train_dataset, train_loader = create_dataloader(train_img_dir, train_mask_dir, imgsz, batch_size, augment, aug_param, shuffle=True, workers=hyp_data['num_workers'], cache=hyp_data['cache'])
    val_dataset, val_loader = create_dataloader(val_img_dir, val_mask_dir, imgsz, batch_size, augment=False, shuffle=False, workers=hyp_data['num_workers'], cache=hyp_data['cache'], with_ann=True)
    nb = len(train_loader)
    nw = max(round(3 * nb), 700)

    LOGGER.info(f'num training imgs: {len(train_dataset)}, num val imgs: {len(val_dataset)}')

    eval_interval = hyp_train['eval_interval']
    best_f1 = best_epoch = -1
    best_val_loss = np.inf

    accumulation_steps = hyp_train['accumulation_steps']
    summary(model, (3, 640, 640), device=DEVICE)
    metric_cls = QuadMetric()
    post_process = SegDetectorRepresenter(thresh=0.5)
    best_f1 = -1
    for epoch in range(start_epoch, epochs):  # epoch ------------------------------------------------------------------
        model.train_db()
        pbar = enumerate(train_loader)
        pbar = tqdm(pbar, total=nb, bar_format='{l_bar}{bar:10}{r_bar}{bar:-10b}')  # progress bar
        m_loss = 0
        m_loss_s = 0
        m_loss_t = 0
        m_loss_b = 0
        for i, batchs in pbar:
            if (i+2) % 256 == 0:
                train_dataset.initialize()
                pbar.set_description(f' training size: {train_dataset.img_size}')
            # warm up
            if hyp_train['warm_up']:
                ni = i + nb * epoch
                if ni <= nw:
                    xi = [0, nw]  # x interp
                    for j, x in enumerate(optimizer.param_groups):
                        x['lr'] = np.interp(ni, xi, [hyp_train['warmup_bias_lr'] if j == 2 else 0.0, x['initial_lr'] * lf(epoch)])
                        if 'momentum' in x:
                            x['momentum'] = np.interp(ni, xi, [hyp_train['warmup_momentum'], hyp_train['momentum']])

            with amp.autocast():
                for key in batchs.keys():
                    batchs[key] = batchs[key].cuda()
                preds = model(batchs['imgs'])
                metric = criterion(preds, batchs, use_bce)
            loss = metric['loss'] / accumulation_steps
            scaler.scale(loss).backward()
            if (i+1) % accumulation_steps == 0:
                scaler.step(optimizer)
                scaler.update()
                optimizer.zero_grad()
            m_loss = (m_loss * i + metric['loss'].detach()) / (i + 1)
            m_loss_s = (m_loss_s * i + metric['loss_shrink_maps'].detach()) / (i + 1)
            m_loss_t = (m_loss_t * i + metric['loss_threshold_maps'].detach()) / (i + 1)
            m_loss_b = (m_loss_b * i + metric['loss_binary_maps'].detach()) / (i + 1)

        if i % eval_interval == 0:
            recall, precision, fmeasure = eval_model(model,  val_loader, post_process, metric_cls)
            log_dict = {}
            log_dict['train/lr'] = optimizer.param_groups[0]['lr']
            log_dict['train/loss'] = m_loss
            log_dict['train/loss_shrink'] = m_loss_s
            log_dict['train/loss_threshold'] = m_loss_t
            log_dict['train/loss_binary_maps'] = m_loss_b
            log_dict['eval/recall'] = recall
            log_dict['eval/precision'] = precision
            log_dict['eval/f1'] = fmeasure

            save_best = best_f1 < fmeasure
            if save_best:
                best_f1 = fmeasure
            last_ckpt = {'epoch': epoch,
                        'best_f1': best_f1,
                        'weights': model.dbnet.state_dict(),
                        'best_val_loss': best_val_loss,
                        'optimizer': optimizer.state_dict(),
                        'scheduler': scheduler.state_dict(),
                        'run_id': logger.wandb.id if logger.wandb is not None else None,
                        'date': datetime.now().isoformat(),
                        'hyp': hyp}
            torch.save(last_ckpt, 'data/db_last.ckpt')
            if save_best:
                shutil.copy('data/db_last.ckpt', 'data/db_best.ckpt')
            if logger is not None:
                logger.on_train_epoch_end(epoch, log_dict)
        scheduler.step()
        pbar.close()

if __name__ == '__main__':
    hyp_p = r'data/train_db_hyp.yaml'
    with open(hyp_p, 'r', encoding='utf8') as f:
        hyp = yaml.safe_load(f.read())

    # hyp['data']['train_img_dir'] = r'../datasets/pixanimegirls/processed'
    hyp['data']['train_img_dir'] = [r'../datasets/codat_manga_v3/images/train', r'../datasets/codat_manga_v3/images/val', r'../datasets/pixanimegirls/processed']
    hyp['data']['train_mask_dir'] = r'../datasets/TextLines'
    # hyp['data']['train_img_dir'] = r'data/dataset/db_sub'
    hyp['data']['val_img_dir'] = r'data/dataset/db_sub'
    hyp['data']['cache'] = False
    # hyp['data']['aug_param']['size_range'] = [-1]

    hyp['train']['lr0'] = 0.01
    hyp['train']['lrf'] = 0.002
    hyp['train']['weight_decay'] = 0.00002
    hyp['train']['batch_size'] = 4
    hyp['train']['epochs'] = 160
    # hyp['train']['optimizer'] = 'sgd'

    hyp['train']['loss'] = 'bce'
    hyp['logger']['type'] =  'wandb'

    # hyp['resume']['resume_training'] = True
    # hyp['resume']['ckpt'] = 'data/db_last_bk.ckpt'
    # hyp['model']['db_weights'] = r'data/db_last.ckpt'
    train(hyp)


#add license

from qiime2.plugin import   (Str, Citations, Plugin, Visualization, MetadataColumn,
                            Categorical, Metadata, Set, Numeric, Choices, List, Float)
import versioneer

#import my functions
import q2_comp
from q2_comp import _alpha as alpha
from q2_comp import _beta as beta
from q2_comp import _denoise as denoise
from q2_comp import _machinelearning as machinelearning
from q2_comp import _taxonomy as taxonomy

#import types
from q2_types.feature_data import FeatureData, Taxonomy, Sequence
from q2_types.feature_table import FeatureTable, Frequency
from q2_types.distance_matrix import DistanceMatrix
from q2_types.sample_data import AlphaDiversity, SampleData
import q2_dada2
from q2_dada2 import DADA2Stats, DADA2StatsFormat, DADA2StatsDirFmt

#(add png downloadable format for all figures)

plugin = Plugin (
    name='comp',
    version='q2_comp.__version__',
    website='https://github.com/dianahaider/q2-comp',
    package='q2-comp',
    description=('This QIIME2 plugin compares two or more feature tables,'
    'alpha diversity vectors, taxonomy classification or denoising statistics'
    'obtained from different denoising methods through statistics and '
    'visualizations.'),
    short_description='Plugin to compare artifacts by denoising methods.',
)

#register the functions
#first function: pairwise comparison of either a diversity index (shannon) or feature table
#maybe combine fun1 and fun2 and add an input 'method: str = {pairwise, raincloud}'

_PLOT_OPT = {'pairwise', 'raincloud', 'violin', 'all'}

plugin.visualizers.register_function(
    function=q2_comp.alpha_frequency,
    inputs={
        'tables': List[FeatureTable[Frequency]]
    },
    parameters={
        'metadata': Metadata, #can seaborn support numerical metadata?
        'metadata_column': Str,
        'palette': Str,
        'style': Str,
        'context': Str,
#        'plot_type': Str % Choices(_PLOT_OPT),
        'labels': List[Str]
    },
    input_descriptions={
        'tables': 'List of frequency feature table containing the samples to be compared.',
    },
    parameter_descriptions={
        'metadata': 'Sample metadata containing metadata_column which will be used to map color the plot.',
        'metadata_column': 'Sample metadata column to use to map color the plot.',
        'palette': 'Palette to be chosen from seaborn color palette.',
        'style': 'Set a figure style according to personal preferences amongst: darkgrid, whitegrid, dark, white, and ticks.',
        'context': 'Set a figure context according to plot use. Contexts are: paper, notebook, talk and poster.',
        'labels': 'List of labels for each respective tables. The number of labels should be the same as the number of tables, and they should be written in the same order.',
    #    'plot_type': 'Type of plot to visualize data. If nothing is provided, all plots will be shown.'
    },
    name= 'Frequency count comparative analysis',
    description= "Visually compare the frequency tables obtained by different clustering methods with pairwise plots of samples ranked by frequency and colored by metadata." ,

)
#maybe can add parameters for labels
#second function: raincloud plot for a diversity or feature table (both boxplot and density plot)

#test function here for a_div vectors
plugin.visualizers.register_function(
    function=q2_comp.alpha_diversity,
    inputs={
        'alpha_diversity': List[SampleData[AlphaDiversity]],
    },
    parameters={
        'metadata': Metadata,
        'metadata_column': Str,
        'palette': Str,
        'style': Str,
        'context': Str,
        'labels': List[Str],
    },
    input_descriptions={
        'alpha_diversity': 'List of frequency feature table containing the samples to be compared.',
    },
    parameter_descriptions={
        'metadata': 'Sample metadata',
        'metadata_column': 'Categorical metadata column to map plot to different colors.',
        'palette': 'Palette to be chosen from seaborn color palette.',
        'style': 'Set a figure style according to personal preferences amongst: darkgrid, whitegrid, dark, white, and ticks.',
        'context': 'Set a figure context according to plot use. Contexts are: paper, notebook, talk and poster.',
        'labels': 'List of labels for each respective tables. The number of labels should be the same as the number of tables, and they should be written in the same order.'
    },
    name='Alpha diversity comparative analysis',
    description= "Visually compare the frequency tables obtained by different clustering methods with probablity curves and boxplots of samples ranked by frequency and colored by metadata.",
)

"""
#removed third function which was initially stats& incorporate in two above and add pipeline instead

#add pipeline function for all a_div

plugin.pipelines.register_function(
    functin=q2_comp.all_alpha_comparison,
    inputs={
        'table': List[Frequency],
        'alpha_diveristy': List[AlphaDiversity]
    },
    parameters={
        'metadata': Metadata,
        'metadata_col': Str
    },
    outputs=[
        ('pairwise', Visualization),
        ('raincloud', Visualization),
        ('violin', Visualization),
    ],
    input_descriptions={
        'table': 'List of all feature tables to be compared',
        'alpha_diversty': 'List of alpha diversity vectors to be compared'
    },
    paremeter_descriptions={
        'metadata': 'Sample metadata to use in the visualizations.',
        'metadata_col': 'Sample metadata column to use to color plots in visualization.'
    },
    output_descriptions={
        'pairwise': 'Pairwise plot.',
        'raincloud': 'Raincloud plot.',
        'violin': 'Violin plot.'
    },
    name='All alpha diversity visual comparisons',
    description='Compute multiple visualizations for the comparison of '
                'resulting diversity when using different parameters.'
)

#function4: alternative to mantel test.... or combination of 2 plots
#take as input distance matrices
plugin.visualizers.register_function(
    function=q2_comp.comp_beta,
    inputs={
        'dm1': skbio.DistanceMatrix,
        'dm2': skbio.DistanceMatrix
    },
    parameters={
        'metadata': MetadataColumn[Categorical]
    },
    outputs=[
        ('notsureyet', Visualization)
        #('merged_tables', also add csv of the merged tables)
    ],
    input_description={
        'dm1': 'Distance matrix between pairs of samples obtained from one method.',
        'dm2': 'Distance matrix between pairs of samples obtained from a different method.'
    },
    parameter_description={
        'metadata': 'Categorical metadata column to map plot to different colors.'
    },
    name={
        'Distance matrix comparison'
    },
    description={
        'Visually compare two distance matrices to identify the correlation between'
        'methods for assessment of beta diversity.'
    },
    citations=[]

)
"""
#function5: denoise bar diagram take as input stats from qiime2 can only do dada2 for now!

plugin.visualizers.register_function(
    function=q2_comp.denoise_stats,
    inputs={
        'stats': List[SampleData[DADA2Stats]],
    },
    parameters={
        'plot_type': Str % Choices (['line', 'bar']),
        'labels' : List[Str], #add default stats1, stats2, etc.
        'style' : Str,
        'context' : Str
    },
    #no metadata here! UNLESS think of complex figure able to show per md categorical column
    input_descriptions={
        'stats': 'List of denoising statistics from DADA2. All paths to file should be separated by a space only.',
    },
    parameter_descriptions={
        'plot_type': 'Type of plot visualization.',
        'labels': 'Label for stats1 in the visualization.',
        'style': 'Set a figure style according to personal preferences amongst: darkgrid, whitegrid, dark, white, and ticks.',
        'context': 'Set a figure context according to plot use. Contexts are: paper, notebook, talk and poster.'
    },
    name= 'Denoising statistics comparison',
    description= "Visually compare the denoising statistics from DADA2 when different sets of parameters to assess which denoising steps are more stringent to your dataset.",
)

"""
#function6: machine learning for prediction of sample metadata
#in qiime2 you can do -classify samples (nested cross validation)
                    # -fit supervised learning
                    # -random forest

plugin.visualizers.register_function(
    function=q2_comp.comp_ml,
    inputs={
        'model-summary1': something,
        'model-summary2': something something
    },
    #no metadata here! UNLESS think of complex figure able to show per md categorical column
    outputs=[
        ('#####', Visualization)
    ],
    input_description={
        'model-summary1': 'Table with model accuracy, and ####',
        'model-summary2': 'Table from same method but different clustering'
    },
    parameter_description={
        'metadata': 'Categorical metadata column to map plot to different colors.'
    },
    name={
        'Model accuracy comparison'
    },
    description={
        'Visually compare one machine learning model from two different'
        'clustering methods to assess which clustering model predicts the data'
        'best.'
    },
    citations=[]

)
"""
#function7: taxonomic comparison (how well does it assign taxonomy from these repseqs)
#sklearn classifier or VSEARCH or naivebayes
plugin.visualizers.register_function(
    function=q2_comp.taxo_variability,
    inputs={
        'taxonomy': List[FeatureData[Taxonomy]],
        'tables': List[FeatureTable[Frequency]]
    },
    parameters={
        'metadata' : Metadata,
        'metadata_column' : Str,
        'palette' : Str,
        'style' : Str,
        'context' : Str,
        'quantile' : Float,
        'labels' : List[Str]
    },
    input_descriptions={
        'taxonomy': 'List of taxonomic classifications from one feature table',
        'tables': 'Respective frequency tables for the classifications'
    },
    parameter_descriptions={
        'metadata': 'Categorical metadata column to map plot to different colors.',
        'metadata_column': '',
        'palette': 'Palette to be chosen from seaborn color palette.',
        'style': 'Set a figure style according to personal preferences amongst: darkgrid, whitegrid, dark, white, and ticks.',
        'context': 'Set a figure context according to plot use. Contexts are: paper, notebook, talk and poster.',
        'labels': 'List of labels for each respective tables. The number of labels should be the same as the number of tables, and they should be written in the same order.',
        'quantile': 'Quantile of most variable total data to plot.'
    },
    name=
        'Taxonomic assignment comparison',
    description=
        'Visually compare the assignment of taxonomy from the same dataset using'
        'either different clustering methods, or different reference datasets for'
        'classification.'
)


from django.urls import path
from . import views, viewsAbsences, viewsCours, viewsEtudiants, viewsGroupeEtudiants, viewsEnseignants

urlpatterns = [
    path('main/', views.main),

    path('absences/ajout', viewsAbsences.ajout),
    path('absences/affiche/', viewsAbsences.affiche),
    path('absences/modif/<int:id>/', viewsAbsences.modif),
    path('absences/supprimer/<int:id>/', viewsAbsences.supprimer),
    path('absences/updatesauvegarder/<int:id>/', viewsAbsences.updatesauvegarder),

    path('cours/ajout', viewsCours.ajout),
    path('cours/affiche/', viewsCours.affiche),
    path('cours/modif/<int:id>/', viewsCours.modif),
    path('cours/supprimer/<int:id>/', viewsCours.supprimer),
    path('cours/updatesauvegarder/<int:id>/', viewsCours.updatesauvegarder),

    path('enseignants/ajout', viewsEnseignants.ajout),
    path('enseignants/affiche/', viewsEnseignants.affiche),
    path('enseignants/modif/<int:id>/', viewsEnseignants.modif),
    path('enseignants/supprimer/<int:id>/', viewsEnseignants.supprimer),
    path('enseignants/updatesauvegarder/<int:id>/', viewsEnseignants.updatesauvegarder),

    path('etudiants/ajout', viewsEtudiants.ajout),
    path('etudiants/affiche/', viewsEtudiants.affiche),
    path('etudiants/modif/<int:id>/', viewsEtudiants.modif),
    path('etudiants/supprimer/<int:id>/', viewsEtudiants.supprimer),
    path('etudiants/updatesauvegarder/<int:id>/', viewsEtudiants.updatesauvegarder),

    path('grpetudiants/ajout', viewsGroupeEtudiants.ajout),
    path('grpetudiants/affiche/', viewsGroupeEtudiants.affiche),
    path('grpetudiants/modif/<int:id>/', viewsGroupeEtudiants.modif),
    path('grpetudiants/supprimer/<int:id>/', viewsGroupeEtudiants.supprimer),
    path('grpetudiants/updatesauvegarder/<int:id>/', viewsGroupeEtudiants.updatesauvegarder),

]


# -*- coding: utf-8 -*-
"""
Created on Fri Mar 19 12:44:17 2021

@author: Admin
"""
import torch
import torch.nn.functional as F
from torch import nn

#from ..utils import initialize_weights

class _EncoderBlock(nn.Module):
    def __init__(self, in_channels, out_channels, dropout=False):
        super(_EncoderBlock, self).__init__()
        layers = [
            nn.Conv2d(in_channels, out_channels, kernel_size=3),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(out_channels, out_channels, kernel_size=3),
            nn.BatchNorm2d(out_channels),
            nn.ReLU(inplace=True),
        ]
        if dropout:
            layers.append(nn.Dropout())
        layers.append(nn.MaxPool2d(kernel_size=2, stride=2))
        self.encode = nn.Sequential(*layers)

    def forward(self, x):
        return self.encode(x)

class _DecoderBlock(nn.Module):
    def __init__(self, in_channels, middle_channels, out_channels):
        super(_DecoderBlock, self).__init__()
        self.decode = nn.Sequential(
            nn.Conv2d(in_channels, middle_channels, kernel_size=3),
            nn.BatchNorm2d(middle_channels),
            nn.ReLU(inplace=True),
            nn.Conv2d(middle_channels, middle_channels, kernel_size=3),
            nn.BatchNorm2d(middle_channels),
            nn.ReLU(inplace=True),
            nn.ConvTranspose2d(middle_channels, out_channels, kernel_size=2, stride=2),
        )

    def forward(self, x):
        return self.decode(x)

class UNet(nn.Module):
    def __init__(self, num_classes, transform_fn=None):
        super(UNet, self).__init__()
        self.enc1 = _EncoderBlock(3, 64)
        self.enc2 = _EncoderBlock(64, 128)
        self.enc3 = _EncoderBlock(128, 256)
        self.enc4 = _EncoderBlock(256, 512, dropout=True)
        self.center = _DecoderBlock(512, 1024, 512)
        self.dec4 = _DecoderBlock(1024, 512, 256)
        self.dec3 = _DecoderBlock(512, 256, 128)
        self.dec2 = _DecoderBlock(256, 128, 64)
        self.dec1 = nn.Sequential(
            nn.Conv2d(128, 64, kernel_size=3),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.Conv2d(64, 64, kernel_size=3),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
        )
        self.b_final = nn.Conv2d(64, 32, kernel_size=1)
        self.final   = nn.Conv2d(32, num_classes, kernel_size=1)
        self.softmax = nn.Softmax(dim=1)

        self.relu1 = nn.ReLU(inplace=True)
        self.relu2 = nn.ReLU(inplace=True)

        #initialize_weights(self)
        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode="fan_out", nonlinearity="relu")
            elif isinstance(m, nn.BatchNorm2d):
                nn.init.constant_(m.weight, 1)
                nn.init.constant_(m.bias, 0)
            elif isinstance(m, nn.Linear):
                nn.init.xavier_normal_(m.weight)
                nn.init.constant_(m.bias, 0)

        self.transform_fn = transform_fn

    def forward(self, x, trans_flag=False):
        if trans_flag:
            x = self.transform_fn(x)

        enc1 = self.enc1(x)
        enc2 = self.enc2(enc1)
        enc3 = self.enc3(enc2)
        enc4 = self.enc4(enc3)

        center = self.center(enc4)

        dec4 = self.dec4(torch.cat([center, F.interpolate(enc4, center.size()[2:], mode='bilinear')], 1))
        dec3 = self.dec3(torch.cat([dec4, F.interpolate(enc3, dec4.size()[2:], mode='bilinear')], 1))
        dec2 = self.dec2(torch.cat([dec3, F.interpolate(enc2, dec3.size()[2:], mode='bilinear')], 1))
        dec1 = self.dec1(torch.cat([dec2, F.interpolate(enc1, dec2.size()[2:], mode='bilinear')], 1))

        b_final = self.relu1(self.b_final(dec1))
        final   = self.relu2(self.final(b_final))

        out = F.interpolate(final, x.size()[2:], mode='bilinear')
        #out = self.softmax(out)
        return out

    def update_batch_stats(self, flag):
        for m in self.modules():
            if isinstance(m, nn.BatchNorm2d):
                m.update_batch_stats = flag


"""
    easier to understand: always look for the pivot point
    1. if the left most number is less than the right most number, the left most is the result
    2. if not, look for the pivot point with binary search

    Time    O(logn)
    Space   O(1)
    12 ms, faster than 100.00%
"""

class Solution(object):
    def findMin(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        left = 0
        right = len(nums)-1
        while left <= right:
            # if no left most < right most, the minval is the left most number
            if nums[left] <= nums[right]:
                return nums[left]

            # binary search, always keep the mid
            # e.g.1
            # 4, 5, 1, 2, 3
            #       ^
            # 4, 5, 1     <- next iteration
            mid = (left + right)//2
            # both < and <= work fine because there are duplicate numbers
            if nums[mid] <= nums[right]:
                right = mid
            else:
                # e.g.1
                # 4, 5, 6, 7, 8, 1, 2
                #          ^
                #             8, 1, 2 <- next iteration
                # e.g.2
                # 4, 5, 6, 7, 1, 2
                #          ^
                #             1, 2 <- next iteration
                left = mid + 1

"""
    variation: find max

    Approach: same as above except return j-1
    Since maxNum is always on the left of the minNum, we just need to return the nums[(left - 1) % n]
"""

class Solution(object):
    def findMax(self, nums):
        """
        :type nums: List[int]
        :rtype: int
        """
        n = len(nums)
        left = 0
        right = n-1
        while left <= right:
            # if no left most < right most, the minval is the left most number
            if nums[left] <= nums[right]:
                return nums[(left - 1) % n]
            # binary search, always keep the mid
            mid = (left + right)//2
            if nums[mid] <= nums[right]:
                right = mid
            else:
                left = mid + 1

a = [3, 4, 5, 1, 2]
print(Solution().findMax(a))

a = [5, 6, 0, 1, 2, 3, 4]
print(Solution().findMax(a))

a = [6, 0, 1, 2, 3, 4, 5]
print(Solution().findMax(a))

a = [2, 1]
print(Solution().findMax(a))

a = [1, 2]
print(Solution().findMax(a))

a = [1, 2, 3, 4, 5, 6]
print(Solution().findMax(a))


from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from functools import reduce
from time import sleep
from statistics import mean
from send import bet_siska

def creation():
    try:
        url = "https://www.handball24.com"
        browser = webdriver.Chrome()
        browser.get(url)
        resume = input("Select matches and press enter to continue(Add to favorite) ")
        browser.implicitly_wait(1)
        matches = browser.find_elements(By.CSS_SELECTOR,"[id^='g_7']")
        checklist = list()
        for i in matches:
            link = i.get_attribute("id")
            urls = f"https://www.handball24.com/match/{link[4:]}"
            checklist.append(urls)
    finally:
        browser.quit()
    return checklist

schedule = creation()

def check_link(url):
    print(url)

    caps = DesiredCapabilities().CHROME
    caps["pageLoadStrategy"] = "eager"
    options = webdriver.ChromeOptions()
    options.add_argument('headless')
    browser = webdriver.Chrome(desired_capabilities=caps, options=options)
    browser.get(url)
    browser.implicitly_wait(1)
    team_home = browser.find_elements(By.CSS_SELECTOR, "a.participant__participantName")[0].get_attribute(
        "href") + "results/"
    team_away = browser.find_elements(By.CSS_SELECTOR, "a.participant__participantName")[1].get_attribute(
        "href") + "results/"
    title = browser.find_element(By.CSS_SELECTOR, ".tournamentHeader__country").text
    print(title)

    def separator(matches):
        match_list = list()
        for i in matches:
            line = i.text
            # print(line)
            if "(" in line or "Awrd" in line or "Abn" in line or "WO" in line:
                continue
            if len([i for i in line.split() if i.isdigit()]) < 6:
                continue
            match_list.append(line.split())
        return match_list

    def get_data(browser, link):
        browser.get(link)
        dataset = browser.find_elements(By.CSS_SELECTOR, "[id^='g_7']")
        matches = separator(dataset)
        team = browser.find_element(By.CSS_SELECTOR, "div.heading__name").get_attribute("innerHTML")
        return matches, team

    def forming(browser, link1, link2):  # NEED ADD TYPE SPORT AND FIXABLE CSS SELECTOR
        match_list_home, team1 = get_data(browser, link1)
        match_list_away, team2 = get_data(browser, link2)
        return match_list_home, match_list_away, team1, team2

    games = forming(browser, team_home, team_away)

    team1_name = games[2].split()
    team2_name = games[3].split()

    def separation_home_away(team_, all_matches):
        home_matches = list()
        away_matches = list()
        waste = ["W", "U18", "U19", "U20", "U21", "U23"]  # WASTE - U20 and another juniors and woman champs//
        for i in waste:
            if i in team_:
                team_ = [j for j in team_ if j not in waste]
        print(team_)
        for k in all_matches:
            i = [j for j in k[:len(k) - 1] if j not in waste] + k[-1:]
            x = i.index(team_[len(team_) - 1])
            if i[x + 1].isdigit():
                away_matches.append(i)
            elif "(" in i[x + 1] and i[x + 2].isdigit():
                away_matches.append(i)
            else:
                home_matches.append(i)
        return home_matches, away_matches

    team1_home, team1_away = separation_home_away(team1_name, games[0])
    team2_home, team2_away = separation_home_away(team2_name, games[1])

    results_1 = games[0]
    results_2 = games[1]

    def get_scores(results):
        scorelines = []
        for match in results:
            if len([ i for i in match if i.isdigit() ]) < 6:
                continue
            if "AET" in match or 'Pen' in match:
                scoreline = match[-9:-1]
            else:
                scoreline = match[-7:-1]
            scorelines.append(list(map(int,scoreline)))
        return scorelines

    for i in get_scores(team1_home):
        print(i)

    def half_one(scores):
        dataset1, dataset2 = [], []

        for matches in scores:
            dataset1.append(matches[2])
            dataset2.append(matches[3])

        return dataset1, dataset2, list(map(sum,zip(dataset1,dataset2)))

    def half_two(scores):
        dataset1, dataset2 = [], []

        for matches in scores:
            dataset1.append(matches[4])
            dataset2.append(matches[5])

        return dataset1, dataset2, list(map(sum,zip(dataset1,dataset2)))

    ''' Team1 at home totals '''
    team1_scored_1h_home_ind = half_one(get_scores(team1_home))[0]
    team1_conceded_1h_home_ind = half_one(get_scores(team1_home))[1]
    team1_1h_home_total = half_one(get_scores(team1_home))[2]
    team1_scored_2h_home_ind = half_two(get_scores(team1_home))[0]
    team1_conceded_2h_home_ind = half_two(get_scores(team1_home))[1]
    team1_2h_home_total = half_two(get_scores(team1_home))[2]
    team1_scored_ind_home = list(map(sum,zip(team1_scored_1h_home_ind, team1_scored_2h_home_ind)))
    team1_conceded_ind_home = list(map(sum, zip(team1_conceded_1h_home_ind, team1_conceded_2h_home_ind)))
    team1_total_home = list(map(sum, zip(team1_scored_ind_home, team1_conceded_ind_home)))
    ''' Team1  away totals '''
    team1_scored_1h_away_ind = half_one(get_scores(team1_away))[1]
    team1_conceded_1h_away_ind = half_one(get_scores(team1_away))[0]
    team1_1h_away_total = half_one(get_scores(team1_away))[2]
    team1_scored_2h_away_ind = half_two(get_scores(team1_away))[1]
    team1_conceded_2h_away_ind = half_two(get_scores(team1_away))[0]
    team1_2h_away_total = half_two(get_scores(team1_away))[2]
    team1_scored_ind_away = list(map(sum,zip(team1_scored_1h_away_ind, team1_scored_2h_away_ind)))
    team1_conceded_ind_away = list(map(sum, zip(team1_conceded_1h_away_ind, team1_conceded_2h_away_ind)))
    ''' Team2 at home totals '''
    team2_scored_1h_home_ind = half_one(get_scores(team2_home))[0]
    team2_conceded_1h_home_ind = half_one(get_scores(team2_home))[1]
    team2_1h_home_total = half_one(get_scores(team2_home))[2]
    team2_scored_2h_home_ind = half_two(get_scores(team2_home))[0]
    team2_conceded_2h_home_ind = half_two(get_scores(team2_home))[1]
    team2_2h_home_total = half_two(get_scores(team2_home))[2]
    team2_scored_ind_home = list(map(sum,zip(team2_scored_1h_home_ind, team2_scored_2h_home_ind)))
    team2_conceded_ind_home = list(map(sum, zip(team2_conceded_1h_home_ind, team2_conceded_2h_home_ind)))
    ''' Team2  away totals '''
    team2_scored_1h_away_ind = half_one(get_scores(team2_away))[1]
    team2_conceded_1h_away_ind = half_one(get_scores(team2_away))[0]
    team2_1h_away_total = half_one(get_scores(team2_away))[2]
    team2_scored_2h_away_ind = half_two(get_scores(team2_away))[1]
    team2_conceded_2h_away_ind = half_two(get_scores(team2_away))[0]
    team2_2h_away_total = half_two(get_scores(team2_away))[2]
    team2_scored_ind_away = list(map(sum,zip(team2_scored_1h_away_ind, team2_scored_2h_away_ind)))
    team2_conceded_ind_away = list(map(sum, zip(team2_conceded_1h_away_ind, team2_conceded_2h_away_ind)))
    team2_total_away = list(map(sum, zip(team2_scored_ind_away, team2_conceded_ind_away)))

    def handicap(scores):
        dataset1_total, dataset2_total = [], []
        dataset1_1half, dataset2_1half = [], []
        dataset1_2half, dataset2_2half = [], []

        for match in scores:
            dataset1_total.append(match[2]+match[4] - match[3]-match[5])
            dataset2_total.append(match[3]+match[5] - match[2]-match[4])

            dataset1_1half.append(match[2] - match[3])
            dataset2_1half.append(match[3] - match[2])

            dataset1_2half.append(match[4] - match[5])
            dataset2_2half.append(match[5] - match[4])

        return dataset1_total, dataset2_total, dataset1_1half, dataset2_1half, dataset1_2half, dataset2_2half

    ''' Team1 handicaps ...'''
    team1_handicap_total_home = handicap(get_scores(team1_home))[0]
    team1_handicap_total_away = handicap(get_scores(team1_away))[1]
    team1_handicap_1half_home = handicap(get_scores(team1_home))[2]
    team1_handicap_1half_away = handicap(get_scores(team1_away))[3]
    team1_handicap_2half_home = handicap(get_scores(team1_home))[4]
    team1_handicap_2half_away = handicap(get_scores(team1_away))[5]
    ''' Team2 handicaps ...'''
    team2_handicap_total_home = handicap(get_scores(team2_home))[0]
    team2_handicap_total_away = handicap(get_scores(team2_away))[1]
    team2_handicap_1half_home = handicap(get_scores(team2_home))[2]
    team2_handicap_1half_away = handicap(get_scores(team2_away))[3]
    team2_handicap_2half_home = handicap(get_scores(team2_home))[4]
    team2_handicap_2half_away = handicap(get_scores(team2_away))[5]

    """  Mean values ... """

    """  Team1   """
    mean_t1_ind_scored_home = round(mean(team1_scored_ind_home), 1)
    mean_t1_ind_scored1H_home = round(mean(team1_scored_1h_home_ind), 1)
    mean_t1_ind_scored2H_home = round(mean(team1_scored_2h_home_ind), 1)
    mean_t1_ind_conceded_home = round(mean(team1_conceded_ind_home), 1)
    mean_t1_ind_conceded1H_home = round(mean(team1_conceded_1h_home_ind), 1)
    mean_t1_ind_conceded2H_home = round(mean(team1_conceded_2h_home_ind), 1)

    mean_t1_ind_scored_away = round(mean(team1_scored_ind_away), 1)
    mean_t1_ind_scored1H_away = round(mean(team1_scored_1h_away_ind), 1)
    mean_t1_ind_scored2H_away = round(mean(team1_scored_2h_away_ind), 1)
    mean_t1_ind_conceded_away = round(mean(team1_conceded_ind_away), 1)
    mean_t1_ind_conceded1H_away = round(mean(team1_conceded_1h_away_ind), 1)
    mean_t1_ind_conceded2H_away = round(mean(team1_conceded_2h_away_ind), 1)

    """  Team2  """
    mean_t2_ind_scored_home = round(mean(team2_scored_ind_home), 1)
    mean_t2_ind_scored1H_home = round(mean(team2_scored_1h_home_ind), 1)
    mean_t2_ind_scored2H_home = round(mean(team2_scored_2h_home_ind), 1)
    mean_t2_ind_conceded_home = round(mean(team2_conceded_ind_home), 1)
    mean_t2_ind_conceded1H_home = round(mean(team2_conceded_1h_home_ind), 1)
    mean_t2_ind_conceded2H_home = round(mean(team2_conceded_2h_home_ind), 1)

    mean_t2_ind_scored_away = round(mean(team2_scored_ind_away), 1)
    mean_t2_ind_scored1H_away = round(mean(team2_scored_1h_away_ind), 1)
    mean_t2_ind_scored2H_away = round(mean(team2_scored_2h_away_ind), 1)
    mean_t2_ind_conceded_away = round(mean(team2_conceded_ind_away), 1)
    mean_t2_ind_conceded1H_away = round(mean(team2_conceded_1h_away_ind), 1)
    mean_t2_ind_conceded2H_away = round(mean(team2_conceded_2h_away_ind), 1)

    def per_minute(data, minute):
        ''' for half minute -> 30
            for fulltime minute -> 60 '''

        return round(data/minute, 2)

    def noramal_value(*args):
        return round(sum(args) / 2, 1)

    '''   Aprox. values for main totol coef.
            to compare with...  '''

    match_mean_total = noramal_value(mean_t1_ind_scored_home, mean_t2_ind_conceded_away, mean_t1_ind_conceded_home, mean_t2_ind_scored_away)
    match_mean_1H = noramal_value(mean_t1_ind_scored1H_home, mean_t2_ind_conceded1H_away, mean_t1_ind_conceded1H_home, mean_t2_ind_scored1H_away)
    match_mean_2H = noramal_value(mean_t1_ind_scored2H_home, mean_t2_ind_conceded2H_away, mean_t1_ind_conceded2H_home,
                                  mean_t2_ind_scored2H_away)
    match_mean_t1_H1_ind = noramal_value(mean_t1_ind_scored1H_home, mean_t2_ind_conceded1H_away)
    match_mean_t2_H1_ind = noramal_value(mean_t1_ind_conceded1H_home, mean_t2_ind_scored1H_away)
    match_mean_t1_ind = noramal_value(mean_t1_ind_scored_home, mean_t2_ind_conceded_away)
    match_mean_t2_ind = noramal_value(mean_t1_ind_conceded_home, mean_t2_ind_scored_away)

    print()
    print('CALC TOTAL:: ', match_mean_total)
    print('CALC 1HALF:: ', match_mean_1H)
    print('CALC 1HALF:: ', match_mean_2H)
    print()
    print('CALC TEAM1 IND. TOTAL::', match_mean_t1_ind,f'each: {mean_t1_ind_scored_home} {mean_t2_ind_conceded_away}')
    print('CALC TEAM2 IND. TOTAL::', match_mean_t2_ind, f'each: {mean_t1_ind_conceded_home} {mean_t2_ind_scored_away}')

    def bet_string(list):
        part1 = sorted(list)[:3]
        part2 = sorted(list)[-3:]
        return f'{part1}<{round(mean(list), 1)}>{part2}'

    print('1 HALF:: ')
    print('1T IND 1H SC:',bet_string(team1_scored_1h_home_ind))
    print('2T IND 1H CD:',bet_string(team2_conceded_1h_away_ind))
    print('1T IND 1H Ha:',bet_string(team1_handicap_1half_home))
    print('2T IND 1H Ha:',bet_string(team2_handicap_1half_away))
    print('1T TOTAL  1H:',bet_string(team1_1h_home_total))
    print('2T TOTAL  1H:',bet_string(team2_1h_away_total))
    print('FULLTIME:: ')
    print('1T IND FT SC:',bet_string(team1_scored_ind_home))
    print('2T IND FT CD:',bet_string(team2_conceded_ind_away))
    print('1T IND FT Ha:',bet_string(team1_handicap_total_home))
    print('2T IND FT Ha:',bet_string(team2_handicap_total_away))
    print('1T TOTAL FT:',bet_string(team1_total_home))
    print('2T TOTAL FT:',bet_string(team2_total_away))

    bet  = (title,' '.join(map(str,team1_name)),' '.join(map(str,team2_name)),
            "1 HALF TOTAL:: ",
            '1T IND 1H SC:' + bet_string(team1_scored_1h_home_ind),
            '2T IND 1H CD:' + bet_string(team2_conceded_1h_away_ind),
            '2T IND 1H SC:' + bet_string(team2_scored_1h_away_ind),
            '1T IND 1H CD:' + bet_string(team1_conceded_1h_home_ind),
            "HANDICAP 1HALF",
            '1T IND 1H Ha:' + bet_string(team1_handicap_1half_home),
            '2T IND 1H Ha:' + bet_string(team2_handicap_1half_away),
            '1T TOTAL  1H:' + bet_string(team1_1h_home_total),
            '2T TOTAL  1H:' + bet_string(team2_1h_away_total),
            'FULLTIME:: ',
            '1T IND FT SC:' + bet_string(team1_scored_ind_home),
            '2T IND FT CD:' + bet_string(team2_conceded_ind_away),
            '2T IND FT SC:' + bet_string(team2_scored_ind_away),
            '1T IND FT CD:' + bet_string(team1_conceded_ind_home),
            "HANDICAP FULLTIME",
            '1T IND FT Ha:' + bet_string(team1_handicap_total_home) ,
            '2T IND FT Ha:' + bet_string(team2_handicap_total_away) ,
            '1T TOTAL  FT:' + bet_string(team1_total_home) ,
            '2T TOTAL  FT:' + bet_string(team2_total_away)

                )

    bet_siska(bet)

for i in schedule:
    try:
        check_link(i)
    except:
        continue