from __future__ import absolute_import, division
from collections import defaultdict
from .components import Edge, Node
from .exceptions import InvalidNodeTypeError, MaxNodeError, GraphTypeError
from .utils import check_cycle
from .algorithms._search import BFS, DFS
from .algorithms._sort import topological_sort
from ._vis.layout import plot_graph_directed, plot_graph_undirected
from graphnet import VECTOR, SCALAR
import matplotlib.pyplot as plt
import numpy as np

class Graph(object):
    """
    Implements the Graph data structure which contains nodes or vertices linked with edges between them

    Graph helps in managing relationship between nodes in a network with their edges.
    Parameters
    ----------
    max_node:int, float, optional, default=infinity
        the maximum amount of nodes that can be added to the graph,
        default is infinity which means in theory nodes can be added
        to the graph indefinitely.
    max_edge:int, float, optional, default=infinity
        the maximum amount of edges that can be added to the graph,
        default is infinity
    type:{'scalar'or'S', 'vector'or'V'},optional, default='scalar'
        this specifies the graph type, the are two graph types 'vector'
        which represents directional graphs and 'scalar' which represents
        un-directional graphs. the type of of graph determines the behavior
        of the graph instance and which algorithm work with it. optionally
        you can substutute 'scalar' with 'S' and 'vector' with 'V'.
    ref:str,optional,default="value"
        the attribute used by the graph for referencing the node object passed in
        for identification and accessing as key value for the graph. change this
        if you are using a inherited node object to the object identifier else if
        you're using the built in Node class directly then leave it as default.

    Attributes
    ----------
    edges:list
        This is a list of all edges in the graph instance
    connections:dict<defaultdict>
        A dictionary thats maps each node to its neighbor with an edge, a node
        in the connection dictionary contains a default dict and in the default dict
        the adjacent node is mapped with its edge object and if there is no relationship
        with the two node 0 will be returned.
    graph_matrix: numpy.array()
        this is a adjacent matrix form of the graph, it is read only.
    get_node: list
        lits of all the node object in the graph, read only.

    """

    def __init__(self, max_node=float("inf"), max_edge=float("inf"), type=SCALAR, ref="value"):
        self.__max_node = max_node
        self.max_edge = max_edge
        self.type = type
        self.ref = ref
        self.edges = []
        self.connections = {}
        self.__nodes = []
        self.__node_map = {}

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

    def __iter__(self):
        self.__counter = 0
        return self

    def __next__(self):
        if self.__counter < self.__len__():
            res = self.__nodes[self.__counter]
            self.__counter += 1
            return res
        else:
            raise StopIteration

    def __getitem__(self, key):
        return self.__node_map[key]

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.clear()

    def add_node(self, node):
        """
        Adds an arbitrary object or Node object to the graph,
        all nodes added should be unique.
        parameters
        ----------
        node: str, int, float, Node

        """
        if self.__len__() <= self.__max_node:
            if type(node) in [str, int, float]:
                node = Node(node)
            if node not in self.__nodes:
                if type(node) == Node or issubclass(node.__class__, Node):
                    self.__nodes.append(node)
                    node_id = self.get_node_id(node)
                    self.__node_map[node_id] = node
                else:
                    raise InvalidNodeTypeError(
                        "Expected object of type str, float, int, Node or subclass of Node but %s was given." % type(node))
                self.connections[node_id] = defaultdict(int)
            else:
                raise ValueError("Node instance already in graph network")
        else:
            raise MaxNodeError(
                "Graph max size exceeded, expected %d node." % (self.__max_node))

    def add_edge(self, _from, _to, weight=1):
        """
        Adds a edge to the graph connecting the _from and _to nodes,
        the values passed to the parameters _from and _to must be a
        node in the graph or a node in the graph must contain the
        value passed in.
        if the type of the graph object is scalar an edge will be added
        from _to to _from.
        parameters
        ----------
        _from:int,str,float,Node
            this is the source from where the edge starts from
        _to:int,str,float,Node
            the destination of the edge
        weight:int, str, float, default=1
            this is the weight of the edge object connecting the nodes
            the default value is 1.
        """
        if len(self.connections) <= self.max_edge:
            if type(_from) == Node or issubclass(_from.__class__, Node):
                _from = self.get_node_id(_from)
            if type(_to) == Node or issubclass(_to.__class__, Node):
                _to = self.get_node_id(_to)
            node = self[_from]
            node_to = self[_to]
            edge = Edge(node, node_to, weight)
            self.connections[_from][_to] = edge
            node.add_node(node_to)
            self.edges.append(edge)
            if self.type == SCALAR:
                edge2 = Edge(_to, _from, weight)
                self.connections[_to][_from] = edge2
                node_to.add_node(node)
                self.edges.append(edge2)
        else:
            raise MaxNodeError(
                "Graph max size exceeded, expected %d node." % (self.max_edge))

    def add_nodes_from_iterable(self, iterable):
        """
        Adds nodes from a an iterable to the graph.
        Parameters
        ----------
        iterable:iter
            an iterable object containing nodes to be added to the graph
        """
        for node in iterable:
            self.add_node(node)

    def add_edges_from_iterable(self, iterable):
        """
        Adds nodes edge a an iterable to the graph.
        Parameters
        ----------
        iterable:iter
            an iterable object containing a iterable with size of 3 or 2
            where the first two values are values of nodes in the graph
            and if there's a third value it will be used as the weight of
            the edge to be added to the graph
        """
        for e in iterable:
            self.add_edge(*e)

    @property
    def graph_matrix(self):
        nodes = self.__nodes
        self.__adj_matrix = np.zeros((self.__len__(), self.__len__()))
        for i in range(self.__len__()):
            id_i = self.get_node_id(nodes[i])
            for j in range(self.__len__()):
                id_j = self.get_node_id(nodes[j])
                edge = self.connections[id_i][id_j] if not self.connections[id_i][id_j] else self.connections[id_i][id_j].weight
                self.__adj_matrix[i, j] = edge
        return self.__adj_matrix

    @property
    def get_nodes(self):
        return self.__nodes

    def from_dict(self, dictionary, weights=1):
        """
        creates and add node from a dictionary. The dictionary passed in must
        have a key with an iterable value which contains all adjacents node, and
        node in the iterable must be in the dictionary or graph. The weight will be
        uniform for all nodes.
        Parmeters
        ---------
        dictionary: dict
            dictionary object thats holds weights and their connections
        weights: int, str, float
            uniform weight passed to all edges.
        """
        self.add_nodes_from_iterable(list(dictionary.keys()))
        for key in dictionary:
            for edge in dictionary[key]:
                self.add_edge(key, edge, weights)

    def is_cyclic(self):
        """
        Checks if the graph contains a cycle, if the graph type is scalar

        returns
        -------
        bool
        """
        if self.type != VECTOR:
            raise GraphTypeError(
                "cyclic check only works for vector type graphs")
        visited = {}
        rec_stack = {}
        for node in self:
            visited[node] = False
            rec_stack[node] = False
        for source in self:
            if check_cycle(source, visited, rec_stack):
                return True

        return False

    def clear(self):
        """
        Clears all the graph content
        """
        self.__nodes.clear()
        self.connections = {}
        self.edges.clear()

    def is_connected(self):
        """
        Checks if a scalar graph is connected or not.

        returns
        -------
        bool
        """
        if self.type == SCALAR:
            traverse = self.BFS()
            for node in self.__nodes:
                if not node in traverse:
                    return False
            return True
        else:
            raise GraphTypeError(
                "Invalid graph type %s expected scalar" % (self.type))

    def display(self, weighted=False, weight_color=False, arrow_color=True, layout="polygon", polygon_radius=5, attr=None, ax=None):
        """
        creates a plot of the graph.

        parameters
        ----------
        weighted:bool, optional, default=False
            if true weights will be displayed.
        weight_color:bool, optional, default=False
            if true the weights will be colored with the edge object color attribute.
        arrow_color:bool, optional, default=True
            if true the arrow will be colored with the edge object color attribute.
        layout: optional, {random, polygon}, default=polygon
            The layout used to arrange the nodes in the plot
        polygon_radius: int, float, default=5
            if polygon layout is used this defines the radius of the polygon shape.
        attr:None, str, int, object, default=None
            The attribute of the Node object to be used as label. if omitted the default value
            will be the ref attribute of the graph.
        ax:.axes.Axes, default=None
            An axis object to plot the graph, if not specified a default axis will be created.

        """
        if attr == None:
            attr = self.ref
        if not ax:
            _, ax = plt.subplots(1, figsize=(20, 20))
        if self.type == VECTOR:
            plot_graph_directed(self, ax, len(
                self), weighted, weight_color, arrow_color, layout, polygon_radius, attr)
        elif self.type == SCALAR:
            plot_graph_undirected(self, ax, len(
                self), weighted, weight_color, arrow_color, layout, polygon_radius, attr)

    def remove_edge(self, _from, _to):
        """
        removes edge connecting to nodes if it exist.

        Parameters
        ----------
            _from:int, str, float
            _to:int, str, float
        """
        del self.connections[_from][_to]
        for i in range(len(self.edges)):
            if self.edges[i]._from == _from and self.edges[i]._to == _to:
                self.edges.pop(i)
                break

        src = self[_from]
        dest = self[_to]
        for i in range(len(src.adjacent_nodes)):
            if dest == src.adjacent_nodes[i]:
                src.adjacent_nodes.pop(i)
                break

    def get_node_id(self, node):
        """
        gets the value of the node object used in identifying
        the node
        Parameters
        ----------
        node:Node
        returns
        -------
        value:str, int, float, object

        """
        for vertex in self.__nodes:
            if vertex == node:
                v = node
                break
        return eval('v.%s' % self.ref)

    def topological_sort(self):
        """
        Sorts the nodes if graph type is vector
        returns
        -------
        res:List
            a list containing  all the node in the graph instance
            in a sorted form
        """
        return topological_sort(self)

    def BFS(self, source=None, key=None):
        """
        An Impelemetaion of breath-first-search for tranversing a
        graph of getting a node.
        Parameters
        ----------
        source:int, str, float, optional
            Value of the node to start the tranverse. if omitted the method
            uses a random node as source.
        key:int, str, float, optional
            Value of the node to stop the tranverse. if omitted the method
            stops when all  node in the graph are tranversed.
        returns
        -------
        value: list
            A list of the path tranversed in order from source to key.
        """
        return BFS(self, source, key)

    def DFS(self, source=None, key=None):
        """
        An Impelemetaion of depth-first-search for tranversing a
        graph of getting a node.
        Parameters
        ----------
        source:int, str, float, optional
            Value of the node to start the tranverse. if omitted the method
            uses the first node in the graph as source.
        key:int, str, float, optional
            Value of the node to stop the tranverse. if omitted the method
            stops when all  node in the graph are tranversed.
        returns
        -------
        value: list
            A list of the path traversed in order from source to key.
        """
        return DFS(self, source, key)

