from django.core.paginator import Paginator
from django.shortcuts import render, redirect
from django.urls import reverse
from df_user import user_decorator
from df_user.models import UserInfo,Information
from .models import GoodsInfo, TypeInfo,GoodsContent,ContentChart
from df_cart.models import CartInfo
from df_order.models import OrderDetailInfo
from df_user.models import GoodsBrowser
from datetime import datetime
from django.contrib import messages

def index(request):
    # 查询各个分类的最新4条，最热4条数据
    username = request.session.get('user_name')
    user = UserInfo.objects.filter(uname=username).first()
    # 联系客服
    # 展示消息
    username1 = "customer"
    informations = Information.objects.filter()
    #判断用户是否给客服发过信息
    informations1=Information.objects.filter(cusername1=username,cusername=username1)
    import datetime
    nowTime = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')  # 现在
    # 消息回复
    user_name = UserInfo.objects.get(uname=username1)  # 获取当前消息用户信息
    # 展示回复消息
    # persons1=Information.objects.filter(cusername=cusername).values('cusername1').distinct().order_by('cusername1')
    if request.method == "POST":
        cusername = user.uname
        cusername1 = user_name.uname
        ccontent_chart = request.POST.get('title')
        cinformation_id = user_name.id
        if ccontent_chart == "":
            messages.success(request, "请输入内容！")
        else:
            Information.objects.create(cusername=cusername, cusername1=cusername1,
                                       ccontent_chart=ccontent_chart, cinformation_id=cinformation_id)
            messages.success(request, "消息发送成功")
            return redirect(reverse("df_goods:index"))
    typelist = TypeInfo.objects.all()
    #  _set 连表操作
    type0 = typelist[0].goodsinfo_set.order_by('-id')[0:4]  # 按照上传顺序
    type01 = typelist[0].goodsinfo_set.order_by('-gclick')[0:4]  # 按照点击量
    type1 = typelist[1].goodsinfo_set.order_by('-id')[0:4]
    type11 = typelist[1].goodsinfo_set.order_by('-gclick')[0:4]
    type2 = typelist[2].goodsinfo_set.order_by('-id')[0:4]
    type21 = typelist[2].goodsinfo_set.order_by('-gclick')[0:4]
    type3 = typelist[3].goodsinfo_set.order_by('-id')[0:4]
    type31 = typelist[3].goodsinfo_set.order_by('-gclick')[0:4]
    type4 = typelist[4].goodsinfo_set.order_by('-id')[0:4]
    type41 = typelist[4].goodsinfo_set.order_by('-gclick')[0:4]
    type5 = typelist[5].goodsinfo_set.order_by('-id')[0:4]
    type51 = typelist[5].goodsinfo_set.order_by('-gclick')[0:4]

    cart_num = 0
    # 判断是否存在登录状态
    # if request.session.has_key('user_id'):
    if 'user_id' in request.session:
        user_id = request.session['user_id']
        cart_num = CartInfo.objects.filter(user_id=int(user_id)).count()

    context = {
        'title': '首页',
        'cart_num': cart_num,
        'guest_cart': 1,
        'type0': type0, 'type01': type01,
        'type1': type1, 'type11': type11,
        'type2': type2, 'type21': type21,
        'type3': type3, 'type31': type31,
        'type4': type4, 'type41': type41,
        'type5': type5, 'type51': type51,
        'user':user,
        'informations': informations,
        'informations1':informations1,
        'username1': username1,
        'user_name': user_name,
        'nowTime':nowTime,
    }

    return render(request, 'df_goods/index.html', context)

def good_list(request, tid, pindex, sort):
    username = request.session.get('user_name')
    user = UserInfo.objects.filter(uname=username).first()
    # tid：商品种类信息  pindex：商品页码 sort：商品显示分类方式
    typeinfo = TypeInfo.objects.get(pk=int(tid))

    # 根据主键查找当前的商品分类
    news = typeinfo.goodsinfo_set.order_by('-id')[0:2]
    # list.html左侧最新商品推荐
    goods_list = []
    # list中间栏商品显示方式
    cart_num, guest_cart = 0, 0

    try:
        user_id = request.session['user_id']
    except:
        user_id = None
    if user_id:
        guest_cart = 1
        cart_num = CartInfo.objects.filter(user_id=int(user_id)).count()

    if sort == '1':  # 默认最新
        goods_list = GoodsInfo.objects.filter(gtype_id=int(tid)).order_by('-id')
    elif sort == '2':  # 按照价格
        goods_list = GoodsInfo.objects.filter(gtype_id=int(tid)).order_by('-gprice')
    elif sort == '3':  # 按照人气点击量
        goods_list = GoodsInfo.objects.filter(gtype_id=int(tid)).order_by('-gclick')

    # 创建Paginator一个分页对象
    paginator = Paginator(goods_list, 4)
    # 返回Page对象，包含商品信息
    page = paginator.page(int(pindex))
    context = {
        'title': '商品列表',
        'guest_cart': guest_cart,
        'cart_num': cart_num,
        'page': page,
        'paginator': paginator,
        'typeinfo': typeinfo,
        'sort': sort,  # 排序方式
        'news': news,
        'user': user,
    }
    return render(request, 'df_goods/list.html', context)

def detail(request, gid):
    if 'user_id' in request.session:
        uid = request.session['user_id']
        user = UserInfo.objects.get(id=uid)
        good_id = gid
        goods = GoodsInfo.objects.get(pk=int(good_id))
        goods.gclick = goods.gclick + 1  # 商品点击量
        goods.save()

        news = goods.gtype.goodsinfo_set.order_by('-id')[0:2]
        context = {
            'title': goods.gtype.ttitle,
            'guest_cart': 1,
            'cart_num': cart_count(request),
            'goods': goods,
            'news': news,
            'id': good_id,
            'user': user,
        }
        response = render(request, 'df_goods/detail.html', context)

        try:
            browsed_good = GoodsBrowser.objects.get(user_id=int(uid), good_id=int(good_id))
        except Exception:
            browsed_good = None
        if browsed_good:
            from datetime import datetime
            browsed_good.browser_time = datetime.now()
            browsed_good.save()
        else:
            GoodsBrowser.objects.create(user_id=int(uid), good_id=int(good_id))
            browsed_goods = GoodsBrowser.objects.filter(user_id=int(uid))
            browsed_good_count = browsed_goods.count()
            if browsed_good_count > 5:
                ordered_goods = browsed_goods.order_by("-browser_time")
                for _ in ordered_goods[5:]:
                    _.delete()
        return response

    else:
        good_id = gid
        goods = GoodsInfo.objects.get(pk=int(good_id))
        news = goods.gtype.goodsinfo_set.order_by('-id')[0:2]
        context = {
            'title': goods.gtype.ttitle,
            'guest_cart': 0,
            'cart_num': cart_count(request),
            'goods': goods,
            'news': news,
            'id': good_id,
        }
        return render(request, 'df_goods/detail.html', context)

