# vim:fileencoding=UTF-8:filetype=python:ts=4:sw=4:sta:et:sts=4:ai

"""Extend calibre's EPUBContainer to work for a KePub."""

__license__ = "GPL v3"
__copyright__ = (
    "2010, Kovid Goyal <kovid@kovidgoyal.net>; "
    + "2013, Joel Goguen <jgoguen@jgoguen.ca>"
)
__docformat__ = "restructuredtext en"

# Be careful editing this! This file has to work in multiple plugins at once,
# so don't import anything from calibre_plugins.

import os
import re
import shutil
import string
import threading
import traceback
from collections import defaultdict
from concurrent.futures import Future
from concurrent.futures import ThreadPoolExecutor
from copy import deepcopy
from typing import Callable
from typing import Dict
from typing import Iterator
from typing import List
from typing import Optional
from typing import Tuple

from calibre import guess_type
from calibre.ebooks.conversion.plugins.epub_input import ADOBE_OBFUSCATION
from calibre.ebooks.conversion.plugins.epub_input import IDPF_OBFUSCATION
from calibre.ebooks.conversion.utils import HeuristicProcessor
from calibre.ebooks.oeb.polish.container import EpubContainer
from calibre.utils.smartypants import smartyPants

from lxml import etree

load_translations()

HTML_MIMETYPES = frozenset(["application/xhtml+xml", "text/html"])
# Technically an unneeded cast, but pyright things guess_type returns str | None
CSS_MIMETYPE: str = str(guess_type("a.css")[0])
# Technically an unneeded cast, but pyright things guess_type returns str | None
JS_MIMETYPE: str = str(guess_type("a.js")[0])
EXCLUDE_FROM_ZIP = frozenset([".DS_Store", ".directory", "mimetype", "thumbs.db"])
NO_SPACE_BEFORE_CHARS = frozenset([c for c in string.punctuation] + ["\xbb"])
ENCRYPTION_NAMESPACES = {
    "enc": "http://www.w3.org/2001/04/xmlenc#",
    "deenc": "http://ns.adobe.com/digitaleditions/enc",
}
XHTML_NAMESPACE = "http://www.w3.org/1999/xhtml"
SKIPPED_TAGS = frozenset(
    [
        "button",
        "circle",
        "defs",
        "figcaption",
        "figure",
        "g",
        "input",
        "math",
        "path",
        "polygon",
        "pre",
        "rect",
        "script",
        "style",
        "svg",
        "use",
        "video",
    ]
)
SPECIAL_TAGS = frozenset(["img"])
ENCODING_RE = re.compile(r'^\<\?.+encoding="([^"]+)"', re.MULTILINE)
SELF_CLOSING_RE = re.compile(
    r"<(meta|link) ([^>]+)>.*?</\1>", re.UNICODE | re.MULTILINE
)
FORCE_OPEN_TAG_RE = re.compile(r"<(script|p) ([^<]+) ?/>", re.UNICODE | re.MULTILINE)
EMPTY_HEADINGS_RE = re.compile(r"(?i)<(h\d+)[^>]*?>\s*</\1>", re.UNICODE | re.MULTILINE)
ELLIPSIS_RE = re.compile(r"(?u)(?<=\w)\s?(\.\s+?){2}\.", re.UNICODE | re.MULTILINE)
MS_CRUFT_RE_1 = re.compile(r"<o:p>\s*</o:p>", re.UNICODE | re.MULTILINE)
MS_CRUFT_RE_2 = re.compile(r"(?i)</?st1:\w+>", re.UNICODE | re.MULTILINE)
TEXT_SPLIT_RE = re.compile(
    r'(\s*.*?[\.\!\?\:][\'"\u201c\u201d\u2018\u2019\u2026]?\s*)',
    re.UNICODE | re.MULTILINE,
)

# TODO: Refactor InvalidEpub from here and device/driver.py to be a common class
class InvalidEpub(ValueError):
    """Designates an invalid ePub file."""

    pass

class ParseError(ValueError):
    """Designates an error parsing an ePub inner file."""

    def __init__(self, name: str, desc: str) -> None:
        """Initialize a ParseError."""
        self.name = name
        self.desc = desc
        ValueError.__init__(self, f"Failed to parse: {name} with error: {desc}")

