"""utils.py

A few useful objects which do not naturally fit anywhere else.
"""
import ast
import difflib
import types
import uuid
from typing import Any, Dict, Iterable, List, Tuple

import pure_eval

from .tb_data import TracebackData  # purely for type checking

def unique_variable_name() -> str:
    """Creates a unique variable name. Useful when attempting to introduce
    a new token to see if it can fix specific cases of SyntaxError."""
    name = uuid.uuid4()
    return f"_{name.hex}"

def eval_expr(expr: str, frame: types.FrameType) -> Any:
    """Attempts to evaluate the expression 'expr' in a frame.
    Note that 'expr' might be a string containing leading spaces which need
    to be removed prior to being evaluated.

    This can raise some exceptions which are meant to be caught by the
    calling function.
    """
    node = ast.parse(expr.strip()).body[0].value  # type: ignore # noqa
    evaluator = pure_eval.Evaluator.from_frame(frame)
    return evaluator[node]  # can raise an exception

def get_similar_words(word_with_typo: str, words: Iterable[str]) -> List[str]:
    """Returns a list of similar words.

    The parameters we chose are based on experimenting with
    different values of the cutoff parameter for the difflib function
    get_close_matches.

    Suppose we have the following words:
    ['cos', 'cosh', 'acos', 'acosh']
    If we use a cutoff of 0.66, and ask for a maximum of 4 matches,
    all will be a match for 'cost'.  However, if we increase the cutoff
    to 0.67, 'acosh' will be dropped from the list, which seems sensible.

    However, this cutoff is "too generous" when dealing with long words.
    Using a cutoff up to 0.75 will result in both 'ascii_lowercase'
    and 'ascii_uppercase' matching 'ascii_lowecase'. Increasing the cutoff
    to 0.76 will drop ascii_uppercase as a match which also seems sensible.

    We thus use a heuristic cutoff based on length which ends up
    matching our expectation as to what a close match should be.

    We also do not return any matches for single character variables,
    nor do we consider single character variable potential matches.
    """
    if len(word_with_typo) == 1:
        return []
    words = set(words)  # removes duplicates
    words = sorted(list(words))  # get predictable order for tests

    if 2 <= len(word_with_typo) <= 4:
        words = [word for word in words if 2 <= len(word) <= 5]
        max_dist = 1
    elif 5 <= len(word_with_typo) <= 8:
        words = [word for word in words if 4 <= len(word) <= 10]
        max_dist = 2
    else:
        words = [word for word in words if len(word) >= 7]
        max_dist = 3

    similar_words: Dict[int, List[str]] = {}
    for word in words:
        distance = _leven(word_with_typo, word, max_dist + 1)
        if distance <= max_dist:
            if distance in similar_words:
                similar_words[distance].append(word)
            else:
                similar_words[distance] = [word]

    similar = []
    for distance in range(1, max_dist + 1):
        if distance in similar_words and similar_words[distance]:
            similar.extend(similar_words[distance])
    if not similar:  # example PI -> pi
        if word_with_typo.lower() in words:
            similar.append(word_with_typo.lower())
        elif word_with_typo.upper() in words:
            similar.append(word_with_typo.upper())
    return similar

# The following code, including comments, has been adapted from
# https://gist.github.com/giststhebearbear/4145811

def _leven(s1: str, s2: str, max_distance: int) -> int:
    #  get the smallest string so our rows are minimized
    s1, s2 = (s1, s2) if len(s1) <= len(s2) else (s2, s1)
    #  set lengths
    l1, l2 = len(s1), len(s2)

    #  We are simulating an NM matrix where n is the longer string
    #  and m is the shorter string. By doing this we can minimize
    #  memory usage to O(M).
    #  Since we are simulating the matrix we only maintain two rows
    #  at a time the current row and the previous rows.
    #  A move from the current cell looking at the cell before it indicates
    #  consideration of an insert operation.
    #  A move from the current cell looking at the cell above it indicates
    #  consideration of a deletion
    #  Both operations are cost 1
    #  A move from the current cell to the cell up and to the left indicates
    #  an edit operation of 0 cost for a matching character and a 1 cost for
    #  a non matching characters
    #  no row has been previously computed yet, set empty row
    #  Since this is also a Damerau-Levenshtein calculation transposition
    #  costs will be taken into account. These look back 2 characters to
    #  determine optimal cost based on a possible transposition
    #  example: aei -> aie with Levensthein has a cost of 2
    #  match a, change e->i change i->e => aie
    #  Damarau-Levenshtein has a cost of 1
    #  match a, transpose ei to ie => aie
    prev_row: List[int] = []

    #  build first leven matrix row
    #  The first row represents transformation from an empty string
    #  to the shorter string making it static [0-n]
    #  since this row is static we can set it as
    #  cur_row and start computation at the second row or index 1
    cur_row = [x for x in range(l1 + 1)]

    # use second length to loop through all the rows being built
    # we start at row one
    for row_num in range(1, l2 + 1):
        #  set transposition, previous, and current
        #  because the row_num always increments by one
        #  we can use row_num to set the value representing
        #  the first column which is indicative of transforming TO
        #  the empty string from our longer string
        #  transposition row maintains an extra row so that it is possible
        #  for us to apply Damarau's formula
        transposition_row, prev_row, cur_row = prev_row, cur_row, [row_num] + [0] * l1

        #  consider if we have passed the max distance if all paths through
        #  the transposition row are larger than the max we can stop calculating
        #  distance and return the last element in that row and return the max
        if transposition_row and all(
            cell_value > max_distance for cell_value in transposition_row
        ):
            return max_distance

        for col_num in range(1, l1 + 1):
            insertion_cost = cur_row[col_num - 1] + 1
            deletion_cost = prev_row[col_num] + 1
            change_cost = prev_row[col_num - 1] + (
                0 if s1[col_num - 1] == s2[row_num - 1] else 1
            )
            #  set the cell value - min distance to reach this
            #  position
            cur_row[col_num] = min(insertion_cost, deletion_cost, change_cost)

            #  test for a possible transposition optimization
            #  check to see if we have at least 2 characters
            if 1 < row_num <= col_num:  # sourcery skip
                #  test for possible transposition
                if (
                    s1[col_num - 1] == s2[col_num - 2]
                    and s2[col_num - 1] == s1[col_num - 2]
                ):
                    cur_row[col_num] = min(
                        cur_row[col_num], transposition_row[col_num - 2] + 1
                    )

    #  the last cell of the matrix is ALWAYS the shortest distance between the two strings
    return cur_row[-1]

def list_to_string(list_: Iterable[str], sep: str = ", ") -> str:
    """Transforms a list of names, like ['a', 'b', 'c'], into a single
    string of names, like "a, b, c"."""
    result = ["{c}".format(c=c.replace("'", "")) for c in list_]
    return sep.join(result)

def expected_in_result(expected: str, result: str) -> Tuple[bool, str]:
    """Used in tests. Intended to help more quickly identify
    differences between what was expected and what was found."""
    if expected in result:
        return True, "Test is satisfied: 'expected' is found in 'result'."
    result = result.strip(" ")
    lines = result.splitlines()
    best_ratio = 0.0
    best_line = ""
    for line in lines:
        line = line.strip(" ")
        ratio = difflib.SequenceMatcher(None, expected, line).ratio()
        if ratio > best_ratio:
            best_ratio = ratio
            best_line = line

    if result == best_line:
        return False, "Only differences are different white spaces at the ends."

    return False, "\n" + "\n".join(difflib.ndiff([expected], [best_line]))

def get_bad_statement(tb_data: TracebackData) -> str:
    """This function attempts to recover a complete statement
    even if it spans multiple lines."""
    try:
        ex = tb_data.records[-1].executing
        [statement] = ex.statements
        return ex.source.asttokens().get_text(statement)  # type: ignore
    except Exception:  # noqa
        if hasattr(tb_data, "original_bad_line"):
            return tb_data.original_bad_line
        elif hasattr(tb_data, "bad_line"):
            return tb_data.bad_line
        return ""

def to_code_block(code: str) -> str:
    """Takes some code and indent it with an added empty line at the top
    and bottom so that it is usable as a Markdown code block without
    showing the triple backquotes.
    """
    indent = " " * 4
    new_lines = ["\n"]
    lines = code.split("\n")
    new_lines.extend(f"{indent}{line}" for line in lines)
    new_lines.append("\n")
    return "\n".join(new_lines)

# The following is currently not used in friendly-traceback but
# is used by friendly.  It is kept here as it is part of
# a trio of functions, with one being used in friendly-traceback.
def get_highlighting_ranges(lines: List[str]) -> Dict[int, List[Tuple[int, int]]]:
    """Given a block of code highlighted primarily with carets (^),
    this function will identify the lines containing carets and break up
    that line into sub-ranges, showing at which indices the carets
    begin and end.

    It returns a dict whose index is the line number where the carets
    were found, and the content is a list of sub-ranges.
    """
    caret_set = set("^->")  # Some lines include --> to indicate that the
    # error continues on the next line
    error_lines = {}
    for index, line in enumerate(lines):
        if set(line.strip().replace(" ", "")).issubset(caret_set) and "^" in line:
            # Ensure the line contains only ^ and spaces
            line = line.replace("-", " ").replace(">", " ").rstrip("\n")
            # Then, add the amount of spaces corresponding to the number of
            # characters in the line to be highlighted
            line = line + " " * (len(lines[index - 1].rstrip("\n")) - len(line))
            error_lines[index] = get_single_line_highlighting_ranges(line)
            # includes the remaining range for the line to be highlighted
    return error_lines

def get_single_line_highlighting_ranges(line: str) -> List[Tuple[int, int]]:
    """Given a single line containing alternating sequences of consecutive
    spaces and carets (^), this function will compute the position of the
    beginning and end of each sequence of consecutive characters,
    recording them as tuples

    It returns a list corresponding containing these tuples alternating between
    regions where carets are not found and where carets are found.
    """
    caret_line = []
    scanning_carets = False
    char_index = begin = 0
    for char_index, char in enumerate(line):
        if not scanning_carets and char == "^":
            caret_line.append((begin, char_index))
            begin = char_index
            scanning_carets = True
        if scanning_carets and char == " ":
            caret_line.append((begin, char_index))
            begin = char_index
            scanning_carets = False
    # include the last subrange
    caret_line.append((begin, char_index + 1))
    return caret_line

def create_caret_highlighted(caret_line: List[Tuple[int, int]]) -> str:
    """Creates a line containing alternating sequences of spaces
    and carets, from a list containing a series of tuples denoting
    the range in which the carets are found.

    It is the inverse of get_single_line_highlighting_ranges().
    """
    line = []
    insert_caret = False
    for begin, end in caret_line:
        if insert_caret:
            line.append("^" * (end - begin))
        else:
            line.append(" " * (end - begin))
        insert_caret = not insert_caret
    return "".join(line)


import pytest

from pyrandall.spec import SpecBuilder
from pyrandall.types import BrokerKafkaSpec, RequestEventsSpec, RequestHttpSpec

@pytest.fixture
def feature():
    builder = SpecBuilder(
        specfile="v2.yaml",
        dataflow_path="examples/",
        default_request_url="http://localhost:5000",
        schemas_url="http://localhost:8899/schemas/",
    )
    return builder.feature()

def test_loads_valid_scenario_yaml():
    with pytest.raises(FileNotFoundError):
        SpecBuilder("spec.yaml", dataflow_path="random_dir").feature()

