import wx
import wx.lib.throbber as  throb
import MatroxFramegrabber
import numpy as np

class wxSaviorTopPanel(wx.Panel):

    """Image acquisition control panel
    """

    def __init__(self,
                 parent,
                 matrox_frame_grabber_wx_event_generator,
                 local_savior_path = '.'):

        """
        """

        # default constructor using the parent window
        wx.Panel.__init__(self, parent, -1)

        self._matrox_frame_grabber_wx_event_generator = matrox_frame_grabber_wx_event_generator
        self._matrox_frame_grabber                    = matrox_frame_grabber_wx_event_generator.GetEventGenerator()
        self._matrox_frame_grabber_wx_event_generator.BindWXEvent("stats_ready", self._OnStatsUpdate)

        self.movie_number = 0

        # If we have a fixation GUI open, grab its reference
        if parent.fixQ is not None:
            self.fixQ = parent.fixQ

        # getting background and foreground colours from the parent panel
        self.SetBackgroundColour(parent.GetBackgroundColour())
        self.SetForegroundColour(parent.GetForegroundColour())

        # creating the sizer and defining control dimensions
        main_sizer             = wx.GridBagSizer(vgap = 10, hgap = 10)
        border                 = 0
        text_ctrl_width        = 40
        text_ctrl_height       = 15
        text_ctrl_size         = (text_ctrl_width, text_ctrl_height)

        # Defining the title font
        bold_font = self.GetFont()
        bold_font.SetWeight(wx.BOLD)
        bold_font.SetPointSize(bold_font.GetPointSize() + 2)
        self.SetFont(bold_font)

        ###################################################################
        #                      creating controls                          #
        ###################################################################

        # discard and image recording section #######################
        blank_icon                  = wx.Bitmap(local_savior_path + '/icons/blank icon.png',                 wx.BITMAP_TYPE_PNG)
        stop_icon                   = wx.Bitmap(local_savior_path + '/icons/Pause-Hot-icon 42.png',          wx.BITMAP_TYPE_PNG)
        live_icon                   = wx.Bitmap(local_savior_path + '/icons/play-normal-blue-bright 42.png', wx.BITMAP_TYPE_PNG)
        record_icon                 = wx.Bitmap(local_savior_path + '/icons/Record-Pressed-icon 42.png',     wx.BITMAP_TYPE_PNG)

        # creating the throbbers (initially in resting mode)
        self.mode_icons             = [stop_icon, live_icon, record_icon, blank_icon]
        throbber_delay              = 0.35
        self.current_mode_throbber = throb.Throbber(self, -1, self.mode_icons, frameDelay = throbber_delay)

        self.SetCurrentModeIcon('stop')

        # adding controls to the sizer
        main_sizer.Add(self.current_mode_throbber,
                       pos  = (0, 1),                               span   = (5, 1),
                       flag = wx.ALIGN_CENTER_VERTICAL|wx.ALL,      border = 0)

        label_acquisition_frame_rate_title = wx.StaticText(self, label = "Acquisition frame rate (FPS): ", style = wx.ALIGN_LEFT)
        self._acquisition_frame_rate_label = wx.StaticText(self, label = '0.0', style = wx.ALIGN_LEFT)

        self._video_writing_frame_rate_title   = wx.StaticText(self, label = "Writing frame rate (FPS): ")
        self._video_writing_rate_label   = wx.StaticText(self, label = '0.0')

        self._video_writing_frame_rate_title.Show(False)
        self._video_writing_rate_label.Show(False)
        self._video_number_label         = wx.StaticText(self, label = 'Not recording')
        self._video_frames_written_label = wx.StaticText(self, label = '0/0')

        self._video_frames_written_label.Show(False)

        main_sizer.Add(label_acquisition_frame_rate_title,
                       pos  = (1,  2),                    span   = (1, 1),
                       flag = wx.EXPAND | wx.ALL | wx.ALIGN_LEFT,                   border = border)
        main_sizer.Add(self._acquisition_frame_rate_label,
                       pos  = (1,  3),                    span   = (1, 1),
                       flag = wx.EXPAND | wx.ALIGN_LEFT | wx.ALL, border = border)

        # size headers ####################################################
        self.temp_header_H               = wx.StaticText(self, label = "  H  ")
        self.temp_header_V               = wx.StaticText(self, label = "  V  ")

        # adding controls to the sizer
        main_sizer.Add(self.temp_header_H,
                       pos  = (1,  5),                              span   = (1, 1),
                       flag = wx.ALIGN_CENTER | wx.ALL, border = border)
        main_sizer.Add(self.temp_header_V,
                       pos  = (1,  6),                              span   = (1, 1),
                       flag = wx.ALIGN_CENTER | wx.ALL, border = border)

        # Image size in degrees  ##########################################
        label_text_imaging_fov_in_deg    = wx.StaticText(self, label = "Field of view (deg):")
        self.text_imaging_width_in_deg   = wx.StaticText(self, label = "2.0")
        self.text_imaging_width_in_deg.SetMinSize((text_ctrl_width, -1))
        label_imaging_x_in_deg           = wx.StaticText(self, label = "x")
        self.text_imaging_height_in_deg  = wx.StaticText(self, label = "2.0")

        # Frame size in pixels ############################################
        label_frame_size_in_pix          = wx.StaticText(self, label = "Image size (pix):")
        label_frame_size_in_pix.SetMinSize((2*text_ctrl_width, -1))
        self.text_frame_width_in_pix     = wx.StaticText(self, label = "     ")
        self.text_frame_width_in_pix.SetMinSize((text_ctrl_width, -1))
        label_frame_size_x               = wx.StaticText(self, label =  "x")
        self.text_frame_height_in_pix    = wx.StaticText(self, label = "     ")

        # adding controls to the sizer
        current_row = 2

        main_sizer.Add(self._video_writing_frame_rate_title,
                       pos  = (current_row,  2),                    span   = (1, 1),
                       flag = wx.EXPAND | wx.ALL| wx.ALIGN_LEFT,                   border = border)
        main_sizer.Add(self._video_writing_rate_label,
                       pos  = (current_row,  3),                    span   = (1, 1),
                       flag = wx.EXPAND | wx.ALIGN_LEFT| wx.ALL, border = border)

        main_sizer.Add(label_text_imaging_fov_in_deg,
                       pos  = (current_row,  4),                    span   = (1, 1),
                                          border = border)
        main_sizer.Add(self.text_imaging_width_in_deg,
                       pos  = (current_row,  5),                    span   = (1, 1),
                       border = border)
        main_sizer.Add(label_imaging_x_in_deg,
                       pos  = (current_row,  6),                    span   = (1, 1),
                       border = border)
        main_sizer.Add(self.text_imaging_height_in_deg,
                       pos  = (current_row, 7),                    span   = (1, 1),
                       border = border)

        current_row += 1
        main_sizer.Add(self._video_number_label,
                       pos  = (current_row,  2),                    span   = (1, 1),
                       flag = wx.EXPAND | wx.ALIGN_LEFT | wx.ALL, border = border)

        main_sizer.Add(self._video_frames_written_label,
                       pos  = (current_row,  3),                    span   = (1, 1),
                       flag = wx.EXPAND | wx.ALIGN_LEFT | wx.ALL, border = border)

        main_sizer.Add(label_frame_size_in_pix,
                       pos  = (current_row,  4),                    span   = (1, 1),
                       border = border)
        main_sizer.Add(self.text_frame_width_in_pix,
                       pos  = (current_row,  5),                    span   = (1, 1),
                       border = border)
        main_sizer.Add(label_frame_size_x,
                       pos  = (current_row,  6),                    span   = (1, 1),
                       border = border)
        main_sizer.Add(self.text_frame_height_in_pix,
                       pos  = (current_row, 7),                    span   = (1, 1),
                       border = border)

        current_row += 1
        main_sizer.AddSizer((0,0),
                       pos  = (current_row, 10),                    span   = (1, 1),
                       flag = wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, border = border)

        # making some columns and row growables
        #main_sizer.AddGrowableCol( 0, proportion = 1)
        main_sizer.AddGrowableCol( 2, proportion = .6)
        main_sizer.AddGrowableCol( 3, proportion = .4)
        # main_sizer.AddGrowableCol( 6, proportion = 2)
        # main_sizer.AddGrowableCol(12, proportion = 2)

        # Fitting the current panel to the needs of the sizer.
        self.SetSizerAndFit(main_sizer)

        # bind the onclose method
        wx.GetApp().GetTopWindow().Bind(wx.EVT_CLOSE, self.OnClose, wx.GetApp().GetTopWindow())
        self._main_sizer = main_sizer

        # end of constructor ##############################################

    def _OnStatsUpdate(self, event):

        matrox_frame_grabber =  self._matrox_frame_grabber_wx_event_generator.GetEventGenerator()

        if matrox_frame_grabber.GetCurrentState() == MatroxFramegrabber.RECORDING or\
            matrox_frame_grabber.GetCurrentState() == MatroxFramegrabber.PAUSED_RECORDING:

            self._video_writing_frame_rate_title.Show(True)
            self._video_writing_rate_label.Show(True)
            self._video_number_label.Show(True)
            self._video_frames_written_label.Show(True)
            self._main_sizer.Layout()
            avi_writing_frame_rate = matrox_frame_grabber.GetAVIWritingFrameRate()
            avi_writing_frame_rate = np.around(avi_writing_frame_rate, 2)

            avi_frames_written     = matrox_frame_grabber.GetCurrentAVIFrameNumber()
            avi_frames_to_write    = matrox_frame_grabber.GetNumberOfAVIFramesToWrite()

            self._video_writing_rate_label.SetLabel(str(avi_writing_frame_rate))

            saving_video_label   = "Saving movie # " + str(self.movie_number)
            frames_written_label = str(avi_frames_written) + '/' + str(avi_frames_to_write)

            self._video_number_label.SetLabel(saving_video_label)
            self._video_frames_written_label.SetLabel(frames_written_label)
        else:
            self._video_writing_frame_rate_title.Show(False)
            self._video_writing_rate_label.Show(False)
            self._video_number_label.Show(False)
            self._video_frames_written_label.Show(False)

        acquisition_rate = matrox_frame_grabber.GetAcquisitionFrameRate()
        display_rate     = matrox_frame_grabber.GetDisplayFrameRate()
        display_rate     = np.around(display_rate, 2)
        acquisition_rate     = np.around(acquisition_rate, 2)
        self._acquisition_frame_rate_label.SetLabel(str(acquisition_rate))

        self.Update()

        event.Skip()

    def OnClose(self, event):

        self.current_mode_throbber.Rest()
        event.Skip()

    def SetCurrentModeIcon(self, current_mode):

        """
        """

        if current_mode == 'live':
            # hiding the pause throbber
            self.current_mode_throbber.Stop()
            self.current_mode_throbber.SetSequence([1, 3])
            #self.current_mode_throbber.Start()

        elif current_mode == 'record':

            self.current_mode_throbber.Stop()
            self.current_mode_throbber.SetSequence([2, 3])
            #self.current_mode_throbber.Start()

        else:

            self.current_mode_throbber.Stop()
            self.current_mode_throbber.SetSequence([0, 0])

        # Force a repaint event
        self.Refresh()

    def ShowEyeTrackingIcon(self, boolean):
        ""
        ""
        pass
        #self.eye_tracking_icon.Show(boolean)

    def UpdateOpticalScannersAndImageSamplingDisplay(self,frame_width_in_deg,
                                                           frame_height_in_deg):

        """
        """

        frame_width_in_pix = self._matrox_frame_grabber.GetVideoWidth()
        frame_height_in_pix =  self._matrox_frame_grabber.GetVideoHeight()

        # getting sizes to guarantee alignment
        temp_size_left  = self.text_frame_width_in_pix.GetSize()
        temp_size_right = self.text_frame_height_in_pix.GetSize()

        # Frame size in pixels
        self.text_frame_width_in_pix.SetLabel(    str(frame_width_in_pix))
        self.text_frame_height_in_pix.SetLabel(   str(frame_height_in_pix))

        self.text_imaging_width_in_deg.SetLabel(  str(round(frame_width_in_deg,  2)))
        self.text_imaging_height_in_deg.SetLabel( str(round(frame_height_in_deg, 2)))

        # Update the fixation GUI as well
        if self.fixQ is not None:
            try:
                # 1 is for FOV changes
                self.fixQ.put( (1, round(frame_width_in_deg,  2),round(frame_height_in_deg,  2)),block=False, timeout=0 )
            except:
                pass

        # adjusting sizes
        self.text_frame_width_in_pix.SetSize(     temp_size_left)
        self.text_frame_height_in_pix.SetSize(    temp_size_right)
        self.text_imaging_width_in_deg.SetSize(   temp_size_left)
        self.text_imaging_height_in_deg.SetSize(  temp_size_right)

    def SetCurrentMovieNumber(self, movie_number):
        self.movie_number = movie_number