class KEPubContainer(EpubContainer):
    """Extends an EpubContainer to work for a KePub."""

    def __init__(
        self, epub_path: str, log, do_cleanup: bool = False, *args, **kwargs
    ) -> None:
        self.paragraph_counter = defaultdict(lambda: 1)  # type: Dict[str, int]
        super(KEPubContainer, self).__init__(epub_path, log, *args, **kwargs)
        self.my_thread = threading.current_thread()
        self.log = log
        self.log.debug(f"Creating KePub Container for ePub at {epub_path}")

        self.__run_async_over_content(self.forced_cleanup)
        if do_cleanup:
            self.__run_async_over_content(self.clean_markup)

    def html_names(self) -> Iterator[str]:
        """Get all HTML files in the OPF file.

        A generator function that yields only HTML file names from the ePub.
        """
        for node in self.opf_xpath("//opf:manifest/opf:item[@href and @media-type]"):
            if node.get("media-type") in HTML_MIMETYPES:
                href = os.path.join(os.path.dirname(self.opf_name), node.get("href"))
                href = os.path.normpath(href).replace(os.sep, "/")
                yield href

    @property
    def is_drm_encumbered(self) -> bool:
        """Determine if the ePub container is DRM-encumbered.

        This method looks for the 'encryption.xml' file which denotes an
        ePub encumbered by Digital Restrictions Management. DRM-encumbered
        files cannot be edited.
        """
        is_encumbered = False
        if "META-INF/encryption.xml" in self.name_path_map:
            try:
                xml = self.parsed("META-INF/encryption.xml")
                if xml is None:
                    # If encryption.xml can't be parsed, assume its presence
                    # means an encumbered file. This may be wrong, but so far
                    # it's proven accurate.
                    return True
                for elem in xml.xpath(
                    "./enc:EncryptedData/enc:EncryptionMethod[@Algorithm]",
                    namespaces=ENCRYPTION_NAMESPACES,
                ):
                    alg = elem.get("Algorithm")

                    # Anything not an acceptable encryption algorithm is a
                    # sign of an encumbered file.
                    if alg not in {ADOBE_OBFUSCATION, IDPF_OBFUSCATION}:
                        is_encumbered = True
                        break
            except Exception as e:
                self.log.error(f"Could not parse encryption.xml: {e}")
                raise

        return is_encumbered

    def copy_file_to_container(
        self, path: str, name: Optional[str] = None, mt: Optional[str] = None
    ) -> str:
        """Copy a file into this Container instance.

        @param path: The path to the file to copy into this Container.
        @param name: The name to give to the copied file, relative to the
        Container root. Set to None to use the basename of path.
        @param mt: The MIME type of the file to set in the manifest. Set to
        None to auto-detect.

        @return: The name of the file relative to the Container root
        """
        if not os.path.isfile(path):
            raise ValueError(_("A source path must be given"))
        if name is None:
            basename: str = os.path.basename(path)
        else:
            basename: str = name
        item = self.generate_item(basename, media_type=mt)
        # Unnecessary casse but pyright things href_to_name could return many things
        basename = str(self.href_to_name(item.get("href"), self.opf_name))

        self.log.info(f"Copying file '{path}' to '{self.root}' as '{basename}'")

        try:
            # Throws an error we can ignore if the directory already exists
            os.makedirs(os.path.dirname(os.path.join(self.root, basename)))
        except Exception:
            pass

        shutil.copy(path, os.path.join(self.root, basename))

        return basename

    def add_content_file_reference(self, name: str) -> None:
        """Add a reference to the named file to all content files.

        Adds a reference to the named file (see self.name_path_map) to all
        content files (self.html_names()). Currently only CSS files with a
        MIME type of text/css and JavaScript files with a MIME type of
        application/x-javascript are supported.
        """
        if name not in self.name_path_map or name not in self.mime_map:
            raise ValueError(_(f"A valid file name must be given (got {name})"))

        self.__run_async_over_content(self.__add_content_file_reference_impl, (name,))

    def __add_content_file_reference_impl(self, infile: str, name: str) -> None:
        self.log.debug(f"Adding reference to {name} to file {infile}")
        root = self.parsed(infile)
        if root is None:
            raise Exception(_(f"Could not retrieve content file {infile}"))
        head = root.xpath("./xhtml:head", namespaces={"xhtml": XHTML_NAMESPACE})
        if head is None:
            head = root.makeelement(f"{{{XHTML_NAMESPACE}}}head")
            root.insert(0, head)
        else:
            head = head[0]
        if head is None:
            raise Exception(
                _(
                    "A <head> section was found but was undefined in content "
                    + f"file {infile}"
                )
            )

        if self.mime_map[name] == CSS_MIMETYPE:
            elem = head.makeelement(
                f"{{{XHTML_NAMESPACE}}}link",
                rel="stylesheet",
                href=os.path.relpath(name, os.path.dirname(infile)).replace(
                    os.sep, "/"
                ),
            )
        elif self.mime_map[name] == JS_MIMETYPE:
            elem = head.makeelement(
                f"{{{XHTML_NAMESPACE}}}script",
                type="text/javascript",
                src=os.path.relpath(name, os.path.dirname(infile)).replace(os.sep, "/"),
            )
        else:
            elem = None

        if elem is not None:
            head.append(elem)
            if self.mime_map[name] == CSS_MIMETYPE:
                self.fix_tail(elem)
            self.commit_item(infile, keep_parsed=True)

    def fix_tail(self, item: etree._Element) -> None:
        """Fix self-closing elements.

        Designed only to work with self closing elements after item has just
        been inserted/appended
        """
        parent = item.getparent()
        idx = parent.index(item)
        if idx == 0:
            # item is the first child element, move the text to after item
            item.tail = parent.text
        else:
            # There are other elements, possibly also text, before this child
            # element.
            # Move this element's tail to the previous element (note: .text is
            # only the text after the last child element, text before that and
            # surrounding elements are attributes of the elements)
            item.tail = parent[idx - 1].tail
            # If this is the last child element, it gets the remaining text.
            if idx == len(parent) - 1:
                parent[idx - 1].tail = parent.text

    def forced_cleanup(self, name: str) -> None:
        """Perform cleanup considered essential for standards compliance."""
        self.log.debug(f"Forcing cleanup for file {name}")
        html = self.raw_data(name, decode=True, normalize_to_nfc=True)
        if html is None:
            self.log.warning(f"No HTML content in {name}")
            return

        encoding_match = ENCODING_RE.search(str(html[:75]))
        if encoding_match and encoding_match.group(1).upper() != "UTF-8":
            html = re.sub(encoding_match.group(1), "UTF-8", html, 1, re.MULTILINE)

        # Force meta and link tags to be self-closing
        html = SELF_CLOSING_RE.sub(r"<\1 \2 />", html)

        # Force open script tags
        html = FORCE_OPEN_TAG_RE.sub(r"<\1 \2></\1>", html)

        # Remove Unicode replacement characters
        html = html.replace("\uFFFD", "")

        self.replace(name, self.parse_xhtml(html))
        self.commit_item(name, keep_parsed=True)

    def clean_markup(self, name: str) -> None:
        """Clean HTML markup.

        This cleans the HTML markup for things which are not strictly
        non-compliant but can cause problems.
        """
        self.log.debug(f"Cleaning markup for file {name}")
        html = self.raw_data(name, decode=True, normalize_to_nfc=True)
        if html is None:
            self.log.warning(f"No HTML content in {name}")

        # Get rid of Microsoft cruft
        html = MS_CRUFT_RE_1.sub(" ", html)
        html = MS_CRUFT_RE_2.sub("", html)

        # Remove empty headings
        html = EMPTY_HEADINGS_RE.sub("", html)

        self.replace(name, self.parse_xhtml(html))
        self.commit_item(name, keep_parsed=True)

    def smarten_punctuation(self) -> None:
        self.__run_async_over_content(self.__smarten_punctuation_impl)

    def __smarten_punctuation_impl(self, name: str) -> None:
        """Convert standard punctuation to "smart" punctuation."""
        preprocessor = HeuristicProcessor(log=self.log)

        self.log.debug(f"Smartening punctuation for file {name}")
        html = self.raw_data(name, decode=True, normalize_to_nfc=True)
        if html is None:
            self.log.warning(f"No HTML content in file {name}")

        # Fix non-breaking space indents
        html = preprocessor.fix_nbsp_indents(html)

        # Smarten punctuation
        # q : quotes
        # B : backtick quotes (``double'' and `single')
        # d : dashes
        # e : ellipses
        html = smartyPants(html, attr="qBde")

        # Unicode char code for em-dash and en-dash not converted by smartyPants()
        html = html.replace("\x97", " – ")
        html = html.replace("\u2013", " – ")
        html = html.replace("\u2014", " — ")

        self.replace(name, self.parse_xhtml(html))
        self.commit_item(name, keep_parsed=True)

    def __run_async(self, func: Callable, args: List[Tuple[str, ...]]) -> None:
        # Verify that we aren't making subthreads of a subthread
        if threading.current_thread() != self.my_thread:
            self.log.debug("__run_async called by a subthread")
            traceback.print_stack()
            raise Exception("__run_async called by a subthread")

        futures: List[Future] = []
        with ThreadPoolExecutor() as pool:
            try:
                for arg in args:
                    self.log.debug(
                        f"Starting thread: func={func.__name__}, name={arg[0]}"
                    )
                    futures.append(pool.submit(func, *arg))

                for future in futures:
                    name = future.result(timeout=60)
                    self.log.debug(f"thread processing {name} finished")
            except Exception as e:
                self.log.error(f"Unhandled exception in thread processing. {str(e)}")
                raise e

        # Be sure dirtied trees are committed. These should be trees dirtied in
        # our superclass because trees dirtied here have already been committed
        for n in list(self.dirtied):
            self.log.debug(f"Committing dirtied: {n}")
            self.commit_item(n)

    def __run_async_over_content(
        self, func: Callable, args: Optional[Tuple[str, ...]] = None
    ) -> None:
        args = args or ()
        names = [(name,) + args for name in self.html_names()]
        self.__run_async(func, names)

    def convert(self) -> None:
        """The entry point for converting to KePub"""
        self.__run_async_over_content(self.add_kobo_spans)
        self.__run_async_over_content(self.add_kobo_divs)

    def add_kobo_divs(self, name) -> None:
        """Add KePub divs to the HTML file."""
        self.log.debug(f"Adding Kobo divs to {name}")
        root = self.parsed(name)
        kobo_div_count = int(
            root.xpath(
                'count(//xhtml:div[@id="book-inner"])',
                namespaces={"xhtml": XHTML_NAMESPACE},
            )
        )
        if kobo_div_count > 0:
            self.log.warning(
                _(f"Skipping file {name}")
                + ", "
                + ngettext(
                    "Kobo <div> tag present", "Kobo <div> tags present", kobo_div_count
                )
            )
            return name

        # NOTE: Hackish heuristic: Forgo this if we have more div's than
        # p's, which would potentially indicate a book using div's instead
        # of p's...
        # Apparently, doing this on those books appears to blow up in a
        # spectacular way, so, err, don't ;).
        # FIXME: Try to figure out what's really happening instead of
        # sidestepping the issue?
        div_count = int(
            root.xpath("count(//xhtml:div)", namespaces={"xhtml": XHTML_NAMESPACE})
        )
        p_count = int(
            root.xpath("count(//xhtml:p)", namespaces={"xhtml": XHTML_NAMESPACE})
        )
        if div_count > p_count:
            self.log.warning(
                _(f"Skipping file {name}")
                + " ("
                + ngettext(
                    f"{div_count} <div> tag", f"{div_count} <div> tags", div_count
                )
                + ", "
                + ngettext(f"{p_count} <p> tag", f"{p_count} <p> tags", p_count)
                + ")"
            )
            return name

        self.__add_kobo_divs_to_body(root)

        self.replace(name, root)
        self.commit_item(name, keep_parsed=True)
        return name

    def __add_kobo_divs_to_body(self, root: etree._Element) -> None:
        body = root.xpath("./xhtml:body", namespaces={"xhtml": XHTML_NAMESPACE})[0]

        # save node content for later
        body_text = body.text
        body_children = deepcopy(body.getchildren())
        body_attrs = {}
        for key in list(body.keys()):
            body_attrs[key] = body.get(key)

        # reset current node, to start from scratch
        body.clear()

        # restore node attributes
        for key in body_attrs:
            body.set(key, body_attrs[key])

        # Wrap the full body in a div
        inner_div = etree.Element(
            f"{{{XHTML_NAMESPACE}}}div", attrib={"id": "book-inner"}
        )

        # Handle the node text
        if body_text is not None:
            inner_div.text = body_text

        # re-add the node children, but as children of the div
        for child in body_children:
            # save child tail for later
            child_tail = child.tail
            child.tail = None
            inner_div.append(child)
            # Handle the child tail
            if child_tail is not None:
                inner_div[-1].tail = child_tail

        # Finally, wrap that div in another one...
        outer_div = etree.Element(
            f"{{{XHTML_NAMESPACE}}}div", attrib={"id": "book-columns"}
        )
        outer_div.append(inner_div)

        # And re-chuck the full div pyramid in the now empty body
        body.append(outer_div)

    def add_kobo_spans(self, name: str) -> None:
        """Add KePub spans (used for in-book location) the HTML file."""
        self.log.debug(f"Adding Kobo spans to {name}")
        root = self.parsed(name)
        kobo_span_count = int(
            root.xpath(
                'count(.//xhtml:span[@class="koboSpan" '
                + 'or starts-with(@id, "kobo.")])',
                namespaces={"xhtml": XHTML_NAMESPACE},
            )
        )
        if kobo_span_count > 0:
            self.log.warning(
                _(f"Skipping file {name}")
                + ", "
                + ngettext(
                    "Kobo <span> tag present",
                    "Kobo <span> tags present",
                    kobo_span_count,
                )
            )
            return

        body = root.xpath("./xhtml:body", namespaces={"xhtml": XHTML_NAMESPACE})[0]
        self._add_kobo_spans_to_node(body, name)

        self.replace(name, root)
        self.commit_item(name, keep_parsed=True)

    def _add_kobo_spans_to_node(
        self, node: etree._Element, name: str
    ) -> etree._Element:
        # process node only if it is not a comment or a processing instruction
        if (
            node is None
            or isinstance(node, etree._Comment)
            or isinstance(node, etree._ProcessingInstruction)
        ):
            if node is not None:
                node.tail = None
            self.log.debug(f"[{name}] Skipping comment/ProcessingInstruction node")
            return node

        # Special case some tags
        special_tag_match = re.search(r"^(?:\{[^\}]+\})?(\w+)$", node.tag)
        if special_tag_match:
            # Skipped tags are just flat out skipped
            if special_tag_match.group(1) in SKIPPED_TAGS:
                self.log.debug(f"[{name}] Skipping '{special_tag_match.group(1)}' tag")
                return node

            # Special tags get wrapped in a span and their children are ignored
            if special_tag_match.group(1) in SPECIAL_TAGS:
                self.log.debug(
                    f"[{name}] Wrapping '{special_tag_match.group(1)}' tag and "
                    + "ignoring children"
                )
                span = etree.Element(
                    f"{{{XHTML_NAMESPACE}}}span",
                    attrib={
                        "id": f"kobo.{self.paragraph_counter[name]}.1",
                        "class": "koboSpan",
                    },
                )
                span.append(node)
                return span

        # save node content for later
        node_text = node.text
        node_children = deepcopy(node.getchildren())
        node_attrs = {}
        for key in list(node.keys()):
            node_attrs[key] = node.get(key)

        # reset current node, to start from scratch
        node.clear()

        # restore node attributes
        for key in node_attrs:
            node.set(key, node_attrs[key])

        # the node text is converted to spans
        if node_text is not None:
            if not self._append_kobo_spans_from_text(node, node_text, name):
                # didn't add spans, restore text
                node.text = node_text
            else:
                self.paragraph_counter[name] += 1

        # re-add the node children
        for child in node_children:
            # save child tail for later
            child_tail = child.tail
            child.tail = None
            node.append(self._add_kobo_spans_to_node(child, name))
            # the child tail is converted to spans
            if child_tail is not None:
                if not self._append_kobo_spans_from_text(node, child_tail, name):
                    # didn't add spans, restore tail on last child
                    node[-1].tail = child_tail
                else:
                    self.paragraph_counter[name] += 1

        return node

    def _append_kobo_spans_from_text(
        self, node: etree._Element, text: str, name: str
    ) -> etree._Element:
        if not text:
            self.log.error(f"[{name}] No text passed, can't add spans")
            return False

        # if text is only whitespace, don't add spans
        if text.strip() == "":
            self.log.warning(f"[{name}] Found only whitespace, not adding spans")
            return False

        # split text in sentences
        groups = TEXT_SPLIT_RE.split(text)
        # remove empty strings resulting from split()
        groups = [g for g in groups if g != ""]

        # TODO: To match Kobo KePubs, the trailing whitespace needs to
        # be prepended to the next group. Probably equivalent to make
        # sure the space stays in the span at the end.
        # add each sentence in its own span
        segment_counter = 1
        for g in groups:
            span = etree.Element(
                f"{{{XHTML_NAMESPACE}}}span",
                attrib={
                    "class": "koboSpan",
                    "id": f"kobo.{self.paragraph_counter[name]}.{segment_counter}",
                },
            )
            span.text = g
            node.append(span)
            segment_counter += 1

        return True