def test_request_url_missing():
    with pytest.raises(ValueError) as e:
        SpecBuilder("v2.yaml", dataflow_path="examples/").feature()
        expected = (
            "self.default_request_url is None. "
            "See README.md on how to configure a request URL."
        )
        assert expected == e.message

def test_description_present(feature):
    assert feature.description == "V2 schema exampe"

def test_scenarios_attributes_present(feature):
    scenarios = feature.scenario_items
    assert len(scenarios) == 2
    scenario_1 = scenarios[0]
    assert scenario_1.description == "HTTP to an ingest API and key-value API"
    scenario_2 = scenarios[1]
    assert scenario_2.description == "Produce and Consumer to kafka"

def test_creates_executors_for_simulate_only(feature):
    simulators = []
    for s in feature.scenario_items:
        for s in s.simulate_tasks:
            simulators.append(s)

    assert len(simulators) == 2
    i1 = simulators.pop()
    assert isinstance(i1, BrokerKafkaSpec)
    i0 = simulators.pop()
    assert isinstance(i0, RequestEventsSpec)

def test_creates_executors_for_validators_only(feature):
    validators = []
    for s in feature.scenario_items:
        for s in s.validate_tasks:
            validators.append(s)

    assert len(validators) == 3
    i3 = validators.pop()
    assert isinstance(i3, BrokerKafkaSpec)
    i2 = validators.pop()
    assert isinstance(i2, BrokerKafkaSpec)
    i1 = validators.pop()
    assert isinstance(i1, RequestHttpSpec)

def test_parse_events_plugins():
    SpecBuilder(
        specfile="v2.yaml",
        dataflow_path="examples/",
        default_request_url="http://localhost:5000",
        schemas_url="http://localhost:8899/schemas/",
    )


import datetime
from urllib.parse import urljoin
import bs4
import pymongo
import requests

mon = {
    "января": 1,
    "февраля": 2,
    "марта": 3,
    "апреля": 4,
    "мая": 5,
    "июня": 6,
    "июля": 7,
    "августа": 8,
    "сентября": 9,
    "октября": 10,
    "ноября": 11,
    "декабря": 12,
}

class MagnitParse:
    def __init__(self, promo_url, to_save):
        self.url = promo_url
        self.mongodb_save = to_save["Data_mining"]
        self.collections = self.mongodb_save["Magnit_promo_parse"]

    def run(self):
        soup = self._get_soup(self.url)
        catalog = soup.find("div", attrs={"class": "сatalogue__main"})
        for product in catalog.find_all("a", attrs={"class": "card-sale", "target": False}):
            product_info = self._parse(product)
            self._save(product_info)

    def _get_response(self, promo_url):
        return requests.get(promo_url)

    def _get_soup(self, promo_url):
        response = self._get_response(promo_url)
        return bs4.BeautifulSoup(response.text, "lxml")

    def _parse(self, product):
        data = {}
        for key, value in self.get_template().items():
            try:
                data[key] = value(product)
            except (AttributeError, ValueError):
                pass
        return data

    def get_template(self):
        return {
            "url": lambda n: urljoin(self.url, n.attrs.get("href", "")),
            "promo_name": lambda n: n.find("div", attrs={"class": "card-sale__header"}).text,
            "product_name": lambda n: n.find("div", attrs={"class": "card-sale__title"}).text,
            "old_price": lambda n: float(
                n.find("div", attrs={"class": "label__price_old"}).text.strip().replace("\n", ".")
            ),
            "new_price": lambda n: float(
                ".".join(n.find("div", attrs={"class": "label__price_new"}).text.split())
            ),
            "image_url": lambda n: urljoin(self.url, n.find("img").attrs.get("data-src")),
            "date_from": lambda n: self._get_date(
                n.find("div", attrs={"class": "card-sale__date"}).text.strip().split("\n")[0]
            ),
            "date_to": lambda n: self._get_date(
                n.find("div", attrs={"class": "card-sale__date"}).text.strip().split("\n")[-1]
            ),
        }

    def _get_date(self, date_text):
        y = int(datetime.datetime.today().strftime("%Y"))
        m = int(mon[date_text.split()[-1]])
        d = int(date_text.split()[1])
        return str(datetime.date(y, m, d))

    def _save(self, data):
        self.collections.insert_one(data)

if __name__ == "__main__":
    url = "https://magnit.ru/promo/"
    db_client = pymongo.MongoClient("mongodb://localhost:27017")
    parser = MagnitParse(url, db_client)
    parser.run()


"""pystargram URL Configuration

The `urlpatterns` list routes URLs to views. For more information please see:
    https://docs.djangoproject.com/en/1.11/topics/http/urls/
Examples:
Function views
    1. Add an import:  from my_app import views
    2. Add a URL to urlpatterns:  url(r'^$', views.home, name='home')
Class-based views
    1. Add an import:  from other_app.views import Home
    2. Add a URL to urlpatterns:  url(r'^$', Home.as_view(), name='home')
Including another URLconf
    1. Import the include() function: from django.conf.urls import url, include
    2. Add a URL to urlpatterns:  url(r'^blog/', include('blog.urls'))
"""
from django.conf import settings
from django.conf.urls import url, include
from django.conf.urls.static import static
from django.contrib import admin
# django.contrib.auth 안에 인증관련 패키지들을 담아둠
from django.contrib.auth import views as auth_views

from photos.views import hello, detail, create, signup

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^hello/$',hello),
    url(r'^photos/upload/$', create, name='create'),
        #패턴을 묶는 클라스
        #ctrl shift i 누르면 이동함 컨트롤 클릭하는 번거로움
    url(r'^photos/(?P<pk>\d+)/$', detail, name='detail'),
        # url(r'^photos/(?P<pk>\d+)/$', detail, name='detail',kwargs={'hidden':True}),
        #이런식으로 kwargs에 인자값으로 임의의 값을 넘길 수있음 저걸 받아서 뭐 이것저것
        #할수있지
        #받을때는 인자로 받으면 된다
        #def aa(hidden = False)뭐 이런식으로 ㅋ

        #내장함수 사용 이게 가장 안정적
        #이렇게하면 login내장함수가 form 이라는 이름으로 아이디 비번을 넘김 login.html로 넘김
        #넘기는 폼은 액션값 유알엘에 지정한유알엘 따라감
        #인증 순서
        #양식을 보내면 authenticate()함수가 settings의 BACKENDS 항목에 등록된 인증
        #체계기반 클래스를 하나씩 가져와서 authenticate()메서드를 호출하여 인증을 시도함

    url(r'^accounts/login/',auth_views.login,name='login',kwargs={
            'template_name':'login.html'
    }),
    url(r'^aaa/login/',auth_views.login,name='login2',kwargs={
            'template_name':'login2.html'
    }),
    url(r'^accounts/logout/',auth_views.logout,name='logout',kwargs={
            'next_page':settings.LOGIN_URL
            #여기서 알아야 할 부분은
            #로그인여부를 검사하고 로그인하지않으면 로그인 URL로 사용자를 이동시킴
            #이 URL을 저기서 가져온다 기본값은 /accounts/login/임
            #다른걸 쓰고싶으면 저 항목을 다른걸로 지정하면됨
        }),
    url(r'^users/',include('profiles.urls')),
    url(r'^join/$',signup)
    ]
        #from django.conf.urls.static import static upload_files의 주소를
    #저 document_root의 값으로 매핑 하겠다는 소리야
    #그래서 화면에는 upload_files라고 나오는데 사실은 이게 내가 MEDIA_ROOT에 설정해둔
    #값으로 되는거지 이 설명을 안써줬네
    #궁금하면 컨트롤 클릭해서 들어가보면 자세히 알려준다
    #url(r'^이렇게하고 리턴값 자체가 그 유알엘이야 들어가봐 궁금하면
    #아마 static 메서드가 정적파일을 관리해주는거 같다
    #이게 매핑 값임 매핑이 여기서 되는거임 세팅에서는 그냥 아무것도안됨 매핑은 꼭여기서
urlpatterns +=static('upload_files',document_root=settings.MEDIA_ROOT)
    #가장 중요한건 static함수는 셋팅에서 DEBUG=True 인경우에만 동작한다는 거다
    #static 파일은 이런거 안해줘도 된다 그냥 알아서 해줌

    #바디태그가있어야 동작함디버그 툴바
if settings.DEBUG:
    import debug_toolbar
    urlpatterns += [url(r'^__debug__/', include(debug_toolbar.urls)),]


from datetime import datetime

from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import redirect, render

from .forms import CourseRegisterForm, CourseTupleRegisterForm
from .models import Course, CourseTuple
from users.models import Profile

def home(request):
    return render(request, "courses/home.html", context())

def faq(request):
    faq_elements = {
        "Why do I need to create an account?":
            "We made the design decision to use accounts for three main reasons. First, it allows you to easily see "
            "and change which sections you are currently monitoring at any given time. This way, you won't receive an "
            "email about a section that you were briefly interested in joining and then forgot about months after the "
            "fact. Second, it simplifies the process of monitoring multiple sections. Instead of entering and "
            "confirming your email for every section, you only need to log in. Finally, if someone else nabs the "
            "opening we just emailed you about, you won't have to come back here and re-add it. You will continue to "
            "get emails about openings until you remove the section from your profile.",
        "What information do you store?":
            "We only store your username, email, and any sections you are currently monitoring. Passwords are "
            "encrypted using industry-standard practices and cannot be accessed by anyone once set.",
        "Why can't I add any more sections?":
            "We are currently working with rather limited resources. We are using free or budget plans for our web "
            "hosting, databases, and emails. In order to help the most students possible, we are limiting the number "
            "of sections that each user can monitor. We know that this is a less-than-optimal solution and are "
            "looking into other ways to improve our capabilities.",
        "What is the 'Year' field in the 'Add A Course' menu for?":
            "It's for the year of the session (e.g. 2020W) of the section you want to monitor. The form should "
            "autofill the correct value for you.",
        "How often do you poll the SSC?":
            "We send one request every every couple of minutes. While more requests could theoretically improve one's "
            "chances of finding a seat opening, we don't want the SSC to think that it's getting DDoSed and block our "
            "IP. Besides, checking a section that often should be more than enough to catch at least 90% of seat "
            "openings.",
        "I haven't been receiving any emails even though I know there have been openings in sections I'm monitoring.":
            "Check your Spam/Junk folder and whitelist notifier@ubccoursemonitor.email to make sure that you receive "
            "any future notifications.",
        "I got an email but the section was full when I checked.":
            "Unfortunately, someone else likely got the open seat before you did. To maximize your chances of getting "
            "into a section that you are monitoring, be sure to check the SSC as soon as you receive an email from us.",
        "I didn't act fast enough after receiving an email. Do I need to add the section again?":
            "No, you don't. After an email is sent out for a section, there will be a brief cooldown period of about "
            "twelve hours or so (so we don't spam you if a section stays open for a while), after which we will "
            "resume polling.",
        "I got a seat in the section I was monitoring. What should I do next?":
            "Great! Now you'll likely want to remove the section from your profile, so you don't get any more emails. "
            "You can do so by unticking the boxes next to them on your Profile page.",
        "The section I want to get into has a waitlist. Should I use the UBC Course Monitor or just add the waitlist "
        "section?":
            "Just add the waitlist section. UBC Course Monitor cannot help you bypass a waitlist. It could, "
            "potentially, get you into a waitlist section if it is full, but you cannot use it to bypass a waitlist.",
    }
    return render(
        request, "courses/faq.html", context(title="FAQ", faq_elements=faq_elements)
    )

