import datetime
import uuid

from py4web import request, URL
from pydal.validators import *
from .my_validators import IS_ISO_DATETIME, IS_REAL_LIST_OF_EMAILS, IS_DOMAIN

from .components.vueform import VueForm

from .constants import *
from .common import db, session, auth, Field
from .util import random_id
from .common import url_signer
from .models import get_assignment_teachers, set_assignment_teachers, get_user_email
from .util import normalize_email_list
from .settings import MAX_GRADES_24H

FIELDS = [
    Field('name', length=STRING_FIELD_LENGTH, required=True,
          requires=[IS_NOT_EMPTY(), IS_LENGTH(STRING_FIELD_LENGTH)],
          help="Name of the assignment."),  # Assignment name.
    Field('domain_restriction', length=STRING_FIELD_LENGTH,
          requires=IS_DOMAIN(),
          help="If specified, students must use Google accounts from the domain.  For instance, if you specify ucsc.edu as domain restrictions, students must use accounts of the form name@ucsc.edu to participate in the assignment."),
    Field('instructors', length=STRING_FIELD_LENGTH,
          requires=IS_REAL_LIST_OF_EMAILS(),
          help="List of email addresses of additional instructors.  These instructors will be able to view student submissions, but they will not be able to edit the assignment."),
    Field('available_from', 'datetime', required=True, requires=[IS_ISO_DATETIME(), IS_NOT_EMPTY()],
          help="Date from which students can access the assignment."),
    Field('submission_deadline', 'datetime', required=True, requires=[IS_ISO_DATETIME(), IS_NOT_EMPTY()],
          label="Due Date",
          help="Date when students need to submit the assignment for it to be considered submitted on time."),
    Field('available_until', 'datetime', required=True, requires=[IS_ISO_DATETIME(), IS_NOT_EMPTY()],
          help="Date until when student can access the assignment and submit a solution, even if late."),
    Field('max_submissions_in_24h_period', 'integer', default=3, requires=[IS_INT_IN_RANGE(1, MAX_GRADES_24H), IS_NOT_EMPTY()], label="Maximum number of submissions in a 24h period", help="Students will be able to only submit this many solutions in any 24h period."),
]

class AssignmentForm(VueForm):

    def __init__(self, fields, path, **kwargs):
        super().__init__(fields, session, path, auth=auth, db=db, signer=url_signer, **kwargs)

    def read_values(self, record_id):
        values = {}
        assert record_id is not None
        row = self.db.assignment[record_id]
        if row is not None:
            for f_name, f in self.fields.items():
                values[f_name] = f.formatter(row.get(f_name))
            # Reads the additional instructors.
            values['instructors'] = ", ".join(get_assignment_teachers(record_id))
        return values

class AssignmentFormView(AssignmentForm):

    def __init__(self, path, **kwargs):
        super().__init__(FIELDS, path, use_id=True, readonly=True, **kwargs)

class AssignmentFormEdit(AssignmentForm):
    """This class must also enforce the ordering relations between dates."""

    def __init__(self, path, use_id=True, redirect_url=None, readonly=False,
                 validate=None, **kwargs):
        super().__init__(FIELDS, path, use_id=use_id, readonly=readonly,
                         validate=validate or self.validate_dates, **kwargs)
        self.redirect_url = redirect_url

    def validate_dates(self, fields, validated_values):
        d1 = validated_values['available_from']
        d2 = validated_values['submission_deadline']
        d3 = validated_values['available_until']
        errors = {}
        if d2 < d1:
            errors['submission_deadline'] = "The submission deadline should come after the assignment is available"
        if d3 < d2:
            errors['available_until'] = "An assignment should be available at least until its deadline"
        user = get_user_email()
        if user not in validated_values['instructors']:
            validated_values['instructors'].append(user)
        validated_values['instructors'] = normalize_email_list(validated_values['instructors'])
        return errors

    def process_post(self, record_id, validated_values):
        new_instructors = validated_values['instructors']
        user = get_user_email()
        if user not in new_instructors:
            new_instructors.append(user)
        set_assignment_teachers(record_id, new_instructors)
        del validated_values['instructors']
        self.db(self.db.assignment.id == record_id).update(**validated_values)
        return dict(redirect_url=URL(self.redirect_url, record_id))

class AssignmentFormCreate(AssignmentFormEdit):

    def __init__(self, path, redirect_url=None, **kwargs):
        super().__init__(path, use_id=False, readonly=False,
                         validate=self.validate_dates, **kwargs)
        self.redirect_url = redirect_url

    def validate_dates(self, fields, validated_values):
        d1 = validated_values['available_from']
        d2 = validated_values['submission_deadline']
        d3 = validated_values['available_until']
        errors = {}
        if d2 < datetime.datetime.utcnow():
            errors['submission_deadline'] = "The submission deadline should be in the future"
        if d2 < d1:
            errors['submission_deadline'] = "The submission deadline should come after the assignment is available"
        if d3 < d2:
            errors['available_until'] = "An assignment should be available at least until its deadline"
        return errors

    def read_values(self, record_id):
        values = {}
        for f_name, f in self.fields.items():
            values[f_name] = f.formatter(None)
        return values

    def process_post(self, null_record_id, validated_values):
        new_instructors = validated_values['instructors']
        del validated_values['instructors']
        new_id = self.db.assignment.insert(**validated_values)
        user = get_user_email()
        if user not in new_instructors:
            new_instructors.append(user)
        set_assignment_teachers(new_id, new_instructors)
        return dict(redirect_url=URL(self.redirect_url, new_id))


#!/bin/env python3
import re
import random
from math import log10, ceil, trunc, degrees, radians, sqrt, cos, sin, atan2, acos

import os
if 'tools' not in os.listdir():
    os.chdir("..")
os.chdir('tools')

# VDIM = 23
# ADIM = 36

def fromdat(filename, skip_header, skip_footer, usecols):
    try:
        file = open(filename)
        lines = file.readlines()[skip_header:-skip_footer]
    except OSError as e:
        print(e)
        return None
    file.close()

    res = [[] for _ in usecols]
    for line in lines:
        l = line.split()
        l = list(map(float, l))
        for i, c in enumerate(usecols):
            res[i].append(l[c])
    return res

def gettags(filename):
    try:
        vdat = open(filename)
    except OSError as e:
        print(e)
        return None, None, None

    expr = re.compile(" >(.+[umc]?m)\s+(\d+-\d+).+\s(\d+)\s+\d+\.\d+")
    while "size       Vmag".upper() not in vdat.readline().upper():
        continue
    dsize = []
    vrang = []
    angle = []
    for l in vdat:
        dsi, vra, ang = expr.findall(l)[0]
        if dsi not in dsize:
            dsize.append(dsi)
        if vra not in vrang:
            vrang.append(vra)
        if ang not in angle:
            angle.append(ang)
    vdat.close()

    return dsize, vrang, angle

def getthreshold(filename):
    try:
        cmd = open(filename)
    except OSError as e:
        print(e)
        return None
    expr = re.compile("\sthreshold=(.+m)\s")
    th = expr.findall(cmd.readline())[0]
    return th

