#!/usr/bin/env python

###################################################################################################
#                                Multiple Hypothesis Tracker Class                                #
#                                                                                #
# Multiple Hypothesis Tracker with added functionality to suspend and restart tracks for objects  #
# that become occluded. Multiple possible data associations are explored for a number of frames   #
# before any decisions are made. Two implementations of a multiple hypothesis tracker are         #
# available.                                                                                #
#                                                                                #
# The first uses hypotheses, groups and assignment matrices and is based on the work              #
# of Ingemar J. Cox and Sunita L. Hingorani in "An Efficient Implementation of Reid's Multiple    #
# Hypothesis Tracking Algorithm and Its Evaluation for the Purpose of Visual Tracking", 1996.     #
#                                                                                #
# The second implementation uses the Maximum Weighted Independent Set implementation outlined in  #
# the paper "Multiple hypothesis tracking revisited" by C. Kim et. al. in 2015 and is based on    #
# code from openmht by Jon Perdomo available at https://github.com/jonperdomo/openmht             #
#                                                                                #
# Author: Marco Grond                                                                             #
# Version: 0.1.0                                                                                #
###################################################################################################

import numpy as np
import math
from WeightedGraph import WeightedGraph
from Tree import Tree
from TrackTree import TrackTree
from Group import Group
from copy import deepcopy
from scipy.optimize import linear_sum_assignment as hungarian

import time

