"""
This file is meant to optimize the import speed. Import modules from YOLOv7 projects
and Ultralytics take significant amount of time
"""
import glob
import math
import logging
import numpy as np
import os
import re
import time
import urllib

from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from threading import Thread

import cv2
import torch
import torch.nn as nn
import torchvision

logging.basicConfig(filename="history.log",
                    format="%(asctime)s - %(levelname)s - %(module)s: %(message)s",
                    datefmt="%Y-%m-%d %H:%M:%S %p",
                    level=logging.INFO)

"""
From utils.general
"""

def box_iou(box1, box2):
    # https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
    """
    Return intersection-over-union (Jaccard index) of boxes.
    Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
    Arguments:
        box1 (Tensor[N, 4])
        box2 (Tensor[M, 4])
    Returns:
        iou (Tensor[N, M]): the NxM matrix containing the pairwise
            IoU values for every element in boxes1 and boxes2
    """

    def box_area(box):
        # box = 4xn
        return (box[2] - box[0]) * (box[3] - box[1])

    area1 = box_area(box1.T)
    area2 = box_area(box2.T)

    # inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2)
    inter = (torch.min(box1[:, None, 2:], box2[:, 2:]) - torch.max(box1[:, None, :2], box2[:, :2])).clamp(0).prod(2)
    return inter / (area1[:, None] + area2 - inter)  # iou = inter / (area1 + area2 - inter)

def clean_str(s):
    # Cleans a string by replacing special characters with underscore _
    return re.sub(pattern="[|@#!¡·$€%&()=?¿^*;:,¨´><+]", repl="_", string=s)

def make_divisible(x, divisor):
    # Returns x evenly divisible by divisor
    return math.ceil(x / divisor) * divisor

def check_img_size(img_size, s=32):
    # Verify img_size is a multiple of stride s
    new_size = make_divisible(img_size, int(s))  # ceil gs-multiple
    if new_size != img_size:
        print('WARNING: --img-size %g must be multiple of max stride %g, updating to %g' % (img_size, s, new_size))
    return new_size

def clip_coords(boxes, img_shape):
    # Clip bounding xyxy bounding boxes to image shape (height, width)
    boxes[:, 0].clamp_(0, img_shape[1])  # x1
    boxes[:, 1].clamp_(0, img_shape[0])  # y1
    boxes[:, 2].clamp_(0, img_shape[1])  # x2
    boxes[:, 3].clamp_(0, img_shape[0])  # y2

def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
    # Rescale coords (xyxy) from img1_shape to img0_shape
    if ratio_pad is None:  # calculate from img0_shape
        gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1])  # gain  = old / new
        pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2  # wh padding
    else:
        gain = ratio_pad[0][0]
        pad = ratio_pad[1]

    coords[:, [0, 2]] -= pad[0]  # x padding
    coords[:, [1, 3]] -= pad[1]  # y padding
    coords[:, :4] /= gain
    clip_coords(coords, img0_shape)
    return coords

def xyxy2xywh(x):
    # Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right
    y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
    y[:, 0] = (x[:, 0] + x[:, 2]) / 2  # x center
    y[:, 1] = (x[:, 1] + x[:, 3]) / 2  # y center
    y[:, 2] = x[:, 2] - x[:, 0]  # width
    y[:, 3] = x[:, 3] - x[:, 1]  # height
    return y

def xywh2xyxy(x):
    # Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right
    y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x)
    y[:, 0] = x[:, 0] - x[:, 2] / 2  # top left x
    y[:, 1] = x[:, 1] - x[:, 3] / 2  # top left y
    y[:, 2] = x[:, 0] + x[:, 2] / 2  # bottom right x
    y[:, 3] = x[:, 1] + x[:, 3] / 2  # bottom right y
    return y

def apply_classifier(x, model, img, im0):
    # applies a second stage classifier to yolo outputs
    im0 = [im0] if isinstance(im0, np.ndarray) else im0
    for i, d in enumerate(x):  # per image
        if d is not None and len(d):
            d = d.clone()

            # Reshape and pad cutouts
            b = xyxy2xywh(d[:, :4])  # boxes
            b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1)  # rectangle to square
            b[:, 2:] = b[:, 2:] * 1.3 + 30  # pad
            d[:, :4] = xywh2xyxy(b).long()

            # Rescale boxes from img_size to im0 size
            scale_coords(img.shape[2:], d[:, :4], im0[i].shape)

            # Classes
            pred_cls1 = d[:, 5].long()
            ims = []
            for j, a in enumerate(d):  # per item
                cutout = im0[i][int(a[1]):int(a[3]), int(a[0]):int(a[2])]
                im = cv2.resize(cutout, (224, 224))  # BGR
                # cv2.imwrite('test%i.jpg' % j, cutout)

                im = im[:, :, ::-1].transpose(2, 0, 1)  # BGR to RGB, to 3x416x416
                im = np.ascontiguousarray(im, dtype=np.float32)  # uint8 to float32
                im /= 255.0  # 0 - 255 to 0.0 - 1.0
                ims.append(im)

            pred_cls2 = model(torch.Tensor(ims).to(d.device)).argmax(1)  # classifier prediction
            x[i] = x[i][pred_cls1 == pred_cls2]  # retain matching class detections

    return x

def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False,
                        labels=()):
    """Runs Non-Maximum Suppression (NMS) on inference results

    Returns:
         list of detections, on (n,6) tensor per image [xyxy, conf, cls]
    """

    nc = prediction.shape[2] - 5  # number of classes
    xc = prediction[..., 4] > conf_thres  # candidates

    # Settings
    min_wh, max_wh = 2, 4096  # (pixels) minimum and maximum box width and height
    max_det = 300  # maximum number of detections per image
    max_nms = 30000  # maximum number of boxes into torchvision.ops.nms()
    time_limit = 10.0  # seconds to quit after
    redundant = True  # require redundant detections
    multi_label &= nc > 1  # multiple labels per box (adds 0.5ms/img)
    merge = False  # use merge-NMS

    t = time.time()
    output = [torch.zeros((0, 6), device=prediction.device)] * prediction.shape[0]
    for xi, x in enumerate(prediction):  # image index, image inference
        # Apply constraints
        # x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0  # width-height
        x = x[xc[xi]]  # confidence

        # Cat apriori labels if autolabelling
        if labels and len(labels[xi]):
            l = labels[xi]
            v = torch.zeros((len(l), nc + 5), device=x.device)
            v[:, :4] = l[:, 1:5]  # box
            v[:, 4] = 1.0  # conf
            v[range(len(l)), l[:, 0].long() + 5] = 1.0  # cls
            x = torch.cat((x, v), 0)

        # If none remain process next image
        if not x.shape[0]:
            continue

        # Compute conf
        if nc == 1:
            x[:, 5:] = x[:, 4:5] # for models with one class, cls_loss is 0 and cls_conf is always 0.5,
                                 # so there is no need to multiplicate.
        else:
            x[:, 5:] *= x[:, 4:5]  # conf = obj_conf * cls_conf

        # Box (center x, center y, width, height) to (x1, y1, x2, y2)
        box = xywh2xyxy(x[:, :4])

        # Detections matrix nx6 (xyxy, conf, cls)
        if multi_label:
            i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T
            x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1)
        else:  # best class only
            conf, j = x[:, 5:].max(1, keepdim=True)
            x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres]

        # Filter by class
        if classes is not None:
            x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)]

        # Apply finite constraint
        # if not torch.isfinite(x).all():
        #     x = x[torch.isfinite(x).all(1)]

        # Check shape
        n = x.shape[0]  # number of boxes
        if not n:  # no boxes
            continue
        elif n > max_nms:  # excess boxes
            x = x[x[:, 4].argsort(descending=True)[:max_nms]]  # sort by confidence

        # Batched NMS
        c = x[:, 5:6] * (0 if agnostic else max_wh)  # classes
        boxes, scores = x[:, :4] + c, x[:, 4]  # boxes (offset by class), scores
        i = torchvision.ops.nms(boxes, scores, iou_thres)  # NMS
        if i.shape[0] > max_det:  # limit detections
            i = i[:max_det]
        if merge and (1 < n < 3E3):  # Merge NMS (boxes merged using weighted mean)
            # update boxes as boxes(i,4) = weights(i,n) * boxes(n,4)
            iou = box_iou(boxes[i], boxes) > iou_thres  # iou matrix
            weights = iou * scores[None]  # box weights
            x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True)  # merged boxes
            if redundant:
                i = i[iou.sum(1) > 1]  # require redundancy

        output[xi] = x[i]
        if (time.time() - t) > time_limit:
            print(f'WARNING: NMS time limit {time_limit}s exceeded')
            break  # time limit exceeded

    return output

"""
From models.common.py
"""

def autopad(k, p=None):  # kernel, padding
    # Pad to 'same'
    if p is None:
        p = k // 2 if isinstance(k, int) else [x // 2 for x in k]  # auto-pad
    return p

class Conv(nn.Module):
    # Standard convolution
    def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True):  # ch_in, ch_out, kernel, stride, padding, groups
        super(Conv, self).__init__()
        self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False)
        self.bn = nn.BatchNorm2d(c2)
        self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity())

    def forward(self, x):
        return self.act(self.bn(self.conv(x)))

    def fuseforward(self, x):
        return self.act(self.conv(x))

"""
From models.experimental.py
"""

class Ensemble(nn.ModuleList):
    # Ensemble of models
    def __init__(self):
        super(Ensemble, self).__init__()

    def forward(self, x, augment=False):
        y = []
        for module in self:
            y.append(module(x, augment)[0])
        # y = torch.stack(y).max(0)[0]  # max ensemble
        # y = torch.stack(y).mean(0)  # mean ensemble
        y = torch.cat(y, 1)  # nms ensemble
        return y, None  # inference, train output