class FluxOfSize:
    def __init__(self, x, y, yp1=None, ypn=None):
        self.n = len(x)
        self.m = 2
        self.x = list(x)
        self.y = list(y)
        self.y2 = [0.0]*self.n
        self.jsave = -1
        self.correlated = 0
        self.dj = min(1, self.n**0.25)
        self.modifiedb = 0.0
        self.modifiede = 0.0
        self.sety2(x, y, yp1, ypn)
        self.modify()
        return

    # Set up for interpolating on a table of x’s and y’s of length m
    # Given a value x, return a value j such that x is (insofar as possible) centered in the sub-range
    # xx[j..j+mm-1], where xx is the stored pointer. The values in xx must be monotonic, either
    # increasing or decreasing. The returned value is not less than 0, nor greater than n-1.
    def locate(self, xp):
        if self.n < 2 or self.m < 2 or self.m > self.n:
            raise ValueError("locate size error.")

        n = self.n
        ascnd = (self.x[n-1] >= self.x[0])  # True if ascending order of table, false otherwise
        jl = 0  # Initialize lower limits
        ju = n - 1  # Initialize upper limits

        while ju - jl > 1:  # If we are not yet done, compute a midpoint
            jm = (ju+jl) >> 1
            if (xp >= self.x[jm]) == ascnd:
                jl = jm
            else:
                ju = jm

        self.correlated = 0 if abs(jl - self.jsave) > self.dj else 1  # Decide whether to use hunt or locate next time.
        self.jsave = jl
        return max(0, min(n - self.m, jl - ((self.m - 2) >> 1)))

    # Given a value x, return a value j such that x is (insofar as possible) centered in the sub-range
    # xx[j..j+mm-1], where xx is the stored pointer. The values in xx must be monotonic, either
    # increasing or decreasing. The returned value is not less than 0, nor greater than n-1.
    def hunt(self, xp):
        if self.n < 2 or self.m < 2 or self.m > self.n:
            raise ValueError("hunt size error.")

        n = self.n
        ascnd = (self.x[n-1] >= self.x[0])  # True if ascending order of table, false otherwise
        jl = self.jsave
        inc = 1

        if jl < 0 or jl > n - 1:  # Input guess not useful. Go immediately to bisection
            jl = 0
            ju = n-1
        else:
            if (xp >= self.x[jl]) == ascnd:
                while True:
                    ju = jl + inc
                    if ju >= n-1:
                        ju = n-1
                        break
                    elif (xp < self.x[ju]) == ascnd:
                        break
                    else:
                        jl = ju
                        inc += inc
            else:
                ju = jl
                while True:
                    jl = jl - inc
                    if jl <= 0:
                        jl = 0
                        break
                    elif (xp >= self.x[jl]) == ascnd:
                        break
                    else:
                        ju = jl
                        inc += inc

        while ju - jl > 1:
            jm = (ju+jl) >> 1
            if (xp >= self.x[jm]) == ascnd:
                jl = jm
            else:
                ju = jm

        self.correlated = 0 if abs(jl - self.jsave) > self.dj else 1
        self.jsave = jl
        return max(0, min(n - self.m, jl - ((self.m - 2) >> 1)))

    # Given a value x, and using pointers to data xx and yy, and the stored vector of second derivatives
    # y2, this routine returns the cubic spline interpolated value y.
    def interp(self, xp):
        jlo = self.hunt(xp) if self.correlated == 1 else self.locate(xp)

        klo = jlo
        khi = jlo + 1

        h = self.x[khi] - self.x[klo]
        if h == 0.0:
            raise ValueError("Bad input to spline interpolation.")
        a = (self.x[khi] - xp) / h
        b = (xp - self.x[klo]) / h
        yp = a*self.y[klo] + b*self.y[khi] + ((a**3 - a)*self.y2[klo] + (b**3 - b)*self.y2[khi])*(h*h)/6.0
        return yp

    # This routine stores an array y2[0..n-1] with second derivatives of the interpolating function
    # at the tabulated points pointed to by xv, using function values pointed to by yv. If yp1 and/or
    # ypn are equal to 1*10^99 or larger, the routine is signaled to set the corresponding boundary
    # condition for a natural spline, with zero second derivative on that boundary; otherwise, they are
    # the values of the first derivatives at the endpoints.
    def sety2(self, x, y, yp1, ypn):
        n = self.n
        u = [0.0]*(n-1)
        if yp1:                         # or else to have a specified first derivative.
            self.y2[0] = -0.5
            u[0] = (3.0 / (x[1] - x[0])) * ((y[1] - y[0]) / (x[1] - x[0]) - yp1)
        else:                           # The lower boundary condition is set either to be “natural"
            self.y2[0] = u[0] = 0.0

        for i in range(1, n-1):         # This is the decomposition loop of the tridiagonal algorithm.
            sig = (x[i]-x[i-1]) / (x[i+1]-x[i-1])
            p = sig * self.y2[i-1]+2.0
            self.y2[i] = (sig-1.0) / p
            u[i] = (y[i+1]-y[i]) / (x[i+1]-x[i]) - (y[i]-y[i-1]) / (x[i]-x[i-1])
            u[i] = (6.0 * u[i] / (x[i+1]-x[i-1])-sig * u[i-1]) / p

        if ypn:                         # The upper boundary condition is set either to be “natural"
            qn = 0.5
            un = (3.0 / (x[n - 1] - x[n - 2])) * (ypn - (y[n - 1] - y[n - 2]) / (x[n - 1] - x[n - 2]))
        else:                           # or else to have a specified first derivative.
            qn = un = 0.0

        self.y2[n-1] = (un - qn*u[n-2])/(qn*self.y2[n-2] + 1.0)
        for k in range(n-2, -1, -1):    # This is the back substitution loop of the tridiagonal algorithm.
            self.y2[k] = self.y2[k] * self.y2[k+1] + u[k]
        return

    def modify(self):
        step = 0.05
        i = self.x[0]
        while i < self.x[-1]:
            if self.interp(i) < self.interp(i + step):
                minx = golden_section_search(self.interp, i - step, i + step)
                maxx = golden_section_search(self.interp, i + step, self.x[-1], kind='max')
                self.modifiedb = bisection_search(self.interp, self.x[0], minx, self.interp(maxx))
                self.modifiede = bisection_search(self.interp, maxx, self.x[-1], self.interp(minx))
            i += step
        return

    def modified_interp(self, xp):
        if self.modifiedb == self.modifiede:
            return self.interp(xp)
        elif self.modifiedb < xp < self.modifiede:
            b = self.interp(self.modifiedb)
            e = self.interp(self.modifiede)
            return b + (e - b) / (self.modifiede - self.modifiedb) * (xp - self.modifiedb)
        else:
            return self.interp(xp)

    def cdf(self, size):
        if size < self.x[0] or size > self.x[-1]:
            raise ValueError("size out of range.")

        return 10**(self.modified_interp(size) - self.y[0])

    def random_size(self, threshold):
        # random sample according to the flux vs size distribution
        pcondition = self.cdf(threshold)
        try:
            s = bisection_search(self.cdf, self.x[0], self.x[-1], random.random()*pcondition)
        except ValueError:
            s = self.x[-1]

        return s

def golden_section_search(f, a, b, tolerance=1e-10, kind='min'):
    # f should be a strictly unimodal function
    if kind != 'max' and kind != 'min':
        raise ValueError("Unknown kind of search.")

    # Golden ration
    gr = (sqrt(5) + 1)/2

    c = b - (b - a) / gr
    d = a + (b - a) / gr
    while abs(c - d) > tolerance:
        if kind == 'min':
            if f(c) < f(d):
                b = d
            else:
                a = c
        else:
            if f(c) > f(d):
                b = d
            else:
                a = c
        # recompute both c and d to avoid loss of precision which may lead to incorrect results or infinite loop
        c = b - (b - a) / gr
        d = a + (b - a) / gr
    return (b + a) / 2