class GraphPriorityQueue:
    """
    A priority queue data structure which stores node
    and get them based on priority. if state is true nodes
    will be gotten by priority and if their state value evaluate
    to true.
    Parameters
    ----------
    graph: Graph
        graph object whose node will be stored in the queue.
    type: {"min", "max"}, default="min"
        the type of priority based on how the values in the
        graph will be retrived, if min the smallest value will
        be prioritized and if max the largest value will be prioritized.
    state: boolean, default=False
        this specifies if state will be used in the queue. If true an
        attribute status will be created to hold the states of all the
        nodes.
    Attributes
    ---------
    type: {"min", "max"}, default="min"
    queue: dict
        this holds node and value pair, the value is what is used in
        as priority in getting items.
    status: dict
        if state is true this will be created and hold a dictionary of
        nodes mapped with boolean.
    """

    def __init__(self, graph, type="min", state=False):
        if type == 'min' or type == 'max':
            self.type = type
        else:
            raise TypeError("type attribute most be 'min' or 'max'")
        self.__graph = graph
        self._top = float('inf') if self.type == 'min' else -float('inf')
        self.queue = dict()
        self.__state = state
        for node in self.__graph:
            self.queue[node] = float('inf')
        if self.__state:
            self.status = dict()
            for node in self.__graph:
                self.status[node] = False

    def __repr__(self):
        return str(list(self.queue.values()))

    def get(self):
        """
        gets a node based on priority if the queue isn't empty

        returns: Node
            if the queue isn't empty returns a Node object else it
            returns None.
        """
        if self.__state:
            if all(self.status.values()):
                return None
        if len(self.queue) != 0:
            sign = "<=" if self.type == "min" else ">=" if self.type == "max" else ''
            state = '' if not self.__state else 'and (not self.status[node])'
            for node in self.queue:
                if eval("(self.queue[node] %s self._top %s)" % (sign, state)):
                    self._top = self.queue[node]
                    res = node
            del self.queue[res]
            if self.__state:
                del self.status[res]
            self._top = float('inf') if self.type == 'min' else -float('inf')
            return res
        return None

    def set_status(self, node, value):
        """
        Tries to set the status of the node if state is true
        Parameters
        ----------
        node: Node
            node object to change state
        value: boolean
            value to change the node state.
        """
        assert (value == True or value == False) and (self.__state)
        self.status[node] = value

    def enqueue(self, node, value=0):
        """
        Inserts a node to the queue with the value to be used for
        prioritizing.
        Parameters
        ----------
        node: Node
            node object to insert
        value:int, float, default=0
            value to be used for prioritizing the node object.
        """
        self.queue[node] = value

    def is_empty(self):
        """
        Checks if the queue is empty
        returns
        -------
        out:boolean
            if True queue is empty else it's not.
        """
        if len(self.queue) != 0:
            return False
        return True


from telethon import TelegramClient, events, sync,utils
from telethon.tl.functions.users import GetFullUserRequest
from telethon.tl.functions.messages import GetDialogsRequest
from telethon.tl.types import InputPeerEmpty, InputPeerChannel, InputPeerUser
from telethon.errors.rpcerrorlist import (PeerFloodError, UserNotMutualContactError ,
                                          UserPrivacyRestrictedError, UserChannelsTooMuchError,
                                          UserBotError, InputUserDeactivatedError)
from telethon.tl.functions.channels import InviteToChannelRequest
import time,os,random,csv,sys
r= "\u001b[31;1m"
a= "\u001b[32m"
y = "\u001b[33;1m"
b="\u001b[34;1m"
m="\u001b[35;1m"
c="\u001b[36;1m"
clear = lambda:os.system('clear')

__banner__ = a+"""*\t🔰DARKNET_OFF1CIAL🔰*

                    xxxx                  xxxx
                 x        x            x        x
                x           x         x           x
                     xx                    xx
                   x    x                x    x
                  x      x              x      x
                  x      x              x      x
                  x    xxx              x    xxx
                  x   xxxx              x   xxxx
                   x xxxx                x xxxx
                    xxx         xxx       xxx
                               x      x
                               x       x
                xx               xxx             xx
              xx                                  xx
            xxx                                    xxx
               xx                                xx
                 xx                             xx
                   xxxx                      xxxx
                        xxx               xxx
                            xxxx     xxx

\tᎢᗴᖇᗰᑌ᙭ ᑌᏃ ᑭᖇᏆᐯᗩᎢᗴ"""
inf = (b+'@'+y+'d'+a+'a'+b+'r'+y+'k'+m+'n'+c+'e'+r+'t'+y+'_'+y+'o'+a+'f'+b+'f'+y+'1'+m+'c'+c+'a'+r+'l')
el=0
def Sleep(timE):
    try:
        time.sleep(timE)
    except KeyboardInterrupt:
        print(r+" KeyboardInterrupt , ........")
def info():
    print("")
    print("")
    print(__banner__)
    print(inf)
    print("")
    print("")
clear()
info()
def ospath():
    o=int(input(b+" Nechta telegram akauntingiz bor ? : "))
    for po in range(o):
        if os.path.isfile('multi_log.txt'):
            with open('multi_log.txt', 'r') as f:
                data = f.readlines()
            v=int(len(data)/2)
            z=v
        else:
            z=0
        api_id= input(b+' Telegram api_id_{}: '.format(z+1))
        api_hash= input(' Telegram api_hash_{}: '.format(z+1))
        with open('multi_log.txt', 'a') as f:
            f.write(api_id+'\n'+api_hash+'\n')
        client = TelegramClient("DarknetHack{}".format(z), api_id, api_hash)
        client.start()
        Sleep(1)
        clear()
        info()
        client.disconnect()
if os.path.isfile('multi_log.txt'):
    xc=input(b+" Oxirgi amalni olib tashlamowchimisiz "+a+" (h/y) ? ")
    if xc=='h':
        cy=input(" Koproq akaunt qoshmoqchimisiz "+a+" (h/y) ? ")
        if cy=='h':
            ospath()
        else:
            pass
    else:
        cv=input(" Oxirgi amalni olib tashlamoqchimisiz "+a+" (h/ny) ? ")
        if cv=='h':
            with open('multi_log.txt', 'r') as f:
                data = f.readlines()
            v=int((len(data))/2)
            con=input(r+" Oxirgi amal bn bogliq malumotlarni delete qilishga rozimisiz "+a+" (h/y) ? ")
            if con in ['', 'y']:
                print(m+" chiqilmoqda..."+'\n'+a+"hech qaysi fayl delete qilinmado ! ")
                sys.exit(1)
            elif con=='y':
                print(r+ " Endi ohirgi amalga bogliq bolgan fayllar delete qilinmoqda..")
                Sleep(1)
                for d in range(v-1):
                    os.remove("DarknetHack{}.session".format(d))
                os.remove('multi_log.txt')
            ospath()
        else:
            sys.exit()
else:
    ospath()

clear()
info()
x=0
inh=2
t=0
with open('multi_log.txt', 'r') as f:
    data = f.readlines()
v=int(len(data)/2)
for s in range(v):
    api_id = data[t]
    api_hash = data[t+1]
    print(a+ ' \nAkauntga ulanish kutilmoqda..  {} \n'.format(x+1)+y+ ' \n api {}= '.format(x+1) +m+ api_id +'\n' +y+ ' api hash {} = '.format(x+1) +m+ api_hash)
    Sleep(1)
    client = TelegramClient("DarknetHack{}".format(x), api_id, api_hash)
    client.start()
    name=utils.get_display_name(client.get_me())
    print(a+" \n\n   Akauntga ulandi {}\n\n".format(name))
    t+=2
    lines=[]
    chats = []
    last_date = None
    chunk_size = 200
    groups=[]
    result = client(GetDialogsRequest(
                 offset_date=last_date,
                 offset_id=0,
                 offset_peer=InputPeerEmpty(),
                 limit=chunk_size,
                 hash = 0
             ))
    chats.extend(result.chats)
    for chat in chats:
        try:
            if chat.megagroup==True:
                groups.append(chat)
        except:
            continue
    print(b+' Qaysi grupadan odam olmoqchisiz:')
    i=0
    for g in groups:
        print(m+str(i) +y+ ' - '+a + g.title)
        i+=1
    g_index = input(b+' Raqamni tanlang (Enter bosib otqazib yuborish): ')
    if g_index == '' :
        info()
        print(m+" Yahshi. Otqazib yuborildi...")
        Sleep(1)
    else:
        info()
        target_group=groups[int(g_index)]
        print(y+' Azolwr uhlatilmoqda...')
        all_participants = []
        all_participants = client.get_participants(target_group)
        print(y+' Hotiraga saqlanmoqda...')
        with open("Members.csv","w",encoding='UTF-8') as f:
            writer=csv.writer(f,delimiter=",",lineterminator="\n")
            for user in all_participants:
                if user.username:
                    username= user.username
                else:
                    username= ""
                if user.first_name:
                    first_name= user.first_name
                else:
                    first_name= ""
                if user.last_name:
                    last_name= user.last_name
                else:
                    last_name= ""
                name= (first_name + ' ' + last_name).strip()
                writer.writerow([username,user.id,user.access_hash,name,target_group.title, target_group.id])
        print(a+' Azolar muvafaqiyatli kochirildi.')
        Sleep(1)
    info()
    print(b+'Qayso guruhga kochirmoqchisiz:')
    i=0
    for group in groups:
        print(m+str(i) +y+ ' - ' +a+ group.title)
        i+=1
    g_index = input(b+' Raqamni kiriting: ')
    if g_index=='':
        print(m+" \n U 've Enter bosing va chiqing...")
        sys.exit()
    users = []
    with open('Members.csv', encoding='UTF-8') as f:
        rows = csv.reader(f,delimiter=",",lineterminator="\n")
        for row in rows:
            lines.append(row)
            user = {}
            user['username'] = row[0]
            user['id'] = int(row[1])
            user['name'] = row[3]
            users.append(user)
    my_participants = client.get_participants(groups[int(g_index)])
    target_group=groups[int(g_index)]

    target_group_entity = InputPeerChannel(target_group.id,target_group.access_hash)
    my_participants_id = []
    for my_participant in my_participants:
        my_participants_id.append(my_participant.id)
    info()
    n,q=0,0
    for user in users:
        usR=str(user['id'])
        n += 1
        if n % 20 == 0:
            info()
            print (y+' waiting for 10 seconds to avoid flooding....')
            Sleep(10)
        elif q>= 9:
            client.disconnect()
            if x<v:
                x+=1
                inh+=1
                break
            else:
                print(b+" Boshqa odamlar topilmadi...")
                Sleep(1)
                sys.exit()
        if user['id'] in my_participants_id:
            print(a+' Bu odan allaqachon bor ekan...')
            n-=1
            with open('Members.csv', 'r',encoding='UTF-8') as f:
                dat = csv.reader(f,delimiter=",",lineterminator="\n")
                for tad in dat:
                    if usR in tad:
                        lines.remove(tad)
                        break
            Sleep(1)
            continue
        else:
            try:
                print (a+' Qoshish {}'.format(user['name']))
                if True :
                    if user['username'] == "":
                        continue
                user_to_add = client.get_input_entity(user['username'])
                client(InviteToChannelRequest(target_group_entity,[user_to_add]))
                print(m+" 2-4 sekund kuting...")
                with open('Members.csv', 'r',encoding='UTF-8') as f:
                    dat = csv.reader(f,delimiter=",",lineterminator="\n")
                    for tad in dat:
                        if usR in tad:
                            lines.remove(tad)
                            break
                with open("Members.csv","w",encoding='UTF-8') as f:
                    writer=csv.writer(f,delimiter=",",lineterminator="\n")
                    for line in lines:
                        writer.writerow(line)

                time.sleep(random.randrange(2,4))

                q=0
            except PeerFloodError:
                print(r+' Afsusku Spamm ekansiz spammdan chiqishiz bilan qayta harakat qib koring yoki boshqa akaunt kiriting.')
                Sleep(1)
                q+= 1
            except UserPrivacyRestrictedError:
                print(r+' Bu user\'s Nastroykasidan Maxfiylikni yoqqan ekan qosha olmadik.')
                with open('Members.csv', 'r',encoding='UTF-8') as f:
                    dat = csv.reader(f,delimiter=",",lineterminator="\n")
                    for tad in dat:
                        if usR in tad:
                            lines.remove(tad)
                            break
                Sleep(1)
            except UserBotError:
                print(r+' Botlarni\'t Otqazib yuborish...')
                with open('Members.csv', 'r',encoding='UTF-8') as f:
                    dat = csv.reader(f,delimiter=",",lineterminator="\n")
                    for tad in dat:
                        if usR in tad:
                            lines.remove(tad)
                            break
            except InputUserDeactivatedError:
                print(r+' Belgilangan foydalanuvchi ochirildi. Otqazib yuborilmoqda...')
                with open('Members.csv', 'r',encoding='UTF-8') as f:
                    dat = csv.reader(f,delimiter=",",lineterminator="\n")
                    for tad in dat:
                        if usR in tad:
                            lines.remove(tad)
                            break
                Sleep(1)
            except UserChannelsTooMuchError:
                print(r+' Foydalanuvchi juda kop kanallar va guruhlarga wushilhan afsusku uni bu grupaga qushomemiz.')
                with open('Members.csv', 'r',encoding='UTF-8') as f:
                    dat = csv.reader(f,delimiter=",",lineterminator="\n")
                    for tad in dat:
                        if usR in tad:
                            lines.remove(tad)
                            break
                Sleep(1)
            except UserNotMutualContactError:
                print(r+' ozaro raqam otqazib yuborildi.')
                with open('Members.csv', 'r',encoding='UTF-8') as f:
                    dat = csv.reader(f,delimiter=",",lineterminator="\n")
                    for tad in dat:
                        if usR in tad:
                            lines.remove(tad)
                            break
                Sleep(1)
            except KeyboardInterrupt:
                i=0
                kst=["stop","continue","switch to next account"]
                for ks in kst:
                    print('\n'+m+ str(i) +y+ ' - ' +a+ ks)
                    i+=1
                keyb=int(input(y+" Raqam kiriting : "))
                if keyb==1:
                    print(a+" yahshi daom etamiz...")
                    Sleep(1)
                elif keyb==0:
                    print(y+" chiqilmoqda...")
                    sys.exit(1)
                else:
                    print(a+ " \n\nkeyingi akauntga otish kutulmoqda...\n\n")
                    x+=1
                    break
            except Exception as e:
                print(r+' Hatolik:', e)
                print('Davom etilmoqda...')
                q += 1
                Sleep(1)
                continue