from django.urls import path
from . import views

urlpatterns = [
    path("", views.dashboard, name='dashboard'),
    path("cotacao/", views.cotacao, name='cotacao'),
    path("entregas/", views.lista_entregas, name='entregas' ),
    path("formulario/", views.formulario, name='formulario')

    # Outras URLs relacionadas à cotação de entregas podem ser adicionadas aqui
]


import numpy as np
import math
from nn import NeuralNet
from typing import List, Dict, Tuple

class AimAssistant:
    """
    Wrapper around the neural net for data preprocessing / postprocessing
    """
    sides_crop = 0.3
    top_crop = 0.35

    def __init__(self, crop: bool = False):
        self.to_crop = crop
        try:
            self.net = NeuralNet()
        except Exception as e:
            print(f"Failed while NN initialization. Error: {e}")
            raise e
        print("Aim assistant successfully initialized")

    def aim(self, frame: np.ndarray) -> dict:
        """

        :param frame:
        :return:
        """
        # Режем кадр, чтобы оставить только центр картинки где располагается опора.
        if self.to_crop:
            relative_coord, frame = AimAssistant.crop_frame(frame, self.sides_crop, self.top_crop)

        # Detect components on the frame
        components = self.net.predict(frame)

        # If components, calculate angles relatively to the frame centre
        output = dict()
        if components:
            # Пересчитаем координаты коробок относительно целого кадра. Сейчас они относительно
            # вырезанного np.ndarray если стоял флаг self.to_crop
            if self.to_crop:
                components = AimAssistant.recalculate_boxes(components, relative_coord)
            output = AimAssistant.calculate_angles(components=components, frame=frame)

        return output

    @staticmethod
    def recalculate_boxes(predictions:List[list], sliced_image_coord: List[int]) -> List[list]:
        """
        Пересчитывает координаты коробки относительно оригинального кадра, а не вырезанной подкартинки
        :param predictions:
        :param sliced_image_coord:
        :return:
        """
        output = list()
        for prediction in predictions:
            class_id = prediction[0]
            conf = prediction[1]
            left = prediction[2] + sliced_image_coord[0]
            top = prediction[3] + sliced_image_coord[1]
            right = prediction[4] + sliced_image_coord[0]
            bot = prediction[5] + sliced_image_coord[1]
            assert all(e > 0 for e in [left, top, right, bot]), "Coordinates recalculated wrong"
            assert all((left < right, top < bot)), "Coordinates recalculated wrong"
            output.append([class_id, conf, left, top, right, bot])

        return output

    @staticmethod
    def crop_frame(frame: np.ndarray, sides: float = 0.3, top: float = 0.35) -> Tuple[list, np.ndarray]:
        """
        Вырезает картинки из кадра где находится опора. Отрезает края и верхушку кадра. Они нам не интеренсты
        если снимаем с курсовой камеры, которая смотрит перед собой.
        :param frame:
        :param sides:
        :param top:
        :return:
        """
        assert 0 < sides < 0.5, "Sides crop needs to be between 0 and 50%"
        assert 0 < top < 0.5, "Top crop needs to be between 0 and 50%"

        frame_height, frame_width = frame.shape[0], frame.shape[1]
        new_left = int(frame_width * sides)
        new_right = int(frame_width - (frame_width * sides))
        new_top = int(frame_height * top)
        new_bot = frame_height
        assert all((new_top < new_bot, new_left < new_right)), "Wrong cropped coordinates"

        #cv2.rectangle(frame, (new_left, new_top), (new_right, new_bot), (0, 255, 255), 2)
        try:
            cropped_image = np.array(frame[new_top:new_bot, new_left:new_right, :])
        except Exception as e:
            print("Failed while cropping the frame. Error: {e}")
            raise e

        return [new_left, new_top, new_right, new_bot], cropped_image

    @staticmethod
    def calculate_angles(components: List[list], frame: np.ndarray) -> Dict[int, dict]:
        """
        :param components: detected components
        :param frame: frame
        :return:
        """
        frame_centre = (frame.shape[1] // 2, frame.shape[0] // 2)  # x,y
        output = dict()
        output_schema = {
            "obj_class": int,
            "bb_centre": tuple,
            "bb_coord": list,
            "aim_angle": tuple,
            "bb_size": float
        }

        for i, component in enumerate(components):
            obj_class, conf = component[0], component[1]
            # Skip concrete pillars, we're interested in only insulators and dumpers
            if obj_class == 2:
                continue
            left, top = component[2], component[3]
            right, bot = component[4], component[5]

            # Calculate element's BB centre relatively to the whole image
            element_x_centre = (left + right) // 2
            element_y_centre = (top + bot) // 2

            # Calculate delta (image centre vs element centre)
            delta_x = abs(frame_centre[0] - element_x_centre)
            delta_y = abs(frame_centre[1] - element_y_centre)

            # Calculate angles
            angle_1 = round(np.rad2deg(np.arctan2(delta_x, delta_y)), 2)
            angle_2 = round(90 - angle_1, 2)
            assert all((0 <= angle_1 <= 90, 0 <= angle_2 <= 90)), "Wrong angles calculated. Expected: [0, 90]"

            # Estimate object's relative diameter
            element_BB_diagonal = math.sqrt((top - bot)**2 +(right - left)**2)
            frame_diagonal = math.sqrt((frame.shape[0])**2 + (frame.shape[1])**2)
            diagonal = round(element_BB_diagonal / frame_diagonal, 3)

            schema_instance = output_schema.copy()
            try:
                schema_instance["obj_class"] = obj_class
                schema_instance["bb_centre"] = (int(element_x_centre), int(element_y_centre))
                schema_instance["bb_coord"] = (left, top, right, bot)
                schema_instance["aim_angle"] = (angle_1, angle_2)
                schema_instance["bb_size"] = diagonal
            except Exception as e:
                print(f"Failed while filling the output schema instance. Error: {e}")
                raise e

            output[i] = schema_instance

        return output


# Released under the MIT License. See LICENSE for details.
#
"""UI functionality related to UI items."""
from __future__ import annotations

from typing import TYPE_CHECKING

import bascenev1 as bs
import bauiv1 as bui

if TYPE_CHECKING:
    from typing import Any

def instantiate_store_item_display(
    item_name: str,
    item: dict[str, Any],
    parent_widget: bui.Widget,
    b_pos: tuple[float, float],
    b_width: float,
    b_height: float,
    boffs_h: float = 0.0,
    boffs_h2: float = 0.0,
    boffs_v2: float = 0,
    delay: float = 0.0,
    button: bool = True,
) -> None:
    """(internal)"""
    # pylint: disable=too-many-statements
    # pylint: disable=too-many-branches
    # pylint: disable=too-many-locals
    assert bui.app.classic is not None
    store = bui.app.classic.store

    plus = bui.app.plus
    assert plus is not None

    del boffs_h  # unused arg
    del boffs_h2  # unused arg
    del boffs_v2  # unused arg
    item_info = store.get_store_item(item_name)
    title_v = 0.24
    price_v = 0.145
    base_text_scale = 1.0

    item['name'] = title = store.get_store_item_name_translated(item_name)

    btn: bui.Widget | None

    # Hack; showbuffer stuff isn't working well when we're showing merch.
    showbuffer = 10 if item_name in {'merch', 'pro', 'pro_sale'} else 76.0

    if button:
        item['button'] = btn = bui.buttonwidget(
            parent=parent_widget,
            position=b_pos,
            transition_delay=delay,
            show_buffer_top=showbuffer,
            enable_sound=False,
            button_type='square',
            size=(b_width, b_height),
            autoselect=True,
            label='',
        )
        bui.widget(edit=btn, show_buffer_bottom=showbuffer)
    else:
        btn = None

    b_offs_x = -0.015 * b_width
    check_pos = 0.76

    icon_tex = None
    tint_tex = None
    tint_color = None
    tint2_color = None
    tex_name: str | None = None
    desc: str | None = None
    modes: bui.Lstr | None = None

    if item_name.startswith('characters.'):
        assert bui.app.classic is not None
        character = bui.app.classic.spaz_appearances[item_info['character']]
        tint_color = (
            item_info['color']
            if 'color' in item_info
            else character.default_color
            if character.default_color is not None
            else (1, 1, 1)
        )
        tint2_color = (
            item_info['highlight']
            if 'highlight' in item_info
            else character.default_highlight
            if character.default_highlight is not None
            else (1, 1, 1)
        )
        icon_tex = character.icon_texture
        tint_tex = character.icon_mask_texture
        title_v = 0.255
        price_v = 0.145
    elif item_name == 'merch':
        base_text_scale = 0.6
        title_v = 0.85
        price_v = 0.15
    elif item_name in ['upgrades.pro', 'pro']:
        base_text_scale = 0.6
        title_v = 0.85
        price_v = 0.15
    elif item_name.startswith('maps.'):
        map_type = item_info['map_type']
        tex_name = map_type.get_preview_texture_name()
        title_v = 0.312
        price_v = 0.17

    elif item_name.startswith('games.'):
        gametype = item_info['gametype']
        modes_l = []
        if gametype.supports_session_type(bs.CoopSession):
            modes_l.append(bui.Lstr(resource='playModes.coopText'))
        if gametype.supports_session_type(bs.DualTeamSession):
            modes_l.append(bui.Lstr(resource='playModes.teamsText'))
        if gametype.supports_session_type(bs.FreeForAllSession):
            modes_l.append(bui.Lstr(resource='playModes.freeForAllText'))

        if len(modes_l) == 3:
            modes = bui.Lstr(
                value='${A}, ${B}, ${C}',
                subs=[
                    ('${A}', modes_l[0]),
                    ('${B}', modes_l[1]),
                    ('${C}', modes_l[2]),
                ],
            )
        elif len(modes_l) == 2:
            modes = bui.Lstr(
                value='${A}, ${B}',
                subs=[('${A}', modes_l[0]), ('${B}', modes_l[1])],
            )
        elif len(modes_l) == 1:
            modes = modes_l[0]
        else:
            raise RuntimeError()
        desc = gametype.get_description_display_string(bs.CoopSession)
        tex_name = item_info['previewTex']
        base_text_scale = 0.8
        title_v = 0.48
        price_v = 0.17

    elif item_name.startswith('icons.'):
        base_text_scale = 1.5
        price_v = 0.2
        check_pos = 0.6

    if item_name.startswith('characters.'):
        frame_size = b_width * 0.7
        im_dim = frame_size * (100.0 / 113.0)
        im_pos = (
            b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
            b_pos[1] + b_height * 0.57 - im_dim * 0.5,
        )
        mask_texture = bui.gettexture('characterIconMask')
        assert icon_tex is not None
        assert tint_tex is not None
        bui.imagewidget(
            parent=parent_widget,
            position=im_pos,
            size=(im_dim, im_dim),
            color=(1, 1, 1),
            transition_delay=delay,
            mask_texture=mask_texture,
            draw_controller=btn,
            texture=bui.gettexture(icon_tex),
            tint_texture=bui.gettexture(tint_tex),
            tint_color=tint_color,
            tint2_color=tint2_color,
        )

    if item_name == 'merch':
        frame_size = b_width * 0.65
        im_dim = frame_size * (100.0 / 113.0)
        im_pos = (
            b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
            b_pos[1] + b_height * 0.47 - im_dim * 0.5,
        )
        bui.imagewidget(
            parent=parent_widget,
            position=im_pos,
            size=(im_dim, im_dim),
            transition_delay=delay,
            draw_controller=btn,
            opacity=1.0,
            texture=bui.gettexture('merch'),
        )

    if item_name in ['pro', 'upgrades.pro']:
        frame_size = b_width * 0.5
        im_dim = frame_size * (100.0 / 113.0)
        im_pos = (
            b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
            b_pos[1] + b_height * 0.5 - im_dim * 0.5,
        )
        bui.imagewidget(
            parent=parent_widget,
            position=im_pos,
            size=(im_dim, im_dim),
            transition_delay=delay,
            draw_controller=btn,
            color=(0.3, 0.0, 0.3),
            opacity=0.3,
            texture=bui.gettexture('logo'),
        )
        txt = bui.Lstr(resource='store.bombSquadProNewDescriptionText')

        item['descriptionText'] = bui.textwidget(
            parent=parent_widget,
            text=txt,
            position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.69),
            transition_delay=delay,
            scale=b_width * (1.0 / 230.0) * base_text_scale * 0.75,
            maxwidth=b_width * 0.75,
            max_height=b_height * 0.2,
            size=(0, 0),
            h_align='center',
            v_align='center',
            draw_controller=btn,
            color=(0.3, 1, 0.3),
        )

        extra_backings = item['extra_backings'] = []
        extra_images = item['extra_images'] = []
        extra_texts = item['extra_texts'] = []
        extra_texts_2 = item['extra_texts_2'] = []

        backing_color = (0.5, 0.8, 0.3) if button else (0.6, 0.5, 0.65)
        b_square_texture = bui.gettexture('buttonSquare')
        char_mask_texture = bui.gettexture('characterIconMask')

        pos = (0.17, 0.43)
        tile_size = (b_width * 0.16 * 1.2, b_width * 0.2 * 1.2)
        tile_pos = (b_pos[0] + b_width * pos[0], b_pos[1] + b_height * pos[1])
        extra_backings.append(
            bui.imagewidget(
                parent=parent_widget,
                position=(
                    tile_pos[0] - tile_size[0] * 0.5,
                    tile_pos[1] - tile_size[1] * 0.5,
                ),
                size=tile_size,
                transition_delay=delay,
                draw_controller=btn,
                color=backing_color,
                texture=b_square_texture,
            )
        )
        im_size = tile_size[0] * 0.8
        extra_images.append(
            bui.imagewidget(
                parent=parent_widget,
                position=(
                    tile_pos[0] - im_size * 0.5,
                    tile_pos[1] - im_size * 0.4,
                ),
                size=(im_size, im_size),
                transition_delay=delay,
                draw_controller=btn,
                color=(1, 1, 1),
                texture=bui.gettexture('ticketsMore'),
            )
        )
        bonus_tickets = str(
            plus.get_v1_account_misc_read_val('proBonusTickets', 100)
        )
        extra_texts.append(
            bui.textwidget(
                parent=parent_widget,
                draw_controller=btn,
                position=(
                    tile_pos[0] - tile_size[0] * 0.03,
                    tile_pos[1] - tile_size[1] * 0.25,
                ),
                size=(0, 0),
                color=(0.6, 1, 0.6),
                transition_delay=delay,
                h_align='center',
                v_align='center',
                maxwidth=tile_size[0] * 0.7,
                scale=0.55,
                text=bui.Lstr(
                    resource='getTicketsWindow.ticketsText',
                    subs=[('${COUNT}', bonus_tickets)],
                ),
                flatness=1.0,
                shadow=0.0,
            )
        )

        for charname, pos in [
            ('Kronk', (0.32, 0.45)),
            ('Zoe', (0.425, 0.4)),
            ('Jack Morgan', (0.555, 0.45)),
            ('Mel', (0.645, 0.4)),
        ]:
            tile_size = (b_width * 0.16 * 0.9, b_width * 0.2 * 0.9)
            tile_pos = (
                b_pos[0] + b_width * pos[0],
                b_pos[1] + b_height * pos[1],
            )
            assert bui.app.classic is not None
            character = bui.app.classic.spaz_appearances[charname]
            extra_backings.append(
                bui.imagewidget(
                    parent=parent_widget,
                    position=(
                        tile_pos[0] - tile_size[0] * 0.5,
                        tile_pos[1] - tile_size[1] * 0.5,
                    ),
                    size=tile_size,
                    transition_delay=delay,
                    draw_controller=btn,
                    color=backing_color,
                    texture=b_square_texture,
                )
            )
            im_size = tile_size[0] * 0.7
            extra_images.append(
                bui.imagewidget(
                    parent=parent_widget,
                    position=(
                        tile_pos[0] - im_size * 0.53,
                        tile_pos[1] - im_size * 0.35,
                    ),
                    size=(im_size, im_size),
                    transition_delay=delay,
                    draw_controller=btn,
                    color=(1, 1, 1),
                    texture=bui.gettexture(character.icon_texture),
                    tint_texture=bui.gettexture(character.icon_mask_texture),
                    tint_color=character.default_color,
                    tint2_color=character.default_highlight,
                    mask_texture=char_mask_texture,
                )
            )
            extra_texts.append(
                bui.textwidget(
                    parent=parent_widget,
                    draw_controller=btn,
                    position=(
                        tile_pos[0] - im_size * 0.03,
                        tile_pos[1] - im_size * 0.51,
                    ),
                    size=(0, 0),
                    color=(0.6, 1, 0.6),
                    transition_delay=delay,
                    h_align='center',
                    v_align='center',
                    maxwidth=tile_size[0] * 0.7,
                    scale=0.55,
                    text=bui.Lstr(translate=('characterNames', charname)),
                    flatness=1.0,
                    shadow=0.0,
                )
            )

        # If we have a 'total-worth' item-id for this id, show that price so
        # the user knows how much this is worth.
        total_worth_item = plus.get_v1_account_misc_read_val('twrths', {}).get(
            item_name
        )
        total_worth_price: str | None
        if total_worth_item is not None:
            price = plus.get_price(total_worth_item)
            total_worth_price = (
                store.get_clean_price(price) if price is not None else '??'
            )
        else:
            total_worth_price = None

        if total_worth_price is not None:
            total_worth_text = bui.Lstr(
                resource='store.totalWorthText',
                subs=[('${TOTAL_WORTH}', total_worth_price)],
            )
            extra_texts_2.append(
                bui.textwidget(
                    parent=parent_widget,
                    text=total_worth_text,
                    position=(
                        b_pos[0] + b_width * 0.5 + b_offs_x,
                        b_pos[1] + b_height * 0.25,
                    ),
                    transition_delay=delay,
                    scale=b_width * (1.0 / 230.0) * base_text_scale * 0.45,
                    maxwidth=b_width * 0.5,
                    size=(0, 0),
                    h_align='center',
                    v_align='center',
                    shadow=1.0,
                    flatness=1.0,
                    draw_controller=btn,
                    color=(0.3, 1, 1),
                )
            )

        mesh_opaque = bui.getmesh('level_select_button_opaque')
        mesh_transparent = bui.getmesh('level_select_button_transparent')
        mask_tex = bui.gettexture('mapPreviewMask')
        for levelname, preview_tex_name, pos in [
            ('Infinite Onslaught', 'doomShroomPreview', (0.80, 0.48)),
            ('Infinite Runaround', 'towerDPreview', (0.80, 0.32)),
        ]:
            tile_size = (b_width * 0.2, b_width * 0.13)
            tile_pos = (
                b_pos[0] + b_width * pos[0],
                b_pos[1] + b_height * pos[1],
            )
            im_size = tile_size[0] * 0.8
            extra_backings.append(
                bui.imagewidget(
                    parent=parent_widget,
                    position=(
                        tile_pos[0] - tile_size[0] * 0.5,
                        tile_pos[1] - tile_size[1] * 0.5,
                    ),
                    size=tile_size,
                    transition_delay=delay,
                    draw_controller=btn,
                    color=backing_color,
                    texture=b_square_texture,
                )
            )

            # Hack - gotta draw two transparent versions to avoid z issues.
            for mod in mesh_opaque, mesh_transparent:
                extra_images.append(
                    bui.imagewidget(
                        parent=parent_widget,
                        position=(
                            tile_pos[0] - im_size * 0.52,
                            tile_pos[1] - im_size * 0.2,
                        ),
                        size=(im_size, im_size * 0.5),
                        transition_delay=delay,
                        mesh_transparent=mod,
                        mask_texture=mask_tex,
                        draw_controller=btn,
                        texture=bui.gettexture(preview_tex_name),
                    )
                )

            extra_texts.append(
                bui.textwidget(
                    parent=parent_widget,
                    draw_controller=btn,
                    position=(
                        tile_pos[0] - im_size * 0.03,
                        tile_pos[1] - im_size * 0.2,
                    ),
                    size=(0, 0),
                    color=(0.6, 1, 0.6),
                    transition_delay=delay,
                    h_align='center',
                    v_align='center',
                    maxwidth=tile_size[0] * 0.7,
                    scale=0.55,
                    text=bui.Lstr(translate=('coopLevelNames', levelname)),
                    flatness=1.0,
                    shadow=0.0,
                )
            )

    if item_name.startswith('icons.'):
        item['icon_text'] = bui.textwidget(
            parent=parent_widget,
            text=item_info['icon'],
            position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.5),
            transition_delay=delay,
            scale=b_width * (1.0 / 230.0) * base_text_scale * 2.0,
            maxwidth=b_width * 0.9,
            max_height=b_height * 0.9,
            size=(0, 0),
            h_align='center',
            v_align='center',
            draw_controller=btn,
        )

    if item_name.startswith('maps.'):
        frame_size = b_width * 0.9
        im_dim = frame_size * (100.0 / 113.0)
        im_pos = (
            b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
            b_pos[1] + b_height * 0.62 - im_dim * 0.25,
        )
        mesh_opaque = bui.getmesh('level_select_button_opaque')
        mesh_transparent = bui.getmesh('level_select_button_transparent')
        mask_tex = bui.gettexture('mapPreviewMask')
        assert tex_name is not None
        bui.imagewidget(
            parent=parent_widget,
            position=im_pos,
            size=(im_dim, im_dim * 0.5),
            transition_delay=delay,
            mesh_opaque=mesh_opaque,
            mesh_transparent=mesh_transparent,
            mask_texture=mask_tex,
            draw_controller=btn,
            texture=bui.gettexture(tex_name),
        )

    if item_name.startswith('games.'):
        frame_size = b_width * 0.8
        im_dim = frame_size * (100.0 / 113.0)
        im_pos = (
            b_pos[0] + b_width * 0.5 - im_dim * 0.5 + b_offs_x,
            b_pos[1] + b_height * 0.72 - im_dim * 0.25,
        )
        mesh_opaque = bui.getmesh('level_select_button_opaque')
        mesh_transparent = bui.getmesh('level_select_button_transparent')
        mask_tex = bui.gettexture('mapPreviewMask')
        assert tex_name is not None
        bui.imagewidget(
            parent=parent_widget,
            position=im_pos,
            size=(im_dim, im_dim * 0.5),
            transition_delay=delay,
            mesh_opaque=mesh_opaque,
            mesh_transparent=mesh_transparent,
            mask_texture=mask_tex,
            draw_controller=btn,
            texture=bui.gettexture(tex_name),
        )
        item['descriptionText'] = bui.textwidget(
            parent=parent_widget,
            text=desc,
            position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.36),
            transition_delay=delay,
            scale=b_width * (1.0 / 230.0) * base_text_scale * 0.78,
            maxwidth=b_width * 0.8,
            max_height=b_height * 0.14,
            size=(0, 0),
            h_align='center',
            v_align='center',
            draw_controller=btn,
            flatness=1.0,
            shadow=0.0,
            color=(0.6, 1, 0.6),
        )
        item['gameModesText'] = bui.textwidget(
            parent=parent_widget,
            text=modes,
            position=(b_pos[0] + b_width * 0.5, b_pos[1] + b_height * 0.26),
            transition_delay=delay,
            scale=b_width * (1.0 / 230.0) * base_text_scale * 0.65,
            maxwidth=b_width * 0.8,
            size=(0, 0),
            h_align='center',
            v_align='center',
            draw_controller=btn,
            shadow=0,
            flatness=1.0,
            color=(0.6, 0.8, 0.6),
        )

    if not item_name.startswith('icons.'):
        item['title_text'] = bui.textwidget(
            parent=parent_widget,
            text=title,
            position=(
                b_pos[0] + b_width * 0.5 + b_offs_x,
                b_pos[1] + b_height * title_v,
            ),
            transition_delay=delay,
            scale=b_width * (1.0 / 230.0) * base_text_scale,
            maxwidth=b_width * 0.8,
            size=(0, 0),
            h_align='center',
            v_align='center',
            draw_controller=btn,
            color=(0.7, 0.9, 0.7, 1.0),
        )

    item['purchase_check'] = bui.imagewidget(
        parent=parent_widget,
        position=(b_pos[0] + b_width * check_pos, b_pos[1] + b_height * 0.05),
        transition_delay=delay,
        mesh_transparent=bui.getmesh('checkTransparent'),
        opacity=0.0,
        size=(60, 60),
        color=(0.6, 0.5, 0.8),
        draw_controller=btn,
        texture=bui.gettexture('uiAtlas'),
    )
    item['price_widget'] = bui.textwidget(
        parent=parent_widget,
        text='',
        position=(
            b_pos[0] + b_width * 0.5 + b_offs_x,
            b_pos[1] + b_height * price_v,
        ),
        transition_delay=delay,
        scale=b_width * (1.0 / 300.0) * base_text_scale,
        maxwidth=b_width * 0.9,
        size=(0, 0),
        h_align='center',
        v_align='center',
        draw_controller=btn,
        color=(0.2, 1, 0.2, 1.0),
    )
    item['price_widget_left'] = bui.textwidget(
        parent=parent_widget,
        text='',
        position=(
            b_pos[0] + b_width * 0.33 + b_offs_x,
            b_pos[1] + b_height * price_v,
        ),
        transition_delay=delay,
        scale=b_width * (1.0 / 300.0) * base_text_scale,
        maxwidth=b_width * 0.3,
        size=(0, 0),
        h_align='center',
        v_align='center',
        draw_controller=btn,
        color=(0.2, 1, 0.2, 0.5),
    )
    item['price_widget_right'] = bui.textwidget(
        parent=parent_widget,
        text='',
        position=(
            b_pos[0] + b_width * 0.66 + b_offs_x,
            b_pos[1] + b_height * price_v,
        ),
        transition_delay=delay,
        scale=1.1 * b_width * (1.0 / 300.0) * base_text_scale,
        maxwidth=b_width * 0.3,
        size=(0, 0),
        h_align='center',
        v_align='center',
        draw_controller=btn,
        color=(0.2, 1, 0.2, 1.0),
    )
    item['price_slash_widget'] = bui.imagewidget(
        parent=parent_widget,
        position=(
            b_pos[0] + b_width * 0.33 + b_offs_x - 36,
            b_pos[1] + b_height * price_v - 35,
        ),
        transition_delay=delay,
        texture=bui.gettexture('slash'),
        opacity=0.0,
        size=(70, 70),
        draw_controller=btn,
        color=(1, 0, 0),
    )
    badge_rad = 44
    badge_center = (
        b_pos[0] + b_width * 0.1 + b_offs_x,
        b_pos[1] + b_height * 0.87,
    )
    item['sale_bg_widget'] = bui.imagewidget(
        parent=parent_widget,
        position=(badge_center[0] - badge_rad, badge_center[1] - badge_rad),
        opacity=0.0,
        transition_delay=delay,
        texture=bui.gettexture('circleZigZag'),
        draw_controller=btn,
        size=(badge_rad * 2, badge_rad * 2),
        color=(0.5, 0, 1),
    )
    item['sale_title_widget'] = bui.textwidget(
        parent=parent_widget,
        position=(badge_center[0], badge_center[1] + 12),
        transition_delay=delay,
        scale=1.0,
        maxwidth=badge_rad * 1.6,
        size=(0, 0),
        h_align='center',
        v_align='center',
        draw_controller=btn,
        shadow=0.0,
        flatness=1.0,
        color=(0, 1, 0),
    )
    item['sale_time_widget'] = bui.textwidget(
        parent=parent_widget,
        position=(badge_center[0], badge_center[1] - 12),
        transition_delay=delay,
        scale=0.7,
        maxwidth=badge_rad * 1.6,
        size=(0, 0),
        h_align='center',
        v_align='center',
        draw_controller=btn,
        shadow=0.0,
        flatness=1.0,
        color=(0.0, 1, 0.0, 1),
    )