def about(request):
    return render(request, "courses/about.html", context(title="About"))

@login_required()
def courses(request):
    if request.method == "POST":
        if (
            request.user.is_staff
            or request.user.profile.is_premium
            or request.user.profile.number_of_courses()
            < settings.MAX_NON_PREMIUM_SECTIONS
        ):
            c_form = CourseRegisterForm(request.POST)
            ct_form = CourseTupleRegisterForm(request.POST)

            if c_form.is_valid() and ct_form.is_valid():
                c, new_c = Course.objects.get_or_create(
                    campus=c_form.instance.campus,
                    year=c_form.instance.year,
                    session=c_form.instance.session,
                    subject=c_form.instance.subject,
                    number=c_form.instance.number,
                    section=c_form.instance.section,
                )
                c_name = c.sub_num_sec()

                ct_form.instance.course = c
                ct, new_ct = CourseTuple.objects.get_or_create(
                    course=ct_form.instance.course,
                    restricted=ct_form.instance.restricted,
                )

                get_seats = c.get_seats()
                if new_c:
                    if get_seats is False:
                        fail_message = (
                            f"We were unable to download {c_name}'s page from the SSC. This could be "
                            f"because of the SSC being down for maintenance or a database update (this "
                            f"usually occurs in the early morning, Pacific time). It could also be caused "
                            f"by high SSC traffic, usually around registration or grade releases. Please "
                            f"try again in a couple hours."
                        )
                        messages.warning(request, fail_message)
                        c.delete()
                        return redirect("courses-list")
                    elif get_seats == "invalid":
                        invalid_message = (
                            f"This section does not appear to exist. Please check your form entry for "
                            f"errors and try again. If the course does, in fact, exist, please contact "
                            f"us at contact@ubccoursemonitor.email."
                        )
                        messages.warning(request, invalid_message)
                        c.delete()
                        return redirect("courses-list")

                if ct not in request.user.profile.courses.all():
                    if get_seats == "stt":
                        stt_message = (
                            "The remaining seats in this section appear to only be available through an "
                            "STT. You will be notified if any non-STT seats open up in this section."
                        )
                        messages.warning(request, stt_message)
                    elif isinstance(get_seats, tuple) and get_seats[4]:
                        blocked_message = (
                            "This section is currently blocked for registration. You will be notified "
                            "if the section is unblocked and there is an opening."
                        )
                        messages.warning(request, blocked_message)

                    try:
                        ct_opposite_restricted = CourseTuple.objects.get(
                            course=ct_form.instance.course,
                            restricted=(not ct_form.instance.restricted),
                        )
                        if ct_opposite_restricted in request.user.profile.courses.all():
                            request.user.profile.courses.remove(ct_opposite_restricted)
                    except ObjectDoesNotExist:
                        pass
                    finally:
                        request.user.profile.courses.add(ct)
                        message = f"Your are now monitoring {c_name} "
                        message += (
                            "for any seat openings."
                            if ct.restricted
                            else "for general seat openings."
                        )
                        messages.success(request, message)
                else:
                    message = f"You are already monitoring {c_name} "
                    message += (
                        "for any seat openings."
                        if ct.restricted
                        else "for general seat openings."
                    )
                    messages.warning(request, message)
                return redirect("courses-list")

        else:
            message = (
                f"Sorry! Due to limited resources, you can only monitor {settings.MAX_NON_PREMIUM_SECTIONS} "
                f"sections at once. We are working on expanding this limit in the future. For now, go to your "
                f"profile and deselect any sections that you do not need."
            )
            messages.warning(request, message)
            return redirect("courses-list")

    else:
        default_year = (
            str(datetime.now().year)
            if datetime.now().month >= 3
            else str(datetime.now().year - 1)
        )
        default_session = (
            "W" if datetime.now().month >= 7 or datetime.now().month <= 2 else "S"
        )
        c_form = CourseRegisterForm(
            initial={"campus": "UBC", "year": default_year, "session": default_session}
        )
        ct_form = CourseTupleRegisterForm()

    user_courses = request.user.profile.courses.all()

    view_context = context(
        title="Add Course", c_form=c_form, ct_form=ct_form, courses=user_courses
    )

    return render(request, "courses/courses.html", view_context)

def context(title=None, **kwargs):
    output = {}

    if title is not None:
        output["title"] = title

    users = Profile.objects.all()
    courses_list = []
    for course in Course.objects.all():
        if users.filter(courses__course=course).count() > 0:
            courses_list.append(course)
    if len(courses_list) > 0:
        output["courses_total"] = len(courses_list)
        output["users_total"] = Profile.objects.count()

    output["active"] = settings.NON_PREMIUM_NOTIFICATIONS

    output.update(kwargs)

    return output


from Bio.Seq import Seq
from nanoRNA.scrpit.nano_utils import read_fasta_to_dic
from collections import OrderedDict
fasta_path='/home/zhguo/tombo_autogluon_tmp_file/blast_results/saureus_gene.fasta'
output_path="/home/zhguo/tombo_autogluon_tmp_file/blast_results/protein_saureus_gene.fasta"
fasta = read_fasta_to_dic(fasta_path)

result_dict=OrderedDict()
for key in fasta.keys():
    value=fasta[key]
    coding_dna = Seq(value)
    protein=coding_dna.translate(table="Bacterial")
    result_dict[key]=str(protein)

f=open(output_path,'w+')
for key,value in result_dict.items():
    f.write('>'+key+'\n')
    f.write(value+'\n')
f.close()


from __future__ import print_function

import datetime
import os
import time
import shutil
from itertools import chain
import dill

from arguments import get_args

if __name__ == '__main__':
    import torch
    import torch.multiprocessing as mp
    mp.set_start_method('spawn')

    import my_optim
    from envs import create_env
    from model import *
    from test import test
    from train_reward_module import train_rewards
    from common import *
    from train_dynamics_module import train_dynamics
    from train_online import train_online
    from eval_modules import eval_reward
    from tensorboardX import SummaryWriter

    os.environ['OMP_NUM_THREADS'] = '1'
    args = get_args()
    log(args)

    if not args.gpu:
        os.environ['CUDA_VISIBLE_DEVICES'] = ""

    torch.manual_seed(args.seed)

    args_param = vars(args)
    toprint = ['seed', 'lr', 'entropy_coef', 'value_loss_coef', 'num_steps',
               'dim']

    if args.planning:
        toprint += ['rollout']

    env_name = args.env_name
    if args.env_name.endswith("MazeEnv"):
        env_name += 'mazeid%slength%s' % (args.maze_id, args.maze_length)
        toprint += ['random_start', 'difficulty']
    if args.baseline:
        model_type = 'baseline'
        if args.neg_reward:
            model_type += '_neg_reward'
        if args.file_path:
            model_type += '_dynamics_transfer'
        toprint += ['algo', 'gae', 'num_processes']
    elif args.train_dynamics:
        model_type = 'dynamics_planning'
        toprint = ['lr', 'forward_loss_coef', 'dec_loss_coef', 'inv_loss_coef', 'rollout', 'dim',
                   'train_size']
        # env_name = os.path.basename(args.train_set.strip('/'))
        if args.single_env:
            data_args = torch.load(os.path.join(args.train_set, 'args.pt'))
            args.maze_structure = data_args.maze_structure
    elif args.train_reward:
        model_type = 'reward'
        if args.neg_reward:
            model_type += '_neg_reward'
        if args.file_path:
            model_type += '_dynamics_transfer'
        toprint += ['algo', 'gae']
        if args.planning:
            model_type += '_planning'
    elif args.train_online:
        model_type = 'online'
        toprint += ['lr', 'dec_loss_coef', 'inv_loss_coef', 'rollout', 'dim']
    if args.transfer:
        model_type += '_transfer'

    name = ''
    for arg in toprint:
        name += '_{}{}'.format(arg, args_param[arg])
    out_dir = os.path.join(args.out, env_name, model_type, name)
    args.out = out_dir

    dynamics_path = ''
    if args.dynamics_module is not None and not args.baseline:
        dynamics_path = args.dynamics_module.split('/')
        dynamics_path = dynamics_path[-4] + dynamics_path[-2] +\
            '_' + dynamics_path[-1].strip('.pt')
        args.out = os.path.join(out_dir, dynamics_path)
    os.makedirs(args.out, exist_ok=True)

    # create the tensorboard summary writer here
    tb_log_dir = os.path.join(args.log_dir, env_name, model_type, name,
        dynamics_path, 'tb_logs')
    print(tb_log_dir)
    print(args.out)

    if args.reset_dir:
        shutil.rmtree(tb_log_dir, ignore_errors=True)
    os.makedirs(tb_log_dir, exist_ok=True)
    tb_writer = SummaryWriter(log_dir=tb_log_dir)

    # dump all the arguments in the tb_log_dir
    print(args, file=open(os.path.join(tb_log_dir, "arguments"), "w"))

    env = create_env(args.env_name, framework=args.framework, args=args)
    if args.train_dynamics:
        train_dynamics(env, args, None) # tb_writer
    if args.train_reward:
        model_name = 'rewards_module'
        if args.from_checkpoint is not None:  # using curriculum
            model_name += 'curr'
        if args.single_env:
            model_name += '_single_env'
            args.maze_structure = env._env.MAZE_STRUCTURE
        args.model_name = model_name
        enc = None
        d_module = None
        assert args.dynamics_module is not None
        enc = load_encoder(env.observation_space.shape[0], args)
        if args.planning:
            d_module = load_d_module(env.action_space.shape[0], args)

        shared_model = R_Module(env.action_space.shape[0], args.dim,
                            discrete=args.discrete, baseline=args.baseline,
                            state_space=env.observation_space.shape[0])

        # shared reward module for everyone
        shared_model.share_memory()

        if args.no_shared:
            optimizer = None
        else:
            optimizer = my_optim.SharedAdam(shared_model.parameters(), lr=args.lr)
            optimizer.share_memory()

        processes = []

        train_agent_method = None

        total_args = args
        train_agent_method = train_rewards

        for rank in range(0, args.num_processes):
            if rank==0:
                p = mp.Process(target=train_agent_method, args=(
                    rank, total_args, shared_model, enc, optimizer, tb_log_dir,
                    d_module))
            else:
                p = mp.Process(target=train_agent_method, args=(
                    rank, total_args, shared_model, enc, optimizer, None, d_module))
            p.start()
            processes.append(p)

        for p in processes:
            p.join()

        torch.save((shared_model.state_dict(), args), os.path.join(
            args.out, model_name + '%s.pt' % args.num_episodes))

        print(os.path.join(args.out, model_name))
    if args.train_online:
        model_name = 'rewards_module'
        if args.from_checkpoint is not None:  # using curriculum
            model_name += 'curr'
        if args.single_env:
            model_name += '_single_env'
            args.maze_structure = env._env.MAZE_STRUCTURE
        args.model_name = model_name
        shared_enc = Encoder(env.observation_space.shape[0], args.dim,
                      use_conv=args.use_conv)
        shared_dec = Decoder(env.observation_space.shape[0], args.dim,
                      use_conv=args.use_conv)
        shared_d_module = D_Module(env.action_space.shape[0], args.dim,
                                   args.discrete)
        shared_r_module = R_Module(env.action_space.shape[0], args.dim,
                                discrete=args.discrete, baseline=args.baseline,
                                state_space=env.observation_space.shape[0])

        shared_enc = Encoder(env.observation_space.shape[0], args.dim,
                      use_conv=args.use_conv)
        shared_dec = Decoder(env.observation_space.shape[0], args.dim,
                      use_conv=args.use_conv)
        shared_d_module = D_Module(env.action_space.shape[0], args.dim,
                                   args.discrete)
        shared_r_module = R_Module(env.action_space.shape[0], args.dim,
                                discrete=args.discrete, baseline=args.baseline,
                                state_space=env.observation_space.shape[0])

        shared_enc.share_memory()
        shared_dec.share_memory()
        shared_d_module.share_memory()
        shared_r_module.share_memory()
        all_params = chain(shared_enc.parameters(), shared_dec.parameters(),
                           shared_d_module.parameters(),
                           shared_r_module.parameters())
        shared_model = [shared_enc, shared_dec, shared_d_module, shared_r_module]

        if args.single_env:
            model_name += '_single_env'
            args.maze_structure = env.MAZE_STRUCTURE

        if args.no_shared:
            optimizer = None
        else:
            optimizer = my_optim.SharedAdam(all_params, lr=args.lr)
            optimizer.share_memory()

        train_agent_method = train_online

        processes = []
        for rank in range(0, args.num_processes):
            if rank==0:
                p = mp.Process(target=train_agent_method, args=(
                    rank, args, shared_model, optimizer, tb_log_dir))
            else:
                p = mp.Process(target=train_agent_method, args=(
                    rank, args, shared_model, optimizer))
            p.start()
            processes.append(p)

        # start an eval process here
        eval_agent_method = eval_reward
        p = mp.Process(target=eval_agent_method, args=(
            args, shared_model, tb_log_dir))
        p.start()
        processes.append(p)

        for p in processes:
            p.join()
        results_dict = {'args': args}
        torch.save((shared_r_module.state_dict(), args), os.path.join(
            args.out, 'reward_module%s.pt' % args.num_episodes))
        results_dict['enc'] = shared_enc.state_dict()
        results_dict['dec'] = shared_dec.state_dict()
        results_dict['d_module'] = shared_d_module.state_dict()
        torch.save(results_dict,
            os.path.join(args.out, 'dynamics_module%s.pt' % args.num_episodes))
        log("Saved model %s" % os.path.join(args.out, model_name))