#评论
def content(request, gid,pindex):
    if 'user_id' in request.session:
        uid = request.session['user_id']
        user = UserInfo.objects.get(id=uid)
        good_id = gid
        goods = GoodsInfo.objects.get(pk=int(good_id))
        #获取当前货物信息
        news = goods.gtype.goodsinfo_set.order_by('-id')[0:2]
        #条件查询
        goodsContents = GoodsContent.objects.filter(cgoodsname_id=good_id).order_by('-date_publish')

        print("当前商品评论数：")
        print(goodsContents.count())
        #订单信息
        goodsOrderDetailInfos=OrderDetailInfo.objects.filter()
        # for goodsOrderDetailInfo in goodsOrderDetailInfos:
        #     if goodsOrderDetailInfo.username == user.uname and goodsOrderDetailInfo.goods_id == goods.id:
        #         xinxi="买过"
        #     else:
        #         xinxi=""
        # 创建Paginator一个分页对象
        paginator = Paginator(goodsContents, 2)
        # 返回Page对象，包含商品信息
        page = paginator.page(int(pindex))
        for goodsContent in page:
            if goodsContent.cgoodsname_id == goods.id:
                content_id=goodsContent.id

        context = {
            'title': goods.gtype.ttitle,
            'guest_cart': 1,
            'cart_num': cart_count(request),
            'goods': goods,
            'id': good_id,
            'news': news,
            'user': user,
            'goodsContents': goodsContents,
            'paginator':paginator,
            'page':page,
            'goodsOrderDetailInfos':goodsOrderDetailInfos,
            # 'xinxi':xinxi,
        }
        if request.method == "POST":
            ctitle = goods.gtitle
            cpic = request.FILES.get('pic')
            cusername = user.uname
            clogo=user.ulogo
            cuser_content = request.POST.get('text')
            cgoodsname_id = goods.id
            if cpic == "":
                GoodsContent.objects.create(ctitle=ctitle, cusername=cusername, cuser_content=cuser_content,cgoodsname_id=cgoodsname_id, clogo=clogo)
                messages.success(request, "评论成功！")
            else:
                GoodsContent.objects.create(ctitle=ctitle, cpic=cpic, cusername=cusername, cuser_content=cuser_content,cgoodsname_id=cgoodsname_id,clogo=clogo)
                messages.success(request, "评论成功！")

        return render(request, 'df_goods/content.html', context)
    else:
        good_id = gid
        goods = GoodsInfo.objects.get(pk=int(good_id))
        # 获取当前货物信息
        news = goods.gtype.goodsinfo_set.order_by('-id')[0:2]
        # 条件查询
        goodsContents = GoodsContent.objects.filter(cgoodsname_id=good_id).order_by('-date_publish')

        print("当前商品评论数：")
        print(goodsContents.count())
        # 订单信息
        goodsOrderDetailInfos = OrderDetailInfo.objects.filter()
        # for goodsOrderDetailInfo in goodsOrderDetailInfos:
        #     if goodsOrderDetailInfo.username == user.uname and goodsOrderDetailInfo.goods_id == goods.id:
        #         xinxi="买过"
        #     else:
        #         xinxi=""
        # 创建Paginator一个分页对象
        paginator = Paginator(goodsContents, 2)
        # 返回Page对象，包含商品信息
        page = paginator.page(int(pindex))
        for goodsContent in page:
            if goodsContent.cgoodsname_id == goods.id:
                content_id = goodsContent.id

        context = {
            'title': goods.gtype.ttitle,
            'guest_cart': 0,
            'cart_num': cart_count(request),
            'goods': goods,
            'id': good_id,
            'news': news,
            'goodsContents': goodsContents,
            'paginator': paginator,
            'page': page,
            'goodsOrderDetailInfos': goodsOrderDetailInfos,
            # 'xinxi':xinxi,
        }
        return render(request, 'df_goods/content.html', context)

@user_decorator.login
def cart_count(request):
    if 'user_id' in request.session:
        return CartInfo.objects.filter(user_id=request.session['user_id']).count
    else:
        return 0

def ordinary_search(request):
    username = request.session.get('user_name')
    user = UserInfo.objects.filter(uname=username).first()
    from django.db.models import Q

    search_keywords = request.GET.get('q', '')
    pindex = request.GET.get('pindex', 1)
    search_status = 1
    cart_num, guest_cart = 0, 0

    try:
        user_id = request.session['user_id']
    except:
        user_id = None

    if user_id:
        guest_cart = 1
        cart_num = CartInfo.objects.filter(user_id=int(user_id)).count()

    goods_list = GoodsInfo.objects.filter(
        Q(gtitle__icontains=search_keywords) |
        Q(gcontent__icontains=search_keywords) |
        Q(gjianjie__icontains=search_keywords)).order_by("gclick")

    if goods_list.count() == 0:
        # 商品搜索结果为空，返回推荐商品
        search_status = 0
        goods_list = GoodsInfo.objects.all().order_by("gclick")[:4]

    paginator = Paginator(goods_list, 4)
    page = paginator.page(int(pindex))

    context = {
        'title': '搜索列表',
        'search_status': search_status,
        'guest_cart': guest_cart,
        'cart_num': cart_num,
        'page': page,
        'paginator': paginator,
        'user':user,
    }
    return render(request, 'df_goods/ordinary_search.html', context)


from typing import Any, Union

import affine
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Axes
from matplotlib.colors import Colormap, Normalize
from mpl_toolkits.axes_grid1 import make_axes_locatable
from rasterio.plot import plotting_extent

class Plotter:
    def __init__(
        self, data: np.ndarray, data_mask: np.ndarray, transform: affine.Affine
    ) -> None:
        self._data = data
        self._mask = data_mask
        self._transform = transform

    def __call__(
        self,
        ax: Union[Axes, None] = None,
        cmap: Union[str, Colormap] = "viridis",
        vmin: Union[float, None] = None,
        vmax: Union[float, None] = None,
        under_color: Union[str, None] = None,
        norm: Union[Normalize, None] = None,
        **kwargs: Any,
    ) -> Axes:
        if (vmin is not None or vmax is not None) and norm is not None:
            raise ValueError("Cannot pass both vmin/vmax and norm.")
        if norm is None:
            norm = Normalize(vmin=vmin, vmax=vmax)

        kwargs["extent"] = plotting_extent(self._data, self._transform)

        show_plt = False
        if not ax:
            show_plt = True
            ax = plt.gca()

        _make_image_plot(self._data, self._mask, norm, cmap, under_color, ax, **kwargs)

        if show_plt:
            plt.show()

        return ax

    def test_normalization(
        self,
        norm: Normalize,
        nbins: int = 50,
        cmap: Union[str, Colormap] = "viridis",
        under_color: Union[str, None] = None,
    ) -> None:
        """Test a normalization of the data for plotting."""
        vmin = norm.vmin
        vmax = norm.vmax
        mask = (vmin <= self._data) & (self._data <= vmax) & ~self._mask
        data = self._data[mask].flatten()

        result = norm(data)

        fig, axes = plt.subplots(figsize=(15, 15), ncols=2, nrows=2)

        _make_hist_plot(data, nbins, "Raw data", axes[0, 0])
        _make_image_plot(
            data,
            self._mask,
            Normalize(vmin=vmin, vmax=vmax),
            cmap,
            under_color,
            axes[1, 0],
        )

        _make_hist_plot(result, nbins, "Normalized data", axes[0, 1])
        _make_image_plot(result, self._mask, norm, cmap, under_color, axes[1, 1])

        plt.show()