# Once contacts are imported, we will create relationships
# Since Meetings are hierarchical along with Wagtail Pages,
# we need to move the existing pages to the correct parent.
# https://stackoverflow.com/a/57057466/1191545
import re

from django.core.exceptions import ObjectDoesNotExist
from contact.models import Meeting, MeetingPresidingClerk, Person, PersonIndexPage
from content_migration.management.shared import parse_csv_file

def extract_contact_id_from(contact_name: str) -> int:
    # Contact names contain contact ID in parenthesis
    regex = r"([0-9]+)"

    match = re.search(regex, contact_name).group(0)  # type: ignore

    return int(match)

def extract_given_name(given_name_with_contact_id: str) -> str:
    # name is first part before empty space
    return given_name_with_contact_id.split(" ")[0]

def extract_given_and_family_name(contact_name: str) -> list[str]:
    """Extract given and family names from a CiviCRM contact name string.

    CiviCRM contact export contains name in the following format.

    "family_name, given_name (contact_id)"
    """

    try:
        family_name, given_name_with_contact_id = contact_name.split(", ")
    except ValueError:
        family_name = ""
        given_name_with_contact_id = contact_name

    given_name = extract_given_name(given_name_with_contact_id)

    return [family_name, given_name]

def extract_contact_ids_from(relationship: dict) -> dict[str, int]:
    meeting_name = relationship["Contact B"]
    clerk_name = relationship["Contact A"]

    meeting_id = extract_contact_id_from(meeting_name)
    clerk_id = extract_contact_id_from(clerk_name)

    return {"meeting_id": meeting_id, "clerk_id": clerk_id}