###################################################################
#                           Test code                             #
###################################################################

class TestApp(wx.App):

    def __init__ (self):

        wx.App.__init__(self)

        # initializing the window
        test_frame       = wx.Frame(None,
                                    size  = (700, 250),
                                    title = 'wxSaviorImageAcquisitionPanel test')

        self.SetTopWindow(test_frame)
        sizer = wx.BoxSizer(wx.VERTICAL)

        # creating panel
        self.test_panel      = wxSaviorTopPanel(test_frame)

        self.test_panel.UpdateOpticalScannersAndImageSamplingDisplay(
                             frame_width_in_pix            = 800,
                             frame_height_in_pix           = 750,
                             frame_width_in_deg            = 1.8,
                             frame_height_in_deg           = 1.7)

        sizer.Add(self.test_panel, wx.GROW)

        live_button = wx.Button(test_frame, label = "Start Live")
        sizer.Add(live_button)
        live_button.Bind(wx.EVT_BUTTON, self.OnLive)

        record_button = wx.Button(test_frame, label = "Start Record")
        sizer.Add(record_button)
        record_button.Bind(wx.EVT_BUTTON, self.OnRecord)

        stop_button = wx.Button(test_frame, label = "Stop")
        sizer.Add(stop_button)
        stop_button.Bind(wx.EVT_BUTTON, self.OnStop)

        test_frame.SetSizer(sizer)
        test_frame.Show()

        # starting the main event loop
        self.MainLoop()

    def OnLive(self, event):
        self.test_panel.SetCurrentModeIcon('live')
        event.Skip()

    def OnRecord(self, event):
        self.test_panel.SetCurrentModeIcon('record')
        event.Skip()

    def OnStop(self, event):
        self.test_panel.SetCurrentModeIcon('stop')
        event.Skip()

if __name__ == '__main__':

    # initializing the application
    app              = TestApp()


"""Disposable module with temporary work-arounds until upstream changes are made in Dask/Xarray"""
import warnings
import xarray as xr
import dask.array as da
from dask.dataframe import DataFrame
from dask.array import Array
from dask.array.overlap import overlap, trim_internal
from numbers import Integral
# pylint: disable=no-name-in-module
from tlz import merge, pipe, concat, partial, get

def map_overlap(
    func, *args, depth=None, boundary=None, trim=True, align_arrays=True, **kwargs
):
    """ Map a function over blocks of arrays with some overlap

    We share neighboring zones between blocks of the array, map a
    function, and then trim away the neighboring strips.

    Parameters
    ----------
    func: function
        The function to apply to each extended block
    args : dask arrays
    depth: int, tuple, dict or list
        The number of elements that each block should share with its neighbors
        If a tuple or dict then this can be different per axis.
        If a list then each element of that list must be an int, tuple or dict
        defining depth for the corresponding array in `args`.
        Asymmetric depths may be specified using a dict value of (-/+) tuples.
        Note that asymmetric depths are currently only supported when
        ``boundary`` is 'none'.
        The default value is 0.
    boundary: str, tuple, dict or list
        How to handle the boundaries.
        Values include 'reflect', 'periodic', 'nearest', 'none',
        or any constant value like 0 or np.nan.
        If a list then each element must be a str, tuple or dict defining the
        boundary for the corresponding array in `args`.
        The default value is 'reflect'.
    trim: bool
        Whether or not to trim ``depth`` elements from each block after
        calling the map function.
        Set this to False if your mapping function already does this for you
    align_arrays: bool
        Whether or not to align chunks along equally sized dimensions when
        multiple arrays are provided.  This allows for larger chunks in some
        arrays to be broken into smaller ones that match chunk sizes in other
        arrays such that they are compatible for block function mapping. If
        this is false, then an error will be thrown if arrays do not already
        have the same number of blocks in each dimensions.
    **kwargs:
        Other keyword arguments valid in ``map_blocks``

    Examples
    --------
    >>> import numpy as np
    >>> import dask.array as da

    >>> x = np.array([1, 1, 2, 3, 3, 3, 2, 1, 1])
    >>> x = da.from_array(x, chunks=5)
    >>> def derivative(x):
    ...     return x - np.roll(x, 1)

    >>> y = x.map_overlap(derivative, depth=1, boundary=0)
    >>> y.compute()
    array([ 1,  0,  1,  1,  0,  0, -1, -1,  0])

    >>> x = np.arange(16).reshape((4, 4))
    >>> d = da.from_array(x, chunks=(2, 2))
    >>> d.map_overlap(lambda x: x + x.size, depth=1).compute()
    array([[16, 17, 18, 19],
           [20, 21, 22, 23],
           [24, 25, 26, 27],
           [28, 29, 30, 31]])

    >>> func = lambda x: x + x.size
    >>> depth = {0: 1, 1: 1}
    >>> boundary = {0: 'reflect', 1: 'none'}
    >>> d.map_overlap(func, depth, boundary).compute()  # doctest: +NORMALIZE_WHITESPACE
    array([[12,  13,  14,  15],
           [16,  17,  18,  19],
           [20,  21,  22,  23],
           [24,  25,  26,  27]])
    """
    # Look for invocation using deprecated single-array signature
    # map_overlap(x, func, depth, boundary=None, trim=True, **kwargs)
    if isinstance(func, Array) and callable(args[0]):
        warnings.warn(
            "Detected use of signature map_overlap(x, func) rather than "
            "map_overlap(func, *args) for multi-array support. Arguments "
            "will be swapped in this case but such an exception will not "
            "be made in a future release.",
            FutureWarning,
        )
        sig = ["func", "depth", "boundary", "trim"]
        depth = get(sig.index("depth"), args, depth)
        boundary = get(sig.index("boundary"), args, boundary)
        trim = get(sig.index("trim"), args, trim)
        func, args = args[0], [func]

    if not callable(func):
        raise TypeError(
            "First argument must be callable function, not {}\n"
            "Usage:   da.map_overlap(function, x)\n"
            "   or:   da.map_overlap(function, x, y, z)".format(type(func).__name__)
        )
    if not all(isinstance(x, Array) for x in args):
        raise TypeError(
            "All variadic arguments must be arrays, not {}\n"
            "Usage:   da.map_overlap(function, x)\n"
            "   or:   da.map_overlap(function, x, y, z)".format(
                [type(x).__name__ for x in args]
            )
        )

    # Coerce depth and boundary arguments to lists of individual
    # specifications for each array argument
    def coerce(xs, arg, fn):
        if not isinstance(arg, list):
            arg = [arg] * len(xs)
        return [fn(x.ndim, a) for x, a in zip(xs, arg)]

    depth = coerce(args, depth, coerce_depth)
    boundary = coerce(args, boundary, coerce_boundary)

    # Align chunks in each array to a common size
    if align_arrays:
        # Reverse unification order to allow block broadcasting
        inds = [list(reversed(range(x.ndim))) for x in args]
        _, args = da.core.unify_chunks(*list(concat(zip(args, inds))), warn=False)

    for i, x in enumerate(args):
        for j in range(x.ndim):
            if isinstance(depth[i][j], tuple) and boundary[i][j] != "none":
                raise NotImplementedError(
                    "Asymmetric overlap is currently only implemented "
                    "for boundary='none', however boundary for dimension "
                    "{} in array argument {} is {}".format(j, i, boundary[i][j])
                )

    def assert_int_chunksize(xs):
        assert all(type(c) is int for x in xs for cc in x.chunks for c in cc)

    assert_int_chunksize(args)
    args = [overlap(x, depth=d, boundary=b) for x, d, b in zip(args, depth, boundary)]
    assert_int_chunksize(args)
    x = da.map_blocks(func, *args, **kwargs)
    assert_int_chunksize([x])
    if trim:
        # Find index of array argument with maximum rank and break ties by choosing first provided
        i = sorted(enumerate(args), key=lambda v: (v[1].ndim, -v[0]))[-1][0]
        # Trim using depth/boundary setting for array of highest rank
        return trim_internal(x, depth[i], boundary[i])
    else:
        return x