def bisection_search(f, a, b, f0, tolerance=1e-10):
    # f should be a monotone increasing or decreasing function
    fa = f(a) - f0
    fb = f(b) - f0
    if fa*fb > 0:
        # print(a, b, fa, fb, f0)
        raise ValueError("ill-conditioned search range.")

    while abs(a - b) > tolerance:
        fab = f((a + b)/2) - f0
        if fab == 0:
            break
        if fab*fa > 0:
            a = (a + b)/2
        else:
            b = (a + b)/2
    return (a + b)/2

def main():
    f10um, f100um, f1mm, f1cm, f10cm, f1m = fromdat("results/TABLESC.DAT",
                                                    skip_header=5,
                                                    skip_footer=2,
                                                    usecols=(3, 4, 5, 6, 7, 8))
    flux = [f10um, f100um, f1mm, f1cm, f10cm, f1m]

    th = getthreshold('ORDEM2000.CMD')
    thresholds = {'10um': 0, '100um': 1, '1mm': 2, '1cm': 3, '10cm': 4, '1m': 5}
    thidx = thresholds[th]

    segs = len(f10um)
    size, vrang, angle = gettags("results/VREL0001.DAT")

    # bfly10um = [[0.0] * ADIM for _ in range(VDIM)]
    # bfly100um = [[0.0] * ADIM for _ in range(VDIM)]
    # bfly1mm = [[0.0] * ADIM for _ in range(VDIM)]
    # bfly1cm = [[0.0] * ADIM for _ in range(VDIM)]
    # bfly10cm = [[0.0] * ADIM for _ in range(VDIM)]
    # bfly1m = [[0.0] * ADIM for _ in range(VDIM)]
    # butterfly = [bfly10um, bfly100um, bfly1mm, bfly1cm, bfly10cm, bfly1m]

    outfile = open("sample.txt", mode='wt')

    expr = re.compile(" >.+[umc]?m\s+\d+-\d+.+\s\d+\s+(\d+\.\d+)")
    expe = re.compile(".+:\s+([-+]?\d+\.\d+)\s+([-+]?\d+\.\d+)\s+([-+]?\d+\.\d+)")
    vstride = len(angle)
    sstride = len(angle)*len(vrang)
    for i in range(segs):
        # print("\r{:5.1f}%".format(i/segs*100), end='')
        try:
            vdat = open("results/VREL{:0>4}.DAT".format(i+1))
        except OSError as e:
            # print(e)
            return
        while "S/C velocity components (km/s)".upper() not in vdat.readline().upper():
            continue
        else:
            r, veast0, vnorth0 = map(float, expe.findall((vdat.readline()))[0])
            a0 = atan2(vnorth0, veast0)
        while "size       Vmag".upper() not in vdat.readline().upper():
            continue

        percentage = []
        for l in vdat:
            per = expr.findall(l)[0]
            percentage.append(float(per)/100.)
        vdat.close()

        # flux vs size distribution in local
        sizex = [-3., -2., -1., 0., 1., 2.]     # log10(size) for size in 0.001cm 0.01cm 0.1cm 1cm 10cm 100cm
        fluxy = [log10(f10um[i]), log10(f100um[i]), log10(f1mm[i]), log10(f1cm[i]), log10(f10cm[i]), log10(f1m[i])]
        foz = FluxOfSize(sizex, fluxy)

        for s, stag in enumerate(size):
            if s != thidx:
                continue
            for j, vtag in enumerate(vrang):
                v1, v2 = map(float, vtag.split('-'))
                for k, atag in enumerate(angle):
                    if percentage[s*sstride + j*vstride + k] < 1e-8:
                        continue

                    t = flux[s][i]/segs * percentage[s*sstride + j*vstride + k]
                    n = max(10, 10**ceil(log10(t)))
                    #n = 1
                    fraction = t/n

                    a1 = float(angle[k])
                    a2 = a1 + 10.  # Default to 10 degrees according to ORDEM2000 specification

                    for ii in range(n):
                        # randomize the diameter according to the flux vs size distribution
                        try:
                            z = 10**foz.random_size(sizex[thidx])
                        except ValueError:
                            continue

                        # randomize the velocity and angle
                        v = random.uniform(v1, v2)
                        a = random.uniform(a1, a2)
                        a = radians(a)
                        veast = v*cos(a)
                        vnorth = v*sin(a)
                        # relative velocity
                        relv = sqrt((veast-veast0)**2 + (vnorth-vnorth0)**2)
                        # relative angle in -180 to 180 degrees
                        rela = degrees(acos(cos(a)*cos(a0) + sin(a)*sin(a0)))
                        if cos(a)*-sin(a0) + sin(a)*cos(a0) < 0:
                            rela += -1.
                        print("{:>8.5f} {:>8.4f} {:>7.1f} {:>7.1f} {:>11.4e}".format(
                            z, relv, rela, 0, fraction), file=outfile)

                        # rearrange the relative angle in 0 to 360 degrees
                        # angl = rela - a0 if rela - a0 > 0 else rela - a0 + 360
                        # vi = trunc(relv)
                        # ai = trunc(angl/10.)
                        # butterfly[s][vi][ai] += fraction
                        # end single test
                    # end angle range
                # end velocity range
            # end all size
        # end all segments
    outfile.close()
    # print("\rfinished")

    # bflyfile = open("10um", mode='wt')
    # for s, stag in enumerate(size):
    #     for j in range(VDIM):
    #         for k in range(ADIM):
    #             print(butterfly[s][j][k], file=bflyfile, end=' ')
    #         print(file=bflyfile)
    #     break
    # bflyfile.close()
    return

if __name__ == '__main__':
    main()


import json
import os
from model.Puzzle import Puzzle
from PIL import Image, ImageDraw, ImageFont, ImageColor
from cryptography.fernet import Fernet
import base64
from controller.Context import Context
from controller.Strategy import plainSave, encryptSave
# from customExcept import OverwriteSave

# State class holding all save, load, and related methods