from rest_framework import serializers
from api.models import OauthRecord

class OauthRecordSerializer(serializers.ModelSerializer):

    class Meta:
        model = OauthRecord
        exclude = ('id', 'user')

    def create(self, data):
        instance = OauthRecord.objects.create(
            user=data['user'],
            provider=data['provider'],
            uid=data['uid']
        )

        return instance


from selenium import webdriver
from selenium.webdriver.firefox.service import Service
from webdriver_manager.firefox import GeckoDriverManager
import requests

driver = webdriver.Firefox(service=Service(GeckoDriverManager().install()))

driver.get("https://en.wikipedia.org/wiki/SpaceX")

wikipedia_xpath="/html/body/div[3]/div[3]/div[5]/div[1]/table[1]/tbody/tr[9]/td/div/ul/li[1]/a[1]"
wikipedia_spacexCEO=driver.find_element("xpath",wikipedia_xpath).text
driver.quit()

query="""query{
  company {
    ceo
  }
}
"""
response=requests.post("https://api.spacex.land/graphql/", json={'query':query})
print(response.status_code)
api_spacexCEO=response.json()

print("CEO SpaceX menurut Wikipedia",wikipedia_spacexCEO)
print('CEO SpaceX menurut api.spacex.land/graphql',api_spacexCEO)
print("Apakah CEO SpaceX pada wikipedia sama dengan api.spacex.land ?")
print(wikipedia_spacexCEO==api_spacexCEO["data"]["company"]["ceo"])


from .backend import backend as bd
import numpy as np
import os
import time
import fdtd_1d as f
import matplotlib.pyplot as plt
from .constants import c0, BLUE, CYAN, TEAL, ORANGE, RED, MAGENTA, GREY
from werkzeug.utils import cached_property
from multiprocessing import Pool

color_spec = [BLUE, CYAN, TEAL, ORANGE, RED, MAGENTA, GREY]

# Implementation of a dielectric slab
def theory_dielectric_slab_complex(grid):
    d = ((grid.materials[0].position[-1] - grid.materials[0].position[0] + 1)) * grid.dx
    omega = grid.sources[0].omega
    n_real = np.sqrt((np.abs(grid.materials[0].epsilon_complex(omega)) + grid.materials[0].epsilon_real(omega)) / 2)
    kappa = np.sqrt((np.abs(grid.materials[0].epsilon_complex(omega)) - grid.materials[0].epsilon_real(omega)) / 2)
    n = n_real + 1j*kappa
    k0 = grid.sources[0].omega / c0
    k = n*k0
    q = ((n - 1) ** 2) / ((n + 1) ** 2) * np.exp(2j * k * d)
    e_inc = grid.sources[0].ampl
    e_tr = e_inc * (2 / (n + 1)) * (2 * n / (n + 1)) * (1 / (1 - q)) * np.exp(1j * (k - k0) * d)
    theo_amplitude = np.abs(e_tr)
    theo_phasenunterschied = np.angle(e_tr)
    return theo_amplitude, theo_phasenunterschied

def theory_dielectric_slab(grid):
    d = ((grid.materials[0].position[-1] - grid.materials[0].position[0] + 1)) * grid.dx
    n = np.sqrt(grid.eps[grid.materials[0].position[1]])
    k0 = grid.sources[0].omega / c0
    k = n * k0
    q = ((n - 1) ** 2) / ((n + 1) ** 2) * np.exp(2j * k * d)
    e_inc = grid.sources[0].ampl
    e_tr = e_inc * (2 / (n + 1)) * (2 * n / (n + 1)) * (1 / (1 - q)) * np.exp(1j * (k - k0) * d)
    theo_amplitude = np.abs(e_tr)
    theo_phasenunterschied = np.angle(e_tr)
    return theo_amplitude, theo_phasenunterschied

class benchmark:
    '''Parent class for all benchmarks. Declares path/directory to store data'''

    def __init__(self, name, benchmark_type):
        self.name = name
        self.benchmark_type = benchmark_type
        self.dir_path = None
        self.grids = []

    def allocate_directory(self):

        self.dir_path = os.path.join(os.path.dirname(__file__), 'saved_data/'+self.benchmark_type+'/'+self.name)
        os.mkdir(path=self.dir_path)

    def store_obs_data(self):
        self.allocate_directory()
        for grid in self.grids:
            grid.store_obs_data(benchmark=True, benchmark_name=self.name)