def attempt_load(weights, map_location=None):
    # Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a
    model = Ensemble()
    for w in weights if isinstance(weights, list) else [weights]:
        # attempt_download(w)
        ckpt = torch.load(w, map_location=map_location)  # load
        model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().fuse().eval())  # FP32 model

    # Compatibility updates
    for m in model.modules():
        if type(m) in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU]:
            m.inplace = True  # pytorch 1.7.0 compatibility
        elif type(m) is nn.Upsample:
            m.recompute_scale_factor = None  # torch 1.11.0 compatibility
        elif type(m) is Conv:
            m._non_persistent_buffers_set = set()  # pytorch 1.6.0 compatibility

    if len(model) == 1:
        return model[-1]  # return model
    else:
        print('Ensemble created with %s\n' % weights)
        for k in ['names', 'stride']:
            setattr(model, k, getattr(model[-1], k))
        return model  # return ensemble

"""
From utils.datasets.py
"""
img_formats = ['bmp', 'jpg', 'jpeg', 'png', 'tif', 'tiff', 'dng', 'webp', 'mpo']  # acceptable image suffixes
vid_formats = ['mov', 'avi', 'mp4', 'mpg', 'mpeg', 'm4v', 'wmv', 'mkv', 'webm']  # acceptable video suffixes

def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32):
    # Resize and pad image while meeting stride-multiple constraints
    shape = img.shape[:2]  # current shape [height, width]
    if isinstance(new_shape, int):
        new_shape = (new_shape, new_shape)

    # Scale ratio (new / old)
    r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
    if not scaleup:  # only scale down, do not scale up (for better test mAP)
        r = min(r, 1.0)

    # Compute padding
    ratio = r, r  # width, height ratios
    new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
    dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1]  # wh padding
    if auto:  # minimum rectangle
        dw, dh = np.mod(dw, stride), np.mod(dh, stride)  # wh padding
    elif scaleFill:  # stretch
        dw, dh = 0.0, 0.0
        new_unpad = (new_shape[1], new_shape[0])
        ratio = new_shape[1] / shape[1], new_shape[0] / shape[0]  # width, height ratios

    dw /= 2  # divide padding into 2 sides
    dh /= 2

    if shape[::-1] != new_unpad:  # resize
        img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
    top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
    left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
    img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)  # add border
    return img, ratio, (dw, dh)

class LoadImages:  # for inference
    def __init__(self, path, img_size=640, stride=32):
        p = str(Path(path).absolute())  # os-agnostic absolute path
        if '*' in p:
            files = sorted(glob.glob(p, recursive=True))  # glob
        elif os.path.isdir(p):
            files = sorted(glob.glob(os.path.join(p, '*.*')))  # dir
        elif os.path.isfile(p):
            files = [p]  # files
        else:
            raise Exception(f'ERROR: {p} does not exist')

        images = [x for x in files if x.split('.')[-1].lower() in img_formats]
        videos = [x for x in files if x.split('.')[-1].lower() in vid_formats]
        ni, nv = len(images), len(videos)

        self.img_size = img_size
        self.stride = stride
        self.files = images + videos
        self.nf = ni + nv  # number of files
        self.video_flag = [False] * ni + [True] * nv
        self.mode = 'image'
        if any(videos):
            self.new_video(videos[0])  # new video
        else:
            self.cap = None
        assert self.nf > 0, f'No images or videos found in {p}. ' \
                            f'Supported formats are:\nimages: {img_formats}\nvideos: {vid_formats}'

    def __iter__(self):
        self.count = 0
        return self

    def __next__(self):
        if self.count == self.nf:
            raise StopIteration
        path = self.files[self.count]

        if self.video_flag[self.count]:
            # Read video
            self.mode = 'video'
            ret_val, img0 = self.cap.read()
            if not ret_val:
                self.count += 1
                self.cap.release()
                if self.count == self.nf:  # last video
                    raise StopIteration
                else:
                    path = self.files[self.count]
                    self.new_video(path)
                    ret_val, img0 = self.cap.read()

            self.frame += 1
            # print(f'video {self.count + 1}/{self.nf} ({self.frame}/{self.nframes}) {path}: \n', end='')

        else:
            # Read image
            self.count += 1
            img0 = cv2.imread(path)  # BGR
            assert img0 is not None, 'Image Not Found ' + path
            #print(f'image {self.count}/{self.nf} {path}: ', end='')

        # Padded resize
        img = letterbox(img0, self.img_size, stride=self.stride)[0]

        # Convert
        img = img[:, :, ::-1].transpose(2, 0, 1)  # BGR to RGB, to 3x416x416
        img = np.ascontiguousarray(img)

        return path, img, img0, self.cap

    def new_video(self, path):
        self.frame = 0
        self.cap = cv2.VideoCapture(path)
        self.nframes = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))

    def __len__(self):
        return self.nf  # number of files

class LoadStreams:  # multiple IP or RTSP cameras
    def __init__(self, sources='streams.txt', img_size=640, stride=32):
        self.mode = 'stream'
        self.img_size = img_size
        self.stride = stride

        if os.path.isfile(sources):
            with open(sources, 'r') as f:
                sources = [x.strip() for x in f.read().strip().splitlines() if len(x.strip())]
        else:
            sources = [sources]

        n = len(sources)
        self.imgs = [None] * n
        self.sources = [clean_str(x) for x in sources]  # clean source names for later
        for i, s in enumerate(sources):
            # Start the thread to read frames from the video stream
            print(f'{i + 1}/{n}: {s}... ', end='')
            url = eval(s) if s.isnumeric() else s

            # Remove support for Youtube video

            cap = cv2.VideoCapture(url)
            assert cap.isOpened(), f'Failed to open {s}'
            w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
            h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
            self.fps = cap.get(cv2.CAP_PROP_FPS) % 100

            _, self.imgs[i] = cap.read()  # guarantee first frame
            thread = Thread(target=self.update, args=([i, cap]), daemon=True)
            print(f' success ({w}x{h} at {self.fps:.2f} FPS).')
            thread.start()
        print('')  # newline

        # check for common shapes
        s = np.stack([letterbox(x, self.img_size, stride=self.stride)[0].shape for x in self.imgs], 0)  # shapes
        self.rect = np.unique(s, axis=0).shape[0] == 1  # rect inference if all shapes equal
        if not self.rect:
            print('WARNING: Different stream shapes detected. For optimal performance supply similarly-shaped streams.')

    def update(self, index, cap):
        # Read next stream frame in a daemon thread
        n = 0
        while cap.isOpened():
            n += 1
            # _, self.imgs[index] = cap.read()
            cap.grab()
            if n == 4:  # read every 4th frame
                success, im = cap.retrieve()
                self.imgs[index] = im if success else self.imgs[index] * 0
                n = 0
            if self.fps != 0:
                time.sleep(1 / self.fps)  # wait time
            else:
                time.sleep(0.2)   # in rtsp situation self.fps may be zero. to avoid div by zero, take constant sleep.

    def __iter__(self):
        self.count = -1
        return self

    def __next__(self):
        self.count += 1
        img0 = self.imgs.copy()
        if cv2.waitKey(1) == ord('q'):  # q to quit
            cv2.destroyAllWindows()
            raise StopIteration

        # Letterbox
        img = [letterbox(x, self.img_size, auto=self.rect, stride=self.stride)[0] for x in img0]

        # Stack
        img = np.stack(img, 0)

        # Convert
        img = img[:, :, :, ::-1].transpose(0, 3, 1, 2)  # BGR to RGB, to bsx3x416x416
        img = np.ascontiguousarray(img)

        return self.sources, img, img0, None

    def __len__(self):
        return 0  # 1E12 frames = 32 streams at 30 FPS for 30 years

"""
From ultralytics.yolo.utils.check.py
"""
FILE = Path(__file__).resolve()
ROOT = FILE.parents[2]  # YOLO

def check_suffix(file='yolov8n.pt', suffix=('.pt',), msg=''):
    # Check file(s) for acceptable suffix
    if file and suffix:
        if isinstance(suffix, str):
            suffix = [suffix]
        for f in file if isinstance(file, (list, tuple)) else [file]:
            s = Path(f).suffix.lower()  # file suffix
            if len(s):
                assert s in suffix, f"{msg}{f} acceptable suffix is {suffix}"

def check_file(file, suffix=''):
    # Search/download file (if necessary) and return path
    check_suffix(file, suffix)  # optional
    file = str(file)  # convert to str()
    if Path(file).is_file() or not file:  # exists
        return file
    elif file.startswith(('http:/', 'https:/')):  # download
        url = file  # warning: Pathlib turns :// -> :/
        file = Path(urllib.parse.unquote(file).split('?')[0]).name  # '%2F' to '/', split https://url.com/file.txt?auth
        if Path(file).is_file():
            logging.info(f'Found {url} locally at {file}')  # file already exists
        else:
            logging.info(f'Downloading {url} to {file}...')
            torch.hub.download_url_to_file(url, file)
            assert Path(file).exists() and Path(file).stat().st_size > 0, f'File download failed: {url}'  # check
        return file
    else:  # search
        files = []
        for d in 'models', 'yolo/data':  # search directories
            files.extend(glob.glob(str(ROOT / d / '**' / file), recursive=True))  # find file
        if not files:
            raise FileNotFoundError(f"'{file}' does not exist")
        elif len(files) > 1:
            raise FileNotFoundError(f"Multiple files match '{file}', specify exact path: {files}")
        return files[0]  # return file

"""
From ultralytics.yolo.utils.check.py
"""

def is_ascii(s) -> bool:
    """
    Check if a string is composed of only ASCII characters.

    Args:
        s (str): String to be checked.

    Returns:
        bool: True if the string is composed only of ASCII characters, False otherwise.
    """
    # Convert list, tuple, None, etc. to string
    s = str(s)

    # Check if the string is composed of only ASCII characters
    return all(ord(c) < 128 for c in s)

"""
From ultralytics.yolo.utils.plotting.py
"""

