import json
import math
import os
import random

import torch
from randomdict import RandomDict
import numpy as np
import matplotlib.pyplot as plt
import networkx as nx

from replay.memory import Memory, DoubleMemoryDataStore
from tools.utils import preprocess


class Oegn:
    def __init__(self,steps_init,args,obs_size,action_space,goal_space,context,save_dir=None,**kwargs):
        self.args=args
        data=torch.FloatTensor(1 if not self.args.nodes_number else  self.args.nodes_number, self.args.num_latents if not self.args.state else 2).uniform_(-0.01, 0.01)
        context.memory = Memory(args,obs_size,action_space,context,mode=0)
        context.memory2 = Memory(args,obs_size,action_space,context,mode=1)
        self.storeType = DoubleMemoryDataStore

        self.obs_size=obs_size
        self.steps_init=steps_init
        self.action_space=action_space
        self.goal_space=goal_space
        self.has_deleted = False
        self.period=self.args.gwr_period
        self.units_created =0
        self.steps=-1
        self.context=context
        self.node_list,self.reward_list,self.mix_list,self.exec_reward_list=[],[],[],[]


        if save_dir is not None:
            self.save_dir = save_dir + "/gng/"
            if not os.path.exists(self.save_dir): os.mkdir(self.save_dir)
            # if data.shape[1] == 2:
            self.tmp_act = np.zeros((int(self.period), data.shape[1]))
            self.tmp_mu = None


        self.buffers = RandomDict({})
        for i in range(data.shape[0]):
            self.buffers[i]=self.storeType(i, self.args, self.obs_size, self.action_space,self.context,self)

        self.network = nx.Graph()

        for i in range(data.shape[0]):
            self.network.add_node(self.units_created, count=0, error=0)
            self.add_edge(0, self.units_created)
            self.update_neighbors(self.units_created)
            self.node_list.append(self.units_created)

            self.reward_list.append(0)
            self.mix_list.append(0)
            self.exec_reward_list.append(0)
            self.units_created += 1


        self.available_nodes=torch.ones(data.shape[0],dtype=torch.bool)
        plt.style.use('ggplot')
        self.embeds=data.clone().detach()

        #### To remove unfilled inactives buffers
        self.period_deletes = self.args.sub_nlearn * self.args.num_som_updates * 50000
        self.cpt_steps = 0
        self.inactives = []

        #for logs
        self.mean_interdistance = 0
        self.del_nodes =0
        self.cpt_close_deleted=0
        self.cpt_time_deleted = 0
        self.del_edges = 0
        self.insertions=0
        self.deleted_nodes=[]
        self.cpt_mode = 0

    ############ Utils
    def generate_all(self):
        return self.embeds[self.available_nodes],torch.nonzero(self.available_nodes,as_tuple=False)


    def is_nearest_unit_among_neighbors(self, node,goal):
        if (self.get(self.find_nearest_units(goal.view(1,-1))[0]) != self.get(node)).any():
            return False
        return True

    def find_nearest_units2(self,goal,number):
        norms = torch.norm(self.embeds-goal,2,dim=1)
        v,u=torch.topk(norms,number,dim=0,largest=False)
        return v,u

    def find_nearest_units_all(self,goal):
        norms = torch.norm(self.embeds.view(self.embeds.shape[0],1,-1)-goal.view(1,goal.shape[0],-1),2,dim=2)
        return torch.min(norms,0)

    def find_nearest_two_units(self, goal,goal2):
        norm = torch.norm(self.embeds-goal,2,dim=1)
        norm2 = torch.norm(self.embeds-goal2,2,dim=1)
        return torch.argmin(norm,dim=0).item(),torch.argmin(norm2,dim=0).item()

    def find_nearest_units(self, goal,min=False,both=False,**kwargs):
        assert self.embeds.shape[1] == goal.shape[1]
        norms = torch.norm(self.embeds-goal,2,dim=1)
        if both:
            return torch.min(norms,0)
        elif min:
            return torch.min(norms)
            # return i.item,v.item()
        return torch.argmin(norms,dim=0).item(),None

    def neighbors(self,node):
        return self.network.neighbors(node)

    def maj_paths(self):
        pass
        # if self.args.plan_interval > -1:
        #     print("maj paths")
        #     self.shortest_paths = dict(nx.all_pairs_shortest_path(self.network))

    def get(self,key):
        if self.buffers[key].is_deleted():
            raise Exception(str(key)+" is already deleted")
        return self.embeds[key]

    def get_all(self,keys):
        return self.embeds[keys]

    @property
    def num_nodes(self):
        return self.network.number_of_nodes()

    def __getitem__(self, key):
        return self.get(key)

    def can_learn(self):
        return self.insertions > self.steps_init


    def random_unit(self):
        key = self.buffers.random_key()
        while self.buffers[key].is_deleted():
            key = self.buffers.random_key()
        return key,self.get(key)

    ################### Core step

    def step(self, embed,node_sampled,success,prev,distid,**kwargs):
        if self.args.nodes_number:
            s_1, _ = self.find_nearest_units(embed)
            self.update_operator(embed, s_1)
            s_n = self.find_nearest_units(prev.view(1,-1))[0]
            self.step_edge(s_1=s_1,s_2=s_n)
            return None,s_1

        if self.buffers[node_sampled].is_deleted():
            return None, None

        self.has_deleted = False
        insertion_node = None

        s_prox,_ = self.find_nearest_units(embed)
        s_1 = s_prox
        s_2=node_sampled
        self.network.nodes[s_1]["error"] = 0

        ###Check for inactive nodes
        up_success = (s_2==s_1)
        if not self.inactive_delete_operator(s_2,up_success):
            return None, 0

        ###Create node
        activity=torch.norm(embed-self.get(s_1),2)
        if success and activity > self.args.a_threshold and self.network.nodes[s_1]['count'] >= self.args.gwr_tau:
            insertion_node = self.add_middle_node(embed,s_1,s_2,count=0)

        ###Moves node
        check_success = success #if not self.args.up_success else up_success
        if not insertion_node and check_success and distid:
            self.update_operator(embed, s_1)

        ###Update edges
        if not insertion_node and distid:
            s_n = self.find_nearest_units(prev.view(1,-1))[0]
            self.step_edge(s_1=s_prox,s_2=s_n)

        ###Check for empty and inactives nodes
        self.cpt_steps += 1
        if self.cpt_steps%self.period_deletes == 0:
            self.delete_inactives()
        return insertion_node,s_1


    ################### Operators

    def close_delete_operator(self,s_1,force=False):

        if self.network.number_of_nodes() <= 1 or self.network.nodes[s_1]['count'] < self.args.wait_to_delete:# or self.network.nodes[s_1]['firing'] > self.f_threshold:
            return
        goal=self.get(s_1)
        norms=torch.norm(self.embeds - goal, 2, dim=1)
        indices=torch.nonzero(norms < self.args.a_threshold*self.args.delete_close, as_tuple=False)
        if indices.shape[0] > 1:
            for nodes in indices:
                node=nodes.item()
                if node != s_1:
                    if self.buffers[node].cpt_select_indexes >=  self.buffers[s_1].cpt_select_indexes:
                        tmp = s_1
                        s_1 = node
                        node = tmp
                    nns = list(self.neighbors(node))
                    self.delete_node(node)
                    self.cpt_close_deleted+=1
                    break
            for nn in nns:
                if not self.buffers[nn].is_deleted() and nn != s_1:
                    self.add_edge(s_1, nn)

    def inactive_delete_operator(self,s_1,success,delete=True):
        if success:
            self.network.nodes[s_1]["error"] = 0
            return True

        if not "error" in self.network.nodes[s_1]:
            self.network.nodes[s_1]["error"]=0

        if self.network.number_of_nodes() <= 1 or self.network.nodes[s_1]['count'] < self.args.wait_to_delete:#self.args.gwr_tau:
            return True

        self.network.nodes[s_1]["error"] = self.network.nodes[s_1]["error"]+1
        if delete and self.network.nodes[s_1]["error"] > self.args.error_max:
            self.cpt_time_deleted+=1
            self.delete_node(s_1)
            return False
        return True

    def update_operator(self,goal,s_1):
        if self.args.gwr_lr2 != 0.:
            for neighbor in self.neighbors(s_1):
                update_w_s_n = (goal - self.get(neighbor))
                self.embeds[neighbor] = self.get(neighbor)+ self.args.gwr_lr2*update_w_s_n
        update_w_s_1 = (goal-self.get(s_1))
        self.embeds[s_1] = self.embeds[s_1] + self.args.gwr_lr * update_w_s_1
        if not self.args.nodes_number:
            self.close_delete_operator(s_1)


    def delete_inactives(self):
        self.new_inactives = []
        removed= []
        for k in self.network.nodes():
            if not self.buffers[k].available():
                if k in self.inactives:
                    removed.append(k)
                else:
                    self.new_inactives.append(k)
        for n in removed:
            self.delete_node(n)
        self.inactives = self.new_inactives


    def add_middle_node(self,w,q,*args,**kwargs):
        self.network.add_node(self.units_created, error=0, **kwargs)
        self.network.add_edge(self.units_created, q, age=0)


        ret = self.units_created

        self.node_list.append(ret)
        self.reward_list.append(self.reward_list[q])
        self.mix_list.append(self.mix_list[q])
        self.exec_reward_list.append(self.exec_reward_list[q])

        self.embeds = torch.cat((self.embeds, w.view(1, -1).clone().detach()), dim=0)

        self.buffers[self.units_created]=self.storeType(self.units_created, self.args, self.obs_size, self.action_space,self.context,self)
        self.update_neighbors(self.units_created)
        self.update_neighbors(q)

        self.available_nodes = torch.cat((self.available_nodes,torch.ones((1,),dtype=torch.bool)),dim=0)
        self.units_created += 1
        self.maj_paths()
        return ret

    def step_edge(self, s_1, s_2):
        if self.buffers[s_1].is_deleted() or self.buffers[s_2].is_deleted():
            return
        if s_1 == s_2:
            return

        #reset age of edge            
        self.add_edge(s_2, s_1)

        #check if nodes need to be deleted
        removed=[]
        for neighbor in self.network.neighbors(s_2):
            # 8.  Age edges with an end at s
            self.add_edge(s_2, neighbor, age=self.network.edges[s_2, neighbor]['age']+1)
            if self.network.edges[s_2, neighbor]['age'] > self.args.a_max and self.network.number_of_nodes() >1:
                removed.append((s_2, neighbor))
        self.delete_edge(removed)
        removed=[]
        for neighbor in self.network.neighbors(s_1):
            # 8.  Age edges with an end at s
            self.add_edge(s_1, neighbor, age=self.network.edges[s_1, neighbor]['age']+1)
            if self.network.edges[s_1, neighbor]['age'] > self.args.a_max and self.network.number_of_nodes() >1:
                removed.append((s_1, neighbor))
        self.delete_edge(removed)
    
    def add_edge(self, u, v,age=0):
        connected = self.network.has_edge(u, v)
        self.network.add_edge(u, v, age=age)
        if not connected:
            self.update_neighbors(u)
            self.update_neighbors(v)
            self.maj_paths()


    def delete_edge(self,removed):
        # 10. Delete edges and nodes
        for u,v in removed:
            self.network.remove_edge(u, v)
            self.update_neighbors(u)
            self.update_neighbors(v)
            if self.network.degree(u) == 0 and self.network.number_of_nodes() >1 and not self.args.nodes_number:
                print("delete edge")
                self.delete_node(u)
                self.del_edges += 1
            if self.network.degree(v) == 0 and self.network.number_of_nodes() > 1 and not self.args.nodes_number:
                print("delete edge")
                self.delete_node(v)
                self.del_edges += 1
        if len(removed) > 0:
            self.maj_paths()


    def delete_node(self,node):
        r = list(self.network.neighbors(node))
        # removed=[]
        # for neighbor in r:
        #     removed.append((node, neighbor))
        # self.delete_edge(removed)
        self.has_deleted = True
        self.del_nodes+=1
        self.reward_list[node]=-10000
        self.mix_list[node]=-10000
        self.exec_reward_list[node]=-10000

        self.embeds[node] = -10000
        self.buffers[node].delete(self.get(node))
        self.deleted_nodes.append(node)
        self.available_nodes[node]=False
        self.network.remove_node(node)
        self.maj_paths()
        for n in r:
            self.update_neighbors(n)
            if self.network.degree(n) == 0 and self.network.number_of_nodes() >1 and not self.args.nodes_number:
                print("delete edge")
                if not self.buffers[n].is_deleted():
                    self.delete_node(n)
                    self.del_edges += 1


    def update_neighbors(self,node):
        self.buffers[node].neighbors = [node]+list(self.neighbors(node))

    ##################Buffer and sampling

    def insert(self,embed,prev_obs,obs,action,mask,*args,goal=None,goal_cluster=None,goal_step=None,state=None,act_state=None,**kwargs):
        self.insertions+=1
        e = embed.detach()
        node_goal, _ = self.find_nearest_units(goal.detach() if act_state is None else act_state)
        self.buffers[node_goal].insert(prev_obs, obs, action, mask, *args, goal=goal, embed=e, mode="goal",
                                       node_goal=self.get(node_goal), goal_step=goal_step,state=state, **kwargs)

        node, _ = self.find_nearest_units(e if state is None else state)
        ind = self.buffers[node].insert(prev_obs, obs, action, mask, *args, goal=goal, embed=e, mode="embed",
                                        node_goal=self.get(node), goal_step=goal_step, state=state,**kwargs)
        return ind,self.buffers[node].id


    def random_cluster(self):
        if self.args.ratio_pi > 0. and random.random() < self.args.ratio_pi and self.context.coord_actor.dist_weights is not None:
            key = self.context.coord_actor.dist_weights.sample().item()
            self.used_ratio = True
            return key

        self.used_ratio=False
        if self.args.skew_sample:
            key = random.choices(self.node_list, weights=self.reweight_list, k=1)[0]

        else:
            key = self.buffers.random_key()
        return key

    def sample(self,batch,**kwargs):
        self.batch=batch
        i=0
        sids = []
        inds = []
        ids = []
        bids = []
        self.reweight_list = [(self.buffers[k].cpt_select_indexes+1)**self.args.skew_sample for k in self.buffers]
        # batch.label_obs.zero_()
        while i < self.args.batch_size:
            key=self.random_cluster()
            while not self.buffers[key].available() :
                key = self.random_cluster()
            sid,ind,id,bid=self.buffers[key].sample(batch,i,**kwargs)
            sids.append(sid)
            inds.append(ind)
            ids.append(id)
            bids.append(bid)
            if not self.args.ratio_for_predictor:
                assert self.args.ratio_pi > 0
                batch.sac_train[i:i+1]=self.used_ratio
            i=i+1

        batch.distribution_id = torch.tensor(bids,dtype=torch.bool)
        bids = batch.distribution_id
        noid = ~batch.distribution_id

        inds = torch.tensor(inds)
        sids = torch.tensor(sids)
        ids = torch.tensor(ids)
        mem2_inds = inds[bids]
        mem2_sids = sids[bids]

        mem1_inds = inds[noid]
        mem1_sids = sids[noid]
        mem = self.context.memory
        mem2 = self.context.memory2

        batch.masks = torch.cat((mem.masks[mem1_sids,mem1_inds].view(-1,mem.masks.shape[2]),mem2.masks[mem2_sids,mem2_inds].view(-1,mem.masks.shape[2])),dim=0).to(self.args.device)
        batch.actions = torch.cat((mem.actions[mem1_sids,mem1_inds].view(-1,mem.actions.shape[2]),mem2.actions[mem2_sids,mem2_inds].view(-1,mem.actions.shape[2])),dim=0).to(self.args.device)
        batch.rewards = torch.cat((mem.rewards[mem1_sids,mem1_inds].view(-1,mem.rewards.shape[2]),mem2.rewards[mem2_sids,mem2_inds].view(-1,mem.rewards.shape[2])),dim=0).to(self.args.device)
        batch.goals = torch.cat((mem.goals[mem1_sids,mem1_inds].view(-1,mem.goals.shape[2]),mem2.goals[mem2_sids,mem2_inds].view(-1,mem.goals.shape[2])),dim=0).to(self.args.device)
        batch.goals_step = torch.cat((mem.goals_step[mem1_sids,mem1_inds].view(-1,1),mem2.goals_step[mem2_sids,mem2_inds].view(-1,1)),dim=0).to(self.args.device)
        batch.obs = preprocess(torch.cat((mem.obs[mem1_sids, mem1_inds].view(-1, *self.obs_size),mem2.obs[mem2_sids, mem2_inds].view(-1, *self.obs_size)), dim=0),self.args)
        batch.next_obs = preprocess(torch.cat((mem.next_obs[mem1_sids, mem1_inds].view(-1, *self.obs_size),mem2.next_obs[mem2_sids, mem2_inds].view(-1, *self.obs_size)),dim=0), self.args)
        batch.index = torch.cat((mem1_inds, mem2_inds), dim=0)
        batch.goals_obs = preprocess(torch.cat((mem.goals_obs[mem1_sids,mem1_inds].view(-1,*self.obs_size),mem2.goals_obs[mem2_sids,mem2_inds].view(-1,*self.obs_size)),dim=0),self.args)
        if self.args.state:
            batch.states = torch.cat((mem.states[mem1_sids, mem1_inds].view(-1, 2),mem2.states[mem2_sids, mem2_inds].view(-1, 2)),dim=0)
            batch.prev_states = torch.cat((mem.prev_states[mem1_sids, mem1_inds].view(-1, 2),mem2.prev_states[mem2_sids, mem2_inds].view(-1, 2)),dim=0)

        # batch.ind = torch.cat((mem1_inds,mem2_inds),dim=0)
        batch.ind = torch.cat((ids[noid],ids[batch.distribution_id]),dim=0)
        if self.args.relabeling == 1 or self.args.relabeling == 2:
            batch.label_obs = preprocess(torch.cat((batch.label_obs[noid],batch.label_obs[batch.distribution_id]),dim=0),self.args)
        if not self.args.ratio_for_predictor:
            batch.sac_train = torch.cat((batch.sac_train[noid],batch.sac_train[batch.distribution_id]),dim=0).to(self.args.device)

        batch.distribution_id = torch.cat((batch.distribution_id[noid],batch.distribution_id[batch.distribution_id]),dim=0).to(self.args.device)
        if self.args.type == 1 or self.args.type == 4:
            weights = [self.buffers[k].cpt_select_indexes + 1 if self.buffers[k].learnDataStore.available() and self.buffers[k].cpt_select_indexes != 0 else -10000 for k in self.buffers]
            tweights = torch.tensor(weights,dtype=torch.float)
            if self.args.type == 1:
                batch.densities = 1/torch.sqrt(tweights[batch.ind]).to(self.args.device)
            if self.args.type == 4:
                dist_weights = torch.distributions.Categorical(logits=torch.log(tweights))
                logpb= dist_weights.log_prob(batch.ind).to(self.args.device)
                batch.densities = -logpb

        return batch


    #########################LOGS

    def update_properties(self,s_1,vectors=None,**kwargs):
        # 8 & 9. Update firing counter and update age
        self.network.nodes[s_1]['count']=self.network.nodes[s_1]['count']+1
        removed=[]
        if vectors is not None:
            self.tmp_act[self.steps%self.period]=vectors.numpy()
        self.steps+=1
        self.plot_png()
        return removed

    def plot_png(self):
        if self.save_dir is not None and self.steps % self.period == 0:
            self.plot_network(self.save_dir + str(self.steps // self.period) + '.png')

    def plot_network3d(self,file_path):
        #https://www.idtools.com.au/3d-network-graphs-python-mplot3d-toolkit/
        G=self.network
        # 3D network plot
        with plt.style.context(('ggplot')):
            # fig = plt.figure(figsize=plt.figaspect(0.5))
            fig = plt.figure(figsize=(10, 10))
            from mpl_toolkits.mplot3d import Axes3D
            ax = Axes3D(fig)
            ax.set_xlim3d(-3, 3)
            ax.set_ylim3d(-3, 3)
            ax.set_zlim3d(-3, 3)

            # Loop on the pos dictionary to extract the x,y,z coordinates of each node
            for n in self.network.nodes:
                value=self.get(n)
                xi = value[0].item()
                yi = value[1].item()
                zi = value[2].item()

                # Scatter plot
                ax.scatter(xi, yi, zi, s=100, c="blue", edgecolors='k', alpha=0.2)
                ax.text(xi,yi,zi,str(n))

            # Loop on the list of edges to get the x,y,z, coordinates of the connected nodes
            # Those two points are the extrema of the line to be plotted
            for i, j in enumerate(G.edges()):
                n1=self.get(j[0])
                n2=self.get(j[1])
                x = np.array((n1[0].item(), n2[0].item()))
                y = np.array((n1[1].item(), n2[1].item()))
                z = np.array((n1[2].item(), n2[2].item()))

                # Plot the connecting lines
                ax.plot(x, y, z, c='blue', alpha=0.5)

        ax.scatter(self.tmp_act[:,0],self.tmp_act[:,1],self.tmp_act[:,2], color='green', marker='o',s=4)
        if self.context.estimator.store is not None:
            batch=self.context.estimator.store.cpu().numpy()
            ax.scatter(batch[:, 0],batch[:, 1],batch[:, 2], color='blue', marker='o', s=1)

        # Set the initial view
        ax.view_init(30,30)
        if file_path :
            plt.savefig(file_path)
            plt.close('all')
        return

    def plot_network(self, file_path):
        check_num_dim=self.args.num_latents if not self.args.state else 2
        if check_num_dim == 3:
            return self.plot_network3d(file_path)

        plt.clf()
        node_pos = {}
        for u in self.network.nodes():
            vector = self.get(u)
            node_pos[u] = (vector[0].item(), vector[1].item())
        fig, ax = plt.subplots()
        fig.set_size_inches(10.0, 10.0)
        plt.xlim(-10, 10)
        plt.ylim(-10, 10)
        ax.scatter(self.tmp_act[:,0],self.tmp_act[:,1], color='green', marker='o',s=2)

        if self.context.estimator.store is not None:
            batch=self.context.estimator.store.cpu().numpy()
            ax.scatter(batch[:, 0],batch[:, 1], color='blue', marker='o', s=1)

        nx.draw_networkx(self.network,pos=node_pos,ax=ax,alpha=0.5)
        plt.savefig(file_path)
        plt.close(fig)

    def compute_mean_interdistance(self):
        mean_interdistance = 0
        all_cpt=0
        for n in self.network.nodes:
            interdist=0
            cpt=0
            for nei in self.neighbors(n):
                cpt += 1
                interdist += torch.norm(self.embeds[nei] - self.embeds[n], 2).item()
            if cpt != 0:
                interdist /= cpt
            all_cpt+=1
            mean_interdistance += interdist

        mean_interdistance /= all_cpt
        return mean_interdistance

    def log_rewards(self,save_dir,i):
        file = open(save_dir+"/mode_rewards"+str(i)+".log", "w+")
        for l in range(len(self.reward_list)):
            file.write("%d ; %f ; %f ; %f; %d; %d; %d; %d; %d; %d ;%d ;%d ;%d \n"%(l,round(self.reward_list[l],2),round(self.exec_reward_list[l],2),
                                                   round(self.mix_list[l],2),self.network.nodes[l]["error"] if not self.buffers[l].is_deleted() else -1,
                                                    self.buffers[l].cpt_select_indexes if hasattr(self.buffers[l],"cpt_select_indexes") else 0,
                                                    self.buffers[l].cpt_goal_select_indexes if hasattr(self.buffers[l], "cpt_goal_select_indexes") else 0,
                                                    self.buffers[l].insertions,self.buffers[l].original_insertions,self.buffers[l].cpt_removed_index,
                                                      self.buffers[l].cpt_add_index,self.buffers[l].cpt_goal_removed_index,self.buffers[l].cpt_goal_add_index
                                                      ))
        file.close()

    def load(self):
        if self.context.load_model_path:
            path = self.context.load_model_path + "som_embeds.pt"
            checkpoint = torch.load(path)
            self.embeds = checkpoint['embeds']
            self.insertions=datastore["insertions"]
            self.buffers=RandomDict({})
            for i in range(self.units_created):
                self.buffers[i] = self.storeType(i, self.args, self.obs_size, self.action_space,self.context,self)
                self.buffers[i].load(datastore[str(i)],node=self.get(i))

            self.available_nodes=(self.embeds != -10000).all(dim=1)
            self.node_list = datastore["node_list"]
            self.reward_list = datastore["reward_list"]


    def save(self):
        if self.context.save_model:
            datastore=super().save()
            datastore["node_list"] = self.node_list
            datastore["reward_list"] = self.reward_list
            datastore["insertions"]=self.insertions
            for k in self.buffers:
                datastore[k] = self.buffers[k].save()
            with open(self.context.path_models + "buffers.json", 'w') as f:
                json.dump(datastore, f)
            path = self.context.path_models+"som_embeds.pt"
            torch.save({'embeds':self.embeds}, path)