from data.config import ADMIN_ID
from keyboards.admin_keyboards.admin_default_reply import create_admin_default
from keyboards.admin_keyboards.delete_admin_confirm_inline import create_deleted_admin
from loader import bot
from utils.json_worker.users import delete_admin, get_admins

async def action_da(callback):
    user_id = callback.data.split('?')[1]
    sender_user_id = callback.data.split('?')[2]
    username = (await get_admins())[user_id]['username']

    if sender_user_id == str(ADMIN_ID):
        await delete_admin(user_id)
        await callback.answer(f"Админ {username} успешно удален")

    elif sender_user_id == user_id:
        await callback.message.answer("Невозможно удалить самого себя!",
                                      reply_markup=create_admin_default())
    else:
        await bot.send_message(chat_id=ADMIN_ID,
                               text=f"подтвердите удаление админа {username}",
                               reply_markup=create_deleted_admin(user_id=user_id,
                                                                 sender_user_id=sender_user_id))
        await callback.answer("Действие отправлено на модерацию")


from tensorflow.python.keras.models import Sequential
from tensorflow.python.keras.models import Model
from tensorflow.python.keras import backend as K
from tensorflow.python.keras.layers import Conv2D, MaxPooling2D, UpSampling2D, Layer
from tensorflow.python.keras.layers import Activation, Dropout, Flatten, Dense, Input
from tensorflow.python.keras.layers import BatchNormalization
from tensorflow.python.keras.callbacks import ModelCheckpoint
from tensorflow.python.keras.preprocessing.image import ImageDataGenerator, load_img, img_to_array
from tensorflow.python.keras.applications import VGG16
from tensorflow.python.keras.optimizers import Adam
from PIL import Image
import numpy as np
from skimage import io, color
from skimage.transform import resize
import os
import cv2

import tensorflow as tf
from tensorflow.python import keras
'''
config = tf.ConfigProto()
config.gpu_options.per_process_gpu_memory_fraction = 0.7
keras.backend.set_session(tf.Session(config=config))
'''
directory = "images/"
paths = os.listdir(directory)
#np.random.shuffle(paths)
'''
batch_size = 8

length = len(paths)
valPaths = paths[int(0.8*length):]
paths = paths[:int(0.8*length)]

length = len(paths)
print("Training Set : ", length , " Validation Set : ", len(valPaths))
steps_per_epoch = length/batch_size
'''
def random_brightness(image):
    image_HLS = cv2.cvtColor(image,cv2.COLOR_RGB2HLS) ## Conversion to HLS
    image_HLS = np.array(image_HLS, dtype = np.float64)
    random_brightness_coefficient = np.random.uniform()+0.5 ## generates value between 0.5 and 1.5
    image_HLS[:,:,1] = image_HLS[:,:,1]*random_brightness_coefficient ## scale pixel values up or down for channel 1(Lightness)
    image_HLS[:,:,1][image_HLS[:,:,1]>255]  = 255 ##Sets all values above 255 to 255
    image_HLS = np.array(image_HLS, dtype = np.uint8)
    image_RGB = cv2.cvtColor(image_HLS,cv2.COLOR_HLS2RGB) ## Conversion to RGB
    return image_RGB

def loadXY(paths, directory):
    x = []
    y = []
    for path in paths :
        img = io.imread(directory + path)
        img = random_brightness(img)
        img = color.rgb2lab(img)
        x.append(np.stack((img[:,:,0],)*3, axis=-1))
        #x.append(img[:,:,0])
        y.append(img[:,:,1:])

    x = np.array(x)/255
    #x = x.reshape(x.shape+(1,))
    y = np.array(y)/128

    return x,y

def ValidationLoader(valPaths, batch_size) :
    L = len(valPaths)

    while True:
        batch_start = 0
        batch_end = batch_size

        #np.random.shuffle(valPaths)

        while batch_start < L :
            limit = np.minimum(batch_end, L)

            vx, vy = loadXY(valPaths[batch_start:limit], "trainingSet/")

            yield (vx,vy)

            batch_start += batch_size
            batch_end += batch_size

def ImageLoader(paths, batch_size):
    L = len(paths)

    while True:
        batch_start = 0
        batch_end = batch_size

        np.random.shuffle(paths)

        while batch_start < L :
            limit = np.minimum(batch_end, L)

            x, y = loadXY(paths[batch_start:limit], "trainingSet/")

            yield (x,y)

            batch_start += batch_size
            batch_end += batch_size

vggModel = VGG16(include_top = False, weights = "imagenet", input_shape = (None,None,3))

transfer_layer = vggModel.get_layer('block5_conv3')
vggModel = Model(inputs=vggModel.input, outputs=transfer_layer.output)
vggModel.trainable = False

newModel = Sequential()
newModel.add(vggModel)

#newModel.add(UpSampling2D((2,2)))
newModel.add(BatchNormalization())
newModel.add(Conv2D(512, (3, 3), padding = 'same', activation = "relu"))
newModel.add(BatchNormalization())
newModel.add(Conv2D(512, (3, 3), padding = 'same',  activation = "relu"))

newModel.add(UpSampling2D((2, 2)))
newModel.add(BatchNormalization())
newModel.add(Conv2D(256, (3, 3), padding = 'same', activation = "relu"))
newModel.add(BatchNormalization())
newModel.add(Conv2D(256, (3, 3), padding = 'same',  activation = "relu"))
newModel.add(BatchNormalization())
newModel.add(Conv2D(256, (3, 3), padding = 'same',  activation = "relu"))

newModel.add(UpSampling2D((2, 2)))
newModel.add(BatchNormalization())
newModel.add(Conv2D(128, (3, 3), padding = 'same',  activation = "relu"))
newModel.add(BatchNormalization())
newModel.add(Conv2D(128, (3, 3), padding = 'same',  activation = "relu"))

newModel.add(UpSampling2D((2, 2)))
newModel.add(BatchNormalization())
newModel.add(Conv2D(64, (3, 3), padding = 'same',  activation = "relu"))
newModel.add(BatchNormalization())
newModel.add(Conv2D(64, (3, 3), padding = 'same',  activation = "relu"))

newModel.add(UpSampling2D((2,2)))
newModel.add(Conv2D(2, (3, 3), padding = 'same',  activation = "tanh"))

newModel.summary()

adam = Adam(lr = 0.00001)

newModel.compile(loss='mse', optimizer=adam, metrics=['accuracy'])
newModel.load_weights("weights.hdf5")
'''
filepath="weights2L/colorize-updated-weights-{epoch:02d}-{loss:.4f}.hdf5"
checkpoint = ModelCheckpoint(filepath, monitor='loss', verbose=1, save_best_only=False, mode='min')
callbacks_list = [checkpoint]

newModel.fit_generator(ImageLoader(paths, batch_size), steps_per_epoch=steps_per_epoch, validation_data = ValidationLoader(valPaths, batch_size), validation_steps = len(valPaths)/batch_size,epochs=20, callbacks=callbacks_list)'''
x = []
img = io.imread("k.jpg")
#img = np.stack((img,)*3, axis=-1)
#img = resize(img,(512,1024))
io.imsave("original.jpg", img)
img = color.rgb2lab(img)
x.append(np.stack((img[:,:,0],)*3, axis=-1))
x = np.array(x)
x = x/255
#x = np.expand_dims(x,axis=0)

y = newModel.predict(x)
y = np.squeeze(y)

print(y.shape)

img[:,:,1:] = y * 128
#img[:,:,1] = img[:,:,1] * 1.5
img = np.around(img)
img = color.lab2rgb(img)
print(img.shape)

io.imsave("output.jpg", img)


from collections import deque

def orangesRotting(grid):
  q = deque()
  time, fresh = 0, 0

  ROWS, COLS = len(grid), len(grid[0])
  directions = [[0,1], [0,-1], [1,0], [-1,0]]
  for r in range(ROWS):
    for c in range(COLS):
      if grid[r][c] == 1:
        fresh += 1
      elif grid[r][c] == 2:
        q.append([r, c])

  while q and fresh > 0:
    for _ in range(len(q)): #initial length of queue before we add stuff below, this ensures that for each time period we only evaluate the paths from the oranges that were rotten when it started
      r, c = q.popleft()
      for dr, dc in directions:
        row, col = dr + r, dc + c
        if 0 <= row < ROWS and 0 <= col < COLS and grid[row][col] == 1:
          grid[row][col] = 2
          q.append([row, col])
          fresh -= 1
    time += 1

  return time if fresh == 0 else -1 #return -1 if unable to rot all oranges

def main():
  print(orangesRotting([[2,2],[1,1],[0,0],[2,0]]))
  print(orangesRotting([[2,1,1],[1,1,0],[0,1,1]]))
main()


# -*- coding: utf-8 -*-
from django.db import models, migrations

class Migration(migrations.Migration):

    dependencies = [
        ('helpdesk', '0007_max_length_by_integer'),
    ]

    operations = [
        migrations.AddField(
            model_name='queue',
            name='permission_name',
            field=models.CharField(help_text='Name used in the django.contrib.auth permission system', max_length=50, null=True, verbose_name='Django auth permission name', blank=True),
        ),
    ]


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

