#!/usr/bin/env python
# coding:utf-8

# Disclaimer:
# There is some code from an example switch implementation that Chao found.
# https://www.gitbook.com/book/yeasy/mininet_book/details
# We only use the main idea of having a graph representation of the network and
# the mechanism to figure out the next out_port as marked in the code. Some other
# parts like the table-miss entry are standards that are found in any ryu app

from ryu.base import app_manager
from ryu.controller import ofp_event
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3
from ryu.lib.packet import packet
from ryu.lib.packet import ether_types
from ryu.lib.packet import ethernet
from ryu.lib.packet import ipv4

import networkx as nx
import copy
import time

class ProjectController(app_manager.RyuApp):
    OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]

    def __init__(self, *args, **kwargs):
        """Initialize the Graph representing our test-topology.
        H1 ²---² S1 ⁴----------³ S4 ²---² H4
                 ³                ⁴
                 |                |
                 |                |
                 ⁴                ³
        H2 ²---² S2 ³----------⁴ S3 ²---² H3

        The little numbers are the switch ports."""
        super(ProjectController, self).__init__(*args, **kwargs)

        ############################################
        # Switch on smart features like slicing using QoS queues and automatic
        # rerouting after switch failure
        self.smart_failure = True
        self.disable_slicing = False
        ############################################

        self.hosts = ['10.0.0.1', '10.0.0.2', '10.0.0.3', '10.0.0.4']
        # block_dict is necessary to block the correct port in broadcast-mode,
        # to avoid receiving packets twice from two possible paths
        self.block_dict = {(3, '10.0.0.1'): 2,
                           (4, '10.0.0.2'): 3,
                           (1, '10.0.0.3'): 4,
                           (2, '10.0.0.4'): 1}
        # self.slices is a dictionary that will hold all existing slice flows,
        # keys are the destination-ports, values are sets of slices as seen below
        self.slices = dict()
        # elements  of these sets will be slice-flows represented as
        # tuples: (ipv4_src, ipv4_dst, protocol, dst_port, queue_id, weight, path, of_priority)
        # this is to be able to reestablish existing flows in case of switch
        # failure
        self.slices[5004] = set()   # video
        self.slices[10022] = set()  # latency
        self.slices[10023] = set()  # mission_critical
        # save datapaths of all switches to be able to send them FlowMods
        self.datapaths = []
        # define what protocols are supported: UDP=17 and TCP=6
        # numbers are those used for 'ip_proto' field in parser.OFPMatch
        self.slice_protocols = [17, 6]
        self.DEFAULT_QUEUE = 0
        self.VIDEO_QUEUE = 0 # is set to 0 to use it as base-line-noise
        self.MULTICAST_QUEUE = 1
        self.LATENCY_QUEUE = 2
        self.CRITICAL_QUEUE = 3
        if self.disable_slicing:
                self.MULTICAST_QUEUE = 0
                self.LATENCY_QUEUE = 0
                self.CRITICAL_QUEUE = 0

        # the idea to use networkx.DiGraph and assigning the port to the edge is from Chao's book
        # the use of different weights is our idea!
        self.net = nx.DiGraph()
        for i in range(4):
            self.net.add_node(self.hosts[i])
            self.net.add_edge(i+1, self.hosts[i], port=2, weight=0, video=0, latency=0, mission_critical=0)
            self.net.add_edge(self.hosts[i], i+1, weight=0, video=0, latency=0, mission_critical=0)

        self.net.add_node(1)
        self.net.add_node(2)
        self.net.add_node(3)
        self.net.add_node(4)
        # set different weights for static slicing based on link properties
        # this feature is currently not in use since it was discussed that
        # all links should be the same
        # the idea to save the port in the edge as seen below is from Chao's book
        self.net.add_edge(1, 2, port=3, weight=1, video=1, latency=1, mission_critical=1)
        self.net.add_edge(2, 1, port=4, weight=1, video=1, latency=1, mission_critical=1)
        self.net.add_edge(2, 3, port=3, weight=1, video=1, latency=1, mission_critical=1)
        self.net.add_edge(3, 2, port=4, weight=1, video=1, latency=1, mission_critical=1)
        self.net.add_edge(3, 4, port=3, weight=1, video=1, latency=1, mission_critical=1)
        self.net.add_edge(4, 3, port=4, weight=1, video=1, latency=1, mission_critical=1)
        self.net.add_edge(4, 1, port=3, weight=1, video=1, latency=1, mission_critical=1)
        self.net.add_edge(1, 4, port=4, weight=1, video=1, latency=1, mission_critical=1)
        self.logger.info("**********ProjectController __init__")

    @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
    def switch_features_handler(self, ev):
        """Add table-miss flow entry and add the switches datapath to
        self.datapaths for future Flow Modification."""
        self.logger.info("\n-----------switch_features_handler is called")

        msg = ev.msg
        self.logger.info("Setting table-miss flow entry.")
        datapath = ev.msg.datapath
        self.datapaths.append(datapath)
        dpid = datapath.id
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser

        # empty match means that every packet that comes in matches this rule
        match = parser.OFPMatch()
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                          ofproto.OFPCML_NO_BUFFER)]
        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
                                             actions)]
        mod = parser.OFPFlowMod(
            datapath=datapath, match=match, cookie=0, command=ofproto.OFPFC_ADD,
            idle_timeout=0, hard_timeout=0, priority=0, instructions=inst)
        datapath.send_msg(mod)
        self.logger.info("-----------switch_features_handler is over\n")

    def add_port_based_flow(self, datapath, dst_port, ipv4_src, ipv4_dst, actions, priority, protocol):
        """Add flow with matching protocol (UDP=17/TCP=6), dst_port, ipv4-src and ipv4-dst."""
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        if protocol == 17:
            match = parser.OFPMatch(
                eth_type=ether_types.ETH_TYPE_IP, ip_proto=protocol, udp_dst=dst_port,
                ipv4_src=ipv4_src, ipv4_dst=ipv4_dst)
            #self.logger.info("\nAdding UDP flow: switch {}\nmatch:{}\nactions={}\n".format(datapath.id, match, actions))
        elif protocol == 6:
            match = parser.OFPMatch(
                eth_type=ether_types.ETH_TYPE_IP, ip_proto=protocol, tcp_dst=dst_port,
                ipv4_src=ipv4_src, ipv4_dst=ipv4_dst)
            #self.logger.info("\nAdding TCP flow: switch {}\nmatch:{}\nactions={}\n".format(datapath.id, match, actions))
        else:
            self.logger.error("ERROR: Protocol {} not supported!".format(protocol))
            return
        self.add_any_flow(datapath, match, actions, priority)

    def add_any_flow(self, datapath, match, actions, priority):
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS, actions)]
        mod = parser.OFPFlowMod(
            datapath=datapath, match=match, cookie=0,
            command=ofproto.OFPFC_ADD, idle_timeout=0, hard_timeout=0,
            priority=priority, instructions=inst)
        datapath.send_msg(mod)

    def add_slice(self, datapath, ipv4_src, ipv4_dst, protocol, dst_port, queue_id, weight, of_priority):
        """Calculate the shortest path based on custom weight, then add the
        necessary flow-entry with the correct out_port and the correct
        'queue_id' for this slice."""
        dpid = datapath.id
        try:
            # calculate path from switch to destination
            path = nx.shortest_path(self.net, dpid, ipv4_dst, weight=weight)
        except Exception:
            self.logger.error("ERROR add_slice: {} to {} no shortest_path".format(dpid, ipv4_dst))
            return (None, self.DEFAULT_QUEUE)
        next = path[path.index(dpid) + 1]           # this and all reoccurences
        out_port = self.net[dpid][next]['port']     # are from Chao's book
        actions = [
            datapath.ofproto_parser.OFPActionSetQueue(queue_id=queue_id),
            datapath.ofproto_parser.OFPActionOutput(out_port)]

        try:
            # calculate path from src to destination
            src_path = nx.shortest_path(self.net, ipv4_src, ipv4_dst, weight=weight)
            sl = (ipv4_src, ipv4_dst, protocol, dst_port, queue_id, weight, tuple(src_path), of_priority)
            if sl not in self.slices[dst_port]:
                self.logger.info("\nADDING NEW SLICE-FLOW: ipv4_src={}, ipv4_dst={}, protocol={}, dst_port={},\nqueue_id={}, weight={}\ntuple(path)={}, of_priority={}\n".format(*sl))
                self.slices[dst_port].add(sl)
            else:
                self.logger.info("Slice-flow {} already initialized, just adding rule to switch {}".format(sl, dpid))
        except KeyError:
            self.logger.error("ERROR {} not in self.slices!".format(dst_port))
        except Exception:
            self.logger.error("ERROR add_slice2: {} to {} no shortest_path".format(ipv4_src, ipv4_dst))
            return (None, self.DEFAULT_QUEUE)

        self.add_port_based_flow(datapath=datapath, dst_port=dst_port,
                                 ipv4_src=ipv4_src, ipv4_dst=ipv4_dst,
                                 actions=actions, priority=of_priority,
                                 protocol=protocol)
        return (out_port, queue_id)

    # Chao's work
    def add_broadcast_slice(self, in_port, datapath, ipv4_src, ipv4_dst, protocol, dst_port, queue_id, weight, of_priority):
        """Calculate the shortest path based on custom weight, then add the
        necessary flow-entry with the correct out_port and the correct
        'queue_id' for this slice."""
        dpid = datapath.id
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        out_port = ofproto.OFPP_FLOOD
        if in_port == 2:
            actions = [
                parser.OFPActionSetNwTtl(nw_ttl=3),
                parser.OFPActionSetQueue(queue_id=queue_id),
                parser.OFPActionOutput(out_port)]
        else:
            if (dpid, ipv4_src) in self.block_dict and self.block_dict[(dpid, ipv4_src)] in self.net:
                match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_IP,
                                        ipv4_src=ipv4_src, ipv4_dst=ipv4_dst,
                                        in_port=4)
                actions = []
                self.add_any_flow(datapath=datapath, actions=actions, match=match,
                                  priority=of_priority+1)
                out_port = 2  # don't send it to blocked switch, only to the host
            actions = [
                parser.OFPActionDecNwTtl(),
                parser.OFPActionSetQueue(queue_id=queue_id),
                parser.OFPActionOutput(out_port)]

        self.add_port_based_flow(datapath=datapath, dst_port=dst_port,
                                 ipv4_src=ipv4_src, ipv4_dst=ipv4_dst,
                                 actions=actions, priority=of_priority,
                                 protocol=protocol)
        return (out_port, queue_id)

    # Chao's work
    def add_base_broadcast(self, in_port, datapath, ipv4_src, ipv4_dst):
        dpid = datapath.id
        queu_id = self.DEFAULT_QUEUE
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        out_port = ofproto.OFPP_FLOOD
        if in_port == 2:
            actions = [
                parser.OFPActionSetNwTtl(nw_ttl=3),
                parser.OFPActionSetQueue(queue_id=queue_id),
                parser.OFPActionOutput(out_port)]
        else:
            if (dpid, ipv4_src) in self.block_dict and self.block_dict[(dpid, ipv4_src)] in self.net:
                match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_IP,
                                        ipv4_src=ipv4_src, ipv4_dst=ipv4_dst,
                                        in_port=4)
                actions = []
                self.add_any_flow(datapath=datapath, actions=actions, match=match,
                                  priority=of_priority+1)
                out_port = 2  # don't send it to blocked switch
            actions = [
                parser.OFPActionDecNwTtl(),
                parser.OFPActionSetQueue(queue_id=queue_id),
                parser.OFPActionOutput(out_port)]

        match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_IP,
                                ipv4_src=ipv4_src, ipv4_dst=ipv4_dst)
        self.add_any_flow(datapath=datapath, actions=actions, match=match,
                          priority=of_priority+1)
        return (out_port, queue_id)

    def add_base_flow(self, datapath, ipv4_src, ipv4_dst):
        """Used for non-special traffic, adds a flow based on 'weight'
        and also flows with higher priority to make sure that UDP/TCP packets
        with special ports are send to the controller again for custom
        flow processing (adding the correct slice)."""
        dpid = datapath.id
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        match = parser.OFPMatch(eth_type=ether_types.ETH_TYPE_IP, ipv4_src=ipv4_src, ipv4_dst=ipv4_dst)
        try:
            path = nx.shortest_path(self.net, dpid, ipv4_dst, weight="weight")
        except Exception:
            self.logger.error("ERROR add_base_flow: {} to {} no shortest_path".format(dpid, ipv4_dst))
            return (None, self.DEFAULT_QUEUE)

        next = path[path.index(dpid) + 1]
        out_port = self.net[dpid][next]['port']

        actions = [
            parser.OFPActionSetQueue(queue_id=self.DEFAULT_QUEUE),
            parser.OFPActionOutput(out_port)]
        inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS, actions)]
        mod = parser.OFPFlowMod(
            datapath=datapath, match=match, cookie=0,
            command=ofproto.OFPFC_ADD, idle_timeout=0, hard_timeout=0,
            priority=1, instructions=inst)
        self.logger.info("SWITCH {} : Adding base flow for src:{} dst:{}".format(dpid, ipv4_src, ipv4_dst))
        datapath.send_msg(mod)

        # add additional flows to make sure the switch asks the controller again
        # how to process special-packets
        actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER)]
        self.logger.info("Adding callback rules for higher slice packets.")
        for protocol in self.slice_protocols:
            for dst_port in self.slices:
                self.add_port_based_flow(datapath=datapath, dst_port=dst_port,
                                         ipv4_src=ipv4_src, ipv4_dst=ipv4_dst,
                                         actions=actions, priority=2,
                                         protocol=protocol)
        return (out_port, self.DEFAULT_QUEUE)

    def fail_node(self, failed_node):
        """Removes node from network, deletes all flows from every switch
        and adds table-miss flow again. After this it is as if the network
        had been reset without the failed switch."""
        if failed_node in self.net:
            self.logger.info("Removing node {} from self.net".format(failed_node))
            self.net.remove_node(failed_node)
        else:
            self.logger.info("Node {} was already removed... dropping!".format(failed_node))
            return
        for datapath in self.datapaths:
            # remove all flow-entries from this switch
            self.remove_flows(datapath)
            # set table miss again
            ofproto = datapath.ofproto
            parser = datapath.ofproto_parser
            match = parser.OFPMatch()
            actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
                                              ofproto.OFPCML_NO_BUFFER)]
            inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS,
                                                 actions)]
            mod = parser.OFPFlowMod(
                datapath=datapath, match=match, cookie=0,
                command=ofproto.OFPFC_ADD, idle_timeout=0, hard_timeout=0,
                priority=0, instructions=inst)
            datapath.send_msg(mod)

    def remove_flows(self, datapath):
        """Send OFP flow mod message to remove all flows from a switch."""
        self.logger.info("REMOVING ALL FLOWS FOR SWITCH: {} !!".format(datapath.id))
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        match = parser.OFPMatch()
        instructions = []
        mod = parser.OFPFlowMod(
            datapath, cookie=0, cookie_mask=0, table_id=0,
            command=ofproto.OFPFC_DELETE, idle_timeout=0, hard_timeout=0,
            priority=1, buffer_id=ofproto.OFP_NO_BUFFER,
            out_port=ofproto.OFPP_ANY, out_group=ofproto.OFPG_ANY, flags=0,
            match=match, instructions=instructions)
        datapath.send_msg(mod)

    def repopulate_switches(self, failed_node):
        """Send all necessary rules to all switches to reestablish existing
        flows with new routes. Impossible flows will be removed."""
        # copy dictionary for save iteration
        iter_dict = copy.deepcopy(self.slices)
        # remove all slices with src or dst node unreachable from the self.slices dict
        for port, sl_set in iter_dict.iteritems():
            tmp = set(sl_set)
            for sl in sl_set:
                path = sl[6]
                # the following check is based on the assumption that if e.g.
                # switch 3 has failed the corresponding host h3 is unreachable.
                # to check this the path saved in sl[6] is used: a path looks
                # like this: (10.0.0.1, 1, 4, 3, 10.0.0.3)
                # so if the second element of the path is the failed node it is
                # impossible to reroute, same if the second to last element in
                # the path is the failed node
                if (failed_node == path[1]) or (failed_node == path[-2]):
                    self.logger.info("Permanently removing {} because host h{} became unreachable!".format(sl, failed_node))
                    tmp.remove(sl)
            self.slices[port] = tmp

        # reestablish existing slices based on new topology
        # do it in the order of priority 10023, 10022, 5004
        port = 10023
        tmp = set(self.slices[port])
        for sl in self.slices[port]:
            self.logger.info("-------")
            path = sl[6]
            if failed_node in path:
                self.logger.info("\nRerouting:\n{}".format(sl))
                tmp.remove(sl)
            new_slice = self.reestablish_slice(sl)
            if new_slice not in tmp:
                tmp.add(new_slice)
        self.slices[port] = tmp

        port = 10022
        tmp = set(self.slices[port])
        for sl in self.slices[port]:
            self.logger.info("-------")
            path = sl[6]
            if failed_node in path:
                self.logger.info("\nRerouting:\n{}".format(sl))
                tmp.remove(sl)
            new_slice = self.reestablish_slice(sl)
            if new_slice not in tmp:
                tmp.add(new_slice)
        self.slices[port] = tmp

        port = 5004
        tmp = set(self.slices[port])
        for sl in self.slices[port]:
            self.logger.info("-------")
            path = sl[6]
            if failed_node in path:
                self.logger.info("\nRerouting:\n{}".format(sl))
                tmp.remove(sl)
            new_slice = self.reestablish_slice(sl)
            if new_slice not in tmp:
                tmp.add(new_slice)
        self.slices[port] = tmp

    def reestablish_slice(self, sl):
        """Reestablish an existing slice(-flow) by recalculating the shortest
        path and sending all necessary rules to the switches along the path."""
        ipv4_src, ipv4_dst, protocol, dst_port, queue_id, weight, path, of_priority = sl
        try:
            path = nx.shortest_path(self.net, ipv4_src, ipv4_dst, weight=weight)
        except Exception:
            self.logger.error("ERROR reestablish_slice: {} to {} no shortest_path".format(ipv4_src, ipv4_dst))
            return
        for datapath in self.datapaths:
            dpid = datapath.id
            if dpid in path:
                # switch is in the path so send him the right out_port and queue
                next = path[path.index(dpid) + 1]
                out_port = self.net[dpid][next]['port']
                actions = [
                    datapath.ofproto_parser.OFPActionSetQueue(queue_id=queue_id),
                    datapath.ofproto_parser.OFPActionOutput(out_port)]
                self.add_port_based_flow(datapath=datapath, dst_port=dst_port,
                                         ipv4_src=ipv4_src, ipv4_dst=ipv4_dst,
                                         actions=actions, priority=of_priority,
                                         protocol=protocol)
        return (ipv4_src, ipv4_dst, protocol, dst_port, queue_id, weight, tuple(path), of_priority)

    @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
    def _packet_in_handler(self, ev):
        """packet_in_handler is called whenever a flow does not have a matching
        flow_entry. If the packet is a UDP/TCP packet with one of the
        slice_ports as destination port, then the flow entries to create the
        corresponding network-slice are send to the switch. If it is a
        non-special packet (non of the slice_ports), a 'base_flow' is added to
        ensure connectivity but it only uses the default-queue and no special
        route. For simplicity we only support IPv4-packets, all others are
        dropped."""
        msg = ev.msg
        datapath = msg.datapath
        t1 = time.clock()
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        in_port = msg.match['in_port']
        pkt = packet.Packet(msg.data)
        eth = pkt.get_protocol(ethernet.ethernet)
        ipv4_handle = pkt.get_protocol(ipv4.ipv4)
        dpid = datapath.id
        out_port = None
        queue_id = self.DEFAULT_QUEUE

        if eth.ethertype == ether_types.ETH_TYPE_LLDP:
            # ignore lldp packet
            return
        try:
            dst = ipv4_handle.dst
            src = ipv4_handle.src
            protocol = ipv4_handle.proto
        except Exception:
            return
        self.logger.info("**********_packet_in_handler\nSWITCH {}\n".format(datapath.id))

        # use certain destination IPs to 'detect'/simulate switch failure
        # so in the demo we do: 'switch s3 stop' and 'h1 ping -c1 10.0.0.33'
        # now both mininet and the controller know that s3 has failed
        fail_dict = {'10.0.0.11':1, '10.0.0.22':2, '10.0.0.33':3, '10.0.0.44':4}
        if dst in fail_dict:
            # simulate switch failure
            sw = fail_dict[dst]
            self.net.remove_node("10.0.0.{}".format(fail_dict[dst]))
            self.logger.info("---------FAILURE HANDLING switch {}---------".format(sw))
            self.fail_node(sw)
            if self.smart_failure:
                self.repopulate_switches(sw)
            self.logger.info("---------FAILURE HANDLING OVER---------")
            return

        try:
            dst_port = pkt.protocols[2].dst_port
        except Exception:
            pass

        # shouldn't be necessary but in case a new host is added we can add it
        # to the graph with this
        if src not in self.net:
            self.logger.info("INFO: adding {} to graph".format(src))
            self.net.add_node(src)
            self.net.add_edge(dpid, src, port=in_port, weight=0, video=0, latency=0, mission_critical=0)
            self.net.add_edge(src, dpid, weight=0, video=0, latency=0, mission_critical=0)

        if protocol in self.slice_protocols and dst_port in self.slices:
            if dst_port == 5004:
                if dst == "10.255.255.255":
                    out_port, queue_id = self.add_broadcast_slice(
                                                        in_port=in_port,
                                                        datapath=datapath,
                                                        ipv4_src=src,
                                                        ipv4_dst=dst,
                                                        dst_port=dst_port,
                                                        weight='video',
                                                        queue_id=self.VIDEO_QUEUE,
                                                        protocol=protocol,
                                                        of_priority=3)
                else:
                    out_port, queue_id = self.add_slice(datapath=datapath,
                                                        ipv4_src=src,
                                                        ipv4_dst=dst,
                                                        dst_port=dst_port,
                                                        weight='video',
                                                        queue_id=self.VIDEO_QUEUE,
                                                        protocol=protocol,
                                                        of_priority=3)
            elif dst_port == 10022:
                if dst == "10.255.255.255":
                    self.logger.info("TRY TO ADD BROADCAST 10022")
                    out_port, queue_id = self.add_broadcast_slice(
                                                        in_port=in_port,
                                                        datapath=datapath,
                                                        ipv4_src=src,
                                                        ipv4_dst=dst,
                                                        dst_port=dst_port,
                                                        weight='latency',
                                                        queue_id=self.LATENCY_QUEUE,
                                                        protocol=protocol,
                                                        of_priority=3)
                else:
                    out_port, queue_id = self.add_slice(datapath=datapath,
                                                        ipv4_src=src,
                                                        ipv4_dst=dst,
                                                        dst_port=dst_port,
                                                        weight='latency',
                                                        queue_id=self.LATENCY_QUEUE,
                                                        protocol=protocol,
                                                        of_priority=3)
            elif dst_port == 10023:
                if dst == "10.255.255.255":
                    out_port, queue_id = self.add_broadcast_slice(
                                                        in_port=in_port,
                                                        datapath=datapath,
                                                        ipv4_src=src,
                                                        ipv4_dst=dst,
                                                        dst_port=dst_port,
                                                        weight='mission_critical',
                                                        queue_id=self.CRITICAL_QUEUE,
                                                        protocol=protocol,
                                                        of_priority=3)
                else:
                    out_port, queue_id = self.add_slice(datapath=datapath, ipv4_src=src,
                                                        ipv4_dst=dst, dst_port=dst_port,
                                                        weight='mission_critical',
                                                        queue_id=self.CRITICAL_QUEUE,
                                                        protocol=protocol, of_priority=3)
            else:
                # add other slices
                pass
        elif protocol in self.slice_protocols and dst_port in range(11001, 11445):
            # MULTICAST
            dst = None # destinations are implied by dst_port
            self.add_multicast_flows(msg, src, dst_port)
            return
        else:
            # non-special traffic!
            if dst in self.net and out_port is None:
                if dst == "10.255.255.255":
                    out_port, queue_id = self.add_base_broadcast(
                        datapath=datapath,
                        ipv4_src=src,
                        ipv4_dst=dst)
                else:
                    out_port, queue_id = self.add_base_flow(datapath=datapath,
                                                            ipv4_src=src,
                                                            ipv4_dst=dst)
            else:
                self.logger.info("{} not known to controller, dropping ..".format(dst))
                return
        if out_port is None:
            self.logger.error("\nERROR: NO FLOWS ADDED!!! SWITCH {} : pkt: \n{}\n\nDROPPING..!".format(dpid, pkt))
            return

        actions = [parser.OFPActionSetQueue(queue_id=queue_id),
                   parser.OFPActionOutput(out_port)]

        # make sure data is not lost if packet is not buffered at switch!
        data = None
        if msg.buffer_id == ofproto.OFP_NO_BUFFER:
            data = msg.data

        # PacketOut is the method to send the original packet back to the switch
        # to be forwarded
        out = parser.OFPPacketOut(
            datapath=datapath, buffer_id=msg.buffer_id, in_port=in_port,
            actions=actions, data=data)
        datapath.send_msg(out)
        self.logger.info("TIME elapsed in controller: {}s".format(time.clock() - t1))
        self.logger.info("___________________________packet_in is over\n")

    def add_multicast_flows(self, msg, src, dst_port):
        """Add a multicast flow by decoding the dst_port into a list of
        destination IPs and then use overlapping parts of the shortest paths
        to send packets in an efficient manner."""
        dst_list = []
        for digit in str(dst_port)[2:]:
            tmp_dst = "10.0.0.{}".format(digit)
            if tmp_dst in self.hosts and tmp_dst not in dst_list:
                    dst_list.append(tmp_dst)
        datapath = msg.datapath
        ofproto = datapath.ofproto
        parser = datapath.ofproto_parser
        dpid = datapath.id
        self.logger.info("ADD Multicast flow: src:{src}, destinations:{dst}".format(src=src, dst=", ".join(dst_list)))
        next_ports = dict()
        for dst in dst_list:
            if dst not in self.net:
                continue
            shortest_paths = list(nx.all_shortest_paths(self.net, dpid , dst, "weight"))
            for path in shortest_paths:
                next = path[path.index(dpid) + 1]
                out_port = self.net[dpid][next]['port']
                if out_port in next_ports:
                    next_ports[out_port].add(dst)
                else:
                    next_ports[out_port] = {dst}
        # now we have a dictionary with every next output port and the corresponding
        # destinations, it is still possible that one destination is reachable
        # through multiple output ports!
        ambiguous = set()
        try:
            ambiguous = next_ports[3].intersection(next_ports[4])
            self.logger.info("Ambiguous destinations: {}".format(ambiguous))
        except KeyError:
            pass
        for dst in ambiguous:
            if len(next_ports[3]) < len(next_ports[4]):
                next_ports[3].remove(dst)
            else:
                next_ports[4].remove(dst)

        actions = []
        for out_port, destinations in next_ports.items():
            if len(destinations) == 1:
                dst = destinations.pop()
                actions.extend(
                    [parser.OFPActionSetQueue(queue_id=self.MULTICAST_QUEUE),
                     parser.OFPActionSetField(ipv4_dst=dst),
                     parser.OFPActionSetField(udp_dst=10001),
                     parser.OFPActionOutput(out_port)])
                self.logger.info("Setting UDP-Port to 10001")
                self.logger.info("Sending to {} over switch-port {}".format(dst, out_port))
            else:
                if len(destinations) == 0:
                    continue
                # calculate new port like 11230
                udp_port = "11"
                for dst in destinations:
                    udp_port += dst.split(".")[-1]
                while len(udp_port) < 5:
                    udp_port += "0"
                actions.extend(
                    [parser.OFPActionSetQueue(queue_id=self.MULTICAST_QUEUE),
                     parser.OFPActionSetField(udp_dst=int(udp_port)),
                     parser.OFPActionOutput(out_port)])
                self.logger.info("Setting UDP-Port to {}".format(udp_port))
                self.logger.info("Sending to 10.0.0.254 over switch-port {}".format(out_port))
        match = parser.OFPMatch(
            eth_type=ether_types.ETH_TYPE_IP, ip_proto=17, udp_dst=dst_port,
            ipv4_src=src)
        self.add_any_flow(datapath, match, actions, priority=3)

        data = None
        if msg.buffer_id == ofproto.OFP_NO_BUFFER:
            data = msg.data
        in_port = msg.match['in_port']
        out = parser.OFPPacketOut(
            datapath=datapath, buffer_id=msg.buffer_id, in_port=in_port,
            actions=actions, data=data)
        datapath.send_msg(out)
        self.logger.info("Send packetout")