def scale_image(im1_shape, masks, im0_shape, ratio_pad=None):
    """
    Takes a mask, and resizes it to the original image size

    Args:
      im1_shape (tuple): model input shape, [h, w]
      masks (torch.Tensor): [h, w, num]
      im0_shape (tuple): the original image shape
      ratio_pad (tuple): the ratio of the padding to the original image.

    Returns:
      masks (torch.Tensor): The masks that are being returned.
    """
    # Rescale coordinates (xyxy) from im1_shape to im0_shape
    if ratio_pad is None:  # calculate from im0_shape
        gain = min(im1_shape[0] / im0_shape[0], im1_shape[1] / im0_shape[1])  # gain  = old / new
        pad = (im1_shape[1] - im0_shape[1] * gain) / 2, (im1_shape[0] - im0_shape[0] * gain) / 2  # wh padding
    else:
        pad = ratio_pad[1]
    top, left = int(pad[1]), int(pad[0])  # y, x
    bottom, right = int(im1_shape[0] - pad[1]), int(im1_shape[1] - pad[0])

    if len(masks.shape) < 2:
        raise ValueError(f'"len of masks shape" should be 2 or 3, but got {len(masks.shape)}')
    masks = masks[top:bottom, left:right]
    # masks = masks.permute(2, 0, 1).contiguous()
    # masks = F.interpolate(masks[None], im0_shape[:2], mode='bilinear', align_corners=False)[0]
    # masks = masks.permute(1, 2, 0).contiguous()
    masks = cv2.resize(masks, (im0_shape[1], im0_shape[0]))

    if len(masks.shape) == 2:
        masks = masks[:, :, None]
    return masks

class Annotator:
    # YOLOv8 Annotator for train/val mosaics and jpgs and detect/hub inference annotations
    def __init__(self, im, line_width=None, font_size=None, font='Arial.ttf', pil=False, example='abc'):
        assert im.data.contiguous, 'Image not contiguous. Apply np.ascontiguousarray(im) to Annotator() input images.'
        non_ascii = not is_ascii(example)  # non-latin labels, i.e. asian, arabic, cyrillic
        self.pil = pil or non_ascii
        if self.pil:  # use PIL
            self.im = im if isinstance(im, Image.Image) else Image.fromarray(im)
            self.draw = ImageDraw.Draw(self.im)
            self.font = ImageFont.load_default()  # For simplicity and Performance
        else:  # use cv2
            self.im = im
        self.lw = line_width or max(round(sum(im.shape) / 2 * 0.003), 2)  # line width

    def box_label(self, box, label='', color=(128, 128, 128), txt_color=(255, 255, 255)):
        # Add one xyxy box to image with label
        if self.pil or not is_ascii(label):
            self.draw.rectangle(box, width=self.lw, outline=color)  # box
            if label:
                w, h = self.font.getsize(label)  # text width, height (WARNING: deprecated) in 9.2.0
                # _, _, w, h = self.font.getbbox(label)  # text width, height (New)
                outside = box[1] - h >= 0  # label fits outside box
                self.draw.rectangle(
                    (box[0], box[1] - h if outside else box[1], box[0] + w + 1,
                     box[1] + 1 if outside else box[1] + h + 1),
                    fill=color,
                )
                # self.draw.text((box[0], box[1]), label, fill=txt_color, font=self.font, anchor='ls')  # for PIL>8.0
                self.draw.text((box[0], box[1] - h if outside else box[1]), label, fill=txt_color, font=self.font)
        else:  # cv2
            p1, p2 = (int(box[0]), int(box[1])), (int(box[2]), int(box[3]))
            cv2.rectangle(self.im, p1, p2, color, thickness=self.lw, lineType=cv2.LINE_AA)
            if label:
                tf = max(self.lw - 1, 1)  # font thickness
                w, h = cv2.getTextSize(label, 0, fontScale=self.lw / 3, thickness=tf)[0]  # text width, height
                outside = p1[1] - h >= 3
                p2 = p1[0] + w, p1[1] - h - 3 if outside else p1[1] + h + 3
                cv2.rectangle(self.im, p1, p2, color, -1, cv2.LINE_AA)  # filled
                cv2.putText(self.im,
                            label, (p1[0], p1[1] - 2 if outside else p1[1] + h + 2),
                            0,
                            self.lw / 3,
                            txt_color,
                            thickness=tf,
                            lineType=cv2.LINE_AA)

    def masks(self, masks, colors, im_gpu, alpha=0.5, retina_masks=False):
        """Plot masks at once.
        Args:
            masks (tensor): predicted masks on cuda, shape: [n, h, w]
            colors (List[List[Int]]): colors for predicted masks, [[r, g, b] * n]
            im_gpu (tensor): img is in cuda, shape: [3, h, w], range: [0, 1]
            alpha (float): mask transparency: 0.0 fully transparent, 1.0 opaque
        """
        if self.pil:
            # convert to numpy first
            self.im = np.asarray(self.im).copy()
        if len(masks) == 0:
            self.im[:] = im_gpu.permute(1, 2, 0).contiguous().cpu().numpy() * 255
        colors = torch.tensor(colors, device=im_gpu.device, dtype=torch.float32) / 255.0
        colors = colors[:, None, None]  # shape(n,1,1,3)
        masks = masks.unsqueeze(3)  # shape(n,h,w,1)
        masks_color = masks * (colors * alpha)  # shape(n,h,w,3)

        inv_alph_masks = (1 - masks * alpha).cumprod(0)  # shape(n,h,w,1)
        mcs = (masks_color * inv_alph_masks).sum(0) * 2  # mask color summand shape(n,h,w,3)

        im_gpu = im_gpu.flip(dims=[0])  # flip channel
        im_gpu = im_gpu.permute(1, 2, 0).contiguous()  # shape(h,w,3)
        im_gpu = im_gpu * inv_alph_masks[-1] + mcs
        im_mask = (im_gpu * 255)
        im_mask_np = im_mask.byte().cpu().numpy()
        self.im[:] = im_mask_np if retina_masks else scale_image(im_gpu.shape, im_mask_np, self.im.shape)
        if self.pil:
            # convert im back to PIL and update draw
            self.fromarray(self.im)

    def rectangle(self, xy, fill=None, outline=None, width=1):
        # Add rectangle to image (PIL-only)
        self.draw.rectangle(xy, fill, outline, width)

    def text(self, xy, text, txt_color=(255, 255, 255), anchor='top'):
        # Add text to image (PIL-only)
        if anchor == 'bottom':  # start y from font bottom
            w, h = self.font.getsize(text)  # text width, height
            xy[1] += 1 - h
        self.draw.text(xy, text, fill=txt_color, font=self.font)

    def fromarray(self, im):
        # Update self.im from a numpy array
        self.im = im if isinstance(im, Image.Image) else Image.fromarray(im)
        self.draw = ImageDraw.Draw(self.im)

    def result(self):
        # Return annotated image as array
        return np.asarray(self.im)

class Colors:
    # Ultralytics color palette https://ultralytics.com/
    def __init__(self):
        # hex = matplotlib.colors.TABLEAU_COLORS.values()
        hexs = ('FF3838', 'FF9D97', 'FF701F', 'FFB21D', 'CFD231', '48F90A', '92CC17', '3DDB86', '1A9334', '00D4BB',
                '2C99A8', '00C2FF', '344593', '6473FF', '0018EC', '8438FF', '520085', 'CB38FF', 'FF95C8', 'FF37C7')
        self.palette = [self.hex2rgb(f'#{c}') for c in hexs]
        self.n = len(self.palette)

    def __call__(self, i, bgr=False):
        c = self.palette[int(i) % self.n]
        return (c[2], c[1], c[0]) if bgr else c

    @staticmethod
    def hex2rgb(h):  # rgb order (PIL)
        return tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))

colors = Colors()  # create instance for 'from utils.plots import colors'


#
# Hardware:
# A USB C / PIC32 Breakout Board connected to an SSD1306-based OLED display
# (128x64 pixels) and an M5Stack joystick, both via I2C.
#
# Purpose:
# Illustrates the I2C Master functionality to manipulate two I2C Slaves for
# the purposes of the Kickstarter demo.
#

from usb_device import UsbDevice
from upside_down_display import UpsideDownDisplay
from ssd1306_i2c_slave_display import Ssd1306I2cSlaveDisplay
from keyboard import KeyboardThread
from splash_screen import SplashScreen
from font import Font

from box import Box
from tile import Tile
from ball import Ball
from paddle import Paddle
from playing_area import PlayingArea
from game_loop import GameLoop
from sampled_player import SampledPlayer
from computer_player import ComputerPlayer
from i2c_joystick_player import I2cJoystickPlayer