from zope.interface import implements

from Products.CMFCore.utils import getToolByName
from collective.powertoken.core.interfaces import IPowerActionProvider
from collective.powertoken.core.exceptions import PowerTokenConfigurationError

class WorkflowActionProvider(object):
    """
    Perform workflow action on the given content.

    The workflow transition that will be executed must be given in the params attribute:
        {'workflow_action': 'publish'}
    """
    implements(IPowerActionProvider)

    def __init__(self, context, request):
        self.context = context
        self.request = request

    def doAction(self, action, **kwargs):
        if not action.params.get('workflow_action'):
            raise PowerTokenConfigurationError('workflow.doAction need the workflow_action parameter in the IPowerActionConfiguration object')
        context = self.context
        portal_workflow = getToolByName(context, 'portal_workflow')
        portal_workflow.doActionFor(context, action.params['workflow_action'], comment=kwargs.get('comment', None))
        return portal_workflow.getInfoFor(context, 'review_state')


import pandas as pd
from sklearn.datasets import load_digits
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.model_selection import learning_curve
import numpy as np

# Q1.
mnist = load_digits()
x = pd.DataFrame(mnist.data)
y = pd.DataFrame(mnist.target)

# Q2.
fig, axes = plt.subplots(2, 2, figsize=(8,6))
num = 0
for i in range(2):
    for j in range(2):
        axes[i, j].imshow(mnist.images[num], cmap='gray');
        axes[i, j].axis('off')
        axes[i, j].set_title(f"target: {mnist.target[num]}")
        num = num+1
plt.tight_layout()
plt.show()

# Q3.
# x.shape
# y.shape
# df = pd.concat((x,y),axis=1)
# df.shape

X_train, X_test, y_train, y_test = train_test_split(x,y,test_size=0.2,random_state=6202)
scaler = StandardScaler()
scaler.fit(X_train)

X_train = scaler.transform(X_train)
X_test = scaler.transform(X_test)

mlp = MLPClassifier(hidden_layer_sizes=(10, 10, 10), max_iter=1000,shuffle=True)
mlp.fit(X_train, y_train.values.ravel())

# Q4.
predictions = mlp.predict(X_test)

print(confusion_matrix(y_test,predictions))
print(classification_report(y_test,predictions))

# Q5.
X, y = load_digits(return_X_y=True)
train_sizes, train_scores, test_scores, fit_times, _ = learning_curve(mlp, X, y, cv=30,return_times=True)
plt.plot(train_sizes,np.mean(train_scores,axis=1))
plt.show()


from fastapi import FastAPI
from datetime import datetime
import random
from models import Transaction

app = FastAPI()

#This is the dictionary that holds user information
userDetails = {'kojo':
                {'pin':'1234',
                'balance' : {'GHS' : 35190,
                             'USD' : 5865}},
                'kofi':
                {'pin':'1655',
                'balance' : {'GHS' : 39000,
                             'USD' : 6500}},
                'kwabena':
                {'pin':'0480',
                'balance' : {'GHS' : 20814,
                             'USD' : 3469}},
                'matrevi':
                {'pin':'1306',
                'balance' : {'GHS' : 26856,
                             'USD' : 4476}},
               'justin':
               {'pin':'8743',
                'balance' : {'GHS' : 35196,
                             'USD' : 5866}},
               'fernanda':
               {'pin':'5047',
                'balance' : {'GHS' : 33330,
                             'USD' : 5555}},
                'gifty':
                {'pin' : '6060',
                'balance' : {'GHS' : 34212,
                             'USD' : 5702}},
                'exorm':
                {'pin' : '4658',
                'balance': {'GHS' : 323154,
                            'USD' : 3859}},
                'sampson':
                {'pin' : '9390',
                'balance': {'GHS' : 26322,
                            'USD' : 4387}},
                'georgina':
                {'pin' : '7793',
                'balance': {'GHS' : 31974,
                            'USD' : 5329}},
               }

#This variable is set to false by default but changed to true when a user is validated for a session
is_logged_in = False

username = None

currency = None

transactions = []

#This function is called to verify user credentials and returns a boolean value
 #depending on the validity of credentials entered
def user_is_valid(username, pin):
    if username in userDetails:
        if pin == userDetails[username]['pin']:
            return True

    return False

#This function collects user credentials and validates the session or recalls itself
def login():

    #The global keyword enables global variables to be modified at runtime
    global username
    global is_logged_in

    username = input("Please type your username\n")
    pin = input("Please enter your pin\n")

    if(user_is_valid(username, pin)):
        is_logged_in = True

    else:
        print("Credentials not valid.\n" +
              'Please try again')
        login()

#This function is for depositing money into your account
def deposit(username):

    if (amount <= 0):
        print(f"You cannot deposit {amount} {get_currency()}")
    else:
        balance = userDetails[username]['balance'][get_currency()]
        newBalance = balance + amount
        userDetails[username]['balance'][get_currency()] = newBalance
        print(f"An amount of {amount} {get_currency()} has been deposited into your account" +
        f"\nYour new balance is {get_balance(username, get_currency())} {get_currency()}")

        transaction = Transaction(username, "Deposit", amount, get_currency(), username)
        transactions.append(transaction)

        answer = input("\nWould you like to make another transaction? \n1.Yes\n2.No\n")

        if(answer == '1'):
            welcome_user(username)

@app.put('/{username}/account/deposit/{currency}/{amount}/')
def deposit_money(username, currency, amount):

    if(int(amount) > 0):
        balance = userDetails[username]['balance'][currency]
        userDetails[username]['balance'][currency] = int(balance) + int(amount)
        return userDetails[username]['balance'][currency]

 #This function allows users to withdraw money from their accounts depending on their account balance
@app.put('/{username}/account/{currency}/{amount}/')
def withdraw_money(username, currency, amount):

    balance = int(get_balance(username,currency))

    if(balance - int(amount) >= 0):
        userDetails[username]['balance'][currency] = int(balance) - int(amount)

        return userDetails[username]['balance'][currency]

#This function enables the transfer of money between user accounts
#It recalls itself when the recipient is not found
def transfer_money(username):

        amount = float(input("\nHow much would you like to transfer?\n"))
        balance = userDetails[username]['balance'][get_currency()]
        if (balance - amount) < 0:
            print("You do not have enough funds to complete this transaction\n")
        else:
            balance = balance - amount
            userDetails[username]['balance'][get_currency()] = balance
            userDetails[user]['balance'][get_currency()] += amount
            print(f"You have successfully transferred {amount} {get_currency()} to {user}")
            print(f"Your new balance is {get_balance(username, get_currency())}")

            transaction = Transaction(username, "Transfer", amount, get_currency(), user)
            transactions.append(transaction)

            answer = input("\nWould you like to make another transaction? \n1.Yes\n2.No\n")

            if(answer == '1'):
                welcome_user(username)

@app.get('/{username}/account/{currency}/')
def get_balance(username, currency):
    return userDetails[username]['balance'][currency]

#This function takes a list of transactions and prints details of each transaction
def generate_receipt(transactions):
     pass


"""Implements the Binary Decision Diagram data type.

Defines the "original" BDD-related machinery (as opposed to the
simplification defined in :py:class:`varseq.VarSeq`), including
actual simultaneous variable reordering (inspired by Rudell'93) and
other BDD alignment-related functions, along with technical utilities
(such as load/save to file, etc.). A simple auxiliary class
:py:class:`node` keeps the necessary data related to BDD nodes.

Tests coverage: :py:mod:`BDD_test`

"""

import numpy as np
import pandas as pd
from numpy.random import random as rnd
from random import choice as rnd_choose
import sys
import pytest

import itertools as iters
from graphviz import Digraph
from collections import deque

import copy
import math

# Module-level constants
GSIFTS_MAX_INCREASE_MUL = 100000  # max increase of the BDD size (e.g., 100 times) during greedy sifts
WEIGHT_TOL = 3  # number of decimal points for the arc weights tolerance (swap_up)
C_MAX = 50  # max edge cost (for random generation, non-inclusive)

# special node IDs (see the node class docstring)
NROOT = 0
NTRUE = -1
NFALSE = -2
######################################################################

def simscore(seq_A, seq_B, p_B = None):
    """Calculates a 'similarity score' between `A` and `B`.

    The function has nothing to do with other data structures,
    technically: it just calculates the normalized number of
    inversions between the two ordered lists of variables
    (which we call 'similarity score').

    Args:
        seq_A (list): sequence A
        seq_B (list): sequence B to compare
        p_B (dict): dict of element positions in B (default: None, will be built)

    Returns:
        number (between 0 and 1): share of possible inversions.

    Examples:
        >>> simscore([1,2,3], [1,2,3])
        1.0

        >>> simscore([1,2,3], [3,2,1])
        0.0

        >>> simscore([1,2,3], [1,3,2])
        0.6666666666666667

        >>> simscore([1,2,3,4,5,6,7], [5,6,3,4,2,7,1])
        0.33333333333333337
    """
    N = len(seq_A)
    no_inversions = 0
    if p_B is None:
        p_B = {seq_B[i]: i for i in range(N)}

    assert set(seq_A) == set(seq_B)
    for i in range(N-1):
        for j in range(i, N):
            if p_B[seq_A[i]] > p_B[seq_A[j]]:
                no_inversions += 1

    return 1 - no_inversions / (N*(N-1)/2)

class node(object):
    """Represents a BDD node, with the following attributes.

    Attributes:
        id: an integer node ID
        hi: a pointer to the ``hi``-node
        lo: a pointer to the ``lo``-node
        layer: layer number (id)

    Note:
        There are special node IDs:
        ``0``   for the root,
        ``-1``  for True sink,
        ``-2``  for the False sink.
    """

    def __init__(self, id, hi=None, lo=None, layer=-1):
        """A simple class constructor."""
        self.id = id
        self.hi = hi
        self.lo = lo
        self.layer = layer

    def link(self, to_whom, arc="hi"):
        """Helper function: links the node to another one."""
        if arc == "hi":
            self.hi = to_whom
        elif arc == "lo":
            self.lo = to_whom
        else:
            print("ERROR: wrong arc specifier while creating an edge -- {} (expected 'hi' or 'lo')".format(arc))