def handle_import_civicrm_clerk_relationships(file_name: str) -> None:
    relationships = parse_csv_file(file_name)

    for relationship in relationships:
        contact_ids = extract_contact_ids_from(relationship)

        Meeting.objects.filter(
            civicrm_id=contact_ids["meeting_id"],
        ).exists()

        try:
            meeting = Meeting.objects.get(civicrm_id=contact_ids["meeting_id"])
        except ObjectDoesNotExist:
            print(
                f"Could not find meeting with CiviCRM ID { contact_ids['meeting_id'] }",  # noqa: E501
            )
            pass

        # try:
        #     clerk = Person.objects.get(civicrm_id=contact_ids["clerk_id"])
        # except ObjectDoesNotExist:
        #     print(
        #         f"Could not find person with CiviCRM ID { contact_ids['clerk_id'] }"  # noqa: E501
        #     )
        #     pass

        family_name, given_name = extract_given_and_family_name(
            relationship["Contact A"],
        )

        person_exists = Person.objects.filter(
            given_name=given_name,
            family_name=family_name,
        ).exists()

        if person_exists:
            person = Person.objects.get(
                given_name=given_name,
                family_name=family_name,
            )
        else:
            person = Person(
                given_name=given_name,
                family_name=family_name,
            )

            # Get the only instance of Person Index Page
            person_index_page = PersonIndexPage.objects.get()

            # Add person to site page hiererchy
            person_index_page.add_child(instance=person)
            person_index_page.save()

        meeting_presiding_clerk = MeetingPresidingClerk(
            meeting=meeting,
            person=person,
        )

        meeting_presiding_clerk.save()