class SaveAndLoad:
    __SAVE_DIR = "saveFiles"

    # Display all save names held in json master save file
    @classmethod
    def allSaveNames(cls) -> list[str]:
        # list that is returned
        retNames = []
        # check if file exists, otherwise raise exception
        if os.path.exists(cls.__SAVE_DIR):
            tempList = os.listdir(cls.__SAVE_DIR)
            for i in tempList:
                retNames.append(os.path.splitext(i)[0])
        return retNames

    # isSaved checks the filename in the master save file to see if it is already an in use save name
    @classmethod
    def isSaved(cls, saveName: str) -> bool:
        return os.path.exists(saveName)

    # Save shell that allows for calling save data in 4 different ways scratch, current, overwrite scratch, and overwrite current
    # This is what is called for the user interface in the form state.save(state, "saveName", "saveType")
    @staticmethod
    def saveCurrent(puzzle: Puzzle, saveName: str, encrypt: bool = False):
        if encrypt:
            SaveAndLoad.saveData(saveName, puzzle.getPuzzleLetters(), puzzle.getWordList(),
                        puzzle.getGuessedWords(), puzzle.getCurrentPoints(), puzzle.getMaxPoints(), encrypt = True)
        else:
            SaveAndLoad.saveData(saveName, puzzle.getPuzzleLetters(), puzzle.getWordList(),
                        puzzle.getGuessedWords(), puzzle.getCurrentPoints(), puzzle.getMaxPoints(), encrypt = False)
        # Save shell that allows for calling save data in 4 different ways scratch, current, overwrite scratch, and overwrite current
        # This is what is called for the user interface in the form state.save(state, "saveName", "saveType")

    @staticmethod
    def saveScratch(puzzle: Puzzle, saveName: str, encrypt: bool = False):
        if encrypt:
            SaveAndLoad.saveData(saveName, puzzle.getPuzzleLetters(),
                        puzzle.getWordList(), [], 0, puzzle.getMaxPoints(), encrypt = True)
        else:
            SaveAndLoad.saveData(saveName, puzzle.getPuzzleLetters(),
                        puzzle.getWordList(), [], 0, puzzle.getMaxPoints(), encrypt = False)

    @classmethod
    def saveImg(cls, img: str, imgName: str):

        fnt = font = ImageFont.load_default()

        image = Image.new(mode = "RGB", size = (200,160), color = "black")
        draw = ImageDraw.Draw(image)

        draw.text((10,10), img, font=fnt, fill=(255,255,0))

        image = image.resize((900, 700), resample=Image.NEAREST)
        image.save(imgName)

    @classmethod
    def __checkJsonExt(cls, fileName:str):
        fileName = fileName.strip()
        if not fileName.endswith(".json"):
            fileName += ".json"

        return fileName

    # Main saving function that is only called within the state class
    # Originally two functions, was able to be condensed with the use of default parameters
    # Using default parameters we can do all 4 save routines
    # empty parameters with only cls, savename, wordPuzzle, will default the rest and save from the baseline
    # ^ but with type 1 will overwrite the savefile with scratch
    # full parameters without a type will save current
    # ^ but with type 1 will overwrite the savefile with current
    @classmethod
    def saveData(cls, saveName:str, puzzleLetters, wordList, foundWords=[], currentPoints=0, maxPoints=0, author = "Team SNEK", encrypt = False):

        saveName = cls.__checkJsonExt(saveName)
        # checking if the wordListSize is None, meaning a scratch or overwrite scratch
        save = Context()
        if encrypt:
            save.setStrategy(encryptSave())
        else:
            save.setStrategy(plainSave())

        retList = save.executeStrategy(puzzleLetters, wordList)
        puzzleLettersStr = retList[0]
        listType = retList[1]
        wordList = retList[2]
        requiredLetter = retList[3]
        data = cls.saveParse(puzzleLettersStr, listType, wordList,
                             foundWords, currentPoints, requiredLetter, maxPoints, author, encrypt)

        # dump new save data into master file and create if none is present
        dirs = os.path.dirname(saveName)

        os.makedirs(dirs, exist_ok=True)

        with open(saveName, "w") as f:
            json.dump(data, f, indent=2)

    # load the save data into the class variables into the puzzle class
    @ classmethod
    def load(cls, loadName: str) -> Puzzle:
        loadName = cls.__checkJsonExt(loadName)

        with open(loadName, "r") as loadFile:
            data = json.load(loadFile)

        puzzleLettersStr: str = data["PuzzleLetters"]
        requiredLetter = data["RequiredLetter"]
        puzzleLetters = []
        puzzleLetters.append(requiredLetter)
        # putting the req letter in front
        puzzleLetters = [requiredLetter] + list(puzzleLettersStr.replace(requiredLetter, ""))

        if list(data)[2] == "SecretWordList":
            decrypLis = []
            key = data["Author"]

            for i in range(0, 32-len(key)):
                key += "$"

            key = key.encode("utf-8")
            key = base64.b64encode(key)
            f = Fernet(key)

            temp = data["SecretWordList"].encode("utf-8")
            decrypLis = f.decrypt(temp).decode("utf-8").split(",")
            decrypLis.remove("")

            myPuzzle = Puzzle(puzzleLetters, decrypLis, data["GuessedWords"], data["MaxPoints"], data["CurrentPoints"])
        else:
            myPuzzle = Puzzle(puzzleLetters, data["WordList"], data["GuessedWords"], data["MaxPoints"], data["CurrentPoints"])

        return myPuzzle

    # Translate from variables into json format
    @ staticmethod
    def saveParse(puzzleLetters="", wordListType="WordList", wordList=[], guessedWords=[], currentPoints=0, requiredLetter="", maxPoints=0, author = "Team SNEK", crypBool: bool = False):
        # Translate to json format
        retData = {
            "Author": author,
            "GuessedWords": guessedWords,
            wordListType: wordList,
            "PuzzleLetters": puzzleLetters,
            "RequiredLetter": requiredLetter,
            "CurrentPoints": currentPoints,
            "MaxPoints": maxPoints
        }
        return retData


from typing import Optional

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

from rex.modules.ffn import MLP
from rex.utils.span import find_closest_span_pairs, find_closest_span_pairs_with_index