class Harmonic_Slab_Lorentz_Setup(benchmark):

    def __init__(self,  name, dx, length_grid_in_dx, length_media_in_dx, start_index_media, wavelength, ampl, conductivity, eps_inf, gamma, w0, chi_1, chi_2, chi_3, timesteps, courant=1):
        super().__init__(name=name, benchmark_type='harmonic_lorentz_slab')
        self.start_media = start_index_media
        self.dx = dx
        self.indices = []
        self.grids = []

        for grid in range(len(self.dx)):
            self.indices.append([start_index_media + 2 + i for i in np.arange(0, length_media_in_dx[grid] - 1)])
            self.grids.append('grid_'+str(grid))

        self.eps_inf = eps_inf
        self.Nx = length_grid_in_dx
        self.length_media = np.array(length_media_in_dx)
        self.lamb = wavelength
        self.timesteps = timesteps
        self.courant = courant
        self.N_lambda = np.zeros(len(self.dx))                                                             #[[] for _ in range(len(self.dx))]
        self.ampl = ampl
        self.conductivity = conductivity
        self.gamma = gamma
        self.w0 = w0

        self.chi_1 = chi_1
        self.chi_2 = chi_2
        self.chi_3 = chi_3

        self.eps_real = None
        self.eps_imag = None
        self.eps_complex = None
        self.n_real = None

        self.wo_phase_merged = np.zeros(len(self.dx))
        self.theo_phasenunterschied_merged = np.zeros(shape=(len(self.dx), np.max(self.length_media)-1))
        self.theo_amplitude_merged = np.zeros(shape=(len(self.dx), np.max(self.length_media)-1))
        self.exp_phase_merged = np.zeros(shape=(len(self.dx), np.max(self.length_media)-1))
        self.exp_amplitude_merged = np.zeros(shape=(len(self.dx), np.max(self.length_media)-1))

    def _grid_wo_slab(self):
        position_src = self.start_media - 1
        for grid in range(len(self.dx)):
            position_obs = self.Nx[grid] - 3
            end_mur = self.Nx[grid] - 1
            wo_grid = f.Grid(self.Nx[grid], dx=self.dx[grid], courant=self.courant)
            if wo_grid.courant == 1:
                wo_grid[position_src] = f.ActivatedSinus(name='SinsquaredActivated', wavelength=self.lamb,
                                                         carrier_wavelength=(self.lamb * 30), phase_shift=0,
                                                         amplitude=self.ampl, tfsf=True)
            else:
                wo_grid[position_src] = f.ActivatedSinus(name='SinsquaredActivated', wavelength=self.lamb,
                                                         carrier_wavelength=(self.lamb * 30), phase_shift=0,
                                                         amplitude=self.ampl, tfsf=False)
            wo_grid[position_obs] = f.QuasiHarmonicObserver(name='firstobserver', first_timestep=self.timesteps[grid]-200)

            if wo_grid.courant == 0.5:
                wo_grid[0] = f.LeftSideGridBoundary()
                wo_grid[end_mur] = f.RightSideGridBoundary()
            else:
                wo_grid[0] = f.LeftSideMur()
                wo_grid[end_mur] = f.RightSideMur()

            wo_grid.run_timesteps(self.timesteps[grid], vis=False)
            self.wo_phase_merged[grid] = wo_grid.local_observers[0].phase

    def _grids_w_slab(self):
        position_src = self.start_media - 1
        for grid in range(len(self.dx)):
            position_obs = self.Nx[grid] - 3
            end_mur = self.Nx[grid] - 1
            for ind_media, ind_array in zip(self.indices[grid], range(self.length_media[grid])):
                # Step 1: init grid
                w_grid = 'slab' + str(ind_media)
                w_grid = f.Grid(nx=self.Nx[grid], dx=self.dx[grid], courant=self.courant)

                # Step 2: init media
                w_grid[self.start_media:ind_media] = f.LorentzMedium(name='media', permeability=1, eps_inf=self.eps_inf, conductivity=self.conductivity, gamma=self.gamma, chi_1=self.chi_1, chi_2=self.chi_2, chi_3=self.chi_3, w0=self.w0)

                # Step 3: init source
                if w_grid.courant == 1:
                    w_grid[position_src] = f.ActivatedSinus(name='SinsquaredActivated', wavelength=self.lamb,
                                                             carrier_wavelength=(self.lamb * 30), phase_shift=0,
                                                             amplitude=self.ampl, tfsf=True)
                else:
                    w_grid[position_src] = f.ActivatedSinus(name='SinsquaredActivated', wavelength=self.lamb,
                                                             carrier_wavelength=(self.lamb * 30), phase_shift=0,
                                                             amplitude=self.ampl, tfsf=False)

                # Step 4: add observer
                w_grid[position_obs] = f.QuasiHarmonicObserver(name='firstobserver', first_timestep=self.timesteps[grid]-200)

                # Step 5: add boundaries
                if w_grid.courant == 0.5:
                    w_grid[0] = f.LeftSideGridBoundary()
                    w_grid[end_mur] = f.RightSideGridBoundary()
                else:
                    w_grid[0] = f.LeftSideMur()
                    w_grid[end_mur] = f.RightSideMur()

                # Step 6: run simulation
                w_grid.run_timesteps(timesteps=self.timesteps[grid], vis=False)

                # Step 7: misc
                self.exp_amplitude_merged[grid][ind_array] = w_grid.local_observers[0].amplitude
                self.exp_phase_merged[grid][ind_array] = w_grid.local_observers[0].phase
                self.theo_amplitude_merged[grid][ind_array] = theory_dielectric_slab_complex(w_grid)[0]
                self.theo_phasenunterschied_merged[grid][ind_array] = theory_dielectric_slab_complex(w_grid)[1]

                # if list self.eps_real is empty:
                if self.eps_real is None:
                    self.eps_real = w_grid.materials[0].epsilon_real(w_grid.sources[0].omega)
                    self.eps_imag = w_grid.materials[0].epsilon_imag(w_grid.sources[0].omega)
                    self.eps_complex = w_grid.materials[0].epsilon_complex(w_grid.sources[0].omega)
                    self.n_real = np.sqrt((np.abs(self.eps_complex) + self.eps_real)/2)

    def _visualize(self):
        max_resolution_grid = int(np.argmax(self.N_lambda))

        fig, axes = plt.subplots(2, 2)
        axes[0][0].grid(True, linestyle=(0, (1, 5)), color=GREY, linewidth=1)
        axes[0][1].grid(True, linestyle=(0, (1, 5)), color=GREY, linewidth=1)
        axes[1][0].grid(True, linestyle=(0, (1, 5)), color=GREY, linewidth=1)
        axes[1][1].grid(True, linestyle=(0, (1, 5)), color=GREY, linewidth=1)
        axes[0][0].set_xlabel('width in m', fontsize=14)
        axes[0][1].set_xlabel('width in m', fontsize=14)
        axes[1][0].set_xlabel('width in m', fontsize=14)
        axes[1][1].set_xlabel('width in m', fontsize=14)
        axes[0][0].set_ylabel('transmitted amplitude ' + r'$E_{z,tr}$', fontsize=14)
        axes[1][0].set_ylabel('Phasenunterschied', fontsize=14)
        axes[1][1].set_ylabel(r'$d(\phi_{exp},\phi_{theo})$', fontsize=14)
        axes[0][1].set_ylabel(r'$E_{tr,theo}$ / $E_{tr,FDTD}$', fontsize=14)

        axes[0][0].plot(np.array((np.array(self.indices[max_resolution_grid])) - self.start_media) * self.dx[max_resolution_grid],
                        self.theo_amplitude_merged[max_resolution_grid][0:self.length_media[max_resolution_grid] - 1], label='Theorie', color=ORANGE)
        axes[1][0].plot(np.array((np.array(self.indices[max_resolution_grid])) - self.start_media) * self.dx[max_resolution_grid],
                        self.theo_phasenunterschied_merged[max_resolution_grid][0:self.length_media[max_resolution_grid] - 1], color=ORANGE,
                        label='Theorie')

        for grid in range(len(self.dx)):
            axes[0][0].plot(np.array((np.array(self.indices[grid])) - self.start_media) * self.dx[grid], self.exp_amplitude_merged[grid][0:self.length_media[grid] - 1], color=color_spec[grid], linestyle='dashed', label=r'$N_{\lambda}=$' + '{0:.3}'.format(self.N_lambda[grid]))
            axes[0][1].plot(np.array((np.array(self.indices[grid])) - self.start_media) * self.dx[grid], self.theo_amplitude_merged[grid][0:self.length_media[grid] - 1]/self.exp_amplitude_merged[grid][0:self.length_media[grid] - 1], color=color_spec[grid], linestyle='dashed', label=r'$N_{\lambda}=$' + '{0:.3}'.format(self.N_lambda[grid]))
            axes[1][0].plot(np.array((np.array(self.indices[grid])) - self.start_media) * self.dx[grid], self.get_exp_phasedifference[grid][0:self.length_media[grid] - 1], color= color_spec[grid], linestyle='dashed', label=r'$N_{\lambda}=$' + '{0:.3}'.format(self.N_lambda[grid]))
            axes[1][1].plot(np.array((np.array(self.indices[grid])) - self.start_media) * self.dx[grid], np.abs(self.get_exp_phasedifference[grid][0:self.length_media[grid] - 1] - self.theo_phasenunterschied_merged[grid][0:self.length_media[grid] - 1]), color=color_spec[grid], label=r'$N_{\lambda}=$' + '{0:.3}'.format(self.N_lambda[grid]))

        axes[0][0].legend(loc='best')
        axes[0][1].legend(loc='best')
        axes[1][0].legend(loc='best')
        axes[1][1].legend(loc='best')

        plt.show()

    @cached_property
    def get_exp_phasedifference(self):
        phase_diff = np.zeros(shape=(len(self.dx), np.max(self.length_media)-1))
        for grid in range(len(self.dx)):
            phase_diff[grid][0:self.length_media[grid] - 1] = -np.array(self.exp_phase_merged[grid][0:self.length_media[grid] - 1]) + self.wo_phase_merged[grid]
            mask = (phase_diff[grid][:]) > np.pi
            phase_diff[grid][mask] -= 2*np.pi
        return phase_diff

    def _set_N_lambda(self):
        for grid in range(len(self.dx)):
            self.N_lambda[grid] = self.lamb/(self.dx[grid]*self.n_real)

    def store_obs_data(self):
        self.allocate_directory()
        file_grid_informations = os.path.join(self.dir_path, 'info.npy')
        file_width_in_dx = os.path.join(self.dir_path, 'width.npy')
        file_theory_ampl = os.path.join(self.dir_path, 'theory_ampl.npy')
        file_theory_phase = os.path.join(self.dir_path, 'theory_phase.npy')
        file_exp_ampl = os.path.join(self.dir_path, 'exp_ampl.npy')
        file_exp_phase = os.path.join(self.dir_path, 'exp_phase.npy')

        grid_informations = np.zeros(shape=(len(self.grids), 4))
        width_in_dx = np.zeros(shape=(len(self.grids), np.max(self.length_media)))
        for grid in range(len(self.grids)):
            width_in_dx[grid][0:self.length_media[grid] - 1] = np.array(self.indices[grid]) - self.start_media
            grid_informations[grid][0] = self.dx[grid]
            grid_informations[grid][1] = self.timesteps[grid]
            grid_informations[grid][2] = self.N_lambda[grid]
            grid_informations[grid][3] = self.length_media[grid]

        np.save(file_grid_informations, arr=grid_informations)
        np.save(file_width_in_dx, arr=width_in_dx)
        np.save(file_theory_ampl, arr=self.theo_amplitude_merged)
        np.save(file_theory_phase, arr=self.theo_phasenunterschied_merged)
        np.save(file_exp_ampl, arr=self.exp_amplitude_merged)
        np.save(file_exp_phase, arr=self.get_exp_phasedifference)

    def run_benchmark(self):
        start_time = time.time()
        self._grid_wo_slab()
        self._grids_w_slab()
        self._set_N_lambda()
        print("computed in --- %s seconds ---" % (time.time() - start_time))
        self._visualize()

class QPM_Length_multiple_chi_2(benchmark):
    def __init__(self, number_of_lambdas, timesteps, name, peak_timestep, pulse_duration, number_of_distributed_observer, chi2, multi=True):
        super().__init__(name=name, benchmark_type='qpm_harmonic_length_multiple')
        self.no_of_lambdas = number_of_lambdas
        self.half_qpm = 737
        self.chi_2 = chi2
        self.multi = multi
        self.dx = 4e-09
        self.dt = 0.5*self.dx/c0
        self.timesteps = timesteps
        self.nx = self.no_of_lambdas * (2*self.half_qpm + 1) + 10           # 10 is just a buffer to place boundarys and src
        self.start_media = 5
        self.ending_indices = np.array([self.start_media + i*self.half_qpm for i in range(self.no_of_lambdas*2 + 1)])
        self.peak_timestep = peak_timestep
        self.pulse_duration = pulse_duration
        self.no_observer = number_of_distributed_observer
        self.obs_distance = (self.nx - 10)//self.no_observer
        self.obs_positions = np.array([5 + i*self.obs_distance for i in range(self.no_observer)])

    def _allocate_memory(self):
        self.grid_information = np.array([self.dt, self.dx, self.no_of_lambdas, self.peak_timestep, self.pulse_duration])
        self.relative_observer_pos = self.obs_positions - self.start_media

    def store_obs_data(self):
        start_time = time.time()
        self.allocate_directory()
        self._allocate_memory()

        file_grid_info = os.path.join(self.dir_path, 'info.npy')
        file_relative_pos = os.path.join(self.dir_path, 'relative_ind.npy')
        file_data = os.path.join(self.dir_path, 'E_data.npy')
        file_chi_info = os.path.join(self.dir_path, 'chi2.npy')

        np.save(file_grid_info, arr=self.grid_information)
        np.save(file_relative_pos, arr=self.relative_observer_pos)
        np.save(file_data, arr=self.observed_data)
        np.save(file_chi_info, arr=self.chi_2)
        print("saved in --- %s seconds ---" % (time.time() - start_time))

    def _create_grids(self, chi2):
        print('process initiated')
        qpm_grid = f.Grid(nx=self.nx, dx=self.dx, benchmark='qpm_harmonic_length_multiple', courant=0.5)

        for indices in range(len(self.ending_indices) - 1):
            if indices % 2 == 0:
                    qpm_grid[self.ending_indices[indices]:self.ending_indices[indices + 1]] = f.LorentzMedium(
                        name='Varin', permeability=1, eps_inf=1.0, chi_1=[2.42, 9.65, 1.46], chi_2=[30.e-12, 0, 0],
                        chi_3=[0, 0, 0], conductivity=0, w0=[1.5494e16, 9.776e13, 7.9514e15], gamma=[0, 0, 0])

            else:
                    qpm_grid[self.ending_indices[indices]:self.ending_indices[indices + 1]] = f.LorentzMedium(
                        name='Varin', permeability=1, eps_inf=1.0, chi_1=[2.42, 9.65, 1.46], chi_2=[chi2, 0, 0],
                        chi_3=[0, 0, 0], conductivity=0, w0=[1.5494e16, 9.776e13, 7.9514e15], gamma=[0, 0, 0])

        qpm_grid[3] = f.GaussianImpulseWithFrequency(name='Varin', Intensity=5*10e12, wavelength=1.064e-06, pulse_duration=self.pulse_duration, peak_timestep=self.peak_timestep, tfsf=False)

        for pos in self.obs_positions:
            qpm_grid[int(pos)] = f.E_FFTObserver(name='Varin', first_timestep=0, second_timestep=self.timesteps - 1)

        qpm_grid[0] = f.LeftSideGridBoundary()
        qpm_grid[self.nx - 1] = f.RightSideGridBoundary()

        # step 6: run simulation
        qpm_grid.run_timesteps(timesteps=self.timesteps, vis=False)

        observed_grid_data = bd.zeros(shape=(self.no_observer, self.timesteps))
        for obs in range(self.no_observer):
            observed_grid_data[obs] = qpm_grid.local_observers[obs].observed_E

        return observed_grid_data

    def run_benchmark(self):
        start_time = time.time()
        processpool = Pool()
        self.observed_data = bd.zeros(shape=(len(self.chi_2), self.no_observer, self.timesteps))
        if self.multi:
            self.observed_data = bd.stack(processpool.map(self._create_grids, self.chi_2))
        else:
            for ind_chi, chi in zip(range(len(self.chi_2)), self.chi_2):
                self.observed_data[ind_chi] = self._create_grids(self.chi_2[ind_chi])
        print("computed in --- %s seconds ---" % (time.time() - start_time))