from setuptools import find_packages, setup

with open("README.md", encoding="utf8") as f:
    readme = f.read()

setup(
    name="jupyterhub-tmpauthenticator",
    version="1.0.0",
    description="JupyterHub authenticator that hands out temporary accounts for everyone",
    url="https://github.com/jupyterhub/tmpauthenticator",
    author="Project Jupyter Contributors",  # founded by Yuvi Panda
    author_email="jupyter@googlegroups.com",
    license="3 Clause BSD",
    long_description=readme,
    long_description_content_type="text/markdown",
    entry_points={
        # Thanks to this, user are able to do:
        #
        #     c.JupyterHub.authenticator_class = "tmp"
        #
        # ref: https://jupyterhub.readthedocs.io/en/4.0.0/reference/authenticators.html#registering-custom-authenticators-via-entry-points
        #
        "jupyterhub.authenticators": [
            "tmp = tmpauthenticator:TmpAuthenticator",
        ],
    },
    packages=find_packages(),
    python_requires=">=3.8",
    install_require={
        "jupyterhub>=2.3.0",
        "traitlets",
    },
    extras_require={
        "test": [
            "aiohttp",
            "pytest",
            "pytest-asyncio",
            "pytest-cov",
            "pytest-jupyterhub",
        ],
    },
)