class MHT:

    def __init__(self, model, gateDistance, timeDrop, minTrackUpdates, pNewTrack, pFalse, pDetect,
                 endLambda, nScan, maxHypotheses, minHypothesisRatio, timeInterval=1, modelArgs=(),
                 debug=False):
        """
        Initialize an instance of the Multiple Hypothesis Tracker.

        model - The model to be used when predicting the movement of an object (Kalman)
        gateDistance - The max distance at which a detection may be associated with a track, i.e.
                       the gating distance for a predicted location and object detections
        timeDrop - The time that a track will be kept without an update
        minTrackLen - The minimum number of updates that a track needs to have to be considered
                      a true track

        ###pTrue - The probability of detecting a track

        pNewTrack - The probability of starting a new track
        pFalse - The probability that a data point will not be assigned
        pDetect - The probability of detecting an object that was previously detected
        endLambda - A constant used to determine the likelihood of a track ending
        nScan - The allowed depth of track trees before they are pruned (the length of time a track
                tree is allowed to diverge before being pruned)
        maxHypotheses - The maximum number of hypotheses that are allowed
        minHypothesisRatio - The minimum ratio allowed between the largest and smallest hypotheses
                             weights in a single group
        timeInterval - The interval between different detections (for a frame by frame, would be 1
                        and for seconds at 30 fps would be 1/30)
        modelArgs - Additional arguments passed to the model during initialization. When an
                    instance of the model is initialized, the initial measurement data, time and
                    these arguments are passed. Arguments should be a tuple.
        """

        # Initialize variables used during the operation of the MHT
        self.MODEL = model(*modelArgs)
        self.GATE = gateDistance
        self.MAXT = timeDrop
        self.TIMEINT = timeInterval

        # Set up variables used for computing weights for nodes and leaves
        self.FALSEALARM = np.log(pFalse)
        self.NEWLOG = np.log(pNewTrack)
        self.DETECTLOG = np.log(pDetect)
        self.SKIPLOG = np.log(1.0 - pDetect)
        self.ENDLAMBDA = endLambda

        # Initialize variables to maintain track trees
        self.NSCAN = nScan

        # Initialize datastructures to store track information
        self.TRACKTREES = {}
        self.TRACKID = 1
        self.NODEID = 1
        self.MINTRACKUPDATES = minTrackUpdates
        self.ENDEDTRACKS = {}

        # Initialize datastructures for suspending, restarting and ending tracks
        self.SUSPENDLIST = {}
        self.POSSIBLERESTART = {}
        self.POSSIBLEEND = {}
        self.RESTARTMAP = {}

        # Initialize datastructures and variables for grouping
        self.GROUPS = []
        self.MAXHYPOS = maxHypotheses
        self.MINHYPRATIO = -np.log(minHypothesisRatio)

        self.DEBUG = debug

    def iterateMWIS(self, detectLocs, t):
        """
        Move all the tracks forward to the given time and associate the given detections with
        existing tracks or create new tracks using a multiple hypothesis tracker

        detectLocs - A list of locations and covariances (x, P) corresponding to object detections
        t - The current time step (i.e. the time step at which the detections took place)
        """

        # Compute groups for all of the track trees
        groups = self.getGroups(t-1)

        # Print Iteration information
        print("*"*100)
        print("Time           :", t)
        print("Num Track Trees:", len(self.TRACKTREES))
        print("Num Groups     :", len(groups))

        if self.DEBUG:
            print("Performing iteration with:")
            print("\tObject detections:", detectLocs)

        # Prune all track trees according to the global hypothesis
        startTime = time.time()
        keep, endedTrees = self.computeGlobalHypothesis(groups)
        endTime = time.time()
        print("Global Hypothesis Time:", (endTime - startTime))
        for tt, keepBranch in keep:
            if not self.TRACKTREES[tt].keep(keepBranch, t):
                print("***WARNING: Attempted to keep non existant branch", keepBranch,
                      "in tree", tt, "***")
                print("Removing tree...")
                self.TRACKTREES[tt].end()
                ended = self.TRACKTREES.pop(tt)
                if (ended.getNumUpdates() >= self.MINTRACKUPDATES):
                    self.ENDEDTRACKS[tt] = ended

        # End all track trees that need to be removed accoring to the global hypothesis
        for tt in endedTrees:
            self.TRACKTREES[tt].end()
            ended = self.TRACKTREES.pop(tt)
            if (ended.getNumUpdates() >= self.MINTRACKUPDATES):
                self.ENDEDTRACKS[tt] = ended

        # End all track trees that have not been updated recently enough
        treeKeys = list(self.TRACKTREES.keys())
        for tt in treeKeys:
            if (((t - self.TRACKTREES[tt].getLastUpdate() - self.NSCAN*self.TIMEINT) >= self.MAXT)
                    and (self.TRACKTREES[tt].getDepth() >= self.NSCAN)):
                self.TRACKTREES[tt].end()
                ended = self.TRACKTREES.pop(tt)
                if (ended.getNumUpdates() >= self.MINTRACKUPDATES):
                    self.ENDEDTRACKS[tt] = ended

        # Get all possible object locations (all leaves in all track trees)
        leafNodes = []
        for tt in self.TRACKTREES:
            leafNodes += [(tt, leaf) for leaf in self.TRACKTREES[tt].getLeaves()]

        # For each leaf, find all possible detections that could match and create new branches
        for treeID, leaf in leafNodes:
            objectState, lastTime, lastUpdate = leaf.getData()
            objectState = self.MODEL.extrapolate(objectState, (t - lastTime))
            numMissedUpdates = leaf.getNumMissed()
            endLikelihood = max((1.0 - np.exp(-numMissedUpdates/self.ENDLAMBDA)), 1e-14)
            endLog = np.log(endLikelihood)
            contLog = np.log(1 - endLikelihood)

            # For each object detection, see if it is close enough to the leaf node and create a
            # new branch/leaf in the case that it is
            if (not leaf.shouldEnd()):
                for i, detection in enumerate(detectLocs):
                    if (self.MODEL.distance(detection, objectState) < self.GATE):
                        newState = self.MODEL.update(detection, objectState)
                        nodeWeight = -1/self.MODEL.getMotionScore(detection, newState)
                        Tree(self.NODEID, (i+1), treeID, parent=leaf, children=[], data=(newState, t, t),
                             weight=nodeWeight, numMiss=0, endsTree=False)
                        self.NODEID += 1

            # Add instance where the object might be lost for a frames
            if ((t - lastUpdate) < self.MAXT):
                nodeWeight = -1/self.MODEL.getMotionScore(objectState, objectState)
                Tree(self.NODEID, 0, treeID, parent=leaf, children=[],
                     data=(objectState, t, lastUpdate), weight=nodeWeight,
                     numMiss=numMissedUpdates+1, endsTree=False)
                self.NODEID += 1

            # Add instance where the track should end
            else:
                nodeWeight = leaf.getWeight()
                Tree(self.NODEID, 0, treeID, parent=leaf, children=[],
                     data=(objectState, t, lastUpdate), weight=nodeWeight,
                     numMiss=numMissedUpdates+1, endsTree=True)
                self.NODEID += 1

        # Create a new root node for every object detection
        for i, detection in enumerate(detectLocs):
            startNode = Tree(self.NODEID, (i+1), self.TRACKID, parent=None, children=[],
                             data=(self.MODEL.newState(detection, self.NEWLOG), t, t),
                             weight=0, numMiss=0, endsTree=False)
            self.TRACKTREES[self.TRACKID] = TrackTree(startNode, self.NSCAN, t)
            self.TRACKID += 1

        # Print all track trees at the end of this iteration if in debug mode
        if self.DEBUG:
            print("All current track trees:")
            for tt in self.TRACKTREES:
                print(tt)
                self.TRACKTREES[tt].getRoot().printTree()
                print("\n")
            print("*"*100 + "\n")

    def iterate(self, detectLocs, t):
        """
        Move all the tracks forward to the given time and associate the given detections with
        existing tracks or create new tracks using a multiple hypothesis tracker

        detectLocs - A list of locations and covariances (x, P) corresponding to object detections
        t - The current time step (i.e. the time step at which the detections took place)
        """

        # Output printing at the start of every iteration
        print("Time           :", t)
        print("Num Track Trees:", len(self.TRACKTREES))
        print("Num Groups     :", len(self.GROUPS))
        print()

        if self.DEBUG:
            print("*"*100)
            print("Performing iteration with:")
            print("\tObject detections:", detectLocs)
            print("\tTime:", t, "\n")

        # Remove all tracks that should be ended from the possible end dictionary
        for tt in list(self.POSSIBLEEND.keys()):
            ended = self.POSSIBLEEND.pop(tt)
            if (ended.getNumUpdates() >= self.MINTRACKUPDATES):
                ended.lowerRoot()
                self.ENDEDTRACKS[tt] = ended
        self.POSSIBLEEND = {}

        # Get all possible object locations (all leaves in all track trees)
        leafNodes = []
        for tt in self.TRACKTREES:
            leafNodes += [(tt, leaf) for leaf in self.TRACKTREES[tt].getLeaves()]

        # For each leaf, find all possible detections that could match and create new branches
        missedCounter = -1
        detectionAssigned = [0]*len(detectLocs)
        for treeID, leaf in leafNodes:
            objectState, lastTime, lastUpdate = leaf.getData()
            objectState = self.MODEL.extrapolate(objectState, (t - lastTime))
            numMissedUpdates = leaf.getNumMissed()
            endLikelihood = max((1.0 - np.exp(-numMissedUpdates/self.ENDLAMBDA)), 1e-14)
            endLog = np.log(endLikelihood)
            contLog = np.log(1 - endLikelihood)

            if (not leaf.shouldEnd()):
                # For each object detection, see if it is close enough to the leaf node and create
                # a new branch/leaf in the case that it is
                for i, detection in enumerate(detectLocs):
                    if (self.MODEL.distance(detection, objectState) < self.GATE):
                        newState = self.MODEL.update(detection, objectState)
                        nodeWeight = (leaf.getWeight() + contLog + self.DETECTLOG +
                                     self.MODEL.getScore(newState))
                        Tree(self.NODEID, (i+1), treeID, parent=leaf, children=[], data=(newState, t, t),
                             weight=nodeWeight, numMiss=0, endsTree=False)
                        detectionAssigned[i] = 1
                        self.NODEID += 1

                # Add instance where the object might be lost for a frame
                if ((t - lastUpdate) < self.MAXT):
                    nodeWeight = (leaf.getWeight() + contLog + self.SKIPLOG +
                                 self.MODEL.getScore(objectState))
                    Tree(self.NODEID, 0, treeID, parent=leaf, children=[],
                         data=(objectState, t, lastUpdate), weight=nodeWeight,
                         numMiss=numMissedUpdates+1, endsTree=False)
                    missedCounter -= 1
                    self.NODEID += 1

                # Add instance where the track should end
                else:
                    nodeWeight = leaf.getWeight() + self.SKIPLOG + endLog
                    Tree(self.NODEID, 0, treeID, parent=leaf, children=[],
                         data=(objectState, t, lastUpdate), weight=nodeWeight,
                         numMiss=numMissedUpdates+1, endsTree=True)
                    self.NODEID += 1

            else:
                # Add instance where the tree should be ended
                Tree(self.NODEID, 0, treeID, parent=leaf, children=[], data=(objectState, t, lastUpdate),
                     weight=leaf.getWeight(), numMiss=numMissedUpdates+1, endsTree=True)
                self.NODEID += 1

        # Create a new root node for every object detection and make a note for shared detections
        for i, detection in enumerate(detectLocs):
            tempRoot = Tree(self.NODEID, 0, self.TRACKID, parent=None, children=[],
                            data=(detection + (0,), t, t), weight=0, numMiss=0, endsTree=False)
            self.NODEID += 1
            startNode = Tree(self.NODEID, (i+1), self.TRACKID, parent=tempRoot, children=[],
                             data=(self.MODEL.newState(detection, self.NEWLOG), t, t),
                             weight=self.NEWLOG, numMiss=0, endsTree=False)
            self.NODEID += 1
            falseAlarmNode = Tree(self.NODEID, -(i+1), treeID=self.TRACKID, parent=tempRoot, children=[],
                                  data=(self.MODEL.newState(detection, self.FALSEALARM), t, t),
                                  weight=self.FALSEALARM, numMiss=1, endsTree=True)
            self.NODEID += 1
            falseNode = Tree(self.NODEID, 0, self.TRACKID, parent=tempRoot, children=[],
                             data=(self.MODEL.newState(detection, 0), t, t),
                             weight=0, numMiss=1, endsTree=True)
            self.NODEID += 1
            treeLeaves = [startNode, falseAlarmNode, falseNode]
            self.TRACKTREES[self.TRACKID] = TrackTree(tempRoot, self.NSCAN, t)

            # Add the newly created tree to its own group
            self.GROUPS.append(Group(self.MAXHYPOS, self.MINHYPRATIO, {self.TRACKID: treeLeaves},
                                     [[0, [tempRoot]]], self.NSCAN, self.MAXT, self.DEBUG))
            self.TRACKID += 1

        if self.DEBUG:
            print("Track trees after adding leaves:")
            for tree in self.TRACKTREES:
                self.TRACKTREES[tree].getLastPrune().printTree()
            print()

        # Assign group numbers to all of the track trees
        treeGroups = self.computeGroupNums(t)
        allGroupNums = {}
        for tree in treeGroups:
            allGroupNums[treeGroups[tree]] = set()

        # Split groups
        newGroupList = []
        for group in self.GROUPS:
            newGroupList += group.checkAndSplit(treeGroups)

        # Merge groups
        for i, group in enumerate(newGroupList):
            treesInGroup = group.getGroupTreeIDs()
            for tree in treesInGroup:
                allGroupNums[treeGroups[tree]].add(i)
        self.GROUPS = []
        for groupNum in allGroupNums:
            indices = allGroupNums[groupNum]
            group = newGroupList[indices.pop()]
            for index in indices:
                group.merge(newGroupList[index])
            if (not group.isEmpty()):
                self.GROUPS.append(group)

        # Prune and hypothesize for each group and end any trees that aren't chosen in the global
        # hypothesis
        possibleEndedTrees = []
        for group in self.GROUPS:
            endedTrees = group.generateGroupHypotheses(self.TRACKTREES, t)
            for tt in endedTrees:
                self.TRACKTREES[tt].end()
                ended = self.TRACKTREES.pop(tt)
                self.POSSIBLEEND[tt] = ended
                if (ended.getNumUpdates() >= self.NSCAN):
                    possibleEndedTrees.append([tt, ended])

        if self.DEBUG:
            print("All current track trees:")
            for tt in self.TRACKTREES:
                print(tt)
                self.TRACKTREES[tt].getRoot().printTree()
                print("\n")

        print("*"*100 + "\n")

        return possibleEndedTrees

    def computeGroupNums(self, timeNow):
        """
        Compute and return the group numbers for all current existing trees, which can then be used
        in assigning trees to groups, splitting and merging groups

        returns - A dictionary in which the keys are tree numbers and the values are their
                  respective groups (i.e. trees with the same group number should be put in the
                  same group)
        """

        # Set up lists to keep track of groups
        treeNums = list(self.TRACKTREES.keys())
        treeGroups = [-1]*len(treeNums)
        groupNum = 0

        # Go through all remaining track trees and note which of them have shared object detections
        toBeAdded = []
        detectHistory = []
        allSets = []
        for _ in range(self.NSCAN):
            detectHistory.append(dict())
        for tt in self.TRACKTREES:
            toBeAdded += self.TRACKTREES[tt].getLastPrune().getChildren()
            allSets.append({tt})
        while len(toBeAdded) > 0:
            nextNode = toBeAdded.pop()
            detectID = abs(nextNode.getDetectID())
            toBeAdded += nextNode.getChildren()
            if (detectID != 0):
                treeID = nextNode.getTreeID()
                t = nextNode.getData()[1]
                if (not detectID in detectHistory[timeNow - t]):
                    detectHistory[timeNow - t][detectID] = set()
                detectHistory[timeNow - t][detectID].add(treeID)

        # Get the sets for all shared detections
        for timeStamp in detectHistory:
            for detectID in timeStamp:
                allSets.append(timeStamp[detectID])

        # Combine all sets that have any shared detections
        out = []
        while len(allSets):
            first, *rest = allSets
            lf = -1
            while (len(first) > lf):
                lf = len(first)
                rest2 = []
                for r in rest:
                    if first.intersection(r):
                        first |= r
                    else:
                        rest2.append(r)
                rest = rest2
            out.append(first)
            allSets = rest

        # Assign numbers to each group
        returnDict = {}
        for groupNum, combined in enumerate(out):
            for treeID in combined:
                returnDict[treeID] = groupNum

        return returnDict

    def getGroups(self, timeNow):
        """
        Compute and return the groups for all current existing trees as a list

        returns - A list where every item in the list is a set of track tree IDs that belong
                  to the same group
        """

        # Set up lists to keep track of groups
        treeNums = list(self.TRACKTREES.keys())
        treeGroups = [-1]*len(treeNums)
        groupNum = 0

        # Go through all remaining track trees and note which of them have shared object detections
        toBeAdded = []
        detectHistory = []
        allSets = []
        for _ in range(self.NSCAN):
            detectHistory.append(dict())
        for tt in self.TRACKTREES:
            toBeAdded += self.TRACKTREES[tt].getLastPrune().getChildren()
            allSets.append({tt})
        while len(toBeAdded) > 0:
            nextNode = toBeAdded.pop()
            detectID = abs(nextNode.getDetectID())
            toBeAdded += nextNode.getChildren()
            if (detectID > 0):
                treeID = nextNode.getTreeID()
                t = nextNode.getData()[1]
                if (not detectID in detectHistory[timeNow - t]):
                    detectHistory[timeNow - t][detectID] = set()
                detectHistory[timeNow - t][detectID].add(treeID)

        # Get the sets for all shared detections
        for timeStamp in detectHistory:
            for detectID in timeStamp:
                allSets.append(timeStamp[detectID])

        # Combine all sets that have any shared detections
        out = []
        while len(allSets):
            first, *rest = allSets
            lf = -1
            while (len(first) > lf):
                lf = len(first)
                rest2 = []
                for r in rest:
                    if first.intersection(r):
                        first |= r
                    else:
                        rest2.append(r)
                rest = rest2
            out.append(first)
            allSets = rest

        return out

    def computeGlobalHypothesis(self, groups):
        """
        Computes the best global hypothesis for all track trees that have a depth greater or equal
        to the NSCAN parameter, used to prune trees.

        groups - A list of groups that have shared detections

        """

        globalHypothesis = []
        endedTrees = set()
        for group in groups:

            if self.DEBUG:
                print("\n" + "*"*100)
                print("Computing Global Hypothesis for group:", group)

            # Get all branches, save parent and identifier for each branch and compute branch weight, then prune
            allBranches = []
            branchNum = 0
            for tt in group:
                if (self.TRACKTREES[tt].getDepth() >= self.NSCAN):
                    rootList = self.TRACKTREES[tt].getBranches()
                    branchRoot = self.TRACKTREES[tt].getLastPrune()
                    for rootID, root in rootList:
                        branches = root.getFullBranches()
                        for branchNodes in branches:
                            allBranches.append([tt, [branchRoot, root] + branchNodes])
                            branchNum += 1

            if (len(allBranches) == 0):
                continue

            # Go through all track trees and compute the weight of each branch as well as noting shared
            # detections for computing the graph
            branchWeights = {}
            sharedDetections = {}
            for i in range(self.NSCAN+1):
                sharedDetections[i] = {}
            for branchID, (trackTree, branch) in enumerate(allBranches):
                branchTotal = 0
                for i, node in enumerate(branch):
                    detectID = node.getDetectID()
                    if not (detectID in sharedDetections[i]):
                        sharedDetections[i][detectID] = set()
                    sharedDetections[i][detectID].add(branchID)
                    branchTotal += node.getWeight()
                branchWeights[branchID] = branchTotal

            # Debug printing
            if self.DEBUG:
                print("All branches that are considered for pruning")
                print("BranchNum BranchWeight - TrackTreeNumber: [BranchNodes]")
                for i, branch in enumerate(allBranches):
                    print(str(i) + " " + str(branchWeights[i]) + " - " + str(branch[0]) + ": " + str([b.getNodeID() for b in branch[1]]))
                print("*"*100)

            # Create weighted graph with edges indicating shared detections
            neighbours = {}
            for i in range(branchNum):
                neighbours[i] = set()
            for i in sharedDetections:
                for j in sharedDetections[i]:
                    shared = sharedDetections[i][j]
                    while shared:
                        one = shared.pop()
                        for two in shared:
                            neighbours[one].add(two)
                            neighbours[two].add(one)
            WG = WeightedGraph(graphDict=neighbours, weights=branchWeights, debug=self.DEBUG)

            # Debug print for created weighted graph
            if self.DEBUG:
                print("Neighbour list to be used for weighted graph")
                print(neighbours)
                print("*"*100)

            # Compute the best global hypothesis and return it
            mwis = WG.getMWIS()
            keptTrees = set()
            for keep in mwis:
                globalHypothesis.append([allBranches[keep][0], allBranches[keep][1][1].getNodeID()])
                keptTrees.add(allBranches[keep][0])
            for i in range(len(allBranches)):
                if (not allBranches[i][0] in keptTrees):
                    endedTrees.add(allBranches[i][0])

        # Debug print for kept branches
        if self.DEBUG:
            print("[Tree, Branch] to be kept:", globalHypothesis)
            print("Trees to be ended", endedTrees)
            print("*"*100 + "\n")

        return globalHypothesis, endedTrees

    def getEndedTracks(self):
        """
        Returns the dictionary containing track trees that have been ended
        """
        return self.ENDEDTRACKS

    def getCurrentTracks(self):
        """
        Returns the dictionary containing track trees that are currently active
        """

        return self.TRACKTREES

    def getAllTracks(self):
        """
        Returns the dictionaries containing the track trees that have been ended as well as track
        trees that are still active
        """

        returnDict = self.ENDEDTRACKS.copy()
        for tt in self.TRACKTREES:
            if (self.TRACKTREES[tt].getNumUpdates() >= self.NSCAN):
                returnDict[tt] = self.TRACKTREES[tt]
        for tt in self.SUSPENDLIST:
            returnDict[tt] = self.SUSPENDLIST[tt][0]
        return returnDict

    def getAllObjectIDs(self):
        """
        Return a list of all of the object IDs that have been tracked
        """

        return(set(self.getAllTracks().keys()))