class QPM_Length(benchmark):

    def __init__(self, number_of_lambdas, timesteps, name, peak_timestep, pulse_duration, number_of_distributed_observer):
        super().__init__(name=name, benchmark_type='qpm_harmonic_length')
        self.no_of_lambdas = number_of_lambdas
        self.half_qpm = 737
        self.dx = 4e-09
        self.timesteps = timesteps
        self.nx = self.no_of_lambdas * (2*self.half_qpm + 1) + 10           # 10 is just a buffer to place boundarys and src
        self.start_media = 5
        self.ending_indices = np.array([self.start_media + i*self.half_qpm for i in range(self.no_of_lambdas*2 + 1)])
        self.peak_timestep = peak_timestep
        self.pulse_duration = pulse_duration
        self.no_observer = number_of_distributed_observer
        self.obs_distance = (self.nx - 10)//self.no_observer
        self.obs_positions = np.array([5 + i*self.obs_distance for i in range(self.no_observer)])

    def _create_grid(self):
        qpm_grid = f.Grid(nx=self.nx, dx=self.dx, benchmark='qpm_harmonic_length', courant=0.5)
        self.grids.append(qpm_grid)

        for indices in range(len(self.ending_indices) - 1):
            if indices % 2 == 0:
                    qpm_grid[self.ending_indices[indices]:self.ending_indices[indices + 1]] = f.LorentzMedium(
                        name='Varin', permeability=1, eps_inf=1.0, chi_1=[2.42, 9.65, 1.46], chi_2=[30.e-12, 0, 0],
                        chi_3=[0, 0, 0], conductivity=0, w0=[1.5494e16, 9.776e13, 7.9514e15], gamma=[0, 0, 0])

            else:
                    qpm_grid[self.ending_indices[indices]:self.ending_indices[indices + 1]] = f.LorentzMedium(
                        name='Varin', permeability=1, eps_inf=1.0, chi_1=[2.42, 9.65, 1.46], chi_2=[-30.e-12, 0, 0],
                        chi_3=[0, 0, 0], conductivity=0, w0=[1.5494e16, 9.776e13, 7.9514e15], gamma=[0, 0, 0])

        qpm_grid[3] = f.GaussianImpulseWithFrequency(name='Varin', Intensity=5*10e12, wavelength=1.064e-06, pulse_duration=self.pulse_duration, peak_timestep=self.peak_timestep, tfsf=False)

        for pos in self.obs_positions:
            qpm_grid[int(pos)] = f.E_FFTObserver(name='Varin', first_timestep=0, second_timestep=self.timesteps - 1)

        qpm_grid[0] = f.LeftSideGridBoundary()
        qpm_grid[self.nx - 1] = f.RightSideGridBoundary()

        #qpm_grid[0] = f.LeftSideMur()
        #qpm_grid[self.nx - 1] = f.RightSideMur()

        # step 6: run simulation
        qpm_grid.run_timesteps(timesteps=self.timesteps, vis=False)

    def _create_grid_mono(self):
        qpm_grid_mono = f.Grid(nx=self.nx, dx=self.dx, benchmark='qpm_harmonic_length', courant=0.5)
        self.grids.append(qpm_grid_mono)

        for indices in range(len(self.ending_indices) - 1):
            if indices % 2 == 0:
                    qpm_grid_mono[self.ending_indices[indices]:self.ending_indices[indices + 1]] = f.LorentzMedium(
                        name='Varin', permeability=1, eps_inf=1.0, chi_1=[2.42, 9.65, 1.46], chi_2=[30.e-12, 0, 0],
                        chi_3=[0, 0, 0], conductivity=0, w0=[1.5494e16, 9.776e13, 7.9514e15], gamma=[0, 0, 0])

            else:
                    qpm_grid_mono[self.ending_indices[indices]:self.ending_indices[indices + 1]] = f.LorentzMedium(
                        name='Varin', permeability=1, eps_inf=1.0, chi_1=[2.42, 9.65, 1.46], chi_2=[30.e-12, 0, 0],
                        chi_3=[0, 0, 0], conductivity=0, w0=[1.5494e16, 9.776e13, 7.9514e15], gamma=[0, 0, 0])

        qpm_grid_mono[3] = f.GaussianImpulseWithFrequency(name='Varin', Intensity=5*10e12, wavelength=1.064e-06, pulse_duration=self.pulse_duration, peak_timestep=self.peak_timestep, tfsf=False)

        for pos in self.obs_positions:
            qpm_grid_mono[int(pos)] = f.E_FFTObserver(name='Varin', first_timestep=0, second_timestep=self.timesteps - 1)

        qpm_grid_mono[0] = f.LeftSideGridBoundary()
        qpm_grid_mono[self.nx - 1] = f.RightSideGridBoundary()

        #qpm_grid[0] = f.LeftSideMur()
        #qpm_grid[self.nx - 1] = f.RightSideMur()

        # step 6: run simulation
        qpm_grid_mono.run_timesteps(timesteps=self.timesteps, vis=False)

    def _allocate_memory(self):
        self.observed_data = np.zeros(shape=(self.no_observer, self.timesteps))
        self.observed_data_mono = np.zeros(shape=(self.no_observer, self.timesteps))
        observer_object_list = np.array(self.grids[0].local_observers)
        observer_object_list_mono = np.array(self.grids[1].local_observers)
        for obs_ind in range(self.no_observer):
            self.observed_data[obs_ind][:] = observer_object_list[obs_ind].observed_E[:]
            self.observed_data_mono[obs_ind][:] = observer_object_list_mono[obs_ind].observed_E[:]
        self.grid_information = np.array([self.grids[0].dt, self.grids[0].dx, self.no_of_lambdas, self.peak_timestep, self.pulse_duration])
        self.relative_observer_pos = self.obs_positions - self.start_media

    def store_obs_data(self):
        start_time = time.time()
        self.allocate_directory()
        self._allocate_memory()

        file_grid_info = os.path.join(self.dir_path, 'info.npy')
        file_relative_pos = os.path.join(self.dir_path, 'relative_ind.npy')
        file_data = os.path.join(self.dir_path, 'E_data.npy')
        file_data_mono = os.path.join(self.dir_path, 'E_data_mono.npy')

        np.save(file_grid_info, arr=self.grid_information)
        np.save(file_relative_pos, arr=self.relative_observer_pos)
        np.save(file_data, arr=self.observed_data)
        np.save(file_data_mono, arr=self.observed_data_mono)
        print("saved in --- %s seconds ---" % (time.time() - start_time))

    def run_benchmark(self):
        start_time = time.time()
        self._create_grid()
        self._create_grid_mono()
        print("computed in --- %s seconds ---" % (time.time() - start_time))

class QPM_end_P(benchmark):
    ''' reproduces paper QuasiPhaseMatching from Varin's Paper '''

    def __init__(self, number_of_lambdas, timesteps, name, peak_timestep, pulse_duration):
        super().__init__(name=name, benchmark_type='qpm_harmonic')
        self.no_of_lambdas = number_of_lambdas
        # Varin parameters
        self.half_qpm = 737
        self.dx = 4e-09
        self.timesteps = timesteps
        self.nx = number_of_lambdas * (2*self.half_qpm + 1) + 10
        self.start_media = 5
        self.ending_indices = np.array([self.start_media + i*self.half_qpm for i in range(self.no_of_lambdas*2 + 1)])
        self.peak_timestep = peak_timestep
        self.pulse_duration = pulse_duration

    def _create_grid(self):
        self.position_P_obs = self.nx - 9
        self.position_E_obs = self.nx - 8

        # step 1: init grid
        qpm_grid = f.Grid(nx=self.nx, dx=self.dx, benchmark='qpm_harmonic', courant=0.5)
        self.grids.append(qpm_grid)

        for indices in range(len(self.ending_indices) - 1):
            if indices % 2 == 0:
                qpm_grid[self.ending_indices[indices]:self.ending_indices[indices + 1]] = f.LorentzMedium(
                    name='Varin', permeability=1, eps_inf=1.0, chi_1=[2.42, 9.65, 1.46], chi_2=[30.e-12, 0, 0],
                    chi_3=[0, 0, 0], conductivity=0, w0=[1.5494e16, 9.776e13, 7.9514e15], gamma=[0, 0, 0])

            else:
                qpm_grid[self.ending_indices[indices]:self.ending_indices[indices + 1]] = f.LorentzMedium(
                    name='Varin', permeability=1, eps_inf=1.0, chi_1=[2.42, 9.65, 1.46], chi_2=[-30.e-12, 0, 0],
                    chi_3=[0, 0, 0], conductivity=0, w0=[1.5494e16, 9.776e13, 7.9514e15], gamma=[0, 0, 0])

        qpm_grid[3] = f.GaussianImpulseWithFrequency(name='Varin', Intensity=10e12, wavelength=1.064e-06,
                                                     pulse_duration=self.pulse_duration,
                                                     peak_timestep=self.peak_timestep, tfsf=True)

        qpm_grid[self.position_E_obs] = f.E_FFTObserver(name='Varin', first_timestep=0, second_timestep=self.timesteps - 1)
        qpm_grid[self.position_P_obs] = f.P_FFTObserver(name='Varin', first_timestep=0, second_timestep=self.timesteps - 1)

        qpm_grid[0] = f.LeftSideGridBoundary()
        qpm_grid[self.nx - 1] = f.RightSideGridBoundary()

        #qpm_grid[0] = f.LeftSideMur()
        #qpm_grid[self.nx - 1] = f.RightSideMur()

        # step 6: run simulation
        qpm_grid.run_timesteps(timesteps=self.timesteps, vis=True)

    def _allocate_memory(self):
        self.observed_data = np.zeros(shape=(2, self.timesteps))
        self.observed_data[0][:] = self.grids[0].local_observers[0].observed_E[:]
        self.observed_data[1][:] = self.grids[0].local_observers[1].observed_P[:]
        self.grid_information = np.array([self.grids[0].dt, self.grids[0].dx, self.no_of_lambdas, self.peak_timestep,
                                          self.pulse_duration])

    def store_obs_data(self):
        start_time = time.time()
        self.allocate_directory()
        self._allocate_memory()

        self.relative_observer_pos = np.array([self.position_E_obs - self.start_media, self.position_P_obs - self.start_media])

        file_grid_info = os.path.join(self.dir_path, 'info.npy')
        file_relative_pos = os.path.join(self.dir_path, 'relative_ind.npy')
        file_data = os.path.join(self.dir_path, 'E_P_data.npy')

        np.save(file_grid_info, arr=self.grid_information)
        np.save(file_relative_pos, arr=self.relative_observer_pos)
        np.save(file_data, arr=self.observed_data)
        print("saved in --- %s seconds ---" % (time.time() - start_time))

    def run_benchmark(self):
        start_time = time.time()
        self._create_grid()
        print("computed in --- %s seconds ---" % (time.time() - start_time))

