#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# %pdb
"""

With this program, train multiple agents agent using Proximal Policy Optimisation (PPO) with a Graph Neural Network(GNN).
Running instructions in the README file.

J. Lee, Y. Cheng, D. Niyato, Y. L. Guan and D. González G.,
"Intelligent Resource Allocation in Joint Radar-Communication With Graph Neural Networks,"
in IEEE Transactions on Vehicular Technology, vol. 71, no. 10, pp. 11120-11135, Oct. 2022, doi: 10.1109/TVT.2022.3187377.

"""

import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import gym
import logz
import os
import time
import inspect
from torch.distributions import Categorical, Normal
# from typing import Callable, Union
import torch_geometric
from torch_geometric.nn import GCNConv, TransformerConv, knn_graph, knn
from torch_geometric.data import Data
from torch_geometric.loader import DataLoader
from torch_geometric.utils import to_dense_adj, dense_to_sparse

from beamformJRCenv_multi import Beamform_JRC
from utils import *

# In[]

# device = torch.device(0 if torch.cuda.is_available() else "cpu")
# device = torch.device("cpu")
# print(device)

def setup_logger(logdir, locals_):
    # Configure output directory for logging
    logz.configure_output_dir(logdir)
    # Log experimental parameters
    args = inspect.getargspec(train_AC)[0]
    params = {k: locals_[k] if k in locals_ else None for k in args}
    logz.save_params(params)

def masking(edge_index, in_map, device, msg_ac=None):
    in_map = torch.tensor(in_map, device=device)
    adj = to_dense_adj(edge_index).squeeze(0)
    adj = adj.fill_diagonal_(0)
    adj = adj * (in_map * in_map.reshape(-1,1))
    if msg_ac is not None:
        adj = adj * msg_ac.reshape((-1,1))
    edge_index, _ = dense_to_sparse(adj)

    return edge_index
# In[]

class GCN(nn.Module):
    def __init__(self, ob_dim, policy_out_dim, policy_size, vf_size, discrete):
        super(GCN, self).__init__()
        self.out_dim = policy_out_dim
        self.discrete = discrete

        self.p_conv1 = GCNConv(ob_dim, policy_size[0])
        self.p_conv2 = GCNConv(policy_size[0], policy_size[1])
        self.p_out = GCNConv(policy_size[1], policy_out_dim)

        self.v_conv1 = GCNConv(ob_dim, vf_size[0])
        self.v_conv2 = GCNConv(vf_size[0], vf_size[1])
        self.v_out = GCNConv(vf_size[1], 1)

    def forward(self, data):
        x, edge_index = data.x, data.edge_index

        action_logits = self.p_conv1(x, edge_index)
        action_logits = F.relu(action_logits)
        action_logits = self.p_conv2(action_logits, edge_index)
        action_logits = F.relu(action_logits)
        action_logits = self.p_out(action_logits, edge_index)

        v, edge_index_v = data.x, data.edge_index
        v = self.v_conv1(v, edge_index_v)
        v = F.relu(v)
        v = self.v_conv2(v, edge_index_v)
        v = F.relu(v)
        v = self.v_out(v, edge_index_v)

        if self.discrete == False:
            mean, logstd = torch.split(action_logits, int(self.out_dim/2), dim = int(action_logits.dim()-1))
            dist = Normal(mean, logstd.exp())
        else:
            dist = Categorical(logits = action_logits)

        return dist, v

class GNN2(nn.Module):
    def __init__(self, ob_dim, policy_out_dim, policy_size, vf_size, discrete, recurrent, ic3net, pooling):
        super(GNN2, self).__init__()
        self.out_dim = policy_out_dim
        self.discrete = discrete
        self.recurrent = recurrent
        self.ic3net = ic3net

        self.encoder = nn.Linear(ob_dim, policy_size[0])
        if self.recurrent:
            self.p_lin1 = nn.GRUCell(policy_size[0], policy_size[1])
        else:
            self.p_lin1 = nn.Linear(policy_size[0], policy_size[1])
        if pooling == 'gcn':
            self.p_conv2 = GCNConv(policy_size[1], policy_size[1])
        elif pooling == 'transformer':
            self.p_conv2 = TransformerConv(policy_size[1], policy_size[1])
        self.p_out = nn.Linear(policy_size[1], policy_out_dim)
        if self.ic3net:
            self.p_comm_out = nn.Linear(policy_size[1], 2)

        self.v_out = nn.Linear(policy_size[1], 1)

    def forward(self, data, hidden=None):
        x, edge_index = data.x, data.edge_index

        features = self.encoder(x)
        if self.recurrent:
            features = self.p_lin1(features, hidden)
            next_hidden = features.clone()
        else:
            features = self.p_lin1(features)
            features = F.relu(features)
            next_hidden = None
        features = self.p_conv2(features, edge_index)
        features = F.relu(features)
        action_logits = self.p_out(features)

        if self.ic3net:
            comm_logits = self.p_comm_out(features)
            comm_dist = Categorical(logits=comm_logits)
        else:
            comm_dist = None

        v = self.v_out(features)

        if self.discrete == False:
            mean, logstd = torch.split(action_logits, int(self.out_dim/2), dim = int(action_logits.dim()-1))
            dist = Normal(mean, logstd.exp())
        else:
            dist = Categorical(logits = action_logits)

        return dist, v, next_hidden, comm_dist

# In[]