alphabet = "abcdefghijklmnopqrstuvwxyz"
# alphabet = "abcdefghijklmnopqrstuvwxyz"
# message = "wpau iwt ephhldgs udg iwt uxghi rajt xh tctgvxots"
# message = "ifz!cptt!j!xbt!bcmf!up!hfu!bddftt!up!tpnf!qfstpobm!jogpsnbujpo!gps!uif!ubshfut!zpv!tqfdjgjfe/!up!hfu!ju!zpv!kvtu!ibwf!up!tfoe!bo!fnbjm!up!dszqujdsftqpotftAhnbjm/dpn!nblf!tvsf!uif!tvckfdu!jt!fofshjafe/"
# message = "Rah_cXRaTb_^]bTb@V\\PX[.R^\\"
message = "hi baby chuddles"

def decode(new_message):
    # Try every key value
    for key in range(len(alphabet)):
        new_alphabet = alphabet[key:] + alphabet[:key]
        attempt = ""

        for i in range(len(new_message)):
            index = alphabet.find(new_message[i])

            if index < 0:
                attempt += new_message[i]
            else:
                attempt += new_alphabet[index]

        print("Key: " + str(key) + " - " + attempt)

decode(message)


import requests
import dotenv
import os

from django.shortcuts import render

# load the .env file
dotenv.load_dotenv()

API_KEY = os.getenv("API_KEY")

def home(request):
    # If the request is a post request, then the user has submitted the form for their api key, and it needs to be saved in a session
    if request.method == "POST":
        api_key = request.POST.get("api_key")
        request.session["api_key"] = api_key
        print(f"api_key: {api_key}")

    # if the user has a session, use their api key, otherwise use the default
    if "api_key" in request.session:
        api_key = request.session["api_key"]
        print("Using session api key")
    else:
        api_key = API_KEY

    # get the users selections for sol and camera from the form
    sol = request.GET.get("sol", 1000)
    camera = request.GET.get("camera", "fhaz")
    page = request.GET.get("page", 1)

    # in case the user has an invalid api key or the cookie is invalid, try to search using theirs first, then fall back to the default

    if camera == "all":
        url = f"https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?sol={sol}&page={page}&api_key={api_key}"
    else:
        url = f"https://api.nasa.gov/mars-photos/api/v1/rovers/curiosity/photos?sol={sol}&camera={camera}&page={page}&api_key={api_key}"
    # make a request to the NASA API
    # print the headers
    headers = requests.get(url).headers
    print(headers)
    remaining = headers["X-RateLimit-Remaining"]
    print(f"Remaining requests: {remaining}")

    data = requests.get(url).json()

    return render(
        request,
        "home/index.html",
        {
            "data": data,
            "remaining": remaining,
        },
    )