import time
from lib.callback_handler import CallBackHandler
from machine import Timer

#   Dit: 1 unit
#   Dah: 3 units
#   Intra-character space (the gap between dits and dahs within a character): 1 unit
#   Inter-character space (the gap between the characters of a word): 3 units
#   Word space (the gap between two words): 7 units
#   Characters consist of elements (dits,dahs) that always have a trailing Intra-character space

__version__ = (0, 2, 0)
class StraightKeyer:

    # Events
    NO_TONE = 0
    TONE = 1
    DIT = 2
    DAH = 3
    END_OF_CHAR = 4
    END_OF_WORD = 5
    TOO_SHORT = 6
    TOO_LONG = 7

    defaultHandler = {
        NO_TONE: [(print,'NO_TONE')],
        TONE: [(print,'TONE')],
        DIT: [(print,'DIT')],
        DAH: [(print,'DAH')],
        END_OF_CHAR: [(print,'END_OF_CHAR')],
        END_OF_WORD: [(print,'END_OF_WORD')],
        TOO_SHORT: [(print,'TOO_SHORT')],
        TOO_LONG: [(print,'TOO_LONG')]
    }

    def __init__(self,handler=defaultHandler):
        self.callback = CallBackHandler(handler)
        self.setWpm(17) # WPM count (will self adjust)
        self.startDownTime = 0
        self.downTime = 0
        self.endOfCharTimeout = Timer()
        self.endOfWordTimeout = Timer()
        self.maxDAH = 400 # (millis) This is the DAH length at 9 words per minute
        self.minDIT = 33 # (millis) This is the DIT length at 36 words per minute

    def switchHandler(self,handler):
        self.callback = CallBackHandler(handler)

    def setWpm(self,wpm):
        if wpm < 10:
            print('Try to stay above 10wpm')
            wpm = 10
        if wpm > 35:
            print('Try to stay below 35wpm')
            wpm = 35
        self.unitLength = int(60000 / (wpm * 50)) # (millis) Using PARIS as a word (i.e. 50 units per word)
        self.averageDAH = self.unitLength * 3
        self.averageDIT = self.unitLength
        self.callback(self.getWpm())

    def getWpm(self):
        return int(60000 / (self.unitLength * 50))

    def straightKeyDown(self):
        #print('Key Down')
        self.callback(StraightKeyer.TONE)
        self.startDownTime = time.ticks_ms()
        self.endOfCharTimeout.deinit()
        self.endOfWordTimeout.deinit()

    def straightKeyUp(self):
        #print('Key Up')
        self.callback(StraightKeyer.NO_TONE)
        self.downTime = time.ticks_ms() - self.startDownTime
        self.startDownTime = 0
        self.evaluateDownTime()
        self.endOfCharTimeout.init(mode=Timer.ONE_SHOT,period=int(self.unitLength * 1.7),callback=self.endOfChar)
        self.endOfWordTimeout.init(mode=Timer.ONE_SHOT,period=int(self.unitLength * 4.7),callback=self.endOfWord)

    def endOfChar(self,timer):
        #print('End Of Character')
        self.callback(StraightKeyer.END_OF_CHAR)

    def endOfWord(self,timer):
        #print('End Of Word')
        self.callback(StraightKeyer.END_OF_WORD)

    def evaluateDownTime(self):
        if self.downTime > self.unitLength * 2:
            self.callback(StraightKeyer.DAH)
            self.adjustBasedOnDAH()
        else:
            self.callback(StraightKeyer.DIT)
            self.adjustBasedOnDIT()
        self.callback(self.getWpm())

    def adjustBasedOnDAH(self):
        if self.downTime < self.maxDAH:
            self.averageDAH = int((self.downTime + self.averageDAH * 2) / 3)
            self.averageDIT = int(self.averageDAH / 3)
            self.unitLength = self.averageDIT
        else:
            self.callback(StraightKeyer.TOO_LONG)

    def adjustBasedOnDIT(self):
        if self.downTime > self.minDIT:
            self.averageDIT = int((self.downTime + self.averageDIT * 2) / 3)
            self.averageDAH = self.averageDIT * 3
            self.unitLength = self.averageDIT
        else:
            self.callback(StraightKeyer.TOO_SHORT)


