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')


#18258
import sys
from collections import deque
input = sys.stdin.readline

n = int(input())
queue = deque()

for _ in range(n):
    order = list(input().split())
    if order[0] == "push":
        queue.append(order[1])
    elif order[0] == "pop":
        if not queue:
            print(-1)
        else:
            print(queue.popleft())
    elif order[0] == "size":
        print(len(queue))
    elif order[0] == "empty":
        if not queue:
            print(1)
        else:
            print(0)
    elif order[0] == "front":
        if queue:
            print(queue[0])
        else:
            print(-1)
    elif order[0] == "back":
        if queue:
            print(queue[-1])
        else:
            print(-1)

# python의 삼항 연산자 ??
# def asdf(a, q):
#     if a[0] == 'push':
#         q.append(a[1])
#         return None
#     elif a[0] == 'pop':
#         return q.popleft() if q else '-1'
#     elif a[0] == 'size':
#         return str(len(q))
#     elif a[0] == 'empty':
#         return '0' if q else '1'
#     elif a[0] == 'front':
#         return q[0] if q else '-1'
#     elif a[0] == 'back':
#         return q[-1] if q else '-1'


import tensorflow as tf
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score,mean_squared_error
import numpy as np
tf.set_random_seed(123)

# 1.데이터
datasets = load_iris()

x_data = datasets.data
y_data = datasets.target

from tensorflow.python.keras.utils import to_categorical
y_data = to_categorical(y_data)

x_train, x_test, y_train, y_test = train_test_split(x_data,y_data, train_size = 0.9, random_state=123, stratify = y_data)

# print(x_train.dtype,y_train.dtype)
print(x_data.shape,y_data.shape) #(150, 4) (150, 3)

print(x_train.shape ,x_test.shape)#(135, 4) (15, 4)
print(y_train.shape ,y_test.shape)#(135, 3) (15, 3)

x = tf.placeholder(tf.float32, shape=[None, x_data.shape[1]])
y = tf.placeholder(tf.float32, shape=[None, y_data.shape[1]])

w1 = tf.Variable(tf.random_normal([x_data.shape[1],10]))
b1 = tf.Variable(tf.random_normal([10]))
h1 = tf.sigmoid(tf.matmul(x, w1) + b1)

w2 = tf.Variable(tf.random_normal([10,10]))
b2 = tf.Variable(tf.random_normal([10]))
h2 = tf.nn.relu(tf.matmul(h1, w2) + b2)

w3 = tf.Variable(tf.random_normal([10,10]))
b3 = tf.Variable(tf.random_normal([10]))
h3 = tf.nn.relu(tf.matmul(h2, w3) + b3)

w4 = tf.Variable(tf.random_normal([10,10]))
b4 = tf.Variable(tf.random_normal([10]))
h4 = tf.nn.relu(tf.matmul(h3, w4) + b4)

w5 = tf.Variable(tf.random_normal([10,y_data.shape[1]]))
b5 = tf.Variable(tf.random_normal([y_data.shape[1]]))
h = tf.nn.softmax(tf.matmul(h4, w5) + b5)

# 3-1.컴파일
loss = tf.reduce_mean(-tf.reduce_sum(y * tf.log(h), axis=1))     # categorical_crossentropy
optimizer = tf.train.AdamOptimizer(learning_rate = 0.001)
train = optimizer.minimize(loss)

# 3-2.훈련
sess = tf.compat.v1.Session()
sess.run(tf.compat.v1.global_variables_initializer())

epoch = 1000
for epochs in range(epoch):
    cost_val, hy_val, _  = sess.run([loss, h, train], feed_dict = {x:x_train, y:y_train})
    if epochs %20 == 0:

        print(epochs, 'loss : ', cost_val, 'hy_val : ', hy_val)