def coerce_depth(ndim, depth):
    default = 0
    if depth is None:
        depth = default
    if isinstance(depth, Integral):
        depth = (depth,) * ndim
    if isinstance(depth, tuple):
        depth = dict(zip(range(ndim), depth))
    if isinstance(depth, dict):
        for i in range(ndim):
            if i not in depth:
                depth.update({i: 0})
    return depth

def coerce_boundary(ndim, boundary):
    default = "reflect"
    if boundary is None:
        boundary = default
    if not isinstance(boundary, (tuple, dict)):
        boundary = (boundary,) * ndim
    if isinstance(boundary, tuple):
        boundary = dict(zip(range(ndim), boundary))
    if isinstance(boundary, dict):
        for i in range(ndim):
            if i not in boundary:
                boundary.update({i: default})
    return boundary

def dataframe_to_dataset(df: DataFrame, dim, compute_lengths=True, add_coord=False):
    # Watch: https://github.com/pydata/xarray/issues/3929
    data_vars = {}
    for c in df:
        # Convert Dask Series to xr.DataArray with no coordinates
        # Use lengths=True to include chunk size calculations
        arr = df[c].to_dask_array(lengths=True if compute_lengths else None)
        data_vars[c] = xr.DataArray(arr, dims=dim)
    coords = {dim: da.arange(len(df))} if add_coord else None
    return xr.Dataset(data_vars, coords=coords)


"""
API endpoints for the global teacher campus sync
"""
from django.conf import settings
from django.utils.translation import get_language

from cms.api import add_plugin
from cms.models import Page
from rest_framework.decorators import api_view
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from rest_framework.serializers import as_serializer_error
from richie.apps.core.helpers import create_i18n_page
from richie.apps.courses.models import Course, Organization
from richie.apps.courses.utils import get_signature

def create_course(data):
    """
    Takes input data in the form of a dictionary and uses it to create a new course
    object along with its associated page, roles, and permissions. The input data must include
    'course_code', 'course_title', and 'organization_code'. If any of these fields are missing in
    the input data, a `ValidationError` is raised.

    Args:
        data (dict): A dictionary containing course creation data with keys:
            - 'course_code' (str): The code or identifier for the course.
            - 'course_title' (str): The title of the course.
            - 'organization_code' (str): The code or identifier of the organization associated with
              the course.

    Returns:
        None

    Raises:
        ValidationError: If any of the required fields are missing in the input data.

    Example:
        # Input data for course creation
        data = {
            "course_code": "CS101",
            "course_title": "Introduction to Computer Science",
            "organization_code": "ORG123"
        }

        # Create a new course based on the input data
        create_course(data)
    """
    try:
        code = data["course_code"]
        title = data["course_title"]
        organization_code = data["organization_code"]
    except KeyError:
        raise ValidationError(
            {"input_data": ["You must pass course_code, course_title and organization_code."]}
        )

    courses_page = Page.objects.get(reverse_id="courses", publisher_is_draft=True)
    org_obj = Organization.objects.get(code=organization_code.upper())
    organization_page = Page.objects.get(organization=org_obj.id, publisher_is_draft=True)

    page_obj = create_i18n_page(
        title,
        in_navigation=False,
        languages=None,
        parent=courses_page,
        reverse_id=None,
        template=Course.PAGE["template"],
    )

    course_obj = Course.objects.create(
        extended_object=page_obj, code=code
    )
    course_obj.create_page_role()

    # Add a plugin for the organization
    placeholder = course_obj.extended_object.placeholders.get(
        slot="course_organizations"
    )
    add_plugin(
        language=get_language(),
        placeholder=placeholder,
        plugin_type="OrganizationPlugin",
        **{"page": organization_page},
    )
    course_obj.create_permissions_for_organization(org_obj)

@api_view(["POST"])
def create_courses_from_request(request, version):
    """
    Create courses based on a request.

    This function takes an HTTP request, validates the authentication provided in the request header,
    exactly as the sync_course_runs_from_request in upstream richie does and attempts to create courses
    using the request data. If successful, it returns a response with a status code of 200.
    If there are validation errors or authentication issues, it returns an appropriate response with
    the corresponding status code.

    Args:
        request (HttpRequest): The HTTP request containing course creation data.
        version (str): The version of the request data. Ignored for the time being

    Returns:
        Response: A Django Response object containing the result of the course creation process.

    Raises:
        N/A

    Example:
        # Create courses from an HTTP POST request
        response = create_courses_from_request(request, "1.0")
    """
    message = request.body.decode("utf-8")

    authorization_header = request.headers.get("Authorization")
    if not authorization_header:
        return Response("Missing authentication.", status=403)

    signature_is_valid = any(
        authorization_header == get_signature(message, secret)
        for secret in getattr(settings, "RICHIE_COURSE_RUN_SYNC_SECRETS", [])
    )

    if not signature_is_valid:
        return Response("Invalid authentication.", status=401)

    result = {}
    status = 200
    try:
        create_course(request.data)
    except ValidationError as error:
        result["error"] = as_serializer_error(error)
        status = 400

    return Response(result, status=status)


# Method to iterate through an image collection and merge images that are found within the
# same 16-day LANDSAT passover chunk.

import ee
import copy

ee.Initialize()