"""Arpeggio helper methods
"""

import collections
import logging
import os
import platform

import numpy as np
from openbabel import openbabel as ob
from Bio.PDB.Atom import Atom
from Bio.PDB.Residue import Residue
from arpeggio.core import SelectionError, SiftMatchError, config

# region general

def setup_filetype(filepath):
    """Sets up argument type to be consumedd by openbabel

    Args:
        filepath (str): Path to the source file

    Returns:
        str: file type
    """
    filetype = 'pdb'

    if filepath.endswith('.pdb') or filepath.endswith('.ent'):
        filetype = 'pdb'
    elif filepath.endswith('.mmcif') or filepath.endswith('.cif'):
        filetype = 'mmcif'

    return filetype

def rename_output_file(original_filename, new_extension):
    """

    Args:
        original_filename (str): [description]
        new_extension (str): [description]

    Returns:
        str: new output file name
    """
    original_extension = os.path.splitext(original_filename)[-1]

    if original_extension:
        return os.path.splitext(original_filename)[0] + new_extension

    return original_filename + new_extension

def max_mem_usage():
    """Returns maximum memory usage of the program thus far, in megabytes, as a string.

    Returns:
        str: Program message
    """

    try:
        import resource
        base = 1024.0 if platform.system() == 'Linux' else 1048576.0
        return str(round(resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / base, 2)) + ' MB'
    except Exception as err:
        return 'Resource usage information not available {}'.format(str(err))