######################################################################
class BDD(object):
    """Encodes a BDD and implements layer-ordering (revision) methods.

    Attributes:
        N (int): number of variables (equals number of layers - 1)
        layers (list): a list of layers (sets/hashes), including {T, F}-nodes
        nodes (dict): a set/hash of nodes, key by node ID
        vars (list): variables associated with layers
        T,F (node): pointers to `True` and `False` sink nodes
        weighted (bool): a flag whether arcs have weights
        weights (dict): arc weights, keys: `(id_from, id_to, arc_type)`
                        (with the latter either 'hi', or 'lo')
    """

    def __init__(self, N=2, vars=None, weighted=False):
        """Defines an empty BDD with N variables.

        Args:
            N (int): number of variables ( = number of layers except the terminal),
            vars (list): variable names (strings or ints),
            weighted (bool): a flag for weighted BDD (assigning arc weights).
        """
        if not vars:
            vars = [i for i in range(1, N+1)]

        self.vars = vars
        self.weighted = weighted
        if weighted:
            self.weights = dict()
        self.layers = [set() for i in range(N)]
        self.T = node(NTRUE)
        self.F = node(NFALSE)
        self.layers.append(set([self.T, self.F]))
        self.max_id = -1
        self.av_node_ids = deque()
        self.nodes = dict()
        self.var_pos = dict(zip(vars, [i for i in range(len(self.layers))]))
        self.nodes.update({NTRUE: self.T, NFALSE: self.F})

    def width(self):
        """Returns BDD width (max no. nodes in a layer)."""
        return max([len(L) for L in self.layers])
    # some helper functions

    def link(self, parent, child, etype="hi", edge_weight=0.0):
        """Creates a link between two nodes in the BDD.

        (Saving auxiliary info as appropriate)

        Args:
           parent (int): parent node id
           child (int): child node id
           etype (str): edge type, ``hi`` or ``lo`` (default ``hi``)
           edge_weight (float): edge weight (default 0.0)
        """
        assert parent in self.nodes.keys()
        assert child in self.nodes.keys()
        self.nodes[parent].link(self.nodes[child], etype)
        if self.weighted:
            self.weights[(parent, child, etype)] = edge_weight

    def llink(self, parent, child, etype="hi", edge_weight=0.0):
        """Creates a link between two nodes.

        A version of :py:func:`BDD.BDD.link` that takes
        :py:class:`BDD.node` objects as arguments instead of node
        IDs.

        """
        parent.link(child, etype)
        if self.weighted:
            self.weights[(parent.id, child.id, etype)] = edge_weight

    def __len__(self):
        """Returns no of variables (layers, except the terminal)."""
        return len(self.vars)

    def n(self, i):
        """Returns size of the i-th layer."""
        return len(self.layers[i])

    def p(self, a):
        """Returns position (0-based layer index) of variable ``a``."""
        return self.var_pos[a]

    def size(self):
        """Returns the size of the BDD (total no. of nodes)."""
        return len(self.nodes)-2

    def new_node_name(self):
        """Returns a unique name for the next node to be created.

        Picks the one to re-cycle after some node deletions, when available;
        when it is not -- just takes the ``max_id+1`` (adjusting the ``max_id``)
        """
        if len(self.av_node_ids) > 0:
            return self.av_node_ids.popleft()
        else:
            self.max_id += 1
            return self.max_id

    def dump_gv(self, layerCapt=True, x_prefix="x", node_labels=None):
        """Exports the BDD to the Graphviz format (``.dot``).

        Args:
            layerCapt (bool): whether to generate layer captions.
            x_prefix (str): a prefix to be shown for layer name (default 'x')

        Returns:
            a Digraph object (see ``graphviz`` package).
        """
        g = Digraph()

        for i, layer in enumerate(self.layers):
            with g.subgraph(name="cluster_{}".format(i)) as s:
                for n in layer:
                    if n.id == NTRUE:
                        s.node(str(NTRUE), label="T",fillcolor = "orange",style="filled")
                    elif n.id == NFALSE:
                        s.node(str(NFALSE),label="F", fillcolor = "gray",style="filled")
                    else:
                        if node_labels is None:
                            s.node( str(n.id) )
                        else:
                            s.node(str(n.id), label=f"{n.id}: {node_labels[n.id]}")

                if i!=len(self.layers)-1:
                    if layerCapt:
                        s.attr(label=f"{x_prefix}{self.vars[i]}, sz={len(layer)}", color="lightgrey")
                    else:
                        s.attr(color="lightgrey")
                else:
                    s.attr(color="white")

        for layer in self.layers:
            for n in layer:
                if not n.hi is None:
                    if self.weighted:
                        elabel = f"{self.weights[(n.id, n.hi.id, 'hi')]:.2f}"
                    else:
                        elabel = ""
                    g.edge(str(n.id), str(n.hi.id), label=elabel)

                if not n.lo is None:
                    if self.weighted:
                        elabel = f"{self.weights[(n.id, n.lo.id, 'lo')]:.2f}"
                    else:
                        elabel = ""
                    g.edge(str(n.id), str(n.lo.id), style="dashed", label=elabel)

        return g

    def addnode(self, parent_node, arc="hi", node_id=None, edge_weight=0.0):
        """Adds a node and updates aux info as necessary.

        Args:
            parent_node (node): parent node pointer,
            arc (str): arc type, `hi` or `lo`,
            node_id: node name. If `None` is provided, the value is derived
                from :py:func:`BDD.BDD.new_node_name`,
            edge_weight(float): edge weight

        Returns:
            The created node as a :py:class:`BDD.node` object.
        """
        if node_id is None:
            node_id = self.new_node_name()

        newnode = node(node_id)

        if parent_node is None:
            newnode.layer = 0
            self.layers[0].add(newnode)

        elif arc == "hi":
            parent_node.hi = newnode
            self.layers[parent_node.layer+1].add(newnode)
            newnode.layer = parent_node.layer+1
            if self.weighted:
                self.weights[(parent_node.id,
                              parent_node.hi.id, 'hi')] = edge_weight

        elif arc == "lo":
            parent_node.lo = newnode
            self.layers[parent_node.layer+1].add(newnode)
            newnode.layer = parent_node.layer+1
            if self.weighted:
                self.weights[(parent_node.id,
                              parent_node.lo.id, 'lo')] = edge_weight

        else:
            print("ERROR: Wrong arc type: {}\n(allowed values are 'hi' and 'lo')".format(arc))

        self.nodes.update({newnode.id: newnode})

        return newnode

    def swap_up(self, layer_idx):
        """Swaps the layer with the one immediately above it (in-place).

        (by *index* ! in-place operation)

        Args:
            layer_idx (int): number (0-based index) of the layer to be ''bubbled up''

        Note:
            Operation takes O (`no-of-nodes in the upper layer`)
            time. Drops the layer being swapped. Then iterates
            through the nodes of the layer immediately above it and
            creates nodes as necessary (re-cycling new nodes to avoid
            redundancy: a quasi-reduced BDD keeps this property)

        """
        assert layer_idx >= 1  # otherwise there's no layer to swap to
        assert layer_idx <= len(self.layers)-2  # can't swap-up the last layer

        new_nodes = dict()

        for n in self.layers[layer_idx]:
            del self.nodes[n.id]
            self.av_node_ids.append(n.id)

        self.layers[layer_idx] = set()

        prev_layer = self.layers[layer_idx-1]

        if not self.weighted:
            for F in prev_layer:
                # iterating through the nodes of the upper layer

                # save the pointers
                F_hi_hi = F.hi.hi
                F_hi_lo = F.hi.lo
                F_lo_hi = F.lo.hi
                F_lo_lo = F.lo.lo

                # creating the "hi"-node
                if not (F_hi_hi.id, F_lo_hi.id) in new_nodes.keys():
                    F_hi = self.addnode(F, "hi")
                    F_hi.link(F_hi_hi, "hi")
                    F_hi.link(F_lo_hi, "lo")
                    new_nodes.update({(F_hi.hi.id, F_hi.lo.id): F_hi})
                else:
                    F_hi = new_nodes[(F_hi_hi.id,
                                    F_lo_hi.id)]  # re-cycle the old node
                    F.link(F_hi, "hi")

                # creating the "lo"-node
                if not (F_hi_lo.id, F_lo_lo.id) in new_nodes.keys():
                    F_lo = self.addnode(F, "lo")
                    F_lo.link(F_hi_lo, "hi")
                    F_lo.link(F_lo_lo, "lo")
                    new_nodes.update({(F_lo.hi.id, F_lo.lo.id): F_lo})
                else:
                    F_lo = new_nodes[(F_hi_lo.id,
                                    F_lo_lo.id)]  # re-cycle the old node
                    F.link(F_lo, "lo")

            self.layers[layer_idx] = set(new_nodes.values())

        else:
            # weighted version (with arc costs)
            old_weights = copy.copy(self.weights)
            for F in prev_layer:
                # save the pointers
                F_hi_hi = F.hi.hi
                F_hi_lo = F.hi.lo
                F_lo_hi = F.lo.hi
                F_lo_lo = F.lo.lo

                # calculating the necessary costs
                cF_hi = old_weights[(F.id, F.hi.id, "hi")]
                cF_lo = old_weights[(F.id, F.lo.id, "lo")]
                cF_hi_hi = old_weights[(F.hi.id, F.hi.hi.id, "hi")]
                cF_lo_hi = old_weights[(F.lo.id, F.lo.hi.id, "hi")]
                cF_hi_lo = old_weights[(F.hi.id, F.hi.lo.id, "lo")]
                cF_lo_lo = old_weights[(F.lo.id, F.lo.lo.id, "lo")]

                cH_min = np.round(min(cF_hi + cF_hi_hi, cF_lo + cF_lo_hi),
                                  decimals=WEIGHT_TOL)
                cH_hi = np.round((cF_hi + cF_hi_hi) - cH_min,
                                 decimals=WEIGHT_TOL)
                cH_lo = np.round((cF_lo + cF_lo_hi) - cH_min,
                                 decimals=WEIGHT_TOL)

                cL_min = np.round(min(cF_hi + cF_hi_lo, cF_lo + cF_lo_lo),
                                  decimals=WEIGHT_TOL)
                cL_hi = np.round((cF_hi + cF_hi_lo) - cL_min,
                                 decimals=WEIGHT_TOL)
                cL_lo = np.round((cF_lo + cF_lo_lo) - cL_min,
                                 decimals=WEIGHT_TOL)

                # ----------------------------------------------------
                # creating the "hi"-node

                if not (F_hi_hi.id, F_lo_hi.id,
                        cH_hi, cH_lo) in new_nodes.keys():
                    F_hi = self.addnode(F, "hi", edge_weight=cH_min)
                    self.llink(F_hi, F_hi_hi, "hi", edge_weight=cH_hi)
                    self.llink(F_hi, F_lo_hi, "lo", edge_weight=cH_lo)
                    new_nodes.update({(F_hi.hi.id, F_hi.lo.id,
                                       cH_hi, cH_lo): F_hi})
                else:
                    F_hi = new_nodes[(F_hi_hi.id, F_lo_hi.id,
                                      cH_hi, cH_lo)]  # re-cycle the old node
                    self.llink(F, F_hi, "hi", edge_weight=cH_min)

                # ----------------------------------------------------
                # creating the "lo"-node

                if (F_hi_lo.id, F_lo_lo.id,
                        cL_hi, cL_lo) not in new_nodes.keys():
                    F_lo = self.addnode(F, "lo", edge_weight=cL_min)
                    self.llink(F_lo, F_hi_lo, "hi", edge_weight=cL_hi)
                    self.llink(F_lo, F_lo_lo, "lo", edge_weight=cL_lo)

                    new_nodes.update({(F_hi_lo.id, F_lo_lo.id,
                                       cL_hi, cL_lo): F_lo})
                else:
                    # re-cycle the old node
                    F_lo = new_nodes[(F_hi_lo.id, F_lo_lo.id, cL_hi, cL_lo)]
                    self.llink(F, F_lo, "lo", edge_weight=cL_min)

            self.layers[layer_idx] = set(new_nodes.values())
        # NOTE: old nodes in the source layer must be (physically) deleted
        # by the Python garbage collection

        # rename layers and update aux structures as necessary
        self.var_pos.update({
            self.vars[layer_idx]: layer_idx - 1,
            self.vars[layer_idx - 1]: layer_idx
        })

        v_1 = self.vars[layer_idx-1]
        self.vars[layer_idx-1] = self.vars[layer_idx]
        self.vars[layer_idx] = v_1

    def sift(self, var, pos):
        """Sifts variable ``var`` (by name) to (0-based) position ``pos``.

        Uses :py:func:`BDD.sift` under the hood, in-place operation."""
        assert pos >= 0 and pos < len(self.layers)-1
        assert var in self.vars

        if pos < self.p(var):
            # this is a sift-up
            while self.p(var) > pos:
                self.swap_up(self.p(var))

        elif pos == self.p(var):
            # a trivial case -- nothing to do
            return

        else:
            # this is a sift-down
            while pos > self.p(var):
                self.swap_up(self.p(var)+1)

    def gsifts(self, with_whom, start_order=None):
        """Performs greedy sifts to align with ``with_whom``.

        Runs a simplistic implementation of Rudell'93
        sifting alorithm extension to minimize \|A\|+\|B\|

        starts with aligning to ``self`` or ``start_order`` (if given)
        """
        N = len(self.layers)-1
        if start_order is None:
            start_order = self.vars
        else:
            for i in range(N):
                self.sift(start_order[i], i)

        for i in range(N):
            with_whom.sift(start_order[i], i)

        # now the pair is aligned, but possibly huge
        # let us see if we can compress it:

        processed_vars = set()

        while len(processed_vars) < N:
            i = 0
            while self.vars[i] in processed_vars:
                i += 1  # skip processed variables

            best_pos = i
            active_var = self.vars[i]
            best_size = self.size() + with_whom.size()

            cur_size = best_size

            # try moving the var down
            for j in range(i+1, N):
                #if self.size()+with_whom.size() > cur_size*GSIFTS_MAX_INCREASE_MUL:
                #    break

                self.swap_up(j)
                with_whom.swap_up(j)
                cur_size = self.size()+with_whom.size()

                if cur_size < best_size:
                    best_size = cur_size
                    best_pos = j

            # try moving the var up
            self.sift(active_var, i)
            with_whom.sift(active_var, i)
            cur_size = self.size()+with_whom.size()

            for j in reversed(range(1,i+1)):
                #if self.size()+with_whom.size() > cur_size*GSIFTS_MAX_INCREASE_MUL:
                #    break

                self.swap_up(j)
                with_whom.swap_up(j)
                cur_size = self.size()+with_whom.size()

                if cur_size < best_size:
                    best_size = cur_size
                    best_pos = j-1

            # now choose the best position for the variable
            self.sift(active_var, best_pos)
            with_whom.sift(active_var, best_pos)

            processed_vars.add(active_var)

    # file manipulation procedures
    def save(self, filename):
        """Saves a BDD to a text file.

        Args:
            filename (string): destination file name.

        Note:
            File format is as follows.

                - File header, the first two lines
                    | N= `<no-of-layers>`
                    | vars= `<comma-separated layer labels>`

                - Then, one node description = one line of the format:
                    `id` : `hi` , `lo`

            where ``id`` is node's ID, ``hi`` and ``lo`` are IDs of
            the nodes corresponding to one- and zero- pointers of the
            node ``ID``. The procedure performs breadth-first search
            and just saves all the nodes.

            Three nodes have reserved ID values:

                - **Root** node: 0
                - **True** terminal: -1
                - **False** terminal: -2

        """
        S = deque()  # a deque of node IDs to explore (FIFO)
        V = set()  # set of visited (saved) nodes (by ID)

        for n in self.layers[0]:
            S.append(n.id)

        with open(filename, "w") as f:
            f.write("N={}\n".format(len(self.layers)))
            f.write("vars={}\n".format(', '.join([str(v) for v in self.vars])))

            while len(S) > 0:
                n_id = S.popleft()
                # this is a new node
                if not (self.nodes[n_id].hi is None
                        or self.nodes[n_id].hi is None):
                    n_hi = self.nodes[n_id].hi.id
                    n_lo = self.nodes[n_id].lo.id
                    f.write("{}:{},{}\n".format(n_id, n_hi, n_lo))
                    if not (n_hi in V or n_hi in S or n_hi is None):
                        S.append(n_hi)

                    if not (n_lo in V or n_lo in S or n_lo is None):
                        S.append(n_lo)

                else:
                    f.write("{}:None,None\n".format(n_id))

                V.add(n_id)

    def load(self, filename, weighted=False):
        """Loads a BDD from a (text) file.

        Note:
            The format is described in :py:func:`save`.
        """
        with open(filename, "r") as f:
            line = f.readline()
            while line[0] == "#" or line == "":
                line = f.readline()

            assert line[:2] == 'N='
            N = int(line[2:])
            assert N > 1

            line = f.readline()
            assert line[:5] == 'vars='
            line = line[5:].split(',')
            assert len(line) == N-1

            # initialize the attributes
            self.layers = [set() for i in range(N)]
            self.nodes = dict()
            self.vars = [v.strip() for v in line]
            self.var_pos = dict(zip(self.vars, [i for i in range(N)]))

            self.weighted = weighted
            if weighted:
                self.weights = dict()

            line = f.readline()

            current_layer = 0
            next_layer = set()
            while line:
                if weighted:
                    id, line, weights = line.split(':')
                    weights = weights.split(",")
                    w_hi = float(weights[0])
                    w_lo = float(weights[1])
                else:
                    id, line = line.split(':')

                hi_id, lo_id = line.split(',')
                id = int(id)
                if id != NTRUE and id != NFALSE:
                    hi_id = int(hi_id)
                    lo_id = int(lo_id)
                    if weighted:
                        self.weights[(id, hi_id, "hi")] = w_hi
                        self.weights[(id, lo_id, "lo")] = w_lo
                else:
                    line = f.readline()
                    continue

                if id in next_layer:
                    current_layer += 1
                    next_layer = set()
                elif id not in self.nodes.keys():
                    self.addnode(None, node_id=id)
                    if id != 0:
                        print(f"WARNING: during load, just added an orphan node with id: {id}")

                if hi_id in next_layer:
                    F_hi = self.nodes[hi_id]
                    self.nodes[id].link(F_hi, "hi")
                else:
                    if self.weighted:
                        e_w = w_hi
                    else:
                        e_w = 0.0
                    F_hi = self.addnode(self.nodes[id],
                                        "hi",
                                        node_id=hi_id,
                                        edge_weight=e_w)
                    next_layer.add(hi_id)

                if lo_id in next_layer:
                    F_lo = self.nodes[lo_id]
                    self.nodes[id].link(F_lo, "lo")
                else:
                    if self.weighted:
                        e_w = w_lo
                    else:
                        e_w = 0.0
                    F_lo = self.addnode(self.nodes[id],
                                        "lo",
                                        node_id=lo_id,
                                        edge_weight=e_w)
                    next_layer.add(lo_id)

                if id not in self.nodes.keys():
                    if current_layer > 0:
                        print("WARNING: a node with no source at layer {}".
                              format(current_layer))

                    F = node(id, F_hi, F_lo)
                    self.layers[current_layer].add(F)
                    self.nodes.update({id: F})

                if id == NTRUE:
                    self.T = self.nodes[id]

                if id == NFALSE:
                    self.F = self.nodes[id]

                line = f.readline()

                self.max_id = max([n.id for n in self.nodes.values()])
                self.av_node_ids = deque([
                    i for i in range(1, self.max_id + 1)
                    if i not in self.nodes.keys()
                ])

    @classmethod
    def random(cls, N=5, p=0.5, weighted=False):
        """Generates a random BDD with `N` variables (N+1 layers).

        Layers are numbered with consecutive integers by default,
        starting with ``1``.

        Args:
            N (int):   number of variables (results in N+1 layers, incl. T,F)
            p (float):   tree size parameter
                    0 will generate a non-random exponential-sized diagram,
                    1 will result in a single node per layer.
            weighted (bool): whether to generate a weighted diagram.
        Returns:
            A generated :py:class:`BDD`.

        """
        bdd = BDD(N, weighted=weighted)

        assert N > 1
        bdd.addnode(parent_node=None)  # create a root node

        for layer in range(1, N):
            # generate nodes (and edges) for the layer

            for n in bdd.layers[layer-1]:
                ch = np.random.randint(0, 1+C_MAX)
                cl = np.random.randint(0, 1+C_MAX)
                if rnd() <= p or len(bdd.layers[layer]) == 0:
                    newnode = bdd.addnode(n, arc="hi", edge_weight=ch)
                else:
                    bdd.link(n.id, rnd_choose(tuple(bdd.layers[layer])).id,
                             "hi", edge_weight=ch)

                if rnd() <= p:
                    newnode = bdd.addnode(n, arc="lo", edge_weight=cl)
                else:
                    bdd.link(n.id, rnd_choose(tuple(bdd.layers[layer])).id,
                           "lo", edge_weight=cl)

        # separately process the last layer
        for n in bdd.layers[-2]:
            bdd.link(n.id, rnd_choose(tuple(bdd.layers[-1])).id,
                     "hi", edge_weight=np.random.randint(0, 1+C_MAX))
            bdd.link(n.id, rnd_choose(tuple(bdd.layers[-1])).id,
                     "lo", edge_weight=np.random.randint(0, 1+C_MAX))

        return bdd

    def profile(self):
        """Returns a BDD "profile" -- an (almost) BFS-ordered list of nodes.

        Note:
            This is used to determine, e.g., if the generated
            instance is unique. A 'profile' is a string of the format
            ``<vars>``:``<BFS-nodes>``, where:

            - ``<vars>``      :  comma-separated variable names by layer;
            - ``<BFS-nodes>`` :  comma-separated list of node-numbers (not IDs!)
              in a BFS-traverse order

        """
        Q = deque()
        V = set()
        node_nums = dict()
        cur_node = 0

        p = []

        for n in self.layers[0]:
            node_nums.update({n.id: cur_node})
            Q.append(n)
            p.append(str(cur_node))
            cur_node += 1

        p = [",".join(p) + ";"]

        while(len(Q) > 0):
            n = Q.popleft()

            for successor in [n.hi, n.lo]:
                if successor is None:
                    continue

                if successor.id not in node_nums.keys():
                    node_nums.update({successor.id: cur_node})
                    cur_node += 1

                p.append(str(node_nums[successor.id]))
                if successor.id not in V:
                    Q.append(successor)
                    V.add(successor.id)

        return p[0] + ",".join(p[1:])

    def show(self,
             dir="tmp",
             filename="showfunc.dot",
             layerCapt=True,
             x_prefix="x",
             node_labels=None):
        """Shows the diagram on the screen (saving the auxiliary pdf).

        Generates a ``.dot`` file and compiles it to ``.pdf``.
        Requires ``graphviz`` (``dot`` program)

        Args:
            dir (str): directory to place files (default: ``tmp``)
            filename (str): `.dot` filename (default: ``showfunc.dot``)
            layerCapt (bool): whether to show layer caption (default: ``True``)
            x_prefix(str): a prefix for variable captions (default: ``x``)
            node_labels(dict): node labels to display (optional).

        """
        self.dump_gv(layerCapt, x_prefix, node_labels).view(filename,
                                                            directory=dir,
                                                            cleanup=True)

    def align_to(self, vars_order, inplace=False):
        """Revises the BDD to a given order.

        (with a series of consecutive sifts)
        """
        if inplace:
            aligned = self
        else:
            aligned = copy.deepcopy(self)

        for i in range(len(vars_order)):
            aligned.sift(vars_order[i], i)

        return aligned

    def OA_bruteforce(self, with_what, LR=False):
        """Finds the best (smallest) alignment with BDD ``with_what``.

        Uses brute-force enumeration of all possible orders.

        Args:
            with_what (BDD): target diagram.
            LR (bool): if set, layer-reduces each candidate BDD.

        Returns:
            A tuple of values

                - alternatives (int): number of alternative optima
                - A_aligned (list): of A-aligned (in each optimum)
                - B_aligned (list): of B-aligned (in respective optimum)
        """
        # generate all possible permutations
        perms = iters.permutations(self.vars)
        min_size = math.inf
        A_aligned = []
        B_aligned = []
        alternatives = 0

        for perm in perms:
            A_c = self.align_to(list(perm))
            B_c = with_what.align_to(list(perm))

            if LR:
                A_c.make_reduced()
                B_c.make_reduced()

            if min_size > A_c.size() + B_c.size():
                A_aligned = [A_c]
                B_aligned = [B_c]
                min_size = A_c.size()+B_c.size()
                alternatives = 1
            elif min_size == A_c.size() + B_c.size():
                A_aligned.append(A_c)
                B_aligned.append(B_c)
                alternatives += 1

        return [alternatives, A_aligned, B_aligned]

    def is_reduced(self):
        """Checks if the BDD is quasi-reduced (no "redundant" nodes)."""
        checked_nodes = set()

        # quasi-reduced BDD for weighted diagrams must also check the
        # uniqueness of weights, not just uniqueness of (hi, lo) tuples.
        # Not a big issue, just a separate procedure (need to take care
        # of the precision, though).
        assert (not self.weighted), "ERROR: not implemented for weighted BDDs."

        for layer in range(len(self.layers)-2, -1, -1):
            for n in self.layers[layer]:
                if (n.hi.id, n.lo.id) in checked_nodes:
                    return False
                else:
                    checked_nodes.add((n.hi.id, n.lo.id))

        return True

    def make_reduced(self):
        """Makes the BDD quasi-reduced.

        Swaps each layer back and forth, starting from the last one,
        (which makes sure the results is quasi-reduced due to
        :py:func:`BDD.BDD.swap` implementation.)

        """
        for i in range(len(self.vars)-1, 0, -1):
            self.swap_up(i)
            self.swap_up(i)

    def rename_vars(self, ren_dict):
        """Helper function: renames variables.

        Args:
            ren_dict (dict): a dict of <= :py:attr:`BDD.N` labels
                in the form {``before``: ``after``}
        """
        for v in ren_dict.keys():
            assert v in self.vars, f"{v} is not in {self.vars}"

        new_vars = [(lambda v: ren_dict[v] if v in ren_dict.keys() else v)(v)
                    for v in self.vars]
        self.vars = new_vars
        self.var_pos = dict(zip(self.vars, [i for i in range(len(self.vars))]))

    def shuffle_vars(self):
        """Randomly renames the variables (shuffles their order)."""
        self.rename_vars(
            dict(zip(self.vars, np.random.permutation(self.vars))))

    def is_aligned(self, to_what):
        """Checks if the BDD is aligned with ``to_what``."""
        return np.array_equal(self.vars, to_what.vars)

    # functions related to testing equivalence and finding truth tables
    def get_value(self, x):
        """Finds the terminal node (T or F) -- an endpoint for ``x``.

        Returns a terminal node ID corresponding to the variable choices in ``x``.

        Args:
            x (dict): of {var_name: value}, where value is in {0,1}.

        Returns:
            A tuple of values

                - terminal (bool or `-1`):
                    terminal node corresponding to the path
                    implied by x, encoded as Boolean (or -1 if error)

                - cost (float):
                    if the BDD is weighted, cost of the path.
        """
        (node,) = self.layers[0]
        cost = 0.0

        for i in range(len(x)):
            if x[self.vars[i]] == 0:
                if self.weighted:
                    cost += self.weights[(node.id, node.lo.id, "lo")]
                node = node.lo
            elif x[self.vars[i]] == 1:
                if self.weighted:
                    cost += self.weights[(node.id, node.hi.id, "hi")]
                node = node.hi
            else:
                print(
                    "Wrong value ({}) for variabe {}: 0 or 1 expected".format(
                        x[self.vars[i]], self.vars[i]))
                return -1

        if node.id == self.T.id:
            if self.weighted:
                return [True, cost]
            else:
                return True
        elif node.id == self.F.id:
            if self.weighted:
                return [False, cost]
            else:
                return False
        else:
            print("Error not a True or False node reached!")
            print("node is {}, T is {}, F is {}".format(
                node.id, self.T.id, self.F.id))
            return -1

    def truth_table(self):
        """Returns a "truth table" for the encoded Boolean function.

        Returns:

        ``pandas`` dataframe: column numbers correspond to
        variable names with two additional columns: ``Terminal`` for
        the function value (of Boolean type) and ``Cost`` if the
        diagram is :py:attr:`BDD.weighted`.

        """
        tt = []
        ind = []
        for x_num in range(2**len(self)):
            x = [int(j) for j in np.binary_repr(x_num, width=len(self.vars))]
            if not self.weighted:
                tt.append(x + [self.get_value(dict(zip(self.vars, x)))])
            else:
                tt.append(x + self.get_value(dict(zip(self.vars, x))))
            ind.append(np.binary_repr(x_num, width=len(self.vars)))

        if not self.weighted:
            return pd.DataFrame(tt, columns=[str(x) for x in self.vars] + ["Terminal"], index=ind)
        else:
            return pd.DataFrame(tt,
                                columns=[str(x) for x in self.vars] + ["Terminal", "Cost"],
                                index=ind)

    def is_equivalent(self, B):
        """Checks if two BDDs (A and B) are equivalent.

        Equivalent in the sense that they define the same Boolean
        function and the corresponding path costs coincide. Checks:

            - if the corresponding truth tables coincide.
            - (if the BDD is weighted) if all paths have the same costs.

        Returns:
            A tuple of

                - result (bool): whether the diagrams are equivalent,
                - message (str): information regarding the difference, if found.
        """
        assert self.weighted == B.weighted

        msg = None
        tt_self = self.truth_table()
        tt_B = B.truth_table()
        if self.weighted:
            tt_B = tt_B[[str(x) for x in self.vars] + ['Terminal', 'Cost']]
        else:
            tt_B = tt_B[[str(x) for x in self.vars] + ['Terminal']]

        if self.weighted:
            tt_B['new_index'] = [
                c for c in tt_B.apply(
                    lambda row: "".join([str(x) for x in row[:-2]]), axis=1)
            ]
        else:
            tt_B['new_index'] = [
                c for c in tt_B.apply(
                    lambda row: "".join([str(x) for x in row[:-1]]), axis=1)
            ]
        tt_B.set_index('new_index', inplace=True)

        for idx, row in tt_self.iterrows():
            if row['Terminal'] != tt_B.loc[idx]['Terminal']:
                msg = "\nINFO: Discrepancy found. For x={}, value(self)={}, value(B)={}".format(
                    idx, row['Terminal'], tt_B.loc[idx]['Terminal'])
                return [False, msg]
            if self.weighted:
                if abs(row['Cost'] - tt_B.loc[idx]['Cost']) >= WEIGHT_TOL:
                    msg = "\nINFO: Discrepancy found. For x={}, cost(self)={}, cost(B)={}".format(
                        idx, row['Cost'], tt_B.loc[idx]['Cost'])
                    return [False, msg]

        return [True, msg]

    def shortest_path(self):
        """Finds a shortest-path in O(`no-of-nodes`) time.

        Returns:
            dict: node labels (using node IDs as keys)
            corresponding to a shortest path from the current node to
            **True** terminal.

        Note:
            Expects the diagram to be :py:attr:`BDD.weighted`. (An
            unweighted version, in terms of 'number of hops', does
            not make any sense for a quasi-reduced BDD: the answer
            would be ``len(self)``.)

        """
        assert self.weighted

        node_labels = {NTRUE: 0}

        for L in reversed(range(len(self.vars))):
            for node in self.layers[L]:
                node_label = None
                if node.hi.id in node_labels.keys():
                    node_label = node_labels[node.hi.id] + self.weights[
                        (node.id, node.hi.id, "hi")]

                if node.lo.id in node_labels.keys():
                    if node_label is not None:
                        node_label = min(
                            node_label, node_labels[node.lo.id] +
                            self.weights[(node.id, node.lo.id, "lo")])
                    else:
                        node_label = node_labels[node.lo.id] + self.weights[
                            (node.id, node.lo.id, "lo")]

                if node_label is not None:
                    node_labels.update({node.id: node_label})

        for node in self.nodes:
            if node not in node_labels.keys():
                node_labels[node] = "∞"

        return node_labels

    def simscore(self, B):
        """Computes a simscore with another diagram, `B` (see :py:func:`simscore`)."""
        return simscore(self.vars, B.vars, B.var_pos)