def mergeByChunk(imgcoll):
    imgcoll = imgcoll.toList(imgcoll.size())
    seq = ee.List.sequence(0, imgcoll.size().subtract(1), 1)

    def addID(item, imglist):
        new_img = ee.Image(imgcoll.get(ee.Number(item)))
        new_img = new_img.set({"Merge": ee.Number(imgcoll.size()).add(1),
                               "Amnt_Merge": ee.Number(1),
                               "ID": ee.Number(item)})
        return ee.List(imglist).add(ee.Image(new_img))

    tl = seq.iterate(addID, ee.List([]))
    templist = copy.deepcopy(tl)
    tl = ee.ImageCollection.fromImages(tl)
    list_max = ee.Number(tl.first().get("Merge"))

    def iterator(img, acc_list):

        img = ee.Image(img)
        placeholder = (ee.Image(0)
                       .set({"Centroid": 0, "Chunk": 20000,
                             "Merge": list_max, "Day": 4000,
                             "ID": 1000000000, "CLOUD_COVER_LAND": 200,
                             "SATELLITE": "MODIS"}))

        first_place = ee.Number(img.get("ID")).eq(0)

        diff_chunk = (ee.Number(ee.Image(ee.List(acc_list).get(-1)).get("Chunk"))
                                         .neq(ee.Number(img.get("Chunk"))))
        prev_img = (ee.List(acc_list).get(-1))

        prev = ee.Image(ee.Algorithms.If(first_place,
                                         placeholder,
                                         ee.Algorithms.If(diff_chunk,
                                                          placeholder,
                                                          prev_img)))
        prev = ee.Image(prev)

        last_place = (ee.Number(img.get("ID"))
                      .eq(ee.Number(ee.List(templist).size())
                          .subtract(1)))

        next_img = (ee.Image(ee.List(templist).get(ee.Number(img.get("ID")).add(1))))

        diff_chunk = (ee.Number(img.get("Chunk"))
                      .neq(ee.Number(next_img.get("Chunk")))
                      .Or(ee.String(img.get("SATELLITE"))
                                    .compareTo(next_img.get("SATELLITE")).neq(0)))

        aft = ee.Image(ee.Algorithms.If(last_place,
                                        placeholder,
                                        ee.Algorithms.If(diff_chunk,
                                                         placeholder,
                                                         next_img)))

        already_merged = (ee.Number(img.get("ID")).eq(ee.Number(prev.get("Merge")))
                          .Or(ee.String(img.get("SATELLITE"))
                              .compareTo(ee.String(prev.get("SATELLITE"))).neq(0)))

        same_chunk = ee.Number(prev.get("Chunk")).eq(ee.Number(img.get("Chunk")))

        img_merge = ee.Image(ee.Algorithms.If(already_merged,
                                              img,
                                              ee.Algorithms.If(same_chunk,
                                                               prev,
                                                               aft)))

        less_merge = (ee.Number(img.get("Amnt_Merge"))
                         .gt(ee.Number(img_merge.get("Amnt_Merge"))))

        amnt = ee.Number(ee.Algorithms.If(less_merge,
                                          ee.Number(img.get("Amnt_Merge")),
                                          ee.Number(img_merge.get("Amnt_Merge"))))

        img_m_c = ee.Number(img_merge.get("CLOUD_COVER_LAND"))
        img_c = ee.Number(img.get("CLOUD_COVER_LAND"))

        conditions = (ee.Number(img.get("ID")).eq(ee.Number(img_merge.get("ID")))
                      .Or(ee.Number(img.get("Chunk"))
                          .neq(ee.Number(img_merge.get("Chunk")))))

        props_keep = ["Chunk", "system:index", "system:time_start", "SATELLITE",
                      "Day", "Year", "CLOUD_COVER_LAND", "LANDSAT_ID"]
        props_alt = {"ID": img.get("ID"), "Merge": img_merge.get("ID"),
                    "Amnt_Merge": amnt.add(1),
                    "Merged_Satellites": ee.List(img.get("Merged_Satellites")).cat(ee.List(img_merge.get("Merged_Satellites")))}

        img_top = (ee.ImageCollection.fromImages([img_merge, img]).mosaic()
                   .copyProperties(img, props_keep)
                   .set(props_alt))
        merge_top = (ee.ImageCollection.fromImages([img, img_merge]).mosaic()
                   .copyProperties(img_merge, props_keep)
                   .set(props_alt))

        new_img = ee.Image(ee.Algorithms.If(conditions,
                                            img,
                                            ee.Algorithms.If(img_m_c.gt(img_c),
                                                             img_top,
                                                             merge_top)))
        new_img = new_img.set({"Day": ee.Number(new_img.get("Chunk")).multiply(16)})

        already_merged = ee.Number(img.get("ID")).eq(ee.Number(prev.get("Merge")))
        gotta_pop = ee.Number(new_img.get("Merge")).eq(ee.Number(prev.get("ID")))

        popped_list = (ee.List(acc_list)
                       .slice(0, ee.Number(ee.List(acc_list).size())
                              .subtract(1)).add(ee.List(new_img)))
        added_list = (ee.List(acc_list).add(ee.List(new_img)))

        acc_list = ee.List(ee.Algorithms.If(already_merged,
                                            acc_list,
                                            ee.Algorithms.If(gotta_pop,
                                                             popped_list,
                                                             added_list)))
        return acc_list

    comb = ee.List(tl.iterate(iterator, ee.List([])))
    comb = ee.ImageCollection.fromImages(comb)
    return comb


import argparse
import os
from datetime import datetime as dt
from time import sleep
import estClient.est.errors as est_errors
import estClient.est.client as est_client
import OpenSSL.crypto as openssl

NUM_TRY_LATER_ATTEMPTS = 2  # Number of times request should be resent on HTTP 202-Try Later

class bcolors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

PASSED = bcolors.OKGREEN + 'Passed' + bcolors.ENDC
FAILED = bcolors.FAIL + 'Failed' + bcolors.ENDC

class NmosEst(object):
    def __init__(self, host, port,
                 cacert_path,
                 client_cert_path,
                 client_key_path):
        """
        Class used to provision an NMOS Node with the Root CA and TLS Server certificate, can also be used
        to renew both when expiring.
        If a valid TLS server certificate for the current domain is not present, the externally issued client
        certificate will be used for authentication with the EST server.
        """
        self.estClient = est_client.Client(host, port, implicit_trust_anchor_cert_path=False)
        self.cacert_path = cacert_path
        self.client_cert_path = client_cert_path
        self.client_key_path = client_key_path
        self.try_later_attempts = NUM_TRY_LATER_ATTEMPTS

    def isCertValid(self, cert_data):
        """Check that the TLS certificate is valid, by checking the expiry date and domain for server certificate

        Args:
            cert_data: String of certificate data

        Returns:
            True if certificate passes all tests
        """
        cert = openssl.load_certificate(openssl.FILETYPE_PEM, cert_data)

        if cert.has_expired():
            print('Certificate has expired')
            return False

        return True

    def verifyNmosCert(self, cert_data):
        """Check that the TLS certificate is valid for use as an NMOS Server certificate.
        Validates the date and domain and certificate usage

        Args:
            cert_data: String of certificate data

        Returns:
            True if certificate passes all tests
        """
        test_results = {
            "basic_validation": False,
            "extended_key_usage": False
        }

        test_results['basic_validation'] = self.isCertValid(cert_data)

        cert = openssl.load_certificate(openssl.FILETYPE_PEM, cert_data)

        for i in range(cert.get_extension_count()):
            if cert.get_extension(i).get_short_name() == b'extendedKeyUsage':
                print('Certificate Extended Key usage:\n \
                      {}'.format(cert.get_extension(i)))
                test_results['extended_key_usage'] = True
                # if str(cert.get_extension(i)) == 'TLS Web Server Authentication, TLS Web Client Authentication':
                #     test_results['extended_key_usage'] = True
                # else:
                #     print(f'Certificate does not support Extended Key usage for both Web Server and Web Clients: \
                #           {cert.get_extension(i)}')

        cert_is_valid = True
        for test in test_results:
            result = (PASSED if test_results[test] else FAILED)
            print('Test {}: {}'.format(test, result))
            if not test_results[test]:
                cert_is_valid = False

        return cert_is_valid

    def inspectCert(self, cert_data):
        """Print information about the certificate

        Args:
            cert_data: String of certificate data

        Returns:
            None
        """

        cert = openssl.load_certificate(openssl.FILETYPE_PEM, cert_data)

        print('CA Issuer: {}'.format(cert.get_issuer().get_components()[0][1]))
        print('Cert Subject: {}'.format(cert.get_subject()))

        expiry_date = cert.get_notAfter()
        expiry_date = self.convertAsn1DateToString(expiry_date)

        print('Expiry Date: {}'.format(expiry_date))
        print('Version Number: {}'.format(cert.get_version()))
        print('Extensions:')
        for i in range(cert.get_extension_count()):
            print('   {}'.format(cert.get_extension(i)))

    def inspectCsr(self, csr_data):
        """Print information about the Certificate Signing Request PKCS10

        Args:
            csr_data: String of CSR data

        Returns:
            None
        """
        csr = openssl.load_certificate_request(openssl.FILETYPE_PEM, csr_data)

        print('Cert Subject: {}'.format(csr.get_subject()))

        print('Version Number: {}'.format(csr.get_version()))
        print('Extensions:')
        for ext in csr.get_extensions():
            print('   {}: {}'.format(ext.get_short_name(), ext))

    def convertAsn1DateToString(self, asn1_date):
        """Convert ASN.1 formated date (YYYYMMDDhhmmssZ) to datatime object"""
        if isinstance(asn1_date, bytes):
            # Convert bytes to string
            asn1_date = asn1_date.decode('utf-8')

        # Strip tailing Z from string
        if asn1_date[-1:] == 'Z':
            asn1_date = asn1_date[:-1]

        # Covert to date object
        date = dt.strptime(asn1_date, '%Y%m%d%H%M%S')

        return date

    def _createCsr(self, common_name, subject_alt_name=None, cipher_suite='rsa_2048', private_key=None):
        """Create CSR and private key

        Args:
            common_name: Common name to be included in the certificate

            subject_alt_name: Subject alternative name to be included in the certificate
                eg. b'DNS:test.com'

            cipher_suite: Cipher suite to be used to sign the CSR. rsa_2048 and ecdsa supported

        Returns:
            private_key, csr
        """
        country = 'GB'
        city = 'Manchester'
        organization = 'AMWA'
        organizational_unit = 'ENG'
        email_address = 'test@workshop.nmos.tv'
        private_key, csr = self.estClient.create_csr(common_name,
                                                     country=country,
                                                     city=city,
                                                     organization=organization,
                                                     organizational_unit=organizational_unit,
                                                     email_address=email_address,
                                                     cipher_suite=cipher_suite,
                                                     subject_alt_name=subject_alt_name,
                                                     private_key=private_key)

        # Add OID extension for certificate profile
        # profile_ext = openssl.X509Extension(b'1.3.6.1.4.1.311.20.2', False, b'pc-client')
        # csr.add_extensions([profile_ext])

        self.inspectCsr(csr)

        return private_key, csr

    def setImplictTrustAnchor(self, path):
        self.estClient.implicit_trust_anchor_cert_path = path
        self.cacert_path = path

    def getCaCert(self, newCaPath):
        """Get CA Cert, but do not verify the authenticity of the EST server"""
        try:
            ca_cert = self.estClient.cacerts()
        except est_errors.RequestError as e:
            print("Failed to get Root CA from EST Server")
            print(e)
            return False

        self._writeDataToFile(ca_cert, newCaPath)

        # Use latest Root CA for future requests
        self.setImplictTrustAnchor(newCaPath)

        return True

    def getNewCert(self, hostname, newCertPath, newKeyPath, cipher_suite='rsa_2048'):
        """
        Get a new TLS certificate from EST, using externally issued certificate for authentication with EST server
        """

        # Get CSR attributes from EST server as an OrderedDict.
        # csr_attrs = self.estClient.csrattrs()

        private_key, csr = self._createCsr(hostname, cipher_suite=cipher_suite)

        client_cert = (self.client_cert_path, self.client_key_path)

        cert_response = self._request_cert(self.estClient.simpleenroll, csr, client_cert)
        if not cert_response:
            print('Failed to request new TLS certificate')
            print('for host: {} with cipher suite: {}'.format(hostname, cipher_suite))
            return False

        # Append Chain of trust
        with open(self.cacert_path) as f:
            cert_response += f.read()

        self._writeDataToFile(private_key, newKeyPath)
        self._writeDataToFile(cert_response, newCertPath)

        self.inspectCert(cert_response)
        self.verifyNmosCert(cert_response)

        return True

    def renewCert(self, hostname, newCertPath, newKeyPath, cipher_suite='rsa_2048'):
        """
        Renew existing TLS certificate, using current certificate for authentication with EST server
        """

        # Get CSR attributes from EST server as an OrderedDict.
        # csr_attrs = self.estClient.csrattrs()

        private_key, csr = self._createCsr(hostname, cipher_suite=cipher_suite)

        client_cert = (self.client_cert_path, self.client_key_path)

        cert_response = self._request_cert(self.estClient.simplereenroll, csr, client_cert)
        if not cert_response:
            print('Failed to renew TLS certificate')
            return False

        self._writeDataToFile(private_key, newKeyPath)
        self._writeDataToFile(cert_response, newCertPath)

        self.inspectCert(cert_response)
        self.verifyNmosCert(cert_response)

        return True

    def serverKeyGen(self, hostname, newCertPath, newKeyPath, cipher_suite='rsa_2048'):
        """
        Get new certificate with the EST server generating the key pair
        """

        private_key_unused, csr = self._createCsr(hostname, cipher_suite=cipher_suite)

        client_cert = (self.client_cert_path, self.client_key_path)

        cert_response, private_key = self._request_cert(self.estClient.serverkeygen, csr, client_cert)
        if not cert_response or not private_key:
            print('Failed to renew TLS certificate')
            return False

        self._writeDataToFile(private_key, newKeyPath)
        self._writeDataToFile(cert_response, newCertPath)

        self.inspectCert(cert_response)
        self.verifyNmosCert(cert_response)

        return True

    def _request_cert(self, method, *args):
        """Perform request and handle errors"""
        success = False
        for x in range(self.try_later_attempts):
            try:
                returned_cert = method(*args)
                success = True
                break
            except est_errors.TryLater as e:
                print('Try request certificate again in {} seconds'.format(e.seconds))
                sleep(e.seconds)
            except est_errors.RequestError as e:
                print("Failed to get TLS Certificate from EST Server")
                print(e)
                return False
        if not success:
            return False

        return returned_cert

    def _writeDataToFile(self, data, path, private=False):
        """Write data to file path"""
        if not isinstance(data, str) and not isinstance(data, bytes):
            print('Cert data is not a string or bytes, file: {}'.format(path))
            return False

        if isinstance(data, bytes):
            # Convert bytes to string
            data = data.decode('utf-8')

        with open(path, 'w') as f:
            f.write(data)

        if private:
            os.chmod(path, 0o400)