# #4.평가, 예측
y_predict = sess.run(h,feed_dict={x:x_test})
y_predict = sess.run(tf.argmax(y_predict,axis=1))           #텐서를 새로운 형태로 캐스팅하는데 사용한다.부동소수점형에서 정수형으로 바꾼 경우 소수점 버림.
y_test = sess.run(tf.argmax(y_test,axis=1))             #텐서를 새로운 형태로 캐스팅하는데 사용한다.부동소수점형에서 정수형으로 바꾼 경우 소수점 버림.
                                                                       #Boolean형태인 경우 True이면 1, False이면 0을 출력한다.

from sklearn.metrics import r2_score,mean_absolute_error,accuracy_score

acc = accuracy_score(y_test,y_predict)
print('acc : ', acc)

sess.close()

# acc :  0.9333333333333333


from gensim.models import word2vec
import matplotlib.pyplot as plt

plt.figure(figsize=(30,18))
plt.tick_params(axis='both',which='both',labelsize=40)

filex = open('stantard.txt','r',encoding='utf-8')
lines = filex.read().split('\n')
standard = []
for line in lines:
    words = line.split(' ')
    standard.append(float(words[1]))

for i in range(3,128):
    model = word2vec.Word2Vec.load(".\\modelxiao\\size%d.model"%i)
    file = open("Syn.txt",'r',encoding='utf-8')

    lines = file.read().split('\n')

    sum = 0.0
    count=0

    for line in lines:
        words = line.split(' ')
        if words[0] in model.wv.vocab and words[1] in model.wv.vocab:
            sum+=model.similarity(words[0],words[1])
            count+=1
    plt.scatter(i,sum/count-standard[i])
    print(i,'完成！')

plt.savefig("similarMistandard.png")

plt.show()


# -*- coding: utf-8 -*-
import matplotlib.pyplot as plt
import numpy as np

import geatpy as ea

class MyProblem(ea.Problem):  # 继承Problem父类

    def __init__(self):
        # 目标函数计算中用到的一些数据
        self.datas = np.loadtxt('data.csv', delimiter=',')  # 读取数据
        self.k = 4  # 分类数目
        # 问题类设置
        name = 'MyProblem'  # 初始化name（函数名称，可以随意设置）
        M = 1  # 初始化M（目标维数）
        maxormins = [1]  # 初始化maxormins（目标最小最大化标记列表，1：最小化该目标；-1：最大化该目标）
        Dim = self.datas.shape[1] * self.k  # 初始化Dim
        varTypes = [0] * Dim  # 初始化varTypes（决策变量的类型，元素为0表示对应的变量是连续的；1表示是离散的）
        lb = list(np.min(self.datas, 0)) * self.k  # 决策变量下界
        ub = list(np.max(self.datas, 0)) * self.k  # 决策变量上界
        lbin = [1] * Dim  # 决策变量下边界（0表示不包含该变量的下边界，1表示包含）
        ubin = [1] * Dim  # 决策变量上边界（0表示不包含该变量的上边界，1表示包含）
        # 调用父类构造方法完成实例化
        ea.Problem.__init__(self,
                            name,
                            M,
                            maxormins,
                            Dim,
                            varTypes,
                            lb,
                            ub,
                            lbin,
                            ubin)

    def aimFunc(self, pop):  # 目标函数
        centers = pop.Phen.reshape(int(pop.sizes * self.k),
                                   int(pop.Phen.shape[1] / self.k))  # 得到聚类中心
        dis = ea.cdist(centers, self.datas, 'euclidean')  # 计算距离
        dis_split = dis.reshape(
            pop.sizes, self.k,
            self.datas.shape[0])  # 分割距离矩阵，把各个聚类中心到各个点之间的距离的数据分开
        labels = np.argmin(dis_split, 1)[0]  # 得到聚类标签值
        uni_labels = np.unique(labels)
        for i in range(len(uni_labels)):
            centers[uni_labels[i], :] = np.mean(
                self.datas[np.where(labels == uni_labels[i])[0], :], 0)
        # 直接修改染色体为已知的更优值，加快收敛
        pop.Chrom = centers.reshape(pop.sizes, self.k * centers.shape[1])
        pop.Phen = pop.decoding()  # 染色体解码（要同步修改Phen，否则后面会导致数据不一致）
        dis = ea.cdist(centers, self.datas, 'euclidean')
        dis_split = dis.reshape(pop.sizes, self.k, self.datas.shape[0])
        pop.ObjV = np.sum(np.min(dis_split, 1), 1, keepdims=True)  # 计算个体的目标函数值

    def draw(self, centers):  # 绘制聚类效果图
        dis = ea.cdist(centers, self.datas, 'euclidean')
        dis_split = dis.reshape(1, self.k, self.datas.shape[0])
        labels = np.argmin(dis_split, 1)[0]
        colors = ['r', 'g', 'b', 'y']
        fig = plt.figure()
        ax = fig.add_subplot(111, projection='3d')
        for i in range(self.k):
            idx = np.where(labels == i)[0]  # 找到同一类的点的下标
            datas = self.datas[idx, :]
            ax.scatter(datas[:, 0], datas[:, 1], datas[:, 2], c=colors[i])