def _make_hist_plot(
    data: np.ndarray,
    nbins: int,
    title: str,
    ax: Axes,
) -> Axes:
    """Plot a histogram of the data."""
    hist, bins = np.histogram(data, nbins)
    cdf = hist.cumsum()
    cdf = cdf / cdf.max()
    ax.bar(bins[:-1], hist, width=bins[1:] - bins[:-1], color="tab:blue")
    ax.set_ylabel("Frequency", color="tab:blue", fontsize=14)
    ax.set_title(title, fontsize=16)
    ax2 = ax.twinx()
    ax2.plot(bins[:-1], cdf, color="tab:orange")  # type: ignore[attr-defined]
    ax2.set_ylabel("Cumulative frequency", color="tab:orange", fontsize=14)
    return ax

def _make_image_plot(
    data: np.ndarray,
    mask: np.ndarray,
    norm: Normalize,
    cmap: Union[str, Colormap],
    under_color: Union[str, None],
    ax: Axes,
    **kwargs: Any,
) -> Axes:
    """Plot a histogram of the data."""
    masked: np.ma.MaskedArray = np.ma.masked_array(data, mask=mask)
    im = ax.imshow(
        masked,
        cmap=cmap,
        norm=norm,
        **kwargs,
    )
    if under_color is not None:
        im.cmap.set_under(under_color)  # type: ignore[union-attr]

    divider = make_axes_locatable(ax)
    cax = divider.append_axes("right", size="5%", pad=0.05)
    ax.figure.colorbar(ax.images[0], cax=cax, orientation="vertical")  # type: ignore[union-attr]

    return ax


import pytest
from seat_finder import SeatFinder

@pytest.mark.parametrize(
    "seat_code, expected_seat_row",
    [("FBFBBFFRLR", 44),
    ("BFFFBBFRRR", 70),
    ("FFFBBBFRRR", 14),
    ("BBFFBBFRLL", 102)]
)
def test_getting_row(seat_code, expected_seat_row):
    SEAT_CODE = seat_code
    expected_row = expected_seat_row
    seat_finder = SeatFinder(SEAT_CODE)
    actual_row = seat_finder.get_row_location_code()

    assert actual_row == expected_row

@pytest.mark.parametrize(
    "seat_code, expected_seat_column",
    [("FBFBBFFRLR", 5),
    ("BFFFBBFRRR", 7),
    ("FFFBBBFRRR", 7),
    ("BBFFBBFRLL", 4)]
)
def test_getting_column(seat_code, expected_seat_column):
    seat_finder = SeatFinder(seat_code)
    actual_column = seat_finder.get_column_location_code()

    assert actual_column == expected_seat_column

@pytest.mark.parametrize(
    "seat_row, seat_column, expected_seat_column",
    [(44, 5, 357),
    (70, 7, 567),
    (14, 7, 119),
    (102, 4, 820)]
)
def test_getting_seat_id(seat_row, seat_column, expected_seat_column):
    seat_finder = SeatFinder("dummy")
    actual_id = seat_finder.get_seat_id(seat_column, seat_row)
    assert actual_id == expected_seat_column

@pytest.mark.parametrize(
    "seat_code, expected_seat_id",
    [("FBFBBFFRLR", 357),
    ("BFFFBBFRRR", 567),
    ("FFFBBBFRRR", 119),
    ("BBFFBBFRLL", 820)]
)
def test_getting_seat(seat_code, expected_seat_id):
    seat_finder = SeatFinder(seat_code)
    actual_seat_id = seat_finder.get_seat()
    assert actual_seat_id == expected_seat_id


__author__ = "Dilawar Singh"
__email__ = "dilawar.s.rajput@gmail.com"

import numpy as np
import cv2 as cv
from collections import defaultdict
import logging

def _find_center(vec):
    return np.median(vec)

# Thanks https://codereview.stackexchange.com/a/185794
def normalize(img):
    """normalize image to 0, 255"""
    return np.interp(img, (img.min(), img.max()), (0, 255)).astype(np.uint8)

def fit_trajectory_using_median(traj, T, img):
    (sX, sY), (offX, offY) = T
    res = []
    r, _ = img.shape

    # x, y = zip(*sorted(traj.items()))
    # logging.info((xvec, ys))

    for k in sorted(traj):
        x = k

        vals = np.array(traj[k])

        # For each x, we may multiple pixels in column of the image which might
        # be y. Usually experience is that the trajectories are close to the
        # top rather to the bottom. So we discard call pixel which are below
        # the center of mass (median here)
        # These are opencv pixles. So there valus starts from the top. 0
        # belogs to top row. Therefore > rather than <.
        avg = np.median(vals)
        vals = vals[np.where(vals >= avg)]
        if len(vals) == 0:
            continue

        # Still we have multiple candidates for y for each x.
        # We find the center of these points and call it the y for given x.
        y = _find_center(vals)
        cv.circle(img, (x, int(y)), 1, 255, -1)
        x1 = (x - offX) / sX
        y1 = (r - y - offY) / sY
        res.append((x1, y1))

    # sort by x-axis.
    return sorted(res)

def _valid_px(val: int) -> int:
    return min(max(0, val), 255)