if __name__ == "__main__":

    """
    Example workflow of NMOS EST class
    1. Discover EST server (config file or DNS-SD)
    2. Request root CA
    3. Request RSA cert using external client cert
    4. Request ECDSA cert using external client cert
    4. Renew both certs
    """

    parser = argparse.ArgumentParser()
    parser.add_argument("--ip", default='bbc-0.workshop.nmos.tv')
    parser.add_argument("--port", type=int, default=8443)
    parser.add_argument("--cacert", default='cacert.pem')
    parser.add_argument("--cert", default='certs/man1.ecdsa.product1.cert.chain.pem')
    parser.add_argument("--key", default='certs/man1.ecdsa.product1.key.pem')
    args = parser.parse_args()

    # Location of EST Server
    host = args.ip
    port = args.port

    ca_cert_path = args.cacert
    client_cert_path = args.cert
    client_key_path = args.key

    print('Using EST Server {}:{}'.format(host, port))
    print('Root CA: {}'.format(ca_cert_path))
    print('External Certificate: {}'.format(client_cert_path))
    print('External Private Key: {}'.format(client_key_path))

    nmos_est_client = NmosEst(host, port, None, client_cert_path, client_key_path)

    # Get latest EST server CA certs.
    if not nmos_est_client.getCaCert(ca_cert_path):
        print('Exiting...')
        exit(1)

    # Request TLS Server certificate from EST server, using manufacturer issued client certificate for authentication
    if not nmos_est_client.getNewCert('camera-1.workshop.nmos.tv', f'rsa.test.pem.crt', f'rsa.test.pem.key',
                                      cipher_suite='rsa_2048'):
        print('Exiting...')
        exit(1)


import asyncio
import aiohttp
import re
import itertools
from util import *

class Scouter:
    def __init__(self, replays=list, names=list):
        self.names = set(names)
        self.replay_list = replays
        self.valid_replays = 0
        self.replay_data = self.retrieve_data(replays)
        self.mon_data = []
        self.completed_data = []
        self.mon_usage = {}
        self.mon_duos = {}

    def get_data(self):
        print(self.completed_data)

    def find_replay(self, replayid):
        for item in self.replay_list:
            if (item.find(replayid) > -1):
                return item[:item.find('.json')]
        return "No Replay Availible."

    def find_mon(self, name):
        for mon in self.mon_data:
            if mon.get_name() ==  name:
                return mon
        new_mon = Pokemon(name)
        self.mon_data.append(new_mon)
        return new_mon

    def retrieve_data(self, urls):
        #Some Windows thing: https://stackoverflow.com/questions/45600579/asyncio-event-loop-is-closed-when-getting-loop
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
        return [item for item in asyncio.run(main(urls)) if item and item != 'Could not connect']

    @staticmethod
    def format_string(text: str):
        text = re.sub('\\[u][a-zA-Z0-9]{4}', '', text)
        text = re.sub('[^a-zA-Z0-9]+', '', text)
        return text.lower().replace(' ', '')

    def add_action(self, team, actions, winner, data):
        team_arr = []
        picked_mon = ''

        if len(actions) > 0:
            picked_mon = actions[0]
            if len(actions) > 1:
                completed_actions = list(set(actions[1:len(actions)]))
            else:
                completed_actions = []
        else:
            completed_actions = []

        for mon in team:
            team_arr.append(mon)
            if ((mon == picked_mon) or (mon == 'Urshifu-*' and re.match('Urshifu', picked_mon)) or (mon == 'Silvally' and re.match('Silvally', picked_mon)) or (mon == 'Arceus' and re.match('Arceus', picked_mon))):
                team_arr.append(completed_actions)

        team_arr.append({True: 'W', False: 'L'}[winner])
        team_arr.append(self.find_replay(data[0]))

        return team_arr

    def sort_data(self):
        for item in self.replay_data:
            win_start = find_after(item, '|win')
            winner = item[win_start:item.find('\\n', win_start)]
            data_id = re.findall('id":"(.+?)"', item)
            if data_id[1] in self.names:
                player_index = 1
            elif data_id[2] in self.names:
                player_index = 2
            else:
                continue

            item = item.replace('\\n', '')

            preview_data = re.findall('p' + str(player_index) + '\|(.+?)\|', item)
            team_preview = preview_data[2:len(preview_data)]
            team_actions = re.findall('\|p' + str(player_index) + 'a: .+?\|(.+?)\|', item)

            team_preview = [re.sub(',.+', '', mon) for mon in team_preview]
            #Mons with multiple forms edge case
            if ('Genesect-*' in team_preview):
                team_preview[team_preview.index('Genesect-*')] = 'Genesect'
            if ('Arceus-*' in team_preview):
                team_preview[team_preview.index('Arceus-*')] = 'Arceus'
            if ('Silvally-*' in team_preview):
                team_preview[team_preview.index('Silvally-*')] = 'Silvally'
            team_preview = [mon for mon in team_preview if find_type(mon) != 'notype']
            team_actions = [re.sub(',.+', '', action) for action in team_actions if re.match('^[A-Z]', action)]

            for duo in itertools.combinations(team_preview, 2):
                duo = tuple(sorted(duo))
                if duo in self.mon_duos:
                    self.mon_duos[duo] += 1
                else:
                    self.mon_duos[duo] = 1

            if (len(team_preview) == 3):
                self.find_mon(team_preview[0]).add_duos((team_preview[1], team_preview[2]))
                self.find_mon(team_preview[1]).add_duos((team_preview[0], team_preview[2]))
                self.find_mon(team_preview[2]).add_duos((team_preview[1], team_preview[0]))

            check_winner = (data_id[player_index] == self.format_string(winner))

            for mon in team_preview:
                if mon in self.mon_usage:
                    self.mon_usage[mon] += 1
                else:
                    self.mon_usage[mon] = 1

                current_mon = self.find_mon(mon)
                current_mon.increment_brought()
                if check_winner:
                    current_mon.increment_brought_won()

            #Explaining the below code for documentation purposes
            #The people who I made the scouter for asked for consise information with regards to Urshifu and Silvally pick/brought data
            #The issue is with these Pokemon is that they have multiple forms, necessitating very specific usage of methods to get
            #The most useful data possible, which leads to this 'mumbo jumbo' for lack of a better word.
            if (len(team_actions) > 0 and find_type(team_actions[0]) != 'notype'):
                #Urshifu Edge Case
                if re.match('Urshifu', (team_actions[0])):
                    urshifu = self.find_mon('Urshifu-*')
                    urshifu.increment_picked()
                    picked_urshifu = self.find_mon(team_actions[0])
                    picked_urshifu.increment_picked()
                    picked_urshifu.increment_brought()
                    if check_winner:
                        urshifu.increment_picked_won()
                        picked_urshifu.increment_brought_won()
                        picked_urshifu.increment_picked_won()
                #Silvally and Arceus Edge case
                elif re.match('Silvally-', team_actions[0]):
                    silvally = self.find_mon('Silvally')
                    silvally.increment_picked()
                    picked_val = self.find_mon(team_actions[0])
                    picked_val.increment_picked()
                    picked_val.increment_brought()
                    if check_winner:
                        urshifu.increment_picked_won()
                        picked_val.increment_brought_won()
                        picked_val.increment_picked_won()
                elif re.match('Arceus-', team_actions[0]):
                    arceus = self.find_mon('Arceus')
                    arceus.increment_picked()
                    picked_arc = self.find_mon(team_actions[0])
                    picked_arc.increment_picked()
                    picked_arc.increment_brought()
                    if check_winner:
                        arceus.increment_picked_won()
                        picked_arc.increment_brought_won()
                        picked_arc.increment_picked_won()
                else:
                    self.find_mon(team_actions[0]).increment_picked()
                    if check_winner:
                         self.find_mon(team_actions[0]).increment_picked_won()

            #print(f'{team_preview}\n{team_actions}\n{check_winner}\n{data_id}\n\n')

            self.completed_data.append(self.add_action(team_preview, team_actions, check_winner, data_id))
            self.valid_replays += 1

    def get_complete_data(self):
        self.sort_data()
        full_data = {}

        if len(self.completed_data) > 0:
            full_data['replays'] = (sorted(self.completed_data, key=lambda item: item[len(item) -1], reverse=True))
            if len(self.mon_data) > 0:
                sorted_mons = sorted(self.mon_data, key=lambda item: item.brought_count, reverse=True)
                usage_data = {}
                for mon in sorted_mons:
                    usage_data[mon.get_name()] = mon.compile_data(self.valid_replays)

                full_data['usage'] = usage_data

        return full_data