# # 17070 파이프 옮기기 1

from collections import deque
n = int(input())
arr = [list(map(int, input().split())) for _ in range(n)]

q = deque([(0, 1, 0)]) # 0 가로 1 세로 2 대각선

result = 0

# 끝에 거만 계산하면 되네;;;
def solution(x, y, d):
    global result
    if x == n - 1 and y == n - 1:
        result += 1
        return

    if d == 0:
        if 0 <= x < n and 0 <= y + 1 < n and arr[x][y + 1] == 0:
            solution(x, y + 1, 0)
        if 0 <= x + 1 < n and 0 <= y + 1 < n and arr[x + 1][y + 1] == 0 and arr[x][y + 1] == 0 and arr[x + 1][y] == 0:
            solution(x + 1, y + 1, 2)
    elif d == 1:
        if 0 <= x + 1 < n and 0 <= y < n and arr[x + 1][y] == 0:
            solution(x + 1, y, 1)
        if 0 <= x + 1 < n and 0 <= y + 1 < n and arr[x + 1][y + 1] == 0 and arr[x][y + 1] == 0 and arr[x + 1][y] == 0:
            solution(x + 1, y + 1, 2)
    else:
        if 0 <= x < n and 0 <= y + 1 < n and arr[x][y + 1] == 0:
            solution(x, y + 1, 0)
        if 0 <= x + 1 < n and 0 <= y < n and arr[x + 1][y] == 0:
            solution(x + 1, y, 1)
        if 0 <= x + 1 < n and 0 <= y + 1 < n and arr[x + 1][y + 1] == 0 and arr[x][y + 1] == 0 and arr[x + 1][y] == 0:
            solution(x + 1, y + 1, 2)

solution(0, 1, 0)
print(result)

# n = int(input())
# arr = [list(map(int, input().split())) for _ in range(n)]

# result = 0

# def solution(x1, y1, x2, y2):
#     global result
#     if x2 == n - 1 and y2 == n - 1:
#         result += 1
#         return
#     # 가로
#     if x1 == x2:
#         for (dx1, dy1, dx2, dy2) in [(0, 1, 0, 1), (0, 1, 1, 1)]:
#             nx1, ny1 = x1 + dx1, y1 + dy1
#             nx2, ny2 = x2 + dx2, y2 + dy2

#             if 0 <= nx1 < n and 0 <= ny1 < n and 0 <= nx2 < n and 0 <= ny2 < n:
#                 if arr[nx1][ny1] == 1 or arr[nx2][ny2] == 1:
#                     continue

#                 if nx1 != nx2 and ny1 != ny2:
#                     if arr[nx2 - 1][ny2] == 1 or arr[nx2][ny2 - 1] == 1:
#                         continue