class Soliton(benchmark):

    ''' tries to show the majestic combined effect of GVD and SPM '''

    def __init__(self, name, central_wavelength, pulse_duration, intensities, x_to_snapshot, peak_timestep, frame_width_in_dx, dx, multi=True):
        super().__init__(name=name, benchmark_type='soliton')
        self.central_wavelength = central_wavelength
        self.pulse_duration = pulse_duration
        self.intensities = np.array(intensities)
        self.x_to_snapshot = x_to_snapshot
        self.peak_timestep = peak_timestep
        self.name = name
        self.dx = dx
        self.nx = int(x_to_snapshot[-1]/self.dx) + 5020
        self.frame_width = frame_width_in_dx
        self.multi = multi

    def _allocate_memory(self):
        self.grid_information = np.array([self.peak_timestep, self.pulse_duration, self.dx, self.frame_width])
        self.used_propagations = np.array(self.x_to_snapshot)
        self.used_intensities = np.array(self.intensities)

    def _create_grids(self, intensity):
        soliton_grid = f.Grid(self.nx, dx=self.dx, courant=0.5) #12mm 550000

        # Step 1: init media
        soliton_grid[5:self.nx-10] = f.CentroRamanMedium(name='Varin', chi_1=[0.69617, 0.40794, 0.89748], w0=[2.7537e16, 1.6205e16, 1.9034e14], chi_3=[1.94e-22, 0, 0], alpha=[0.7, 0, 0], wr=[8.7722e13, 0, 0], gamma_K=[0, 0, 0], gamma_R=[3.1250e13, 0, 0], permeability=1, conductivity=0, eps_inf=1)

        # Step 2: init src
        soliton_grid[3] = f.SechEnveloped(name='Varin', wavelength=1.5e-06, pulse_duration=self.pulse_duration, Intensity=intensity, peak_timestep=self.peak_timestep, tfsf=False)

        # Step 3: init frame
        soliton_grid[5:(6+self.frame_width)] = f.MovingFrame(x_to_snapshot=self.x_to_snapshot, central_wavelength=self.central_wavelength)

        # Step 4: init boundaries
        soliton_grid[0] = f.LeftSideGridBoundary()
        soliton_grid[self.nx - 1] = f.RightSideGridBoundary()

        soliton_grid.local_observers[0]._allocate_memory()
        timesteps = soliton_grid.local_observers[0].timesteps_to_store[-1] + 5000

        # Step 5: start benchmark
        soliton_grid.run_timesteps(timesteps, vis=False)

        # due to multiprocessing it is more convenient to store data by process (nobody loves waiting)
        observed_process_data = soliton_grid.local_observers[0].stored_data
        return observed_process_data

    def store_obs_data(self):
        start_time = time.time()
        self._allocate_memory()
        self.allocate_directory()

        file_grid_info = os.path.join(self.dir_path, 'info.npy')
        file_observed_data = os.path.join(self.dir_path, 'Int_Pos_E.npy')
        file_used_propagations = os.path.join(self.dir_path, 'propagations.npy')
        file_used_intensities = os.path.join(self.dir_path, 'intensities.npy')

        np.save(file_grid_info, arr=self.grid_information)
        np.save(file_used_intensities, arr=self.used_intensities)
        np.save(file_observed_data, arr=self.observed_data)
        np.save(file_used_propagations, arr=self.used_propagations)
        print("stored in --- %s seconds ---" % (time.time() - start_time))

    def run_benchmark(self):
        start_time = time.time()
        if self.multi:
            processpool = Pool()
            self.observed_data = np.array(processpool.map(self._create_grids, self.intensities))
        else:
            self.observed_data = np.zeros(shape=(np.size(self.intensities), len(self.x_to_snapshot), self.frame_width + 1))
            for intensity_index, intensity in zip(range(np.size(self.intensities)), self.intensities):
                self.observed_data[intensity_index] = self._create_grids(intensity)
        print("computed in --- %s seconds ---" % (time.time() - start_time))

class TiO2_Si02_Dielectric_Mirror_Setup:

    def __init__(self, N_lambda_media, wavelength_guided_for, wavelength, ampl, timesteps, number_of_layer_pairs, vary_layers=False, vary_inc_wavelength=False):
        self.N_lambda = N_lambda_media
        self.layer_number = [0 + i for i in range(number_of_layer_pairs)]
        self.number_of_layer = number_of_layer_pairs
        self.ampl = ampl
        self.timesteps = timesteps
        self.lamb = wavelength
        self.lamb_guided = wavelength_guided_for
        self.vary_layers = vary_layers
        self.vary_lambda = vary_inc_wavelength
        self.ti_n = 2.519                                                               # refractiveindex.info
        self.si_n = 1.453                                                               # refractiveindex.info
        self.dx = self.lamb/(self.ti_n * self.N_lambda)
        self.d_ti = int(self.lamb_guided / (self.ti_n * 4 * self.dx))                   # huge problem
        self.d_si = int(self.lamb_guided / (self.si_n * 4 * self.dx))                   # huge problem
        self.Nx = self.number_of_layer*(self.d_si + self.d_ti) + 14
        self.starting_locations_ti = [8 + i*(self.d_ti + self.d_si) for i in self.layer_number]
        self.starting_locations_si = np.array(self.starting_locations_ti) + self.d_ti
        self.position_src = 6
        self.position_obs = 3
        self.incident_wavelengths = [self.lamb + (i / 101) * self.lamb_guided for i in np.arange(0, 101, 1)]
        self.refl_ampl = []
        self.theory_R = []

    def _construct_non_vary_grid(self):
        end_mur = self.Nx - 1
        grid_non_vary = f.Grid(dx=self.dx, nx=self.Nx)
        grid_non_vary[0] = f.LeftSideMur()
        grid_non_vary[end_mur] = f.RightSideMur()
        grid_non_vary[self.position_src] = f.ActivatedSinus(name='Laser', wavelength=self.lamb, carrier_wavelength=6000e-09, tfsf=True, amplitude=self.ampl, phase_shift=0)
        grid_non_vary[self.position_obs] = f.QuasiHarmonicObserver(name='Observer', first_timestep=self.timesteps - 200)

        for layer_pair in self.layer_number:
            ti_start = self.starting_locations_ti[layer_pair]
            si_start = self.starting_locations_si[layer_pair]
            grid_non_vary[ti_start:si_start] = f.NonDispersiveMedia('TiO2', permittivity=self.ti_n**2, permeability=1, conductivity=0)
            grid_non_vary[si_start:(si_start+self.d_si)] = f.NonDispersiveMedia('SiO2', permittivity=self.si_n**2, permeability=1, conductivity=0)

        grid_non_vary.run_timesteps(self.timesteps)
        self.refl_ampl.append(grid_non_vary.local_observers[0].amplitude)

    def _construct_vary_grid(self):
        for layer in self.layer_number:
            Nx = (layer + 1) * (self.d_si + self.d_ti) + 12
            end_mur = Nx - 1
            grid_vary = f.Grid(dx=self.dx, nx=Nx)
            grid_vary[end_mur] = f.RightSideMur()
            grid_vary[0] = f.LeftSideMur()
            grid_vary[self.position_obs] = f.QuasiHarmonicObserver(name='Observer', first_timestep=self.timesteps - 300)
            grid_vary[self.position_src] = f.ActivatedSinus(name='Laser', wavelength=self.lamb, carrier_wavelength=6000e-09, tfsf=True, amplitude=self.ampl, phase_shift=0)

            for layers in range(0, layer + 1):
                ti_start = self.starting_locations_ti[layers]
                si_start = self.starting_locations_si[layers]
                grid_vary[ti_start:si_start] = f.NonDispersiveMedia('TiO2', permittivity=self.ti_n**2, permeability=1, conductivity=0)
                grid_vary[si_start:(si_start + self.d_si)] = f.NonDispersiveMedia('SiO2', permittivity=self.si_n**2, permeability=1, conductivity=0)

            grid_vary.run_timesteps(self.timesteps, vis=False)
            self.refl_ampl.append(grid_vary.local_observers[0].amplitude)
            self.theory_R.append(((self.si_n**(2*(layer+1))-self.ti_n**(2*(layer+1)))/(self.si_n**(2*(layer+1))+self.ti_n**(2*(layer+1))))**2)

    def _construct_vary_wavelength_grid(self):
        for wavelength in self.incident_wavelengths:
            end_mur = self.Nx - 1
            grid_vary_lamb = f.Grid(dx=self.dx, nx=self.Nx)
            grid_vary_lamb[0] = f.LeftSideMur()
            grid_vary_lamb[end_mur] = f.RightSideMur()
            grid_vary_lamb[self.position_src] = f.ActivatedSinus('Laser', wavelength=wavelength, carrier_wavelength=6000e-09, tfsf=True, amplitude=self.ampl, phase_shift=0)
            grid_vary_lamb[self.position_obs] = f.QuasiHarmonicObserver(name='Observer', first_timestep=self.timesteps - 200)

            for layer_pair in self.layer_number:
                ti_start = self.starting_locations_ti[layer_pair]
                si_start = self.starting_locations_si[layer_pair]
                grid_vary_lamb[ti_start:si_start] = f.NonDispersiveMedia('TiO2', permittivity=self.ti_n**2, permeability=1, conductivity=0)
                grid_vary_lamb[si_start:(si_start + self.d_si)] = f.NonDispersiveMedia('SiO2', permittivity=self.si_n**2, permeability=1, conductivity=0)

            grid_vary_lamb.run_timesteps(self.timesteps, vis=False)
            self.refl_ampl.append(grid_vary_lamb.local_observers[0].amplitude)

    def _visualize_vary_grid(self):
        fig, axes = plt.subplots(1, 1)
        axes.plot(np.array(self.layer_number)+1, np.array(self.refl_ampl)**2, linestyle='dashed', color='blue', marker='o', alpha=0.5, label='FDTD')
        axes.plot(np.array(self.layer_number)+1, np.array(self.theory_R), linestyle='dashed', color='red', marker='o', alpha=0.5, label='model')
        axes.legend(loc='best')
        axes.grid(True, linestyle=(0, (1, 5)), color='black', linewidth=1)
        axes.set_xlabel('Anzahl der Paare', fontsize=14)
        axes.set_ylabel(r'Reflektionsgrad $\mathcal{R}$', fontsize=14)
        plt.show()

    def _visualize_vary_lamb(self):
        fig, axes = plt.subplots(1, 1)
        axes.plot(np.array(self.incident_wavelengths)*10**9, np.array(self.refl_ampl)**2, linestyle='dashed', color='blue', marker='o',
                  alpha=0.5, label='FDTD')
        axes.grid(True, linestyle=(0, (1, 5)), color='black', linewidth=1)
        axes.set_xlabel(r'$\lambda$ in nm', fontsize=14)
        axes.set_ylabel(r'Reflektionsgrad $\mathcal{R}$', fontsize=14)
        plt.show()

    def run_benchmark(self):
        if self.vary_layers:
            self._construct_vary_grid()
            self._visualize_vary_grid()

        elif self.vary_lambda:
            self._construct_vary_wavelength_grid()
            self._visualize_vary_lamb()

        else:
            self._construct_non_vary_grid()