import asyncio
import math
import os
import psutil
import statistics
from hedra.distributed.models.http import Limit
from typing import Union, List
from .base_limiter import BaseLimiter

class ResourceAdaptiveLimiter(BaseLimiter):

    __slots__ = (
        "max_rate",
        "time_period",
        "_rate_per_sec",
        "_level",
        "_waiters",
        "_loop",
        "_last_check",
        "_cpu_limit",
        "_current_time",
        "_previous_count",
        "_current_cpu",
        "_max_queue",
        "_sample_task",
        "_running",
        "_process",
        "_max_fast_backoff",
        "_min_backoff",
        "_cpu_history",
        "_memory_history",
        "_memory_limit",
        "_current_memory"
    )

    def __init__(
        self,
        limit: Limit
     ) -> None:
        super().__init__(
            limit.max_requests,
            limit.period,
            reject_requests=limit.reject_requests
        )

        cpu_limit = limit.cpu_limit
        if cpu_limit is None:
            cpu_limit = 50

        self._cpu_limit = cpu_limit
        self._backoff = limit.backoff
        self._min_backoff = self._backoff
        self._max_fast_backoff = math.ceil(self._backoff * 10)
        self._max_backoff = math.ceil(self._max_fast_backoff * 10)
        self._last_check = self._loop.time()
        self._current_time = self._loop.time()
        self._previous_count = limit.max_requests

        self._memory_limit = limit.memory

        self._cpu_history: List[float] = []
        self._memory_history: List[float] = []

        self._max_queue = limit.max_requests
        self._sample_task: Union[asyncio.Task, None] = None
        self._running = False
        self._activate_limit = False
        self._process = psutil.Process(os.getpid())

        self._current_cpu = self._process.cpu_percent()
        self._current_memory = self._get_memory()

        self._cpu_history.append(self._current_cpu)

    async def acquire(
        self,
        amount: float = 1,
    ):

        if not self._running:
            self._running = True
            self._sample_task = asyncio.create_task(
                self._sample_cpu()
            )

        if amount > self.max_rate:
            raise ValueError("Can't acquire more than the maximum capacity")

        task = asyncio.current_task(
            loop=self._loop
        )

        assert task is not None

        rejected = False

        while self._activate_limit:

            fut = self._loop.create_future()
            try:

                self._waiters[task] = fut

                await asyncio.wait_for(
                    asyncio.shield(fut),
                    timeout=self._backoff
                )

                self._max_fast_backoff = min(
                    self._max_fast_backoff + (1/math.sqrt(self._rate_per_sec)),
                    self._max_backoff
                )

            except asyncio.TimeoutError:
                pass

            fut.cancel()

            rejected = True

        self._backoff = min(self._backoff * 2, self._max_fast_backoff)
        self._waiters.pop(task, None)
        self._level += amount

        return rejected

    async def _sample_cpu(self):
        while  self._running:

            self._current_cpu = self._process.cpu_percent()
            self._current_memory = self._get_memory()

            self._cpu_history.append(self._current_cpu)
            self._memory_history.append(self._current_memory)

            elapsed = self._loop.time() - self._last_check

            if elapsed > self.time_period:
                self._cpu_history.pop(0)

            median_cpu_usage = statistics.median(self._cpu_history)
            median_memory_usage = statistics.median(self._memory_history)

            if self._current_cpu >= self._cpu_limit or self._current_memory >= self._memory_limit:
                self._activate_limit = True

            elif median_cpu_usage < self._cpu_limit and median_memory_usage < self._memory_limit:
                self._activate_limit = False
                self._max_fast_backoff = max(
                    self._max_fast_backoff - (1/self._rate_per_sec),
                    self._min_backoff
                )

            await asyncio.sleep(0.1)

    def _get_memory(self):
        return self._process.memory_info().rss / 1024 ** 2

    async def close(self):
        self._running = False

        self._sample_task.cancel()
        if not self._sample_task.cancelled():
            try:

                await self._sample_task

            except (
                asyncio.CancelledError,
                asyncio.InvalidStateError
            ):
                pass


from py3rijndael import RijndaelCbc, ZeroPadding

import math

KEY_SIZE = 32
BLOCK_SIZE = 32

class ParameterError(Exception):
    pass

def zeroPad_multiple(string, value):
    l = len(string)
    if l % value == 0:
        return string
    newL = int(value * math.ceil(float(l)/value))
    return string.ljust(newL, b'\x00')

def truncate_multiple(string, value):
    l = len(string)
    if l % value == 0:
        return string
    newL = int(value * math.floor(float(l)/value))
    return string[:newL]

class RSCPEncryptDecrypt:
    def __init__(self, key):
        if len(key) > KEY_SIZE:
            raise ParameterError("Key must be <%d bytes" % (KEY_SIZE))

        self.key = key.ljust(KEY_SIZE,b'\xff')
        self.encryptIV = b'\xff' * BLOCK_SIZE
        self.decryptIV = b'\xff' * BLOCK_SIZE
        self.remainingData = b''
        self.oldDecrypt = b''

    def encrypt(self, plainText):
        encryptor = RijndaelCbc(self.key, self.encryptIV, padding=ZeroPadding(BLOCK_SIZE), block_size = BLOCK_SIZE)
        encText = encryptor.encrypt( plainText )
        self.encryptIV = encText[-BLOCK_SIZE:]
        return encText

    def decrypt(self, encText, previouslyProcessedData = None):
        if previouslyProcessedData is None:
            l = len(self.oldDecrypt)
            if l % BLOCK_SIZE == 0:
                previouslyProcessedData = l
            else:
                previouslyProcessedData = int(BLOCK_SIZE * math.floor(l/BLOCK_SIZE))

        #print previouslyProcessedData
        # previouslyProcessedData was passed by the parent: it means that a frame was decoded and there was some data left. This does not include the padding zeros
        if previouslyProcessedData % BLOCK_SIZE != 0:
            previouslyProcessedData = int(BLOCK_SIZE * math.ceil(previouslyProcessedData/BLOCK_SIZE))

        remainingData = self.oldDecrypt[previouslyProcessedData:]
        if self.oldDecrypt != b'':
            self.decryptIV = self.oldDecrypt[previouslyProcessedData - BLOCK_SIZE:previouslyProcessedData]

        self.oldDecrypt = encText # save current block

        toDecrypt = truncate_multiple(remainingData + encText, BLOCK_SIZE)
        decryptor = RijndaelCbc(self.key, self.decryptIV, padding=ZeroPadding(BLOCK_SIZE), block_size = BLOCK_SIZE)
        return decryptor.decrypt(toDecrypt)

if __name__ == '__main__':
    ed = RSCPEncryptDecrypt(b"love")
    enc = ed.encrypt(b"hello")
    print(enc)
    dec = ed.decrypt(enc)
    print(dec)
    enc2 = ed.encrypt(b"hello")
    print(enc2)
    dec2 = ed.decrypt(enc2)
    print(dec2)


def parse_input(path="input.txt"):
    with open(path, "r")as f:
        lines = f.readlines()

    elfs = list()

    elf = list()
    for line in lines:
        line = line.strip()

        if len(line) == 0:
            elfs.append(elf)
            elf = list()
            continue

        calories = int(line)
        elf.append(calories)

    return elfs

if __name__ == "__main__":
    elfs = parse_input()

    sum_elfs = [sum(elf) for elf in elfs]
    print("Max calories:", max(sum_elfs))

    top_three = sorted(sum_elfs, reverse=True)[:3]
    print("Top three:", sum(top_three))


from ontology.crypto.digest import Digest
from src.types.array_item import ArrayItem
from src.types.bool_item import BoolItem
from src.types.bytearray_item import ByteArrayItem
from src.types.integer_item import IntegerItem
from src.types.interop_item import InteropItem
from src.types.map_item import MapItem
from src.types.stack_items import StackItems
from src.types.struct_item import StructItem
from src.utils.script_op import ScriptOp
from src.vm.vm_state import VMState