# endregion

# region bonds identification

def is_hbond(donor, acceptor, vdw_comp_factor):
    """Determines if atoms share Hbond.

    Args:
        donor (OBAtom): First atom
        acceptor (OBAtom): Second atom
        vdw_comp_factor (float): Compensation factor for VdW radii
            dependent interaction types.

    Returns:
        int: 1/0 binary information whether or not the atoms share
            Hbond.
    """
    for hydrogen_coord in donor.h_coords:
        h_dist = np.linalg.norm(hydrogen_coord - acceptor.coord)

        if h_dist <= config.VDW_RADII['H'] + acceptor.vdw_radius + vdw_comp_factor:
            if get_angle(donor.coord, hydrogen_coord, acceptor.coord) >= config.CONTACT_TYPES['hbond']['angle rad']:
                return 1

    return 0

def is_weak_hbond(donor, acceptor, vdw_comp_factor):
    """Determines if atoms share a weak Hbond.

    Args:
        donor (OBAtom): First atom
        acceptor (OBAtom): Second atom
        vdw_comp_factor (float): Compensation factor for VdW radii
            dependent interaction types.

    Returns:
        int: 1/0 binary information whether or not the atoms share
            weak Hbond.
    """
    for hydrogen_coord in donor.h_coords:
        h_dist = np.linalg.norm(hydrogen_coord - acceptor.coord)

        if h_dist <= config.VDW_RADII['H'] + acceptor.vdw_radius + vdw_comp_factor:
            if get_angle(donor.coord, hydrogen_coord, acceptor.coord) >= config.CONTACT_TYPES['weak hbond']['angle rad']:
                return 1

    return 0

def is_halogen_weak_hbond(donor, halogen, ob_mol, vdw_comp_factor, bio_to_ob, ob_to_bio):
    """Determines if atoms share a weak halogen bond.

    org. desc: `nbr` WILL BE A BIOPYTHON ATOM ... HOPEFULLY
    Args:
        donor (OBAtom): Donor atom
        halogen (OBAtom): Halogen atom
        ob_mol (OBMol): [description]
        vdw_comp_factor (float): [description]
        bio_to_ob (dict of int): biopython to openbabel mapping
        ob_to_bio (dict of int): openbabel to biopython mapping

    Returns:
        int: 1/0 binary information whether or not the atoms share
            weak weak Hbond.
    """

    # entry 5t45 obabel does not identify bond between
    # SER 246 A (OG) and BEF 903 A (F1) which makes neighbour to be
    # None and then it crashes
    neighbour = get_single_bond_neighbour(ob_mol.GetAtomById(bio_to_ob[halogen]))
    if neighbour is None:
        return 0

    nbr = ob_to_bio[neighbour.GetId()]

    for hydrogen_coord in donor.h_coords:

        h_dist = np.linalg.norm(halogen.coord - hydrogen_coord)

        if h_dist <= config.VDW_RADII['H'] + halogen.vdw_radius + vdw_comp_factor:

            if config.CONTACT_TYPES['weak hbond']['cx angle min rad'] <= get_angle(nbr.coord, halogen.coord, hydrogen_coord) <= config.CONTACT_TYPES['weak hbond']['cx angle max rad']:

                return 1

    return 0

def is_xbond(donor, acceptor, ob_mol, bio_to_ob, ob_to_bio):
    """Determines if atoms share a weak halogen bond.

    Args:
        donor (OBAtom): Donor atom
        acceptor (OBAtom): Halogen atom
        ob_mol (OBMol): [description]

    Returns:
        int: 1/0 binary information whether or not the atoms share
            xbond.
    """

    # `nbr` WILL BE A BIOPYTHON ATOM
    # ... HOPEFULLY
    nbr = ob_to_bio[get_single_bond_neighbour(ob_mol.GetAtomById(bio_to_ob[donor])).GetId()]
    theta = get_angle(nbr.coord, donor.coord, acceptor.coord)

    if theta >= config.CONTACT_TYPES['xbond']['angle theta 1 rad']:
        return 1

    return 0

def update_atom_sift(atom, addition, contact_type='INTER'):
    """[summary]

    Args:
        atom ([type]): [description]
        addition ([type]): [description]
        contact_type (str, optional): Defaults to 'INTER'. [description]
    """

    atom.sift = [x or y for x, y in zip(atom.sift, addition)]

    if contact_type == 'INTER':
        atom.sift_inter_only = [x or y for x, y in zip(atom.sift_inter_only, addition)]

    if 'INTRA' in contact_type:
        atom.sift_intra_only = [x or y for x, y in zip(atom.sift_intra_only, addition)]

    if 'WATER' in contact_type:
        atom.sift_water_only = [x or y for x, y in zip(atom.sift_water_only, addition)]

def update_atom_fsift(atom, addition, contact_type='INTER'):
    """[summary]

    Args:
        atom ([type]): [description]
        addition ([type]): [description]
        contact_type (str, optional): Defaults to 'INTER'. [description]
    """

    atom.actual_fsift = [x or y for x, y in zip(atom.actual_fsift, addition)]

    if contact_type == 'INTER':
        atom.actual_fsift_inter_only = [x or y for x, y in zip(atom.actual_fsift_inter_only, addition)]

    if 'INTRA' in contact_type:
        atom.actual_fsift_intra_only = [x or y for x, y in zip(atom.actual_fsift_intra_only, addition)]

    if 'WATER' in contact_type:
        atom.actual_fsift_water_only = [x or y for x, y in zip(atom.actual_fsift_water_only, addition)]

def update_atom_integer_sift(atom, addition, contact_type='INTER'):
    """[summary]

    Args:
        atom ([type]): [description]
        addition ([type]): [description]
        contact_type (str, optional): Defaults to 'INTER'. [description]
    """

    atom.integer_sift = [x + y for x, y in zip(atom.sift, addition)]

    if contact_type == 'INTER':
        atom.integer_sift_inter_only = [x + y for x, y in zip(atom.sift_inter_only, addition)]

    if 'INTRA' in contact_type:
        atom.integer_sift_intra_only = [x + y for x, y in zip(atom.sift_intra_only, addition)]

    if 'WATER' in contact_type:
        atom.integer_sift_water_only = [x + y for x, y in zip(atom.sift_water_only, addition)]

def sift_xnor(sift1, sift2):
    """[summary]

    Args:
        sift1 ([type]): [description]
        sift2 ([type]): [description]

    Raises:
        ValueError: [description]

    Returns:
        [type]: [description]
    """'''
    '''

    out = []

    for x, y in zip(sift1, sift2):

        if x and not y:  # TF
            out.append(0)

        elif not x and not y:  # FF
            out.append(1)

        elif x and y:  # TT
            out.append(1)

        elif not x and y:  # FT
            out.append(0)

        else:
            raise ValueError('Invalid SIFts for matching: {} and {}'.format(sift1, sift2))

    return out

def sift_match_base3(sift1, sift2):
    """[summary]
    0 = UNMATCHED
    1 = MATCHED
    2 = MATCH NOT POSSIBLE

    Assuming that sift1 is the potential SIFt, and sift2 is the actual SIFt.
    Args:
        sift1 ([type]): [description]
        sift2 ([type]): [description]

    Raises:
        SiftMatchError: [description]
        ValueError: [description]

    Returns:
        [type]: [description]
    """

    out = []

    for x, y in zip(sift1, sift2):

        if x and not y:  # TF
            out.append(0)  # UNMATCHED

        elif not x and not y:  # FF
            out.append(2)  # MATCH NOT POSSIBLE

        elif x and y:  # TT
            out.append(1)  # MATCHED

        elif not x and y:  # FT
            raise SiftMatchError

        else:
            raise ValueError('Invalid SIFts for matching: {} and {}'.format(sift1, sift2))

    return out