class Agent(object):
    def __init__(self, computation_graph_args, sample_trajectory_args, estimate_return_args):
        self.ob_dim = computation_graph_args['ob_dim']
        self.ac_dim = computation_graph_args['ac_dim']
        self.num_users = computation_graph_args['num_users']
        self.num_users_NN = computation_graph_args['num_users_NN']
        self.num_intelligent = computation_graph_args['num_intelligent']
        self.mode = computation_graph_args['mode']                       # policy of the unintelligent agents
        self.radar_interval = computation_graph_args['radar_interval']
        self.num_agents = computation_graph_args['num_agents']
        self.discrete = computation_graph_args['discrete']

        ''' NN architecture '''
        self.CNN = computation_graph_args['CNN']
        self.GNN = computation_graph_args['GNN']
        self.GNN_pooling = computation_graph_args['GNN_pooling']
        self.size = computation_graph_args['size']
        self.size_critic = computation_graph_args['size_critic']
        self.n_layers = computation_graph_args['n_layers']
        self.learning_rate = computation_graph_args['learning_rate']
        self.recurrent = computation_graph_args['recurrent']
        self.detach_gap = computation_graph_args['detach_gap']
        self.input_prev_ac = computation_graph_args['input_prev_ac']
        self.GNN_kNN = computation_graph_args['GNN_kNN']
        self.ic3net = computation_graph_args['ic3net']

        ''' learning parameters '''
        self.ppo_epochs = computation_graph_args['ppo_epochs']
        self.mb_size = computation_graph_args['minibatch_size']
        self.ppo_clip = computation_graph_args['ppo_clip']
        self.v_coef = computation_graph_args['v_coef']
        self.entrop_loss_coef_schedule = computation_graph_args['entrop_loss_coef']
        self.max_grad_norm = computation_graph_args['max_grad_norm']
        self.test = computation_graph_args['test']

        self.animate = sample_trajectory_args['animate']
        self.max_path_length = sample_trajectory_args['max_path_length']
        self.min_timesteps_per_batch = sample_trajectory_args['min_timesteps_per_batch']
        self.timesteps_this_batch = 0

        self.gamma = estimate_return_args['gamma']
        self.lamb = estimate_return_args['lambda']
        self.reward_to_go = estimate_return_args['reward_to_go']
        self.gae = estimate_return_args['gae']
        self.n_step = estimate_return_args['n_step']
        self.critic = estimate_return_args['critic']
        self.decentralised_critic = estimate_return_args['decentralised_critic']
        self.normalize_advantages = estimate_return_args['normalize_advantages']

        self.current_progress_remaining = 1.0

        # self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        self.device = torch.device(0 if torch.cuda.is_available() else "cpu")
        self.device = torch.device("cpu")

        policy_in_dim = int(self.ob_dim) # / self.num_users)
        policy_out_dim = int(self.ac_dim)
        if self.discrete == False:
            policy_out_dim = policy_out_dim * 2

        self.users_per_agent = int(self.num_users/self.num_agents)      # for parameter sharing

        self.edge_index = torch.empty((0,2), dtype=torch.long)
        for i in range(self.num_intelligent):
            for j in range(self.num_intelligent):
                if i != j:
                    self.edge_index = torch.cat((self.edge_index, torch.tensor([[i,j]], dtype=torch.long)))
        self.edge_index = self.edge_index.t()

        if self.GNN > 0:
            GNN_in = policy_in_dim #* self.num_intelligent
            GNN_out = policy_out_dim #* self.num_intelligent
            if self.GNN == 1:
                self.net = GNN(GNN_in, GNN_out, self.size, self.size_critic, discrete=self.discrete).to(self.device)
            elif self.GNN == 2:
                self.net = GNN2(GNN_in, GNN_out, self.size, self.size_critic, discrete=self.discrete, recurrent=self.recurrent, ic3net=self.ic3net, pooling=self.GNN_pooling).to(self.device)
            self.optimizer = optim.Adam(self.net.parameters(), lr=self.learning_rate)

    def update_current_progress_remaining(self, num_iterations: int, total_iterations: int) -> None:
        """
        Compute current progress remaining (starts from 1 and ends to 0)

        :param num_timesteps: current number of timesteps
        :param total_timesteps:
        """
        self.current_progress_remaining = 1.0 - float(num_iterations) / float(total_iterations)

    def update_entropy_loss(self):
        self.entrop_loss_coef = self.entrop_loss_coef_schedule(self.current_progress_remaining)

    """
    Prefixes and suffixes:
            ob - observation
            ac - action
            _no - this tensor should have shape (batch self.size /n/, observation dim)
            _na - this tensor should have shape (batch self.size /n/, action dim)
            _n  - this tensor should have shape (batch self.size /n/)
            _uo - (num users, ob dim)
            _nuo- (batch size, num users, ob dim)

    """
    def act(self, ob, prev_comm_ac, hidden=None, steps=None):
        '''
        Parameters
        ----------
        ob : pytorch geometric Data(x=[8, 101], edge_index=[2, 28])
        prev_comm_ac : np array (8,1)
        steps : int

        Returns
        -------
        ac_np : np array (#users,)
        log_prob_ua : torch.Size([#users])
        prev_comm_ac : list len #users
        '''
        # prev_comm_ac = [0 for _ in range(self.num_users)]

        if self.GNN == 0:
            ob_uo = torch.tensor(ob[0 : self.num_intelligent],requires_grad=False,dtype=torch.float32, device=self.device) #.unsqueeze(0)

        dist, value, next_hidden, comm_dist = self.net(ob, hidden)              # logits: torch.size([#agents, ac_dim]), value: torch.Size([#agents,1])

        ac_ua = dist.sample()                   # torch.size([#agents])
        ac_np = ac_ua.cpu().numpy()                   # (#agents,)
        log_prob_ua = dist.log_prob(ac_ua).detach()      # torch.size([#agents])

        if self.ic3net:
            msg_ac = comm_dist.sample()
            log_prob_msg = comm_dist.log_prob(msg_ac).detach()
        else:
            msg_ac = None
            log_prob_msg = None

        ''' for the remaining non-learning users '''
        for i in range(self.num_intelligent, self.num_users):
            if self.mode == 'unif_rand':
                np.append(ac_np, np.array(np.random.randint(int(self.ac_dim))))     # append to numpy array??
            elif self.mode == 'rotate':
                if steps == 0:
                    ac_np[i] = (steps + i) % int(self.ac_dim)
                    prev_comm_ac[i] = ac_np[i]
                elif ((steps + i) % self.radar_interval) == 0:        # choose radar every 4 steps
                    ac_np[i] = np.array(int(self.ac_dim) - 1)
                else:
                    if (prev_comm_ac[i] >= (int(self.ac_dim)-3)):
                        ac_np[i] = 0
                    else:
                        ac_np[i] = prev_comm_ac[i] + 1
                    prev_comm_ac[i] = ac_np[i]

        return ac_np, log_prob_ua, prev_comm_ac, next_hidden, msg_ac, log_prob_msg

    def eval_ac(self, ob, ac_ua, hidden=None, msg_ac=None):
        '''
        Parameters
        ----------
        ob : pytorch geometric 'Data' data type. num nodes=batch size * #agents
        ac_ua : numpy (batch size, #agents)

        Returns
        -------
        values : torch.Size([batch size, #agents])
        log_prob : torch.Size([batch size, #agents])
        entropy : torch.Size([])
        '''
        dist, values, next_hidden, comm_dist = self.net(ob, hidden)  # torch.Size([batch_size * #agents, ac_dim]), [batch_size * #agents, 1], [batch_size, hidden_dim], [batch, comm_dim]

        ac_ua2 = torch.tensor(ac_ua,requires_grad=False,dtype=torch.float32, device=self.device).reshape(-1)
        log_prob = dist.log_prob(ac_ua2)     # torch.Size([batch])
        entropy = dist.entropy().mean()         # torch.Size([])

        if self.ic3net:
            msg_ac = msg_ac.reshape(-1)
            log_prob_comm = comm_dist.log_prob(msg_ac).reshape((-1, self.num_users))
        else:
            log_prob_comm = None

        values = values.reshape((-1, self.num_users))
        log_prob = log_prob.reshape((-1, self.num_users))

        return values, log_prob, entropy, log_prob_comm

    def eval_target(self, next_ob, re_n, terminal_n, next_hidden=None):
        '''
        Parameters
        ----------
        next_ob : pytorch geometric 'Data' data type
        re_n : numpy (#users * batch size, 1)
        terminal_n : numpy (batch size, 1)

        Returns
        -------
        target_n : torch.Size([batch size, #users]), requires_grad = False

        '''
        terminal_n2 = torch.tensor(terminal_n,requires_grad=False,dtype=torch.float32, device=self.device).reshape((-1,1))
        terminal_n2 = torch.tile(terminal_n2, [self.num_users, 1])

        re_n2 = torch.tensor(re_n,requires_grad=False,dtype=torch.float32, device=self.device).reshape((-1,1))

        with torch.no_grad():
            _, next_v_n, _, _ = self.net(next_ob, next_hidden)
            target_n = (re_n2 + (1 - terminal_n2) * self.gamma * next_v_n).detach()
            target_n = target_n.reshape((-1, self.num_users))
        return target_n

    def sample_trajectories(self, itr, env):
        ''' Collect paths until we have enough timesteps '''
        self.timesteps_this_batch = 0
        paths = []
        while True:
            animate_this_episode=(len(paths)==0 and (itr % 10 == 0) and self.animate)
            path = self.sample_trajectory(env, animate_this_episode)
            paths.append(path)
            self.timesteps_this_batch += pathlength(path)
            if self.timesteps_this_batch >= self.min_timesteps_per_batch:
                break
        return paths, self.timesteps_this_batch

    def sample_trajectory(self, env, animate_this_episode):
        '''
        Sample a single episode in the Markov environment.

        Parameters
        ----------
        env : gym style environment
        animate_this_episode : bool

        Returns
        -------
        ob: list length 8, elements np.array (ob_dim,)
        ac: np.array (8,)
        rew: np.array (8,)
        log_prob, log_prob_msg: torch.Size([1,#agents])
        hidden, next_hidden: torch.Size([#agents, hidden_size])
        path : dictionary containing information on the current path/trajectory
        '''
        obs_data, acs, log_probs, rewards, next_obs_data, next_acs, entropys, terminals, values, next_values = \
        [], [], [], [], [], [], [], [], [], []
        hiddens, next_hiddens = [], []
        msg_acs, log_prob_msgs = [], []
        prev_comm_ac = np.zeros((self.num_users,1))
        steps = 0

        ob = env.reset()

        in_map = env.in_map.astype(bool)
        positions = env.positions.copy()    # set positions of agents not in map to high value (so not chosen as NN)
        positions[:,0] = env.positions[:,0] * in_map + 1e7 * np.invert(in_map)
        # For each element in y, find the k nearest points in x.
        edge_index = knn(x=torch.tensor(positions, device=self.device), y = torch.tensor(env.positions, device=self.device), k=self.GNN_kNN+1)
        msg_ac = torch.ones((self.num_users,), device=self.device) if self.ic3net else None
        edge_index = masking(edge_index, env.in_map, self.device, msg_ac)
        ob_data = Data(x = torch.tensor(ob, dtype=torch.float), edge_index = edge_index).to(self.device)  # x torch.Size([# agents, 1 step, ob_dim])

        hidden = torch.zeros(self.num_users, self.size[1], device=self.device) if self.recurrent else None     #agents, hidden_size

        ac, log_prob, prev_comm_ac, hidden, msg_ac, log_prob_msg = self.act(ob_data, prev_comm_ac, hidden, steps)

        while True:
            if animate_this_episode:
                env.render()
                time.sleep(0.1)
                if steps == 0:
                    input("Press any key to continue...")

            obs_data.append(ob_data)
            acs.append(ac)
            log_probs.append(log_prob.detach().reshape(-1,self.num_users))
            if self.recurrent:
                hiddens.append(hidden)
            if self.ic3net:
                msg_acs.append(msg_ac)
                log_prob_msgs.append(log_prob_msg.detach().reshape(-1,self.num_users))

            ob, rew, done, _ = env.step(ac)

            in_map = env.in_map.astype(bool)
            positions = env.positions.copy()    # set positions of agents not in map to high value (so not chosen as NN)
            positions[:,0] = env.positions[:,0] * in_map + 1e7 * np.invert(in_map)
            # For each element in y, find the k nearest points in x.
            edge_index = knn(x=torch.tensor(positions, device=self.device), y = torch.tensor(env.positions, device=self.device), k=self.GNN_kNN+1)
            edge_index = masking(edge_index, env.in_map, self.device, msg_ac)

            ob_data = Data(x = torch.tensor(ob, dtype=torch.float), edge_index = edge_index).to(self.device)   # x torch.Size([# agents, 1 step, ob_dim])

            # ac: (#agents,). log_prob: torch.Size([#agents]). prev_comm_ac: list size #agents
            ac, log_prob, prev_comm_ac, hidden, msg_ac, log_prob_msg = self.act(ob_data, prev_comm_ac, hidden, steps)

            if self.recurrent:
                next_hiddens.append(hidden.clone().detach())
                if ((steps+1) % self.detach_gap)==0:
                    hidden = hidden.detach()
            next_obs_data.append(ob_data)
            next_acs.append(ac)
            rewards.append(rew)     # most recent reward appended to END of list

            steps += 1
            if done or steps >= self.max_path_length:
                terminals.append(1)
                break
            else:
                terminals.append(0)

        path = {}
        path = {"observation" : obs_data,                   # list of length steps. (steps, ob_dim)
             "reward" : np.array(rewards, dtype=np.float32),} # (steps)}
        path["log_prob"] = torch.cat(log_probs, axis=0)     # torch.Size([#steps, #agents])
        path["next_observation"] = next_obs_data            # list of length steps
        path["next_action"] = np.array(next_acs, dtype=np.float32)  # np array (#steps, #agents)
        path["action"] = np.array(acs, dtype=np.float32)            # np array (#steps, #agents)
        path["terminal"] = np.array(terminals, dtype=np.float32)    # np array (#steps,)
        if self.recurrent:
            path["hidden"] = torch.stack(hiddens, axis=0)
            path["next_hidden"] = torch.stack(next_hiddens, axis=0)
        if self.ic3net:
            path["msg_ac"] = torch.stack(msg_acs, axis=0)
            path["log_prob_msg"] = torch.cat(log_prob_msgs, axis=0)     # torch.Size([#steps, #agents])

        # Log additional statistics
        path['r_comm1'] = (env.episode_observation['r_comm1'])
        path['r_rad1'] = (env.episode_observation['r_rad1'])
        path['throughput1'] = (env.episode_observation['throughput1'] / 400)
        path['throughput_av'] = (env.episode_observation['throughput_av'] / 400)
        path['SINR1'] = (env.episode_observation['SINR1'] / 400)
        path['SINR_av'] = (env.episode_observation['SINR_av'] / 400)
        path['num_transmits1'] = (env.episode_observation['num_transmits1'] / 400)
        path['num_transmits_av'] = (env.episode_observation['num_transmits_av'] / 400)

        if self.test:
            path['lane_action_map'] = (env.episode_observation['lane_action_map'])
            path['comm_distance'] = (env.episode_observation['comm_distance'])
            path['all_distances'] = (env.episode_observation['all_distances'])
            path['radar_NN_distance'] = (env.episode_observation['radar_NN_distance'])
            path['NN_distance'] = (env.episode_observation['NN_distance'])
            path['radar_NN_position'] = (env.episode_observation['radar_NN_position'])
            path['NN_position'] = (env.episode_observation['NN_position'])

        return path

    def estimate_decen_adv(self, ob_no, next_ob_no, re_n, terminal_n, hidden=None, next_hidden=None):
        '''
        Parameters
        ----------
        ob_no : List of Pytorch Geometric Data() datatype
        next_ob_no : List of Pytorch Geometric Data() datatype
        re_n : np array (batch_size, num_users)
        terminal_n : np array (batch size,)

        Returns
        -------
        adv_n : np array (batch_size, num_users)
        '''
        lastgaelam = torch.zeros((1, self.num_users), device = self.device)
        re_n2 = torch.tensor(re_n, requires_grad=False,dtype=torch.float32, device=self.device)
        adv_n = torch.zeros_like(re_n2).to(self.device)

        for t in reversed(range(self.min_timesteps_per_batch)):
            if t == self.min_timesteps_per_batch-1:
                nextnonterminal = 1 - 1 #terminal_n[t]
            else:
                nextnonterminal = 1 - terminal_n[t+1]

            with torch.no_grad():
                if self.recurrent:
                    _, v_next, _, _ = self.net(next_ob_no[t], hidden[t])
                    _, v, _, _ = self.net(ob_no[t], next_hidden[t])
                else:
                    _, v_next, _, _ = self.net(next_ob_no[t])     # torch.Size([8,1])
                    _, v, _, _ = self.net(ob_no[t])
            q = re_n2[t].unsqueeze(-1) + self.gamma * v_next * nextnonterminal  # torch.Size([])
            delta = q - v
            adv_n[t] = lastgaelam = delta.t() + self.gamma*self.lamb*nextnonterminal*lastgaelam
        if self.normalize_advantages:
            adv_n = (adv_n - torch.mean(adv_n, axis=0)) / (torch.std(adv_n, axis=0) + 1e-7)

        return adv_n

    def update(self,ob_no, ac_na, adv_n, log_prob_na_old, next_ob_no, re_n, terminal_n, hidden=None, next_hidden=None, msg_ac=None, log_prob_msgs_old=None):

        batch_size = self.mb_size # len(ob_no)
        # num_mb = np.ceil(len(ob_no)/batch_size)
        ob_dataset = RLDataset(ob_no, next_ob_no)
        ob_loader = DataLoader(ob_dataset, batch_size = batch_size, shuffle = True)

        if self.ic3net:
            log_prob_na_old = log_prob_na_old + log_prob_msgs_old

        target_n = []
        for epoch in range(self.ppo_epochs):
            mb_idx = 0
            for ob_batch, next_ob_batch, idx in ob_loader:
                ''' Sample minibatch and learn '''
                # check
                # torch.sum(next_ob_no[idx[1]].x - next_ob_batch[1].x) == 0

                ob_batch.to(self.device)
                next_ob_batch.to(self.device)

                if epoch == 0:
                    ''' ob_batch.x: tensor (minibatch size * #agents, ob_size)
                    reshape hiddens: tensor (minibatch size * #agents, hidden_size) '''
                    next_hidden_mb = next_hidden[idx].reshape((-1,self.size[0])) if self.recurrent else None
                    target_n.append(self.eval_target(next_ob_batch, re_n[idx], terminal_n[idx], next_hidden_mb))    # torch.size([batch, #users])

                hidden_mb = hidden.clone()[idx].reshape((-1,self.size[0])) if self.recurrent else None
                msg_ac_mb = msg_ac[idx] if self.ic3net else None
                values, log_prob_na, entropy, log_prob_msg = self.eval_ac(ob_batch, ac_na[idx], hidden_mb, msg_ac_mb)       # [batch, #users], [batch, #users], []

                if self.ic3net:
                    log_prob_na = log_prob_na + log_prob_msg    # multiply both action probabilities, assuming independence

                policy_loss, policy_loss_after = 0, 0
                # Calc policy loss
                ratio = (log_prob_na - log_prob_na_old[idx].detach()).exp()
                obj = torch.mul(ratio, adv_n[idx])
                obj_clipped = torch.mul(torch.clamp(ratio, 1-self.ppo_clip, 1+self.ppo_clip), adv_n[idx])     # torch.size([batch])
                policy_loss = - torch.min(obj, obj_clipped).mean()

                # Calc value loss
                v_loss = (values - target_n[mb_idx]).pow(2).mean()

                loss = policy_loss + self.v_coef * v_loss - self.entrop_loss_coef * entropy

                # Logging
                losses = loss.item()
                pg_losses = policy_loss.item()
                clip_fraction = torch.mean((torch.abs(ratio - 1) > self.ppo_clip).float()).item()
                approx_kl_divs = torch.mean(log_prob_na_old[idx] - log_prob_na).item()
                value_loss = v_loss.item()
                entropy_loss = entropy.item()

                # Back propagate
                self.optimizer.zero_grad()
                loss.backward() #retain_graph = True)
                nn.utils.clip_grad_norm_(self.net.parameters(), self.max_grad_norm)
                self.optimizer.step()

                # Checking
                # if (epoch == 0) & (mb_idx == 0):
                #     logz.log_tabular("CriticLossBefore", v_loss.detach().cpu().numpy())
                #     logz.log_tabular("LossBefore", loss.detach().cpu().numpy())

                # if (epoch == (self.ppo_epochs-1)) & (mb_idx == 0):
                #     values_after, log_prob_after, entropy_after, log_prob_msg_after = self.eval_ac(ob_batch, ac_na[idx], hidden_mb, msg_ac_mb)

                #     if self.ic3net:
                #         log_prob_after = log_prob_after + log_prob_msg_after    # multiply both action probabilities, assuming independence
                #     ratio = (log_prob_after - log_prob_na_old[idx]).exp().detach().requires_grad_(False)
                #     obj = torch.mul(ratio, adv_n[idx])
                #     obj_clipped = torch.mul(torch.clamp(ratio, 1-self.ppo_clip, 1+self.ppo_clip), adv_n[idx])     # torch.size([batch])
                #     policy_loss_after = - torch.min(obj, obj_clipped).mean()

                #     # Calc value loss
                #     v_loss_after = (values_after - target_n[mb_idx]).pow(2).mean()

                #     loss_after = policy_loss_after + self.v_coef * v_loss_after + self.entrop_loss_coef * entropy_after

                #     logz.log_tabular("LossAfter", loss_after.detach().cpu().numpy())
                #     logz.log_tabular("CriticLossAfter", v_loss_after.detach().cpu().numpy())

                mb_idx += 1

        # Log
        logz.log_tabular("Loss ", np.mean(losses))
        logz.log_tabular("Policy Gradient Loss ", np.mean(pg_losses))
        logz.log_tabular("Clip Fraction ", np.mean(clip_fraction))
        logz.log_tabular("KL Divergence ", np.mean(approx_kl_divs))
        logz.log_tabular("Value Loss ", np.mean(value_loss))
        logz.log_tabular("Entropy Loss ", np.mean(entropy_loss))