if __name__ == "__main__":
	with UsbDevice() as usb:
		usb.i2c.baud_rate_400khz()

		display = UpsideDownDisplay(Ssd1306I2cSlaveDisplay(usb.i2c.slave(0x3c)))
		display.initialise()

		with open("font-5x8.raw", "rb") as font_fd:
			font_5x8 = Font(Tile.from_raw(font_fd.read(), 32 * 6, 4 * 9), 6, 9)

		playing_area = PlayingArea(display)
		paddle_tile = Tile(2, 12, [0b11000000] * 12)
		paddle_x_offset = 2
		paddle_y_offset = 2
		paddle_speed = 1.5
		paddles = [
			Paddle(
				[playing_area.bounding_box.x + paddle_x_offset, -1],
				0,
				Box(
					playing_area.bounding_box.x + paddle_x_offset,
					playing_area.bounding_box.y + paddle_y_offset,
					paddle_tile.width,
					playing_area.bounding_box.height - 2 * paddle_y_offset),
				paddle_tile),
			Paddle(
				[playing_area.bounding_box.max_x - 1 - paddle_x_offset - paddle_tile.width // 2, -1],
				0,
				Box(
					playing_area.bounding_box.max_x - 1 - paddle_x_offset - paddle_tile.width,
					playing_area.bounding_box.y + paddle_y_offset,
					paddle_tile.width,
					playing_area.bounding_box.height - 2 * paddle_y_offset),
				paddle_tile)]

		players = [
			ComputerPlayer(paddles[0], max_speed = paddle_speed, difficulty = 0.1),
			SampledPlayer(
				I2cJoystickPlayer(paddles[1], usb.i2c.slave(0x52), 1, -128, paddle_speed / 128),
				interval = 0.01),
		]

		game = GameLoop(
			playing_area,
			Ball(
				playing_area.centre,
				[1.8, 0],
				playing_area.bounding_box,
				Tile(6, 6, [
					0b00110000,
					0b01111000,
					0b11111100,
					0b11111100,
					0b01111000,
					0b00110000])),
			players,
			score_font = font_5x8,
			message_font = font_5x8)

		with open("lophtware-128x64.raw", "rb") as logo_fd:
			logo = Tile.from_raw(logo_fd.read(), 128, 64)

		SplashScreen(logo).show(display)

		quit = False
		def on_keyboard_input(cmd):
			global quit
			quit = cmd == 'q'
			return quit

		keyboard = KeyboardThread(on_keyboard_input)

		playing_area.draw()
		while not quit:
			if not game.do_frame(display):
				break

			display.blit()

		with open("thanks-128x64.raw", "rb") as thanks_fd:
			thanks = Tile.from_raw(thanks_fd.read(), 128, 64)

		SplashScreen(thanks).show(display)


from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, SelectField
from wtforms.validators import DataRequired
from wtforms import ValidationError

class MyEmailValidation(object):
    def __init__(self, message=None):
        if not message:
            message = "Email isn't valid."
        self.message = message

    def __call__(self, form, field):
        if "@" not in field.data:
            raise ValidationError(self.message)

class CreateUsersForm(FlaskForm):
    email = StringField("Destination email", validators=[DataRequired(), MyEmailValidation()])
    role_selector = SelectField(
        "Select role for new user",
        choices=[("admin", 3), ("moderator", 4), ("operator", 5)]
    )
    submit = SubmitField("Create user")


from sqlalchemy.orm import Session

from fastapi import APIRouter, Depends, status
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.options import Options
from bs4 import BeautifulSoup
import random
import time

def scroll(driver):
    try:
        last_page_height = driver.execute_script("return document.documentElement.scrollHeight")
        while True:
            pause_time = random.uniform(1, 2)
            driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight);")
            time.sleep(pause_time)
            driver.execute_script("window.scrollTo(0, document.documentElement.scrollHeight-50)")
            time.sleep(pause_time)
            new_page_height = driver.execute_script("return document.documentElement.scrollHeight")

            if new_page_height == last_page_height:
                print("스크롤 완료")
                break
            else:
                last_page_height = new_page_height
    except Exception as e:
        print("에러 발생: ", e)

def scrape_youtube_results(keyword):
    service = Service(ChromeDriverManager(driver_version="111.0.5563.64").install())
    chrome_options = Options()
    chrome_options.add_argument("--headless")
    try:
        driver = webdriver.Chrome(service=service, options=chrome_options)
    except ValueError as e:
        raise ValueError(f"웹드라이버 에러 발생! : {e}")

    SEARCH_KEYWORD = keyword.replace(' ', '+')
    URL = "https://www.youtube.com/results?search_query=" + SEARCH_KEYWORD
    driver.get(URL)
    # scroll(driver)
    WebDriverWait(driver, 5).until(EC.presence_of_element_located((By.CSS_SELECTOR, 'ytd-video-renderer')))

    html_source = driver.page_source
    soup_source = BeautifulSoup(html_source, 'html.parser')
    content_total = soup_source.find_all(class_='yt-simple-endpoint style-scope ytd-video-renderer')
    content_total_title = list(map(lambda data: data.get_text().replace("\n", ""), content_total))
    content_total_link = list(map(lambda data: "https://youtube.com" + data["href"], content_total))

    # content_record_src = soup_source.find_all(class_='nline-metadata-item style-scope ytd-video-meta-block')
    # # content_record_src = soup_source.find_all(class_='shortViewCountText')
    # content_view_cnt = [content_record_src[i].get_text().replace('조회수 ', '') for i in range(5, len(content_record_src), 10)]
    # content_upload_date = [content_record_src[i].get_text() for i in range(6, len(content_record_src), 10)]
    # content_view_cnt = [content_record_src[i].get_text() for i in content_record_src]
    # content_upload_date = [content_record_src[i].get_text().replace('\n', '') for i in range(6, len(content_record_src), 10)]

    driver.quit()

    return {
        'title': content_total_title,
        'link': content_total_link,
        # 'view': content_view_cnt,
        # 'upload_date': content_upload_date
    }
youtube_video = APIRouter(prefix="/youtube", tags=["유튜브"])

@youtube_video.get(
    "",
    status_code=status.HTTP_200_OK,
    summary="유튜브 스크래핑",
    description="좋아요 수, 조회수, 영상 링크, 영상 제목 스크래핑",
)
def get_video(keyword: str):
    results = scrape_youtube_results(keyword)
    return results


import numpy
import scipy.interpolate
import scipy.optimize
import scipy.stats

# Columns of csv input file
incols = ['name','Vinoc','dilmin','dilfac','ntot','ninf','comments']

# Columns added to csv output file
outcols = ['mode','68lb','68ub','95lb','95ub','RM','SK']

# label/header for assay parameters
label = {
	# input
	'Vinoc': "Well volume (in mL)",
	'dilmin': "Starting dilution",
	'dilfac': "Dilution factor",
	'ndils': "# dilutions",
	'nreps': "# repeats/dilution",
	'name': "Label",
	'ninf': "# wells infected",
	'ntot': "# wells total",
	'comments': "Comment (optional)",
	# output
	'mode': 'mode log10(SIN/mL)',
	'68lb': '68%CI-lo log10(SIN/mL)',
	'68ub': '68%CI-hi log10(SIN/mL)',
	'95lb': '95%CI-lo log10(SIN/mL)',
	'95ub': '95%CI-hi log10(SIN/mL)',
	'RM': 'RM log10(TCID50/mL)',
	'SK': 'SK log10(TCID50/mL)',
}

# help_text associated with assay parameters
info = {
	'Vinoc': "Typical value for 96-well plate is 0.1 mL.",
	'dilmin': "Must be ≤ 1 (e.g. 10-fold as 0.1, 4-fold as 0.25).",
	'dilfac': "Must be < 1 (e.g. 10-fold as 0.1, 4-fold as 0.25).",
	'ndils': "Typically 7 or 8 or 11 or 12.",
	'nreps': "Typically 4 or 6 or 8.",
	'name': "An identifying label like StrainA-24h-exp1.",
	'ninf': "A list separated by [,] [.] or [tab].",
	'ntot': "A list separated by [,] [.] or [tab].",
	'comments': "Can be anything you want (e.g. 24h).",
}

# parameter values for the example assay
example = {
	'Vinoc': 0.1,
	'dilmin': 0.01,
	'dilfac': 0.1,
	'ndils': 11,
	'nreps': 8,
	'name': "example",
	'ninf': [8,8,8,8,8,7,7,5,2,0,0],
	'ntot': [8,8,8,8,8,8,8,8,8,8,8],
	'comments': '',
}

def RMSK(dilut,Npos,Ntot):
	# if only one well
	if len(Npos) < 2:
		return (numpy.nan,numpy.nan)
	# if no infected well
	elif numpy.sum(Npos) == 0.0:
		return (numpy.nan,numpy.nan)
	# if all wells infected
	elif numpy.sum(Ntot-Npos) == 0.0:
		return (numpy.nan,numpy.nan)
	df = abs( numpy.diff(dilut)[0] )
	frac = 1.0*numpy.cumsum(Npos[::-1])[::-1]
	frac = frac/(frac+numpy.cumsum(Ntot-Npos))
	# Reed-Muench
	idx = numpy.argmax(frac < 0.5)-1
	propdist = (frac[idx]-0.5)/(frac[idx]-frac[idx+1])
	RM = df*propdist - dilut[idx]
	# Spearman-Kaerber
	frac = 1.0*Npos/Ntot # comment out this line to use RM-like smoothing
	idx = numpy.argmin( frac < 1.0 )
	if idx == 0: # if frac<1 in lowest dilution column
		frac = numpy.hstack((1.0,frac))
		dilut = numpy.hstack((dilut[0]+df,dilut))
	SK = df*numpy.trapz(frac[idx:]) - dilut[idx]
	return (RM,SK)