#                 solution(nx1, ny1, nx2, ny2)
#     # 세로
#     elif y1 == y2:
#         for (dx1, dy1, dx2, dy2) in [(1, 0, 1, 0), (1, 0, 1, 1)]:
#             nx1, ny1 = x1 + dx1, y1 + dy1
#             nx2, ny2 = x2 + dx2, y2 + dy2

#             if 0 <= nx1 < n and 0 <= ny1 < n and 0 <= nx2 < n and 0 <= ny2 < n:
#                 solution(nx1, ny1, nx2, ny2)

#     # 대각선
#     else:
#         for (dx1, dy1, dx2, dy2) in [(1, 1, 0, 1), (1, 1, 1, 0), (1, 1, 1, 1)]:
#             nx1, ny1 = x1 + dx1, y1 + dy1
#             nx2, ny2 = x2 + dx2, y2 + dy2

#             if 0 <= nx1 < n and 0 <= ny1 < n and 0 <= nx2 < n and 0 <= ny2 < n:
#                 solution(nx1, ny1, nx2, ny2)

# solution(0, 0, 0, 1)

# print(result)


#tkinter-GUI modules
from tkinter import *
from tkinter import messagebox
from tkinter import ttk
from tkinter import scrolledtext
from PIL import Image
from PIL import ImageTk
import glob
import tkinter.scrolledtext as ScrolledText
import time
import pyttsx3
import speech
import speech_recognition as sr
import os
import ast   #converting string representation of list to list data structures
from tkinter import simpledialog
import pyaudio

#import threading

#spacy related modules - For entity(column extraction)
import spacy

#for importing class EntityMatcher written under entity_extractor py file
from spacy.matcher import PhraseMatcher
from spacy.tokens import Span
from entity_extractor import EntityMatcher as em

#NLTK module - for intent classification
import intent_classification_through_nltk as nl

#for returning queries which are created based on the intents and entities
import query_builder as query

#for extracting dates from natural language
import date_finder as df

#for connecting the front end with db and rerturning the results based on the sql queries
import db_processor as db

#chattebot module- for classifying other than aggrergate functions
from chatterbot.trainers import ListTrainer #method to train the chatterbot
from chatterbot import ChatBot

#list of failureCategory which is a column and the assigned values are the filters applied on the columns
failureCategory = [u'functional defect', u'functional change', u'data issue', u'infrastucture issue', u'technical issue', u'environment issue', u'design update']
team = [u'motor',u'kraft', u'claims', u'schaden', u'kollektiv', u'life',u'leben', u'kranken', u'workflow',u'input and output', u'firmen', u'privat', u'kup', u'collections and disbursement', u'inex',u'cnd',u'provision',u'commission', u'oms',u'contract generally']
failureLogs = [u'retry',u'control',u'robot',u'rmi',u'controlnotfound',u'externalfunction']

#for manually identifying entities (columns) other than the ones which are predefined by scapy
entities_name = ['FAILURECATEGORY', 'TEAM','FAILURELOGS']

#for manually identifying entity values or filters fro columns
entities_value = [failureCategory,team,failureLogs]

#for storing the entities along with there values
entities_extraction_results =[]

#table in the database from which data is going to fetch
table_name="failedData"

#Module used for converting audio to test (use of mike symbol on the GUI)
text_to_audio = pyttsx3.init()
r = sr.Recognizer()

#Setting for chatter bot module used to classify intents like greeting, good bye, etc
bot = ChatBot('queryBot',logic_adapters=[
{
    "import_path": "chatterbot.logic.BestMatch",
    "statement_comparison_function": "chatterbot.comparisons.levenshtein_distance",
    "response_selection_method": "chatterbot.response_selection.get_first_response"
},
{
    "import_path": 'chatterbot.logic.MathematicalEvaluation',
},
{
    'import_path': 'chatterbot.logic.LowConfidenceAdapter',
    'threshold': 0.70,
    'default_response': 'Sorry I can not understand'
}
],
     storage_adapter="chatterbot.storage.SQLStorageAdapter"
)