# In[]

def train_AC(
        exp_name,
        env_name,
        env_config,
        num_intelligent,
        mode,
        radar_interval,
        n_iter,
        gamma,
        lamb,
        min_timesteps_per_batch,
        max_path_length,
        learning_rate,
        reward_to_go,
        gae,
        n_step,
        animate,
        logdir,
        normalize_advantages,
        critic,
        decentralised_critic,
        seed,
        n_layers,
        conv,
        gnn,
        gnn_pooling,
        gnn_knn,
        ic3net,
        size,
        size_critic,
        recurrent,
        detach_gap,
        input_prev_ac,
        ppo_epochs,
        minibatch_size,
        ppo_clip,
        v_coef,
        entrop_loss_coef,
        max_grad_norm,
        policy_net_dir=None,
        value_net_dir=None,
        test = None):

    start = time.time()
    setup_logger(logdir, locals())  # setup logger for results

    env = Beamform_JRC(env_config)

    env.seed(seed)
    torch.manual_seed(seed)
#    np.random.seed(seed)

    # Maximum length for episodes
    max_path_length = max_path_length or env.spec.max_episode_steps

    # Is this env continuous, or self.discrete?
    discrete = isinstance(env.action_space, gym.spaces.Discrete)

    # Observation and action sizes
    ob_dim = env.observation_space.shape[0]                                 # OK
    ac_dim = env.action_space.n if discrete else env.action_space.shape[0]  # OK

    num_users = int(env.N)

    entrop_loss_coef = linear_schedule(entrop_loss_coef)

    computation_graph_args = {
        'num_intelligent': num_intelligent,
        'n_layers': n_layers,
        'ob_dim': ob_dim,
        'ac_dim': ac_dim,
        'num_users': env_config['num_users'],
        'num_users_NN': env_config['num_users_NN'],
        'num_agents': env_config['num_agents'],
        'mode': mode,
        'radar_interval': radar_interval,
        'discrete': discrete,
        'size': size,
        'CNN': conv,
        'GNN': gnn,
        'GNN_pooling': gnn_pooling,
        'GNN_kNN': gnn_knn,
        'ic3net': ic3net,
        'size_critic': size_critic,
        'learning_rate': learning_rate,
        'recurrent': recurrent,
        'detach_gap': detach_gap,
        'input_prev_ac': input_prev_ac,
        'ppo_epochs': ppo_epochs,
        'minibatch_size': minibatch_size,
        'ppo_clip': ppo_clip,
        'v_coef': v_coef,
        'entrop_loss_coef': entrop_loss_coef,
        'max_grad_norm': max_grad_norm,
        'test': test,
        }

    sample_trajectory_args = {
        'animate': animate,
        'max_path_length': max_path_length,
        'min_timesteps_per_batch': min_timesteps_per_batch,
    }

    estimate_return_args = {
        'gamma': gamma,
        'lambda': lamb,
        'reward_to_go': reward_to_go,
        'gae': gae,
        'n_step': n_step,
        'critic': critic,
        'decentralised_critic': decentralised_critic,
        'normalize_advantages': normalize_advantages,
    }

    agent = Agent(computation_graph_args, sample_trajectory_args, estimate_return_args)

    # policy_net_dir = os.path.join('data',
    #                               'PPO_8NN_Attn_IC3_bs1200_n7000_beamform_JRC_multi_vi_03-02-2022_23-44-13_8users_wcomm1.0_w_rad6.0_maxage_8.0_ang45.0_obtimeTrue',
    #                               '1',
    #                               'NN_itr5925')

    test = env_config['test']
    if policy_net_dir != None:
        load_variables(agent.net, policy_net_dir)
        test = True

    #========================================================================================#
    # Training Loop
    #========================================================================================#

    total_timesteps = 0
    best_av_reward = None
    policy_model_file = {}
    for itr in range(n_iter):
        print("********** Iteration %i ************"%itr)
        paths, timesteps_this_batch = agent.sample_trajectories(itr, env)
        total_timesteps += timesteps_this_batch

        agent.update_current_progress_remaining(itr, 5/7*n_iter)
        agent.update_entropy_loss()

        # Build arrays for observation, action for the policy gradient update by concatenating
        # across paths

        ob_no, next_ob_no = [], []
        for path in paths:
            ob_no = ob_no + path['observation']                     # list len batch_size
            next_ob_no = next_ob_no + path['next_observation']
        if recurrent:
            hidden = torch.cat([path["hidden"] for path in paths])              # shape (batch_size, #users, hidden_size)
            next_hidden = torch.cat([path["next_hidden"] for path in paths])    # shape (batch_size, #users, hidden_size)
        else:
            hidden, next_hidden = None, None
        if ic3net:
            msg_ac = torch.cat([path["msg_ac"] for path in paths])
            log_prob_msg = torch.cat([path["log_prob_msg"] for path in paths])  # shape (batch_size, #users)
        else:
            msg_ac, log_prob_msg = None, None
        ac_na = np.concatenate([path["action"] for path in paths]) #, axis = -2)   # shape (batch_size, #users)
        re_n = np.concatenate([path["reward"] for path in paths])               # (batch_size, num_users)
        log_prob_na = torch.cat([path["log_prob"] for path in paths])          # torch.size([batcb_size, #agents])
        next_ac_na = np.concatenate([path["next_action"] for path in paths]) # shape (batch self.size /n/)
        returns = np.array([path["reward"].sum(axis=0, dtype=np.float32) for path in paths]).transpose()   # (num_users, num episodes in batch)
        terminal_n = np.concatenate([path["terminal"] for path in paths])       # (batch_size,)

        r_comm1 = ([path["r_comm1"] for path in paths])
        r_rad1 = ([path["r_rad1"] for path in paths])
        throughput1 = ([path["throughput1"] for path in paths])                 # (batch,)
        throughput_av = ([path["throughput_av"] for path in paths])
        SINR1 = ([path["SINR1"] for path in paths])                      # (batch,)
        SINR_av = ([path["SINR_av"] for path in paths])
        num_transmits1 = ([path["num_transmits1"] for path in paths])
        num_transmits_av = ([path["num_transmits_av"] for path in paths])

        if test:
            lane_action_map = np.sum([path["lane_action_map"] for path in paths], axis=0)
            comm_distance = np.concatenate([path["comm_distance"] for path in paths], axis=0)
            all_distances = np.concatenate([path["all_distances"] for path in paths], axis=0)
            radar_NN_distance = np.concatenate([path["radar_NN_distance"] for path in paths], axis=0)
            NN_distance = np.concatenate([path["NN_distance"] for path in paths], axis=0)
            radar_NN_position = np.concatenate([path["radar_NN_position"] for path in paths], axis=0)
            NN_position = np.concatenate([path["NN_position"] for path in paths], axis=0)

        av_reward = np.mean(returns)

        # Log diagnostics
        logz.log_tabular("Time", time.time() - start)
        logz.log_tabular("Iteration", itr)
        logz.log_tabular("Average Reward", av_reward)   # per agent per episode
        logz.log_tabular("StdReward", np.std(returns))
        logz.log_tabular("MaxReward", np.max(returns))
        logz.log_tabular("MinReward", np.min(returns))
        logz.log_tabular("r_comm1", np.mean(r_comm1))
        logz.log_tabular("r_rad1", np.mean(r_rad1))
        logz.log_tabular("Throughput1", np.mean(throughput1))
        logz.log_tabular("Average Throughput", np.mean(throughput_av))
        logz.log_tabular("SINR1", np.mean(SINR1))
        logz.log_tabular("SINR_av", np.mean(SINR_av))
        logz.log_tabular("Num Transmits 1", np.mean(num_transmits1))
        logz.log_tabular("Average Num Transmits", np.mean(num_transmits_av))

        for i in range(num_users):
            logz.log_tabular("Reward"+str(i+1), np.mean(returns, axis=1)[i])
            logz.log_tabular("StdReward"+str(i+1), np.std(returns, axis=1)[i])
            logz.log_tabular("MaxReward"+str(i+1), np.max(returns, axis=1)[i])
            logz.log_tabular("MinReward"+str(i+1), np.min(returns, axis=1)[i])
        logz.log_tabular("TimestepsThisBatch", timesteps_this_batch)
        logz.log_tabular("TimestepsSoFar", total_timesteps)

        if test != True:
            adv_n = agent.estimate_decen_adv(ob_no, next_ob_no, re_n, terminal_n, hidden, next_hidden)
            agent.update(ob_no, ac_na, adv_n, log_prob_na, next_ob_no, re_n, terminal_n, hidden, next_hidden, msg_ac, log_prob_msg)

            if best_av_reward == None:
                best_av_reward = av_reward
            elif av_reward > best_av_reward:
                best_av_reward = av_reward
                policy_model_file = os.path.join(logdir,"NN"+'_itr'+str(itr))
                save_itr_info(f"{policy_model_file}-{itr}.txt", itr, av_reward)
                save_variables(agent.net, policy_model_file)

            logz.dump_tabular(step=itr)
        else:
            logz.log_tabular("Lane-Action Map", lane_action_map)
            logz.log_tabular("Comm Distance", comm_distance)
            logz.log_tabular("All Distances", all_distances)
            logz.log_tabular('Radar NN Distance', radar_NN_distance)
            logz.log_tabular('NN Distance', NN_distance)
            logz.log_tabular('Radar NN Position', radar_NN_position)
            logz.log_tabular('NN Position', NN_position)
            logz.save_data()

    # At the end of the training loop,
    if test != True:
        policy_model_file = os.path.join(logdir,"NN")
        save_itr_info(f"{policy_model_file}-{itr}.txt", itr, av_reward)
        save_variables(agent.net, policy_model_file)