class PushData(object):
    @staticmethod
    def op_push_data(engine):
        data = PushData.get_push_data(engine)
        PushData.push_data(engine, data)
        return VMState.NONE

    @staticmethod
    def get_push_data(e):
        data = None
        if ScriptOp.OP_PUSHBYTES1.value <= e.op_code.value <= ScriptOp.OP_PUSHBYTES75.value:
            data = e.context.op_reader.read_bytes(e.op_code.value)
        if e.op_code == ScriptOp.OP_PUSH0:
            data = 0
        elif e.op_code == ScriptOp.OP_PUSHDATA1:
            b = e.context.op_reader.read_byte()
            data = e.context.op_reader.read_bytes(b)
        elif e.op_code == ScriptOp.OP_PUSHDATA2:
            data = e.context.op_reader.read_bytes(2)
        elif e.op_code == ScriptOp.OP_PUSHDATA4:
            data = e.context.op_reader.read_bytes(4)
        elif e.op_code == ScriptOp.OP_PUSH1 or ScriptOp.OP_PUSH1.value <= e.op_code.value <= ScriptOp.OP_PUSH16.value:
            data = e.op_code.value - ScriptOp.OP_PUSH1.value + 1
        return data

    @staticmethod
    def push_data(engine, data):
        if type(data) is int:
            engine.evaluation_stack.push(IntegerItem(data))
        elif type(data) is bool:
            engine.evaluation_stack.push(BoolItem(data))
        elif type(data) is bytearray or type(data) is bytes:
            engine.evaluation_stack.push(ByteArrayItem(data))
        elif type(data) is ArrayItem:
            engine.evaluation_stack.push(ArrayItem(data.stack_items))
        elif type(data) is IntegerItem:
            engine.evaluation_stack.push(data)
        elif type(data) is BoolItem:
            engine.evaluation_stack.push(data)
        elif type(data) is ByteArrayItem:
            engine.evaluation_stack.push(data)
        elif type(data) is MapItem:
            engine.evaluation_stack.push(data)
        elif type(data) is StructItem:
            engine.evaluation_stack.push(data)
        elif type(data) is StackItems:
            engine.evaluation_stack.push(data)
        elif type(data) is list:
            engine.evaluation_stack.push(ArrayItem(data))
        elif type(data) is InteropItem:
            engine.evaluation_stack.push(data)

    @staticmethod
    def op_nop(engine):
        return VMState.NONE

    @staticmethod
    def op_jmp(engine):
        offset = engine.context.op_reader.read_int16()
        offset = engine.context.get_instruction_pointer() + offset - 3
        if offset < 0 or offset > len(engine.context.code):
            return VMState.FAULT
        f_value = True
        if engine.op_code.value > ScriptOp.OP_JMP.value:
            if PushData.evaluation_stack_count(engine) < 1:
                return VMState.FAULT
            f_value = PushData.pop_bool(engine)
            if engine.op_code == ScriptOp.OP_JMPIFNOT:
                f_value = not f_value
        if f_value:
            engine.context.set_instruction_pointer(offset)
        return VMState.NONE

    @staticmethod
    def op_call(engine):
        execution_context = engine.context.clone()
        engine.context.set_instruction_pointer(engine.context.get_instruction_pointer() + 2)
        engine.op_code = ScriptOp.OP_JMP
        engine.push_context(execution_context)
        return PushData.op_jmp(engine)

    @staticmethod
    def op_ret(engine):
        engine.pop_context()
        return VMState.NONE

    @staticmethod
    def op_to_dup_from_alt_stack(engine):
        PushData.push(engine, engine.alt_stack.peek(0))
        return VMState.NONE

    @staticmethod
    def op_to_alt_stack(engine):
        engine.alt_stack.push(PushData.pop_stack_item(engine))
        return VMState.NONE

    @staticmethod
    def op_from_alt_stack(engine):
        items = engine.alt_stack.pop()
        PushData.push(engine, items)
        return VMState.NONE

    @staticmethod
    def op_x_drop(engine):
        n = PushData.pop_int(engine)
        engine.alt_stack.remove(n)
        return VMState.NONE

    @staticmethod
    def op_x_swap(engine):
        n = PushData.pop_int(engine)
        if n == 0:
            return VMState.NONE
        engine.evaluation_stack.swap(0, n)
        return VMState.NONE

    @staticmethod
    def op_x_tuck(engine):
        n = PushData.pop_int(engine)
        engine.evaluation_stack.insert(n, PushData.peek_stack_item(engine))
        return VMState.NONE

    @staticmethod
    def op_depth(engine):
        PushData.push_data(engine, PushData.count(engine))
        return VMState.NONE

    @staticmethod
    def op_drop(engine):
        PushData.pop_stack_item(engine)
        return VMState.NONE

    @staticmethod
    def op_dup(engine):
        items = PushData.peek_stack_item(engine)
        PushData.push(engine, items)
        return VMState.NONE

    @staticmethod
    def op_nip(engine):
        x2 = PushData.pop_stack_item(engine)
        PushData.peek_stack_item(engine)
        PushData.push(engine, x2)
        return VMState.NONE

    @staticmethod
    def op_over(engine):
        x2 = PushData.pop_stack_item(engine)
        x1 = PushData.peek_stack_item(engine)
        PushData.peek_stack_item(engine)
        PushData.push(engine, x2)
        PushData.push(engine, x1)
        return VMState.NONE

    @staticmethod
    def op_pick(engine):
        n = PushData.pop_int(engine)
        if n == 0:
            return VMState.NONE
        PushData.push(engine, engine.evaluation_stack.peek(n))
        return VMState.NONE

    @staticmethod
    def op_roll(engine):
        n = PushData.pop_int(engine)
        if n == 0:
            return VMState.NONE
        PushData.push(engine, engine.evaluation_stack.remove(n))
        return VMState.NONE

    @staticmethod
    def op_rot(engine):
        x3 = PushData.pop_stack_item(engine)
        x2 = PushData.pop_stack_item(engine)
        x1 = PushData.pop_stack_item(engine)
        PushData.push(engine, x2)
        PushData.push(engine, x3)
        PushData.push(engine, x1)
        return VMState.NONE

    @staticmethod
    def op_swap(engine):
        x2 = PushData.pop_stack_item(engine)
        x1 = PushData.pop_stack_item(engine)
        PushData.push(engine, x2)
        PushData.push(engine, x1)
        return VMState.NONE

    @staticmethod
    def op_tuck(engine):
        x2 = PushData.pop_stack_item(engine)
        x1 = PushData.pop_stack_item(engine)
        PushData.push(engine, x2)
        PushData.push(engine, x1)
        PushData.push(engine, x2)
        return VMState.NONE

    @staticmethod
    def concat(array1: bytearray, array2: bytearray):
        return array1 + array2

    @staticmethod
    def op_cat(engine):
        bs2 = PushData.pop_bytearray(engine)
        bs1 = PushData.pop_bytearray(engine)
        r = PushData.concat(bs1, bs2)
        PushData.push_data(engine, r)
        return VMState.NONE

    @staticmethod
    def op_sub_str(engine):
        count = PushData.pop_int(engine)
        index = PushData.pop_int(engine)
        arr = PushData.pop_bytearray(engine)
        bs = arr[index:index+count]
        PushData.push_data(engine, bs)
        return VMState.NONE

    @staticmethod
    def op_left(engine):
        count = PushData.pop_int(engine)
        arr = PushData.pop_bytearray(engine)
        bs = arr[0:count]
        PushData.push_data(engine, bs)
        return VMState.NONE

    @staticmethod
    def op_right(engine):
        count = PushData.pop_int(engine)
        arr = PushData.pop_bytearray(engine)
        bs = arr[len(arr)-count:]
        PushData.push_data(engine, bs)
        return VMState.NONE

    @staticmethod
    def op_size(engine):
        arr = PushData.pop_bytearray(engine)
        PushData.push_data(engine, len(arr))
        return VMState.NONE

    @staticmethod
    def op_invert(engine):
        i = PushData.pop_int()
        PushData.push_data(engine, i)
        return VMState.NONE

    @staticmethod
    def op_equal(engine):
        b1 = PushData.pop_stack_item(engine)
        b2 = PushData.pop_stack_item(engine)
        PushData.push_data(engine, b1.equals(b2))
        return VMState.NONE

    @staticmethod
    def op_array_size(engine):
        item = PushData.pop_stack_item(engine)
        if type(item) is ArrayItem:
            bys = item.get_array()
            PushData.push_data(engine, len(bys))
        else:
            bys = item.get_bytearray()
            PushData.push_data(engine, len(bys))
        return VMState.NONE

    @staticmethod
    def op_pack(engine):
        size = PushData.pop_int(engine)
        items = list()
        for i in range(size):
            items.append(PushData.pop_stack_item(engine))
        PushData.push_data(engine, items)
        return VMState.NONE

    @staticmethod
    def op_unpack(engine):
        items = PushData.pop_array(engine)
        l = len(items)
        for i in range(l - 1, -1, -1):
            PushData.push(engine, items[i])
        PushData.push_data(engine, l)
        return VMState.NONE

    @staticmethod
    def op_pick_item(engine):
        index = PushData.pop_stack_item(engine)
        items = PushData.pop_stack_item(engine)
        if type(items) is ArrayItem:
            i = index.get_biginteger()
            arr = items.get_array()
            PushData.push_data(engine, arr[i])
        elif type(items) is MapItem:
            t = items.try_get_value(index)
            PushData.push_data(engine, t)
        return VMState.NONE

    @staticmethod
    def op_set_item(engine):
        new_item = PushData.pop_stack_item(engine)
        index = PushData.pop_stack_item(engine)
        item = PushData.pop_stack_item(engine)
        if type(item) is ArrayItem:
            i = index.get_biginteger()
            items = item.get_array()
            items[i] = new_item
        elif type(item) is MapItem:
            item.add(index, new_item)
        elif type(item) is StructItem:
            i = index.get_biginteger()
            item.stack_items.set(i, new_item)
        return VMState.NONE

    @staticmethod
    def op_new_array(engine):
        count = PushData.pop_int(engine)
        items = list()
        for i in range(count):
            items.append(BoolItem(False))
        PushData.push_data(engine, ArrayItem(items))
        return VMState.NONE

    @staticmethod
    def op_new_struct(engine):
        return VMState.NONE

    @staticmethod
    def op_new_map(engine):
        PushData.push_data(engine, MapItem())
        return VMState.NONE

    @staticmethod
    def op_append(engine):
        new_item = PushData.pop_stack_item(engine)
        items = PushData.pop_stack_item(engine)
        # TODO
        return VMState.NONE

    @staticmethod
    def op_reverse(engine):
        items = PushData.pop_array(engine)
        # TODO
        return VMState.NONE

    @staticmethod
    def op_throw(engine):
        return VMState.FAULT

    @staticmethod
    def op_throw_if_not(engine):
        b = PushData.pop_bool(engine)
        if not b:
            return VMState.FAULT
        return VMState.NONE

    @staticmethod
    def op_hash(engine):
        x = PushData.pop_bytearray(engine)
        PushData.push_data(engine, PushData.hash(x, engine))
        return VMState.NONE

    @staticmethod
    def op_sign(engine):
        x = PushData.pop_int(engine)
        if x < 0:
            PushData.push_data(engine, -1)
        elif x > 0:
            PushData.push_data(engine, 1)
        else:
            PushData.push_data(engine, 0)
        return VMState.NONE

    @staticmethod
    def op_not(engine):
        x = PushData.pop_bool(engine)
        PushData.push_data(engine, not x)
        return VMState.NONE

    @staticmethod
    def op_nz(engine):
        x = PushData.pop_int(engine)
        PushData.push_data(engine, PushData.bigint_comp(x, engine.op_code))
        return VMState.NONE

    @staticmethod
    def op_bigint_zip(engine):
        x2 = PushData.pop_int(engine)
        x1 = PushData.pop_int(engine)
        b = PushData.bigint_zip(x2, x1, engine.op_code)
        PushData.push_data(engine, b)
        return VMState.NONE

    @staticmethod
    def op_bigint(engine):
        x = PushData.pop_int(engine)
        if x <0:
            PushData.push_data(engine, -1)
        elif x> 0:
            PushData.push_data(engine, 1)
        else:
            PushData.push_data(engine, 0)

        return VMState.NONE

    @staticmethod
    def op_bool_zip(engine):
        x2 = PushData.pop_bool(engine)
        x1 = PushData.pop_bool(engine)
        b = PushData.bool_zip(x2, x1, engine.op_code)
        PushData.push_data(engine, b)
        return VMState.NONE

    @staticmethod
    def bool_zip(a: bool, b: bool, op: ScriptOp):
        if op == ScriptOp.BOOLAND:
            return a and b
        if op == ScriptOp.BOOLOR:
            return a or b
        return False

    @staticmethod
    def op_bigint_comp(engine):
        print(engine.evaluation_stack.e[0].value)
        print(engine.evaluation_stack.e[1].value)
        x2 = PushData.pop_int(engine)
        x1 = PushData.pop_int(engine)
        b = PushData.bigint_multi_comp(x1, x2, engine.op_code)
        PushData.push_data(engine, b)
        return VMState.NONE

    @staticmethod
    def op_within(engine):
        b = PushData.pop_int(engine)
        a = PushData.pop_int(engine)
        c = PushData.pop_int(engine)
        PushData.push_data(engine, PushData.within_op(c, a, b))
        return VMState.NONE

    @staticmethod
    def within_op(a: int, b: int, c: int):
        b1 = PushData.bigint_multi_comp(a, b, ScriptOp.GTE)
        b2 = PushData.bigint_multi_comp(a, c, ScriptOp.GTE)
        return PushData.bool_zip(b1, b2, ScriptOp.BOOLOR)

    @staticmethod
    def bigint_multi_comp(a: int, b: int, op: ScriptOp):
        if op == ScriptOp.OP_NUMEQUAL:
            return PushData.compare_to(a, b) == 0
        elif op == ScriptOp.OP_NUMNOTEQUAL:
            return PushData.compare_to(a, b) != 0
        elif op == ScriptOp.OP_LT:
            return PushData.compare_to(a, b) < 0
        elif op == ScriptOp.OP_GT:
            return PushData.compare_to(a, b) > 0
        elif op == ScriptOp.OP_LTE:
            return PushData.compare_to(a, b) <= 0
        elif op == ScriptOp.OP_GTE:
            return PushData.compare_to(a, b) >= 0
        return False

    @staticmethod
    def bigint_zip(a: int, b: int, op: ScriptOp):
        if op == ScriptOp.AND:
            return a and b
        elif op == ScriptOp.OR:
            return a or b
        elif op == ScriptOp.XOR:
            return a ^ b
        elif op == ScriptOp.ADD:
            return a + b
        elif op == ScriptOp.SUB:
            return a - b
        elif op == ScriptOp.MUL:
            return a * b
        elif op == ScriptOp.DIV:
            return a / b
        elif op == ScriptOp.MOD:
            return a % b
        elif op == ScriptOp.SHL:
            return a << b
        elif op == ScriptOp.SHR:
            return a >> b
        elif op == ScriptOp.MIN:
            if a < b:
                return a
            else:
                return b
        elif op == ScriptOp.MAX:
            if a < b:
                return b
            else:
                return a
        return None

    @staticmethod
    def compare_to(a: int, b: int):
        if a > b:
            return 1
        elif a < b:
            return -1
        else:
            return 0

    @staticmethod
    def bigint_comp(a: int, op: ScriptOp):
        if op == ScriptOp.NZ:
            return PushData.compare_to(a, 0) != 0
        return False

    @staticmethod
    def bigint_op(a: int, op: ScriptOp):
        if op == ScriptOp.INC:
            return a + 1
        elif op == ScriptOp.DEC:
            return a - 1
        elif op == ScriptOp.NEGATE:
            return -a
        elif op == ScriptOp.ABS:
            return a.__abs__()
        return a

    @staticmethod
    def hash(bs: bytearray, engine):
        if engine.op_code == ScriptOp.SHA1:
            return None
        elif engine.op_code == ScriptOp.SHA256:
            return Digest.sha256(bs)
        elif engine.op_code == ScriptOp.HASH160:
            return Digest.hash160(bs)
        elif engine.op_code == ScriptOp.HASH256:
            return Digest.hash256(bs)
        return None

    @staticmethod
    def pop_array(engine):
        return engine.evaluation_stack.pop().get_array()

    @staticmethod
    def pop_bytearray(engine):
        return engine.evaluation_stack.pop().get_bytearray()

    @staticmethod
    def pop_stack_item(engine):
        return engine.evaluation_stack.pop()

    @staticmethod
    def peek_stack_item(engine):
        return engine.evaluation_stack.peek(0)

    @staticmethod
    def pop_int(engine):
        item = engine.evaluation_stack.pop()
        return item.get_biginteger()

    @staticmethod
    def push(engine, ele: StackItems):
        engine.evaluation_stack.push(ele)

    @staticmethod
    def pop_bool(engine):
        return engine.evaluation_stack.pop().get_bool()

    @staticmethod
    def evaluation_stack_count(engine):
        return engine.evaluation_stack.count()

    @staticmethod
    def pop_interop_interface(engine):
        res = engine.evaluation_stack.pop()
        print(res)
        print(type(res))
        return res.get_interface()

    @staticmethod
    def peek_biginteger(engine):
        return PushData.peek_stack_item(engine).get_biginteger()

    @staticmethod
    def peek_n_stack_item(i: int, engine):
        return engine.evaluation_stack.peek(i)

    @staticmethod
    def count(engine):
        return engine.evaluation_stack.count()