#set the trainer for chatter bot module
bot.set_trainer(ListTrainer)   #set the trainer

#reading the conversation saved in the text file for giving replies
for filename in glob.glob('greeting.txt'):
    chats = open(filename , 'r').readlines()
    bot.train(chats)

#setting object for tkinter module (welcome window)
window = Tk()

window.title("Welcome to queryBot")

#disabling maximize button of the window
window.resizable(0,0)

response_list=[]

def text_to_speech(response,chk):
    """
        This function is used to read the user responses printed on the gui and converts it to audio format if checkbox is selected
    """
    if chk == True:
        text_to_audio.say(response)
        text_to_audio.runAndWait()

def show_query_result(query_display,conversation,chk):
    """
        This function is used to write the responses return by db in a user readable format on the conversation(scrolledttext)

    """
    if query_display != None:
        query_display = ast.literal_eval(query_display)
        len_query_display = len(query_display)
        #print(len(query_display))
        if type(query_display[0])==tuple:
            for i_items in query_display:
                items_count = 0

                for items in i_items:

                    if items_count == len(i_items)-1:
                        if items == None:
                            conversation.insert(
                            END, "Not Analysed" + "\n"
                    )
                        else:
                            conversation.insert(
                            END, str(items) + "\n"
                    )
                        conversation.see(END)
                    else:
                        if items == None:
                            conversation.insert(
                            END, "Not Analysed" + "->"
                    )
                        else:
                            conversation.insert(
                            END, str(items) + "->"
                    )
                        conversation.see(END)
                    items_count = items_count + 1

        elif type(query_display[0])==list:
            for len_list in range(len_query_display):

                for i_items in query_display[len_list]:
                    items_count = 0

                    for items in i_items:

                        if items_count == len(i_items)-1:
                            if items == None:
                                conversation.insert(
                                END, "Not Analysed" + "\n"
                        )
                            else:
                                conversation.insert(
                                END, str(items) + "\n"
                        )
                            conversation.see(END)
                        else:
                            if items == None:
                                conversation.insert(
                                END, "Not Analysed" + "->"
                        )
                            else:
                                conversation.insert(
                                END, str(items) + "->"
                        )
                            conversation.see(END)
                        items_count = items_count + 1
        conversation.see(END)
        if len(query_display)<20:
            text_to_speech(query_display,chk)

def writing_to_conversation(name,request,response,windows,conversation,flag,chk):
    """
        This function is used to seperate the validations from the queries based on the flag set while calling this function
    """
    conversation['state'] = 'normal'
    if flag == 0:
        conversation.insert(
                END, "ChatBot: " + str(response) + "\n", 'validation_text'
        )
        conversation.tag_config('validation_text', foreground='red')
        conversation.see(END)                           #automatic scroll down to the latest linein conversation.
        text_to_speech(response,chk)

    elif flag == 1:
        conversation.insert(
                END, "ChatBot: ", "chatbot_name"
        )
        conversation.insert(
                END, "Have look on the requested results..." + "\n"
        )
        conversation.tag_config('chatbot_name', foreground='red')
        conversation.see(END)
        show_query_result(str(response),conversation,chk)
        #text_to_speech(response,chk)

    elif flag == 2:
        conversation.insert(
                END, "ChatBot: ", "chatbot_name"
        )
        conversation.insert(
                END, str(response) + "\n"
        )
        conversation.tag_config('chatbot_name', foreground='red')
        conversation.see(END)
        text_to_speech(response,chk)