# In[]

if __name__ == "__main__":

    import argparse
    parser = argparse.ArgumentParser()
    # Env config
    parser.add_argument('env_name', type=str)
    parser.add_argument('--num_lanes', type=int, default=2)
    parser.add_argument('--num_users_NN', type=int)
    parser.add_argument('--num_users', type=int, nargs='+', default = [0])
    parser.add_argument('--num_intelligent', type=int, default = 0)
    parser.add_argument('--num_agents', type=int)
    parser.add_argument('--knn', type=int, default = -1)
    parser.add_argument('--obj', choices=['peak','avg'], default='peak')
    parser.add_argument('--x_dim', type=int, nargs='+', default=[150, 150, 10])
    parser.add_argument('--age_max', type=float, default=3)
    parser.add_argument('--w_comm', type=float, default=1)
    parser.add_argument('--w_rad', type=float, default=1)
    parser.add_argument('--ang_range', type=float, default=45)
    parser.add_argument('--ob_time', action='store_true')
    parser.add_argument('--radar_interval', '-r_int', type=int, default=4)
    parser.add_argument('--interference', action='store_true')
    parser.add_argument('--OFDM_comm', action='store_true')

    parser.add_argument('--exp_name', type=str, default='vpg')
    parser.add_argument('--render', action='store_true')

    # Algorithm hyperparameters
    parser.add_argument('--mode', choices=['unif_rand','urg5','best','csma-ca','rotate'], default='rotate') # policy for unintelligent agents
    parser.add_argument('--discount', type=float, default=0.99)
    parser.add_argument('--lamb', type=float, default=0.95)
    parser.add_argument('--n_iter', '-n', type=int, default=100)
    parser.add_argument('--batch_size', '-b', type=int, default=1000)
    parser.add_argument('--ep_len', '-ep', type=float, default=-1.)
    parser.add_argument('--learning_rate', '-lr', type=float, default=3e-4)
    parser.add_argument('--reward_to_go', '-rtg', action='store_true')
    parser.add_argument('--generalised_adv_est', '-gae', action='store_true')
    parser.add_argument('--n_step', '-ns', type=int, default=0)
    parser.add_argument('--dont_normalize_advantages', '-dna', action='store_true')
    parser.add_argument('--critic', type=str, default ='v')
    parser.add_argument('--decentralised_critic', '-dc', action='store_true')
    parser.add_argument('--seed', type=int, default=1)
    parser.add_argument('--n_experiments', '-e', type=int, default=1)

    # PPO parameters
    parser.add_argument('--ppo_epochs', type=int, default=10)
    parser.add_argument('--minibatch_size', '-mbs', type=int, default=64)
    parser.add_argument('--ppo_clip', type=float, default=0.2) #0.05
    parser.add_argument('--v_coef', type=float, default=0.5)
    parser.add_argument('--entrop_loss_coef', type=float, default=0) #0.001) #0.0005
    parser.add_argument('--max_grad_norm', type=float, default=0.5)

    # NN architecture parameters
    parser.add_argument('--n_layers', '-l', type=int, default=2)
    parser.add_argument('--conv', action='store_true')
    parser.add_argument('--size', '-s', type=int, nargs='+', default=[64, 64])
    parser.add_argument('--size_critic', '-sc', type=int, nargs='+', default=[64, 64])
    parser.add_argument('--input_prev_ac','-ipa', action='store_true')
    parser.add_argument('--ac_dist', default='Gaussian')
    parser.add_argument('--recurrent', '-r', action='store_true')
    parser.add_argument('--detach', type=int, default=8)
    parser.add_argument('--ic3net', action='store_true')

    # GNN parameters
    parser.add_argument('--gnn', type=int, default = 1)
    parser.add_argument('--gnn_pooling', choices=['gcn','transformer'], default='gcn')

    parser.add_argument('--policy_net_filename', '-p_file', type=str)
    parser.add_argument('--value_net_filename', '-v_file', type=str)
    parser.add_argument('--test', '-t', action='store_true')
    parser.add_argument('--pre_uuid', '-uuid', type=str)
    args = parser.parse_args()

    if not(os.path.exists('data')):
        os.makedirs('data')

    if args.pre_uuid != None:
        logdir = args.exp_name + '_' + args.env_name + '_' + args.pre_uuid
        logdir = os.path.join('data/' + args.pre_uuid , logdir)
    else:
        logdir = args.exp_name + '_' + args.env_name + '_' + time.strftime("%d-%m-%Y_%H-%M-%S")
        logdir = os.path.join('data', logdir)

    print("------")
    print(logdir)
    print("------")

    max_path_length = args.ep_len if args.ep_len > 0 else None
    args.policy_net_dir = None
    args.value_net_dir = None

    if args.policy_net_filename != None:
        args.policy_net_dir = os.path.join(os.getcwd(),args.policy_net_filename)
    # if args.value_net_filename != None:
    #     args.value_net_dir = os.path.join(os.getcwd(),args.value_net_filename)

    if args.num_intelligent == 0:
        args.num_intelligent = args.num_users
    if args.num_users == [0]:
        args.num_users[0] = args.num_users_NN
    if args.knn == -1:
        args.knn = args.num_users

    for num_users in args.num_users:
        for x_dim in range(args.x_dim[0], args.x_dim[1], args.x_dim[2]):
            for e in range(args.n_experiments):

                seed = args.seed + 10*e
                print('Running experiment with seed %d'%seed)

                test = False
                if args.policy_net_filename != None:
                    test = True

                env_config = {'num_users': num_users,
                              'num_agents': args.num_agents,
                              'num_users_NN': args.num_users_NN,
                              'age_max': args.age_max,
                              'x_dim': x_dim,
                              'num_lanes': args.num_lanes,
                              'w_comm': args.w_comm,
                              'w_rad': args.w_rad,
                              'radar_angular_range': args.ang_range,
                              'ob_time': args.ob_time,
                              'interference': args.interference,
                              'test': args.test,
                              'OFDM_comm': args.OFDM_comm,
                              }

                logdir_w_params = logdir + '_{}users_wcomm{}_w_rad{}_maxage_{}_ang{}_obtime{}'.format(num_users,args.w_comm, args.w_rad,args.age_max,args.ang_range,args.ob_time)

                train_AC(
                        exp_name=args.exp_name,
                        env_name=args.env_name,
                        env_config=env_config,
                        num_intelligent=args.num_intelligent,
                        mode = args.mode,
                        radar_interval = args.radar_interval,
                        n_iter=args.n_iter,
                        gamma=args.discount,
                        lamb=args.lamb,
                        min_timesteps_per_batch=args.batch_size,
                        max_path_length=max_path_length,
                        learning_rate=args.learning_rate,
                        reward_to_go=args.reward_to_go,
                        gae = args.generalised_adv_est,
                        n_step = args.n_step,
                        animate=args.render,
                        logdir=os.path.join(logdir_w_params,'%d'%seed),
                        normalize_advantages=not(args.dont_normalize_advantages),
                        critic=args.critic,
                        decentralised_critic= args.decentralised_critic,
                        seed=seed,
                        n_layers=args.n_layers,
                        size=args.size,
                        conv=args.conv,
                        gnn=args.gnn,
                        gnn_pooling=args.gnn_pooling,
                        gnn_knn = args.knn,
                        ic3net = args.ic3net,
                        size_critic=args.size_critic,
                        recurrent = args.recurrent,
                        detach_gap = args.detach,
                        input_prev_ac = args.input_prev_ac,
                        ppo_epochs = args.ppo_epochs,
                        minibatch_size = args.minibatch_size,
                        ppo_clip = args.ppo_clip,
                        v_coef = args.v_coef,
                        entrop_loss_coef = args.entrop_loss_coef,
                        max_grad_norm = args.max_grad_norm,
                        policy_net_dir = args.policy_net_filename,
                        value_net_dir = args.value_net_filename,
                        test = args.test
                        )


from Crypto.Cipher import DES
from Crypto.Random import get_random_bytes
import binascii

def des_encrypt(key, plaintext):
    cipher = DES.new(key, DES.MODE_ECB)
    return cipher.encrypt(plaintext)

def hamming_distance(str1, str2):
    return sum(bin(b1 ^ b2).count('1') for b1, b2 in zip(str1, str2))

def difference_ratio(hamming_distance, length):
    return (hamming_distance / length) * 100

# 給予設定好的明文、金鑰(只是為了展示崩塌效應，因此隨便給予)。
binary_key1 = '0001001000110101010101100111100010011010101111011101111011110000'
binary_key2 = '0001001000110101010101100110100110011010101111011101111011110000'
# 使binary_key2與binary_key1只有1bit的不同(在第28個bit)，其他不同則是奇偶校驗的結果，因為最後會被捨棄因此無所謂

key1 = int(binary_key1, 2).to_bytes(8, byteorder='big')
key2 = int(binary_key2, 2).to_bytes(8, byteorder='big')  # 將key轉成16進位儲存

binary_plaintext1 = '0110000101100010011000110110010001100101011001100110011101101000'
binary_plaintext2 = '0110000101100010011000110110011001100101011001100110011101101000' # 使binary_plaintext2與binary_plaintext1只有1bit的不同(在第31個bit)
plaintext1 = int(binary_plaintext1, 2).to_bytes(8, byteorder='big')
plaintext2 = int(binary_plaintext2, 2).to_bytes(8, byteorder='big')  # 將plaintext轉成16進位儲存

# 利用內建函式加密第一個案例:不同key(相差1bit)，相同明文
ciphertext1_case1 = des_encrypt(key1, plaintext1)
ciphertext2_case1 = des_encrypt(key2, plaintext1)

# 利用內建函式加密第二個案例:不同明文(相差1bit)，相同key
ciphertext1_case2 = des_encrypt(key1, plaintext1)
ciphertext2_case2 = des_encrypt(key1, plaintext2)

# 利用內建函式計算兩個案例的漢明距離跟差異比例
distance_case1 = hamming_distance(ciphertext1_case1, ciphertext2_case1)
ratio_case1 = difference_ratio(distance_case1, len(ciphertext1_case1) * 8)

distance_case2 = hamming_distance(ciphertext1_case2, ciphertext2_case2)
ratio_case2 = difference_ratio(distance_case2, len(ciphertext1_case2) * 8)

# 顯示結果。
#可以清晰地看到1bit不同卻會導致大約50%的差異，清晰地感受到崩塌效應。
print("案例1:")
print(f"Ciphertext 1: {binascii.hexlify(ciphertext1_case1)}")
print(f"Ciphertext 2: {binascii.hexlify(ciphertext2_case1)}")
print(f"漢明距離: {distance_case1}")
print(f"差異比例: {ratio_case1:.2f}%")
print("\n案例2:")
print(f"Ciphertext 1: {binascii.hexlify(ciphertext1_case2)}")
print(f"Ciphertext 2: {binascii.hexlify(ciphertext2_case2)}")
print(f"漢明距離: {distance_case2}")
print(f"差異比例: {ratio_case2:.2f}%")


import json
from json.decoder import JSONDecodeError

from django.http import (
    HttpResponse,
    HttpResponseBadRequest,
    HttpResponseNotAllowed,
    JsonResponse,
)
from django.views.decorators.csrf import csrf_exempt