class Assay(object):
	def __init__(self, Vinoc, dilmin, dilfac, ninf, ntot):
		# Save user input
		self.pack = {'Vinoc':Vinoc, 'dilmin':dilmin, 'dilfac':dilfac}
		# computer n (# of unspoiled wells)
		self.pack['ntot'] = numpy.array(ntot)
		# Compute k (# of wells infected)
		self.pack['ninf'] = numpy.array(ninf)
		# Compute n-k (# of wells uninfected)
		self.nmks = self.pack['ntot']-self.pack['ninf']
		# Raise flag if no well was infected (lower limit of detection)
		if sum(self.pack['ninf']) == 0.0:
			self.isempty = True
		else:
			self.isempty = False
		# Raise flag if all wells were infected (upper limit of detection)
		if sum(self.nmks) == 0.0:
			self.isfull = True
		else:
			self.isfull = False
		# Compute arg of lnqbase = exp[ - Vinoc * dilmin * dilfac^pow ]
		self.VDs = Vinoc * dilmin * dilfac**numpy.arange(len(self.nmks))
		# Compute the remainder of the assay payload
		self.payload()

	def lCmode(self):
		""" Computes the mode of the posterior PDF for lCvir, and the TCID50 via the Reed-Munch and Spearmann-Kaerber estimation methods. """
		if 'mode' in self.pack.keys():
			return self.pack['mode']
		# If no infected well: give lC upper bound
		if self.isempty or self.isfull:
			self.pack['mode'] = numpy.nan
			return self.pack['mode']
		# Estimate most likely lCvir value (mode of dist)
		bracket = -numpy.log10((self.VDs[0]*10.0, self.VDs[numpy.where(self.nmks)][0], self.VDs[-1]/10.0))
		res = scipy.optimize.minimize_scalar(lambda x: -self.lCcalc(x), bracket=bracket)
		assert res.success, 'Could not find lC mode'
		self.pack['mode'] = res.x
		return self.pack['mode']

	def lCcalc(self, lCvec):
		""" Compute posterior likelihood distribution, i.e. value of exp(lnProb), for all elements in vector lCvec, and returns it as a vector of the same size as lCvec, suitable for plotting. """
		P = numpy.ones_like(lCvec)
		for VD,n,k in zip(-self.VDs,self.pack['ntot'],self.pack['ninf']):
			pinfvec = -numpy.expm1(10.0**lCvec*VD)
			P *= scipy.stats.binom.pmf(k,n,pinfvec)
		return P

	def lCdist(self, lCvec=None):
		""" Creates (if not provided) and stores the lCvir vector, stores the posterior PDF vector computed by lCcalc for the values in lCvir, and computes and stores the CDF vector corresponding to the PDF for the values in lCvir. """
		if lCvec is None:
			if self.isempty or self.isfull:
				a = -numpy.log10(self.VDs[0])-10.0
				b = -numpy.log10(self.VDs[-1])+10.0
				lb = scipy.optimize.brentq(lambda x: self.lCcalc(x)-0.0001,a,b)
				ub = scipy.optimize.brentq(lambda x: self.lCcalc(x)-0.9999,a,b)
				lCvec = numpy.linspace(lb,ub,500)
			else:
				lCvec = numpy.arange(0.0,1.0,0.01)
				lCvec = numpy.hstack((lCvec-2,numpy.arange(-1.0,1.0,0.002),lCvec+1))
				lCvec += self.lCmode()
		self.pack['lCvec'] = lCvec
		# Compute posterior likelihood distribution (pdf) for lVec
		self.pack['pdf'] = self.lCcalc(lCvec)
		# Compute CDF from posterior likelihood dist
		self.pack['cdf'] = numpy.cumsum(self.pack['pdf'][1:]*numpy.diff(self.pack['lCvec']))
		# Re-normalize so that CDF is 1 at Cvir= max in lCvec
		self.pack['cdf'] = numpy.hstack((0.0,self.pack['cdf']))/self.pack['cdf'].max()

	def lCbounds(self):
		""" Computes and stores the 68% and 95% bounds of lCvir likelihood as a 4-element list: [68-lower,68-upper,95-lower, 95-upper]. """
		if 'cdf' not in self.pack.keys():
			self.lCdist()
		if self.isempty or self.isfull:
			return [numpy.nan]*4
		ppf = scipy.interpolate.interp1d( self.pack['cdf'], self.pack['lCvec'], bounds_error=False, fill_value=0.0 )
		subbounds = []
		for frac in (0.68,0.95):
			res = scipy.optimize.minimize_scalar(lambda x: ppf(x+frac)-ppf(x),bounds=(0.0,1.0-frac),method='bounded')
			assert res.success, 'Could not find credible region.'
			subbounds += list( ppf([res.x,res.x+frac]) )
		return subbounds

	def payload(self):
		# Compute Reed-Muench and Spearman-Kaerber
		for key,val in zip(('RM','SK'),RMSK(numpy.log10(self.VDs),self.pack['ninf'],self.pack['ntot'])):
			self.pack[key] = val
		self.pack['bounds'] = self.lCbounds()
		self.pack['dilutions'] = numpy.log10(self.VDs/self.pack['Vinoc'])
		self.pack['mode'] = self.lCmode()
		self.pack['mean'] = numpy.sum(self.pack['lCvec']*self.pack['pdf'])
		self.pack['mean'] /= self.pack['pdf'].sum()
		return self.pack


from IPython.display import clear_output
def basic_info():
    print("Welcome to Tic Tac Toe Board Game.")
    choice = "Wrong"
    choice1 = 'Wrong'
    while choice != 'Y':
        user1 = input("Please Your Name as User 1: ")
        choice = input(f"Your Name is {user1}, Correct? Y or N: ").upper()
    while choice1 != 'Y':
        user2 = input("Please Your Name as User 2: ")
        choice1 = input(f"Your Name is {user2}, Correct? Y or N: ").upper()
    return [user1,user2]

def board_pattern():
    board_1 = ["1","2","3"]
    #board_2 = ["-","-","-","-","-"]
    board_3 = ["4","5","6"]
    #board_4 = ["-","-","-","-","-"]
    board_5 = ["7","8","9"]
    borad = [board_1,board_3,board_5]
    return borad

def playing():
    board = board_pattern()
    count = 0
    clear_output()
    while count < 9:
        varis = input("Select position and mark. (format: posi,mark) ")
        posi,mark = varis.split(',')
        posi = int(posi)
        if posi <= 3: # use to replace the board 1 row
            board[0][posi-1] = mark
        elif posi <= 6:# use to replace the board 2 row
            posi = posi - 3
            board[1][posi-1] = mark
        elif posi <= 9:# use to replace the board 3 row
            posi = posi - 6
            board[2][posi-1] = mark
        for new_board in board:
            print(new_board)
        if board[0][0] == board[0][1] == board[0][2] == mark: # these line that following can be use a better method to do it
            return True, mark
        elif board[1][0] == board[1][1] == board[1][2] == mark:
            return True, mark
        elif board[2][0] == board[2][1] == board[2][2] == mark:
            return True, mark
        elif board[0][0] == board[1][0] == board[2][0] == mark:
            return True, mark
        elif board[0][1] == board[1][1] == board[2][1] == mark:
            return True, mark
        elif board[0][2] == board[1][2] == board[2][2] == mark:
            return True, mark
        elif board[0][0] == board[1][1] == board[2][2] == mark:
            return True, mark
        elif board[0][2] == board[1][1] == board[2][0] == mark:
            return True, mark
        count += 1
    return False, mark

def interact():
    user1,user2 = basic_info() #grab the basic info of users, return uesr1 and user2
    hashmap = {user1:'X',user2:'O'}
    for board in board_pattern():# print origenal board
        print(board)
    continues_game = input('Do you want to continue the game? Y or N ').upper()
    if continues_game == 'Y':
        print(f"First user will always go first, and {user1} uses X and {user2} uses O")
        status,mark = playing()
        if status == True:
            print(f'{list(hashmap.keys())[list(hashmap.values()).index(mark)]} is the winner') # This line to use value to get key
        else:
            print('Game is tied. No winner.')
    elif continues_game == 'N':
            print('Closing Game.')

interact()


import atexit
import logging.config
import logging.handlers
import os
import tempfile

import zmq

import slivka

_context = zmq.Context()
atexit.register(_context.destroy, 0)

class ZMQQueueHandler(logging.handlers.QueueHandler):
    def __init__(self, address, ctx: zmq.Context = None):
        ctx = ctx or _context
        socket = ctx.socket(zmq.PUSH)
        socket.connect(address)
        super().__init__(socket)

    def emit(self, record):
        message = self.format(record)
        msg = record.__dict__.copy()
        msg.update(
            message=message,
            msg=message,
            args=None,
            exc_info=None,
            exc_text=None
        )
        self.queue.send_json(msg)

class ZMQQueueListener(logging.handlers.QueueListener):
    def __init__(self, address, handlers=(), ctx: zmq.Context = None):
        self._address = address
        ctx = ctx or _context
        self._ctx = ctx
        socket = ctx.socket(zmq.PULL)
        socket.bind(address)
        super().__init__(socket, *handlers, respect_handler_level=False)
        self.handlers = list(self.handlers)

    def __enter__(self):
        self.start()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.stop()
        self.cleanup()

    def add_handler(self, handler):
        self.handlers.append(handler)

    def remove_handler(self, handler):
        self.handlers.remove(handler)

    def dequeue(self, block):
        msg = self.queue.recv_json()
        if msg == self._sentinel:
            self.queue.close(0)
            return msg
        else:
            return logging.makeLogRecord(msg)

    def enqueue_sentinel(self):
        socket = self._ctx.socket(zmq.PUSH)
        socket.connect(self._address)
        socket.send_json(self._sentinel)

    def stop(self):
        super().stop()
        self.queue.close(0)

    def cleanup(self):
        if self._address.startswith('ipc://'):
            os.unlink(self._address[6:])

def get_logging_sock():
    from hashlib import md5
    from base64 import b64encode
    home = slivka.conf.settings.directory.home
    suffix = b64encode(md5(home.encode()).digest()[:6], b'-_').decode()
    tmp = tempfile.gettempdir()
    path = 'ipc://%s/slivka-logging-%s.sock' % (tmp, suffix)
    return path

def _get_default_logging_config():
    return {
        'version': 1,
        'disable_existing_loggers': False,
        'formatters': {
            'full': {
                'format': "%(asctime)s %(levelname)-10s %(name)s %(message)s",
                'datefmt': "%d/%m/%y %H:%M:%S"
            },
            'minimal': {
                'format': '%(levelname)s %(message)s'
            }
        },
        'handlers': {
            'slivka.logging_queue': {
                'class': 'slivka.conf.logging.ZMQQueueHandler',
                'formatter': 'full',
                'level': 'DEBUG',
                'address': get_logging_sock()
            },
            'console': {
                'class': 'logging.StreamHandler',
                'formatter': 'minimal',
                'level': 'DEBUG'
            }
        },
        'loggers': {
            'slivka': {
                'level': 'DEBUG',
                'propagate': False,
                'handlers': ['slivka.logging_queue', 'console']
            }
        }
    }

def configure_logging(config=None):
    config = config or _get_default_logging_config()
    logging.config.dictConfig(config)


"""
FLUX: OPTIMUM RANGE
===================
"""

from math import isclose
from pathlib import Path
from typing import Literal

import matplotlib.gridspec as gridspec
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from matplotlib.legend_handler import HandlerTuple
from pandas import DataFrame

from diive.core.plotting.plotfuncs import save_fig