def intent_entity_extractor(name,request_entry,user_query,windows,conversation,chk):
    """
        This function extract the entities from the natuaral language and store them in the list entities_extraction_results as [Entity,Entity_value]
    """
    entities_extraction_results =[]
    user_query=user_query.lower()
    for loops in range(len(entities_name)):
        nlp = spacy.load('en_core_web_sm')
        entity_matcher = em(nlp, entities_value[loops], entities_name[loops])
        nlp.add_pipe(entity_matcher, after='ner')
        doc = nlp(u"{}".format(user_query))
        for ent in doc.ents:
            entities_extraction_results.append([ent.label_,ent.text])

    #calling classify function which returns the intent of the query
    intent_name_tuple=nl.classify(user_query)

    intent_name = intent_name_tuple[0]

    #print(intent_name)

    #extract date from the user query
    date_entity = df.date_picker(user_query)

    #storing executiondate entity to the existing list of entity if date is present in the user query
    if (date_entity !=None):
        entities_extraction_results.append(['EXECUTIONDATE',date_entity])

    #classification based on the intent
    if intent_name == 'count':
        query_extracted = query.query_builder(user_query,intent_name,entities_extraction_results,table_name,date_entity)

        if "SELECT" in query_extracted:
            query_display = db.db_processor(query_extracted)

            if len(query_display) != 0:
                writing_to_conversation(name,user_query,query_display,windows,conversation,1,chk)

            else :
                writing_to_conversation(name,user_query,"No records found",windows,conversation,0,chk)

        else:
            writing_to_conversation(name,user_query,"Please try out something different query...",windows,conversation,0,chk)
            #print("queryBot: Please try out something different query...")

    elif intent_name == 'greeting':
        response=bot.get_response(user_query)
        writing_to_conversation(name,user_query,response,windows,conversation,2,chk)

    elif intent_name == 'min':
        query_extracted = query.query_builder(user_query,intent_name,entities_extraction_results,table_name,date_entity)
        query_extracted_temp=str(query_extracted)
        if query_extracted!=None:
            if "SELECT" in query_extracted:
                query_display = db.db_processor(query_extracted)

                if (len(query_display)!=0):
                    writing_to_conversation(name,user_query,query_display,windows,conversation,1,chk)
                else:
                    writing_to_conversation(name,user_query,"Sorry we dont find any records for your query. Please try something else.",windows,conversation,0,chk)
                    #print("Sorry we dont find any records for your query. Please try something else.")

            elif "(" in query_extracted_temp:
                writing_to_conversation(name,user_query,query_extracted,windows,conversation,1,chk)

            elif "queryBot" in query_extracted:
                writing_to_conversation(name,user_query,"Please try asking different things",windows,conversation,0,chk)
                #print("queryBot : Please try asking different things")

        else:
            writing_to_conversation(name,user_query,"Sorry we do not have any data to show. Please try entering the query in other way.",windows,conversation,0,chk)

    elif intent_name == 'max':
        query_extracted = query.query_builder(user_query,intent_name,entities_extraction_results,table_name,date_entity)
        query_extracted_temp=str(query_extracted)
        if query_extracted!=None:
            if "SELECT" in query_extracted:
                query_display = db.db_processor(query_extracted)

                if (len(query_display)!=0):
                    writing_to_conversation(name,user_query,query_display,windows,conversation,1,chk)
                else:
                    writing_to_conversation(name,user_query,"Sorry we dont find any records for your query. Please try something else.",windows,conversation,0,chk)
                    #print("Sorry we dont find any records for your query. Please try something else.")

            elif "(" in query_extracted_temp:
                writing_to_conversation(name,user_query,query_extracted,windows,conversation,1,chk)

            elif "queryBot" not in query_extracted:
                writing_to_conversation(name,user_query,"Please try asking different things",windows,conversation,0,chk)
                #print("queryBot : Please try asking different things")

        else:
            writing_to_conversation(name,user_query,"Sorry we do not have any data to show. Please try entering the query in other way.",windows,conversation,0,chk)

    elif intent_name == None:
        writing_to_conversation(name,user_query,"Sorry I dont understand. Please ask me in some different way.",windows,conversation,0,chk)
        #print("queryBot : Sorry I dont understand. Please ask me in some different way.")

    #text_to_speech(user_query,chk)