from .models import Hero

# Create your views here.

@csrf_exempt
def hero_list(request):
    if request.method == "GET":
        hero_all_list = [hero.to_dict() for hero in Hero.objects.all().iterator()]
        return JsonResponse(hero_all_list, safe=False)
    elif request.method == "POST":
        try:
            body = request.body.decode()
            body = json.loads(body)
            hero_name = body["name"]
            hero_age = int(body["age"])
        except (KeyError, ValueError, JSONDecodeError):
            return HttpResponseBadRequest()
        hero = Hero(name=hero_name, age=hero_age)
        hero.save()
        response_dict = hero.to_dict()
        return JsonResponse(response_dict, status=201)
    else:
        return HttpResponseNotAllowed(["GET", "POST"])

@csrf_exempt
def hero_info(request, id_: int):
    if request.method != "GET" and request.method != "PUT":
        return HttpResponseNotAllowed(["GET", "PUT"])

    try:
        hero = Hero.objects.get(id=id_)
    except Hero.DoesNotExist:
        return HttpResponseBadRequest()

    if request.method == "GET":
        return JsonResponse(hero.to_dict())
    elif request.method == "PUT":
        try:
            body = request.body.decode()
            body = json.loads(body)
            hero_name = body["name"]
            hero_age = int(body["age"])
        except (KeyError, ValueError, JSONDecodeError):
            return HttpResponseBadRequest()
        hero.name = hero_name
        hero.age = hero_age
        hero.save()
        return JsonResponse(hero.to_dict())

def hero_id(request, id_: int):
    return HttpResponse(f"Your id is {id_}!")

def hero_name(request, name: str):
    return HttpResponse(f"Your name is {name}!")


#!/usr/bin/env python3
# A simple ROS subscriber node in Python

#source ~/marsworks_official/catkin_ws/devel/setup.bash
#roslaunch realsense2_camera rs_camera.launch
import rospy
from sensor_msgs.msg import Image
from cv_bridge import CvBridge
import cv2
import os
import numpy as np
import time
import imutils

class Nodo(object):

    def __init__(self):
        # Params
        self.image = None
        self.br = CvBridge()
        # Node cycle rate (in Hz).
        self.loop_rate = rospy.Rate(1)
        self.t = 0
        self.imgs = []
        time.sleep(2)
        # Subscribers
        rospy.Subscriber("/camera/color/image_raw",Image,self.callback)

    def callback(self, msg):
        rospy.loginfo('Image received...')
        self.image = cv2.cvtColor(self.br.imgmsg_to_cv2(msg), cv2.COLOR_RGB2BGR)
        time.sleep(1)
        self.imgs.append(self.image)
        self.t += 1
        print(self.t)
        if self.t == 25:
            print("stich")
            img = self.stitch(self.imgs)

            img = cv2.copyMakeBorder(img, 10, 10, 10, 10,cv2.BORDER_CONSTANT, (0, 0, 0))
            gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
            thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)[1]
            cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
            cnts = imutils.grab_contours(cnts)
            c = max(cnts, key=cv2.contourArea)
            mask = np.zeros(thresh.shape, dtype="uint8")
            (x, y, w, h) = cv2.boundingRect(c)
            img = img[y:y + h, x:x + w]
            cv2.imwrite('output.png',img)

            # cv2.rectangle(mask, (x, y), (x + w, y + h), 255, -1)
            # minRect = mask.copy()
            # sub = mask.copy()
            # while cv2.countNonZero(sub) > 0:
            #     minRect = cv2.erode(minRect, None)
            #     sub = cv2.subtract(minRect, thresh)
            os._exit(0)
        # self.show_image(self.image)

    def stitch(self,images):
        stitchy=cv2.Stitcher.create()
        (dummy,output)=stitchy.stitch(images)
        if dummy != cv2.STITCHER_OK:
        # checking if the stitching procedure is successful
        # .stitch() function returns a true value if stitching is
        # done successfully
            print("stitching ain't successful")
            os._exit(0)
        else:
            print('Your Panorama is ready!!!')
            return output

    def show_image(self, img):
        cv2.imshow("Image Window", img)
        cv2.waitKey(3)

    def start(self):
        rospy.spin()
        while not rospy.is_shutdown():
            self.loop_rate.sleep()

if __name__ == '__main__':
    rospy.init_node("panaroma")
    my_node = Nodo()
    my_node.start()


import torch
import os,glob
import random,csv
import pandas as pd
import numpy as np

from torch.utils.data import Dataset,DataLoader
from torchvision import transforms
from PIL import Image

class CNN_Geneset(Dataset):
    def __init__(self,root,resize,mode):
        super(Dataset,self).__init__()
        self.root = root
        self.resize = resize

        self.name2label = {} # ”sq...“ : 0
        for name in sorted(os.listdir(os.path.join(root))):
            if not os.path.isdir(os.path.join(root,name)):
                continue
            self.name2label[name] = len(self.name2label.keys())

        #print(self.name2label)
        # image, label
        self.images,self.labels = self.load_csv('iamges.csv')

        if mode == 'train':
            self.images = self.images[:int(0.8 * len(self.images))]
            self.labels = self.labels[:int(0.8 * len(self.labels))]
        elif mode == 'all':
            self.images = self.images[:]
            self.labels = self.labels[:]
        else:
            self.images = self.images[int(0.8*len(self.images)):]
            self.labels = self.labels[int(0.8 * len(self.labels)):]

    def load_csv(self, filename):

        if not os.path.exists(os.path.join(self.root,filename)):
            images = []
            for name in self.name2label.keys():
                # 'data\\0\\0.jpg'
                images += glob.glob(os.path.join(self.root,name,'*jpg'))
            # 301, 'data\\1\\36.jpg'
            print(len(images),images)

            random.shuffle(images)
            with open(os.path.join(self.root, filename), mode='w',newline='') as f:
                writer = csv.writer(f)
                for img in images: # 'data\\0\\0.jpg'
                    name = img.split(os.sep)[-2]
                    label = self.name2label[name]
                    # 'data\\0\\0.jpg', 0
                    writer.writerow([img, label])
                print('writen into csv file:', filename)

        # read from csv file
        images,labels = [],[]
        with open(os.path.join(self.root,filename)) as f:
            reader = csv.reader(f)
            value = pd.read_csv('value.csv')
            value = np.array(value)
            #print(value)
            for row in reader:
                # 'data\\0\\0.jpg', 0
                img,label = row
                if value[int(img.split(os.sep)[-1][:-4])] != -1:
                    label = int(value[int(img.split(os.sep)[-1][:-4])])
                    images.append(img)
                    labels.append(label)
                else:
                    images.append(img)
                    labels.append(-1)

        assert  len(images) == len(labels)

        return  images,labels

    def __len__(self):
        return len(self.images)

    def __getitem__(self, idx):
        # idx~[0~len(images)]
        # self.images, self.labels
        # img: 'data\\0\\0.jpg'
        # label； 0
        img, label = self.images[idx],self.labels[idx]
        tf = transforms.Compose([
            lambda x:Image.open(x),
            #lambda x: Image.open(x).convert('RGB'), # string path =>  image data
            transforms.Resize((self.resize,self.resize)),
            transforms.ToTensor()
        ])
        img = tf(img)
        label = torch.tensor(label)

        return img,label

def main():

    import visdom
    import time

    #viz = visdom.Visdom()
    db = CNN_Geneset('data',100,'all')

    encoder_loader = DataLoader(db, batch_size=301)

    for x, label in encoder_loader:
        print(label.shape)
    value = pd.read_csv('value.csv')
    value = np.array(value)
    num = 0
    for i in value:
        if i != -1:
            num += 1
    print(num)

if __name__ == '__main__':
    main()


import torch
import torch.utils.data as data
import torchvision.transforms as T
from PIL import Image
import pickle
import numpy as np
import cv2
import random

def random_ff_mask(configDict=None, size = (256, 256)):
    """
    Generate a random free form mask according to "Free-Form Image Inpainting with Gated Convolution".
    With default setting, around 1/5 is covered by mask, on average.

    Args:
        a dict of mask config with following attrs:
        -- shape: a sequence of mask shape
        -- vertex: num of vertex range
        -- length: trace length range
        -- bWid: brush width range
        -- angle: angle change in rad range
        -- cnt: num of traces range
        size -- tuple of expected mask size

    Returns:
        np.ndarray random free form mask

    Modified from https://github.com/avalonstrel/GatedConvolution_pytorch
    """

    if configDict == None:
        configDict = {'shape':(256, 256), \
                        'vertex':20, \
                        'length':50, \
                        'bWid':20, \
                        'angle':4.0, \
                        'cnt':5 }

    h,w = configDict['shape']
    mask = np.zeros((h,w))
    num_c = random.randint(1, configDict['cnt'])

    for _ in range(num_c):
        start_x = random.randint(1, w)
        start_y = random.randint(1, h)
        num_v = random.randint(10, configDict['vertex'])
        for i in range(num_v):
            angle = 0.01+np.random.randint(configDict['angle'])
            if i % 2 == 0:
                angle = 2 * 3.1415926 - angle
            length = random.randint(10, configDict['length'])
            brush_w = random.randint(10, configDict['bWid'])
            end_x = (start_x + length * np.sin(angle)).astype(np.int32)
            end_y = (start_y + length * np.cos(angle)).astype(np.int32)

            cv2.line(mask, (start_y, start_x), (end_y, end_x), 1.0, brush_w)
            start_x, start_y = end_x, end_y

    mask = cv2.resize(mask, size, cv2.INTER_NEAREST)
    return mask.reshape(mask.shape+(1,)).astype('uint8') * 255

class celebA(data.Dataset):
    def __init__(self, pathFile, img_transform, mask_transform, maskDumpFile=None, train='train', mask_config=None, size=(256, 256)):
        super().__init__()
        self.img_transform = img_transform
        self.mask_transform = mask_transform
        self.train = train
        self.mask_config = mask_config
        self.size = size

        filePathDict = pickle.load(open(pathFile, "rb"))
        if self.train == 'train':
            self.paths = filePathDict['train']
            self.maskList = None
        elif self.train == 'test':
            self.paths = filePathDict['test']
            self.maskList = None
        elif self.train == 'val' and maskDumpFile != None:
            self.paths = filePathDict['train']
            self.maskList = pickle.load(open(maskDumpFile, "rb"))
        else:
            raise("DsetTagError")

    def __getitem__(self, index):
        gt = Image.open(self.paths[index])
        gt = T.Compose(self.img_transform)(gt.convert('RGB'))
        if self.train == 'train' or self.train == 'test' or self.maskList == None:
            mask = random_ff_mask(self.mask_config, self.size)
        else:
            mask = cv2.resize(self.maskList[index % 16], self.size, cv2.INTER_NEAREST)
        mask = T.Compose(self.mask_transform)(255-mask)
        return gt * mask, mask, gt

    def __len__(self):
        return len(self.paths)


# -*- coding: utf-8 -*-

import numpy as np
import math
import pandas as pd
from scipy.stats import norm

class CpCpk:
    def __init__(self):
        super(CpCpk, self).__init__()

    # noinspection PyMethodMayBeStatic
    def Cp(self, mylist, usl, lsl):
        arr = np.array(list(np.float_(mylist)))
        arr = arr.ravel()
        sigma = np.std(arr)
        Cp = float(usl - lsl) / (6 * sigma)
        return Cp

    # noinspection PyMethodMayBeStatic
    def Cpk(self, mylist, usl, lsl):
        arr = np.array(list(np.float_(mylist)))
        arr = arr.ravel()
        sigma = np.std(arr)
        m = np.mean(arr)

        Cpu = float(usl - m) / (3 * sigma)
        Cpl = float(m - lsl) / (3 * sigma)
        Cpk = np.min([Cpu, Cpl])

        return Cpk

    # noinspection PyMethodMayBeStatic
    def cpk(self, df: pd.DataFrame, usl, lsl):
        """
        :param df: data dataframe
             :param usl: upper limit of data index
             :param lsl: lower limit of data index
        :return:
        """
        sigma = 3
        # If the lower limit is 0, use the upper limit reverse negative value instead
        if int(lsl) == 0:
            lsl = 0 - usl

            # Data average
        u = df.mean()[0]

        # Data standard deviation
        stdev = np.std(df.values, ddof=1)

        # Generate average distribution of horizontal axis data
        x1 = np.linspace(u - sigma * stdev, u + sigma * stdev, 1000)

        # Calculate the normal distribution curve
        y1 = np.exp(-(x1 - u) ** 2 / (2 * stdev ** 2)) / (math.sqrt(2 * math.pi) * stdev)

        cpu = (usl - u) / (sigma * stdev)
        cpl = (u - lsl) / (sigma * stdev)

        # Get cpk
        cpk = min(cpu, cpl)

        return cpk

    # noinspection PyMethodMayBeStatic
    def get_yield(self, cpk):
        return norm.cdf(cpk * 3)