import requests
import os
import subprocess

# Enter your GitHub username here
username = "rishavnandi"

# API endpoint to get repositories
url = f"https://api.github.com/users/{username}/repos"

# Make a GET request to the GitHub API
response = requests.get(url)

# Check if the request was successful
if response.status_code == 200:
    repos = response.json()

    # Iterate over each repository
    for repo in repos:
        repo_name = repo["name"]
        repo_url = repo["clone_url"]

        # Clone the repository using git clone command
        subprocess.run(["git", "clone", repo_url, repo_name])

        print(f"Cloned repository: {repo_name}")
else:
    print("Failed to retrieve repositories.")


import torch
import torch.nn as nn
import torch.nn.functional as F

import syft as sy
from syft.workers.websocket_client import WebsocketClientWorker
from syft.workers.virtual import VirtualWorker

import mylogger
import sys
import argparse

import utils as Federator
import client_dataset
from client_model import ClientModel

def getArgs(args=sys.argv[1:]):
	parser = argparse.ArgumentParser(
		description="Federated Learning using PySyft Workers"
	)

	parser.add_argument("--batch_size", type=int, default=64, help="Batch Size")
	parser.add_argument("--epochs", type=int, default=2, help="number of epochs to train")
	parser.add_argument("--lr", type=float, default=.01, help="learning rate")
	parser.add_argument("--seed", type=int, default=1, help="seed used for random init values")
	parser.add_argument("--save_model", action="store_true", help="model will be saved")
	parser.add_argument("--use_virtual", action="store_true", default=False, help="virtual workers will be used instead of websocket workers")
	parser.add_argument("--use_cuda", action="store_true", help="use gpu for training")
	parser.add_argument("--verbose", action="store_true", default=True, help="Verbose Output shown")
	return parser.parse_args(args=args)