def click(name,request_entry,user_query,windows,conversation,chk):
    """
        This function is triggred when ENTER is pressed or when send button is clicked and forwards the control for nlp
    """
    conversation['state'] = 'normal'  #open conversation in edit mode
    request_entry.delete(0,'end')   #removing the query after hitting ENTER key from keyword or by clicking button
    if len(user_query)==0:
        conversation.insert(
            END, "Please enter something in the query box before hitting ENTER or pressing \"Ask Something?\" button. " + "\n", "validation_text"
    )
        conversation.tag_config('validation_text', foreground='red')
    else:
        conversation.insert(
                 END, name + ": ", "user_name"
       )
        conversation.insert(
                 END, user_query + "\n"
       )
        conversation.tag_config('user_name', foreground='green')
        intent_entity_extractor(name,request_entry,user_query,windows,conversation,chk)
    conversation['state'] = 'disabled'  #set conversation to read only mode

def say_identify_team(user_query):
    kranken_list = ['drunken', 'franken','cranking','company','cancun','chicken','priyanka','uncle','franklin']
    privat_list = ['prabhat']
    koll_list = ['colective']
    kraft_list = ['craft']
    firmen_list = ['Perman']
    leben_list = ['libin','ribbon','Lebanon','IBN','Livon']
    schaden_list = ['shadan']

    user_query_temp=[re.sub(word.lower(),"kranken",user_query) for word in kranken_list if word.lower() in user_query]
    if(len(user_query_temp)>0):
        user_query = "".join(user_query_temp)

    user_query_temp=[re.sub(word.lower(),"privat",user_query) for word in privat_list if word.lower() in user_query]

    if(len(user_query_temp)>0):
        user_query = "".join(user_query_temp)

    user_query_1=[re.sub(word.lower(),"kollektiv",user_query) for word in koll_list if word.lower() in user_query]

    if(len(user_query_temp)>0):
        user_query = "".join(user_query_temp)

    user_query_temp=[re.sub(word.lower(),"kraft",user_query) for word in kraft_list if word.lower() in user_query]
    if(len(user_query_temp)>0):
        user_query = "".join(user_query_temp)

    user_query_temp=[re.sub(word.lower(),"firmen",user_query) for word in firmen_list if word.lower() in user_query]
    if(len(user_query_temp)>0):
        user_query = "".join(user_query_temp)

    user_query_temp=[re.sub(word.lower(),"leben",user_query) for word in leben_list if word.lower() in user_query]
    if(len(user_query_temp)>0):
        user_query = "".join(user_query_temp)

    user_query_temp=[re.sub(word.lower(),"schaden",user_query) for word in schaden_list if word.lower() in user_query]
    if(len(user_query_temp)>0):
            user_query = "".join(user_query_temp)
    return user_query

def say(name,request_entry,windows,conversation,chk):
    """
        This function is triggered after clicking mike button and converts the audio from user to text and prints it on the conversation
    """

    conversation['state'] = 'normal'

    request_entry.delete(0,'end')

    with sr.Microphone() as source:

        r.adjust_for_ambient_noise(source, duration=1)
        CHUNK = 1024
        FORMAT = pyaudio.paInt16 # the Format is picked up from the pyaudio
        CHANNELS = 2
        RATE = 44100
        source.CHUNK = CHUNK
        source.format = FORMAT # FORMATING THE SOURCE FILE
        r.energy_threshold += 280
        audio = r.listen(source,phrase_time_limit=2)

    try:
        user_query = r.recognize_google(audio,language="en-IN") #r.recognize_sphinx(audio)
        if len(user_query)==0:
            conversation.insert(
                END, "Please enter something in the query box before hitting ENTER or pressing \"Ask Something?\" button. " + "\n", "validation_text"
        )
            conversation.tag_config('validation_text', foreground='red')
        else:
            user_query=say_identify_team(user_query)
            conversation.insert(
                 END, name + ": ", "user_name"
           )
            conversation.insert(
                 END, user_query + "\n"
           )
            conversation.tag_config('user_name', foreground='green')
            #time.sleep(5)
            intent_entity_extractor(name,request_entry,user_query,windows,conversation,chk)

    except sr.UnknownValueError:
        conversation.insert(
            END, "ChatBot: " + "Our machine says it could not understand the audio. Please speak louder orelse speak clearer" + "\n", "validation_text"
        )
        conversation.tag_config('validation_text', foreground='red')
        conversation.see(END)
    except sr.RequestError as e:
        print("Could not request results; {0}".format(e))

    conversation['state'] = 'disabled'