if __name__ == '__main__':
    cpcpk = CpCpk()
    a1 = np.arange(0, 10)
    print(a1)
    print(cpcpk.Cp(a1, 10, 0))
    print(cpcpk.Cpk(a1, 10, 0))


from argparse import ArgumentParser
from itertools import starmap
import json

import pandas as pd

if __name__ == "__main__":
    parser = ArgumentParser(description="Change case file format to epiestim style")
    parser.add_argument("in_path", type=str, help="path to input csv")
    parser.add_argument("out_path", type=str, help="where to save outputs")
    parser.add_argument(
        "--region_codes",
        type=str,
        default=None,
        help=(
            "Where to save JSON file with region codes, if at all."
            " This will be dict(area_name -> code)."
        ),
    )
    args = parser.parse_args()

    case_counts = (
        pd.read_csv(args.in_path, index_col=0)
        .set_index("Area name")
        .unstack(0)
        .reset_index(level=1)
        .rename(columns={"Area name": "region", 0: "confirm"})[["confirm", "region"]]
        .rename_axis(index="date")
    )

    case_counts.to_csv(args.out_path)

    if args.region_codes is not None:
        area_codes = dict(
            starmap(lambda k, v: (v, k), enumerate(sorted(case_counts.region.unique())))
        )
        with open(args.region_codes, "w") as f:
            json.dump(area_codes, f)


from sqlalchemy.orm import Session
from . import models, schemas
from ..exceptions import FormException
from sqlalchemy import or_

def get_phone_book(db: Session, search: str | None = None):
    if not search:
        return db.query(models.Phone_book).all()
    return db.query(models.Phone_book).filter(
        or_(
            (models.Phone_book.first_name + " " + models.Phone_book.last_name).ilike(f'%{search}%'),
            models.Phone_book.tel.ilike(f'%{search.replace(" ", "").replace("-","")}%'),
            models.Phone_book.email.ilike(f'%{search}%'),
        )
    ).all()

def get_phone_book_by_phone(db: Session, tel: str):
    return db.query(models.Phone_book).filter(models.Phone_book.tel == tel).first()

def get_phone_book_by_id(db: Session, entry_id: int):
    return db.query(models.Phone_book).filter(models.Phone_book.id == entry_id).first()

def create_phone_book_entry(db: Session, entry: schemas.PhonebookInput):
    db_entry = models.Phone_book(
        first_name=entry.first_name,
        last_name=entry.last_name,
        tel=entry.tel,
        email=entry.email,
        author_id=entry.author_id
    )
    db.add(db_entry)
    db.commit()
    db.refresh(db_entry)

def update_phone_book_entry(db: Session, entry: schemas.PhonebookUpdate, entry_id: int):
    db_entry = get_phone_book_by_id(db, entry_id)
    if not db_entry:
        raise FormException(status_code=400, detail="Entry not in Phone Book")
    entry_data = entry.dict(exclude_none=True, exclude_unset=True)
    for key, value in entry_data.items():
        setattr(db_entry, key, value)

    db.add(db_entry)
    db.commit()
    db.refresh(db_entry)

def delete_phone_book_entry(db: Session, entry_id: int):
    db_entry = get_phone_book_by_id(db, entry_id)
    if not db_entry:
        raise FormException(status_code=400, detail="Entry not in Phone Book")
    db.delete(db_entry)
    db.commit()


from django.conf.urls import patterns, url
from surfice import views, ajax

# Delete when deploying!
from django.contrib.staticfiles.urls import staticfiles_urlpatterns

urlpatterns = patterns('',
	# Home
	url(r'^$', views.index, name='index'),

	#url(r'^ajax', ajax.ajax, name='ajax'), # No params
	#url(r'^about/$', views.about, name='about'),
	# surf_url is passed to the surf method in views.py
	# regex to look for any sequence of alphanumeric characters
	# and underscores before the trailing slash
	#url(r'^surf/(?P<surf_url>\w+)/$', views.surf, name='surf'),

	# AJAX
	url(r'^.*?ajax/(?P<action>[^/]*)', ajax.dispatch, name='ajax'),

	# Admin
	url(r'^admin/$', views.admin, name='admin'),
	url(r'^admin/surfs/$', views.surfs, name='surfs'),
	url(r'^admin/surfices/$', views.surfices, name='surfices'),
	url(r'^admin/settings/$', views.settings, name='settings'),
	url(r'^admin/statuses/$', views.statuses, name='statuses'),

	url(r'^admin/events/(?P<page>\w*)(?:\/)(?P<order_by>[-\w]*)', views.events, name='events'),
	url(r'^admin/events/(?P<page>\w*)', views.events, name='events'),
	url(r'^admin/events/$', views.events, name='events'),

	url(r'^admin/dings/(?P<page>\w*)(?:\/)(?P<order_by>[-\w]*)', views.dings, name='dings'),
	url(r'^admin/dings/(?P<page>\w*)', views.dings, name='dings'),
	url(r'^admin/dings/$', views.dings, name='dings'),

	# Need to comment out because I'm not going to use any except ding
	#url(r'^admin/surfice/(?P<surfice>.*)/$', views.surfice, name='surfice'),
	#url(r'^admin/status/(?P<status>.*)/$', views.status, name='status'),
	url(r'^admin/ding/(?P<ding>.*)', views.ding, name='ding'),
)


from pprint import pprint
from datetime import timedelta

from django.conf import settings
from django.utils import timezone

from django.core.paginator import Paginator
from django.core.paginator import InvalidPage

from rest_framework import pagination
from rest_framework.response import Response
from rest_framework.authentication import SessionAuthentication, BasicAuthentication

from rest_framework_simplejwt.authentication import JWTTokenUserAuthentication

from sendgrid.sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail

from sentry_sdk import capture_exception

class CurrentPagePagination(pagination.PageNumberPagination):
    page_size = 8
    invalid_page_message = "server_invalid_page_number"

    def get_paginated_response(self, data):
        return Response(
            {
                "items_per_page": self.page_size,
                "current_page": self.page.number,
                "total_pages": self.page.paginator.num_pages,
                "total_items": self.page.paginator.count,
                "results": data,
            }
        )

class CommonUtil:
    @staticmethod
    def get_authentication_classes():
        authentication_classes = [
            JWTTokenUserAuthentication,
        ]

        if settings.ENVIRONMENT != "production":
            authentication_classes.append(SessionAuthentication)
            authentication_classes.append(BasicAuthentication)

        return authentication_classes

class EmailUtil:
    @staticmethod
    def send_signup_email(instance, verification_token):

        verify_url = settings.DOMAIN_NAME + "/#/signup/verify?"

        verify_url += (
            "email="
            + str(instance.email)
            + "&"
            + "token="
            + str(verification_token.token)
        )

        message = Mail(
            from_email=settings.DEFAULT_FROM_EMAIL,
            to_emails=instance.email,
        )

        message.dynamic_template_data = {"verify_url": verify_url}

        # NOTE: DYNAMIC TEMPLATE ID OF THE TEMPLATE IN THE SENDGRID ACCOUNT for Sign Up email
        message.template_id = settings.SENDGRID_TEMPLATE_ID_LABORATORY_SIGNUP_EMAIL

        try:
            sendgrid_client = SendGridAPIClient(settings.SENDGRID_API_KEY)
            response = sendgrid_client.send(message)

        except Exception as error:
            # log to sentry
            capture_exception(error)

    @staticmethod
    def send_request_password_reset_email(instance, verification_token):

        verify_url = settings.DOMAIN_NAME + "/#/password/reset?"

        verify_url += (
            "email="
            + str(instance.email)
            + "&"
            + "token="
            + str(verification_token.token)
        )

        message = Mail(
            from_email=settings.DEFAULT_FROM_EMAIL,
            to_emails=instance.email,
        )

        message.dynamic_template_data = {"verify_url": verify_url}

        # NOTE: DYNAMIC TEMPLATE ID OF THE TEMPLATE IN THE RESET PASSWORD email
        message.template_id = settings.SENDGRID_TEMPLATE_ID_REQUEST_PASSWORD_RESET_EMAIL

        try:
            sendgrid_client = SendGridAPIClient(settings.SENDGRID_API_KEY)
            response = sendgrid_client.send(message)

        except Exception as error:
            # log to sentry
            capture_exception(error)

class TimeUtil:
    @staticmethod
    def get_minutes_from_now(MINUTES):
        return timezone.now() + timedelta(minutes=MINUTES)


## https://leetcode.com/problems/letter-combinations-of-a-phone-number/

from typing import List
from collections import deque

class Solution:
    def letterCombinations(self, digits: str) -> List[str]:
        alphas = {"2" : ["a", "b", "c"], "3" : ["d", "e", "f"], "4" : ["g", "h", "i"], "5" : ["j", "k", "l"],
                    "6" : ["m", "n", "o"], "7" : ["p", "q", "r", "s"], "8" : ["t", "u", "v"], "9" : ["w", "x", "y", "z"]}

        if not digits:
            return []

        q = deque()
        q.extend([(alpha, 0) for alpha in alphas[digits[0]]])

        while len(q[0][0]) < len(digits):
            string, length = q.popleft()
            for alpha in alphas[digits[length+1]]:
                q.append((string + alpha, length + 1))

        return [string for string, length in q]

print(Solution().letterCombinations("23"))


from django.http.response import Http404
from rest_framework import serializers

from rest_framework import status
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework.decorators import api_view, permission_classes
from .models import HousingData
from .serializers import HousingDataSerializer
# from .models import DataCart
# from .serializers import DataCartSerializer
from django.contrib.auth.models import User

# Create your views here.
class HousingDataList(APIView):

    permission_classes = [AllowAny]
    # change to [IsAuthenticated for saved data shopping cart model where reqs userId]

    def get(self, request):
        qs = HousingData.objects.all()
        country = request.GET.get('country')
        state = request.GET.get('state')
        city = request.GET.get('city')
        lower_price = request.GET.get('lower')

        if city:
            qs = qs.filter(city=city)

        if state:
            qs = qs.filter(state=state)

        if country:
            qs = qs.filter(country=country)

        if lower_price:
            qs = qs.filter(rent1YearAvg__gte=lower_price)
            # modify and/or and more?

        serializer = HousingDataSerializer(qs, many=True)
        return Response(serializer.data)

    def post(self, request):
        serializer = HousingDataSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

class HousingDataDetail(APIView):

    def get_object(self,pk):
        try:
            return HousingData.objects.get(pk=pk)
        except HousingData.DoesNotExist:
            raise Http404

    def get(self,request,pk):
        housingdata = self.get_object(pk)
        serializer = HousingDataSerializer(housingdata)
        return Response(serializer.data)

    def put(self, request, pk):
        housingdata = self.get_object(pk)
        serializer = HousingDataSerializer(housingdata, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk):
        housingdata = self.get_object(pk)
        housingdata.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

# class DataCartList(APIView):

#     permission_classes = [AllowAny]
#     # change to [IsAuthenticated for saved data shopping cart model where reqs userId]

#     def get(self, request):
#         stats = DataCart.objects.all()
#         serializer = DataCartSerializer(DataCart, many=True)
#         return Response(serializer.data)


import jax
import jax.numpy as jnp
from functools import partial

def get_eval_fn(pure_logits_fn, task):

    @jax.jit
    def pure_eval_fn(params, key, token_ids):
        logits = pure_logits_fn.apply(params, key,
                                      token_ids, training=False,
                                      task=task)
        return logits

    return pure_eval_fn