class Pokemon:
    def __init__(self, name):
        self.name = name
        self.brought_count = 0
        self.brought_won = 0
        self.picked_count = 0
        self.picked_won = 0
        self.type = find_type(self.name)
        self.teammates = {}
        self.teammate_duos = {}
        self.type_duos = {}

    def get_name(self):
        return self.name

    def get_type(self):
        return self.type

    def get_brought_count(self):
        return self.brought_count

    def get_picked_count(self):
        return self.picked_count

    def get_teammates(self):
        return self.teammates

    def add_teammate(self, name=str):
        if name in self.teammates:
            self.teammates[name] += 1
        else:
            self.teammates[name] = 1

    def add_duos(self, duo=tuple):
        duo = tuple(sorted(duo))
        if duo in self.teammate_duos:
            self.teammate_duos[duo] += 1
        else:
            self.teammate_duos[duo] = 1

        self.add_teammate(duo[0])
        self.add_teammate(duo[1])

        type_duo = (find_type(duo[0]), find_type(duo[1]))

        for found_type in type_duo[0]:
            for other_found_type in type_duo[1]:
                type_combo = tuple(sorted([found_type, other_found_type]))
                if type_combo in self.type_duos:
                    self.type_duos[type_combo] += 1
                else:
                    self.type_duos[type_combo] = 1

    def increment_brought(self):
        self.brought_count += 1

    def increment_brought_won(self):
        self.brought_won += 1

    def increment_picked(self):
        self.picked_count += 1

    def increment_picked_won(self):
        self.picked_won += 1

    def make_json_valid(self, item:dict):
        return dict([(f'{item[0][0]} / {item[0][1]}', item[1]) for item in item.items()])

    def compile_data(self, value):
        if self.brought_count > 0:
            self.type_duos = self.make_json_valid(self.type_duos)
            self.teammate_duos = self.make_json_valid(self.teammate_duos)

            try:
                return ({'brought': self.brought_count, 'brought_percentage': round((self.brought_count/value) * 100, 2), 'brought_win_percentage': round((self.brought_won/self.brought_count) * 100, 2),
                'picked': self.picked_count, 'brought_picked_percentage': round((self.picked_count/self.brought_count) * 100, 2), 'picked_win_percentage': round((self.picked_won/self.picked_count) * 100, 2),
                'teammates': sort_dict(self.teammates), 'teammate_duos': sort_dict(self.teammate_duos), 'type_duos': sort_dict(self.type_duos)})

            except ZeroDivisionError:
                return ({'brought': self.brought_count, 'brought_percentage': round((self.brought_count/value) * 100, 2), 'brought_win_percentage': round((self.brought_won/self.brought_count) * 100, 2),
                'picked': self.picked_count, 'brought_picked_percentage': round((self.picked_count/self.brought_count) * 100, 2), 'picked_win_percentage': 0,
                'teammates': sort_dict(self.teammates), 'teammate_duos': sort_dict(self.teammate_duos), 'type_duos':sort_dict(self.type_duos)})
        else:
            return ({'brought': 0, 'brought_percentage': 0, 'brought_win_percentage': 0, 'picked': 0,
            'brought_picked_percentage': 0, 'picked_win_percentage': 0, 'teammates': {}, 'teammate_duos': {}, 'type_duos':{}})

# Two async commands is basic web scrapping. Check out https://youtu.be/lUwZ9rS0SeM?t=234
async def get_page(session, url):
    try:
        async with session.get(url) as r:
            return await r.text()
    except:
        pass

async def main(urls):
    async with aiohttp.ClientSession() as session:
        return await asyncio.gather(*(get_page(session, url) for url in urls))

def get_data(form_text, players):
    if (players.find(',') > -1):
        players = {Scouter.format_string(p) for p in players.split(', ')}
    else:
        players = [Scouter.format_string(players)]
    replays_list = [f'{replay.rstrip()}.json' for replay in form_text.split('\n') if re.match('^https://replay.pokemonshowdown.com.+?gen.1v1', replay)]
    REPLAY_SCOUTER = Scouter(replays_list, players)
    return (REPLAY_SCOUTER.get_complete_data())

def convertUsage(usage_data):
    try:
        usage_data = usage_data['usage']
        usage_data = dict(sorted(usage_data.items(),key=lambda item: item[1]['brought'],reverse=True))
        returned_string = ''
        for key, value in usage_data.items():
            returned_string += ('+----------------------------------------+')
            returned_string += (f'\n{box_string(42, key)}')
            returned_string += ('\n+----------------------------------------+')
            message = (f'Brought: {value["brought"]} ({value["brought_percentage"]}%)')
            returned_string += (f'\n{box_string(42, message)}')
            message = (f'Won when brought: {value["brought_win_percentage"]}%')
            returned_string += (f'\n{box_string(42, message)}')
            message = (f' - Picked: {value["picked"]} (Win %: {value["picked_win_percentage"]}%)')
            returned_string += (f'\n{box_string(42, message)}')
            returned_string += ('\n+----------------------------------------+')
            returned_string += (f'\n{box_string(42, "[Teammates]")}')
            returned_string += ('\n+----------------------------------------+')
            for key1, value1 in value['teammates'].items():
                returned_string += (f'\n{box_string(42, key1, str(value1))}')
            returned_string += ('\n+----------------------------------------+')
            returned_string += (f'\n{box_string(42, "[Type Duos]")}')
            returned_string += ('\n+----------------------------------------+')
            for key1, value1 in value['type_duos'].items():
                returned_string += (f'\n{box_string(42, key1, str(value1))}')
            returned_string += ('\n+----------------------------------------+')
            returned_string += ('\n\n')
        return returned_string
    except:
        return 'Failed to Parse.'


from __future__ import annotations

import itertools
import logging
import re

from requests import Response
from typing import TYPE_CHECKING, Union
from collections import defaultdict

from dkutils.constants import (
    API_DELETE,
    API_GET,
    API_POST,
    API_PUT,
)

from dkutils.datakitchen_api.vault import Vault

if TYPE_CHECKING:
    from .datakitchen_client import DataKitchenClient

logger = logging.getLogger(__name__)