import requests
import os
url = 'http://ubmcmm.baidustatic.com/media/v1/0f000ZjaV0Hbb1uWirPKsf.jpg'
root = 'D://pics//'
path = root + url.split('/')[-1]#保留原来文件名

try:
    if not os.path.exists(root):
        os.mkdir(root)
    if not os.path.exists(path):
        r = requests.get(url)
        with open(path,'wb') as f:
            f.write(r.content)
            print('success')
    else:
        print('It already exists')
except:
    print('fail')


import unittest
from xml.etree import ElementTree as ET
from xmlcomparer import transformer

import logging
#logging.basicConfig(level=logging.DEBUG)

class TestTransformer(unittest.TestCase):
    def test_can_ignore_order(self):
        xml_incorrect_order = ET.parse("../../demo/sort_only2.xml")
        t = transformer.Transformer(xml_incorrect_order)
        t.ignore_tag_order(".*")
        xml_incorrect_order_transformed = t.transform()
        xml_correct_order = ET.parse("../../demo/sort_only1.xml")

        print("--- correct")
        print(ET.tostring(xml_correct_order.getroot(), encoding="unicode"))
        print("--- incorrect")
        print(ET.tostring(xml_incorrect_order.getroot(), encoding="unicode"))
        print("--- transformed")
        print(ET.tostring(xml_incorrect_order_transformed.getroot(), encoding="unicode"))


import requests

from utility.conversions import number_to_ranks

class LobbySetup:
    '''
    Retrieve lobby details. Must be in game.
    '''

    def __init__(self, headers):
        self.headers = headers

    def get_latest_season_id(self, region):
        try:
            response = requests.get(
                f"https://shared.{region}.a.pvp.net/content-service/v3/content", headers=self.headers, verify=False)
            content = response.json()
            for season in content["Seasons"]:
                if season["IsActive"]:
                    return season["ID"]
        except:
            return -1

    def get_matchmaking_history(self, region, puuid) -> list:
        '''
        Returns last 10 comp matches
        '''
        try:
            response = requests.get(
                f"https://pd.{region}.a.pvp.net/match-history/v1/history/{puuid}?queue=competitive", headers=self.headers, verify=False
            )

            content = response.json()
            match_list:list = []
            for match in content["History"]:
                match_list.append(match)
            return match_list
        except:
            return -1

    def get_match_id(self, region, puuid) -> str:
        '''
        Get match ID of current match.
        '''
        try:
            response = requests.get(
                f"https://glz-{region}-1.{region}.a.pvp.net/core-game/v1/players/{puuid}", headers=self.headers, verify=False)
            content = response.json()
            match_id: str = content["MatchID"]
            return match_id
        except:
            return -1

    def get_ongoing_match(self, region, match_id):
        '''
        Get ongoing match details
        '''
        try:
            response = requests.get(
                f"https://glz-{region}-1.{region}.a.pvp.net/core-game/v1/matches/{match_id}", headers=self.headers, verify=False)
            content = response.json()
            players = content["Players"]
            # Get all PUUIDs in lobby
            player_dict = {"puuid": [], "agent": [], "team": [], "incognito": bool}
            for player in players:
                # Do not look up streamer mode players.
                if player["PlayerIdentity"]["Incognito"] == False:
                    player_dict["incognito"].append(False)
                    player_dict["puuid"].append(player["Subject"])
                    player_dict["agent"].append(player["CharacterID"])
                    player_dict["team"].append(player["TeamID"])
                else:
                    player_dict["incognito"].append(True)
                    player_dict["agent"].append(player["CharacterID"])
                    player_dict["team"].append(player["TeamID"])

            map = str(content["MapID"]).rsplit("/", 1)[1]
            if content["ProvisioningFlow"] == "CustomGame":
                game_mode_in_ongoing_match = "Custom".upper()
            else:
                game_mode_in_ongoing_match = str(
                    content["MatchmakingData"]["QueueID"]).upper()
            return player_dict, map, game_mode_in_ongoing_match

        except:
            print('Error retrieving ongoing match.')
            return -1

    def get_player_mmr(self, region, player_id, seasonId):
        response = requests.get(
            f"https://pd.{region}.a.pvp.net/mmr/v1/players/{player_id}", headers=self.headers, verify=False)
        keys = ['CurrentRank', 'RankRating', 'Leaderboard']
        try:
            if response.ok:
                r = response.json()
                if r["QueueSkills"]["competitive"]["SeasonalInfoBySeasonID"][seasonId] or r["QueueSkills"]["competitive"]["SeasonalInfoBySeasonID"][seasonId]["NumberOfWinsWithPlacements"] != 0:
                    numberOfWins = r["QueueSkills"]["competitive"]["SeasonalInfoBySeasonID"][seasonId]["NumberOfWinsWithPlacements"]
                    numberOfGames = r["QueueSkills"]["competitive"]["SeasonalInfoBySeasonID"][seasonId]["NumberOfGames"]
                    winPercent = round(int(numberOfWins) /
                                       int(numberOfGames), 3) * 100
                else:
                    winPercent = 0
                rankTIER = r["QueueSkills"]["competitive"]["SeasonalInfoBySeasonID"][seasonId]["CompetitiveTier"]
                if int(rankTIER) >= 24:
                    rank = [rankTIER,
                            r["QueueSkills"]["competitive"]["SeasonalInfoBySeasonID"][seasonId]["RankedRating"],
                            r["QueueSkills"]["competitive"]["SeasonalInfoBySeasonID"][seasonId]["LeaderboardRank"],
                            ]
                elif int(rankTIER) not in (0, 1, 2):
                    rank = [rankTIER,
                            r["QueueSkills"]["competitive"]["SeasonalInfoBySeasonID"][seasonId]["RankedRating"],
                            'N/A',
                            ]
                else:
                    rank = [0, 0, 'N/A']
            else:
                print("Failed retrieving rank.")
                rank = [0, 0, 'N/A']
                winPercent = 0
        except TypeError:
            rank = [0, 0, 'N/A']
            winPercent = 0
        except KeyError:
            rank = [0, 0, 'N/A']
            winPercent = 0

        # Convert tier to rank name.
        rank[0] = number_to_ranks[rank[0]]
        return dict(zip(keys, rank)), winPercent


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Wed Apr 10 17:43:35 2019