def intersect(A, B):
    """Produces an intersection BDD of ``A`` and ``B``.

    Notes:
        BDDs need to be order-associated (same vars in the same order).

    Returns:
        :py:class:`BDD` : an intersection BDD.
    """
    assert A.vars == B.vars
    assert A.weighted == B.weighted

    weighted = A.weighted
    C = BDD(vars=A.vars, N=len(A.vars), weighted=weighted)
    root = C.addnode(parent_node=None)
    current_layer = {(NROOT, NROOT): root}

    # for all layers but the last one
    for i in range(len(A.vars)-1):
        new_layer = dict()
        for n in current_layer:
            niA, niB = n
            nA = A.nodes[niA]
            nB = B.nodes[niB]
            nC = current_layer[n]
            # yes-arc
            new_yes = (nA.hi.id, nB.hi.id)
            wA = A.weights[(nA.id, nA.hi.id, "hi")]
            wB = B.weights[(nB.id, nB.hi.id, "hi")]
            if new_yes in new_layer:
                C.link(nC.id, new_layer[new_yes].id,
                       "hi", edge_weight=wA+wB)
            else:
                new_layer[new_yes] = C.addnode(nC,
                                               "hi", edge_weight=wA+wB)
            # no-arc
            new_no = (nA.lo.id, nB.lo.id)
            wA = A.weights[(nA.id, nA.lo.id, "lo")]
            wB = B.weights[(nB.id, nB.lo.id, "lo")]
            if new_no in new_layer:
                C.link(nC.id, new_layer[new_no].id,
                       "lo", edge_weight=wA+wB)
            else:
                new_layer[new_no] = C.addnode(nC,
                                              "lo", edge_weight=wA+wB)

        current_layer = new_layer

    # a special case of the last layer
    for n in current_layer:
        niA, niB = n
        nA = A.nodes[niA]
        nB = B.nodes[niB]
        nC = current_layer[n]
        # yes-arc
        wA = A.weights[(nA.id, nA.hi.id, "hi")]
        wB = B.weights[(nB.id, nB.hi.id, "hi")]
        if nA.hi.id == NTRUE and nB.hi.id == NTRUE:
            C.link(nC.id, NTRUE, "hi", edge_weight=wA+wB)
        else:
            C.link(nC.id, NFALSE, "hi", edge_weight=wA+wB)
        # no-arc
        wA = A.weights[(nA.id, nA.lo.id, "lo")]
        wB = B.weights[(nB.id, nB.lo.id, "lo")]
        if nA.lo.id == NTRUE and nB.lo.id == NTRUE:
            C.link(nC.id, NTRUE, "lo", edge_weight=wA+wB)
        else:
            C.link(nC.id, NFALSE, "lo", edge_weight=wA+wB)

    return C