class Harmonic_Slab_Setup:

    def __init__(self, dx,  length_grid_in_dx, length_media_in_dx, start_index_media, wavelength, epsilon, ampl, timesteps):
        self.indices = [start_index_media + 2 + i for i in np.arange(0, length_media_in_dx - 1)]
        self.start_media = start_index_media
        self.dx = dx
        self.eps = epsilon
        self.Nx = length_grid_in_dx
        self.length_media = length_media_in_dx
        self.lamb = wavelength
        self.ampl = ampl
        self.wo_phase = []
        self.theo_phasenunterschied = []
        self.theo_amplitude = []
        self.exp_phase = []
        self.exp_amplitude = []
        self.timesteps = timesteps

    def _grid_wo_slab(self):
        position_src = self.start_media - 1
        position_obs = self.Nx - 3
        end_mur = self.Nx - 1
        wo_grid = f.Grid(self.Nx, dx=self.dx)
        wo_grid[position_src] = f.ActivatedSinus(name='SinsquaredActivated', wavelength=self.lamb, carrier_wavelength=10000.e-09, phase_shift=0, amplitude=self.ampl, tfsf=True)
        wo_grid[position_obs] = f.QuasiHarmonicObserver(name='firstobserver', first_timestep=self.timesteps-200)
        wo_grid[0] = f.LeftSideMur()
        wo_grid[end_mur] = f.RightSideMur()
        wo_grid.run_timesteps(self.timesteps, vis=False)
        self.wo_phase.append(wo_grid.local_observers[0].phase)

    def _grids_w_slab(self):
        position_src = self.start_media - 1
        position_obs = self.Nx - 3
        end_mur = self.Nx - 1
        for ind in self.indices:
            # Step 1: init grid
            w_grid = 'slab' + str(ind)
            w_grid = f.Grid(nx=self.Nx, dx=self.dx)

            # Step 2: init media
            w_grid[self.start_media:ind] = f.NonDispersiveMedia(name='media', permeability=1, permittivity=self.eps, conductivity=0)

            # Step 3: init source
            w_grid[position_src] = f.ActivatedSinus(name='SinsquaredActivated', wavelength=self.lamb, carrier_wavelength=10000.e-09, phase_shift=0, amplitude=self.ampl, tfsf=True)

            # Step 4: add observer
            w_grid[position_obs] = f.QuasiHarmonicObserver(name='firstobserver', first_timestep=self.timesteps-200)

            # Step 5: add boundaries
            w_grid[0] = f.LeftSideMur()
            w_grid[end_mur] = f.RightSideMur()

            # Step 6: run simulation
            w_grid.run_timesteps(timesteps=self.timesteps, vis=False)

            # Step 7: misc
            self.exp_amplitude.append(w_grid.local_observers[0].amplitude)
            self.exp_phase.append(w_grid.local_observers[0].phase)
            self.theo_amplitude.append(theory_dielectric_slab_complex(w_grid)[0])
            self.theo_phasenunterschied.append(theory_dielectric_slab_complex(w_grid)[1])

    def _visualize(self):
        fig, axes = plt.subplots(2, 2)
        fig.suptitle(r'$\epsilon_r=$ {epsilon}'.format(epsilon=self.eps) + r'   $N_{\lambda_{media}}=$' + '{0:.3}'.format(self.lamb/(self.dx*np.sqrt(self.eps))), fontsize=20)
        axes[0][0].plot(np.array(self.indices) - self.start_media, np.array(self.theo_amplitude), label='theorie', color='blue', marker='o', alpha=0.5)
        axes[0][0].grid(True, linestyle=(0, (1, 5)), color='black', linewidth=1)
        axes[0][0].plot(np.array(self.indices) - self.start_media, np.array(self.exp_amplitude), label='FDTD', linestyle='dashed', color='red', marker='s', alpha=0.5)
        axes[0][0].legend(loc='best')
        axes[0][0].set_xlabel('Breite des Mediums in ' + r'$\Delta_x$', fontsize=14)
        axes[0][0].set_ylabel('Transmittierte Amplitude ' + r'$Ez_{tr}$', fontsize=14)
        axes[0][0].set_xlim([0, self.length_media + 1])
        axes[0][1].plot(np.array(self.indices) - self.start_media, np.array(self.theo_amplitude) / np.array(self.exp_amplitude), color='black')
        axes[0][1].set_ylabel(r'$E_{tr,theo}$ / $E_{tr,FDTD}$', fontsize=14)
        axes[0][1].set_xlabel('Breite des Mediums in ' + r'$\Delta_x$', fontsize=14)
        axes[0][1].grid(True, linestyle=(0, (1, 5)), color='black', linewidth=1)
        axes[0][1].set_xlim([0, self.length_media + 1])
        axes[1][0].set_ylabel('Phasenunterschied', fontsize=14)
        axes[1][0].plot(np.array(self.indices) - self.start_media, self.theo_phasenunterschied, label='theorie', color='blue', alpha=0.5)
        axes[1][0].set_xlabel('Breite des Mediums in ' + r'$\Delta_x$', fontsize=14)
        axes[1][0].grid(True, linestyle=(0, (1, 5)), color='black', linewidth=1)
        axes[1][0].plot(np.array(self.indices) - self.start_media, -np.array(self.exp_phase) + self.wo_phase, color='red', linestyle='dashed',
                        label='FDTD', alpha=0.5)
        axes[1][0].set_xlim([0, self.length_media + 1])
        axes[1][0].legend()
        axes[1][1].set_xlabel('Breite des Mediums in ' + r'$\Delta_x$', fontsize=14)
        axes[1][1].set_ylabel(r'$d(\phi_{exp},\phi_{theo})$', fontsize=14)
        axes[1][1].plot(np.array(self.indices) - self.start_media,
                        np.abs(-np.array(self.exp_phase) + self.wo_phase - np.array(self.theo_phasenunterschied)), color='black')
        axes[1][1].grid(True, linestyle=(0, (1, 5)), color='black', linewidth=1)
        axes[1][1].set_xlim([0, self.length_media + 1])
        plt.show()

    def run_benchmark(self):
        self._grid_wo_slab()
        self._grids_w_slab()
        self._visualize()


from django.urls import path

from . import views
from reviews import views as reviewsViews
from .views import SearchBooks

app_name = "books"
urlpatterns = [
    path("home/", views.Home, name="home_page"),

    path("list/", views.ListBooks, name="list_books"),
    path("list/top_ten", views.TopTen, name="top_ten"),

    path("<int:book_id>/", views.displayBookInfo, name="view_book"),
    path("<int:book_id>/review/", reviewsViews.MakeReview, name="review_book"),
    path("<int:book_id>/review/edit", reviewsViews.EditReview, name="edit_review"),
    path("<int:book_id>/review/delete", reviewsViews.DeleteReview, name="delete_review"),

    path("<int:book_id>/review/like_dislike", reviewsViews.LikeDislike, name="like_dislike"),
    path('<int:book_id>/favorite', views.FavoriteBook, name='favorite_book'),

    path('search/', views.SearchBooks, name='search_books'),

    path('search_isbn/', views.SearchIsbn, name='search_isbn'),
    path('isbn_create/', views.CreateFromIsbn, name='create_from_isbn')
]


# W tym pliku będą znajdować się ważne funkcje dotyczące
# Symulatora Julki. Zrobiłem to po to by nie zaśmiecać
# głównego pliku wykonywalnego.
import json

configFilePath = "./config.json"
loopedAnsw = False
randomizeAnsw = False
stoppingKeywords = None
userCustomJulkaAnsw = None

def initConfig():
    global configFilePath

    fetchConfigData(configFilePath)

def fetchConfigData(configFilePath):
    global userCustomJulkaAnsw, loopedAnsw, randomizeAnsw, stoppingKeywords

    try:
        config = open(configFilePath, 'r', encoding="utf-8")
    except:
        print("Coś poszło nie tak. Ścieżka do pliku konfiguracyjnego jest nieprawidłowa lub takowy plik nie istnieje.")
        return False

    configData = json.load(config)

    if configData["customRespStatus"] == True:
        print("'customRespStatus' w pliku konfiguracyjnym jest równy: True!")
        userCustomJulkaAnsw = configData["customResp"]

    loopedAnsw = configData["looped"]
    randomizeAnsw = configData["randomizeResp"]
    stoppingKeywords = configData["stoppingKeywords"]

    print(f"DEBUG INFO: userCustomJulkaAnsw == {userCustomJulkaAnsw}")
    print(f"DEBUG INFO: loopedAnsw == {loopedAnsw}")
    print(f"DEBUG INFO: randomizeAnsw == {randomizeAnsw}")
    print(f"DEBUG INFO: stoppingKeywords == {stoppingKeywords}")

    config.close()

    # Kiedy wszystko pójdzie OK to funckja zwróci 'True'.
    # Dzięki temu zapobiegnie to trywialnym błędom i
    # będe mógł sprawdzić czy rzeczywiście wszystko poszło
    # dobrze z planem.
    return True

# Funkcja wypluwająca z siebie losowo
# wybierane odpowiedzi aniżeli chodząca
# po kolei po tablicy.
#
# Zmienna 'actualJulkaAnsw' jest po to
# żeby funkcja wiedziała jaką aktualnie
# używa tablice do odpowiedzi Julki.
def randomResp(actualJulkaAnsw, looped=False):
    from random import randint

    actualJulkaAnswRNG = actualJulkaAnsw

    if looped == True:
        while True:
            actualRandNum = randint(0, (len(actualJulkaAnsw)-1))

            userInput = input("TY: ")

            if userInput in stoppingKeywords:
                break

            print("JULKA:", actualJulkaAnsw[actualRandNum])
    else:
        while True:
            if len(actualJulkaAnswRNG) <= 0:
                break

            actualRandNum = randint(0, (len(actualJulkaAnswRNG)-1))

            userInput = input("TY: ")

            if userInput in stoppingKeywords:
                break

            print("JULKA:", actualJulkaAnswRNG[actualRandNum])

            actualJulkaAnswRNG.pop(actualRandNum)

# Podobnie jak w przypadku funkcji 'randomResp',
# ale cóż... bez losowości.
def typicalResp(actualJulkaAnsw, looped=False):
    if looped == True:
        i = 0

        while True:
            if i < len(actualJulkaAnsw):
                userInput = input("TY: ")

                if userInput in stoppingKeywords:
                    break

                print("JULKA:", actualJulkaAnsw[i])

                i += 1
            else:
                i = 0
    else:
        for i in range(len(actualJulkaAnsw)):
            userInput = input("TY: ")

            if userInput in stoppingKeywords:
                break

            print("JULKA:", actualJulkaAnsw[i])


from django.urls import path

from . import views

urlpatterns = [
    path('<slug:category_name>/', views.CategoryView.as_view(), name="category"),
    path('<slug:category>/<slug:slug>/', views.PostDetailView.as_view(), name="detail_post"),
    path('<slug:tag_slug>/', views.TagDetailView.as_view(), name="detail_tag"),

    path("", views.HomeView.as_view()),
]