@author: stef

This script is to analyse and produce some summary statistics for (a subset of)
all the NOS dictionaries extracted using the function scrape_nos.py.
Which dictionaries to analyse can be set using the ``which_files'' list.

"""

import os
import pickle
from collections import defaultdict
#import copy
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

####### Definitions
def unique(list1):
    '''
    # this functions takes a list of reference numbers for NOS and returns a list
    '''
    # without repetitions and a list with the repeated ref. numbers.
    # intilize a null list
    unique_list = []
    repeated_elems = []
    repeated_indices = []
    # traverse for all elements
    for ix, x in enumerate(list1):
        # check if exists in unique_list or not
        if x not in unique_list:
            # this list contains all unique refs, including the ones that are repeated
            unique_list.append(x)
        else:
            if x not in repeated_elems:
                repeated_elems.append(x)
                # get where the repeated indices are to then get the file names
                tmp = [i for i,y in enumerate(list1) if y==x]
                repeated_indices.append(tmp)
    repeated_counts = np.zeros(len(repeated_elems))
    for ii,x in enumerate(repeated_elems):
        repeated_counts[ii]= len([jj for jj in list1 if jj == x])
    return unique_list, repeated_elems, repeated_counts, repeated_indices

######## Main code

data_dir = '../../results/NOS/extracted'
summary_dir = '../../results/NOS/summary'
#set which NOS dictionaries to analyse
which_files = ['extracted_standards_New NOS 1.pickle', 'extracted_standards_New NOS 2.pickle']
for ii in range(1,23):
    which_files.append('extracted_standards_Old NOS {}.pickle'.format(ii))
#which_file = 'extracted_standards_Old NOS 22.pickle'

# Write the whole summary to file if WRITEFILE is True
version_load = 'new_' #'v2_'
WRITEFILE = True
if WRITEFILE:
    fsave  =open(os.path.join(summary_dir, version_load + 'summary1.txt'), 'w')

dict_len = []
sections_list = []
sections_count = {}

total_doc_extracted = 0
total_doc_failed = 0
total_doc_unique = 0

# collect all standards that failed, the repeated ones and those with missing urn
all_standard_failed = []
all_repeated_ref = []
all_repeated_counts = np.zeros(0)
all_repeated_files = defaultdict()
all_urnmissing_files = []
all_urnmissing_names = []

texts = []
for which_file in which_files:
    texts.append('Working on: ' + which_file + '\n')

    # load the file
    # choose the prefix of the version to load
    #version_load = 'new_' #'v2_'
    with open(os.path.join(data_dir, version_load + which_file),'rb') as f:
        standard_info, standard_ref, standard_failed, standard_files = pickle.load(f)

    # OBS1: Some standard refs are the same!
    # Get the unique and repeated ones
    unique_ref, repeated_ref, repeated_counts, repeated_indices = unique(standard_ref)
    all_repeated_ref += repeated_ref
    all_repeated_counts = np.concatenate((all_repeated_counts, repeated_counts))
    for irep, rep in enumerate(repeated_ref):
        all_repeated_files[rep] = {}
        for iind1,iind2 in enumerate(repeated_indices[irep]):
            all_repeated_files[rep]['file{}'.format(iind1+1)] = which_file[
                    20:30].replace('.','') + '/' + standard_files[iind2]
    total_doc_unique += len(unique_ref)

    # for all the repeated urns also collect the NOS name and the file name (?)

    # total number of files in this folder:
    Ntot = len(standard_failed)+len(standard_ref)
    Nfailed = len(standard_failed)

    all_standard_failed+= standard_failed
    total_doc_extracted += len(standard_ref)
    total_doc_failed += len(standard_failed)
    # print number of actually failed extractions
    texts.append('Correct extractions: {:4f}% \n'.format(len(standard_info)/Ntot*100))
    texts.append('That is, {} files over {} failed the extraction\n'.format(Nfailed, Ntot))

    #%%
    # Collect some info on the extracted dictionaries
    for idct, key_dct in enumerate(standard_info):
        dct = standard_info[key_dct]
        dict_len.append(len(dct))
        for key_sec in dct:
            if key_sec in sections_list:
                sections_count[key_sec] += 1
            else:
                sections_list.append(key_sec)
                sections_count[key_sec] = 1
            # if the section notes is present, get the filename because this was
            # a standard with missing URN (had the string '[URN]' instead)
            if key_sec == 'notes':
                all_urnmissing_files.append(which_file[20:30].replace(
                        '.','') + '/' + standard_files[idct])
                all_urnmissing_names.append(dct['Title'])

    texts.append('-'*60)

texts.append('Total numbers of extracted and failed files are {} and {}, respectively'.format(
        total_doc_extracted, total_doc_failed))

# now something about duplicates NOS
texts.append('Total numbers of unique and repeated URNs are {} and {}, respectively'.format(
        total_doc_unique - len(all_repeated_ref), len(all_repeated_ref)))

texts.append('Average number of repetitions for non-unique RNs is {}'.format(
        all_repeated_counts.mean()))
if all_repeated_counts.std()>0:
    high_rep_ref = np.where(all_repeated_counts>2)[0]
    texts.append('The number of RNs repeated more than two times is {}'.format(
            len(high_rep_ref)))
    texts.append('And these standards are: \n')
    for ii in high_rep_ref:
        texts.append(all_repeated_ref[ii])

# ADD example of extracted standard
texts.append('#'*80 + '\n')
texts.append('Example of extracted standard \n')

for key in dct:
    texts.append('\n Section name: {}. '.format(key))
    texts.append('Section content:')
    if isinstance(dct[key],list):
        for elem in dct[key]:
            texts.append(elem)
    else:
        texts.append(dct[key])

# finally, print a list of all failed NOS
# save everything on file and possibly print on screen too
if WRITEFILE:
    for text in texts:
        #print(text)
        fsave.write(text + '\n')

#plot histograms with count of section appearances across document
# (as a proportion of documents)
section_props = np.zeros((len(sections_count.keys())))
for ii,ikey in enumerate(sections_count):
    section_props[ii] = sections_count[ikey]/total_doc_extracted*100
    x = np.arange(ii+1) # this is a lazy and inefficient trick, yes

# sort them in descending order
IX = np.argsort(section_props)[::-1]
section_props = section_props[IX]
ticks = list(sections_count.keys())
ticks= [ticks[ii] for ii in IX]
fig, ax = plt.subplots(figsize = (8,4))
plt.bar(x, section_props)
plt.xticks(x, ticks, rotation = 'vertical')
plt.ylabel('% of NOS')
plt.xlabel('section')
plt.tight_layout()
plt.savefig(os.path.join(summary_dir, version_load + 'sections_counts.png'))
#plt.show()

# print the numbers too:
if WRITEFILE:
    fsave.write('\n')
    fsave.write('#'*80 + '\n')
    fsave.write('Counting how many times each section (i.e. performance criteria or glossary) appears in all the extracted NOS\n')
    fsave.write('\n')
    for ii,perc in enumerate(section_props):
        fsave.write('Section \'{}\' was extracted for {:.4f}% of the documents'.format(ticks[ii],perc))
        fsave.write('\n')

    fsave.write('\n')
    fsave.write('NOTE: The percentage of NOS with a \'note\' field (added during the \n'
                + 'extraction process) corresponds to those NOS that had \'[URN]\' \n'
            + 'or \'[Unique Reference Number]\' in the header instead of a proper \n' +
            'reference number. For these files I used the original URN field as their \n'
            + 'reference number.')
    fsave.write('\n')
    fsave.write('\n')

    fsave.write('#'*80 + '\n')
    fsave.write('All the standards that failed are: \n')
    for ifile in all_standard_failed:
        fsave.write(ifile)
        fsave.write('\n')

    fsave.close()

    # save the dictionary with the repeated URNs
    pd.DataFrame.from_dict(all_repeated_files, orient = 'index').to_csv(
            summary_dir + '/' + version_load + 'repeated_NOS.csv')

    # save the dictionary of files with missing URNs in their header
    #pd.DataFrame(all_urnmissing_files).to_csv(summary_dir + '/missing_urn_NOS.csv')
    pd.DataFrame({'filename of missing URN': all_urnmissing_files,
                  'standard name': all_urnmissing_names}).to_csv(
                        summary_dir + '/' + version_load + 'missing_urn_NOS.csv')