def human_sift_match(sift_match, feature_sift=config.FEATURE_SIFT):
    """Takes a base-3 SIFt indicating contact matched-ness and converts it to a human readable form.

    Args:
        sift_match ([type]): [description]
        feature_sift ([type], optional): Defaults to config.FEATURE_SIFT. [description]

    Raises:
        ValueError: [description]

    Returns:
        [type]: [description]
    """

    terms = []

    for e, fp in enumerate(sift_match):

        if fp == 2:  # MATCH NOT POSSIBLE
            continue

        elif fp == 1:
            terms.append('Matched {}'.format(feature_sift[e]))

        elif fp == 0:
            terms.append('Unmatched {}'.format(feature_sift[e]))

        else:
            raise ValueError

    terms.sort()
    return ':'.join(terms)
# endregion

def int2(x):
    '''
    Return integer from base 2 number.

    Can accept a list/tuple of 0s and 1s.
    '''

    if isinstance(x, collections.Iterable):
        x = ''.join([str(k) for k in x])
    else:
        x = str(x)

    return int(x, 2)

def is_digit(x):
    try:
        int(x)
        return True
    except ValueError:
        return False

def int3(x):
    '''
    Return integer from base 3 number.

    Can accept a list/tuple of 0s, 1s and 2s.
    '''

    if isinstance(x, collections.Iterable):
        x = ''.join([str(k) for k in x])
    else:
        x = str(x)

    return int(x, 3)

def selection_parser(selection_list, atom_list):
    '''
    Selection syntax:

    /<chain_id>/<res_num>[<ins_code>]/<atom_name>

    HET:<het_id>

    Other formats will be rejected, for now.
    You can omit fields as long as the number of `/` is correct.

    Selections are additive, so if you select chain A with /A//,
    adding /A/91/C23 won't make any difference.
    '''

    final_atom_list = set([])

    for selection in selection_list:

        # selection_dict = {
        #    'chain': None,
        #    'residue_number': None,
        #    'atom_name': None
        # }

        chain = None
        residue_number = None
        insertion_code = ' '
        # residue_range = None # TODO
        atom_name = None

        current_atom_list = atom_list[:]

        original_selection = selection
        selection = selection.strip()

        if selection.startswith('RESNAME:'):

            selection = selection.replace('RESNAME:', '').strip()

            # RESNAMES ARE MAX LENGTH 3
            if len(selection) > 3:
                raise SelectionError(original_selection)

            current_atom_list = [x for x in current_atom_list if x.get_parent().resname.strip() == selection]

            for selected_atom in current_atom_list:
                final_atom_list.add(selected_atom)

        # SELECT ALL ORGANIC SMALL-MOLECULE LIGANDS
        elif selection.startswith('LIGANDS'):

            current_atom_list = [x for x in current_atom_list if
                                 not x.get_parent().is_polypeptide  # MUST NOT BE POLYPEPTIDE
                                 and len(x.get_parent().child_list) >= 5  # MIN NUMBER OF ATOMS
                                 and len(x.get_parent().child_list) <= 100  # MAX NUMBER OF ATOMS
                                 and 'C' in set([y.element for y in x.get_parent().child_list])  # MUST CONTAIN CARBON
                                 and x.get_parent().resname.strip().upper() not in config.COMMON_SOLVENTS  # MUST NOT BE COMMON SOLVENT
                                 and x.get_parent().resname.strip().upper() not in config.STANDARD_NUCLEOTIDES  # MUST NOT BE NUCLEOTIDE
                                 and not x.get_parent().resname.startswith('+')  # MUST NOT BE MODIFIED NUCLEOTIDE
                                 ]

            for selected_atom in current_atom_list:
                final_atom_list.add(selected_atom)

        elif selection.startswith('/'):

            selection = selection.lstrip('/').split('/')

            # print selection

            if len(selection) != 3:
                raise SelectionError(original_selection)

            # CHAIN
            if selection[0]:
                chain = selection[0]

            # RESIDUE AND INS CODE
            if selection[1]:

                if is_digit(selection[1]):

                    # JUST THE RESNUM
                    residue_number = int(selection[1])

                elif selection[1].isalnum():

                    # CHECK FOR VALID RESNUM+INSCODE
                    if selection[1][-1].isalpha() and is_digit(selection[1][:-1]):

                        residue_number = int(selection[1][:-1])
                        insertion_code = selection[1][-1]

                    else:
                        raise SelectionError(original_selection)

                else:
                    raise SelectionError(original_selection)

            # ATOM NAME
            if selection[2]:

                if not selection[2].isalnum() and "'" not in selection[2]:
                    raise SelectionError(original_selection)
                else:
                    atom_name = selection[2]

            # NOW MAKE THE SELECTION WITH BIOPYTHON
            if chain:
                current_atom_list = [x for x in current_atom_list if x.get_parent().get_parent().id == chain]

            if residue_number:
                current_atom_list = [x for x in current_atom_list if x.get_parent().id[1] == residue_number and x.get_parent().id[2] == insertion_code]

            if atom_name:
                current_atom_list = [x for x in current_atom_list if x.name == atom_name]

            for selected_atom in current_atom_list:
                final_atom_list.add(selected_atom)

        else:
            raise SelectionError(original_selection)

    final_atom_list = list(final_atom_list)

    if not final_atom_list:
        logging.error('Selection was empty.')
        raise SelectionError('entity not found')

    return list(final_atom_list)

def make_pymol_json(entity):
    '''
    Feed me a BioPython atom or BioPython residue.

    See `http://pymol.sourceforge.net/newman/user/S0220commands.html`.

    chain-identifier/resi-identifier/name-identifier
    chain-identifier/resi-identifier/
    '''

    if isinstance(entity, Atom):
        chain = entity.get_parent().get_parent()
        residue = entity.get_parent()
        atom_name = entity.name

        return {
            'label_comp_id': residue.resname,
            'auth_seq_id': residue.id[1],
            'auth_asym_id': chain.id,
            'auth_atom_id': atom_name,
            'pdbx_PDB_ins_code': residue.id[2]
        }

    elif isinstance(entity, Residue):
        chain = entity.get_parent()

        return {
            'label_comp_id': entity.resname,
            'auth_seq_id': entity.id[1],
            'auth_asym_id': chain.id,
            'pdbx_PDB_ins_code': entity.id[2]
        }

    else:
        raise TypeError('Cannot make a json object from non-Atom/Residue object.')

def make_pymol_string(entity):
    """Feed me a BioPython atom or BioPython residue.

    See `http://pymol.sourceforge.net/newman/user/S0220commands.html`.

    chain-identifier/resi-identifier/name-identifier
    chain-identifier/resi-identifier/

    Args:
        entity ([type]): [description]

    Raises:
        TypeError: [description]

    Returns:
        str: String representation of the entity
    """

    if isinstance(entity, Atom):

        chain = entity.get_parent().get_parent()
        residue = entity.get_parent()
        atom_name = entity.name

    elif isinstance(entity, Residue):
        chain = entity.get_parent()
        residue = entity
        atom_name = ''

    else:
        raise TypeError('Cannot make a PyMOL string from a non-Atom or Residue object.')

    res_num = residue.id[1]

    # ADD INSERTION CODE IF NEED BE
    if residue.id[2] != ' ':
        res_num = str(res_num) + residue.id[2]

    macro = '{}/{}/{}'.format(chain.id,
                              res_num,
                              atom_name)

    return macro

def get_single_bond_neighbour(ob_atom):
    """[summary]

    Args:
        ob_atom ([type]): [description]

    Returns:
        [type]: [description]
    """'''
    '''

    for bond in ob.OBAtomBondIter(ob_atom):

        if not (bond.GetBondOrder()==1 and not bond.IsAromatic()):
            continue

        current_neighbour = bond.GetNbrAtom(ob_atom)

        if current_neighbour.GetAtomicNum() == 1:
            continue

        return current_neighbour

    return None

def group_angle(group, point_coords, degrees=False, signed=False):
    """
    Adapted from CREDO: `https://bitbucket.org/blundell/credovi/src/bc337b9191518e10009002e3e6cb44819149980a/credovi/structbio/aromaticring.py?at=default`

    `group` should be a dict with Numpy array 'center' and 'normal' attributes.
    `point_coords` should be a Numpy array.
    """

    cosangle = np.dot(group['normal'], point_coords) / (np.linalg.norm(group['normal']) * np.linalg.norm(point_coords))

    # GET THE ANGLE AS RADIANS
    rad = np.arccos(cosangle)

    if not degrees:
        return rad

    # CONVERT RADIANS INTO DEGREES
    # CONVERT INTO A SIGNED ANGLE
    if signed:
        rad = rad - np.pi if rad > np.pi / 2 else rad

    # RETURN DEGREES
    return rad * 180 / np.pi

def group_group_angle(group, group2, degrees=False, signed=False):
    """Adapted from CREDO: `https://bitbucket.org/blundell/credovi/src/bc337b9191518e10009002e3e6cb44819149980a/credovi/structbio/aromaticring.py?at=default`

    `group` and `group2` should be a dict with Numpy array 'center' and 'normal' attributes.
    Args:
        group ([type]): [description]
        group2 ([type]): [description]
        degrees (bool, optional): Defaults to False. [description]
        signed (bool, optional): Defaults to False. [description]

    Returns:
        [type]: [description]
    """

    cosangle = np.dot(group['normal'], group2['normal']) / (np.linalg.norm(group['normal']) * np.linalg.norm(group2['normal']))

    # GET THE ANGLE AS RADIANS
    rad = np.arccos(cosangle)

    if not degrees:
        return rad

    # CONVERT RADIANS INTO DEGREES
    else:

        # CONVERT INTO A SIGNED ANGLE
        if signed:
            rad = rad - np.pi if rad > np.pi / 2 else rad

        # RETURN DEGREES
        return rad * 180 / np.pi