@partial(jax.jit, static_argnums = (1,2,))
def greedy(key, pure_logits_eval_fn, max_length, params, initial_token_ids):

    token_ids = initial_token_ids
    for i in range(max_length):

        key, subkey = jax.random.split(key)
        logits = pure_logits_eval_fn(params, subkey, token_ids)

        predictions = jnp.argmax(logits[:,i,:], axis=-1)

        token_ids = jax.ops.index_update(token_ids, jax.ops.index[:,i+1],
                                         predictions)
    return token_ids

def first_padded_idx(x, pad_id):
    return x.shape[-1]-jnp.sum(x==pad_id, axis=-1)[0]

def seeded_greedy(key, pure_logits_eval_fn, config, params, initial_token_ids):

    token_ids = initial_token_ids
    j = first_padded_idx(initial_token_ids, config['pad_id'])
    for i in range(j, config['max_length']):

        key, subkey = jax.random.split(key)
        logits = pure_logits_eval_fn(params, subkey, token_ids)

        predictions = jnp.argmax(logits[:,i,:], axis=-1)

        token_ids = jax.ops.index_update(token_ids, jax.ops.index[:,i+1],
                                         predictions)
    return token_ids


# -*- coding:utf-8 -*-
import sys
import os
import time
import json
import re
import random

def get_millis_time():
    return int(round(time.time() * 1000))

def dict_to_query_str(param_dict):
    s = ''
    for key in param_dict:
        s += "{}={}&".format(key, param_dict[key])
    return s[0:-1]

def jsonp_to_dict(jsonp):
    if isinstance(jsonp, str):
        match = re.findall(r'.+\((.+)\)', jsonp)
        if len(match) == 0:
            return False
        try:
            d = json.loads(match[0])
        except:
            return False
        finally:
            if isinstance(d, dict):
                return d
    return False

def random_str(length, no_repeat=False):
    if length > 18:
        length = 18
    s = '1234567890qwertyuiopasdfghjklzxcvbnm'
    ls = len(s)
    t = []
    while True:
        j = s[random.randint(0, ls-1)]
        if no_repeat:
            if j not in t:
                t.append(j)
        else:
            t.append(j)
        if len(t) >= length:
            break

    return "".join(t)

def main_is_frozen():
    """Return ''True'' if we're running from a frozen program."""
    import imp
    return (
        # new py2exe
        hasattr(sys, "frozen") or
        # tools/freeze
        imp.is_frozen("__main__"))

def get_main_dir():
    """Return the script directory - whether we're frozen or not."""
    if main_is_frozen():
        return os.path.abspath(os.path.dirname(sys.executable))
    return os.path.abspath(os.path.dirname(sys.argv[0]))


import re
import bs4
import string
import nltk

def strip_json(file_path):
    """
    Strips lines of html and css. Converts file contents from dictionary to string format. Removes broken unicode
    :param file_path: Path to json file
    :return: List of strings decoded from html and css format
    """

    # List of laws
    contents = []

    # Open json file
    with open(file_path) as f:

        # Loop through each line in file
        for line in f:

            # Create Beautiful soup html data_parser object
            soup = bs4.BeautifulSoup(line, "html.parser")

            # Remove tags (soup.get_text) and json format (10:-4)
            stripped = str(soup.get_text()[10:-4])

            # Remove unicode that was decoded incorrectly (\\u201a, \\n, ...)
            stripped = re.sub(r'\\[\w][\d|\w][\d|\w][\d|\w][\d|\w]||\\n', '', stripped)

            # Remove things like
            #   section 3151(e)(2) of this title
            #   September 30, 2009,
            #   paragraph (1)
            #   subsection (a)
            #   subparagraph (A)(iv)
            #   clause (ii)
            #   (D)
            #   (1)
            stripped = re.sub(r"((\w+)\.?\s(\d+)\,\s(\d{4}))|"
                              r"(\w*(?<=section).*of this title)|"
                              r"(\w*(?<=sections).*of this title)|"
                              r"(\w*(?<=section).*of title \d+)|"
                              r"(\w*(?<=sections).*of title \d+)|"
                              r"(paragraph \(\d+\))|"
                              r"(subsection \(\w+\))|"
                              r"(subparagraph \(\w+\)\(\w+\))|"
                              r"(clause \(\w+\))|"
                              r"(\(\w+\))|"
                              r"(\(\d+\))"
                              , "", stripped)

            # Add lines that have text on them
            if len(stripped) > 0:
                contents.append(stripped)

    return contents

def list_to_file(data, file_path):
    """
    Writes a list of strings to a file
    :param data: List of strings
    :param file_path: Path to file
    :return: None
    """

    # Open target file
    with open(file_path, "w") as f:

        # Write each element of list to a new line
        for line in data:
            f.write(line + "\n")

def file_to_list(file_path):
    """
    Convert file contents into a list of strings
    :param file_path: Path to file
    :return: List of strings
    """

    # List of lines in file
    contents = []

    # Open file
    with open(file_path) as f:

        # Loop through each line in file
        for line in f:

            # Add line to list
            contents.append(line)

    return contents

def create_n_grams(data, n_grams=None, max_n_gram=7):
    """
    Splits data into n-grams, ranging from n=1 to n=max_n_gram
    :param data: List of strings
    :param n_grams: Dictionary of old n_grams, if avaliable
    :return: All n-grams found for each string element in data
    """

    # If n-gram dictionary doesnt exist yet
    if n_grams is None:

        # Initialize n-gram dictionary
        n_grams = {}
        for i in range(1, max_n_gram+1):
            n_grams[i] = []

    # Fill n-gram dictionary
    for line in data:

        # Change text to lowercase
        line = line.lower()

        # Remove punctuation
        line = line.translate(str.maketrans("", "", string.punctuation))

        # Split the line by space
        split = line.split()

        # Create n-grams ranging from n=1 to n=max_n_gram
        for n in range(1, max_n_gram+1):

            # Check if current sentence is long enough to support the n-gram
            if len(split) >= n:
                current_n_grams = list(nltk.ngrams(split, n))

                # Append each n-gram
                for n_gram in current_n_grams:
                    n_grams[n].append(n_gram)

            # else, sentence is too short for current n-gram and subsequent larger n-grams
            else:
                break

    return n_grams

def get_frequency(n_grams):
    """
    Finds the number of unique n-grams
    :param n_grams: Dictionary of lists of n-grams
    :return: Dictionary of lists of unique n-grams
    """

    # Unique n-grams
    n_gram_frequency = {}

    # Loop through all n-grams starting from 1-grams
    for i in range(1, len(n_grams)+1):

        # Create a dictionary for each layer (i) of n-grams
        n_gram_frequency[i] = {}

        # Loop through all the tokens in a layer and
        for token in n_grams[i]:

            # Add tokens to dictionary
            if token not in n_gram_frequency[i]:
                n_gram_frequency[i][token] = 1
            else:

                # Count number of times the specific token is repeated
                n_gram_frequency[i][token] = n_gram_frequency[i][token] + 1

    return n_gram_frequency


# Тонкая настройка моделей методом сеточного поиска (Grid Search)

# В машинном обучении существует 2 типа параметров: параметры, извлекаемые из тренировочных данных, например весовые коэффициенты в
# логистической регрессии, и параметры алгоритма обучения, которые оптимизируются отдельно.

# Вторые параметры - это настроечные (гиперпараметры) параметры, которые оптимизируются отдельно, например, параметр регуляризации в
# логистической регрессии или глубина дерева решений.

# Принцип работы данного алгоритма весьма прост, мы создаем список значений для различных гиперпараметров, после этого компьютер оценивает
# качество модели для каждой их комбинации.

# Вместо раздельного выполнения шагов по подгонке и преобразованию тренировочного и тестового наборов данных мы можем расположить бъекты
# StandardScaler, PCA и LogisticRegression друг за другом в конвейере.

# -=-=-=-=-=-=-=- Этап чтения данных
import pandas as pd
import numpy as np
# url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data'
# df = pd.read_csv(url, header=None)

url = '/Users/grigorev-ee/Work/AnacondaProjects/My_projects/ml_w_python/data/cancer.base'
df = pd.read_csv(url)

names = ['id_number', 'diagnosis', 'radius_mean',
         'texture_mean', 'perimeter_mean', 'area_mean',
         'smoothness_mean', 'compactness_mean', 'concavity_mean',
         'concave_points_mean', 'symmetry_mean',
         'fractal_dimension_mean', 'radius_se', 'texture_se',
         'perimeter_se', 'area_se', 'smoothness_se',
         'compactness_se', 'concavity_se', 'concave_points_se',
         'symmetry_se', 'fractal_dimension_se',
         'radius_worst', 'texture_worst', 'perimeter_worst',
         'area_worst', 'smoothness_worst',
         'compactness_worst', 'concavity_worst',
         'concave_points_worst', 'symmetry_worst',
         'fractal_dimension_worst']

df = df[names]

# Присвоим 30 признакоы массиву X библиотеки NumPy.
# При помощи LabelEncoder преобразуем метки классов из их исходного строкового представления (M и B) в целочисленное.
from sklearn.preprocessing import LabelEncoder
X = df.iloc[:,2:].values
y = df.iloc[:,1].values

print(y[:36])

le = LabelEncoder()
y=le.fit_transform(y)

# M-злокачественная опухоль (malignant), B-доброкачественная (benign). Выведем первые 36 элементов из набора данных.
print(y[:36])

# Выведем пример того, как работает кодировщик на тестовом наборе данных
le.transform(['M','M','B'])

# Прежде, чем в следующем подразделе мы построим наш первый модельный конвейер, рзделим выборку на обучающую и тренировочную.
# Оценка модели на ранее не встречавшихся данных
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=1)

# -=-=-=-=-=-=-= Основной алгоритм -=-=-=-=-=-=-=-=-=-=
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn. model_selection import GridSearchCV
from sklearn.svm import SVC
pipe_svc = Pipeline([('scl', StandardScaler()), ('clf', SVC(random_state=1))])
param_range = [0.0001, 0.001, 0.01, 0.1, 1.0, 10.0, 100.0, 1000.0]

param_grid = [{'clf__C': param_range,
               'clf__kernel': ['linear']},
              {'clf__C': param_range,
               'clf__gamma': param_range,
               'clf__kernel': ['rbf']}]

# Инициализируем объект GridSearchCV, чтобы натренировать и настроить конвейер методо опорных векторов (SVM).
# Мы назначаем параметру param_grid объекта GridSearchCV список словарей с определением параметров, которые мы хотели бы настроить.
# Для линейного классификатора SVM мы настраиваем только один параметр, параметр обратной регуляризации C, для ядерного SVM c РБФ в
# качестве ядра мы выполнили настройку двух параметров: C и gamma. Отметим, что параметр gamma имеет непосредственное отношение к
# ядерным методам SVM. По результатам применения оптимизационного поиска по сетке параметров в трибуте best_score мы получим оценку
# наиболее качественной модели и обратились к его параметрам через атрибут best_params.
gs=GridSearchCV(estimator=pipe_svc, param_grid=param_grid, scoring='accuracy', cv=10, n_jobs=-1)
gs=gs.fit(X_train, y_train)
print('Перекрестно-проверочная верность: ', gs.best_score_)
print('Лучшие параметры: ', gs.best_params_)

# Воспользуемся независимым тестовым набором данных для оценки качества наилучшей отобранной модели, которая доступна через атрибут
# best_estimator_ объекта GridSearchCV
clf = gs.best_estimator_
clf.fit(X_train, y_train)
print('Верность на тестовом наборе: %.3f' % clf.score(X_test, y_test))

# Поиск по сетке параметров является мощным методом нахождения оптимальных параметров, но при этом очень затратным в плане ресурсов.
# Альтернативным подходом является метод рандомизированного поиска (RandomizedSearchCV).
# Дополнительные примеры можно найти по ссылке:
# http://scikit-learn.org/stable/modules/grid_search.html#randomized-parameter-optimization


import requests
import json

URL = "http://127.0.0.1:8000/deserializer/"

data = {
    'name': 'Tushar',
    'roll': 101,
    'city': 'Dahaka'
}