def createWorkers(SyftWorker, worker_ports, worker_ids, worker_kwargs):
	workers = []

	for (id, port) in zip(worker_ids, worker_ports):
		print(id, port)
		workers.append(
			SyftWorker(id=id, port=port, **worker_kwargs)
		)

	return workers

def main():
	args = getArgs()
	hook = sy.TorchHook(torch)
	SyftWorker = WebsocketClientWorker if not args.use_virtual else VirtualWorker

	worker_kwargs = {'host': 'localhost', 'hook': hook, 'verbose': args.verbose}
	worker_ports = (8777, 8778, 8779)
	worker_ids = ('alice', 'bob', 'charlie')

	print("Creating Workers...")
	workers = createWorkers(SyftWorker, worker_ports=worker_ports, worker_ids=worker_ids, worker_kwargs=worker_kwargs)

	use_cuda = torch.cuda.is_available() and args.use_cuda
	print("Using Cuda:", use_cuda)

	torch.manual_seed(args.seed)

	device = torch.device('cuda' if use_cuda else 'cpu')

	model = ClientModel().to(device)

	federated_train_loader = client_dataset.GetTrainLoader(workers, args)
	test_loader = client_dataset.GetTestLoader(workers, args)

	for epoch in range(args.epochs):
		mylogger.logger.info("Epoch %s/%s", epoch, args.epochs)

		model = Federator.FederatedTrainer(model, device, federated_train_loader, args.lr, fed_after_n_batches=25)
		Federator.test(model, device, test_loader)

	if args.save_model:
		torch.save(model.state_dict(), "fed_model.pt")

if __name__ == '__main__':
	main()


import uuid
import random
from django.conf import settings

from django.db import models
from django.utils.text import slugify
from PIL import Image as pil_image
from io import BytesIO
from django.core.files import File
from django.core.files.base import ContentFile

from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType

from django.contrib.auth.models import User

prefix = ''
default_cover_img_path = 'main/static/img/default-cover.png'

def resizeImage(imageField, tupleSize):
    im = pil_image.open(imageField)  # Catch original
    source_image = im
    #source_image = im.convert('RGB') # Uncomment for put solid background
    source_image.thumbnail(tupleSize)  # Resize to size
    output = BytesIO()
    source_image.save(output, format='PNG') # Save resize image to bytes
    output.seek(0)

    content_file = ContentFile(output.read())  # Read output and create ContentFile in memory
    file = File(content_file)

    random_name = f'{uuid.uuid4()}.png'
    imageField.save(random_name, file, save=False)

# Tag
class Tag(models.Model):
    class Meta:
        db_table = prefix + 'tag'

    # keys
    id = models.AutoField(primary_key=True)
    # fields
    name1 = models.CharField(max_length=100)
    name2 = models.CharField(max_length=100)

    def __str__(self) -> str:
        return self.name1
# -------- -------- -------- --------

# Story
class Story(models.Model):
    class Meta:
        db_table = prefix + 'story'

    # primary key
    id = models.AutoField(primary_key=True)
    # fields
    title1 = models.CharField(max_length=255)
    title2 = models.CharField(max_length=255)
    cover = models.ImageField(upload_to=settings.IMAGES_PATH, default=default_cover_img_path, verbose_name='cover')
    description1 = models.CharField(max_length=255, null=False, blank=True, default='')
    description2 = models.CharField(max_length=255, null=False, blank=True, default='')

    xp_required = models.IntegerField(default=0)

    tags = models.ManyToManyField(Tag, blank=True)

    likes_number = models.IntegerField(default=0)
    route = models.SlugField(max_length=255, unique=True, null=False, default='')
    created_at = models.DateTimeField(auto_now_add=True, null=True)
    updated_at = models.DateTimeField(auto_now=True, null=True)

    def get_portada(self):
        return self.cover.name

    def del_portada(self, to_delete):
        if to_delete != default_cover_img_path:
            self.cover.storage.delete(to_delete)

    # Override methods
    def save(self, *args, **kwargs):
        if not self.likes_number:
            self.likes_number = random.randint(270, 300)
        self.route = slugify(self.title1)

        if self.cover.name != default_cover_img_path:
            resizeImage(self.cover, (800, 800))
        super(Story, self).save(*args, **kwargs)

    def delete(self, *args, **kwargs):
        if self.cover.name != default_cover_img_path:
            self.cover.storage.delete(self.cover.name)

        # delete pages related
        for page in self.pages.all():
            page.delete()
        # delete story
        super(Story, self).delete(*args, **kwargs)

    def __str__(self):
        return f'title: {self.title1}'
# -------- -------- -------- --------

class UserProfile(models.Model):
    class Meta:
        db_table = prefix + 'user_profile'
    # key for link one to one to the django user
    user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')

    # extra fields
    scored_stories = models.ManyToManyField(Story, related_name='users_scored', through='Score')
    liked_stories = models.ManyToManyField(Story, related_name='users_liked', through='LikedStory')
    saved_stories = models.ManyToManyField(Story, related_name='saved_by', through='SavedStory')

    xp = models.IntegerField(default=0)
    level = models.FloatField(default=1)

    DEFAULT_PROFILE_IMAGE_CHOICES = [
        ('/static/img/profile_pictures/pp1.jpg', 'Friendly bear'),
        ('/static/img/profile_pictures/pp2.jpg', 'Thoughtful cat'),
        ('/static/img/profile_pictures/pp3.jpg', 'Hungry panda'),
        ('/static/img/profile_pictures/pp4.jpg', 'Cute Hamster'),
        ('/static/img/profile_pictures/pp5.jpg', 'Tall giraffe'),
        ('/static/img/profile_pictures/pp6.jpg', 'Chilly bird'),
    ]
    default_profile_image = models.CharField(max_length=50, choices=DEFAULT_PROFILE_IMAGE_CHOICES, blank=True, null=True)

    def __str__(self) -> str:
        return f'username: {self.user.username}'

class LikedStory(models.Model):
    class Meta:
        db_table = prefix + 'liked_story'
    # keys
    user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
    story = models.ForeignKey(Story, on_delete=models.CASCADE)

    # fields
    date = models.DateTimeField(auto_now_add=True, null=True)

class SavedStory(models.Model):
    class Meta:
        db_table = prefix + 'saved_story'
    # keys
    user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
    story = models.ForeignKey(Story, on_delete=models.CASCADE)

    # fields
    date = models.DateTimeField(auto_now_add=True, null=True)

class Score(models.Model):
    class Meta:
        db_table = prefix + 'score'
    # keys
    user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
    story = models.ForeignKey(Story, on_delete=models.CASCADE)
    # fields
    date = models.DateTimeField(auto_now_add=True, null=True)

    # Score punctuation
    score = models.IntegerField(default=0)
    score_limit = models.IntegerField(default=1)

    # Percentages
    score_percentage = models.FloatField(default=0)
    writing_percentage = models.FloatField(default=100)
    comprehension_percentage = models.FloatField(default=100)
    speaking_percentage = models.FloatField(default=100)

    def __str__(self) -> str:
        string = f"""
            date: {self.date}
            username: {self.user_profile.user.username}
            story: {self.story.title1}
            score_percentage: {self.score_percentage}
            """
        #f'username: {self.user_profile.user.username}, story: {self.story.title1}, score: {self.score}'
        return string