class SubjObjSpan(nn.Module):
    """
    Inputs:
        hidden: (batch_size, seq_len, hidden_size)
        one_subj_head: object golden head with one subject (batch_size, hidden_size)
        one_subj_tail: object golden tail with one subject (batch_size, hidden_size)
    """

    def __init__(self, hidden_size, num_classes, threshold: Optional[float] = 0.5):
        super().__init__()
        self.threshold = threshold
        self.subj_head_ffnn = nn.Linear(hidden_size, 1)
        self.subj_tail_ffnn = nn.Linear(hidden_size, 1)
        self.obj_head_ffnn = nn.Linear(hidden_size, num_classes)
        self.obj_tail_ffnn = nn.Linear(hidden_size, num_classes)

    def get_objs_for_specific_subj(self, subj_head_mapping, subj_tail_mapping, hidden):
        # (batch_size, 1, hidden_size)
        subj_head = torch.matmul(subj_head_mapping, hidden)
        # (batch_size, 1, hidden_size)
        subj_tail = torch.matmul(subj_tail_mapping, hidden)
        # (batch_size, 1, hidden_size)
        sub = (subj_head + subj_tail) / 2
        # (batch_size, seq_len, hidden_size)
        encoded_text = hidden + sub
        # (batch_size, seq_len, num_classes)
        pred_obj_heads = self.obj_head_ffnn(encoded_text)
        # (batch_size, seq_len, num_classes)
        pred_obj_tails = self.obj_tail_ffnn(encoded_text)
        return pred_obj_heads, pred_obj_tails

    def build_mapping(self, subj_heads, subj_tails):
        """
        Build head & tail mapping for predicted subjects,
        for each instance in a batch, for a subject in all
        the predicted subjects, return a single subject
        and its corresponding mappings.
        """
        for subj_head, subj_tail in zip(subj_heads, subj_tails):
            subjs = find_closest_span_pairs(subj_head, subj_tail)
            seq_len = subj_head.shape[0]
            for subj in subjs:
                subj_head_mapping = torch.zeros(seq_len, device=subj_head.device)
                subj_tail_mapping = torch.zeros(seq_len, device=subj_tail.device)
                subj_head_mapping[subj[0]] = 1.0
                subj_tail_mapping[subj[1]] = 1.0
                yield subj, subj_head_mapping, subj_tail_mapping

    def build_batch_mapping(self, subj_head, subj_tail):
        """
        Build head & tail mapping for predicted subjects,
        for each instance in a batch, return all the predicted
        subjects and mappings.
        """
        subjs = find_closest_span_pairs(subj_head, subj_tail)
        seq_len = subj_head.shape[0]
        if len(subjs) > 0:
            subjs_head_mapping = torch.zeros(
                len(subjs), seq_len, device=subj_head.device
            )
            subjs_tail_mapping = torch.zeros(
                len(subjs), seq_len, device=subj_tail.device
            )

            for subj_idx, subj in enumerate(subjs):
                subjs_head_mapping[subj_idx, subj[0]] = 1.0
                subjs_tail_mapping[subj_idx, subj[1]] = 1.0
            return subjs, subjs_head_mapping, subjs_tail_mapping
        else:
            return None, None, None

    def forward(self, hidden, subj_head, subj_tail):
        # subj_head_out, subj_tail_out: (batch_size, seq_len, 1)
        subj_head_out = self.subj_head_ffnn(hidden)
        subj_tail_out = self.subj_tail_ffnn(hidden)
        # subj_head, subj_tail: (batch_size, seq_len)
        # obj_head_out, obj_tail_out: (batch_size, seq_len, num_classes)
        obj_head_out, obj_tail_out = self.get_objs_for_specific_subj(
            subj_head.unsqueeze(1), subj_tail.unsqueeze(1), hidden
        )

        return (
            subj_head_out.squeeze(-1),
            subj_tail_out.squeeze(-1),
            obj_head_out,
            obj_tail_out,
        )

    def predict(self, hidden):
        # hidden: 1 x hidden_size
        if hidden.shape[0] != 1:
            raise RuntimeError(
                (
                    "eval batch size must be 1 x hidden_size, "
                    f"while hidden is {hidden.shape}"
                )
            )
        # 1, hidden_size, 1
        subj_head_out = self.subj_head_ffnn(hidden)
        # 1, hidden_size, 1
        subj_tail_out = self.subj_tail_ffnn(hidden)
        subj_head_out = torch.sigmoid(subj_head_out)
        subj_tail_out = torch.sigmoid(subj_tail_out)
        pred_subj_head = subj_head_out.ge(self.threshold).long()
        pred_subj_tail = subj_tail_out.ge(self.threshold).long()

        triples = []
        # subjs: [(1, 3), (3, 3), (17, 17)]
        # subj_head_mappings: (len(subjs), seq_len)
        subjs, subj_head_mappings, subj_tail_mappings = self.build_batch_mapping(
            pred_subj_head.squeeze(0).squeeze(-1), pred_subj_tail.squeeze(0).squeeze(-1)
        )

        if subjs:
            # obj_head_out: (len(subjs), seq_len, num_classes)
            obj_head_out, obj_tail_out = self.get_objs_for_specific_subj(
                subj_head_mappings.unsqueeze(1), subj_tail_mappings.unsqueeze(1), hidden
            )
            obj_head_out = torch.sigmoid(obj_head_out)
            obj_tail_out = torch.sigmoid(obj_tail_out)
            obj_head_out = obj_head_out.ge(self.threshold).long()
            obj_tail_out = obj_tail_out.ge(self.threshold).long()
            for subj_idx, subj in enumerate(subjs):
                objs = find_closest_span_pairs_with_index(
                    obj_head_out[subj_idx].permute(1, 0),
                    obj_tail_out[subj_idx].permute(1, 0),
                )
                for relation_idx, obj_pair_start, obj_pair_end in objs:
                    triples.append(
                        (
                            (subj[0], subj[1] + 1),
                            relation_idx,
                            (obj_pair_start, obj_pair_end + 1),
                        )
                    )
        return [triples]


from typing import Optional

from httpx import Client

from .responses import (
    InfoResponse,
    LoginResponse,
    SeasonObject,
    SeasonResponse,
    SeasonsObject,
    SeasonsResponse,
)

class Api:
    def __init__(self, client: Optional[Client] = None) -> None:
        """
        Creates an API instance.
        """

        self.client = client or Client(
            http2=True,
            headers={
                "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0"
            },
        )
        self.conn_id: Optional[str] = None

    def login(self) -> LoginResponse:
        """
        Login to VVVVID.
        """

        r = self.client.get("https://www.vvvvid.it/user/login")
        login = LoginResponse(r)
        self.conn_id = login.conn_id
        return login

    def info(self, show_id: int) -> InfoResponse:
        """
        Get information about a show.
        """

        r = self.client.get(
            f"https://www.vvvvid.it/vvvvid/ondemand/{show_id}/info",
            params={"conn_id": self.conn_id},
        )
        return InfoResponse(r)

    def seasons(self, show_id: int) -> list[SeasonsObject]:
        """
        Get all the seasons the show has.
        """

        r = self.client.get(
            f"https://www.vvvvid.it/vvvvid/ondemand/{show_id}/seasons",
            params={"conn_id": self.conn_id},
        )
        s = SeasonsResponse(r)
        return s.get()

    def season(self, show_id: int, season_id: int) -> list[SeasonObject]:
        """
        Get information about a season.
        """

        r = self.client.get(
            f"https://www.vvvvid.it/vvvvid/ondemand/{show_id}/season/{season_id}",
            params={"conn_id": self.conn_id},
        )
        s = SeasonResponse(r)
        return s.get(self.client)


from functools import cached_property

from typing import Dict, List, Sequence

from .column_names import COLUMN_NAME
from .events_factory import EventsFactory
from .lttng_info import LttngInfo
from .ros2_tracing.data_model import Ros2DataModel
from .value_objects import TimerCallbackValueLttng, TimerControl, TimerInit
from ...common import Columns, Util
from ...record import (merge, merge_sequential,
                       merge_sequential_for_addr_track,
                       RecordFactory,
                       RecordsFactory,
                       RecordsInterface)