class Kitchen:

    def __init__(self, client: DataKitchenClient, name: str) -> None:
        """
        Kitchen object for performing kitchen related API requests.

        Parameters
        ----------
        client : DataKitchenClient
            Client for making requests.
        name : str
            Name of existing kitchen.
        """
        self._client = client
        self._name = name
        self._parent_name = None

    @property
    def name(self) -> str:
        """
        Kitchen name
        """
        return self._name

    @property
    def parent_name(self) -> str:
        """
        Parent kitchen name
        """
        if self._parent_name is None:
            self._parent_name = self._get_settings()['kitchen']['parent-kitchen']
        return self._parent_name

    def is_ingredient(self) -> bool:
        """
        Return true if this is an ingredient kitchen, False otherwise.

        Returns
        -------
        bool
            True if this kitchen is an ingredient kitchen, False otherwise.
        """
        match = re.match(pattern=r'(?P<parent_name>\w+)_(?P<uuid>\w{32})', string=self._name)
        return match.group('parent_name') == self.parent_name if match else False

    @staticmethod
    def create(
            client: DataKitchenClient,
            parent_kitchen_name: str,
            new_kitchen_name: str,
            description: str = ''
    ) -> Kitchen:
        """
        Create a new kitchen and return a kitchen object

        Parameters
        ----------
        client : DataKitchenClient
        parent_kitchen_name : str
            New kitchen will be a child of this parent kitchen
        new_kitchen_name : str
            New kitchen name
        description : str
            New kitchen description

        Returns
        -------
        Kitchen
            :class:`Kitchen <Kitchen>` object

        Raises
        ------
        HTTPError
            If the request fails.
        """
        logger.debug(f'Creating child kitchen of {parent_kitchen_name} named {new_kitchen_name}...')
        client._api_request(
            API_PUT,
            'kitchen',
            'create',
            parent_kitchen_name,
            new_kitchen_name,
            description=description,
        )
        return Kitchen(client, new_kitchen_name)

    def delete(self) -> Response:
        """
        Delete the current kitchen.

        Returns
        -------
        requests.Response
            :class:`Response <Response>` object

        Raises
        ------
        HTTPError
            If the request fails.
        """
        logger.debug(f'Deleting kitchen: {self._name}...')
        return self._client._api_request(API_DELETE, 'kitchen', 'delete', self._name)

    def _get_settings(self) -> dict:
        """
        Retrieve kitchen settings JSON.

        Returns
        -------
        settings : dict

        Raises
        ------
        HTTPError
            If the request fails.
        """
        logger.debug(f'Retrieving settings for kitchen: {self._name}...')
        response = self._client._api_request(API_GET, 'kitchen', self._name)
        return response.json()

    def _update_settings(self, settings: dict) -> Response:
        """
        Update kitchen settings JSON.

        Parameters
        ----------
        settings : dict
            Kitchen settings JSON with updated values.

       Returns
        -------
        requests.Response
            :class:`Response <Response>` object

        Raises
        ------
        HTTPError
            If the request fails.
        ValueError
            If the name in the given settings does not match that of the current kitchen.
        """
        kitchen_name = settings['kitchen']['name']
        if kitchen_name != self._name:
            raise (
                ValueError(
                    f'Name in settings: {kitchen_name} does not match current kitchen: {self._name}'
                )
            )

        response = self._client._api_request(
            API_POST, 'kitchen', 'update', self._name, json={"kitchen.json": settings['kitchen']}
        )
        return response.json()

    def _get_roles(self, settings: dict = None) -> dict:
        """
        Retrieve the staff and their associated roles assigned to this kitchen.

        Parameters
        ----------
        settings : dict, optional
            Dictionary representing kitchen_settings.json.

        Raises
        ------
        HTTPError
            If the request fails.

        Returns
        -------
        roles : dict
            Dictionary of current kitchen staff and their associated roles of the form::

                {
                    'Admin': ['admin@gmail.com'],
                    'Developer': ['developer1@gmail.com', 'developer2@gmail.com'],
                }
        """
        kitchen_settings = settings if settings else self._get_settings()
        kitchen_roles = kitchen_settings['kitchen']['kitchen-roles']
        result = defaultdict(list)
        for email, permission in kitchen_roles.items():
            result[permission].append(email)
        return result

    def _get_staff_set(self, settings: dict = None) -> set:
        """
        Retrieve the set of staff usernames assigned to this kitchen.

        Parameters
        ----------
        settings : dict, optional
            Dictionary representing kitchen_settings.json.

        Raises
        ------
        HTTPError
            If the request fails.

        Returns
        -------
        roles : list
            set of current kitchen staff usernames of the form::

                {
                    'admin@gmail.com',
                    'developer1@gmail.com',
                    'developer2@gmail.com',
                }
        """
        _settings = settings if settings else self._get_settings()
        return set(_settings['kitchen']['kitchen-staff'])

    def _ensure_admin(self, roles: dict = None, settings: dict = None) -> None:
        """
        Ensure the user in the provided client has Admin privileges in this kitchen. Otherwise,
        raise a PermissionError.

        Parameters
        ----------
        roles : dict, optional
            Dictionary keyed by role and valued by list of associated usernames.
        settings : dict, optional
            Dictionary representing kitchen_settings.json.

        Raises
        ------
        HTTPError
            If the request fails.
        PermissionError
            If the client user is not an Admin in this kitchen.
        """
        if not roles:
            roles = self._get_roles(settings=settings)

        current_user = self._client._username
        if current_user not in roles['Admin']:
            raise PermissionError(f'Current user ({current_user}) not an Admin. Permission denied.')

    def _ensure_disjoint(self, lists: list) -> bool:
        """
        Return True if all the provided lists (converted to sets) are disjoint, False otherwise.

        Parameters
        ----------
        lists : list
            List of lists. Each list will be converted to a set to ensure they are all disjoint.

        Returns
        -------
        bool
            True if provided lists are disjoint, False otherwise.
        """
        if len(lists) > 1:
            merged_set = set(lists[0])
            for l in lists[1:]:
                if not merged_set.isdisjoint(set(l)):
                    return False
                merged_set = merged_set | set(l)
        return True

    def get_alerts(self) -> dict:
        """
        Retrieve alerts set on this kitchen.

        Returns
        -------
        alerts : dict
            Dictionary of current kitchen alerts of the form::
                {
                    'Start': ['foo@gmail.com'],
                    'Warning': None,
                    'OverDuration': ['foo@gmail.com', 'bar@gmail.com'],
                    'Success': None,
                    'Failure': ['foo@gmail.com'],
                }
        """
        alerts = self._get_settings()['kitchen']['settings']['alerts']
        return {
            'Start': alerts['orderrunStart'],
            'Warning': alerts['orderrunWarning'],
            'OverDuration': alerts['orderrunOverDuration'],
            'Success': alerts['orderrunSuccess'],
            'Failure': alerts['orderrunError'],
        }

    def add_alerts(self, alerts: dict) -> None:
        """
        Add the provided alerts to the kitchen.

        Parameters
        ----------
        alerts : dict
            Alerts to add in the form::

                {
                    'Start': ['foo@gmail.com'],
                    'Warning': None,
                    'OverDuration': ['foo@gmail.com', 'bar@gmail.com'],
                    'Success': None,
                    'Failure': ['foo@gmail.com'],
                }

        Raises
        ------
        KeyError
            If an unrecognized alert field is provided - valid fields are Start, Warning,
            OverDuration, Success, and Failure
        """

        settings = self._get_settings()
        existing_alerts = settings['kitchen']['settings']['alerts']
        for k, v in alerts.items():
            k = 'orderrunError' if k == 'Failure' else f'orderrun{k}'
            if k not in existing_alerts:
                raise KeyError(
                    'Unrecognized alert field: {k}. Expected fields are Start, Warning, OverDuration, Success, and Failure'  # noqa: E501
                )

            if isinstance(v, str):
                v = [v]

            alert_emails = set(existing_alerts[k]) if existing_alerts[k] else set()
            alert_emails = list(alert_emails.union(set(v)))
            existing_alerts[k] = alert_emails

        self._update_settings(settings)

    def delete_alerts(self, alerts: dict) -> None:
        """
        Delete the provided kitchen alerts.

        Parameters
        ----------
        alerts : dict
            Alerts to delete in the form::

                {
                    'Start': ['foo@gmail.com'],
                    'Warning': None,
                    'OverDuration': ['foo@gmail.com', 'bar@gmail.com'],
                    'Success': None,
                    'Failure': ['foo@gmail.com'],
                }

        Raises
        ------
        KeyError
            If an unrecognized alert field is provided - valid fields are Start, Warning,
            OverDuration, Success, and Failure
        """
        settings = self._get_settings()

        existing_alerts = settings['kitchen']['settings']['alerts']
        for k, v in alerts.items():
            k = 'orderrunError' if k == 'Failure' else f'orderrun{k}'
            if k not in existing_alerts:
                raise KeyError(
                    'Unrecognized alert field: {k}. Expected fields are Start, Warning, OverDuration, Success, and Failure'  # noqa: E501
                )

            if isinstance(v, str):
                v = [v]

            if existing_alerts[k] is not None:
                alert_emails = list(set(existing_alerts[k]) - set(v))
                existing_alerts[k] = alert_emails

        self._update_settings(settings)

    def get_staff(self) -> dict:
        """
        Retrieve the staff and their associated roles assigned to this kitchen.

        Raises
        ------
        HTTPError
            If the request fails.
        PermissionError
            If the current user is not an Admin

        Returns
        -------
        staff : dict
            Dictionary of current kitchen staff and their associated roles of the form::

                {
                    'Admin': ['admin@gmail.com'],
                    'Developer': ['developer1@gmail.com', 'developer2@gmail.com'],
                }
        """
        return self._get_roles()

    def ensure_users_is_part_of_staff(
            self, users_to_check: Union[set, list], current_staff: Union[set, list] = None
    ) -> None:
        """
        Ensure a list of users is part of Kitchen Staff. Otherwise, raise a ValueError.

        Parameters
        ----------
        users_to_check : set or list
            List or set of emails of the users to check if they are part of the staff.
        current_staff : set or list
            List or set of the current staff.
        Raises
        ------
        ValueError
            If the users to check are not part of the Kitchen Staff.

        """
        if not current_staff:
            current_staff = self._get_staff_set()

        if not users_to_check.issubset(current_staff):
            raise ValueError(
                f'The following staff do not already exist in kitchen: {users_to_check - current_staff}'
            )
        pass

    def delete_staff(self, staff_to_delete: Union[set, list]) -> Response:
        """
        Delete the provided staff from this kitchen.

        Parameters
        ----------
        staff_to_delete : set or list
            List or set of usernames to delete from this kitchen's staff.

        Raises
        ------
        HTTPError
            If the request fails.
        PermissionError
            If the current user is not an Admin

        Returns
        -------
        requests.Response
            :class:`Response <Response>` object
        """
        if not isinstance(staff_to_delete, set):
            staff_to_delete = set(staff_to_delete)

        settings = self._get_settings()
        self._ensure_admin(settings=settings)

        current_staff = self._get_staff_set(settings)
        self.ensure_users_is_part_of_staff(staff_to_delete, current_staff)

        # Remove staff from list
        settings['kitchen']['kitchen-staff'] = list(current_staff - staff_to_delete)

        # Remove staff from roles
        for staff in staff_to_delete:
            if staff in settings['kitchen']['kitchen-roles']:
                del settings['kitchen']['kitchen-roles'][staff]

        return self._update_settings(settings)

    def add_staff(self, staff_to_add: dict) -> Response:
        """
        Add the provided staff to this kitchen.

        Parameters
        ----------
        staff_to_add : dict
            Dictionary keyed by role and valued with list of users to add to that role in the form::

                {
                    'Admin': ['admin@gmail.com'],
                    'Developer': ['developer1@gmail.com', 'developer2@gmail.com'],
                }

        Raises
        ------
        HTTPError
            If the request fails.
        PermissionError
            If the current user is not an Admin
        ValueError
            If the provided staff are not new or unique across roles.

        Returns
        -------
        requests.Response
            :class:`Response <Response>` object
        """
        if not self._ensure_disjoint(list(staff_to_add.values())):
            raise ValueError(f'Staff lists for each role must be unique.')

        settings = self._get_settings()
        self._ensure_admin(settings=settings)

        # Add staff to list
        current_staff = self._get_staff_set(settings)
        new_staff = set(itertools.chain.from_iterable(staff_to_add.values()))

        if not current_staff.isdisjoint(new_staff):
            raise ValueError(
                f'The following staff already exist in kitchen: {current_staff & new_staff}'
            )

        settings['kitchen']['kitchen-staff'] = list(current_staff | new_staff)

        # Add staff to roles
        for role, list_of_staff_to_add in staff_to_add.items():
            for staff_to_add in list_of_staff_to_add:
                settings['kitchen']['kitchen-roles'][staff_to_add] = role

        return self._update_settings(settings)

    def update_staff(self, staff_to_update: dict) -> Response:
        """
        Update roles for the provided staff to this kitchen.

        Parameters
        ----------
        staff_to_update : dict
            Dictionary keyed by role and valued with list of users to update to that role in the form::

                {
                    'Admin': ['admin@gmail.com'],
                    'Developer': ['developer1@gmail.com', 'developer2@gmail.com'],
                }

        Raises
        ------
        HTTPError
            If the request fails.
        PermissionError
            If the current user is not an Admin
        ValueError
            If the provided staff are not new or unique across roles.

        Returns
        -------
        requests.Response
            :class:`Response <Response>` object
        """
        if not self._ensure_disjoint(list(staff_to_update.values())):
            raise ValueError(f'Staff lists for each role must be unique.')

        settings = self._get_settings()
        self._ensure_admin(settings=settings)

        # Add staff to list
        current_staff = self._get_staff_set(settings)
        updated_staff = set(itertools.chain.from_iterable(staff_to_update.values()))

        self.ensure_users_is_part_of_staff(updated_staff, current_staff)

        # Update staff to roles
        for role, list_staff in staff_to_update.items():
            for staff in list_staff:
                settings['kitchen']['kitchen-roles'][staff] = role

        return self._update_settings(settings)

    def get_vault(self):
        return Vault(self._client, self._name)