def get_angle(point_a, point_b, point_c):
    """Get the angle between three points in 3D space.
    Points should be supplied in Numpy array format.

    http://stackoverflow.com/questions/19729831/angle-between-3-points-in-3d-space

    Args:
        point_a ([type]): [description]
        point_b ([type]): [description]
        point_c ([type]): [description]

    Returns:
        [type]: [description]
    """
    # In pseudo-code, the vector BA (call it v1) is:
    # v1 = {A.x - B.x, A.y - B.y, A.z - B.z}
    v1 = point_a - point_b

    # Similarly the vector BC (call it v2) is:
    # v2 = {C.x - B.x, C.y - B.y, C.z - B.z}
    v2 = point_c - point_b

    # The dot product of v1 and v2 is a function of the cosine of the angle between them
    # (it's scaled by the product of their magnitudes). So first normalize v1 and v2:

    # v1mag = sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z)
    v1_mag = np.sqrt(v1[0] * v1[0] + v1[1] * v1[1] + v1[2] * v1[2])

    # v1norm = {v1.x / v1mag, v1.y / v1mag, v1.z / v1mag}
    v1_norm = np.array([v1[0] / v1_mag, v1[1] / v1_mag, v1[2] / v1_mag])

    # v2mag = sqrt(v2.x * v2.x + v2.y * v2.y + v2.z * v2.z)
    v2_mag = np.sqrt(v2[0] * v2[0] + v2[1] * v2[1] + v2[2] * v2[2])

    # v2norm = {v2.x / v2mag, v2.y / v2mag, v2.z / v2mag}
    v2_norm = np.array([v2[0] / v2_mag, v2[1] / v2_mag, v2[2] / v2_mag])

    # Then calculate the dot product:

    # res = v1norm.x * v2norm.x + v1norm.y * v2norm.y + v1norm.z * v2norm.z
    res = v1_norm[0] * v2_norm[0] + v1_norm[1] * v2_norm[1] + v1_norm[2] * v2_norm[2]

    # And finally, recover the angle:
    angle = np.arccos(res)

    if np.isnan(angle):
        logging.warning(f'Angle for <{point_a}, {point_b}, {point_c}> was NaN, setting to PI.')
        angle = np.pi

    return angle

def get_residue_name(entity):
    """
    Returns the name of residue

    Args:
        entity (Atom/Residue): Atom or Residue objects from BioPython

    Returns:
        str: Name of the residue
    """

    if isinstance(entity, Atom):
        residue = entity.get_parent()
        return residue.get_resname()

    elif isinstance(entity, Residue):
        return entity.get_resname()

    else:
        raise TypeError('Cannot return Residue from from non-Atom/non-Residue object.')


from django.http import HttpResponse
from django.shortcuts import render

def home(request):
    return render(request,"index.html")

def reverse(request):
    djtext = request.GET.get("reverse","default")

    final_text = djtext[::-1]

    para = {
        "answer":final_text,
        "purpose":"reversed"
    }
    return render(request,"base.html",para)

def removepunc(request):
    djtext = request.GET.get("removepunc","default")

    mark = ["!","@","#","$","%","^","&","*","(",")","_","-","=","+",",","<",">",".","/","?","{","}","[","]","'",'"']
    ans = ""
    for i in djtext:
        if i not in mark:
            ans += i

    para = {
        "answer":ans,
        "purpose":"punctuation markless"
    }

    return render(request,"base.html",para)

def countChar(request):
    djtext = request.GET.get("countChar","default")
    ans = len(djtext)

    para = {
        "answer":ans,
        "purpose":"charactor count of your"
    }
    return render(request,"base.html",para)

def CapChar(request):
    djtext = request.GET.get("CapChar","default")
    ans = djtext.upper()

    para = {
        "answer":ans,
        "purpose":"capitalized"
    }
    return render(request,"base.html",para)


from django.urls import path

from workflow.views.container_views import GetSetDigitizedContainer, GetContainerMetadata, \
    GetContainerMetadataByLegacyID
from workflow.views.finding_aids_views import GetFAEntityMetadataByItemID
from workflow.views.translation_view import GetTranslationToOriginal, GetTranslationToEnglish

app_name = 'workflow'

urlpatterns = [
    # Used by the digitization workflow to push technical metadata
    path('containers/<str:barcode>/', GetSetDigitizedContainer.as_view(),
         name='list_set_digitized_container'),

    # Used by the digitization workflow to collect descriptive metadata
    path('containers/metadata/<str:barcode>/', GetContainerMetadata.as_view(),
         name='list_get_container_metadata'),

    # Used by research-cloud-upload workflow
    path('containers/by-legacy-id/<str:legacy_id>/', GetContainerMetadataByLegacyID.as_view(),
         name='get_container_by_legacy_id'),

    # Used by research-cloud-upload workflow
    path('finding_aids/by-item-id/<str:item_id>/', GetFAEntityMetadataByItemID.as_view(),
         name='get_fa_entity_by_item_id'),

    # Used by admin UI to translate documents
    path('translate_to_original/', GetTranslationToOriginal.as_view(),
         name='get_translation'),
    path('translate_to_english/', GetTranslationToEnglish.as_view(),
         name='get_translation')
]


# from sklearn.feature_selection import VarianceThreshold
import pandas as pd
# import pickle
# import joblib
# from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
# from sklearn.linear_model import LinearRegression, Lasso
from sklearn.linear_model import RidgeClassifier
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import mean_squared_error
from sklearn.neighbors import KNeighborsClassifier
import contextlib
import time
# import numba
# from sklearn.tree import DecisionTreeClassifier
# from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import AdaBoostClassifier, \
    GradientBoostingClassifier, \
    RandomForestClassifier
from sklearn.neural_network import MLPClassifier
# from sklearn.feature_selection import SelectKBest
# from sklearn.feature_selection import chi2
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import classification_report, confusion_matrix
import seaborn as sns
# from xgboost import XGBRegressor
from xgboost import XGBClassifier
# from sklearn.preprocessing import LabelEncoder
# from sklearn.datasets import make_regression
# from sklearn.model_selection import RepeatedStratifiedKFold
from sklearn.model_selection import GridSearchCV
# from utils import visualise_predictions
# from sklearn.metrics import accuracy_score
# from sklearn.metrics import precision_score
# from sklearn.metrics import recall_score
# from sklearn.metrics import make_scorer
from sklearn.linear_model import SGDClassifier
# from sklearn.ensemble import ExtraTreesClassifier
# import statsmodels.api as sm
# from sklearn.feature_selection import SelectFromModel
# from sklearn.svm import LinearSVC

'''
class LinearRegression:
    def __init__(self, n_features: int):  # initialize parameters
        np.random.seedDTC = DecisionTreeClassifier(random_state = 11,
        max_features = "auto", class_weight = "auto",max_depth = None)(10)
        self.W = np.random.randn(n_features, 1)  # randomly initialise weight
        self.b = np.random.randn(1)  # randomly initialise bias

    def __call__(self, X):
# how do we calculate output from an input in our model?
        ypred = np.dot(X, self.W) + self.b
        return ypred  # return prediction

    def update_params(self, W, b):
        self.W = W
        # set this instance's weights
        # to the new weight value passed to the function
        self.b = b  from sklearn.metrics import mean_squared_error
do the same for the bias

df = fetch_california_housing(as_frame=True)['data']

X, y = fetch_california_housing(return_X_y=True)GradientBoostingRegressor
print(X_train.shape, y_train.shape)

model = LinearRegression(n_features=8)  # instantiate our linear model
y_pred = model(X_test)  # make prediction on data
print("Predictions:\n", y_pred[:10])  # print first 10 predictions
'''

@contextlib.contextmanager
def timer(function):
    start = time.time()
    yield
    print(f"Elapsed time for {function.__name__}: {(time.time() - start)}")

def plot_predictions(y_pred, y_true, title):
    samples = len(y_pred)
    plt.figure()
    plt.title(title)
    plt.scatter(np.arange(samples), y_pred, c='r', label='predictions')
    plt.scatter(np.arange(samples), y_true, c='b',
                label='true labels', marker='x')
    plt.legend()
    plt.xlabel('Sample numbers')
    plt.ylabel('Values')
    plt.show()

# def mean_squared_error(y_pred, y_true):
# # define our criterion (loss function)
#     errors = y_pred - y_true  # calculate errors
#     squared_errors = errors ** 2  # square errors
#     return np.mean(squared_errors)

# def minimize_loss(X_train, y_train):
#     X_with_bias = np.hstack((np.ones((X_train.shape[0], 1)), X_train))
#     optimal_w = np.matmul(
#         np.linalg.inv(np.matmul(X_with_bias.T, X_with_bias)),
#         np.matmul(X_with_bias.T, y_train),
#     )from sklearn.metrics import classification_report, confusion_matrix
#     return optimal_w[1:], optimal_w[0]

# weights, bias = minimize_loss(X_train, y_train)
# print(weights, bias)
# cost = mean_squared_error(y_pred, y_train)
# print(cost)

# model.update_params(weights, bias)
# y_pred = model(X_train)
# cost = mean_squared_error(y_pred, y_train)
# print(cost)

# sel = VarianceThreshold(threshold=(.8 * (1 - .8)))
# X = sel.fit_transform(X)

# X = SelectKBest(chi2, k=20).fit_transform(X, y)

# X_validation, X_test, y_validation, y_test = train_test_split(
#     X_test, y_test, test_size=0.5
# )

models = [  # LinearRegression(),
    KNeighborsClassifier(n_neighbors=151),
    # MLPClassifier(hidden_layer_sizes=(150, 100, 50), max_iter=1000,
    #               activation='tanh', solver='adam', random_state=1,
    #               learning_rate='adaptive'),
    # # MLPRegressor(activation='tanh', alpha=0.1,
    #    hidden_layer_sizes=(150, 100, 50),
    #    learning_rate='adaptive', solver='sgd',
    #    max_iter=1000),
    # DecisionTreeClassifier(random_state=1,
    #  max_features="sqrt",
    #  max_depth=None),
    # DecisionTreeRegressor(criterion='squared_error',
    # max_depth=5),
    # Lasso(alpha=0.00023),
    AdaBoostClassifier(learning_rate=1.0, n_estimators=10000),
    # AdaBoostRegressor(learning_rate=0.01, n_estimators=10000),
    RandomForestClassifier(
        criterion='entropy', max_depth=128,
        max_features='log2', n_estimators=1024),
    # RandomForestRegressor(criterion='poisson',
    # max_depth=12, max_features='log2',
    # n_estimators=256),
    GradientBoostingClassifier(criterion='friedman_mse',
                               learning_rate=0.2, loss='log_loss',
                               max_depth=8, max_features='sqrt',
                               min_samples_leaf=0.1,
                               min_samples_split=0.18,
                               n_estimators=10, subsample=1),
    # GradientBoostingRegressor(criterion='friedman_mse',
    # learning_rate=0.2, loss='squared_error',
    # max_depth=8, max_features='log2',
    # min_samples_leaf=0.1,
    # min_samples_split=0.18,
    # n_estimators=10, subsample=1),
    XGBClassifier(learning_rate=0.01, max_depth=6, n_estimators=324),
    # XGBRegressor(learning_rate=0.05, max_depth=4, n_estimators=220),
    SGDClassifier(alpha=0.01, loss='log_loss', penalty='none'),
    # SGDRegressor(alpha=0.01, loss='squared_error', penalty='none'),
    RidgeClassifier()
]