# Page
class Page(models.Model):
    class Meta:
        db_table = prefix + 'page'
    # keys
    id = models.AutoField(primary_key=True)
    story = models.ForeignKey(Story, on_delete=models.CASCADE, related_name='pages')
    # fields
    subtitle1 = models.CharField(max_length=100, default="")
    subtitle2 = models.CharField(max_length=100, default="")
    page_type = models.IntegerField(default=0)
    date_created = models.DateTimeField(auto_now_add=True, null=True)
    time_created = models.TimeField(auto_now_add=True, null=True)

    def delete(self, *args, **kwargs):
        # delete images related
        for image in self.images.all():
            image.delete()
        # delete page
        super(Page, self).delete(*args, **kwargs)
# -------- -------- -------- --------

# Audio
class Audio(models.Model):
    class Meta:
        db_table = prefix + 'audio'

    page = models.ForeignKey(Page, on_delete=models.CASCADE, related_name='audios')
    # fields
    element_number = models.IntegerField()
    audio_file = models.FileField(upload_to=settings.AUDIOS_PATH, null=True, blank=True)
    label_name = models.CharField(max_length=255, blank=True)
    show_description = models.BooleanField(default=False)
    description = models.CharField(max_length=600, blank=True)
    t_description = models.CharField(max_length=600, blank=True)

# Video
class Video(models.Model):
    class Meta:
        db_table = prefix + 'video'
    # keys
    id = models.AutoField(primary_key=True)
    page = models.ForeignKey(Page, on_delete=models.CASCADE, related_name='videos')
    # fields
    element_number = models.IntegerField()
    url = models.CharField(max_length=2048)

# Image
class Image(models.Model):
    class Meta:
        db_table = prefix + 'image'
    # keys
    id = models.AutoField(primary_key=True)
    page = models.ForeignKey(Page, on_delete=models.CASCADE, related_name='images')
    # fields
    element_number = models.IntegerField()
    image = models.ImageField(upload_to=settings.IMAGES_PATH, null=True, blank=True)

    # Override methods
    def delete(self, *args, **kwargs):
        self.image.delete(save=False)
        super(Image, self).delete(*args, **kwargs)

# Text
class Text(models.Model):
    class Meta:
        db_table = prefix + 'text'
    # keys
    id = models.AutoField(primary_key=True)
    page = models.ForeignKey(Page, on_delete=models.CASCADE, related_name='texts')

    language1 = models.TextField(null=False, blank=False)
    language2 = models.TextField(null=False, blank=False)
    element_number = models.IntegerField()

    def __str__(self) -> str:
        return f'id: {self.id}, l1: {self.language1}, l2: {self.language2}'

# Dialogue
class Dialogue(models.Model):
    class Meta:
        db_table = prefix + 'dialogue'
    # keys
    id = models.AutoField(primary_key=True)
    page = models.ForeignKey(Page, on_delete=models.CASCADE, related_name='dialogues')
    # fields
    name = models.CharField(max_length=30)
    content1 = models.CharField(max_length=255)
    content2 = models.CharField(max_length=255)
    color = models.CharField(max_length=7, default='#000000')
    element_number = models.IntegerField()

# -------- -------- -------- --------

# ---------- Exercises ---------- #
# Repeat phrase
class RepeatPhrase(models.Model):
    class Meta:
        db_table = prefix + 'repeat_phrase'
    # keys
    id = models.AutoField(primary_key=True)
    page = models.ForeignKey(Page, on_delete=models.CASCADE, related_name='repeat_phrases')
    # fields
    content1 = models.CharField(max_length=255)
    content2 = models.CharField(max_length=255)
    show_text = models.BooleanField(default=True)
    element_number = models.IntegerField()

    def __str__(self) -> str:
        return f'{self.page.id} - {self.content1}'

# Spellcheck
class Spellcheck(models.Model):
    class Meta:
        db_table = prefix + 'spellcheck'

    # keys
    id = models.AutoField(primary_key=True)
    page = models.ForeignKey(Page, on_delete=models.CASCADE, related_name='spellchecks')
    # fields
    element_number = models.IntegerField()
    wrong_text = models.CharField(max_length=600)
    right_text = models.CharField(max_length=600)
    translated_right_text = models.CharField(max_length=600)

# Question
class MultipleChoiceQuestion(models.Model):
    class Meta:
        db_table = prefix + 'multiple_choice_question'
    # keys
    id = models.AutoField(primary_key=True)
    page = models.ForeignKey(Page, on_delete=models.CASCADE, related_name='questions')
    # fields
    element_number = models.IntegerField()
    text = models.CharField(max_length=255)
    t_text = models.CharField(max_length=255)
    randomize_choices = models.BooleanField(default=True)

# Question Option
class QuestionChoice(models.Model):
    class Meta:
        db_table = prefix + 'question_choice'
    # keys
    id = models.AutoField(primary_key=True)
    question = models.ForeignKey(MultipleChoiceQuestion, on_delete=models.CASCADE, related_name='choices')
    # fields
    choice_number = models.IntegerField(default=0)
    text = models.CharField(max_length=255)
    t_text = models.CharField(max_length=255)
    correct = models.BooleanField(default=False)

class UserAnswer(models.Model):
    class Meta:
        db_table = prefix + 'user_answer'

    # keys
    id = models.AutoField(primary_key=True)
    user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='answers')
    story = models.ForeignKey(Story, on_delete=models.CASCADE)
    page = models.ForeignKey(Page, on_delete=models.CASCADE)
    # Key for different exercises
    exercise_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    exercise_id = models.PositiveIntegerField()
    exercise = GenericForeignKey('exercise_type', 'exercise_id')
    # fields
    answer = models.CharField(max_length=255, blank=True, default='')
    submited = models.BooleanField(default=False)
    evaluated = models.BooleanField(default=False)

    def __str__(self) -> str:
        string = f'{self.user_profile.id} - {self.story.id} - {self.page.id}\n'
        string += f'{self.exercise_type}, {self.exercise_id}, {self.exercise}\n'
        string += f'{self.answer} -- {self.submited}'
        return string

class FlashcardCollection(models.Model):
    class Meta:
        db_table = prefix + 'flashcard_collection'

    # keys
    id = models.AutoField(primary_key=True)
    user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE, related_name='flashcards_collections')

    # fields
    color = models.CharField(max_length=7, default='#cbe5ff')
    collection_name = models.CharField(max_length=100, blank=False, null=False, default='New Collection')
    description = models.CharField(max_length=255, blank=True, default='')

    created_at = models.DateTimeField(auto_now_add=True, null=True)
    updated_at = models.DateTimeField(auto_now=True, null=True)

class Flashcard(models.Model):
    class Meta:
        db_table = prefix + 'flashcard'

    # keys
    id = models.AutoField(primary_key=True)
    flashcard_collection = models.ForeignKey(FlashcardCollection, on_delete=models.CASCADE, related_name='flashcards')

    # fields
    color = models.CharField(max_length=7, default='#cbe5ff')
    front = models.CharField(max_length=255)
    back = models.CharField(max_length=255)
    created_at = models.DateTimeField(auto_now_add=True, null=True)
    updated_at = models.DateTimeField(auto_now=True, null=True)

class CBRSettings(models.Model):
    class Meta:
        db_table = prefix + 'cbr_settings'

    # keys
    id = models.AutoField(primary_key=True)
    # fields
    timeout = models.IntegerField(default=0)
    update_on_alter_stories = models.BooleanField(default=True)

    def __str__(self) -> str:
        return 'Timeout: {}, Update: {}'.format(self.timeout, self.update_on_alter_stories)

class UBRSettings(models.Model):
    class Meta:
        db_table = prefix + 'ubr_settings'

    # keys
    id = models.AutoField(primary_key=True)
    # fields
    timeout = models.IntegerField(default=1800)

    def __str__(self) -> str:
        return 'Timeout: {}'.format(self.timeout)

# Reports
class StoryReport(models.Model):
    class Meta:
        db_table = prefix + 'story_report'

    story = models.ForeignKey(Story, on_delete=models.CASCADE, related_name='reports')
    page = models.ForeignKey(Page, on_delete=models.CASCADE, related_name='reports', null=True)

    user_profile = models.ForeignKey(UserProfile, on_delete=models.CASCADE)
    description = models.CharField(max_length=2550, blank=True, default='')

    STATUS_CHOICES = (
        ('unread', 'Unread'),
        ('read', 'Read'),
        ('in_progress', 'In Progress'),
        ('fixed', 'Fixed'),
    )
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='unread')

    created_at = models.DateTimeField(auto_now_add=True, null=True)
    updated_at = models.DateTimeField(auto_now=True, null=True)
    fixed_at = models.DateTimeField(null=True)