class FindOptimumRange:

    def __init__(self,
                 df: DataFrame,
                 xcol: str,
                 ycol: str,
                 n_vals_per_bin: int = 300,
                 bins_agg: Literal['median'] = 'median',
                 rwinsize: float = 0.1,
                 ragg: Literal['mean'] = 'mean',
                 define_optimum: Literal['min', 'max'] = 'max'):
        """
        Find x range for optimum y

        First, x data are aggregated in y bins. By default, the median
        value of x is calculated for each y bin (*bins_agg*). The number
        of bins that is used is defined by total length of data divided
        by *n_vals_per_bin*, i.e., each bin should contain e.g. 300 values.
        Then, the rolling mean (*ragg*) with window size *rwinsize* is
        calculated across all binned values. Here, *rwinsize* is given as
        the fraction of the total number of detected bins. The optimum is
        detected as the maximum (or other, *define_optimum*) of the values
        found in the rolling aggregation.

        Example: VPD (x) range where NEE (y) carbon uptake is highest (=smallest number)

        Args:
            df: Data
            xcol: Column name of x in df
            ycol: Column name of y in df
            n_vals_per_bin: Number of values per x bin
            bins_agg: How data in bins are aggregated
            rwinsize: Window size for rolling aggregation, expressed as fraction of
                the total number of bins. The total number of bins is calculated
                from the total length of the data and *n_vals_per_bin*. The resulting
                window size is then an integer value that is used in further calculations.
                If the integer window size results in an even number, +1 is added since
                the window size must be an odd number.
            ragg: Rolling aggregation that is used in the rolling window.
            define_optimum: Optimum can be based on 'min' or 'max'
        """
        self.df = df[[xcol, ycol]].copy()
        self.xcol = xcol
        self.ycol = ycol
        self.n_vals_per_bin = n_vals_per_bin
        self.bins_agg = bins_agg
        self.rwinsize = rwinsize
        self.ragg = ragg
        self.define_optimum = define_optimum

        self._results_optrange = {}

    @property
    def results_optrange(self) -> dict:
        """Return optimum range results"""
        if not self._results_optrange:
            raise Exception('Results for optimum range are empty')
        return self._results_optrange

    def find_optimum(self):
        # self._prepare_data() todo?

        bins_df, bin_aggs_df, n_xbins = self._divide_xdata_into_bins()

        winsize = int(n_xbins * self.rwinsize)
        winsize = winsize + 1 if (winsize % 2 == 0) else winsize  # Must be odd number
        rbin_aggs_df = self._rolling_agg(bin_aggs_df=bin_aggs_df,
                                         use_bin_agg=self.bins_agg,
                                         rolling_agg=self.ragg,
                                         winsize=winsize)

        roptimum_bin, roptimum_val = self._find_rolling_optimum(rolling_df=rbin_aggs_df,
                                                                use_rolling_agg=self.ragg)

        # rwinsize = int(num_xbins / 5)  # Window size for rolling aggs

        optimum_xstart, optimum_xend, optimum_ymean, \
        optimum_start_bin, optimum_end_bin = self._get_optimum_range(grouped_df=bin_aggs_df,
                                                                     roptimum_bin=roptimum_bin,
                                                                     winsize=winsize)
        self._validate(roptimum_val=roptimum_val, optimum_ymean=optimum_ymean)

        vals_in_optimum_range_df = \
            self._values_in_optimum_range(optimum_xstart=optimum_xstart, optimum_xend=optimum_xend)

        self._results_optrange = dict(
            optimum_xstart=optimum_xstart,
            optimum_xend=optimum_xend,
            optimum_ymean=optimum_ymean,
            optimum_start_bin=optimum_start_bin,
            optimum_end_bin=optimum_end_bin,
            bin_aggs_df=bin_aggs_df,
            rbin_aggs_df=rbin_aggs_df,
            rwinsize=winsize,
            roptimum_bin=roptimum_bin,
            roptimum_val=roptimum_val,
            n_xbins=n_xbins,
            xcol=self.xcol,
            ycol=self.ycol,
            vals_in_optimum_range_df=vals_in_optimum_range_df
        )

    def _values_in_optimum_range(self, optimum_xstart: float, optimum_xend: float) -> pd.DataFrame:
        df = self.df[[self.xcol, self.ycol]].copy()

        # Full data range
        fullrange_df = df.groupby(df.index.year).agg({self.xcol: ['count', 'mean']})

        xcounts_df = pd.DataFrame()
        # xcounts_df['vals_total'] = df.groupby(df.index.year).agg({'count'})
        xcounts_df['vals_total'] = \
            df.groupby(df.index.year).agg(vals_total=(self.xcol, 'count'))

        # Data in optimum
        _filter = (df[self.xcol] > optimum_xstart) & (df[self.xcol] <= optimum_xend)
        xcounts_df['vals_inoptimum'] = \
            df.loc[_filter].groupby(df.loc[_filter].index.year).agg(vals_inoptimum=(self.xcol, 'count'))

        # Above optimum
        _filter = (df[self.xcol] > optimum_xend)
        xcounts_df['vals_aboveoptimum'] = \
            df.loc[_filter].groupby(df.loc[_filter].index.year).agg(vals_aboveoptimum=(self.xcol, 'count'))

        # Below optimum
        _filter = (df[self.xcol] <= optimum_xstart)
        xcounts_df['vals_belowoptimum'] = \
            df.loc[_filter].groupby(df.loc[_filter].index.year).agg(vals_belowoptimum=(self.xcol, 'count'))

        # Percentages
        xcounts_df['vals_inoptimum_perc'] = xcounts_df['vals_inoptimum'].div(xcounts_df['vals_total']).multiply(100)
        xcounts_df['vals_aboveoptimum_perc'] = xcounts_df['vals_aboveoptimum'].div(xcounts_df['vals_total']).multiply(
            100)
        xcounts_df['vals_belowoptimum_perc'] = xcounts_df['vals_belowoptimum'].div(xcounts_df['vals_total']).multiply(
            100)

        # NaNs correspond to zero,
        # e.g. if no values above optimum are found
        xcounts_df = xcounts_df.fillna(0)

        return xcounts_df

    def _prepare_data(self):
        # Keep x values > 0
        self.df = self.df.loc[self.df[self.xcol] > 0, :]

    def _divide_xdata_into_bins(self) -> tuple[DataFrame, DataFrame, int]:
        """
        Divide x data into bins

        Column w/ bin membership is added to data

        Args:
            n_xbins: number of bins

        """
        bins_df = self.df.copy()

        # Detect number of x bins
        n_xbins = int(len(bins_df) / self.n_vals_per_bin)

        # Divide data into bins and add as column
        xbins = pd.qcut(bins_df[self.xcol], n_xbins, duplicates='drop')  # How awesome!
        bins_df = bins_df.assign(xbins=xbins)

        # Aggregate by bin membership
        bin_aggs_df = bins_df.groupby('xbins').agg({self.bins_agg, 'count'})

        return bins_df, bin_aggs_df, n_xbins

    def _rolling_agg(self, bin_aggs_df, use_bin_agg, winsize, rolling_agg):
        rolling_df = bin_aggs_df[self.ycol][use_bin_agg].rolling(winsize, center=True)
        return rolling_df.agg({rolling_agg, 'std'}).dropna()

    def _find_rolling_optimum(self, rolling_df: DataFrame, use_rolling_agg: str = 'mean'):
        """Find optimum bin in rolling data
        The rolling data is scanned for the bin with the highest or lowest value.
        """
        # Find bin with rolling mean min or max (e.g. max carbon uptake = minimum NEE value)
        roptimum_bin = None  # Index given as bin interval
        roptimum_val = None  # Value at bin interval
        if self.define_optimum == 'min':
            roptimum_bin = rolling_df[use_rolling_agg].idxmin()
            roptimum_val = rolling_df[use_rolling_agg][roptimum_bin]
        elif self.define_optimum == 'max':
            roptimum_bin = rolling_df[use_rolling_agg].idxmax()
            roptimum_val = rolling_df[use_rolling_agg].iloc[roptimum_bin]
        print(f"Optimum {self.define_optimum} found in class: {roptimum_bin}  /  value: {roptimum_val}")
        return roptimum_bin, roptimum_val

    def _get_optimum_range(self, grouped_df: DataFrame, roptimum_bin: pd.IntervalIndex, winsize: int):
        """Get data range (start and end) that was used to calculate rolling optimum"""

        # Find integer location of bin where rolling optimum value (y min or y max) was found
        int_loc = grouped_df.index.get_loc(roptimum_bin)
        print(f"Index integer location of found optimum: {int_loc}  /  {grouped_df.index[int_loc]}")

        # Get data range start and end
        roptimum_start_ix = int_loc - (int(winsize / 2))
        roptimum_end_ix = int_loc + (int(winsize / 2) + 1)  # was +1 b/c end of range not included in slicing

        # Optimum end index cannot be larger than available indices
        roptimum_end_ix = len(grouped_df) - 1 if roptimum_end_ix > len(grouped_df) - 1 else roptimum_end_ix

        # Optimum start index cannot be smaller than the first available index 0
        roptimum_start_ix = 0 if roptimum_start_ix < 0 else roptimum_start_ix

        # Get data range indices
        optimum_start_bin = grouped_df.iloc[roptimum_start_ix].name
        optimum_end_bin = grouped_df.iloc[roptimum_end_ix].name

        optimum_range_xstart = optimum_start_bin.left
        optimum_range_xend = optimum_end_bin.right
        optimum_range_ymean = grouped_df[self.ycol]['median'].iloc[roptimum_start_ix:roptimum_end_ix].mean()
        return optimum_range_xstart, optimum_range_xend, optimum_range_ymean, \
               optimum_start_bin, optimum_end_bin

    def _validate(self, roptimum_val, optimum_ymean):
        check = isclose(roptimum_val, optimum_ymean, abs_tol=10 ** -3)
        if check:
            print("Validation OK.")
        else:
            print("(!)Validation FAILED.")
            assert isclose(roptimum_val, optimum_ymean)

    def showfig(self,
                saveplot: bool = False,
                title: str = None,
                path: Path or str = None):
        fig = plt.figure(figsize=(16, 9))
        gs = gridspec.GridSpec(4, 1)  # rows, cols
        gs.update(wspace=.2, hspace=.5, left=.05, right=.95, top=.95, bottom=.05)
        ax1 = fig.add_subplot(gs[0:2, 0])
        ax2 = fig.add_subplot(gs[2, 0])
        ax3 = fig.add_subplot(gs[3, 0])
        ax = self.plot_vals_in_optimum_range(ax=ax1)
        ax = self.plot_bin_aggregates(ax=ax2)
        ax = self.plot_rolling_bin_aggregates(ax=ax3)
        fig.show()
        if saveplot:
            save_fig(fig=fig, title=title, path=path)

    def plot_vals_in_optimum_range(self, ax):
        """Plot optimum range: values in, above and below optimum per year"""

        # kudos: https://matplotlib.org/stable/gallery/lines_bars_and_markers/horizontal_barchart_distribution.html#sphx-glr-gallery-lines-bars-and-markers-horizontal-barchart-distribution-py

        # Get data
        df = self.results_optrange['vals_in_optimum_range_df'].copy()
        plotcols = ['vals_inoptimum_perc', 'vals_aboveoptimum_perc', 'vals_belowoptimum_perc']
        df = df[plotcols]
        df = df.round(1)
        # xcol = results_optrange['xcol']
        # ycol = results_optrange['ycol']

        # Names of categories, shown in legend above plot
        category_names = ['values in optimum range (%)', 'above optimum range (%)', 'below optimum range (%)']
        # category_names = ['vals_inoptimum_perc', 'vals_aboveoptimum_perc', 'vals_belowoptimum_perc']

        # Format data for bar plot
        results = {}
        for ix, row in df.iterrows():
            results[ix] = df.loc[ix].to_list()

        year_labels = list(results.keys())
        data = np.array(list(results.values()))
        data_cum = data.cumsum(axis=1)
        category_colors = plt.colormaps['RdYlBu_r'](np.linspace(0.20, 0.80, data.shape[1]))

        # fig, ax = plt.subplots(figsize=(9.2, 5))
        ax.invert_yaxis()
        ax.xaxis.set_visible(False)
        ax.set_xlim(0, np.sum(data, axis=1).max())

        for i, (colname, color) in enumerate(zip(category_names, category_colors)):
            widths = data[:, i]
            starts = data_cum[:, i] - widths
            rects = ax.barh(year_labels, widths, left=starts, height=0.9,
                            label=colname, color=color)

            r, g, b, _ = color
            text_color = 'white' if r * g * b < 0.5 else 'darkgrey'
            ax.bar_label(rects, label_type='center', color=text_color)
        ax.legend(ncol=len(category_names), bbox_to_anchor=(0, 1),
                  loc='lower left', fontsize='small')

        # default_format(ax=ax, txt_xlabel="year", txt_ylabel=f'counts',
        #                              txt_ylabel_units='[#]')
        # default_grid(ax=ax)

        return ax

    def plot_bin_aggregates(self, ax):
        """Plot y median in bins of x"""

        # Get data
        bin_aggs_df = self.results_optrange['bin_aggs_df'].copy()
        xcol = self.results_optrange['xcol']
        ycol = self.results_optrange['ycol']
        n_xbins = self.results_optrange['n_xbins']
        optimum_start_bin = self.results_optrange['optimum_start_bin']
        optimum_end_bin = self.results_optrange['optimum_end_bin']
        optimum_xstart = self.results_optrange['optimum_xstart']
        optimum_xend = self.results_optrange['optimum_xend']

        # Find min/max of y, used for scaling yaxis
        ymax = bin_aggs_df[ycol]['median'].max()
        ymin = bin_aggs_df[ycol]['median'].min()
        ax.set_ylim(ymin, ymax)

        # Show rolling mean
        bin_aggs_df[ycol]['median'].plot(ax=ax, zorder=99,
                                         title=f"{ycol} medians in {n_xbins} bins of {xcol}")

        # Show optimum range
        optimum_start_bin_ix = bin_aggs_df.index.get_loc(optimum_start_bin)
        optimum_end_bin_ix = bin_aggs_df.index.get_loc(optimum_end_bin)
        ax.axvline(optimum_start_bin_ix)
        ax.axvline(optimum_end_bin_ix)
        area_opr = ax.fill_between([optimum_start_bin_ix,
                                    optimum_end_bin_ix],
                                   ymin, ymax,
                                   color='#FFC107', alpha=0.5, zorder=1,
                                   label=f"optimum range {self.define_optimum} between {optimum_xstart} and {optimum_xend}")

        l = ax.legend(
            [area_opr],
            [area_opr.get_label()],
            scatterpoints=1,
            numpoints=1,
            handler_map={tuple: HandlerTuple(ndivide=None)},
            ncol=2)

    def plot_rolling_bin_aggregates(self, ax):
        """Plot rolling mean of y medians in bins of x"""

        # Get data
        rbin_aggs_df = self.results_optrange['rbin_aggs_df'].copy()
        xcol = self.results_optrange['xcol']
        ycol = self.results_optrange['ycol']
        n_xbins = self.results_optrange['n_xbins']
        optimum_start_bin = self.results_optrange['optimum_start_bin']
        optimum_end_bin = self.results_optrange['optimum_end_bin']
        optimum_xstart = self.results_optrange['optimum_xstart']
        optimum_xend = self.results_optrange['optimum_xend']

        # Find min/max across dataframe, used for scaling yaxis
        rbin_aggs_df['mean+std'] = rbin_aggs_df['mean'].add(rbin_aggs_df['std'])
        rbin_aggs_df['mean-std'] = rbin_aggs_df['mean'].sub(rbin_aggs_df['std'])
        dfmax = rbin_aggs_df[['mean+std', 'mean-std']].max().max()
        dfmin = rbin_aggs_df.min().min()
        ax.set_ylim(dfmin, dfmax)

        # Show rolling mean
        rbin_aggs_df.plot(ax=ax, y='mean', yerr='std', zorder=99,
                          title=f"Rolling mean of {ycol} medians in {n_xbins} bins of {xcol}")

        # Show optimum range
        optimum_start_bin_ix = rbin_aggs_df.index.get_loc(optimum_start_bin)
        optimum_end_bin_ix = rbin_aggs_df.index.get_loc(optimum_end_bin)
        ax.axvline(optimum_start_bin_ix)
        ax.axvline(optimum_end_bin_ix)
        area_opr = ax.fill_between([optimum_start_bin_ix,
                                    optimum_end_bin_ix],
                                   dfmin, dfmax,
                                   color='#FFC107', alpha=0.5, zorder=1,
                                   label=f"optimum range {self.define_optimum} between {optimum_xstart} and {optimum_xend}")

        l = ax.legend(
            [area_opr],
            [area_opr.get_label()],
            scatterpoints=1,
            numpoints=1,
            handler_map={tuple: HandlerTuple(ndivide=None)},
            ncol=2)