def model_comparisons(models, columns=None):
    data = pd.read_csv('cleaned_dataset.csv')
    y = data['Result'].values
    X = data.drop(['Result', 'Date_New', 'Link'], inplace=False, axis=1)
    X.League = X.League.astype('category').cat.codes
    if columns is None:
        columns = X.columns
    X_sc = scale_array(X[columns])
    X_train, X_test, y_train, y_test = feature_selection(columns)
    for model in models:
        model = model.fit(X_train, y_train)
        y_pred = model.predict(X_test)
        plot_predictions(y_pred[:10], y_test[:10], model)
        print(f'Mean Squared Error:{mean_squared_error(y_test, y_pred)}')
        score = model.score(X_sc, y)
        print(f'Score: {score}')
        try:
            cm = confusion_matrix(y_test, y_pred)
            print(cm)
            print(
                f'Classification Report:  \
                    {classification_report(y_test, y_pred)}')
            plt.figure(figsize=(6, 6))
            sns.heatmap(cm, annot=True, fmt="d", linewidths=.5,
                        square=True, cmap="Blues_r")
            plt.ylabel("Actual label")
            plt.xlabel("Predicted label")
            all_sample_title = f"Accuracy Score:{score}"
            plt.title(all_sample_title, size=15)
            print(accuracy(cm))
        except ValueError:
            pass
        # print(accuracy_score(y_pred, y_test))
        print(sum(y_pred.round() == y_test)/len(y_pred))
        # dump(model, f'{str(model)}.joblib')

def MLPGridSearch(X_sc, y):
    mlp_gs = MLPClassifier(max_iter=1000)
    parameter_space = {
        'hidden_layer_sizes': [(10, 30, 10), (20,), (150, 100, 50)],
        'activation': ['tanh', 'relu'],
        'solver': ['sgd', 'adam'],
        'alpha': [0.0001, 0.05],
        'learning_rate': ['constant', 'adaptive'],
    }
    clf = GridSearchCV(mlp_gs, parameter_space, n_jobs=-1, cv=5)
    clf.fit(X_sc, y)  # X is train samples and y is the corresponding labels

def scale_array(df):
    scaler = MinMaxScaler()
    scaler.fit(df)
    X_sc = scaler.transform(df)
    return X_sc

def grid_search(estimator, parameters, columns):
    '''
    param_test1 = {
    "loss":["squared_error","absolute_error"],
    "learning_rate": [0.01, 0.025, 0.05, 0.075, 0.1, 0.15, 0.2],
    "min_samples_split": np.linspace(0.1, 0.5, 6),
    "min_samples_leaf": np.linspace(0.1, 0.5, 6),
    "max_depth":[3,5,8],
    "max_features":["log2","sqrt"],
    "criterion": ["friedman_mse",  "squared_error"],
    "subsample":[0.5, 0.618, 0.8, 0.85, 0.9, 0.95, 1.0],
    "n_estimators":[10]
    }
    '''
    X_train, X_test, y_train, y_test = feature_selection(columns)
    gsearch1 = GridSearchCV(estimator=estimator,
                            param_grid=parameters, n_jobs=4, cv=5)
    gsearch1.fit(X_train, y_train)
    gsearch1.best_params_

important_columns = ['Season', 'Home_Team_Goals_For_This_Far',
                     'Home_Team_Goals_Against_This_Far',
                     'Away_Team_Goals_For_This_Far',
                     'Away_Team_Goals_Against_This_Far',
                     'Home_Team_Points',
                     'Away_Team_Points', 'Elo_home', 'Elo_away', 'Capacity',
                     'Home_Team_Yellows_This_Far',
                     'Away_Team_Yellows_This_Far']

important_features = [0, 3, 4, 5, 6, 7, 8, 15, 16, 17, 20, 22]

def feature_selection(features):
    data = pd.read_csv('cleaned_dataset.csv')
    y = data['Result'].values
    X = data.drop(['Result', 'Date_New', 'Link'], inplace=False, axis=1)
    X.League = X.League.astype('category').cat.codes
    X = X[features].values
    X_sc = scale_array(X)
    X_train, X_test, y_train, y_test = train_test_split(X_sc, y, test_size=0.1)
    return X_train, X_test, y_train, y_test

def accuracy(confusion_matrix):
    diagonal_sum = confusion_matrix.trace()
    sum_of_all_elements = confusion_matrix.sum()
    return diagonal_sum / sum_of_all_elements

def feature_select_RF():
    model = RandomForestClassifier()
    data = pd.read_csv('cleaned_dataset.csv')
    y = data['Result'].values
    X = data.drop(['Result', 'Date_New', 'Link'], inplace=False, axis=1)
    X_train, X_test, y_train, y_test = feature_selection(important_columns)
    model = model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    plot_predictions(y_pred[:10], y_test[:10], model)
    print(f'Mean Squared Error:{mean_squared_error(y_test, y_pred)}')
    score = model.score(scale_array(X[important_columns]), y)
    print(f'Score: {score}')
    try:
        cm = confusion_matrix(y_test, y_pred)
        print(cm)
        print(
            f'Classification Report:  \
                {classification_report(y_test, y_pred)}')
        plt.figure(figsize=(6, 6))
        sns.heatmap(cm, annot=True, fmt="d", linewidths=.5,
                    square=True, cmap="Blues_r")
        plt.ylabel("Actual label")
        plt.xlabel("Predicted label")
        all_sample_title = f"Accuracy Score:{score}"
        plt.title(all_sample_title, size=15)
    except ValueError:
        pass
    # print(accuracy_score(y_pred, y_test))
    print(sum(y_pred.round() == y_test)/len(y_test))

key_columns = ['Result', 'Home_Team_Goals_For_This_Far',
               'Home_Team_Goals_Against_This_Far',
               'Away_Team_Goals_For_This_Far',
               'Away_Team_Goals_Against_This_Far', 'Home_Team_Points',
               'Away_Team_Points', 'Home_Team_Losing_Streak',
               'Away_Team_Losing_Streak', 'Home_Team_Winning_Streak',
               'Away_Team_Winning_Streak', 'Home_Team_Unbeaten_Streak',
               'Away_Team_Unbeaten_Streak', 'Elo_home', 'Elo_away',
               'Home_Wins_This_Far', 'Home_Losses_This_Far',
               'Away_Wins_This_Far',
               'Away_Losses_This_Far', 'Home_Wins_This_Far_at_Home',
               'Home_Losses_This_Far_at_Home', 'Home_Wins_This_Far_Away',
               'Home_Losses_This_Far_Away', 'Away_Wins_This_Far_at_Home',
               'Away_Losses_This_Far_at_Home', 'Away_Wins_This_Far_Away',
               'Away_Losses_This_Far_Away', 'Capacity', 'Home_Yellow',
               'Away_Red',
               'Home_Points_Per_Game', 'Home_Goals_Per_Game',
               'Home_Goals_Against_Per_Game', 'Away_Points_Per_Game',
               'Away_Goals_Per_Game', 'Away_Goals_Against_Per_Game']

svm_cols = ['Season', 'Teams_in_League', 'Home_Team_Goals_For_This_Far',
            'Home_Team_Goals_Against_This_Far', 'Away_Team_Goals_For_This_Far',
            'Away_Team_Goals_Against_This_Far', 'Home_Team_Points',
            'Away_Team_Points', 'Away_Team_Winning_Streak',
            'Home_Team_Unbeaten_Streak', 'Away_Team_Unbeaten_Streak',
            'Elo_home',
            'Elo_away', 'Home_Wins_This_Far', 'Home_Draws_This_Far',
            'Home_Losses_This_Far', 'Away_Draws_This_Far',
            'Home_Wins_This_Far_at_Home', 'Home_Draws_This_Far_at_Home',
            'Home_Losses_This_Far_at_Home', 'Home_Draws_This_Far_Away',
            'Away_Wins_This_Far_at_Home', 'Away_Draws_This_Far_at_Home',
            'Away_Losses_This_Far_at_Home', 'Away_Wins_This_Far_Away',
            'Away_Draws_This_Far_Away', 'Capacity', 'Home_Yellow',
            'Away_Team_Yellows_This_Far', 'Away_Red', 'Home_Points_Per_Game',
            'Home_Goals_Per_Game', 'Home_Goals_Against_Per_Game',
            'Away_Points_Per_Game', 'Away_Goals_Per_Game',
            'Away_Goals_Against_Per_Game', 'Away_Cards_Per_Game',
            'Pitch_Match',
            'League']

def final_test():
    X = pd.read_csv('cleaned_dataset.csv')
    X = X[X.Season != 2022]
    X = X[X.League != 'eerste_divisie']
    X = X[X.Season > 2012]
    X.League = X.League.astype('category').cat.codes
    y = X['Result'].values
    X = X[svm_cols]
    X_sc = scale_array(X)
    model = RandomForestClassifier(
        criterion='entropy', max_depth=128,
        max_features='log2', n_estimators=1024)
    model = model.fit(X_sc, y)
    test_data = pd.read_csv('cleaned_results.csv')
    y_test = test_data.Result.values
    test_data = test_data[svm_cols]
    test_data.League = test_data.League.astype('category').cat.codes
    X_new = scale_array(test_data)
    y_pred = model.predict(X_new)
    cm = confusion_matrix(y_pred, y_test)
    print(cm)
    print(accuracy(cm))

# X_train, X_test, y_train, y_test = feature_selection(important_columns)
if __name__ == '__main__':
    # model_comparisons(models)
    pass


import torch

import torchvision
import torchvision.transforms as transforms

from PIL import Image

# set up model
model_conv = torchvision.models.resnet50(pretrained=True)
num_ftrs = model_conv.fc.in_features
model_conv.fc = torch.load('./test_model_resnet.pth', map_location=torch.device('cpu'))
model_conv = model_conv.to('cpu')  # force cpu
model_conv.eval()

# image loading pipeline
transform = transforms.Compose([
        transforms.Resize(224),
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]
)

def image_loader(image_name):
    image = Image.open(image_name).convert('RGB')
    image = transform(image)
    return image.unsqueeze(0)

def predict(request):

    # Set CORS headers for the preflight request
    if request.method == 'OPTIONS':
        # Allows GET/POST requests from any origin with the Content-Type
        # header and caches preflight response for an 3600s
        headers = {
            'Access-Control-Allow-Origin': '*',
            'Access-Control-Allow-Methods': 'GET, POST',
            'Access-Control-Allow-Headers': 'Content-Type',
            'Access-Control-Max-Age': '3600'
        }

        return ('', 204, headers)

    # Set CORS headers for the main request
    headers = {
        'Access-Control-Allow-Origin': '*'
    }

    if 'file' not in request.files:
        return {'success': False, 'message': 'Image not found'}, 400, headers
    image = image_loader(request.files['file'])
    out = model_conv(image)
    pred = out.tolist()[0]

    # process predictions
    labels = [
        'amusement',
        'anger',
        'awe',
        'contentment',
        'disgust',
        'excitement',
        'fear',
        'sadness'
    ]

    result = dict(zip(labels, pred))
    return {'success': True, 'results': result}, 200, headers