import json
import pprint
from abc import ABC
from pathlib import Path

from econicer.auxiliary import json2Dict

pp = pprint.PrettyPrinter()

class Settings(ABC):
    _settingsName = ""

    def listCurrentSettings(self):
        """Print all attributes"""
        print(f"Current {self._settingsName} settings:")
        out = self.getSettingsDict()
        pp.pprint(out)

    def getSettingsDict(self):
        out = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}

        for k, v in out.items():
            if isinstance(v, Path):
                out[k] = str(v)
        return out

    def write(self):
        out = self.getSettingsDict()

        outPath = Path(".db\\settings.json")
        outPath.parent.mkdir(parents=True, exist_ok=True)
        with open(Path(str(outPath)), "w") as f:
            json.dump(out, f, indent=4)

class DatabaseSettings(Settings):
    _settingsName = "Database Settings"

    delimiter = ";"
    # owner = 3
    # accountNumber = 4
    # bank = 5
    # beginTable = 11
    owner = "owner"
    bank = "bank"
    accountNumber = "account number"
    dateFormat = "%d.%m.%Y"
    table = [
        "date",
        "valuta",
        "customer",
        "type",
        "usage",
        "saldo",
        "saldoCurrency",
        "value",
        "valueCurrency",
        "groupID",
        "uid",
    ]
    encoding = "utf-8"

class ExternalSettings(Settings):
    """Abstract class which defines settings behavior"""

    _defaultFile = ""

    def __init__(self, filename="", verbose=True):
        self.verbose = verbose

        if filename == "":
            self._filename = self._defaultFile
        else:
            self._filename = filename
        settingsPath = Path(self._filename)

        if settingsPath.is_file():
            settDict = json2Dict(settingsPath)

            for k, v in self.__class__.__dict__.items():
                if k.startswith("_"):
                    continue

                if callable(v):
                    continue

                if k in settDict.keys():
                    setattr(self, k, settDict[k])
                else:
                    setattr(self, k, v)
        else:
            if verbose:
                print(
                    f"Could not find settings file for {self._settingsName}. Use default parameters"
                )

            for k, v in self.__class__.__dict__.items():
                # add attribute checking
                if k.startswith("_"):
                    continue

                if callable(v):
                    continue

                setattr(self, k, v)

class EconicerSettings(ExternalSettings):
    """Define parameters for the econicer app"""

    _settingsName = "Econicer"

    currentAccount = ""
    currentAccountFile = ""
    accountList = []
    inputType = Path(r"config\bank.json")
    group = Path(r"config\grouping.json")
    # database = Path(r"config\database.json")
    plotDir = Path("plots")

    def changeAccount(self, accountName, accountFile):
        if accountName == self.currentAccount:
            if self.verbose:
                print(f"Already on {self.currentAccount} account")
        elif accountName in self.accountList:
            self.currentAccount = accountName
            self.currentAccountFile = accountFile
            self.plotDir = str(Path("plots") / self.currentAccount)
            if self.verbose:
                print(f"Set current account to {self.currentAccount}")
        else:
            if self.verbose:
                print(f"Unknown account {accountName}")
                print("Available accounts are:")
                pp.pprint(self.accountList)

    def updateAccountList(self):
        if not self._filename:
            accList = []
        else:
            basePath = Path(self._filename).parent
            accList = [p.stem for p in basePath.iterdir() if p.is_dir()]

        self.accountList = accList

        # files = [x for x in p if x.is_file()]

class GroupSettings(ExternalSettings):
    """Define grouping settings for transaction analysis"""

    _settingsName = "transaction grouping"

    dbIdentifier = []
    groups = {}

class BankFileSettings(ExternalSettings):
    """Define read parameters for transaction file from some Bank"""

    _settingsName = "bank transaction file"

    delimiter = ""
    # beginTable = -1
    accountNumber = -1
    bank = -1
    owner = -1
    dateFormat = r""
    table = {}
    encoding = ""

if __name__ == "__main__":
    sdb = EconicerSettings(".db\\settings.json")
    sdb.updateAccountList()
    sdb.write()
    sdb.listCurrentSettings()
    print(sdb.accountList)

    sdb = EconicerSettings()
    print(sdb.accountList)


import re
import json
data = {
    "msg": "sucess!",
    "code": 0,
    "data": [{
            "id": 15,
            "name": "test",
            "sex": "",
            "age": 21,
            "mail": "283340479@qq.com",
            "create_time": "2020-12-17"
        }
    ]
}
result = json.dumps(data) #json字符串
# print(type(data))
# print(data)
# print(type(result))
# print(result)

c = re.findall('"mail": "(.+?)"', result)
print(c)