def example():
    pd.options.display.width = None
    pd.options.display.max_columns = None
    pd.set_option('display.max_rows', 3000)
    pd.set_option('display.max_columns', 3000)

    # Test data
    from diive.core.io.files import load_pickle
    df_orig = load_pickle(
        filepath=r"L:\Dropbox\luhk_work\20 - CODING\26 - NOTEBOOKS\GL-NOTEBOOKS\_data\ch-dav\CH-DAV_FP2022.1_1997-2022.08_ID20220826234456_30MIN.diive.csv.pickle")

    # # Check columns
    # import fnmatch
    # [print(col) for col in alldata_df.columns if any(fnmatch.fnmatch(col, ids) for ids in ['NEE_CUT_50*'])]

    # Select daytime data between May and September
    df = df_orig.copy()
    df = df.loc[(df.index.month >= 5) & (df.index.month <= 9)]
    df = df.loc[df['PotRad_CUT_REF'] > 20]

    # Optimum range
    optrange = FindOptimumRange(df=df, xcol='RH', ycol='NEE_CUT_REF_f', define_optimum="min", rwinsize=0.3)
    optrange.find_optimum()
    optrange.plot_results()

if __name__ == '__main__':
    example()


import numpy as np
import pandas as pd
from scipy.stats import norm
from sklearn.preprocessing import MinMaxScaler

from dash import  ALL
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate

from contents.app import *

@app.callback(
    Output('var-plot-sliders-container', 'children'),
    Input('var-plot-sliders', 'value'),
    State('the-data', 'data'),
)
def render_var_plot_sliders(sliders, data):
    if sliders and data:
        df = pd.DataFrame.from_dict(data)
        children = []
        for slider in sliders:
            if slider == 'index':
                min = df.index.min()
                max = df.index.max()
            else:
                min = df[slider].min()
                max = df[slider].max()
            children.append(html.Div([
                html.B(slider, style={'margin-top': '5px', 'white-space': 'nowrap'}),
                html.Div(style={'margin-top': '5px'}),
                html.Div([
                    dcc.RangeSlider(
                        id={'type': 'var-plot-slider', 'index': slider},
                        min=min,
                        max=max,
                        value=[min, max],
                        marks=None,
                        tooltip={'always_visible': False, 'placement': 'bottom'},
                    )
                ], style={'width': '100%', 'margin-top': '10px'})
            ], style={'display': 'flex'}))
        return children
    return []

@app.callback(
    Output('var-plot-container', 'children'),
    Input('var-plot-type', 'value'),
    Input('var-plot-scale-data', 'on'),
    Input('var-plot-sliders', 'value'),
    Input({'type': 'var-plot-slider', 'index': ALL}, 'value'),
    State('the-data', 'data')
)
def render_var_plot(plot_type, scale_data, feature_filters, filter_ranges, data):
    if plot_type and data:
        df = pd.DataFrame.from_dict(data)
        if feature_filters and filter_ranges:
            for feature, range in zip(feature_filters, filter_ranges):
                if feature == 'index': df = df[(df.index >= range[0]) & (df.index <= range[1])]
                else: df = df[(df[feature] >= range[0]) & (df[feature] <= range[1])]
        if scale_data: df = normalize_df(df)

        if plot_type == 'box':
            return [
                dcc.Graph(
                    figure={
                        'data': [go.Box(y=df[col], name=col, boxpoints='outliers') for col in df.columns],
                        'layout': go.Layout(
                            title='Feature Box Plot',
                            paper_bgcolor='rgba(0,0,0,0)',
                            plot_bgcolor='rgba(0,0,0,0)',
                            font=dict(color='#FFFFFF'),
                            xaxis=dict(showgrid=False),
                            yaxis=dict(showgrid=False),
                        ),
                    }
                ),
            ]
        elif plot_type == 'violin':
            return [
                dcc.Graph(
                    figure={
                        'data': [go.Violin(y=df[col], name=col, points='outliers', meanline_visible=True) for col in df.columns],
                        'layout': go.Layout(
                            title='Feature Violin Plot',
                            paper_bgcolor='rgba(0,0,0,0)',
                            plot_bgcolor='rgba(0,0,0,0)',
                            font=dict(color='#FFFFFF'),
                            xaxis=dict(showgrid=False),
                            yaxis=dict(showgrid=False),
                        ),
                    }
                ),
            ]

    return []