class RecordsSource():

    def __init__(
        self,
        data: Ros2DataModel,
        info: LttngInfo
    ) -> None:
        self._data = data
        self._preprocess(self._data)
        self._info = info

    @staticmethod
    def _preprocess(data: Ros2DataModel):
        data.rclcpp_publish_instances.rename_columns(
            {COLUMN_NAME.RCLCPP_PUBLISH_TIMESTAMP: COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP}
        )

    @cached_property
    def _grouped_callback_start(self) -> Dict[int, RecordsInterface]:
        records = self._data.callback_start_instances.clone()
        group: Dict[int, RecordsInterface] = {}
        for k, v in records.groupby([COLUMN_NAME.IS_INTRA_PROCESS]).items():
            assert len(k) == 1
            group[k[0]] = v
        return group

    @cached_property
    def inter_proc_comm_records(self) -> RecordsInterface:
        """
        Compose inter process communication records.

        Used tracepoints
        - rclcpp_publish
        - dds_bind_addr_to_addr
        - dds_bind_addr_to_stamp
        - rcl_publish (Optional)
        - dds_write (Optional)
        - dispatch_subscription_callback
        - callback_start

        Returns
        -------
        RecordsInterface
            columns
            - callback_object
            - callback_start_timestamp
            - publisher_handle
            - rclcpp_publish_timestamp
            - rcl_publish_timestamp (Optional)
            - dds_write_timestamp (Optional)
            - message_timestamp
            - source_timestamp

        """
        publish = self._data.rclcpp_publish_instances

        publish = merge_sequential_for_addr_track(
            source_records=publish,
            copy_records=self._data.dds_bind_addr_to_addr,
            sink_records=self._data.dds_bind_addr_to_stamp,
            source_stamp_key=COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP,
            source_key=COLUMN_NAME.MESSAGE,
            copy_stamp_key=COLUMN_NAME.DDS_BIND_ADDR_TO_ADDR_TIMESTAMP,
            copy_from_key=COLUMN_NAME.ADDR_FROM,
            copy_to_key=COLUMN_NAME.ADDR_TO,
            sink_stamp_key=COLUMN_NAME.DDS_BIND_ADDR_TO_STAMP_TIMESTAMP,
            sink_from_key=COLUMN_NAME.ADDR,
            columns=[
                COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP,
                COLUMN_NAME.DDS_BIND_ADDR_TO_STAMP_TIMESTAMP,
                COLUMN_NAME.MESSAGE,
                COLUMN_NAME.SOURCE_TIMESTAMP,
            ],
            progress_label='binding: message_addr and rclcpp_publish',
        )

        rcl_publish_records = self._data.rcl_publish_instances
        rcl_publish_records.drop_columns([COLUMN_NAME.PUBLISHER_HANDLE])
        if len(rcl_publish_records) > 0:
            publish = merge_sequential(
                left_records=publish,
                right_records=rcl_publish_records,
                left_stamp_key=COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP,
                right_stamp_key=COLUMN_NAME.RCL_PUBLISH_TIMESTAMP,
                join_left_key=COLUMN_NAME.MESSAGE,
                join_right_key=COLUMN_NAME.MESSAGE,
                how='left',
                columns=[
                    COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP,
                    COLUMN_NAME.RCL_PUBLISH_TIMESTAMP,
                    COLUMN_NAME.PUBLISHER_HANDLE,
                    COLUMN_NAME.MESSAGE,
                    COLUMN_NAME.MESSAGE_TIMESTAMP,
                ],
                progress_label='binding: rclcpp_publish and rcl_publish',
            )

        dds_write = self._data.dds_write_instances
        if len(dds_write) > 0:
            publish = merge_sequential(
                left_records=publish,
                right_records=dds_write,
                left_stamp_key=COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP,
                right_stamp_key=COLUMN_NAME.DDS_WRITE_TIMESTAMP,
                join_left_key=COLUMN_NAME.MESSAGE,
                join_right_key=COLUMN_NAME.MESSAGE,
                columns=Columns(publish.columns + dds_write.columns).as_list(),
                how='left',
                progress_label='binding: rcl_publish and dds_write',
            )

        # When both intra_publish and inter_publish are used, value mismatch occurs when merging.
        # In order to merge at the latency time,
        # align the time to intra_publish if intra_process communication is used.
        intra_publish = self._data.rclcpp_intra_publish_instances.clone()
        intra_publish.drop_columns([
            COLUMN_NAME.MESSAGE, COLUMN_NAME.MESSAGE_TIMESTAMP
        ])
        publish = merge_sequential(
            left_records=intra_publish,
            right_records=publish,
            left_stamp_key=COLUMN_NAME.RCLCPP_INTRA_PUBLISH_TIMESTAMP,
            right_stamp_key=COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP,
            join_left_key=COLUMN_NAME.PUBLISHER_HANDLE,
            join_right_key=COLUMN_NAME.PUBLISHER_HANDLE,
            columns=Columns(intra_publish.columns + publish.columns).as_list(),
            how='right'
        )
        rclcpp_publish = [None] * len(publish.data)
        for i, record in enumerate(publish.data):
            if COLUMN_NAME.RCLCPP_INTRA_PUBLISH_TIMESTAMP in record.data:
                rclcpp_publish[i] = record.data[COLUMN_NAME.RCLCPP_INTRA_PUBLISH_TIMESTAMP]
            else:
                rclcpp_publish[i] = record.data[COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP]
        publish.append_column(
            COLUMN_NAME.RCLCPP_PUBLISH_TIMESTAMP, rclcpp_publish)
        publish.drop_columns([
            COLUMN_NAME.RCLCPP_INTRA_PUBLISH_TIMESTAMP,
            COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP
        ])

        callback_start_instances = self.inter_callback_records
        subscription = self._data.dispatch_subscription_callback_instances

        subscription = merge_sequential(
            left_records=subscription,
            right_records=callback_start_instances,
            left_stamp_key=COLUMN_NAME.DISPATCH_SUBSCRIPTION_CALLBACK_TIMESTAMP,
            right_stamp_key=COLUMN_NAME.CALLBACK_START_TIMESTAMP,
            join_left_key=COLUMN_NAME.CALLBACK_OBJECT,
            join_right_key=COLUMN_NAME.CALLBACK_OBJECT,
            columns=Columns(subscription.columns + callback_start_instances.columns).as_list(),
            how='left',
            progress_label='binding: dispatch_subscription_callback and callback_start',
        )

        # communication = merge(
        #     publish,
        #     self._data.on_data_available_instances,
        #     join_left_key=COLUMN_NAME.SOURCE_TIMESTAMP,
        #     join_right_key=COLUMN_NAME.SOURCE_TIMESTAMP,
        #     columns=publish.columns,
        #     how='left',
        #     progress_label='binding: source_timestamp and on_data_available',
        # )

        communication = merge(
            publish,
            subscription,
            join_left_key=COLUMN_NAME.SOURCE_TIMESTAMP,
            join_right_key=COLUMN_NAME.SOURCE_TIMESTAMP,
            columns=Columns(publish.columns + subscription.columns).as_list(),
            how='left',
            progress_label='binding: source_timestamp and callback_start',
        )

        communication.drop_columns(
            [
                COLUMN_NAME.IS_INTRA_PROCESS,
                COLUMN_NAME.ADDR,
                COLUMN_NAME.MESSAGE,
                COLUMN_NAME.DDS_BIND_ADDR_TO_STAMP_TIMESTAMP,
                COLUMN_NAME.DISPATCH_SUBSCRIPTION_CALLBACK_TIMESTAMP,
            ],
        )

        return communication

    @cached_property
    def publish_records(self) -> RecordsInterface:
        """
        Compose publish records.

        Returns
        -------
        RecordsInterface
            Columns
            - publisher_handle
            - rclcpp_publish_timestamp
            - rcl_publish_timestamp (Optional)
            - dds_write_timestamp (Optional)
            - message_timestamp
            - source_timestamp

        """
        inter_proc_publish = self._data.rclcpp_publish_instances

        rcl_publish_records = self._data.rcl_publish_instances
        rcl_publish_records.drop_columns([COLUMN_NAME.PUBLISHER_HANDLE])
        if len(rcl_publish_records) > 0:
            inter_proc_publish = merge_sequential(
                left_records=inter_proc_publish,
                right_records=rcl_publish_records,
                left_stamp_key=COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP,
                right_stamp_key=COLUMN_NAME.RCL_PUBLISH_TIMESTAMP,
                join_left_key='tid',
                join_right_key='tid',
                how='left',
                columns=[
                    COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP,
                    COLUMN_NAME.RCL_PUBLISH_TIMESTAMP,
                    COLUMN_NAME.PUBLISHER_HANDLE,
                    COLUMN_NAME.MESSAGE,
                    'tid',
                    COLUMN_NAME.MESSAGE_TIMESTAMP,
                ],
                progress_label='binding: rclcpp_publish and rcl_publish',
            )

        dds_write = self._data.dds_write_instances
        if len(dds_write) > 0:
            inter_proc_publish = merge_sequential(
                left_records=inter_proc_publish,
                right_records=dds_write,
                left_stamp_key=COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP,
                right_stamp_key=COLUMN_NAME.DDS_WRITE_TIMESTAMP,
                join_left_key='tid',
                join_right_key='tid',
                columns=Columns(inter_proc_publish.columns + dds_write.columns).as_list(),
                how='left',
                progress_label='binding: rclcppp_publish and dds_write',
            )

        inter_proc_publish = merge_sequential(
            left_records=inter_proc_publish,
            right_records=self._data.dds_bind_addr_to_stamp,
            left_stamp_key=COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP,
            right_stamp_key=COLUMN_NAME.DDS_BIND_ADDR_TO_STAMP_TIMESTAMP,
            join_left_key='tid',
            join_right_key='tid',
            columns=Columns(
                inter_proc_publish.columns + self._data.dds_bind_addr_to_stamp.columns).as_list(),
            how='left',
            progress_label='binding: rclcppp_publish and source_timestamp',
        )

        inter_proc_publish.drop_columns(
            [
                COLUMN_NAME.IS_INTRA_PROCESS,
                COLUMN_NAME.ADDR,
                COLUMN_NAME.MESSAGE,
                COLUMN_NAME.DDS_BIND_ADDR_TO_STAMP_TIMESTAMP,
                COLUMN_NAME.DISPATCH_SUBSCRIPTION_CALLBACK_TIMESTAMP,
            ],
        )

        intra_proc_publish = self._data.rclcpp_intra_publish_instances.clone()
        intra_proc_publish.drop_columns([COLUMN_NAME.MESSAGE])
        # intra_proc_publish.drop_columns([COLUMN_NAME.MESSAGE])

        # When publishing to both intra-process and inter-process,
        # intra-process communication is done first.
        # On the other hand,
        # there are cases where only one or the other is done, so we use outer join.
        # https://github.com/ros2/rclcpp/blob/galactic/rclcpp/include/rclcpp/publisher.hpp#L203
        publish = merge_sequential(
            left_records=intra_proc_publish,
            right_records=inter_proc_publish,
            left_stamp_key=COLUMN_NAME.RCLCPP_INTRA_PUBLISH_TIMESTAMP,
            right_stamp_key=COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP,
            join_left_key=COLUMN_NAME.PUBLISHER_HANDLE,
            join_right_key=COLUMN_NAME.PUBLISHER_HANDLE,
            columns=Columns(inter_proc_publish.columns + intra_proc_publish.columns).as_list(),
            how='outer',
            progress_label='binding intra_publish and inter_publish'
        )

        publish_stamps = []
        maxsize = 2**64-1
        for record in publish.data:
            rclcpp_publish, rclcpp_intra_publish = maxsize, maxsize
            if COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP in record.columns:
                rclcpp_publish = record.get(COLUMN_NAME.RCLCPP_INTER_PUBLISH_TIMESTAMP)
            if COLUMN_NAME.RCLCPP_INTRA_PUBLISH_TIMESTAMP in record.columns:
                rclcpp_intra_publish = record.get(COLUMN_NAME.RCLCPP_INTRA_PUBLISH_TIMESTAMP)
            inter_intra_publish = min(rclcpp_publish, rclcpp_intra_publish)
            publish_stamps.append(inter_intra_publish)

        publish.append_column(COLUMN_NAME.RCLCPP_PUBLISH_TIMESTAMP, publish_stamps)

        columns = []
        columns.append(COLUMN_NAME.PUBLISHER_HANDLE)
        columns.append(COLUMN_NAME.RCLCPP_PUBLISH_TIMESTAMP)
        if COLUMN_NAME.RCL_PUBLISH_TIMESTAMP in publish.columns:
            columns.append(COLUMN_NAME.RCL_PUBLISH_TIMESTAMP)
        if COLUMN_NAME.DDS_WRITE_TIMESTAMP in publish.columns:
            columns.append(COLUMN_NAME.DDS_WRITE_TIMESTAMP)
        columns.append(COLUMN_NAME.MESSAGE_TIMESTAMP)
        columns.append(COLUMN_NAME.SOURCE_TIMESTAMP)

        drop_columns = set(publish.columns) - set(columns)
        publish.drop_columns(list(drop_columns))
        publish.reindex(columns)

        return publish

    def create_timer_events_factory(
        self,
        timer_callback: TimerCallbackValueLttng
    ) -> EventsFactory:
        """
        Create tiemr events factory.

        Parameters
        ----------
        timer_callback : TimerCallbackValueLttng
            target callback to create timer events.

        Returns
        -------
        EventsFactory

        """
        class TimerEventsFactory(EventsFactory):

            def __init__(self, ctrls: Sequence[TimerControl]) -> None:
                self._ctrls = ctrls

            def create(self, until_ns: int) -> RecordsInterface:

                columns = [
                    COLUMN_NAME.TIMER_EVENT_TIMESTAMP,
                ]

                records = RecordsFactory.create_instance(None, columns)
                for ctrl in self._ctrls:

                    if isinstance(ctrl, TimerInit):
                        ctrl._timestamp
                        timer_timestamp = ctrl._timestamp
                        while timer_timestamp < until_ns:
                            record_dict = {
                                            COLUMN_NAME.TIMER_EVENT_TIMESTAMP: timer_timestamp,
                            }
                            record = RecordFactory.create_instance(record_dict)
                            records.append(record)
                            timer_timestamp = timer_timestamp+ctrl.period_ns

                return records

        timer_ctrls = self._info.get_timer_controls()

        filtered_timer_ctrls = Util.filter_items(
            lambda x: x.timer_handle == timer_callback.timer_handle, timer_ctrls)

        return TimerEventsFactory(filtered_timer_ctrls)

    @cached_property
    def tilde_publish_records(self) -> RecordsInterface:
        """
        Compose tilde publish records.

        Returns
        -------
        RecordsInterface
            columns:
            - tilde_publish_timestamp
            - tilde_publisher
            - tilde_message_id
            - tilde_subscription

        """
        records = self._data.tilde_publish
        records.rename_columns({'publisher': 'tilde_publisher'})

        subscription: List[int] = []
        for record in records:
            subscription_id = record.get('subscription_id')
            subscription.append(self._info.tilde_sub_id_map[subscription_id])

        records.append_column('tilde_subscription', subscription)
        records.drop_columns(['subscription_id'])
        return records

    @cached_property
    def tilde_subscribe_records(self) -> RecordsInterface:
        """
        Compose tilde subscribe records.

        Returns
        -------
        RecordsInterface
            columns:
            - tilde_subscribe_timestamp
            - tilde_subscription
            - tilde_message_id

        """
        records = self._data.tilde_subscribe
        records.rename_columns({'subscription': 'tilde_subscription'})
        return records

    @cached_property
    def intra_callback_records(self) -> RecordsInterface:
        intra_proc_subscribe = RecordsFactory.create_instance(
            None,
            ['callback_start_timestamp', 'callback_object', 'is_intra_process']
        )
        if 1 in self._grouped_callback_start:
            intra_callback_start = self._grouped_callback_start[1].clone()
            intra_proc_subscribe.concat(intra_callback_start)
        return intra_proc_subscribe

    @cached_property
    def inter_callback_records(self) -> RecordsInterface:
        intra_proc_subscribe = RecordsFactory.create_instance(
            None,
            ['callback_start_timestamp', 'callback_object', 'is_intra_process']
        )
        if 0 in self._grouped_callback_start:
            intra_callback_start = self._grouped_callback_start[0].clone()
            intra_proc_subscribe.concat(intra_callback_start)
        return intra_proc_subscribe

    @cached_property
    def subscribe_records(self) -> RecordsInterface:
        callback_start_instances = self.inter_callback_records
        inter_proc_subscrube = self._data.dispatch_subscription_callback_instances

        inter_proc_subscrube = merge_sequential(
            left_records=inter_proc_subscrube,
            right_records=callback_start_instances,
            left_stamp_key=COLUMN_NAME.DISPATCH_SUBSCRIPTION_CALLBACK_TIMESTAMP,
            right_stamp_key=COLUMN_NAME.CALLBACK_START_TIMESTAMP,
            join_left_key=COLUMN_NAME.CALLBACK_OBJECT,
            join_right_key=COLUMN_NAME.CALLBACK_OBJECT,
            columns=Columns(
                inter_proc_subscrube.columns + callback_start_instances.columns).as_list(),
            how='left',
            progress_label='binding: dispatch_subscription_callback and callback_start',
        )

        intra_proc_subscribe = self.intra_callback_records

        subscribe = merge_sequential(
            left_records=inter_proc_subscrube,
            right_records=intra_proc_subscribe,
            left_stamp_key=COLUMN_NAME.CALLBACK_START_TIMESTAMP,
            right_stamp_key=COLUMN_NAME.CALLBACK_START_TIMESTAMP,
            join_left_key=COLUMN_NAME.CALLBACK_OBJECT,
            join_right_key=COLUMN_NAME.CALLBACK_OBJECT,
            columns=Columns(inter_proc_subscrube.columns + intra_proc_subscribe.columns).as_list(),
            how='outer',
            progress_label='binding intra and inter subscribe'
        )

        return subscribe

    @cached_property
    def intra_proc_comm_records(self) -> RecordsInterface:
        """
        Compose intra process communication records.

        Used tracepoints
        - dispatch_intra_process_subscription_callback
        - rclcpp_publish
        - message_construct
        - callback_start

        Returns
        -------
        RecordsInterface
            columns:
            - callback_object
            - callback_start_timestamp
            - publisher_handle
            - rclcpp_publish_timestamp
            - message_timestamp

        """
        sink_records = self._data.dispatch_intra_process_subscription_callback_instances
        intra_publish_records = merge_sequential_for_addr_track(
            source_records=self._data.rclcpp_intra_publish_instances,
            copy_records=self._data.message_construct_instances,
            sink_records=sink_records,
            source_stamp_key=COLUMN_NAME.RCLCPP_INTRA_PUBLISH_TIMESTAMP,
            source_key=COLUMN_NAME.MESSAGE,
            copy_stamp_key=COLUMN_NAME.MESSAGE_CONSTRUCT_TIMESTAMP,
            copy_from_key=COLUMN_NAME.ORIGINAL_MESSAGE,
            copy_to_key=COLUMN_NAME.CONSTRUCTED_MESSAGE,
            sink_stamp_key=COLUMN_NAME.DISPATCH_INTRA_PROCESS_SUBSCRIPTION_CALLBACK_TIMESTAMP,
            sink_from_key=COLUMN_NAME.MESSAGE,
            columns=[
                'tid',
                COLUMN_NAME.DISPATCH_INTRA_PROCESS_SUBSCRIPTION_CALLBACK_TIMESTAMP,
                COLUMN_NAME.PUBLISHER_HANDLE,
                COLUMN_NAME.CALLBACK_OBJECT,
                COLUMN_NAME.RCLCPP_INTRA_PUBLISH_TIMESTAMP,
                COLUMN_NAME.MESSAGE_TIMESTAMP,
            ],
            progress_label='bindig: publish_timestamp and message_addr',
        )

        # note: Incorrect latency is calculated when intra_publish of ros-rclcpp is included.
        intra_publish_records = merge(
            left_records=intra_publish_records,
            right_records=sink_records,
            join_left_key=COLUMN_NAME.DISPATCH_INTRA_PROCESS_SUBSCRIPTION_CALLBACK_TIMESTAMP,
            join_right_key=COLUMN_NAME.DISPATCH_INTRA_PROCESS_SUBSCRIPTION_CALLBACK_TIMESTAMP,
            columns=intra_publish_records.columns + [COLUMN_NAME.MESSAGE],
            how='inner'
        )

        intra_publish_records = merge_sequential(
            left_records=intra_publish_records,
            right_records=sink_records,
            left_stamp_key=COLUMN_NAME.DISPATCH_INTRA_PROCESS_SUBSCRIPTION_CALLBACK_TIMESTAMP,
            right_stamp_key=COLUMN_NAME.DISPATCH_INTRA_PROCESS_SUBSCRIPTION_CALLBACK_TIMESTAMP,
            join_left_key=COLUMN_NAME.MESSAGE,
            join_right_key=COLUMN_NAME.MESSAGE,
            columns=intra_publish_records.columns,
            how='left_use_latest'
        )

        callback_start_instances = self.intra_callback_records

        intra_records = merge_sequential(
            left_records=intra_publish_records,
            right_records=callback_start_instances,
            left_stamp_key=COLUMN_NAME.DISPATCH_INTRA_PROCESS_SUBSCRIPTION_CALLBACK_TIMESTAMP,
            right_stamp_key=COLUMN_NAME.CALLBACK_START_TIMESTAMP,
            join_left_key=COLUMN_NAME.CALLBACK_OBJECT,
            join_right_key=COLUMN_NAME.CALLBACK_OBJECT,
            columns=intra_publish_records.columns + [
                COLUMN_NAME.CALLBACK_START_TIMESTAMP, COLUMN_NAME.IS_INTRA_PROCESS],
            how='left',
            progress_label='bindig: dispath_subsription and callback_start',
        )

        intra_records.drop_columns(
            [
                COLUMN_NAME.DISPATCH_INTRA_PROCESS_SUBSCRIPTION_CALLBACK_TIMESTAMP,
                COLUMN_NAME.MESSAGE,
                COLUMN_NAME.IS_INTRA_PROCESS
            ]
        )
        intra_records.rename_columns(
            {
                COLUMN_NAME.RCLCPP_INTRA_PUBLISH_TIMESTAMP: COLUMN_NAME.RCLCPP_PUBLISH_TIMESTAMP
            }
        )

        return intra_records

    @cached_property
    def callback_records(self) -> RecordsInterface:
        """
        Compose callback records.

        Used tracepoints
        - callback_start
        - callback_end

        Returns
        -------
        RecordsInterface
            columns:
            - callback_start_timestamp
            - callback_end_timestamp
            - callback_object

        """
        records: RecordsInterface

        records = merge_sequential(
            left_records=self._data.callback_start_instances,
            right_records=self._data.callback_end_instances,
            left_stamp_key=COLUMN_NAME.CALLBACK_START_TIMESTAMP,
            right_stamp_key=COLUMN_NAME.CALLBACK_END_TIMESTAMP,
            join_left_key=COLUMN_NAME.CALLBACK_OBJECT,
            join_right_key=COLUMN_NAME.CALLBACK_OBJECT,
            columns=[
                COLUMN_NAME.CALLBACK_START_TIMESTAMP,
                COLUMN_NAME.CALLBACK_END_TIMESTAMP,
                COLUMN_NAME.CALLBACK_OBJECT,
                COLUMN_NAME.IS_INTRA_PROCESS,
            ],
            how='inner',
            progress_label='binding: callback_start and callback_end'
        )

        records.drop_columns(
            [
                COLUMN_NAME.IS_INTRA_PROCESS
            ]
        )

        return records

    @cached_property
    def system_and_sim_times(self) -> RecordsInterface:
        return self._data.sim_time


import sqlalchemy as sa
from sqlalchemy import func

from flashcards.database import Base

class Card(Base):
    __tablename__ = 'cards'

    id = sa.Column(sa.Integer, primary_key=True)
    user_id = sa.Column(sa.Integer, sa.ForeignKey('users.id'), index=True)
    title = sa.Column('title', sa.String, nullable=False, index=True)
    content = sa.Column('content', sa.Text, nullable=True)
    created_at = sa.Column(sa.DateTime, nullable=False, server_default=func.now(), index=True)