import logging
import os
import uuid
from typing import Tuple, Dict, Optional, Callable, Any

from nebullvm.base import (
    ModelParams,
    DeepLearningFramework,
    QuantizationType,
    Device,
)
from nebullvm.config import (
    AUTO_TVM_TUNING_OPTION,
    AUTO_TVM_PARAMS,
    QUANTIZATION_DATA_NUM,
    CONSTRAINED_METRIC_DROP_THS,
)
from nebullvm.inference_learners.tvm import (
    TVM_INFERENCE_LEARNERS,
    ApacheTVMInferenceLearner,
)
from nebullvm.measure import compute_relative_difference
from nebullvm.optimizers.base import BaseOptimizer
from nebullvm.optimizers.quantization.tvm import TVMCalibrator
from nebullvm.optimizers.quantization.utils import (
    check_quantization,
    check_precision,
)
from nebullvm.optional_modules.onnx import onnx
from nebullvm.optional_modules.torch import torch, Module
from nebullvm.optional_modules.tvm import (
    tvm,
    IRModule,
    NDArray,
    XGBTuner,
    autotvm,
    relay,
    ToMixedPrecision,
)
from nebullvm.transformations.base import MultiStageTransformation
from nebullvm.utils.data import DataManager
from nebullvm.utils.onnx import (
    get_input_names,
)
from nebullvm.utils.torch import create_model_inputs_torch

logger = logging.getLogger("nebullvm_logger")

class ApacheTVMOptimizer(BaseOptimizer):
    """Class for compiling the AI models on Nvidia GPUs using TensorRT."""

    def optimize_from_torch(
        self,
        torch_model: Module,
        model_params: ModelParams,
        device: Device,
        input_tfms: MultiStageTransformation = None,
        metric_drop_ths: float = None,
        quantization_type: QuantizationType = None,
        metric: Callable = None,
        input_data: DataManager = None,
        model_outputs: Any = None,
    ) -> Optional[Tuple[ApacheTVMInferenceLearner, float]]:
        logger.info(
            f"Optimizing with {self.__class__.__name__} and "
            f"q_type: {quantization_type}."
        )
        target = self._get_target(device)
        mod, params = self._build_tvm_model_from_torch(
            torch_model, model_params, device
        )
        if quantization_type is not None:
            if quantization_type is QuantizationType.HALF:
                mod = ToMixedPrecision(mixed_precision_type="float16")(mod)
            else:
                if quantization_type is QuantizationType.DYNAMIC:
                    inputs = None
                elif quantization_type is QuantizationType.STATIC:
                    inputs = input_data.get_split("train").get_numpy_list(
                        QUANTIZATION_DATA_NUM
                    )
                    input_names = [f"input_{n}" for n in range(len(inputs[0]))]
                    inputs = TVMCalibrator(inputs, input_names)
                else:
                    return
                mod = self._quantize(mod, params, input_data=inputs)
        tuning_records = self._tune_tvm_model(target, mod, params)
        with autotvm.apply_history_best(tuning_records):
            with tvm.transform.PassContext(opt_level=3, config={}):
                lib = relay.build(mod, target=target, params=params)

        # Remove temporary file created by tvm
        os.remove(tuning_records)

        learner = TVM_INFERENCE_LEARNERS[
            DeepLearningFramework.PYTORCH
        ].from_runtime_module(
            input_tfms=input_tfms,
            network_parameters=model_params,
            lib=lib,
            target_device=target,
            input_names=[
                f"input_{i}" for i in range(len(model_params.input_infos))
            ],
            input_data=list(input_data.get_list(1)[0])
            if input_data is not None
            else None,
        )

        test_input_data, ys = input_data.get_split("test").get_list(
            with_ys=True
        )
        is_valid, metric_drop = check_precision(
            learner,
            test_input_data,
            model_outputs,
            metric_drop_ths
            if quantization_type is not None
            else CONSTRAINED_METRIC_DROP_THS,
            metric_func=metric
            if quantization_type is not None
            else compute_relative_difference,
            ys=ys,
        )
        if not is_valid:
            if quantization_type is None:
                logger.warning(
                    "The model optimized with Pytorch tvm gives a "
                    "different result compared with the original model. "
                    "This compiler will be skipped."
                )
            return None
        return learner, metric_drop

    def optimize(
        self,
        model: str,
        output_library: DeepLearningFramework,
        model_params: ModelParams,
        device: Device,
        input_tfms: MultiStageTransformation = None,
        metric_drop_ths: float = None,
        quantization_type: QuantizationType = None,
        metric: Callable = None,
        input_data: DataManager = None,
        model_outputs: Any = None,
    ) -> Optional[Tuple[ApacheTVMInferenceLearner, float]]:
        """Optimize the input model with Apache TVM.

        Args:
            model (str): Path to the saved onnx model.
            output_library (str): DL Framework the optimized model will be
                compatible with.
            model_params (ModelParams): Model parameters.
            device: (Device): Device where the model will be run.
            input_tfms (MultiStageTransformation, optional): Transformations
                to be performed to the model's input tensors in order to
                get the prediction.
            metric_drop_ths (float, optional): Threshold for the accepted drop
                in terms of precision. Any optimized model with an higher drop
                will be ignored.
            quantization_type (QuantizationType, optional): The desired
                quantization algorithm to be used.
            metric (Callable, optional): If given it should
                compute the difference between the quantized and the normal
                prediction.
            input_data (DataManager, optional): User defined data.
            model_outputs (Any): Outputs computed by the original model.

        Returns:
            ApacheTVMInferenceLearner: Model optimized with TVM. The model
                will have an interface in the DL library specified in
                `output_library`.
        """
        logger.info(
            f"Optimizing with {self.__class__.__name__} and "
            f"q_type: {quantization_type}."
        )
        check_quantization(quantization_type, metric_drop_ths)
        target = self._get_target(device)
        mod, params = self._build_tvm_model_from_onnx(model, model_params)
        if quantization_type is not None:
            if quantization_type is QuantizationType.HALF:
                mod = ToMixedPrecision(mixed_precision_type="float16")(mod)
            else:
                if quantization_type is QuantizationType.DYNAMIC:
                    inputs = None
                elif quantization_type is QuantizationType.STATIC:
                    inputs = input_data.get_split("train").get_numpy_list(
                        QUANTIZATION_DATA_NUM
                    )
                    inputs = TVMCalibrator(inputs, get_input_names(model))
                else:
                    return
                mod = self._quantize(mod, params, input_data=inputs)
        tuning_records = self._tune_tvm_model(target, mod, params)
        with autotvm.apply_history_best(tuning_records):
            with tvm.transform.PassContext(opt_level=3, config={}):
                lib = relay.build(mod, target=target, params=params)

        # Remove temporary file created by tvm
        os.remove(tuning_records)

        learner = TVM_INFERENCE_LEARNERS[output_library].from_runtime_module(
            input_tfms=input_tfms,
            network_parameters=model_params,
            lib=lib,
            target_device=target,
            input_names=get_input_names(model),
            input_data=list(input_data.get_list(1)[0])
            if input_data is not None
            else None,
        )
        test_input_data, ys = input_data.get_split("test").get_list(
            with_ys=True
        )
        is_valid, metric_drop = check_precision(
            learner,
            test_input_data,
            model_outputs,
            metric_drop_ths
            if quantization_type is not None
            else CONSTRAINED_METRIC_DROP_THS,
            metric_func=metric
            if quantization_type is not None
            else compute_relative_difference,
            ys=ys,
        )
        if not is_valid:
            if quantization_type is None:
                logger.warning(
                    "The model optimized with ONNX tvm gives a "
                    "different result compared with the original model. "
                    "This compiler will be skipped."
                )
            return None
        return learner, metric_drop

    @staticmethod
    def _build_tvm_model_from_torch(
        torch_model: Module, model_params: ModelParams, device: Device
    ) -> Tuple[IRModule, Dict[str, NDArray]]:
        shape_dict = {
            f"input_{i}": (
                model_params.batch_size,
                *input_size,
            )
            for i, input_size in enumerate(model_params.input_sizes)
        }
        inputs = tuple(
            create_model_inputs_torch(
                model_params.batch_size, model_params.input_infos
            )
        )
        if device is not Device.GPU:
            inputs = tuple(input_.cpu() for input_ in inputs)
            torch_model.cpu()
        with torch.no_grad():
            _ = torch_model(*inputs)
            model_trace = torch.jit.trace(torch_model, inputs)
            model_trace.eval()
        mod, params = relay.frontend.from_pytorch(
            model_trace, list(shape_dict.items())
        )
        return mod, params

    @staticmethod
    def _build_tvm_model_from_onnx(
        onnx_model_path: str, model_params: ModelParams
    ) -> Tuple[IRModule, Dict[str, NDArray]]:
        shape_dict = {
            input_key: (
                model_params.batch_size,
                *input_size,
            )
            for input_key, input_size in zip(
                get_input_names(onnx_model_path), model_params.input_sizes
            )
        }
        onnx_model = onnx.load(onnx_model_path)
        mod, params = relay.frontend.from_onnx(onnx_model, shape_dict)
        return mod, params

    @staticmethod
    def _quantize(
        mod: IRModule,
        params: Dict[str, NDArray],
        input_data: TVMCalibrator = None,
    ) -> IRModule:
        if input_data is not None:
            with relay.quantize.qconfig(
                calibrate_mode="kl_divergence", weight_scale="max"
            ):
                mod = relay.quantize.quantize(mod, params, dataset=input_data)
        else:
            with relay.quantize.qconfig(
                calibrate_mode="global_scale", global_scale=8.0
            ):
                mod = relay.quantize.quantize(mod, params)
        return mod

    @staticmethod
    def _get_target(device) -> str:
        if device is Device.GPU:
            return str(tvm.target.cuda())
        else:
            return "llvm"  # run on CPU

    @staticmethod
    def _tune_tvm_model(
        target: str, mod: IRModule, params: Dict[str, NDArray]
    ) -> str:
        """Tune the model using AutoTVM."""
        # TODO: add support to Ansor
        tuning_records = f"{uuid.uuid4()}_model_records.json"
        # create a TVM runner
        runner = autotvm.LocalRunner(
            number=AUTO_TVM_PARAMS["number"],
            repeat=AUTO_TVM_PARAMS["repeat"],
            timeout=AUTO_TVM_PARAMS["timeout"],
            min_repeat_ms=AUTO_TVM_PARAMS["min_repeat_ms"],
            # TODO modify min_repeat_ms for GPU usage
            enable_cpu_cache_flush=True,
        )
        # begin by extracting the tasks from the onnx model
        tasks = autotvm.task.extract_from_program(
            mod["main"], target=target, params=params
        )

        # Tune the extracted tasks sequentially.
        for i, task in enumerate(tasks):
            tuner_obj = XGBTuner(task, loss_type="rank")
            tuner_obj.tune(
                n_trial=min(
                    AUTO_TVM_TUNING_OPTION["trials"], len(task.config_space)
                ),
                early_stopping=AUTO_TVM_TUNING_OPTION["early_stopping"],
                measure_option=autotvm.measure_option(
                    builder=autotvm.LocalBuilder(build_func="default"),
                    runner=runner,
                ),
                callbacks=[
                    autotvm.callback.log_to_file(tuning_records),
                ],
            )
        return tuning_records