@app.callback(
    Output('dist-plot-sliders-container', 'children'),
    Input('dist-plot-sliders', 'value'),
    State('the-data', 'data'),
)
def render_dist_plot_sliders(sliders, data):
    if sliders and data:
        df = pd.DataFrame.from_dict(data)
        children = []
        for slider in sliders:
            if slider == 'index':
                min = df.index.min()
                max = df.index.max()
            else:
                min = df[slider].min()
                max = df[slider].max()
            children.append(html.Div([
                html.B(slider, style={'margin-top': '5px', 'white-space': 'nowrap'}),
                html.Div(style={'margin-top': '5px'}),
                html.Div([
                    dcc.RangeSlider(
                        id={'type': 'dist-plot-slider', 'index': slider},
                        min=min,
                        max=max,
                        value=[min, max],
                        marks=None,
                        tooltip={'always_visible': False, 'placement': 'bottom'},
                    )
                ], style={'width': '100%', 'margin-top': '10px'})
            ], style={'display': 'flex'}))
        return children
    return []

@app.callback(
    Output('dist-plot-container', 'children'),
    Input('dist-plot-scale-data', 'on'),
    Input('dist-plot-feature', 'value'),
    Input('dist-plot-distributions', 'value'),
    Input('dist-plot-sliders', 'value'),
    Input({'type': 'dist-plot-slider', 'index': ALL}, 'value'),
    State('the-data', 'data'),
)
def render_dist_plot(scale_data, feature, distributions, feature_filters, filter_ranges, data):
    if feature and distributions and data:
        df = pd.DataFrame.from_dict(data)
        if feature_filters and filter_ranges:
            for feature, range in zip(feature_filters, filter_ranges):
                if feature == 'index': df = df[(df.index >= range[0]) & (df.index <= range[1])]
                else: df = df[(df[feature] >= range[0]) & (df[feature] <= range[1])]
        if scale_data: df = normalize_df(df)

        graph = dcc.Graph(
            figure=go.Figure(
                layout=go.Layout(
                    title='Empirical vs Theoretical Distributions',
                    paper_bgcolor='rgba(0,0,0,0)',
                    plot_bgcolor='rgba(0,0,0,0)',
                    font=dict(color='#FFFFFF'),
                    xaxis=dict(
                        title=feature,
                        titlefont=dict(color='#FFFFFF'),
                        showgrid=False,
                    ),
                    yaxis=dict(
                        title='Density',
                        titlefont=dict(color='#FFFFFF'),
                        showgrid=False,
                    ),
                )
            )
        )
        graph.figure.add_trace(
            go.Histogram(
                x=df[feature],
                name=feature,
                histnorm='probability density',
                marker=dict(color='#37699b'),
            )
        )
        for dist in distributions:
            if dist == 'normal':
                mu, std = norm.fit(df[feature])
                x = np.linspace(min(df[feature]), max(df[feature]), 100)
                p = norm.pdf(x, mu, std)
                graph.figure.add_trace(
                    go.Scatter(
                        x=x,
                        y=p,
                        mode='lines',
                        name='Normal',
                    )
                )
            elif dist == 'lognormal':
                pass

        return graph

    return []

# Helper Methods
def normalize_df(df):
    """ Normalize a dataframe with MinMaxScaler (Keep the column names) """
    cols = df.columns
    df = pd.DataFrame(MinMaxScaler().fit_transform(df))
    df.columns = cols
    return df


import datetime as dt

f = open("def.dat", "r")

CLAIM_NUM = int(f.readline())
HST_RATE = float(f.readline)
CURR_DATE = dt.datetime.now()

f.close()

while True:
    emp_name = input("Employee name: ")
    emp_num = input("Employee number: ")
    location = input("Location: ")
    start_date = "2023-11-06"
    end_date = "2023-11-09"
    num_days = 3
    car_status = "O"

    num_km = 0
    if car_status == "O":
        num_km = 1400

    if num_days <= 3:
        per_diem = num_days * 85.00
    else:
        per_diem = num_days * 100.00

    if car_status == "O":
        mileage = num_km * 0.10
    else:
        mileage = num_days * 56.00

    claim_amt = per_diem + mileage
    taxes = claim_amt * HST_RATE
    claim_total = claim_amt + taxes

    print(f"Claim num: {CLAIM_NUM}")
    print(f"Employee num: {emp_num}")
    print(f"Employee name: {emp_name}")
    print(f"Location: {location}")
    print(f"Claim total: ${claim_total}")

    # write data to a file called claims.dat
    f = open("claims.dat", "a")  # a for append

    f.write(f"{CLAIM_NUM}, ")
    f.write(f"{str(CURR_DATE)}, ")
    f.write(f"{num_days}\n")

    CLAIM_NUM += 1
    print("Claim data written.")

    cont = input("Continue?: ").upper()

    if cont == "N":
        break

f = open("def.dat", "w")
f.write(f"{CLAIM_NUM}\n")
f.write(f"{HST_RATE}\n")
f.close()

print("Thank you for using the claim processing program.")


################011011100110010101101111####
### neo Command Line #######################
############################################

def getcmdlist():
    cmds = {
            "f"			:"Find And Replace : Find and replace in family parameters.",
            "froxl"		:"Family Replacer : Open Excel file.",
            "frp"		:"Family Replacer : in Project : Replace family and types of instances edited in Excel.",
            "frv"		:"Family Replacer : in ActiveView : Replace family and types of instances edited in Excel."
            }
    return cmds

def runcmd(cmd, msg, recallCL=False):

    if cmd == 'f':
        from lib.find import neo_findreplace_main as find
        find.Main()
    elif cmd == 'froxl':
        from lib.xl import neo_xl_type_replacer as tyrep
        tyrep.GetWb()
    elif cmd == 'frp':
        from lib.xl import neo_xl_type_replacer as tyrep
        tyrep.ImportXl("Project")
    elif cmd == 'frv':
        from lib.xl import neo_xl_type_replacer as tyrep
        tyrep.ImportXl("ActiveView")
    else:
        from neocl import unknowncmd
        unknowncmd(cmd, recallCL, getcmdlist())


import os
import glob
import pickle
import logging
import argparse
from multiprocessing import Pool

import numpy as np
import pandas as pd

from core.utils import timer, do_job

# PATH
DATA_PATH = os.getenv("DATA_PATH")
PREPROCESSED_DATA_PATH = os.getenv("PREPROCESSED_DATA_PATH")
TXT_DATA_NAME = os.getenv("TXT_DATA_NAME")
print(TXT_DATA_NAME)
DW2V_PATH = os.getenv("DW2V_PATH")
PARAM_PATH = os.getenv("PARAM_PATH")

# Logger
LOGGER = logging.getLogger('JobLogging')
LOGGER.setLevel(10)
fh = logging.FileHandler('job.log')
LOGGER.addHandler(fh)
formatter = logging.Formatter('%(asctime)s:%(lineno)d:%(levelname)s:%(message)s')
fh.setFormatter(formatter)
LOGGER.info("job start")

parser = argparse.ArgumentParser(description='train Dynamic Word Embeddings')
parser.add_argument('--without_preprocess', type=int, default=0, metavar='N',
                    help='if preprocessor is not neccessary, set 1')
parser.add_argument('--n_job', type=str, default="10", metavar='N',
                    help='number of cpu for multiprocessing')
parser.add_argument('--word_freq_min', type=str, default="5", metavar='N',
                    help='minmiun freqency for target word')
args = parser.parse_args()

os.environ["N_JOB"] = args.n_job
os.environ["WORD_FREQ_MIN"] = args.word_freq_min
N_JOB = int(os.getenv("N_JOB"))

if __name__ =="__main__":
    if args.without_preprocess == 0:
        # 前処理
        with do_job("preprocess tweet", LOGGER):
            from core.preprocess_tweet import preprocess_one_day_tweet

            TWEETS_PATHS = glob.glob(DATA_PATH+"alldata_20*")

            if not os.path.exists(PREPROCESSED_DATA_PATH+TXT_DATA_NAME):
                os.mkdir(PREPROCESSED_DATA_PATH+TXT_DATA_NAME)

            with Pool(processes=N_JOB) as p:
                p.map(preprocess_one_day_tweet, TWEETS_PATHS)

    # 単語の共起を確認
    with do_job("make co occ dict", LOGGER):
        from core.make_DW2V import make_unique_word2idx, make_whole_day_co_occ_dict

        TWEETS_PATHS = glob.glob(PREPROCESSED_DATA_PATH+TXT_DATA_NAME+"/*")

        # 全単語のチェック
        make_unique_word2idx(TWEETS_PATHS)

        if not os.path.exists(PREPROCESSED_DATA_PATH+"co_occ_dict_word_count/"):
            os.mkdir(PREPROCESSED_DATA_PATH+"co_occ_dict_word_count/")

        TWEETS_PATHS = glob.glob(PREPROCESSED_DATA_PATH+TXT_DATA_NAME+"/*")
        make_whole_day_co_occ_dict(TWEETS_PATHS)

    # PPMIの計算
    with do_job("make PPMI", LOGGER):
        from core.make_DW2V import make_whole_day_ppmi_list

        TWEETS_PATHS = sorted(glob.glob(PREPROCESSED_DATA_PATH+TXT_DATA_NAME+"/*"))
        DICTS_PATHS = sorted(glob.glob(PREPROCESSED_DATA_PATH+"co_occ_dict_word_count/*"))
        PATH_TUPLES = [(tweet_p, dict_p) for tweet_p, dict_p in zip(TWEETS_PATHS, DICTS_PATHS)]

        make_whole_day_ppmi_list(PATH_TUPLES)

    # DW2Vの計算
    with do_job("make DW2V", LOGGER):
        from core.make_DW2V import make_DW2V

        make_DW2V(PARAM_PATH+"params_0803.json")