def getting_started(name):
    """
        This function mainly deals with the gui part and handling of conversation
    """
    if len(name)>0:
        #destroying start window so that new conversation window will open
        window.destroy()

        #initializing new object for new window(conversation window)
        windows=Tk()
        windows.resizable(0,0)
        windows.title("queryBot " + "(Conversation with " + name + ")")

        windows=Frame(windows)
        conversation = ScrolledText.ScrolledText(windows,state="disabled")
        conversation.pack()

        request_entry = Entry(windows,width=70)
        request_entry.pack(side=LEFT,ipadx=60)

        #gui setting for mike button
        width = 25
        height = 25
        img = Image.open("./images/mike_1.png")
        img = img.resize((width,height), Image.ANTIALIAS)
        photoImg =  ImageTk.PhotoImage(img)

        sayBtn = Button(windows, text="Say", activebackground="green", fg="black",image=photoImg,command=lambda :say(name,request_entry,windows,conversation,chk_state.get()))
        sayBtn.pack(side=LEFT)

        #used for triggering code after hitting enter from keyboard
        request_entry.bind("<Return>",lambda _ :click(name,request_entry,request_entry.get(),windows,conversation,chk_state.get()))

        #gui setting for send button
        width_send = 25
        height_send = 25
        img = Image.open("./images/send_1.png")
        img = img.resize((width,height), Image.ANTIALIAS)
        photoImg_send =  ImageTk.PhotoImage(img)

        sendBtn = Button(windows, text="Ask something?", activebackground="green", fg="black",image=photoImg_send,command=lambda :click(name,request_entry,request_entry.get(),windows,conversation,chk_state.get()))
        sendBtn.pack(side=LEFT)
        chk_state =BooleanVar()
        chk_state.set(False) #set check state to unselected by default

        #gui setting for Voice checkbox
        width = 30
        height = 30
        img = Image.open("./images/voice.png")
        img = img.resize((width,height), Image.ANTIALIAS)
        photoImg_voice =  ImageTk.PhotoImage(img)

        chk = Checkbutton(windows, text='Voice?',image=photoImg_voice, var=chk_state)
        chk.pack(side=LEFT)

        windows.pack()
        windows.mainloop()

    elif len(name)==0:
        #deals with empty name field
        lbl_name_validation = Label(window, text="Name cannot be empty.")
        lbl_name_validation.grid(column=1, row=2)

lbl_name = Label(window, text="Please enter user name*")
lbl_name.grid(column=0, row=1)
name_entry = Entry(window,width=40)
name_entry.grid(column=1, row=1, sticky='nesw')
name_entry.bind("<Return>", lambda _ : getting_started(name_entry.get()))

#gui setting for start button
##width = 120
##height = 50
##img = Image.open("./images/start.png")
##img = img.resize((width,height), Image.ANTIALIAS)
##photoImg_start =  ImageTk.PhotoImage(img)
btn_getting_started = Button(window, text="Continue", command=lambda:getting_started(name_entry.get()))

btn_getting_started.grid(column=1, row=3)
window.mainloop()