from django.shortcuts import render, HttpResponse
from app01 import models
import json

# Create your views here.

def students(request):
    stu_list = models.Students.objects.all()
    cls_list = models.Classes.objects.all()

    return render(request, 'students.html', locals())

def add_student(request):
    # print(request.POST)
    response = {'status': True, 'msg': '添加成功'}
    username = request.POST.get('username')
    age = request.POST.get('age')
    gender = request.POST.get('gender')
    cs_id = request.POST.get('classes')
    try:
        obj = models.Students.objects.create(username=username, age=age, gender=gender, cs_id=cs_id)
        response['data'] = obj.id
    except Exception as e:
        response['status'] = False
        response['msg'] = '用户输入错误'
        response['data'] = None
    return HttpResponse(json.dumps(response))

def del_student(request):
    nid = request.POST.get('nid')
    response = {'status': True, 'msg': '删除成功'}
    try:
        models.Students.objects.filter(id=nid).delete()
    except Exception as e:
        response['status'] = False
        response['msg'] = '删除失败'
    return HttpResponse(json.dumps(response))
def edit_student(request):

    response = {'code':1000,'msg':None}
    try:
        nid = request.POST.get('nid')
        gender = request.POST.get('gender')
        user = request.POST.get('user')
        cls_id = request.POST.get('cls_id')
        age = request.POST.get('age')
        models.Students.objects.filter(id=nid).update(username=user,age=age,gender=gender,cs_id=cls_id)
    except Exception as e:
        response['code'] = 1001
        response['msg'] = str(e)
    return HttpResponse(json.dumps(response))

#分页组件

USER_LIST = []
for i in range(1,666):
    temp={'name':'root'+str(i),'age':i}
    USER_LIST.append(temp)
def index(request):
    per_count = 10
    current_page=request.GET.get('p')
    current_page = int(current_page)

    start = (current_page -1) * per_count
    end = current_page * per_count
    data = USER_LIST[start:end]

    prev_page= current_page-1
    next_page = current_page+1
    return render(request,'index.html',{'users':data,'prev_page':prev_page,'next_page':next_page})

from django.core.paginator import Paginator,EmptyPage,PageNotAnInteger
# 扩展内置分页
class CustomPaginator(Paginator):
    def __init__(self,current_page,per_pager_num,*args,**kwargs):
        # 当前页
        self.current_page = int(current_page)
        # 最多显示的页码数量
        self.per_pager_num = int(per_pager_num)
        super(CustomPaginator,self).__init__(*args,**kwargs)
    # 定义默认显示的页码范围
    def pager_num_range(self):
        # 总页数
        # self.num_pages
        if self.num_pages < self.per_pager_num:
            return range(1,self.num_pages+1)
#         总页数特别多
        part = int(self.per_pager_num/2)
        if self.current_page <= part:
            return range(1,self.per_pager_num+1)
        # 如果当前页 加上 5 大于最多页
        if (self.current_page + part) > self.num_pages:
            return range(self.num_pages-self.per_pager_num+1,self.num_pages+1)
        return range(self.current_page-part,self.current_page+part+1)

# 内置分页组件
def index1(request):
    current_page = request.GET.get('p')

    # 全部数据 :USER_LIST   ->得出一共多少数据
    # 10 每页显示的条数

    # paginiator = Paginator(USER_LIST,10)
    paginiator = CustomPaginator(current_page,11,USER_LIST, 10)
    try:
        posts = paginiator.page(current_page)
    except PageNotAnInteger:
        posts = paginiator.page(1)
    except EmptyPage:
        posts = paginiator.page(paginiator.num_pages)
    return render(request,'index1.html',{'post':posts})

def index2(request):
    from app01.pager import Pagination
    current_page = request.GET.get('p')
    page_obj = Pagination(666,current_page,10)

    data_list = USER_LIST[page_obj.start():page_obj.end()]
    return render(request,'index2.html',{'data':data_list,'page_obj':page_obj})


#miniproject PART1 - a neuron should learn logical and operation
#-----------------IMPORTS-------------------
import Neuron
import Trainer
import numpy as np
import matplotlib.pyplot as plt
#------------------------------------------

#first randomly initilized weights and set bias to 1
w1 = np.random.randn()
w2 = np.random.randn()
w = [w1,w2]
b = 1
and_training_set ={(0,0):0,(0,1):0,(1,0):0,(1,1):1}

and_neuron = Neuron.LogicGateNeuron(w,b) #create an instance of LogicGateNeuron

#using a trainer to train the and_neuron !
epochs = 50
and_trainer = Trainer.Trainer(and_neuron,and_training_set)
and_trainer.train(epochs,lr = 2)

#plotting training erro vs epochs:---------------------------
plt.plot(range(0,epochs),and_trainer.trainerrList)
plt.axis([0 , epochs , 0 , 0.5])
plt.title("Traning ERROR for and_neuron")
plt.xlabel("epochs")
plt.ylabel("MSE")
plt.show()
#-------------------------------------------------------------
acc = and_trainer.trainerrList[epochs-1] #accuracy
print(acc)


# the data loader for imagenet
# by mym
# images are resized so that shorter side is 256pix, then cropped according to a given size
import numpy as np
from skimage import io
from skimage.transform import resize
import matplotlib.pyplot as plt
from os import listdir as ldir

class imagenet_loader:
  def __init__(self,class_seq=None):
    # create a data generator only for given classes
    # class_seq is a list of string like [n014xxxxx,n013xxxxx,...]
    self.class_ids=self.get_class_ids()
    if class_seq!=None:
      self.class_ids=class_seq
    self.image_names={}
    self.get_image_names()
    self.img_coordinates=[]
    self.get_image_coordinates()
    self.sample_counter=0
    self.batch_counter=0
    self.class_num=len(self.class_ids)

  def get_class_ids(self):
    # get class ids(folder names)
    # thanks to https ://gist.github.com/aaronpolhamus/964a4411c0906315deb9f4a3723aac57
    class_id_original=open("./map_clsloc.txt","r").read().split("\n")
    class_id=[]
    for line in class_id_original:
      class_id.append(line[:9])
    return class_id

  def get_image_names(self):
    # get the listed image names for each folder in training set
    for class_name in self.class_ids:
      self.image_names[class_name]=ldir("./train/"+class_name)

  def get_image_coordinates(self):
    # get all possible 2d image ids: [class_id,image_id]---[str,int]
    # for later random shuffle
    for class_name in self.image_names:
      for i in range(len(self.image_names[class_name])):
        self.img_coordinates.append([class_name,i])

  def preprocess(self,image,shorter=256):
    # resize given image so that shorter edge is the given size
    # image shape: [H,W,C]
    sh=np.shape(image)
    if sh[0]<=sh[1]:
      img=resize(image,output_shape=(shorter,int(sh[1]*shorter/sh[0])))
    else:
      img=resize(image,output_shape=(int(sh[0]*shorter/sh[1]),shorter))
    return img

  def random_crop(self,image,size=224):
    # crop a random square area of the given image
    # image shape: [H,W,C]
    sh=np.shape(image)
    shifted_h=np.random.randint(low=0,high=sh[0]-size)
    shifted_w=np.random.randint(low=0,high=sh[1]-size)
    return image[shifted_h:(shifted_h+size),shifted_w:(shifted_w+size),:]

  def train_next_batch(self,batch_size,batch_per_epoch=float("inf")):
    batch_x=[]
    batch_y=[]
    np.random.shuffle(self.img_coordinates)
    while True:
      class_name=self.img_coordinates[self.sample_counter][0]
      image_name=self.image_names[self.img_coordinates[self.sample_counter][0]][self.img_coordinates[self.sample_counter][1]]
      img=io.imread("./train/"+class_name+"/"+image_name)
      img=self.preprocess(img)
      crop=self.random_crop(img)
      io.imsave("./test.png",crop)
      batch_x.append(crop)
      batch_y.append(np.eye(self.class_num)[self.class_ids.index(class_name)])
      self.sample_counter+=1
      if self.sample_counter%batch_size==0 or self.sample_counter==len(self.img_coordinates):
        yield [batch_x,batch_y]
        batch_x=[]
        batch_y=[]
      if self.batch_counter==batch_per_epoch or self.sample_counter==len(self.img_coordinates):
        np.random.shuffle(self.img_coordinates)
        self.sample_counter=0


from collections import deque
import time

import psutil
import matplotlib.pyplot as plt

from nupic.data.inference_shifter import InferenceShifter
from nupic.frameworks.opf.model_factory import ModelFactory

import model_params

SECONDS_PER_STEP = 2
WINDOW = 60

# turn matplotlib interactive mode on (ion)
plt.ion()
fig = plt.figure()
# plot title, legend, etc
plt.title('CPU prediction example')
plt.xlabel('time [s]')
plt.ylabel('CPU usage [%]')

def runCPU():
  """Poll CPU usage, make predictions, and plot the results. Runs forever."""
  # Create the model for predicting CPU usage.
  model = ModelFactory.create(model_params.MODEL_PARAMS)
  model.enableInference({'predictedField': 'cpu'})
  # The shifter will align prediction and actual values.
  shifter = InferenceShifter()
  # Keep the last WINDOW predicted and actual values for plotting.
  actHistory = deque([0.0] * WINDOW, maxlen=60)
  predHistory = deque([0.0] * WINDOW, maxlen=60)

  # Initialize the plot lines that we will update with each new record.
  actline, = plt.plot(range(WINDOW), actHistory)
  predline, = plt.plot(range(WINDOW), predHistory)
  # Set the y-axis range.
  actline.axes.set_ylim(0, 100)
  predline.axes.set_ylim(0, 100)

  while True:
    s = time.time()

    # Get the CPU usage.
    cpu = psutil.cpu_percent()

    # Run the input through the model and shift the resulting prediction.
    modelInput = {'cpu': cpu}
    result = shifter.shift(model.run(modelInput))

    # Update the trailing predicted and actual value deques.
    inference = result.inferences['multiStepBestPredictions'][5]
    if inference is not None:
      actHistory.append(result.rawInput['cpu'])
      predHistory.append(inference)

    # Redraw the chart with the new data.
    actline.set_ydata(actHistory)  # update the data
    predline.set_ydata(predHistory)  # update the data
    plt.draw()
    plt.legend( ('actual','predicted') )

    # Make sure we wait a total of 2 seconds per iteration.
    try:
      plt.pause(SECONDS_PER_STEP)
    except:
      pass

if __name__ == "__main__":
  runCPU()