json_data = json.dumps(data)

req = requests.post(url=URL, data=json_data)

data = req.json()
print(data)


"""
Santander Web Pay Plus
"""
import re
import asyncio
import os
import urllib
import xml.etree.ElementTree as ET

from dotenv import load_dotenv
import requests

from lib.AESEncryption import AES128Encryption
from lib.exceptions import (
    CitasConnectionError,
    CitasMissingConfigurationError,
    CitasNotValidAnswerError,
    CitasTimeoutError,
    CitasRequestError,
    CitasUnknownError,
    CitasEncryptError,
    CitasGetURLFromXMLEncryptedError,
    CitasDesencryptError,
    CitasBankResponseInvalidError,
    CitasXMLReadError,
)

XML_ENCRYPT_REGEXP = r"^[a-zA-Z0-9=+\/]{32,}$"

RESPUESTA_EXITO = "approved"
RESPUESTA_DENEGADA = "denied"
RESPUESTA_ERROR = "error"

load_dotenv()
WPP_COMMERCE_ID = os.getenv("WPP_COMMERCE_ID", None)
WPP_COMPANY_ID = os.getenv("WPP_COMPANY_ID", None)
WPP_BRANCH_ID = os.getenv("WPP_BRANCH_ID", None)
WPP_KEY = os.getenv("WPP_KEY", None)
WPP_PASS = os.getenv("WPP_PASS", None)
WPP_TIMEOUT = int(os.getenv("WPP_TIMEOUT", "12"))
WPP_URL = os.getenv("WPP_URL", None)
WPP_USER = os.getenv("WPP_USER", None)

def create_chain_xml(
    pago_id: int,
    amount: float,
    email: str,
    description: str,
    cit_client_id: int,
) -> str:
    """Crear cadena XML"""

    # Validar WPP_COMPANY_ID
    if WPP_COMPANY_ID is None:
        raise CitasMissingConfigurationError("Falta declarar la variable de entorno WPP_COMPANY_ID.")

    # Validar WPP_BRANCH_ID
    if WPP_BRANCH_ID is None:
        raise CitasMissingConfigurationError("Falta declarar la variable de entorno WPP_BRANCH_ID.")

    # Validar WPP_USER
    if WPP_USER is None:
        raise CitasMissingConfigurationError("Falta declarar la variable de entorno WPP_USER.")

    # Validar WPP_PASS
    if WPP_PASS is None:
        raise CitasMissingConfigurationError("Falta declarar la variable de entorno WPP_PASS.")

    # Inicializar XML
    root = ET.Element("P")

    # Inicializar datos de la organización
    business = ET.SubElement(root, "business")
    ET.SubElement(business, "id_company").text = WPP_COMPANY_ID
    ET.SubElement(business, "id_branch").text = WPP_BRANCH_ID
    ET.SubElement(business, "user").text = WPP_USER
    ET.SubElement(business, "pwd").text = WPP_PASS

    # Inicializar datos de la version
    ET.SubElement(root, "version").text = "IntegraWPP"

    # Inicializar datos del cliente y del pago
    url = ET.SubElement(root, "url")
    ET.SubElement(url, "reference").text = str(pago_id)
    ET.SubElement(url, "amount").text = str(amount)
    ET.SubElement(url, "moneda").text = "MXN"
    ET.SubElement(url, "canal").text = "W"
    ET.SubElement(url, "omitir_notif_default").text = "0"
    ET.SubElement(url, "st_correo").text = "1"
    ET.SubElement(url, "mail_cliente").text = email

    # Inicializar datos adicionales
    data = ET.SubElement(url, "datos_adicionales")

    # Inicializar descripción del servicio
    data1 = ET.SubElement(data, "data")
    data1.attrib["id"] = "1"
    data1.attrib["display"] = "true"
    label1 = ET.SubElement(data1, "label")
    label1.text = "Servicio"
    value1 = ET.SubElement(data1, "value")
    value1.text = description

    # Inicializar descripción del cliente
    data2 = ET.SubElement(data, "data")
    data2.attrib["id"] = "2"
    data2.attrib["display"] = "false"
    label2 = ET.SubElement(data2, "label")
    label2.text = "Cliente ID"
    value2 = ET.SubElement(data2, "value")
    value2.text = str(cit_client_id)

    # Entregar cadena XML
    return ET.tostring(root, encoding="unicode")

def encrypt_chain(chain: str) -> str:
    """Cifrar cadena XML"""

    # Validar WPP_KEY
    if WPP_KEY is None:
        raise CitasMissingConfigurationError("Falta declarar la variable de entorno WPP_KEY.")

    # Cifrar la cadena
    aes_encryptor = AES128Encryption()
    ciphertext = aes_encryptor.encrypt(chain, WPP_KEY)

    # Entregar cadena cifrada
    return ciphertext

def decrypt_chain(chain_encrypted: str) -> str:
    """Descifrar cadena XML"""

    # Validar WPP_KEY
    if WPP_KEY is None:
        raise CitasMissingConfigurationError("Falta declarar la variable de entorno WPP_KEY.")

    # Eliminar avances de línea y espacios en blanco
    chain_encrypted = chain_encrypted.replace("\n", "").replace(" ", "")

    # Validar la cadena cifrada con la expresión regular
    if re.fullmatch(XML_ENCRYPT_REGEXP, chain_encrypted) is None:
        raise CitasBankResponseInvalidError(f"Error porque la respuesta del banco no pasa la validación por expresión regular: [{chain_encrypted}]")

    # Descifrar la cadena
    aes_encryptor = AES128Encryption()
    try:
        plaintext = aes_encryptor.decrypt(WPP_KEY, chain_encrypted)
    except Exception as error:
        raise CitasDesencryptError("Error porque no se pudo desencritar la respuesta del banco.") from error

    # Entregar cadena descifrada
    return plaintext

def create_chain_xml_sender(chain: str) -> str:
    """Crear cadena para XML de envío WPP"""

    # Validar WPP_COMMERCE_ID
    if WPP_COMMERCE_ID is None:
        raise CitasMissingConfigurationError("Falta declarar la variable de entorno WPP_COMMERCE_ID.")

    # Validar WPP_URL
    if WPP_URL is None:
        raise CitasMissingConfigurationError("Falta declarar la variable de entorno WPP_URL.")

    # Empacar la cadena
    root = ET.Element("pgs")
    ET.SubElement(root, "data0").text = WPP_COMMERCE_ID
    ET.SubElement(root, "data").text = chain
    chain_bytes = ET.tostring(root, encoding="unicode")

    # Entregar cadena
    return urllib.parse.quote(chain_bytes, "utf-8")

async def send_chain(chain: str) -> str:
    """Enviar cadena a WPP"""

    # Validar WPP_URL
    if WPP_URL is None:
        raise CitasMissingConfigurationError("Falta declarar la variable de entorno WPP_URL.")

    # Validar WPP_TIMEOUT
    if WPP_TIMEOUT is None:
        raise CitasMissingConfigurationError("Falta declarar la variable de entorno WPP_TIMEOUT.")

    # Preparar la carga para la petición
    payload = "xml=" + create_chain_xml_sender(chain)
    headers = {"Content-Type": "application/x-www-form-urlencoded"}

    # Enviar la petición al banco
    try:
        response = requests.request(
            "POST",
            WPP_URL,
            headers=headers,
            data=payload,
            timeout=WPP_TIMEOUT,
        )
    except requests.exceptions.ConnectionError as error:
        raise CitasConnectionError("Error porque no se pudo conectar al banco.") from error
    except requests.exceptions.Timeout as error:
        raise CitasTimeoutError("Error porque se agoto el tiempo de espera con el banco.") from error
    except requests.exceptions.RequestException as error:
        raise CitasRequestError("Error al enviar la cadena al banco.") from error
    except Exception as error:
        raise CitasUnknownError("Error desconocido al enviar la cadena al banco.") from error

    # Entregar
    return response.text

def get_url_from_xml_encrypt(xml_encrypt: str):
    """Extrae la url del xml de respuesta"""

    # Desencriptar el XML
    try:
        xml_str = decrypt_chain(xml_encrypt)
    except Exception as error:
        raise CitasDesencryptError(f"No se puede desencriptar el XML del banco. {str(error)}") from error

    # Leer el contenido XML
    try:
        root = ET.fromstring(xml_str)
    except ET.ParseError as error:
        raise CitasXMLReadError("Error porque el XML del banco no es válido.") from error

    # Obtener el URL
    url = root.find("nb_url").text
    if url is None or url == "":
        raise CitasXMLReadError("Error porque el XML del banco no tiene la URL.")

    # Entregar
    return url

def create_pay_link(
    pago_id: int,
    email: str,
    service_detail: str,
    cit_client_id: int,
    amount: float,
) -> str:
    """Regresa el link para mostrar el formulario de pago"""

    # Crear cadena XML
    chain = create_chain_xml(
        pago_id=pago_id,
        amount=amount,
        email=email,
        description=service_detail,
        cit_client_id=cit_client_id,
    )

    # Encriptación del XML
    try:
        chain_encrypt = encrypt_chain(chain).decode()  # bytes
    except Exception as error:
        raise CitasEncryptError("Error al encriptar el XML") from error

    # Enviar cadena XML a WPP
    respuesta = None
    try:
        respuesta = asyncio.run(send_chain(chain_encrypt))
    except asyncio.TimeoutError as error:
        raise CitasTimeoutError("Error porque se agoto el tiempo de espera con el banco.") from error
    except Exception as error:
        raise CitasUnknownError("Error al tratar de enviar la cadena XML al banco.") from error

    # Si no hay respuesta, causar error
    if respuesta is None or respuesta == "" or respuesta == "\n":
        raise CitasNotValidAnswerError("Error en la respuesta del banco (respuesta vacía).")

    # Descifrar la respuesta y extraer la url
    try:
        url = get_url_from_xml_encrypt(respuesta)
    except Exception as error:
        raise CitasGetURLFromXMLEncryptedError(f"Error al obtener la URL del banco desde su XML encriptado. {str(error)}") from error

    # Entregar
    return url

def convert_xml_to_dict(xml_str: str) -> dict:
    """Convertir el xml descifrado a un diccionario"""

    # Leer el contenido XML
    try:
        root = ET.fromstring(xml_str)
    except ET.ParseError as error:
        raise CitasXMLReadError("Error porque el XML que dio el banco no es válido.") from error

    # Obtener pago_id
    pago_id = root.find("reference").text
    if pago_id is None or pago_id == "":
        raise CitasXMLReadError("Error porque el XML no tiene el pago_id.")

    # Obtener respuesta
    respuesta = root.find("response").text
    if respuesta is None or respuesta == "":
        raise CitasXMLReadError("Error porque el XML no tiene la respuesta.")

    # Obtener folio
    folio = root.find("foliocpagos").text
    if folio is None or folio == "":
        raise CitasXMLReadError("Error porque el XML no tiene el folio.")

    # Obtener auth
    auth = root.find("auth").text
    if auth is None or auth == "":
        raise CitasXMLReadError("Error porque el XML no tiene el auth.")

    # Obtener email
    email = root.find("email").text
    if email is None or email == "":
        raise CitasXMLReadError("Error porque el XML no tiene el email.")

    # Entregar diccionario
    return {
        "pago_id": pago_id,
        "respuesta": respuesta,
        "folio": folio,
        "auth": auth,
        "email": email,
    }


import random
from functools import wraps
from time import time

import numpy as np

REAL_COUNTS_LIST = 'real_counts_list'

ESTIMATED_COUNTS_LIST = 'estimated_counts_list'

CHANGES = 'changes_'

def timing(f):
    # a decorator for function timinf
    @wraps(f)
    def wrap(*args, **kw):
        ts = time()
        result = f(*args, **kw)
        te = time()
        # print('func:%r args:[%r, %r] took: %2.4f sec' % \
        #       (f.__name__, args, kw, te - ts))
        print('func:%r took: %2.4f sec' % \
              (f.__name__, te - ts))
        return result

    return wrap

def set_random_seed(random_seed):
    # set a random seed for both random and numpy
    random.seed(random_seed)
    np.random.seed(random_seed)