def find_trajectory(img: np.ndarray, pixel: int, T):
    logging.info(f"Extracting trajectory for color {pixel}")
    assert (
        img.min() <= pixel <= img.max()
    ), f"{pixel} is outside the range: [{img.min()}, {img.max()}]"

    # Find all pixels which belongs to a trajectory.
    o = 6
    _clower, _cupper = _valid_px(pixel - o // 2), _valid_px(pixel + o // 2)

    Y, X = np.where((img >= _clower) & (img <= _cupper))
    traj = defaultdict(list)
    for x, y in zip(X, Y):
        traj[x].append(y)

    assert traj, "Empty trajectory"

    # this is a simple fit using median.
    new = np.zeros_like(img)
    res = fit_trajectory_using_median(traj, T, new)
    return res, np.vstack((img, new))


from typing import List

from summer.compute import ComputedValueProcessor

from autumn.models.covid_19.constants import (
    INFECT_DEATH, INFECTION, Compartment, NOTIFICATIONS, HISTORY_STRATA, INFECTION_DEATHS,
    COMPARTMENTS, Vaccination, PROGRESS, Clinical, History, Tracing, NOTIFICATION_CLINICAL_STRATA,
    HOSTPIALISED_CLINICAL_STRATA,
)
from autumn.models.covid_19.parameters import Sojourn, VaccinationRisk
from autumn.settings import COVID_BASE_AGEGROUPS
from autumn.models.covid_19.stratifications.clinical import CLINICAL_STRATA
from autumn.models.covid_19.constants import INCIDENCE
from autumn.models.covid_19.stratifications.strains import Strain
from autumn.core.utils.utils import get_complement_prop
from autumn.model_features.outputs import OutputsBuilder

class TimeProcess(ComputedValueProcessor):
    """
    This is just sitting here ready to go, in case anyone wants to produce any outputs that are dependent on modelled
    time.

    """

    def process(self, compartment_values, computed_values, time):
        return time

class CovidOutputsBuilder(OutputsBuilder):
    """
    The object responsible for collating and requesting all the derived outputs for the model.

    Attributes:
        is_contact_tracing: Whether contact tracing is active in this model

    """

    def __init__(self, model, compartments, is_contact_tracing):
        OutputsBuilder.__init__(self, model, compartments)
        self.is_contact_tracing = is_contact_tracing
        self.untraced_stratum = {"tracing": Tracing.UNTRACED} if is_contact_tracing else {}

    def request_incidence(self):
        """
        Incidence is the transition from late exposed to early active - i.e. the rate of onset of an "episode".

        Generates the following derived outputs both overall and by age group:
            incidence: Rate of onset of new episodes

        """

        # Unstratified
        self.model.request_output_for_flow(name=INCIDENCE, flow_name=INCIDENCE)

        # Stratified by age group
        self.request_stratified_output_for_flow(INCIDENCE, COVID_BASE_AGEGROUPS, "agegroup")

        # Stratified by age group and by clinical stratum
        self.request_double_stratified_output_for_flow(
            INCIDENCE, COVID_BASE_AGEGROUPS, "agegroup", CLINICAL_STRATA, "clinical"
        )

    def request_infection(self):
        """
        Track the rate at which people are newly infected.
        Infection is the transition from susceptible to early exposed, of course.

        Generates the following derived outputs both overall and by age group:
            infection: Rate of new infections by time

        """

        self.model.request_output_for_flow("infection", INFECTION)

    def request_notifications(self, cumul_inc_start_time: float, hospital_reporting: float):
        """
        Calculate the rate of notifications over time.

        Args:
            cumul_inc_start_time: The starting time to start the cumulative calculations
            hospital_reporting: The proportion of hospitalisations notified (defaults to one)

        Generates the following derived outputs:
            notifications overall: Progressions to active for those in the last three strata or any strata if traced
            notifications by age group: As previous, split by age

        """

        # Age-specific notifications
        for agegroup in COVID_BASE_AGEGROUPS:
            notification_pathways = []

            # First track all traced cases (regardless of clinical stratum, on the assumption that all traced patients
            # are identified as they are in quarantine)
            if self.is_contact_tracing:
                traced_dest_strata = {"agegroup": agegroup, "tracing": Tracing.TRACED}
                name = f"progress_{Tracing.TRACED}Xagegroup_{agegroup}"
                notification_pathways.append(name)
                self.model.request_output_for_flow(
                    name=name, flow_name=PROGRESS, dest_strata=traced_dest_strata, save_results=False,
                )

            # Then track untraced cases that are passively detected - either regardless of tracing status,
            # (empty dictionary if tracing not active) or just in the untraced group otherwise
            for clinical in NOTIFICATION_CLINICAL_STRATA:

                # Include reporting adjustment if requested
                reporting = hospital_reporting if clinical in HOSTPIALISED_CLINICAL_STRATA else 1.
                untraced_dest_strata = {"clinical": clinical, "agegroup": agegroup}
                untraced_dest_strata.update(self.untraced_stratum)
                notified_hospitalisations = f"_progress_{Tracing.UNTRACED}Xagegroup_{agegroup}Xclinical_{clinical}"
                self.model.request_output_for_flow(
                    name=notified_hospitalisations, flow_name=PROGRESS, dest_strata=untraced_dest_strata,
                    save_results=False,
                )

                # Has to have a different name to the flow output to avoid summer error
                name = notified_hospitalisations[1:]
                notification_pathways.append(name)
                self.model.request_function_output(
                    name=name, func=lambda rate: rate * reporting, sources=(notified_hospitalisations,),
                    save_results=False,
                )

            final_name = f"{NOTIFICATIONS}Xagegroup_{agegroup}"
            self.model.request_aggregate_output(name=final_name, sources=notification_pathways)

        notifications_by_agegroup = [f"{NOTIFICATIONS}Xagegroup_{i_age}" for i_age in COVID_BASE_AGEGROUPS]
        self.model.request_aggregate_output(name=NOTIFICATIONS, sources=notifications_by_agegroup)

        # Cumulative unstratified notifications
        if cumul_inc_start_time:
            self.model.request_cumulative_output(
                name=f"accum_{NOTIFICATIONS}", source=NOTIFICATIONS, start_time=cumul_inc_start_time,
            )

    def request_non_hosp_notifications(self):
        """
        Calculate the rate of non-hospitalised notifications over time and
        the prevalence of the non-hospitalised notifications to reflect the number of quarantined persons.

        Generates the following derived outputs:
            non hospitalised notifications: Age specific notifications in Symptomatic ambulatory ever detected
            prevalence non hospitalised notifications: Age specific outputs. For the traced everyone in notification
            clinical strata and infectious compartments. For the untraced symptomatic detected in active compartments.

        """

        # Age-specific non-hospitalised notifications
        for agegroup in COVID_BASE_AGEGROUPS:
            age_notification_pathways = []

            # First track all traced cases (in Symptomatic ambulatory ever detected)
            if self.is_contact_tracing:
                name = f"progress_traced_non_hospitalisedXagegroup_{agegroup}"
                age_notification_pathways.append(name)
                self.model.request_output_for_flow(
                    name=name,
                    flow_name=PROGRESS,
                    dest_strata={"clinical": Clinical.SYMPT_ISOLATE, "tracing": Tracing.TRACED, "agegroup": agegroup},
                    save_results=False,
                )

            # Then track untraced cases in Symptomatic ambulatory ever detected
            name = f"progress_non_hosp_Xclinical_{Clinical.SYMPT_ISOLATE}Xagegroup_{agegroup}Xtraced_{Tracing.UNTRACED}"
            dest_strata = {"clinical": Clinical.SYMPT_ISOLATE, "agegroup": agegroup}.update(self.untraced_stratum)
            age_notification_pathways.append(name)
            self.model.request_output_for_flow(
                name=name,
                flow_name=PROGRESS,
                dest_strata=dest_strata,
                save_results=False,
            )
            agg_name = f"non_hospitalised_notificationsXagegroup_{agegroup}"
            self.model.request_aggregate_output(name=agg_name, sources=age_notification_pathways)

        # calculating the prevalence of the non hospitalised notifications by age group
        for agegroup in COVID_BASE_AGEGROUPS:
            age_notification_pathways = []

            # First track traced cases in all clinical strata except hospitalisations
            if self.is_contact_tracing:
                for clinical in NOTIFICATION_CLINICAL_STRATA:
                    name = f"progress_prevalence_traced_X{agegroup}X{clinical}"
                    age_notification_pathways.append(name)
                    self.model.request_output_for_compartments(
                        name=name,
                        compartments=[Compartment.LATE_ACTIVE],
                        strata={"clinical": clinical, "agegroup": agegroup, "tracing": Tracing.TRACED},
                    )

            # Then track untraced cases (everyone in notified clinical stratum)
            dest_strata = {"clinical": Clinical.SYMPT_ISOLATE, "agegroup": agegroup}.update(self.untraced_stratum)
            name = f"progress_prevalence_{Tracing.UNTRACED}Xagegroup_{agegroup}Xclinical_{Clinical.SYMPT_ISOLATE}"
            age_notification_pathways.append(name)
            self.model.request_output_for_compartments(
                name=name,
                compartments=[Compartment.LATE_ACTIVE],
                strata=dest_strata,
            )

            self.model.request_aggregate_output(
                name=f"prevalence_non_hospitalised_notificationsXagegroup_{agegroup}", sources=age_notification_pathways
            )

    def request_adult_paeds_notifications(self):
        """
        Split the age-specific notifications previously generated into paediatric and adult.

        Generates the following derived outputs:
            notificationsXpaediatric: Notifications for those aged under 15
            notificationsXadult: Notifications for those aged 15 and above

        """

        # Split by child and adult
        paed_notifications = [f"notificationsXagegroup_{agegroup}" for agegroup in COVID_BASE_AGEGROUPS[:3]]
        adult_notifications = [f"notificationsXagegroup_{agegroup}" for agegroup in COVID_BASE_AGEGROUPS[3:]]
        self.model.request_aggregate_output(name="notificationsXpaediatric", sources=paed_notifications)
        self.model.request_aggregate_output(name="notificationsXadult", sources=adult_notifications)

    def request_cdr(self):
        """
        Just make the computed value CDR (case detection rate) available as a derived output.

        """

        self.model.request_computed_value_output("cdr")

    def request_deaths(self):
        """
        Track COVID-19-related deaths.

        Generates the following derived outputs both overall and by age group:
            infection_deaths: Rate of deaths over time
            accum_deaths: Cumulative deaths that have accrued by that point in time

        """

        # Unstratified
        self.model.request_output_for_flow(name=INFECTION_DEATHS, flow_name=INFECT_DEATH)
        self.model.request_cumulative_output(name="accum_deaths", source="infection_deaths")

        # Stratified by age
        self.request_stratified_output_for_flow(
            INFECT_DEATH, COVID_BASE_AGEGROUPS, "agegroup", name_stem=INFECTION_DEATHS, filter_on="source"
        )
        for agegroup in COVID_BASE_AGEGROUPS:
            self.model.request_cumulative_output(
                name=f"accum_deathsXagegroup_{agegroup}",
                source=f"infection_deathsXagegroup_{agegroup}",
            )

        # Stratified by age and clinical stratum
        self.request_double_stratified_output_for_flow(
            INFECT_DEATH, COVID_BASE_AGEGROUPS, "agegroup", CLINICAL_STRATA, "clinical", name_stem=INFECTION_DEATHS,
            filter_on="source"
        )

    def request_admissions(self):
        """
        Track COVID-19-attributable admissions to hospital and to ICU.

        Generates the following derived outputs both overall and by age group:
            icu_admissions: Only those being admitted to ICU
            hospital_admissions: All those being admitted to hospital

        """

        # Track non-ICU hospital admissions (transition from early to late active in hospital, non-ICU stratum)
        self.model.request_output_for_flow(
            name="non_icu_admissions",
            flow_name=PROGRESS,
            source_strata={"clinical": Clinical.HOSPITAL_NON_ICU},
            dest_strata={"clinical": Clinical.HOSPITAL_NON_ICU},
            save_results=False,
        )

        # Track ICU admissions (transition from early to late active in ICU stratum)
        self.model.request_output_for_flow(
            name="icu_admissions",
            flow_name=PROGRESS,
            source_strata={"clinical": Clinical.ICU},
            dest_strata={"clinical": Clinical.ICU},
        )

        # Track all hospitalisations as the sum of hospital non-ICU and ICU
        self.model.request_aggregate_output("hospital_admissions", sources=["icu_admissions", "non_icu_admissions"])

        for agegroup in COVID_BASE_AGEGROUPS:
            self.model.request_output_for_flow(
                name=f"non_icu_admissionsXagegroup_{agegroup}",
                flow_name=PROGRESS,
                source_strata={"clinical": Clinical.HOSPITAL_NON_ICU, "agegroup": agegroup},
                dest_strata={"clinical": Clinical.HOSPITAL_NON_ICU, "agegroup": agegroup},
                save_results=False,
            )
            self.model.request_output_for_flow(
                name=f"icu_admissionsXagegroup_{agegroup}",
                flow_name=PROGRESS,
                source_strata={"clinical": Clinical.ICU, "agegroup": agegroup},
                dest_strata={"clinical": Clinical.ICU, "agegroup": agegroup},
                save_results=False,
            )
            self.model.request_aggregate_output(
                f"hospital_admissionsXagegroup_{agegroup}",
                sources=[f"icu_admissionsXagegroup_{agegroup}", f"non_icu_admissionsXagegroup_{agegroup}"]
            )

    def request_occupancy(self, sojourn_periods: Sojourn):
        """
        Track the number of people in hospital or in ICU over time.

        Args:
            sojourn_periods: The sojourn periods for the hospitalised compartments

        Generates the following derived outputs both overall and by age group:
            icu_occupancy: Only those currently in ICU
            hospital_occupancy: All those currently in hospital

        """

        # Hospital occupancy is represented as all ICU, all hospital late active, and some early active ICU cases
        compartment_periods = sojourn_periods
        icu_early_period = compartment_periods["icu_early"]
        hospital_early_period = compartment_periods["hospital_early"]
        period_icu_patients_in_hospital = max(icu_early_period - hospital_early_period, 0.)
        proportion_icu_patients_in_hospital = period_icu_patients_in_hospital / icu_early_period

        # Unstratified calculations
        self.model.request_output_for_compartments(
            "icu_occupancy",
            compartments=[Compartment.LATE_ACTIVE],
            strata={"clinical": Clinical.ICU},
        )
        self.model.request_output_for_compartments(
            "_late_active_hospital",
            compartments=[Compartment.LATE_ACTIVE],
            strata={"clinical": Clinical.HOSPITAL_NON_ICU},
            save_results=False,
        )
        self.model.request_output_for_compartments(
            "_early_active_icu",
            compartments=[Compartment.EARLY_ACTIVE],
            strata={"clinical": Clinical.ICU},
            save_results=False,
        )
        self.model.request_function_output(
            name="_early_active_icu_proportion",
            func=lambda patients: patients * proportion_icu_patients_in_hospital,
            sources=["_early_active_icu"],
            save_results=False,
        )
        self.model.request_aggregate_output(
            name="hospital_occupancy",
            sources=[
                "_late_active_hospital",
                "icu_occupancy",
                "_early_active_icu_proportion",
            ],
        )

        # Stratified by age group
        for agegroup in COVID_BASE_AGEGROUPS:

            age_icu_name = f"icu_occupancyXagegroup_{agegroup}"
            self.model.request_output_for_compartments(
                name=age_icu_name,
                compartments=[Compartment.LATE_ACTIVE],
                strata={"clinical": Clinical.ICU, "agegroup": agegroup},
                save_results=True,
            )
            age_late_hospital_name = f"late_hospitalXagegroup_{agegroup}"
            self.model.request_output_for_compartments(
                name=age_late_hospital_name,
                compartments=[Compartment.LATE_ACTIVE],
                strata={"clinical": Clinical.HOSPITAL_NON_ICU, "agegroup": agegroup},
                save_results=False,
            )
            age_icu_ealy_name = f"early_active_icuXagegroup_{agegroup}"
            self.model.request_output_for_compartments(
                name=age_icu_ealy_name,
                compartments=[Compartment.EARLY_ACTIVE],
                strata={"clinical": Clinical.ICU, "agegroup": agegroup},
                save_results=False,
            )
            age_icu_early_in_hospital_name = f"early_active_icu_in_hospitalXagegroup_{agegroup}"
            self.model.request_function_output(
                name=age_icu_early_in_hospital_name,
                func=lambda patients: patients * proportion_icu_patients_in_hospital,
                sources=[age_icu_ealy_name],
                save_results=False,
            )
            self.model.request_aggregate_output(
                name=f"hospital_occupancyXagegroup_{agegroup}",
                sources=[
                    age_icu_name,
                    age_late_hospital_name,
                    age_icu_early_in_hospital_name,
                ],
            )

    def request_tracing(self):
        """
        Collate up all the computed values used during the process of working out the effect of contact tracing.

        Additionally generates the following derived outputs:
            prop_contacts_quarantined: The proportion of all contacts (including those of undetected cases) identified

        """

        # Standard calculations always computed when contact tracing requested
        self.model.request_computed_value_output("prevalence")
        self.model.request_computed_value_output("prop_detected_traced")
        self.model.request_computed_value_output("prop_contacts_with_detected_index")
        self.model.request_computed_value_output("traced_flow_rate")

        # Proportion of quarantined contacts among all contacts
        self.model.request_function_output(
            name="prop_contacts_quarantined",
            func=lambda prop_detected_traced, prop_detected_index: prop_detected_traced * prop_detected_index,
            sources=["prop_detected_traced", "prop_contacts_with_detected_index"],
        )

    def request_strains(self, voc_names: List[str]):
        """
        VoC-related outputs.

        Args:
            voc_names: The names of all the VoCs being implemented in the model

        Generates the following derived outputs:
            incidence (by strain): See above for definition
            prop_incidence (by strain): Proportion of incidence attributable to that strain

        """

        # Incidence rate for each strain implemented
        all_strains = [Strain.WILD_TYPE] + voc_names
        self.request_stratified_output_for_flow(INCIDENCE, all_strains, "strain")

        # Convert to a proportion
        for strain in all_strains:
            self.model.request_function_output(
                name=f"prop_{INCIDENCE}_strain_{strain}",
                func=lambda strain_inc, total_inc: strain_inc / total_inc,
                sources=[f"{INCIDENCE}Xstrain_{strain}", INCIDENCE]
            )

    def request_vaccination(self, is_dosing_active: bool, vacc_strata: List[str]):
        """
        Vaccination-related outputs

        Args:
            is_dosing_active: Are we including an additional model stratum for those who have been fully vaccinated
            vacc_strata: All the vaccination strata being implemented

        Generates the following derived outputs:
            proportions of people (by vaccination status): See the population distribution by vaccination status
            at_least_one_dose_prop: To facilitate calculating total vaccination coverage, regardless of model structure

        """

        # Track proportions vaccinated by vaccination status (depends on total_population previously being requested)
        for vacc_stratum in vacc_strata:
            self.model.request_output_for_compartments(
                name=f"_{vacc_stratum}",
                compartments=COMPARTMENTS,
                strata={"vaccination": vacc_stratum},
                save_results=False,
            )
            self.model.request_function_output(
                name=f"proportion_{vacc_stratum}",
                sources=[f"_{vacc_stratum}", "total_population"],
                func=lambda vaccinated, total: vaccinated / total,
            )

        if is_dosing_active:
            self.model.request_function_output(
                name="at_least_one_dose_prop",
                func=lambda unvacc, pop: 1. - unvacc / pop,
                sources=[f"_{Vaccination.UNVACCINATED}", "total_population"]
            )

    def request_vacc_aefis(self, vacc_risk_params: VaccinationRisk):
        """
        Calculate the risk of TTS and myocarditis for the vaccines predominantly responsible for these.

        Args:
            vacc_risk_params: The parameters defining vaccine-related risk (i.e. AEFIs)

        """

        risk_multiplier = vacc_risk_params.risk_multiplier

        # Track the rate of adverse events and hospitalisations by age, if adverse events calculations are requested
        self.request_stratified_output_for_flow("vaccination", COVID_BASE_AGEGROUPS, "agegroup", filter_on="source")

        for agegroup in COVID_BASE_AGEGROUPS:
            agegroup_string = f"agegroup_{agegroup}"

            # TTS for AstraZeneca vaccines
            self.model.request_function_output(
                name=f"tts_casesX{agegroup_string}",
                sources=[f"vaccinationX{agegroup_string}"],
                func=lambda vaccinated:
                vaccinated * vacc_risk_params.tts_rate[agegroup] * vacc_risk_params.prop_astrazeneca * risk_multiplier
            )
            self.model.request_function_output(
                name=f"tts_deathsX{agegroup_string}",
                sources=[f"tts_casesX{agegroup_string}"],
                func=lambda tts_cases:
                tts_cases * vacc_risk_params.tts_fatality_ratio[agegroup]
            )

            # Myocarditis for mRNA vaccines
            self.model.request_function_output(
                name=f"myocarditis_casesX{agegroup_string}",
                sources=[f"vaccinationX{agegroup_string}"],
                func=lambda vaccinated:
                vaccinated * vacc_risk_params.myocarditis_rate[agegroup] * vacc_risk_params.prop_mrna * risk_multiplier
            )

        # Aggregate using larger age-groups
        aggregated_age_groups = {"15_19": ["15"]}
        for age_min in [20 + i * 10 for i in range(5)]:
            age_max = age_min + 9
            aggregated_age_groups[f"{age_min}_{age_max}"] = [str(age_min), str(age_min + 5)]
        aggregated_age_groups["70_plus"] = ["70", "75"]

        cumul_start_time = None if not vacc_risk_params.cumul_start_time else vacc_risk_params.cumul_start_time
        for output_type in ["tts_cases", "tts_deaths", "myocarditis_cases", "hospital_admissions"]:
            for aggregated_age_group, agegroups in aggregated_age_groups.items():
                agg_output = f"{output_type}Xagg_age_{aggregated_age_group}"
                agg_output_sources = [f"{output_type}Xagegroup_{agegroup}" for agegroup in agegroups]
                self.model.request_aggregate_output(name=agg_output, sources=agg_output_sources)

                # Cumulative output calculation
                self.model.request_cumulative_output(
                    name=f"cumulative_{agg_output}",
                    source=agg_output,
                    start_time=cumul_start_time
                )

    def request_experienced(self):
        """
        History status-related outputs.
        Arguably the naive group should exclude the people with active infection - this is not currently being done.

        Generates the following derived outputs:
            proportions of people (by history status)
            prop_ever_infected: Everyone who is not infection-naive

        """

        self.request_stratified_output_for_compartment("prop", COMPARTMENTS, HISTORY_STRATA, "history")
        self.model.request_function_output(
            name="prop_ever_infected",
            func=get_complement_prop,
            sources=[f"propXhistory_{History.NAIVE}", "total_population"],
        )

        # Stratified by age group
        self.request_stratified_output_for_compartment(
            "total_population", COMPARTMENTS, COVID_BASE_AGEGROUPS, "agegroup", save_results=False
        )
        for agegroup in COVID_BASE_AGEGROUPS:
            for stratum in HISTORY_STRATA:
                name = f"prop_{stratum}Xagegroup{agegroup}"
                self.model.request_output_for_compartments(
                    name=name,
                    compartments=COMPARTMENTS,
                    strata={"history": stratum, "agegroup": agegroup},
                )
            self.model.request_function_output(
                name=f"prop_ever_infectedXagegroup_{agegroup}",
                func=get_complement_prop,
                sources=[f"prop_{History.NAIVE}Xagegroup{agegroup}", f"total_populationXagegroup_{agegroup}"],
            )

    def request_time(self):
        """
        Currently not used, but available if needed.

        """

        self.model.request_computed_value_output("time_process")


from typing import List
from collections import defaultdict

def def_value() -> int:
    return 0;

def subseq(s: List[str], t: List[str]) -> bool:
    '''Return True if s is a subsequence of t;
    return False otherwise
    '''
    d = defaultdict(def_value)
    for st in t:
        d[st]+=1

    for st in s:
        if d[st] != 0:
            d[st]-=1
        else:
            return False

    return True

if __name__ == '__main__':
    t = ["wake up", "sleep", "wake up", "eat", "sleep"]
    s1 = ["wake up", "eat"]
    s2 = ["eat", "eat"]
    print(subseq(s1, t))
    print(subseq(s2, t))


# Given the root of a binary tree, return the level order traversal of its
# nodes' values. (i.e., from left to right, level by level).

from collections import deque

class Solution:
    def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
        if not root:
            return []

        queue = deque([root])
        levels = []
        while queue:
            level_size = len(queue)
            level = []
            for _ in range(level_size):
                curr = queue.pop()
                level.append(curr.val)
                if curr.left:
                    queue.appendleft(curr.left)
                if curr.right:
                    queue.appendleft(curr.right)
            levels.append(level)

        return levels

    def levelOrderV2(self, root: Optional[TreeNode]) -> List[List[int]]:
        levels = []
        self.levelOrderHelper(root, 0, levels)
        return levels

    def levelOrderHelper(self, root, level, levels):
        if not root:
            return None
        if level == len(levels):
            levels.append([])
        levels[level].append(root.val)
        self.levelOrderHelper(root.left, level + 1, levels)
        self.levelOrderHelper(root.right, level + 1, levels)


from flask import Blueprint, request, jsonify, render_template
from helpers import token_required
from models import db, User, Car, car_schema, cars_schema

api = Blueprint('api',__name__, url_prefix='/api')

# ADD NEW CAR
@api.route('/cars', methods=['POST'])
@token_required
def add_car(current_user_token):
    nickname = request.json['nickname']
    make = request.json['make']
    model = request.json['model']
    prodyear = request.json['prodyear']
    mileage = request.json['mileage']

    new_car = Car(current_user_token.token, nickname, make, model, prodyear, mileage)

    db.session.add(new_car)
    db.session.commit()

    return jsonify(car_schema.dump(new_car))

# GET ALL CARS
@api.route('/cars', methods=['GET'])
@token_required
def get_all(current_user_token):
    current_cars = Car.query.filter_by(user_token=current_user_token.token).all()
    return jsonify(cars_schema.dump(current_cars)) if current_cars else jsonify({'message': 'You have no cars. Add one with a POST request and a JSON string like the given example.','example':{'nickname': '','make': '','model': '','prodyear': 2023,'mileage': 1}})

# GET SPECIFIC CAR
@api.route('/cars/<id>', methods=['GET'])
@token_required
def get_from_id(current_user_token, id):
    # find car by id
    current_car = Car.query.get(id)
    # check car's recorded user_token against given current_user_token
    if current_user_token.token == current_car.user_token:
        # if user tokens match, return the car's data
        return jsonify(car_schema.dump(current_car))
    else:
        # if user tokens do not match, return user an error message
        return jsonify({'message': f'Given car {id} does not belong to given access token'})

# UPDATE CAR
@api.route('/cars/<id>', methods=['POST','PUT'])
@token_required
def update_id(current_user_token, id):
    # find car by id
    updated_car = Car.query.get(id)
    # check car's recorded user_token against given current_user_token
    if current_user_token.token == updated_car.user_token:
        # if user tokens match, update the data
        updated_car.nickname = request.json['nickname']
        updated_car.make = request.json['make']
        updated_car.model = request.json['model']
        updated_car.prodyear = request.json['prodyear']
        updated_car.mileage = request.json['mileage']

        db.session.commit()
        return jsonify(car_schema.dump(updated_car))
    else:
        # if user tokens do not match, return user an error message
        return jsonify({'message': f'Given car {id} does not belong to given access token'})

# DELETE CAR
@api.route('/cars/<id>', methods=['DELETE'])
@token_required
def delete_id(current_user_token, id):
    # find car by id
    deleted_car = Car.query.get(id)
    # check car's recorded user_token against given current_user_token
    if current_user_token.token == deleted_car.user_token:
        # if user tokens match, delete the car
        db.session.delete(deleted_car)
        db.session.commit()
        return jsonify(car_schema.dump(deleted_car))
    else:
        # if user tokens do not match, return user an error message
        return jsonify({'message': f'Given car {id} does not belong to given access token'})


import inspect
import pathlib
from typing import Dict, List

from py4vasp import combine, exception
from py4vasp._util import convert

class Calculations:
    """A class to handle multiple Calculations all at once.

    This class combines the functionality of the Calculation class for more than one
    calculation. Create a Calculations object using either a wildcard for a set of
    paths or files or pass in paths and files directly. Then you can access the
    properties of all calculations via the attributes of the object.

    Examples
    --------
    >>> calcs = Calculations.from_paths(calc1="path_to_calc1", calc2="path_to_calc2")
    >>> calcs.energies.read() # returns a dictionary with the energies of calc1 and calc2
    >>> calcs.forces.read()   # returns a dictionary with the forces of calc1 and calc2
    >>> calcs.stresses.read() # returns a dictionary with the stresses of calc1 and calc2

    Notes
    -----
    To create new instances, you should use the classmethod :meth:`from_paths` or
    :meth:`from_files`. This will ensure that the paths to your VASP calculations are
    properly set and all features work as intended. Note that this is an alpha version
    and the API might change in the future.
    """

    def __init__(self, *args, **kwargs):
        if not kwargs.get("_internal"):
            message = """\
Please setup new CompareCalculations instance using the classmethod CompareCalculations.from_paths()
or CompareCalculations.from_files() instead of the constructor CompareCalculations()."""
            raise exception.IncorrectUsage(message)

    def _path_finder(**kwargs):
        for key, value in kwargs.items():
            if not (isinstance(value, pathlib.Path) or isinstance(value, str)):
                message = """\
Please provide a path to a VASP calculation as a string or pathlib.Path."""
                raise exception.IncorrectUsage(message)
            paths = pathlib.Path(value).expanduser().absolute()
            if "*" in paths.as_posix():
                paths = sorted(list(paths.parent.glob(paths.name)))
            else:
                paths = [paths]
            yield key, paths

    @classmethod
    def from_paths(cls, **kwargs):
        """Set up a Calculations object for paths.

        Setup a calculation for paths by passing in a dictionary with the name of the
        calculation as key and the path to the calculation as value.

        Parameters
        ----------
        **kwargs : Dict[str, str or pathlib.Path]
            A dictionary with the name of the calculation as key and the path to the
            calculation as value. Wildcards are allowed.
        """
        calculations = cls(_internal=True)
        calculations._paths = {}
        for key, paths in cls._path_finder(**kwargs):
            calculations._paths[key] = paths
        calculations = _add_all_combination_classes(
            calculations, _add_attribute_from_path
        )
        return calculations

    @classmethod
    def from_files(cls, **kwargs):
        """Set up a Calculations object from files.

        Setup a calculation for files by passing in a dictionary with the name of the
        calculation as key and the path to the calculation as value. Note that this
        limits the amount of information, you have access to, so prefer creating the
        instance with the :meth:`from_paths` if possible.

        Parameters
        ----------
        **kwargs : Dict[str, str or pathlib.Path]
            A dictionary with the name of the calculation as key and the files to the
            calculation as value. Wildcards are allowed.
        """
        calculations = cls(_internal=True)
        calculations._paths = {}
        calculations._files = {}
        for key, paths in cls._path_finder(**kwargs):
            basedir_paths = [path.parent for path in paths]
            calculations._paths[key] = basedir_paths
            calculations._files[key] = paths
        calculations = _add_all_combination_classes(
            calculations, _add_attribute_from_file
        )
        return calculations

    def paths(self) -> Dict[str, List[pathlib.Path]]:
        """Return the paths of the calculations."""
        return self._paths

    def files(self) -> Dict[str, List[pathlib.Path]]:
        """Return the files of the calculations."""
        return self._files

    def number_of_calculations(self) -> Dict[str, int]:
        """Return the number of calculations for each calculation."""
        return {key: len(value) for key, value in self._paths.items()}

def _add_attribute_from_path(calc, class_):
    instance = class_.from_paths(calc.paths())
    setattr(calc, convert.quantity_name(class_.__name__), instance)
    return calc

def _add_attribute_from_file(calc, class_):
    instance = class_.from_files(calc.files())
    setattr(calc, convert.quantity_name(class_.__name__), instance)
    return calc

def _add_all_combination_classes(calc, add_single_class):
    for _, class_ in inspect.getmembers(combine, inspect.isclass):
        calc = add_single_class(calc, class_)
    return calc


"""
https://adventofcode.com/2017/day/3
"""
from collections import defaultdict
from utils.point2d import Point2D

# This seems like a lot of code, but it's mostly me trying to be a smartarse
# and do part 1 in (almost) constant time.

def next_odd_square(num):
    """
    Return the next odd square number equal to or greater than num.
    This is useful because the last number in each ring/layer of the sequence
    for part 1 is an odd square (1, 9, 25, etc..).
    """
    sqrt = int(num ** 0.5)
    if sqrt % 2 == 0:
        sqrt += 1
    return sqrt**2

def get_layer(num):
    """
    Which 'layer' of the spiral pattern is the given number on?
    The first number (1) is on layer 0, next ring (2->9) is layer 1.
    """
    sqrt = int(num ** 0.5)
    return sqrt // 2

def get_steps_to_access_port(num):
    """
    Returns how many steps it takes to move the specified number from its
    initial position to the access port at 0, 0.
    Steps is the Manhattan Distance from the starting position to 0, 0.
    We could brute force this entire thing and just walk the spiral from 1 to
    num, but this is a faster solution for large numbers.
    """
    # Determine the layer of this number, and from that the minimum and maximum
    # possible steps from a number in the layer to the access port / origin.
    # e.g. in layer 1, you can be at min 1 step away, and at max 2 steps.
    #      in layer 2, you can be between 2 and 4 steps away.
    #      etc...
    layer_num = get_layer(num)
    min_steps = layer_num
    max_steps = layer_num*2

    steps = max_steps
    direction = -1

    # The largest number on any layer is in the bottom right corner and is an
    # odd square (e.g. 1, 9, 25, etc..).
    # How far our number is from the next odd square tells us how many steps
    # we have to walk clockwise around the spiral to get from the bottom right
    # of the layer to our number.
    # As we walk around the spiral from the bottom right to our number we move
    # first closer to the access port (moving left from bottom right towards
    # bottom centre) and then further away (moving left from bottom centre to
    # bottom left). The same pattern of getting closer and then further away
    # from the access port repeats along each side until we reach our
    # number. By taking account of whether we're moving a step closer to the
    # access port, or a step further away, as we move around the spiral, we
    # can figure out the final distance of our number from the access port.
    for _ in reversed(range(num, next_odd_square(num))):
        steps += direction
        if steps == min_steps or steps == max_steps:
            direction *= -1

    return steps

def get_next_position():
    """
    Generator function to return the next position in the spiral sequence.
    """
    pos = Point2D(0, 0)
    direction = Point2D(1, 0)

    # Always start at 0, 0
    yield pos

    while True:
        # Should we start the next layer?
        if pos.x >= 0 and pos.x == pos.y:
            pos += Point2D(1, 0)
            direction = Point2D(0, -1)
        else:
            # Should we start moving left?
            if pos.x > 0 and pos.x == -pos.y:
                direction = Point2D(-1, 0)
            # Should we start moving down?
            elif pos.x < 0 and pos.x == pos.y:
                direction = Point2D(0, 1)
            # Should we start moving right?
            elif pos.x < 0 and pos.x == -pos.y:
                direction = Point2D(1, 0)

            pos += direction
        yield pos

def get_first_stress_num_greater_than(target):
    """
    Returns the first number produced in the stress test pattern that is
    greater than the target number.
    """
    nums = defaultdict(int)
    nums[Point2D(0, 0)] = 1

    OFFSETS = (Point2D(-1, -1), Point2D(0, -1), Point2D(1, -1),
            Point2D(-1, 0), Point2D(1, 0),
            Point2D(-1, 1), Point2D(0, 1), Point2D(1, 1))

    # Walk round the spiral and add the values of the surrounding positions
    # to determine the value of the current position.
    for p in get_next_position():
        nums[p] = max(1, sum(nums[p + o] for o in OFFSETS))
        if nums[p] > target:
            return nums[p]

# Part 1
# How many steps does it take to move the number 312051 to the data port at
# the first position?
print(get_steps_to_access_port(312051))

# Part 2
# Using the stress pattern what's the first number generated that's greater
# than 312051?
print(get_first_stress_num_greater_than(312051))