import numpy as np
import numpy.ma as ma
from shapely import geometry
from shapely.ops import transform
from metpy.units import check_units, concatenate, units
from matplotlib.path import Path
from pyproj import Geod

def storm_objects(refc,proj,REFlev,REFlev1,big_storm,smoothed_ref,ax,rlons,rlats,storm_index,tracking_index,scan_index,tracks_dataframe,track_dis):
    #Inputs,
    #refc: Contours of smoothed_ref at first reflectivity threshold (REFlev)
    #proj: Projection of Earth's surface to be used for accurate area and distance calculations
    #REFlev: First reflectivity threshold
    #REFlev1: Second reflectivity threshold
    #big_storm: Threshold for "big storm" classification in km^2
    #smoothed_ref: Gaussian filtered reflectivity, masked below 20dBz
    #ax: Subplot object to be built on with each contour
    #rlons,rlats: Full volume geographic coordinates, longitude and latitude respectively
    #storm_index: Index to count and label storm objects as they are realized
    #tracking_index: Index to track which iteration of tracking algorithm is present
    #scan_index: Index used to track radar scans
    #tracks_dataframe: Final Pandas dataframe to be used to collect all output data
    #track_dis: Maximum distance for dislocation of storm obects between iterations to be considered same object

    #Empty arrays made for storm characteristics
    ref_areas = []
    max_lons_c = []
    max_lats_c = []
    storm_ids = []
    if np.max(smoothed_ref) > REFlev[0]:
        for level in refc.collections:
            #Loops through each closed reflectivity polygon in the contour list
            for contour_poly in level.get_paths():
                for n_contour,contour in enumerate(contour_poly.to_polygons()):
                    contour_a = np.asarray(contour[:])
                    xa = contour_a[:,0]
                    ya = contour_a[:,1]
                    polygon_new = geometry.Polygon([(i[0], i[1]) for i in zip(xa,ya)])
                    #Eliminates 'holes' in the polygons
                    if n_contour == 0:
                        polygon = polygon_new
                    else:
                        polygon = polygon.difference(polygon_new)

                #Transform the polygon's coordinates to the proper projection and calculate area
                #Testing a try statement here to eliminate some errors
                try:
                    pr_area = (transform(proj, polygon).area * units('m^2')).to('km^2')
                except:
                    continue
                #Use the polygon boundary to select all points within the polygon via a mask
                boundary = np.asarray(polygon.boundary.xy)
                polypath = Path(boundary.transpose())
                coord_map = np.vstack((rlons[0,:,:].flatten(), rlats[0,:,:].flatten())).T
                maskr = polypath.contains_points(coord_map).reshape(rlons[0,:,:].shape)
                meanr = np.mean(smoothed_ref[maskr])
                g = Geod(ellps='sphere')
                if pr_area > 10 * units('km^2') and meanr > REFlev[0]:
                    #For large reflectivity contours with embedded supercells, find the embedded storm cores
                    #Normal/"big storm" cutoff 300 km^2
                    if pr_area > big_storm * units('km^2'):
                        rlon_2 = rlons[0,:,:]
                        rlat_2 = rlats[0,:,:]
                        smoothed_ref_m = ma.masked_where(maskr==False, smoothed_ref)
                        smoothed_ref_m = ma.filled(smoothed_ref_m, fill_value = -2)
                        rlon2m = ma.MaskedArray(rlon_2, mask=maskr)
                        rlat2m = ma.MaskedArray(rlat_2, mask=maskr)

                        #This section uses the 2nd reflectivity threshold to subdivide big storms, in a similar manner to the
                        #previous section's method for finding storms
                        refc1 = ax.contour(rlon2m,rlat2m,smoothed_ref_m,REFlev1, linewidths = 3, linestyle = '--', alpha=.01)
                        #Look for reflectivity centroids
                        for level1 in refc1.collections:
                            for contour_poly1 in level1.get_paths():
                                for n_contour1,contour1 in enumerate(contour_poly1.to_polygons()):
                                    contour_a1 = np.asarray(contour1[:])
                                    xa1 = contour_a1[:,0]
                                    ya1 = contour_a1[:,1]
                                    polygon_new1 = geometry.Polygon([(i[0], i[1]) for i in zip(xa1,ya1)])
                                    if n_contour1 == 0:
                                        polygon1 = polygon_new1
                                    else:
                                        polygon1 = polygon1.difference(polygon_new1)

                                pr_area1 = (transform(proj, polygon1).area * units('m^2')).to('km^2')
                                boundary1 = np.asarray(polygon1.boundary.xy)
                                polypath1 = Path(boundary1.transpose())
                                maskr1 = polypath1.contains_points(coord_map).reshape(rlons[0,:,:].shape)
                                meanr1 = np.mean(smoothed_ref[maskr1])
                                #Add objects that fit requirements to the list of storm objects
                                if pr_area1 > 10 * units('km^2') and meanr1 > REFlev1[0]:
                                    ref_areas.append((pr_area.magnitude))
                                    max_lons_c.append((polygon1.centroid.x))
                                    max_lats_c.append((polygon1.centroid.y))
                                    #For tracking, assign ID numbers and match current storms to any previous storms that are close enough to be the same
                                    if tracking_index == 0:
                                        storm_ids.append((storm_index))
                                        storm_index = storm_index + 1
                                    else:
                                        max_lons_p = np.asarray(tracks_dataframe['storm_lon'].loc[scan_index-1].iloc[:])
                                        max_lats_p = np.asarray(tracks_dataframe['storm_lat'].loc[scan_index-1].iloc[:])
                                        storm_ids_p = np.asarray(tracks_dataframe['storm_id1'].loc[scan_index-1].iloc[:])
                                        dist_track = np.zeros((np.asarray(max_lons_p).shape[0]))
                                        for i in range(max_lons_p.shape[0]):
                                            distance_track = g.inv(polygon1.centroid.x, polygon1.centroid.y,
                                                                   max_lons_p[i], max_lats_p[i])
                                            dist_track[i] = distance_track[2]/1000.
                                        if np.min(dist_track) < track_dis:
                                            storm_ids.append((storm_ids_p[np.where(dist_track == np.min(dist_track))[0][0]]))
                                        else:
                                            storm_ids.append((storm_index))
                                            storm_index = storm_index + 1

                    #Do the same thing for objects from the 1st reflectivity threshold
                    else:
                        ref_areas.append((pr_area.magnitude))
                        max_lons_c.append((polygon.centroid.x))
                        max_lats_c.append((polygon.centroid.y))
                        if tracking_index == 0:
                            storm_ids.append((storm_index))
                            storm_index = storm_index + 1
                        else:
                            #To fix the problem of the algorithm crashing when it loses track of a storm, putting a try statement here
                            try:
                                max_lons_p = np.asarray(tracks_dataframe['storm_lon'].loc[scan_index-1].iloc[:])
                                max_lats_p = np.asarray(tracks_dataframe['storm_lat'].loc[scan_index-1].iloc[:])
                                storm_ids_p = np.asarray(tracks_dataframe['storm_id1'].loc[scan_index-1].iloc[:])
                                dist_track = np.zeros((np.asarray(max_lons_p).shape[0]))
                                for i in range(max_lons_p.shape[0]):
                                    distance_track = g.inv(polygon.centroid.x, polygon.centroid.y,
                                                           max_lons_p[i], max_lats_p[i])
                                    dist_track[i] = distance_track[2]/1000.

                                if np.min(dist_track) < track_dis:
                                    storm_ids.append((storm_ids_p[np.where(dist_track == np.min(dist_track))[0][0]]))
                                else:
                                    storm_ids.append((storm_index))
                                    storm_index = storm_index + 1
                            #If previous storms cant be accessed due to a long gap with no storms, add new indicies
                            except:
                                storm_ids.append((storm_index))
                                storm_index = storm_index + 1

    #Returning Variables,
    #storm_ids: List of Storm IDs to track from iteration to iteration, incremented using storm_index
    #max_lons_c,max_lats_c: Centroid coordinates of storm objects
    #ref_areas: Area of storm objects at reflectivity contours of REFlev or REFlev1 for standard and "big storms" respectively
    #storm_index: Index to count and label storm objects as they are realized; now including possible new objects from this iteration
    return storm_ids,max_lons_c,max_lats_c,ref_areas,storm_index


import spacy
import pandas as pd
import numpy as np
import nltk
from nltk.tokenize.toktok import ToktokTokenizer
import re
from bs4 import BeautifulSoup
import unicodedata
from c_text_word_2_number import text2int

nlp = spacy.load('en', parse = False, tag=False, entity=False)
tokenizer = ToktokTokenizer()
stopword_list = nltk.corpus.stopwords.words('english')
stopword_list.remove('no')
stopword_list.remove('not')
stopword_list.extend('&')
stopword_list.extend('@')
stopword_list.extend('#')
stopword_list.extend('$')
stopword_list.extend('%')
stopword_list.extend('^')
stopword_list.extend(';')

units = [
        'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight',
        'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen',
        'sixteen', 'seventeen', 'eighteen', 'nineteen',
        'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety',
        'hundred', 'thousand', 'million', 'billion', 'trillion']

def remove_accented_chars(text):
    text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('utf-8', 'ignore')
    return text

def strip_html_tags(text):
    soup = BeautifulSoup(text, "html.parser")
    stripped_text = soup.get_text()
    return stripped_text

def remove_stopwords(text, is_lower_case=False):
    tokens = tokenizer.tokenize(text)
    tokens = [token.strip() for token in tokens]
    if is_lower_case:
        filtered_tokens = [token for token in tokens if token not in stopword_list]
    else:
        filtered_tokens = [token for token in tokens if token.lower() not in stopword_list]
    filtered_text = ' '.join(filtered_tokens)
    return filtered_text

def lemmatize_text(text):
    text = nlp(text)
    text = ' '.join([word.lemma_ if word.lemma_ != '-PRON-' else word.text for word in text])
    return text

def remove_special_characters(text):
    text = re.sub('[^a-zA-z0-9\s]', '', text)
    return text

def normalize_corpus(corpus, html_stripping=True, contraction_expansion=True,
                     accented_char_removal=True, text_lower_case=True,
                     text_lemmatization=True, special_char_removal=True,
                     stopword_removal=True):

    normalized_corpus = []
    # normalize each document in the corpus
    for doc in corpus:

        # strip HTML
        if html_stripping:
            doc = strip_html_tags(doc)

        # remove accented characters
        if accented_char_removal:
            doc = remove_accented_chars(doc)

        # lowercase the text
        if text_lower_case:
            doc = doc.lower()

        # remove extra newlines
        doc = re.sub(r'[\r|\n|\r\n]+', ' ',doc)

        # insert spaces between special characters to isolate them
        special_char_pattern = re.compile(r'([{.(-)!}])')
        doc = special_char_pattern.sub(" \\1 ", doc)

        # lemmatize text
        if text_lemmatization:
            doc = lemmatize_text(doc)

        # remove special characters
        if special_char_removal:
            doc = remove_special_characters(doc)

        # remove extra whitespace
        doc = re.sub(' +', ' ', doc)

        # remove stopwords
        if stopword_removal:
            doc = remove_stopwords(doc, is_lower_case=text_lower_case)

        normalized_corpus.append(doc)

    return normalized_corpus

def word_2_numbers(context_line):
    tokens = nltk.word_tokenize(context_line[0])
    nums_idx = []
    idx = 0
    for n,word in enumerate(tokens):
        if word in stopword_list:
            tokens.remove(word)
        if word in units:
            nums_idx.append(n)

    dup = nums_idx.copy()
    dic = {}
    for id in nums_idx:
        if id+1 not in dup:
            continue
        else:
            if id+2 not in dup and len(dup)>2:
                new_word = tokens[id]+' '+tokens[id+1]
                dup.remove(id)
                dup.remove(id+1)
                dic[id] = new_word

            else:
                if id+3 not in dup and len(dup)>3:
                    new_word = tokens[id]+' '+tokens[id+1]+' '+tokens[id+2]
                    dup.remove(id)
                    dup.remove(id+1)
                    dup.remove(id+2)
                    dic[id] = new_word

                else:
                    if id+4 not in dup and len(dup)>4:
                        new_word = tokens[id]+' '+tokens[id+1]+' '+tokens[id+2]+' '+tokens[id+3]
                        dup.remove(id)
                        dup.remove(id+1)
                        dup.remove(id+2)
                        dup.remove(id+3)
                        dic[id] = new_word

    idx_values = dic.keys()
    new_context = tokens.copy()
    for i in idx_values:
        new_context[i] = dic[i]
        skip_length = len(dic[i].split())
        for removee in range(i+1,i+skip_length):
            try:
                new_context.remove(new_context[removee])
            except:
                continue

    return new_context

# cont_lines = [line.rstrip('\n') for line in open('./data/data/contexts.txt')]
# answer = [line.rstrip('\n') for line in open('./data/data/answers.txt')]

# pre = 0

# for i in range(0,252):
#     print(i)
#     test_line = cont_lines[i]
#     norm_line = normalize_corpus([test_line], text_lemmatization=False, stopword_removal=True, text_lower_case=True)
#     print('***************')
#     processed_tokens = word_2_numbers(norm_line)
#     final_context = []
#     for toks in processed_tokens:
#         try:
#             final_context.append(text2int(toks))
#         except:
#             final_context.append(toks)

#     ans = answer[i].lower()
#     print(ans)

#     print(final_context)
#     # for w in final_context:
#     #     if w==str(ans):
#     #         print(w,ans)
#     #     else:
#     #         print(ans,'not present')

#     print(type(final_context))
#     if ans in final_context:
#         print(ans,'present')
#         pre = pre+1
#     else:
#         ans_lis_1 = ans.split('.')
#         ans_lis_2 = ans.split(' ')
#         print(ans_lis_2)
#         #print(ans_lis_1)
#         #if ans_lis_1[0] in final_context:
#         #    print(ans_lis_1,'1111')
#         for k in range(len(final_context)):
#             #print(w)
#             if final_context[k]==ans_lis_2[0]:
#                 print('ci')

# print(pre,'hahahah')


import argparse
import sys
from ablit import AblitDataset

def check(original_text,
          abridged_text,
          original_version_psgs,
          abridged_version_psgs,
          paired_psgs):

    original_version_psgs = list(original_version_psgs)
    abridged_version_psgs = list(abridged_version_psgs)
    paired_psgs = list(paired_psgs)

    direct_orig_psgs = [psg.text for psg in original_version_psgs]
    direct_orig_text = "".join(direct_orig_psgs)
    assert direct_orig_text == original_text

    inferred_orig_psgs = [psg.original for psg in paired_psgs]
    inferred_orig_text = "".join(inferred_orig_psgs)
    assert inferred_orig_text == original_text

    assert direct_orig_text == inferred_orig_text

    direct_abrg_psgs = [psg.text for psg in abridged_version_psgs]
    direct_abrg_text = "".join(direct_abrg_psgs)
    assert direct_abrg_text == abridged_text

    inferred_abrg_psgs = [psg.abridged for psg in paired_psgs]
    inferred_abrg_text = "".join(inferred_abrg_psgs)
    assert inferred_abrg_text == abridged_text

    assert direct_abrg_text == inferred_abrg_text

    assert original_text == "".join([original_text[psg.start_char:psg.end_char]
                                     for psg in original_version_psgs])
    assert original_text == "".join([original_text[psg.original_start_char:psg.original_end_char]
                                     for psg in paired_psgs])

    assert abridged_text == "".join([abridged_text[psg.start_char:psg.end_char]
                                     for psg in abridged_version_psgs])

def test(dataset):
    for book in dataset.books:
        print("Checking data for book: {}...".format(book.book_title))
        for chapter in book.chapters:
            print("Chapter {}...".format(chapter.chapter_idx))

            print("checking sentences...")
            check(chapter.original,
                  chapter.abridged,
                  chapter.original_version.sentences,
                  chapter.abridged_version.sentences,
                  chapter.sentences)

            print("checking paragraphs...")
            check(chapter.original,
                  chapter.abridged,
                  chapter.original_version.paragraphs,
                  chapter.abridged_version.paragraphs,
                  chapter.paragraphs)

            print("checking chunks with merge_n_sentences=3...")
            check(chapter.original,
                  chapter.abridged,
                  chapter.original_version.chunks(merge_n_sentences=3),
                  chapter.abridged_version.chunks(merge_n_sentences=3),
                  chapter.chunks(merge_n_sentences=3))

            print("checking chunks with max_n_sentences=3...")
            check(chapter.original,
                  chapter.abridged,
                  chapter.original_version.chunks(max_n_sentences=3),
                  chapter.abridged_version.chunks(max_n_sentences=3),
                  chapter.chunks(max_n_sentences=3))

            print("checking row spans...")
            check(chapter.original,
                  chapter.abridged,
                  chapter.original_version.row_spans,
                  chapter.abridged_version.row_spans,
                  chapter.rows)

    print("OK!")

if __name__ == '__main__':
    parser = argparse.ArgumentParser()

    parser.add_argument("--data_dir", "-data_dir",
                        help="Directory path for dataset.",
                        type=str, required=True)

    args = parser.parse_args()

    dataset = AblitDataset(dirpath=args.data_dir)
    test(dataset)


"""Loads image assets from the disk and perform necessary processing.
"""
import numpy as np
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

import cv2

ASSET_FOLDER = Path(__file__).parent.joinpath("../assets")

@dataclass
class ImageLoader:
    """
    Attributes
        name: Name of tileset; can be different from its folder name.
        asset_root: Relative path to tileset folder.
        structure: files used for game structural-buildings.
        surface: files used as floor tiles.
        raw_image_set: Points to an ImageSet instance holding raw resolution images.

    """
    name: str
    asset_root: Path
    structure: Dict[str, Dict[str, str]]
    surface: Optional[Dict[str, List[Path]]] = field(default_factory=lambda: {})
    raw_image_set: Dict[int, Dict[str, Dict[str, Any]]] = field(default_factory=lambda: {})

    def __post_init__(self):
        """Formating and check data correctness."""
        assert self.name, "Make sure name is not empty."
        self.asset_root = Path(self.asset_root)
        # load and dump raw image arrays to an ImageSet instance
        for image_name, v in self.structure.items():
            path, sizes, type = self.unpack_path_size_type(v)
            images: List[np.ndarray] = self.load_image_from_disk(
                asset_path=path, split=len(sizes)
            )
            self.raw_image_set[type] = {
                "name": image_name,
                "images": images,
                "sizes": sizes
            }
        for k, v in self.surface.items():
            self.surface[k] = [Path(p) for p in v]

    @staticmethod
    def unpack_path_size_type(prop_set: Dict[str, Any]) -> Tuple[Path, List[Tuple[int]], int]:
        """Unpack image path, sub-image orientation and sizes
        from yaml parser to usable format.
        """
        asset_path: Path = Path(prop_set['path'])
        size_str: List[str] = prop_set['sizes'].split(", ")
        asset_type: int = int(prop_set['type'])
        asset_sizes: List(Tuple[int, int]) = [
            (int(x[0]), int(x[1])) for x in size_str
        ]
        return asset_path, asset_sizes, asset_type

    def load_image_from_disk(self, asset_path: Path, split: int = 1) -> List[np.ndarray]:
        """Load one image file to memory and perform necessary checks and split.
        """
        image_path: Path = ASSET_FOLDER.joinpath(self.asset_root).joinpath(asset_path)
        assert image_path.is_file(), f"Image file {image_path} not found."
        image: np.ndarray = cv2.imread(str(image_path), cv2.IMREAD_UNCHANGED)
        self.image_check(image)

        if split > 1:  # split on x-axis
            images: List[np.ndarray] = np.split(image, split, axis=1)
        else:
            images = [image]
        return [ImageLoader.crop_border(image) for image in images]

    @staticmethod
    def image_check(image: np.ndarray):
        """Basic image validation like shape and channel."""
        assert len(image.shape) == 3
        h, w, c = image.shape
        assert c == 4, "Image missing alpha channel."

    @staticmethod
    def crop_border(image: np.ndarray):
        """Remove blank border of an image. Needed for low-quality assets."""
        mask: np.ndarray = (image != 0)
        n: int = mask.ndim
        dims: Tuple[int, ...] = list(range(n))
        slices: List[int] = [None] * n

        for i in dims:
            mask_i = mask.any(tuple(dims[:i] + dims[i+1:]))
            slices[i] = (mask_i.argmax(), len(mask_i) - mask_i[::-1].argmax())
        return image[tuple([slice(*s) for s in slices])]

    def get_imageset(self):
        """Retrive processed raw image set."""
        return self.raw_image_set


import unittest
from PIL import Image
import ProbablyPython

class mazeUnitTests(unittest.TestCase):

    def test_pointException(self):
        with self.assertRaises(Exception) as context:
            ProbablyPython.Point("0", "1")
        self.assertTrue("Can not pass type string to class Point", str(context.exception))

    def test_setNextColor(self):
        inputs = {"sizeX": 51, "sizeY": 51, "shouldSolve": None, "solveColor": 1, "isGif": None}
        foo = [0,0,0,255]
        color = ProbablyPython.setNextColor(1, [255,0,0,255], [0,0,255,255], foo, foo, inputs)
        color = ProbablyPython.setNextColor(1, color, [0,0,255,255], foo, foo, inputs)
        self.assertEqual([0,0,255,255], color)

    def test_setEntranceExit(self):
        point = ProbablyPython.Point
        inputs = {"sizeX": 51, "sizeY": 51, "shouldSolve": None, "solveColor": 1, "isGif": None}
        myimage = Image.new('RGBA', (inputs['sizeX'],inputs['sizeY']), color=(0,0,0,255))
        img = myimage.load()
        coordinates = {'pos': point(1, 1), 'entrancePos': point(1, 1),
                       'exitPos': point(inputs['sizeX'] - 2, inputs['sizeY'] - 2)}
        ProbablyPython.setEntranceExit(img, coordinates, [0,0,255,255], [0,0,0,255], inputs)
        self.assertFalse(coordinates['entrancePos'].x > 1 and coordinates['entrancePos'].x < inputs['sizeX'] - 2
                         and coordinates['entrancePos'].y > 1 and coordinates['entrancePos'].y < inputs['sizeY'] - 2)
        self.assertFalse(coordinates['exitPos'].x > 1 and coordinates['exitPos'].x < inputs['sizeX'] - 2
                         and coordinates['exitPos'].y > 1 and coordinates['exitPos'].y < inputs['sizeY'] - 2)

class mazeIntegrationTests(unittest.TestCase):
    def test_mazeGen(self):
        data = {"sizeX": 51, "sizeY": 51, "shouldSolve": None, "solveColor": None, "isGif": None}
        image = ProbablyPython.generateMaze(data)
        img = image.load()
        testArray = []
        for x in range(image.size[0] - 1):
            for y in range(image.size[1] - 1):
                if x % 2 != 0 and y % 2 != 0:
                    testArray.append(img[x, y])
                    self.assertEqual(img[x, y], (255, 255, 255, 255))

    def test_mazeGenWithSolution(self):
        inputsRecieved = {"sizeX": 51, "sizeY": 51, "shouldSolve": True, "solveColor": 1, "isGif": None}
        image = ProbablyPython.generateMaze(inputsRecieved)
        img = image.load()
        testArray = []
        for x in range(image.size[0]-1):
            for y in range(image.size[1]-1):
                if x % 2 != 0 and y % 2 != 0:
                    testArray.append(img[x,y])
                    self.assertNotEqual(img[x,y], (0,0,0,255))

if __name__ == '__main__':
    unittest.main()


from django.conf import settings
from django.conf.urls.static import static
from django.urls import path

from health_indicators.views import (
    CreateIndicatorView,
    IndicatorDetailView,
    IndicatorListView,
)

app_name = "health_indicators"

urlpatterns = [
    path("", IndicatorListView.as_view(), name="indicator_list"),
    path("indicator/<int:pk>", IndicatorDetailView.as_view(), name="indicator"),
    # path("calendar/", CalendarView.as_view(), name="calendar"),
    path("indicator/add/", CreateIndicatorView.as_view(), name="add_indicator"),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)


# -*- coding: utf-8 -*-
"""
API to the plugin

Author:  Jorge Chato
Repo:    github.com/jorgechato/sauce
Web:     jorgechato.com
"""
import os
from flask import Flask
from flask import jsonify, request, abort, session
from flask.views import MethodView

from sauce.data import Data
from sauce.utils import *
from sauce.LSTM.text_generation import Generate

app = Flask(__name__)
global generate, graph
generate = Generate()
graph = generate.load()

class ModelLoader(MethodView):

    def __init__(self):
        """Initialize ModelLoader class."""
        pass

    def post(self):
        if not request.json:
            abort(400)

        seq = " "*40
        seq += request.json["input"].lower()
        if len(seq) > 40:
            seq = seq[-40:]

        with graph.as_default():
            prediction = generate.predict(seq, 5)

            return jsonify(
                    input = seq,
                    next  = prediction,
                    )

if __name__ == '__main__':
    port = int(os.environ.get('PORT', 5000))

    app.add_url_rule('/predict', view_func=ModelLoader.as_view('predict'))

    # If you are using debug=True it might broadcast a "failed to create cublas
    # handle: CUBLAS_STATUS_NOT_INITIALIZED" error
    app.run(host='0.0.0.0', port=port)


import os
import sys
sys.path.append('/home/brianyao/Documents/stad-cvpr2020')
import torch
from torchvision.transforms import functional as F
from torch.utils.data import DataLoader

from PIL import Image
import cv2
import glob
from utils.flow_utils import read_flow
from utils.build_samplers import make_data_sampler, make_batch_data_sampler

import json
from tqdm import tqdm
import pdb

class A3DDataset(torch.utils.data.Dataset):
    def __init__(self,
                 root,
                 split_file,
                 mode='gray',
                 transforms=None,
                 horizontal_flip=None,
                 save_dir='',
                 seq_len=10):
        self.W = 227
        self.H = 227
        img_root = os.path.join(root, 'frames/')
        flo_root = os.path.join('/mnt/workspace/datasets/A3D_2.0','flownet2/')
        self.mode = mode
        self.samples = []
        self.labels = []

        # each sample is 10 continuous images
        annos = json.load(open(split_file, 'r'))
        if 'train' in split_file:
            self.split = 'train'
        elif 'val' in split_file:
            self.split = 'val'
            with open(os.path.join(os.path.join(root, 'val_split_with_obj.txt'))) as f:
                self.valid_videos = f.read().splitlines()
            self.all_annotated_objs = {}
        for video_name, anno in tqdm(annos.items()):
            if mode == 'gray':
                all_images = sorted(glob.glob(os.path.join(img_root, video_name, 'images', '*.jpg')))
            elif mode == 'flow':
                all_images = sorted(glob.glob(os.path.join(flo_root, video_name, '*.flo')))
                all_images.insert(0, all_images[0])
            else:
                raise NameError('Mode unknown:', mode)

            # Only use normal frames for training
            max_frame_id = anno['anomaly_start'] - seq_len + 1
            if self.split == 'train':
                for idx in range(max_frame_id):
                    labels = torch.ones(seq_len)
                    self.samples.append((video_name,
                                         idx,
                                         all_images[idx:idx + seq_len],
                                         labels
                                         ))#[[] for t in range(seq_len)]
            else:
                if video_name not in self.valid_videos:
                    continue

                # Get labels
                labels = torch.zeros(anno['num_frames'])
                labels[anno['anomaly_start']: anno['anomaly_end']] = 1
                # NOTE: load all bbox annotations for evaluation use later
                detailed_annos = json.load(open(os.path.join(root, 'final_labels', video_name+'.json'), 'r'))
                annotated_objs = []
                for per_frame_label in detailed_annos['labels']:
                    objs = per_frame_label['objects']
                    if len(objs) > 0:
                        annotated_objs.append(torch.tensor([obj['bbox'] for obj in objs]))
                    else:
                        annotated_objs.append([])
                # Padding, make num_frames as the mutiple of sequence length
                while len(labels) % seq_len != 0:
                    all_images.append(all_images[-1])
                    labels = torch.cat((labels, labels[-1:]))
                    annotated_objs.append(annotated_objs[-1])
                self.all_annotated_objs[video_name] = annotated_objs

                # prepare samples for testing
                for t_idx, t in enumerate(range(0, len(labels), seq_len)):
                    inputs = []
                    self.samples.append((video_name,
                                         t,
                                         all_images[t:t+seq_len],
                                         labels[t:t+seq_len]
                                         ))       #annotated_objs[t:t+seq_len]

    def __getitem__(self, index):
        video_name, t_idx, file_names, labels = self.samples[index]

        inputs = []
        for file_name in file_names:
            if self.mode == 'gray':
                frame = Image.open(file_name).convert('L')
                # resize
                frame = F.resize(frame, (self.W, self.H))
                frame = F.to_tensor(frame)
            elif self.mode == 'flow':
                frame = read_flow(file_name)
                # resize
                frame = torch.tensor(cv2.resize(frame, (self.H, self.W)))
                frame = frame.permute(2, 1, 0)
            inputs.append(frame)
        # NOTE: convlstm takes (B,T,C,H,W), ConvAE takes (B, T*C, H, W)
        inputs = torch.cat(inputs, dim=0)
        return video_name, t_idx, inputs, labels

    def __len__(self):
        return len(self.samples)

def make_dataloader(root,
                    split_file,
                    is_train=True,
                    mode='gray',
                    shuffle=True,
                    distributed=False,
                    batch_per_gpu=1,
                    num_workers=0,
                    max_iters=10000):
    dataset = A3DDataset(root, split_file, mode=mode)

    sampler = make_data_sampler(dataset, shuffle=shuffle, distributed=distributed, is_train=is_train)
    batch_sampler = make_batch_data_sampler(dataset,
                                            sampler,
                                            aspect_grouping=False,
                                            batch_per_gpu=batch_per_gpu,
                                            max_iters=max_iters,
                                            start_iter=0,
                                            dataset_name='A3D')

    dataloader =  DataLoader(dataset,
                            num_workers=num_workers,
                            batch_sampler=batch_sampler)

    return dataloader


import datetime as dt
from decimal import Decimal
import itertools
from collections import namedtuple

class TimeZone:
    """TimeZone class"""

    def __init__(self, name:str, offset:float):
        """
        TimeZone initializer
        parameters:
            name: timezone name
            offset: the offset of timezone from UTC
        """
        self.name = name
        self.offset = offset

    @property
    def name(self):
        """TimeZone name"""
        return self._name

    @name.setter
    def name(self, value:str):
        if type(value) != str or not value:
            raise ValueError('Timezone name must be a non-empty string.')
        else:
            self._name = value.strip()

    @property
    def offset(self):
        """TimeZone offset hours"""
        return self._offset

    @offset.setter
    def offset(self, value:float):
        if type(value) != int and type(value) != float:
            raise ValueError('Hour offset must be a numeric value.')

        if value < -12 or value > 14:
            raise ValueError('Offset must be between -12:00 and +14:00.')

        self._offset = dt.timedelta(hours=value)

    def __repr__(self):
        return (f"TimeZone(name='{self.name}', offset={self.offset})")

class BankAcc:
    """This is the main class"""

    encodeTimeFormat = '%Y%m%d%H%M%S'
    trans_counter = itertools.count(100)
    trans_types = {
        'deposit' : 'D',
        'withdraw' : 'W',
        'interest' : 'I',
        'declined' : 'X',
    }
    accNumbers:list = [] # it could be more efficient if we had a database.
    INTREST_RATE = 0.5 # persent

    def __init__(self, accNum:int, name:str, family:str, \
         tz:TimeZone=None, balance:Decimal=Decimal('0.0')):
        """
        BankAcc initializer
        parameters:
            accNum: the account number of the customer
            name: name of customer
            family: family of customer
            tz: timezone of the customer
            balance: the initial value of customer balance
        """
        if self.is_valid_accNum(accNum):
            self.__accNum = accNum
            BankAcc.accNumbers.append(accNum)

        self.name = name
        self.family = family
        self.tz = tz if tz else TimeZone("Tehran", 3.5)

        if self.is_valid_balance(balance):
            self.__balance = balance

    fullname = property(fget = lambda self: self.name + " " + self.family, doc="Customer's fullname")

    @property
    def name(self):
        """name of the customer"""
        return self._name

    @name.setter
    def name(self, value:str):
        if not value or type(value) != str:
            raise ValueError("name must be a non-empty string.")
        else:
            self._name = value.strip()

    @property
    def family(self):
        """family of the customer"""
        return self._family

    @family.setter
    def family(self, value:str):
        if not value or type(value) != str:
            raise ValueError("family must be a non-empty string.")
        else:
            self._family = value.strip()

    @property
    def tz(self):
        """Customer timezone"""
        return self._tz

    @tz.setter
    def tz(self, value:TimeZone):
        if not isinstance(value, TimeZone):
            raise TypeError(f"tz must be an object of "
            f"class TimeZone you entered {value.__class__.__name__}.")
        else:
            self._tz = value

    @property
    def accNum(self):
        """unique account number"""
        return self.__accNum

    @staticmethod
    def is_valid_accNum(accNum:int) -> bool:
        """Validates customer's account number"""
        if type(accNum) != int:
            raise TypeError("accNum must be an integer.")

        if accNum in BankAcc.accNumbers:
            raise ValueError("accNum must be unique.")

        return True

    @property
    def balance(self):
        """Customer balance"""
        return self.__balance

    @staticmethod
    def is_valid_balance(balance:int) -> bool:
        """Validates the initial value of balance."""
        if type(balance) not in (int, float, Decimal):
            raise TypeError("balance must be a numeric value.")

        if balance < 0:
            raise ValueError("balance must be non-negative.")

        return True

    def generate_confirmation_code(self, transCode:str) -> str:
        """
        description: Generates confirmation code
        return:
            f"{the transaction code}-{the account number}-{the date/time in UTC of the transaction}-{the transaction number}"
        """
        trNum = next(BankAcc.trans_counter)
        dt_str = (dt.datetime.utcnow()+self.tz.offset).strftime(self.encodeTimeFormat)

        return f"{transCode}-{self.accNum}-{dt_str}-{trNum}"

    def confirmation_code_parser(self, confCode:str, timeFormat:str="%Y-%m-%d %H:%M:%S", utcTimeFormat:str="%Y-%m-%dT%H:%M:%S"):
        """Parses the confirmation code and return its information"""
        info = namedtuple("info", "transaction_code account_number time transaction_id time_utc")
        transaction_code, account_number, time, transaction_id, time_utc = *confCode.split("-"), dt.datetime.utcnow()
        time_utc = time_utc.strftime(utcTimeFormat)
        time = dt.datetime.strptime(time, self.encodeTimeFormat)
        time = time.strftime(timeFormat) + f" ({self.tz.name})"
        result = info(transaction_code, int(account_number), time, int(transaction_id), time_utc)
        return result

    def deposit(self, amount:Decimal) -> str:
        """
        Deposit money to the account.
        parameters:
            amount: amount of money to deposit
        return:
            result confirmation_code
        """
        if type(amount) not in (int, float, Decimal):
            raise TypeError("Deposit amount must be numeric.")
        else:
            amount = Decimal(amount)

        if amount <= Decimal('0.0'):
            raise ValueError("Deposit amount must be positive.")

        self.__balance += amount

        return self.generate_confirmation_code(self.trans_types['deposit'])

    def withdraw(self, amount:Decimal) -> str:
        """
        Withdraw money from the account
        parameters:
            amount: amount of money to withdraw
        return:
            result confirmation_code
        """
        if type(amount) not in (int, float, Decimal):
            raise TypeError("withdraw amount must be numeric.")
        else:
            amount = Decimal(amount)

        if amount <= Decimal('0.0'):
            raise ValueError("withdraw amount must be positive.")

        if self.balance < amount:
            return self.generate_confirmation_code(self.trans_types['declined'])
        else:
            self.__balance -= amount
            return self.generate_confirmation_code(self.trans_types['withdraw'])

    def pay_interest(self) -> str:
        """Pay monthly interest of customer"""
        amount = Decimal(self.balance * BankAcc.INTREST_RATE / 100)
        self.__balance += amount

        return self.generate_confirmation_code(self.trans_types['interest'])

if __name__ == "__main__":
    help(BankAcc)


#!python

from scipy.fftpack import dct, idct

# implement 2D DCT
def dct2(a):
    return dct(dct(a.T, norm='ortho').T, norm='ortho')

# implement 2D IDCT
def idct2(a):
    return idct(idct(a.T, norm='ortho').T, norm='ortho')

def viewable(orig):
    im = orig.copy()
    # make im viewable by scaling it all to the 0..1 range
    max_im = max(im[y][x]
                  for y in range(len(im))
                  for x in range(len(im[y])))
    min_im = min(im[y][x]
                  for y in range(len(im))
                  for x in range(len(im[y])))
    for y in range(len(im)):
        for x in range(len(im[y])):
            im[y][x] = (im[y][x]-min_im)/(max_im-min_im)
    return im

from skimage.io import imread
from skimage.color import rgb2gray
import numpy as np
import matplotlib.pylab as plt
from skimage.transform import rescale, resize, downscale_local_mean

plt.gray()
orig = rgb2gray(imread('dog.jpg'))
orig = resize(orig, (100,100))

# plot original and reconstructed images with matplotlib.pylab
plt.subplot(3,2,1)
plt.imshow(orig)
plt.axis('off')

imF = dct2(orig)
plt.subplot(3,2,2)
plt.imshow(viewable(imF))
plt.axis('off')

fraction = 10
for y in range(len(orig)):
    for x in range(len(orig[y])):
        if x > len(orig[y])//fraction or y > len(orig)//fraction:
            # blacken pixels that aren't in the top left corner
            orig[y][x] = 0
            imF[y][x] = 0
im1 = idct2(imF)

plt.subplot(3,2,4)
plt.imshow(viewable(imF))
plt.axis('off')

plt.subplot(3,2,5)
plt.imshow(orig)
plt.axis('off')

plt.subplot(3,2,6)
plt.imshow(im1)
plt.axis('off')
plt.show()


#!/usr/bin/env python
# coding=utf-8

from tokenize import Pointfloat
from xmlrpc.client import Boolean
import rospy
from std_msgs.msg import String, Bool, Int16
from geometry_msgs.msg import Point
from config.symmetries import ax12_symmetry
from cdf_msgs.msg import Pic_Action
import numpy as np

import time

DEFAULT_TEAM = "Jaune"  # couleur par défaut, chaîne de caractères exacte envoyée par le PIC
DEFAULT_TEMPO = 0.2  # tempo entre deux actions
DEFAULT_TIMEOUT = 20  # delai pour dire que l'action a échouée (pas vraiment utilisé dans cette version du code )

"""
Dans la suite du code env correspond a un objet MatchEnvironment ( défini dans match_handler.py ),
Cet objet contient les infos dont chaque fonction a besoin. Il dispose des attributs suivantes
env.script_name
env.state_team
env.TEAM_factor
env.state_status_match
env.state_motion_done
env.state_ax12_status
env.score_pub

et des fonctions suivantes:
env.update_team() => met a jour env.state_team et renvoie sa nouvelle valeur
env.update_team_factor() => met a jour env.TEAM_factor et renvoie sa nouvelle valeur

Les actions sont donc des fonctions qui prennent en paramètre:
- l'environment décrit précédemment ( nommé env et donné en premier paramètre )
- d'autres paramètres, qui seront donnés dans les scripts de matchs

Ce sont les actions qui permettent de temporiser les scripts de matchs. Il faut donc que les fonctions soient
bloquantes le temps que l'action se réalise. Pour se faire, il y a deux possibilités:
 1. un bon vieux sleep
 2. utiliser la fonction rospy.wait_for_message pour bloquer an attendant qu'un message arrive sur un topic
"""

#####################################################
# ACTIONS
#####################################################

def tempo(env, time):
    """
    Fonction bloquante pendant <time> secondes

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - time (float) : Le temps d'attente en secondes
    """
    rospy.loginfo("waiting %s secs" % time)
    rospy.sleep(time)

def score(env, plus_score):
    """
    Fonction qui change et affiche le score sur le lcd du PIC

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - s (int) : Le nombre de point à ajouter au score
    """
    newscore = env.score + plus_score
    rospy.loginfo("score: %s" % newscore)
    print_lcd(env, 1, "             ")
    print_lcd(env, 2, "Score: %d" % newscore)
    env.score = newscore

def wait_init(env):
    """
    Fonction permettant de gérer la sélection de l'équipe en phase d'initialisation

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
    """
    rospy.loginfo("waiting Init button press")
    buttons = rospy.wait_for_message(
        env.topic_dict["buttons"],
        String)
    print_lcd(env, 1, "Choix Equipe :")
    print_lcd(env, 2, "   Jaune   ")
    while buttons.data[3] != "1":
        buttons = rospy.wait_for_message(
            env.topic_dict["buttons"],
            String)
        env.state_team = "Jaune" if int(buttons.data[2]) == 0 else "Violet"
        rospy.loginfo("Team changed to %s" % env.state_team)
        if env.state_team == "Jaune":
            print_lcd(env, 2, "   Jaune   ")
            ledrgb(env, 255, 255, 0)
        elif env.state_team == "Violet":
            ledrgb(env, 127, 0, 255)
            print_lcd(env, 2, "   Violet   ")
    env.update_team_factor()
    rospy.loginfo("Init Buttons Pressed with team %s!" % env.state_team)

def wait_recal(env):
    """
    Fonction attendant l'ordre de démarrage du recalage

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
    """
    rospy.loginfo("waiting Recal button press")
    buttons = rospy.wait_for_message(
        env.topic_dict["buttons"],
        String)
    while buttons.data[4] != "1":
        buttons = rospy.wait_for_message(
            env.topic_dict["buttons"],
            String)
    ledrgb(env, 0, 0, 255)
    rospy.loginfo("Recal Buttons Pressed !")

def init_ax(env):
    """
    Fonction qui initialise les axes du robot

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
    """
    rospy.loginfo("restart timer")
    msg = Pic_Action()

    msg.action_destination = 'motor'
    msg.action_msg = "restarttimer"
    env.action_pub.publish(msg)

    rospy.loginfo("init ax12")
    msg.action_msg = "INITAX"
    env.action_pub.publish(msg)
    rospy.sleep(4)
    for command in []: # Remplir la liste avec les actions a réaliser à l'initialisation
        succes = False
        rospy.loginfo("doing AX12Action: %s" % command)
        msg.action_msg = command
        env.action_pub.publish(msg)

def wait_laisse(env):
    """
    Attend que la laisse soit déclenchée et démarre les timers

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
    """
    ledrgb(env, 255, 255, 0) if env.state_team == DEFAULT_TEAM else ledrgb(env, 127, 0, 255)

    rospy.loginfo("waiting the start of the match")

    buttons = rospy.wait_for_message(
        env.topic_dict["buttons"],
        String)
    while buttons.data[1] != "0":
        buttons = rospy.wait_for_message(
            env.topic_dict["buttons"],
            String)
        if buttons.data[5] == "1":
            rospy.loginfo("reset button pressed !")
            raise rospy.ROSInterruptException
    ledrgb(env, 255, 0, 0)

    # On démarre les timers
    msg = Pic_Action()
    msg.action_destination = 'user'
    msg.action_msg = "starttimer"
    env.action_pub.publish(msg)
    msg.action_destination = 'motor'
    msg.action_msg = "starttimer"
    env.action_pub.publish(msg)

    # On mémorise le temps de départ
    env.start_timestamp = time.time()

    rospy.loginfo("Match started !")

def check_position(env, status, raise_on_evitement=False):
    """
    Vérification de la position du robot

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - status (bool) : True si on veut vérifier que le robot est à la position attendue, False si on veut vérifier que le robot n'est pas à la position attendue
    """
    try:
        dt = time.time() - env.start_timestamp
    except:
        dt = -1.
    if (dt > 95) and (raise_on_evitement):
        rospy.logerr("90s with raise_on_evitement=True")
        raise RuntimeError("90s triggered")
    try:
        rospy.loginfo("checking position")
        action_done = rospy.wait_for_message(
            env.topic_dict["motion_done"],
            Bool, timeout=DEFAULT_TIMEOUT)
        while action_done.data != status:
            action_done = rospy.wait_for_message(
                env.topic_dict["motion_done"],
                Bool, timeout=DEFAULT_TIMEOUT)
            if env.robot_stopped == True:
                if raise_on_evitement:
                    rospy.logerr("evitement triggered wiht raise_on_evitement=True")
                    raise RuntimeError("evitement triggered")
                else:
                    while env.robot_stopped == True:
                        pass
                    return False
        return status
    except rospy.exceptions.ROSException as e:
        if rospy.is_shutdown():
            raise e
        rospy.loginfo("action timed out !")
        return False

def chainedmove(env, x, y, raise_on_evitement=False):
    success = False
    while not success:
        rospy.loginfo("chainedmove to %s, %s" % (x, y))
        env.nav_goal_pub.publish(Point(x, y, 0))
        success = check_position(env, False, raise_on_evitement)
        success = check_position(env, True, raise_on_evitement) and not success

def moveseg_xy(env, x, y, raise_on_evitement=False):
    '''
    Fonction qui envoi une commande de mouvement rectiligne à une position x, y

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - x (float) : La position en x
        - y (float) : La position en y
    '''
    success = False
    while not success:
        rospy.loginfo("moving to %.3f, %.3f" % (x, (y*env.TEAM_factor)))
        msg = Pic_Action()
        # todo: check format commande
        msg.action_destination = 'motor'
        msg.action_msg = "MOVESEG %.3f %.3f" % (x, y*env.TEAM_factor)
        env.action_pub.publish(msg)
        success = check_position(env, False, raise_on_evitement)
        success = check_position(env, True, raise_on_evitement) and not success

def move_arc(env, x, y):
    """
    Fonction qui envoi une commande de mouvement circulaire à une position x, y

    Paramètres
    ----------

        - env (<MatchEnvironment>) : L'environment du match
        - x (float) : La position en x
        - y (float) : La position en y
    """
    success = False
    while not success:
        rospy.loginfo("moving arc to %.3f, %.3f" % (x, (y*env.TEAM_factor)))
        msg = Pic_Action()
        # todo; check TEAM factor ( sur x ou y? )
        # todo: check format commande
        msg.action_destination = 'motor'
        msg.action_msg = "MOVE %.3f %.3f" % (x, y*env.TEAM_factor)
        env.action_pub.publish(msg)
        rospy.sleep(DEFAULT_TEMPO)
        success = check_position(env, False)
        success = check_position(env, True) and not success

def angle(env, theta):
    """
    Fonction qui envoi une commande d'angle au robot

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - theta (float) : L'angle à atteindre
    """
    success = False
    while not success:
        rospy.loginfo("changing angle to %.3f" % (theta * env.TEAM_factor))
        msg = Pic_Action()
        msg.action_destination = 'motor'
        msg.action_msg = "ANGLE %.3f" % (theta * env.TEAM_factor)
        env.action_pub.publish(msg)
        rospy.sleep(DEFAULT_TEMPO)
        success = check_position(env, False)
        success = check_position(env, True) and not success

def move_speed(env, v, time):
    """
    Fonction qui dépace le robot à une vitesse `v` pendant `time` secondes

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - v (float) : La vitesse de déplacement
        - time (float) : Le temps de déplacement
    """
    rospy.loginfo("move speed at v:%.3f pendant:%.3f" % (v, time))
    msg = Pic_Action()
    msg.action_destination = 'motor'
    msg.action_msg = "SPEED %.3f 0" % v
    env.action_pub.publish(msg)
    rospy.sleep(time)
    motion_free(env)

def move_endstop(env, v):
    """
    Fonction qui dépace le robot à une vitesse `v` jusqu'à ce que les endstops soient activés

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - v (float) : La vitesse de déplacement
    """
    rospy.loginfo("move speed at v:%.3f " % v)
    msg = Pic_Action()
    msg.action_destination = 'motor'
    msg.action_msg = "SPEED %.3f 0" % v
    env.action_pub.publish(msg)
    success = check_position(env, False)
    success = check_position(env, True) and not success
    motion_free(env)

def motion_free(env):
    """
    Fonction permettant de libérer les moteurs du robot

    Paramètres
    ----------

        - env (<MatchEnvironment>) : L'environment du match
    """
    rospy.loginfo("motion free")
    msg = Pic_Action()
    msg.action_destination = 'motor'
    msg.action_msg = "FREE"
    env.action_pub.publish(msg)
    rospy.sleep(DEFAULT_TEMPO)

def set_t(env, t):
    """
    Fonction permettant de recalibrer l'angle du robot

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - t (float) : L'angle à atteindre
    """
    rospy.loginfo("set_t %.3f" % (t *env.TEAM_factor))
    msg = Pic_Action()
    msg.action_destination = 'motor'
    msg.action_msg = "SETT %.3f" % (t * env.TEAM_factor)
    env.action_pub.publish(msg)
    rospy.sleep(DEFAULT_TEMPO)

def set_y(env, y):
    """
    Fonction permettant de recalibrer la position en x du robot

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - x (float) : La position en x à atteindre
    """
    rospy.loginfo("set_y %.3f" % (y * env.TEAM_factor))
    msg = Pic_Action()
    msg.action_destination = 'motor'
    msg.action_msg = "SETY %.3f" % (y*env.TEAM_factor)
    env.action_pub.publish(msg)
    rospy.sleep(DEFAULT_TEMPO)

def set_x(env, x):
    """
    Fonction permettant de recalibrer la position en y du robot

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - y (float) : La position en y à atteindre
    """
    rospy.loginfo("set_x %.3f" % x)
    msg = Pic_Action()
    msg.action_destination = 'motor'
    msg.action_msg = "SETX %.3f" % x
    env.action_pub.publish(msg)
    rospy.sleep(DEFAULT_TEMPO)

def set_v(env, v):
    """
    Fonction permettant de régler la vitesse max de déplacement rectiligne du robot

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - v (float) : La vitesse de déplacement
    """
    rospy.loginfo("set_v %.3f" % v)
    msg = Pic_Action()
    msg.action_destination = 'motor'
    msg.action_msg = "VMAX %.3f" % v
    env.action_pub.publish(msg)
    rospy.sleep(DEFAULT_TEMPO)

def set_vt(env, vt):
    """
    Fonction permettant de régler la vitesse max de rotation du robot

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - v (float) : La vitesse de déplacement
    """
    rospy.loginfo("set_vt %.3f" % vt)
    msg = Pic_Action()
    msg.action_destination = 'motor'
    msg.action_msg = "VTMAX %.3f" % vt
    env.action_pub.publish(msg)
    rospy.sleep(DEFAULT_TEMPO)

def do_AX12action(env, command, timeout=DEFAULT_TIMEOUT):
    """
    Execute une commande ax12, bloque tant que l'action n'est pas terminée, retente sur timeout

    Paramètres
    ----------
        - command (str) : commande a executer ( chaine de caracteres sans le `\n` )
        - time (float) : temps d'attente avant de passer a l'action suivante
    """
    succes = False
    while not succes:
        rospy.loginfo("doing AX12Action: %s" % command)
        msg = Pic_Action()
        msg.action_destination = 'user'
        msg.action_msg = command if env.state_team == DEFAULT_TEAM else ax12_symmetry[command]
        env.action_pub.publish(msg)
        succes = check_AX12action(env, command, timeout)

def check_AX12action(env, command, timeout=DEFAULT_TIMEOUT):
    """
    Retourne la valeur True si l'action AX12 s'est bien passée

    Paramètres
    ----------
        - env (<MatchEnvironment>) : L'environment du match
        - command (str) : commande ax a executer
        - time (float) : temps d'attente avant de passer a l'action suivante

    Return
    ------
        Retourne la valeur True si l'action AX12 s'est bien passée
    """
    try:
        command = command if env.state_team == DEFAULT_TEAM else ax12_symmetry[command]
        rospy.loginfo("checking AX12Action: %s" % command)
        action_done = rospy.wait_for_message(
            env.topic_dict["ax12_status"],
            Bool, timeout=timeout)
        return action_done

    except rospy.exceptions.ROSException as e:
        if rospy.is_shutdown():
            raise e
        rospy.loginfo("action timed out !")
        return False

def ledrgb(env, r, g, b, num=None):
    """
    Fonction qui permet de changer la couleur des LEDs

    Paramètres
    ----------
        - env (<MatchEnvironment>) : l'environnement du match
        - r (int): Valeur de rouge(0-255)
        - g (int) : Valeur de vert(0-255)
        - b (int) : Valeur de bleu(0-255)
        - num (int) : Numéro de la LED a modifier(1-5)
    """
    msg = Pic_Action()
    msg.action_destination = 'user'

    if num == None:
        msg.action_msg = "ledrgb %d %d %d" % (r, g, b)
    else:
        msg.action_msg = "ledrgb %d %d %d %d" % (r, g, b, num)

    env.action_pub.publish(msg)

def print_lcd(env, line, text):
    """
    Fonction qui permet d'afficher du texte sur le LCD

    Paramètres
    ----------
        - env (<MatchEnvironment>) : l'environnement du match
        - line (int) : Ligne sur laquelle afficher le texte (1 ou 2)
        - text (str) : Texte à afficher
    """
    msg = Pic_Action()
    msg.action_destination = 'user'
    msg.action_msg = 'screen ' + str(line) + ' "' + str(text) + '"' # Petite manip pour mettre les guillemets
    env.action_pub.publish(msg)

def sick_enable(env, avant, arriere):
    """
    Fonction qui active les sick avant et/ou arriere

    Paramètres
    ----------
        - env (<MatchEnvironment>) : l'environnement du match
        - avant (bool) : True si on active les sick avant
        - arriere (bool) : True si on active les sick arriere
    """
    msg = Pic_Action()
    msg.action_destination = 'motor'
    msg.action_msg = "ensick %d %d" % (int(avant), int(arriere))
    env.action_pub.publish(msg)

def boucle_fin_match(env):
    """
    Fonction qui permet de faire une boucle infinie pouvant être déclenchée à la fin du match

    Paramètres
    ----------
        - env (<MatchEnvironment>) : l'environnement du match
    """
    while not rospy.is_shutdown():
        pass

def danse(env):
    msg = Pic_Action()
    msg.action_destination = 'user'
    msg.action_msg = "danse"
    env.action_pub.publish(msg)


import json
import urllib.request
import pprint
pp = pprint.PrettyPrinter(indent=2)
from ruamel.yaml import YAML
yaml=YAML()

def request(action, **params):
    return {'action': action, 'params': params, 'version': 6}

def invoke(action, **params):
    requestJson = json.dumps(request(action, **params)).encode('utf-8')
    response = json.load(urllib.request.urlopen(urllib.request.Request('http://localhost:8765', requestJson)))
    if len(response) != 2:
        raise Exception('response has an unexpected number of fields')
    if 'error' not in response:
        raise Exception('response is missing required error field')
    if 'result' not in response:
        raise Exception('response is missing required result field')
    if response['error'] is not None:
        raise Exception(response['error'])
    return response['result']

def get_kanji_data():
    result = invoke('deckNamesAndIds')
    result = list(filter(lambda v: v[0].startswith('G-Anki::Volume 1::'), result.items()))
    kanjis = []
    for i in range(1, 3):
        cards = invoke('findNotes', query=f'"deck:G-Anki::Volume 1::G-Anki Lesson {str(i).zfill(2)}"')
        card = invoke('notesInfo', notes=cards)
        current_kanjis = {}
        for e in card:
            fields = e['fields']
            if 'Kanji' in fields and 'Kana Reading' in fields:
                current_kanjis[fields['Kana Reading']['value']] = fields['Kanji']['value']

        kanjis.append(current_kanjis)
    for i, d in enumerate(kanjis):
        with open(f'utils/script_output/kanji_data_{str(i+1).zfill(2)}.yaml', 'w+') as o:
            yaml.dump(d, o)
    return kanjis


import math

import bmesh
from bmesh.types import BMesh, BMVert, BMEdge, BMFace
from bmesh.utils import (
    edge_split, vert_splice, face_split,
    vert_collapse_edge, vert_dissolve, face_join,
    face_vert_separate,
)
from bmesh.ops import dissolve_verts, dissolve_edges, dissolve_faces
from mathutils import Vector

from ...addon_common.common.utils import iter_pairs
from ...addon_common.common.debug import dprint
from ...addon_common.common.profiler import profiler
from ...addon_common.common.maths import (
    triangle2D_det, triangle2D_area,
    segment2D_intersection,
    Vec2D, Point, Point2D, Vec, Direction, Normal,
)

from ...config.options import options

'''
BMElemWrapper wraps BMverts, BMEdges, BMFaces to automagically handle
world-to-local and local-to-world transformations.

Must override any property that can be set (TODO: find more elegant
way to handle this!) and function that returns a BMVert, BMEdge, or
BMFace.  All functions and read-only properties are handled with
__getattr__().

user-writable properties:

    BMVert: co, normal
    BMEdge: seam, smooth
    BMFace: material_index, normal, smooth
    common: hide, index. select, tag

NOTE: RFVert, RFEdge, RFFace do NOT mark RFMesh as dirty!
'''

class BMElemWrapper:
    @staticmethod
    def wrap(rftarget):
        BMElemWrapper.rftarget = rftarget
        BMElemWrapper.xform = rftarget.xform
        BMElemWrapper.l2w_point = rftarget.xform.l2w_point
        BMElemWrapper.w2l_point = rftarget.xform.w2l_point
        BMElemWrapper.l2w_normal = rftarget.xform.l2w_normal
        BMElemWrapper.w2l_normal = rftarget.xform.w2l_normal
        BMElemWrapper.symmetry_real = rftarget.symmetry_real
        BMElemWrapper.mirror_mod = rftarget.mirror_mod

    @staticmethod
    def _unwrap(bmelem):
        try:    return bmelem.bmelem
        except: return bmelem

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

    def __repr__(self):
        return f'<{"" if self.bmelem.is_valid else "XXX_"}{type(self).__name__}: {repr(self.bmelem)}>'

    def __hash__(self):
        return hash(self.bmelem)

    def __eq__(self, other):
        if other is None:
            return False
        if isinstance(other, BMElemWrapper):
            return self.bmelem == other.bmelem
        return self.bmelem == other

    def __ne__(self, other):
        return not self.__eq__(other)

    @property
    def hide(self):
        return self.bmelem.hide

    @hide.setter
    def hide(self, v):
        self.bmelem.hide = v

    @property
    def index(self):
        return self.bmelem.index

    @index.setter
    def index(self, v):
        self.bmelem.index = v

    @property
    def select(self):
        return self.bmelem.select and not self.bmelem.hide

    @select.setter
    def select(self, v):
        self.bmelem.select = v

    @property
    def unselect(self):
        return not self.bmelem.select and not self.bmelem.hide

    @property
    def tag(self):
        return self.bmelem.tag

    @tag.setter
    def tag(self, v):
        self.bmelem.tag = v

    def __getattr__(self, k):
        if k in self.__dict__:
            return getattr(self, k)
        return getattr(self.bmelem, k)

class RFVert(BMElemWrapper):
    @staticmethod
    def get_link_edges(rfverts):
        return { RFEdge(bme) for bmv in rfverts for bme in bmv.bmelem.link_edges }

    @staticmethod
    def get_link_faces(rfverts):
        return { RFFace(bmf) for bmv in rfverts for bmf in bmv.bmelem.link_faces }

    @property
    def co(self):
        return self.l2w_point(self.bmelem.co)

    @co.setter
    def co(self, co):
        if not self.bmelem.is_valid: return
        if any(math.isnan(v) for v in co): return
        # assert not any(math.isnan(v) for v in co), f'Setting RFVert.co to {co}'
        if options['pin enabled'] and self.pinned: return
        if options['pin seam']    and self.seam:   return
        co = self.symmetry_real(co, to_world=False)
        # # the following does not work well, because new verts have co=(0,0,0)
        # mm = BMElemWrapper.mirror_mod
        # if mm.use_clip:
        #     rft = BMElemWrapper.rftarget
        #     th = mm.symmetry_threshold * rft.unit_scaling_factor / 2.0
        #     ox,oy,oz = self.bmelem.co
        #     nx,ny,nz = (mm.x and abs(ox) <= th),(mm.y and abs(oy) <= th),(mm.z and abs(oz) <= th)
        #     if nx or ny or nz:
        #         co = rft.snap_to_symmetry(co, mm._symmetry, to_world=False, from_world=False)
        self.bmelem.co = co

    @property
    def pinned(self):
        return bool(self.bmelem[self.rftarget.layer_pin])
    @pinned.setter
    def pinned(self, v):
        self.bmelem[self.rftarget.layer_pin] = 1 if bool(v) else 0

    @property
    def seam(self):
        return any(e.seam for e in self.bmelem.link_edges)

    @property
    def normal(self):
        return self.l2w_normal(self.bmelem.normal)

    @normal.setter
    def normal(self, norm):
        self.bmelem.normal = self.w2l_normal(norm)

    @property
    def co_normal(self):
        return (self.co, self.normal)

    @co_normal.setter
    def co_normal(self, co_normal):
        self.co, self.normal = co_normal

    @property
    def link_edges(self):
        return [RFEdge(bme) for bme in self.bmelem.link_edges]

    @property
    def link_faces(self):
        return [RFFace(bmf) for bmf in self.bmelem.link_faces]

    def is_on_symmetry_plane(self):
        mm = BMElemWrapper.mirror_mod
        th = mm.symmetry_threshold * BMElemWrapper.rftarget.unit_scaling_factor / 2.0
        x,y,z = self.bmelem.co
        if mm.x and abs(x) <= th: return True
        if mm.y and abs(y) <= th: return True
        if mm.z and abs(z) <= th: return True
        return False

    def is_on_boundary(self, symmetry_as_boundary=False):
        '''
        similar to is_boundary property, but optionally discard symmetry boundaries
        '''
        if not symmetry_as_boundary:
            if self.is_on_symmetry_plane(): return False
        return self.bmelem.is_boundary

    #############################################

    def share_edge(self, other):
        if not self.is_valid or not other.is_valid: return False
        bmv0 = BMElemWrapper._unwrap(self)
        bmv1 = BMElemWrapper._unwrap(other)
        return any(bmv1 in bme.verts for bme in bmv0.link_edges if bme.is_valid)

    def shared_edge(self, other):
        if not self.is_valid or not other.is_valid: return False
        bmv0 = BMElemWrapper._unwrap(self)
        bmv1 = BMElemWrapper._unwrap(other)
        bme = next((bme for bme in bmv0.link_edges if bme.is_valid and bmv1 in bme.verts), None)
        return RFEdge(bme) if bme else None

    def share_face(self, other):
        if not self.is_valid or not other.is_valid: return False
        bmv0 = BMElemWrapper._unwrap(self)
        bmv1 = BMElemWrapper._unwrap(other)
        return any(bmv1 in bmf.verts for bmf in bmv0.link_faces if bmf.is_valid)

    def shared_faces(self, other):
        if not self.is_valid or not other.is_valid: return False
        bmv0 = BMElemWrapper._unwrap(self)
        bmv1 = BMElemWrapper._unwrap(other)
        return [RFFace(bmf) for bmf in bmv0.link_faces if bmf.is_valid and bmv1 in bmf.verts]

    def face_separate(self, f):
        if not (self.is_valid and f.is_valid): return None
        bmv = BMElemWrapper._unwrap(self)
        bmf = BMElemWrapper._unwrap(f)
        new_bmv = face_vert_separate(bmf, bmv)
        return RFVert(new_bmv)

    def merge(self, other):
        if not (self.is_valid and other.is_valid):
            if self.is_valid: return self
            if other.is_valid: return other
            return None

        try:
            bmv0 = BMElemWrapper._unwrap(self)
            bmv1 = BMElemWrapper._unwrap(other)
            vert_splice(bmv1, bmv0)
            return RFVert(bmv0)
        except Exception as e:
            print(f'Caught Exception while trying to merge')
            print(e)
            print(f'Will try more robust merge')
            return self.merge_robust(other)

    def merge_robust(self, other):
        if not (self.is_valid and other.is_valid):
            if self.is_valid: return self
            if other.is_valid: return other
            return None

        rftarget = self.rftarget

        if self.share_edge(other):
            bmv = self.shared_edge(other).collapse()
            rftarget.remove_duplicate_bmfaces(bmv)
            rftarget.clean_duplicate_bmedges(bmv)
            return bmv

        if not self.share_face(other):
            bmv = self.merge(other)
            rftarget.remove_duplicate_bmfaces(bmv)
            rftarget.clean_duplicate_bmedges(bmv)
            return bmv

        bmfs = self.shared_faces(other)
        for bmf in bmfs: bmf.split(self, other)
        rftarget.remove_duplicate_bmfaces(self)
        rftarget.clean_duplicate_bmedges(self)
        bmv = self.shared_edge(other).collapse()
        rftarget.remove_duplicate_bmfaces(bmv)
        rftarget.clean_duplicate_bmedges(bmv)
        return bmv

    def dissolve(self):
        bmv = BMElemWrapper._unwrap(self)
        vert_dissolve(bmv)

    def compute_normal(self):
        return Normal.average(f.compute_normal() for f in self.link_faces)

class RFEdge(BMElemWrapper):
    @staticmethod
    def get_verts(rfedges):
        bmvs = { bmv for bme in rfedges for bmv in bme.bmelem.verts }
        return { RFVert(bmv) for bmv in bmvs }

    @property
    def seam(self):
        return self.bmelem.seam

    @seam.setter
    def seam(self, v):
        self.bmelem.seam = v

    @property
    def smooth(self):
        return self.bmelem.smooth

    @smooth.setter
    def smooth(self, v):
        self.bmelem.smooth = v

    def first_vert(self):
        return RFVert(self.bmelem.verts[0])

    def other_vert(self, bmv):
        bmv = self._unwrap(bmv)
        o = self.bmelem.other_vert(bmv)
        if o is None:
            return None
        return RFVert(o)

    def share_vert(self, bme):
        if not self.is_valid or not bme.is_valid: return False
        bme = self._unwrap(bme)
        return any(v in bme.verts for v in self.bmelem.verts if v.is_valid)

    def shared_vert(self, bme):
        if not self.is_valid or not bme.is_valid: return None
        bme = self._unwrap(bme)
        verts = [v for v in self.bmelem.verts if v.is_valid and v in bme.verts]
        if not verts:
            return None
        return RFVert(verts[0])

    def nonshared_vert(self, bme):
        if not self.is_valid or not bme.is_valid: return None
        bme = self._unwrap(bme)
        verts = [v for v in self.bmelem.verts if v.is_valid and v not in bme.verts]
        if len(verts) != 1:
            return None
        return RFVert(verts[0])

    def share_face(self, bme):
        if not self.is_valid or not bme.is_valid: return False
        bme = self._unwrap(bme)
        return any(f in bme.link_faces for f in self.bmelem.link_faces)

    def shared_faces(self, bme):
        if not self.is_valid or not bme.is_valid: return set()
        bme = self._unwrap(bme)
        return {
            RFFace(f)
            for f in (set(self.bmelem.link_faces) & set(bme.link_faces))
            if f.is_valid
        }

    @property
    def verts(self):
        bmv0, bmv1 = self.bmelem.verts
        return (RFVert(bmv0), RFVert(bmv1))

    @property
    def link_faces(self):
        return [RFFace(bmf) for bmf in self.bmelem.link_faces]

    def get_left_right_link_faces(self):
        v0, v1 = self.bmelem.verts
        bmfl, bmfr = None, None
        if len(self.bmelem.link_faces) == 2:
            bmfl, bmfr = self.bmelem.link_faces
        elif len(self.bmelem.link_faces) == 1:
            bmfl = next(iter(self.bmelem.link_faces))
        else:
            return (None, None)

        for lv0, lv1 in iter_pairs(bmfl.verts, True):
            if lv0 == v0 and lv1 == v1:
                # correct orientation!
                break
        else:
            # swap left and right faces
            bmfl, bmfr = bmfr, bmfl

        if bmfl:
            bmfl = RFFace(bmfl)
        if bmfr:
            bmfr = RFFace(bmfr)
        return (bmfl, bmfr)

    #############################################

    def compute_normal(self):
        return Normal.average(bmf.normal for bmf in self.link_faces)

    def calc_length(self):
        v0, v1 = self.bmelem.verts
        return (self.l2w_point(v0.co) - self.l2w_point(v1.co)).length

    @property
    def length(self):
        return self.calc_length()

    def calc_center(self):
        v0, v1 = self.bmelem.verts
        return self.l2w_point((v0.co + v1.co) / 2)

    def vector(self, from_vert=None, to_vert=None):
        v0, v1 = self.verts
        if from_vert:
            if v1 == from_vert: v0, v1 = v1, v0
            assert v0 == from_vert
        elif to_vert:
            if v0 == to_vert: v0, v1 = v1, v0
            assert v1 == to_vert
        return v1.co - v0.co

    def vector2D(self, Point_to_Point2D, from_vert=None, to_vert=None):
        v0, v1 = self.verts
        if from_vert:
            if v1 == from_vert: v0, v1 = v1, v0
            assert v0 == from_vert
        elif to_vert:
            if v0 == to_vert: v0, v1 = v1, v0
            assert v1 == to_vert
        return Point_to_Point2D(v1.co) - Point_to_Point2D(v0.co)

    def direction(self, from_vert=None, to_vert=None):
        return Direction(self.vector(from_vert=from_vert, to_vert=to_vert))

    def perpendicular(self):
        d = self.vector()
        n = self.normal()
        return Direction(d.cross(n))

    @staticmethod
    def get_direction(bme):
        v0, v1 = bme.verts
        return Direction(v1.co - v0.co)

    #############################################

    def get_next_edge_in_strip(self, rfvert):
        r'''
        given self=A and bmv=B, return C

        o-----o-----o...     o-----o-----o...
        |     |     |        |     |     |
        o--A--B--C--o...     o--A--B--C--o...
        |     |     |        |     |\
        o-----o-----o...     o-----o o...
                                    \|
                                     o...
               crawl dir: ======>

        left : "normal" case, where B is part of 4 touching quads
        right: here, find the edge with the direction most similarly
               pointing in same direction
        '''
        bmv = self._unwrap(rfvert)
        assert bmv in self.bmelem.verts, "Vert not part of Edge"

        link_faces = list(self.bmelem.link_faces)
        link_edges = [bme for bme in bmv.link_edges if bme != self.bmelem]

        # for details, see: https://github.com/CGCookie/retopoflow/issues/554#issuecomment-408185805

        if len(link_faces) == 0:
            if len(link_edges) != 1: return None
            bme = link_edges[0]
            if len(bme.link_faces) != 0: return None
            return RFEdge(bme)

        if len(link_faces) == 1:
            bmf0 = link_faces[0]
            lbme = [bme for bme in link_edges if len(bme.link_faces) == 1]
            lbme = [bme for bme in lbme if bmf0 not in bme.link_faces]
            lbme = [bme for bme in lbme if any(bme0 == bme1 for bme0 in bmf0.edges for bmf1 in bme.link_faces for bme1 in bmf1.edges)]
            if len(lbme) != 1: return None
            return RFEdge(lbme[0])

        if len(link_faces) == 2 and len(bmv.link_faces) == 4 and len(bmv.link_edges) == 4:
            # bmv is part of 4 touching quads and all quads are touching
            # (left figure above)
            # find bme that does not share a face with self
            for bme in rfvert.link_edges:
                if len(bme.link_faces) != 2: continue
                if not (set(bme.link_faces) & set(link_faces)):
                    return bme
            return None

        return None

    #############################################

    def split(self, vert=None, fac=0.5):
        bme = BMElemWrapper._unwrap(self)
        bmv = BMElemWrapper._unwrap(vert) or bme.verts[0]
        bme_new, bmv_new = edge_split(bme, bmv, fac)
        return RFEdge(bme_new), RFVert(bmv_new)

    def collapse(self):
        bme = BMElemWrapper._unwrap(self)
        bmv0, bmv1 = bme.verts
        del_faces = [f for f in bme.link_faces if len(f.verts) == 3]
        for bmf in del_faces:
            self.rftarget.bme.faces.remove(bmf)
        bmesh.ops.collapse(self.rftarget.bme, edges=[bme], uvs=True)
        return RFVert(bmv0 if bmv0.is_valid else bmv1)

    # # not working
    # def separate(self, face):
    #     bme = BMElemWrapper._unwrap(self)
    #     bmf = BMElemWrapper._unwrap(face)
    #     loops = list(bme.link_loops)
    #     floops = [loop for loop in loops if loop.face == bmf]
    #     print(f'{bmf=} {loops=} {floops=}')
    #     loop = next(iter(floops))
    #     bmv0 = bmesh.utils.loop_separate(loop)
    #     return RFVert(bmv0)

class RFFace(BMElemWrapper):
    @staticmethod
    def get_verts(rffaces):
        bmvs = { bmv for bmf in rffaces for bmv in bmf.bmelem.verts }
        return { RFVert(bmv) for bmv in bmvs }

    @property
    def material_index(self):
        return self.bmelem.material_index

    @material_index.setter
    def material_index(self, v):
        self.bmelem.material_index = v

    @property
    def normal(self):
        return self.l2w_normal(self.bmelem.normal)

    @normal.setter
    def normal(self, v):
        self.bmelem.normal = self.w2l_normal(v)

    @property
    def smooth(self):
        return self.bmelem.smooth

    @smooth.setter
    def smooth(self, v):
        self.bmelem.smooth = v

    @property
    def edges(self):
        return [RFEdge(bme) for bme in self.bmelem.edges]

    def share_edge(self, other):
        bmes = set(self._unwrap(other).edges)
        return any(e in bmes for e in self.bmelem.edges)

    def shared_edge(self, other):
        edges = set(self.bmelem.edges)
        for bme in other.bmelem.edges:
            if bme in edges:
                return RFEdge(bme)
        return None

    def opposite_edge(self, e):
        if len(self.bmelem.edges) != 4:
            return None
        e = self._unwrap(e)
        for i, bme in enumerate(self.bmelem.edges):
            if bme == e:
                return RFEdge(self.bmelem.edges[(i + 2) % 4])
        return None

    def neighbor_edges(self, e):
        e = self._unwrap(e)
        l = len(self.bmelem.edges)
        for i, bme in enumerate(self.bmelem.edges):
            if bme == e:
                return (
                    RFEdge(self.bmelem.edges[(i - 1) % l]),
                    RFEdge(self.bmelem.edges[(i + 1) % l])
                )
        return None

    @property
    def verts(self):
        return [RFVert(bmv) for bmv in self.bmelem.verts]

    def get_vert_co(self):
        return [self.l2w_point(bmv.co) for bmv in self.bmelem.verts]

    def get_vert_normal(self):
        return [self.l2w_normal(bmv.normal) for bmv in self.bmelem.verts]

    def is_quad(self):
        return len(self.bmelem.verts) == 4

    def is_triangle(self):
        return len(self.bmelem.verts) == 3

    def center(self):
        return Point.average(self.l2w_point(bmv.co) for bmv in self.bmelem.verts)

    #############################################

    def compute_normal(self):
        ''' computes normal based on verts '''
        # TODO: should use loop rather than verts?
        an = Vec((0,0,0))
        vs = list(self.bmelem.verts)
        bmv1,bmv2 = vs[-2],vs[-1]
        v1 = bmv2.co - bmv1.co
        for bmv in vs:
            bmv0,bmv1,bmv2 = bmv1,bmv2,bmv
            v0,v1 = -v1,bmv2.co-bmv1.co
            an = an + v0.cross(v1)
        return self.l2w_normal(Normal(an))

    def is_flipped(self):
        fn = self.w2l_normal(self.compute_normal())
        vs = list(self.bmelem.verts)
        return any(v.normal.dot(fn) <= 0 for v in vs)

    def overlap2D(self, other, Point_to_Point2D):
        return self.overlap2D_center(other, Point_to_Point2D)

    def overlap2D_center(self, other, Point_to_Point2D):
        verts0 = list(map(Point_to_Point2D, [v.co for v in self.bmelem.verts]))
        verts1 = list(
            map(Point_to_Point2D, [v.co for v in self._unwrap(other).verts]))
        center0 = sum(map(Vec2D, verts0), Vec2D((0, 0))) / len(verts0)
        center1 = sum(map(Vec2D, verts1), Vec2D((0, 0))) / len(verts1)
        radius0 = sum((v - center0).length for v in verts0) / len(verts0)
        radius1 = sum((v - center1).length for v in verts1) / len(verts1)
        ratio = 1 - (center0 - center1).length / (radius0 + radius1)
        return max(0, ratio)

    def overlap2D_Sutherland_Hodgman(self, other, Point_to_Point2D):
        '''
        computes area in image space of overlap between self and other
        this is done by clipping other to self by iterating through all of
        edges in self and clipping to the "inside" half-space.
        Sutherland-Hodgman Algorithm:
          https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm
        '''

        # NOTE: assumes self and other are convex! (not a terrible assumption)

        verts0 = list(map(Point_to_Point2D, [v.co for v in self.bmelem.verts]))
        verts1 = list(
            map(Point_to_Point2D, [v.co for v in self._unwrap(other).verts]))

        for v00, v01 in zip(verts0, verts0[1:] + verts0[:1]):
            # other polygon (verts1) by line v00-v01
            len1 = len(verts1)
            sides = [triangle2D_det(v00, v01, v1) <= 0 for v1 in verts1]
            intersections = [
                segment2D_intersection(v00, v01, v10, v11)
                for v10, v11 in zip(verts1, verts1[1:] + verts1[:1])
            ]
            nverts1 = []
            for i0 in range(len1):
                i1 = (i0 + 1) % len1
                v10, v11 = verts1[i0], verts1[i1]
                s10, s11 = sides[i0], sides[i1]

                if s10 and s11:
                    # both outside. might intersect
                    if intersections[i0]:
                        nverts1 += [intersections[i0]]
                elif not s11:
                    if s10:
                        # v10 is outside, v11 is inside
                        if intersections[i0]:
                            nverts1 += [intersections[i0]]
                    nverts1 += [v11]
            verts1 = nverts1

        if len(verts1) < 3:
            return 0
        v0 = verts1[0]
        return sum(
            triangle2D_area(v0, v1, v2)
            for v1, v2 in zip(verts1[1:-1], verts1[2:])
        )

    def merge(self, other):
        # find vert of other that is closest to self's v0
        verts0, verts1 = list(self.bmelem.verts), list(other.bmelem.verts)
        l = len(verts0)
        assert l == len(verts1), 'RFFaces must have same vert count'
        self.rftarget.bme.faces.remove(self._unwrap(other))
        offset = min(range(l), key=lambda i: (
            verts1[i].co - verts0[0].co).length)
        # assuming verts are in same rotational order (should be)
        for i0 in range(l):
            i1 = (i0 + offset) % l
            bme = next((
                bme
                for bme in verts0[i0].link_edges
                if verts1[i1] in bme.verts
            ), None)
            if bme:
                # issue #372
                # TODO: handle better
                dprint('bme: ' + str(bme))
                pass
            else:
                vert_splice(verts1[i1], verts0[i0])
        # for v in verts0:
        #    self.rftarget.clean_duplicate_bmedges(v)

    #############################################

    def split(self, vert_a, vert_b, coords=[]):
        bmf = BMElemWrapper._unwrap(self)
        bmva = BMElemWrapper._unwrap(vert_a)
        bmvb = BMElemWrapper._unwrap(vert_b)
        coords = [BMElemWrapper.w2l_point(c) for c in coords]
        bmf_new, bml_new = face_split(bmf, bmva, bmvb, coords=coords)
        return RFFace(bmf_new)

    def shatter(self):
        working = [ self ]
        ret = set()

        while working:
            bmf = working.pop()
            if not bmf.is_valid: continue
            ret.add(bmf)
            # see if one bmv connects to another
            touched_bmvs, path = set(), []
            def find_exit(bmv0):
                nonlocal touched_bmvs, path, bmf
                touched_bmvs.add(bmv0)
                path.append(bmv0)
                for bme in bmv0.link_edges:
                    if bme.link_faces: continue  # not a potential edge
                    bmv1 = bme.other_vert(bmv0)
                    if bmv1 in touched_bmvs: continue # already seen bmv1 (loop?)
                    if bmf in bmv1.link_faces:
                        path += [bmv1]
                        return True
                    if find_exit(bmv1): return True
                path.pop() # working with bmv0 does not work, so remove bmv0 from path
            # find bmvs around perimeter of bmf that could possibly be an entrance for shatter
            for bmv in bmf.verts:
                if not any(len(bme.link_faces)==0 for bme in bmv.link_edges):
                    continue
                if find_exit(bmv): break
            else:
                # could not shatter current bmf
                continue
            # found a path to shatter bmf
            try:
                nbmf = bmf.split(path[0], path[-1], coords=[bmv.co for bmv in path[1:-1]])
            except Exception as e:
                print(f'shatter: Caught exception while trying to split {bmf} along {path}')
                print(e)
                continue
            for bmv_old in path[1:-1]:
                bmv_new,_ = min(((bmv,(bmv.co-bmv_old.co).length) for bmv in nbmf.verts), key=lambda d:d[1])
                if bmv_old.select: bmv_new.select = True
                bmv_new.merge(bmv_old)
            for bmv in bmf.verts + nbmf.verts:
                self.rftarget.clean_duplicate_bmedges(bmv)
            working.append(bmf)  # check bmf again!
            working.append(nbmf) # check new bmf

        return ret

class RFEdgeSequence:
    def __init__(self, sequence):
        if not sequence:
            self.verts = []
            self.edges = []
            self.loop = False
            return

        seq = list(BMElemWrapper._unwrap(elem) for elem in sequence)

        if type(seq[0]) is BMVert:
            self.verts = seq
            self.loop = (
                len(seq) > 1 and
                len(set(seq[0].link_edges) & set(seq[-1].link_edges)) != 0
            )
            self.edges = [next(iter(set(v0.link_edges) & set(v1.link_edges)))
                          for v0, v1 in iter_pairs(seq, self.loop)]
        elif type(seq[0]) is BMEdge:
            self.edges = seq
            self.loop = len(seq) > 2 and len(
                set(seq[0].verts) & set(seq[-1].verts)) != 0
            if len(seq) == 1 and not self.loop:
                self.verts = seq[0].verts
            else:
                self.verts = [next(iter(set(e0.verts) & set(e1.verts)))
                              for e0, e1 in iter_pairs(seq, self.loop)]
        else:
            assert False, 'unhandled type: %s' % str(type(seq[0]))

    def __repr__(self):
        e = min(map(repr, self.edges)) if self.edges else None
        return f'<RFEdgeSequence: {len(self.verts)}, {self.loop}, {e}>'

    def __len__(self):
        return len(self.edges)

    def get_verts(self):
        return [RFVert(bmv) for bmv in self.verts]

    def get_edges(self):
        return [RFEdge(bme) for bme in self.edges]

    def is_loop(self):
        return self.loop

    def iter_vert_pairs(self):
        return iter_pairs(self.get_verts(), self.loop)

    def iter_edge_pairs(self):
        return iter_pairs(self.get_edges(), self.loop)


import random
from typing import List, Optional, Tuple

import pyglove.core as pg
from pyglove.ext.evolution import base

# We disable implicit str concat as it is commonly used class schema docstr.
# pylint: disable=implicit-str-concat

def where_fn_spec():
  """Returns ValueSpec for 'where' function."""
  return pg.typing.Callable([pg.typing.Object(pg.DNA)],
                            returns=pg.typing.Bool()).noneable()

@pg.members([
    ('where', where_fn_spec(),
     'A callable to determine which nodes of the DNA are mutable. By default, '
     'all nodes are mutable. When writing a custom `where`, it can be '
     'assumed that the DNA arg has a `spec` field with its DNASpec.'),
    ('seed', pg.typing.Int().noneable(), 'Random seed for mutation.')
], init_arg_list=['where', 'seed'])
class Uniform(base.Mutator):
  """Mutates a DNA by randomizing a branch of the DNA.

  This is a minimal mutator. It acts as follows. PyGlove represents a DNA as a
  tree, with information at each node, where child nodes are conditional on the
  value of parent nodes. This mutator will pick a node uniformly at random and
  mutate the subtree rooted at that node (inclusive), respecting dependencies
  specified in the DNASpec.

  However, in general, we recommended that you write your own Mutator subclass
  so you can tailor it to your search space. This would allow you, for example:
  i) to modify a value drawing from a custom distribution: e.g. a
  gaussian-distributed additive change may be more appropriate in many cases.
  ii) to choose a node in the tree with a non-uniform distribution. E.g. you
  may want to modify some nodes more frequently if they encode areas of the
  space that should be explored more thoroughly.
  iii) perform mutations that implement a different type of locality than that
  represented by the tree structure. E.g. if two nodes at the same level need
  to be modified in a coordinated way.
  """

  def _on_bound(self):
    super()._on_bound()
    if self.seed is None:
      self._random = random
    else:
      self._random = random.Random(self.seed)

  def mutate(self, dna: pg.DNA, step: int = 0) -> pg.DNA:
    """Mutates the DNA at a given step."""
    del step
    dna = dna.clone(deep=True)  # Prevent overwriting argument.
    child_nodes, parent_nodes, child_indexes = self._get_relationships(dna)
    if not child_nodes:
      raise RuntimeError(f'Immutable DNA: {dna!r}')
    child_node, parent_node, child_index = self._random.choice(list(zip(
        child_nodes, parent_nodes, child_indexes)))
    if parent_node is None:
      # The node mutated ("child") is the root of the DNA tree.
      return pg.random_dna(
          child_node.spec, self._random, previous_dna=child_node)
    else:
      # The node mutated is not the root of the DNA tree.
      if _node_needs_distinct(child_node.spec):
        # The approach taken here is inefficient in the special case when there
        # are many choices. If a random choice is likely to succeed, that
        # scenario can be sped up by redrawing random choices until success.
        # Consider adding a branch to handle that case, depending on need.

        # Compute mutated node value, enforcing distinct constraint.
        distinct_candidates = (set(range(len(child_node.spec.candidates)))
                               - set([c.value for c in parent_node.children]))
        if distinct_candidates:
          new_child_value = self._random.choice(list(distinct_candidates))
          # Create a new sub-tree.
          new_child_node = pg.DNA(
              new_child_value,
              children=[pg.random_dna(
                  child_node.spec.candidates[new_child_value],
                  # Choice has changed for the new node,
                  # thus previous_dna does not apply.
                  self._random, previous_dna=None)])
          new_child_node.use_spec(child_node.spec)
        else:
          new_child_node = None
      else:
        new_child_node = pg.random_dna(
            child_node.spec, self._random, previous_dna=child_node)
      if new_child_node is not None:
        # NOTE(daiyip): we update the children without invalidating the internal
        # states of the DNA for better performance.
        parent_node.children.rebind(
            {child_index: new_child_node}, skip_notification=True)
        if _node_needs_sorting(child_node.spec):
          parent_spec = child_node.spec.parent_spec
          children = sorted(parent_node.children, key=lambda c: c.value)
          # When child choices are reordered, their DNASpec need to be
          # realigned.
          assert len(children) == parent_spec.num_choices
          for i, child in enumerate(children):
            child.use_spec(parent_spec.subchoice(i))
          parent_node.rebind(children=children, skip_notification=True)
      return dna

  def _get_relationships(self, dna: pg.DNA) -> Tuple[
      List[pg.DNA], List[Optional[pg.DNA]], List[Optional[int]]]:
    """Extracts the parent-child node relationships in a DNA.

    Note that PyGlove represents the nodes in a DNA instance as DNA instances
    themselves.

    Args:
      dna: the DNA that will be mutated.

    Returns:
      A tuple of 3 lists of the same length with corresponding elements:
      -child_nodes: a list of every node in the DNA.
      -parent_nodes: a list of the parent node of the corresponding node in
        `child_nodes`.
      -child_indexes: a list of indexes. For all j, child_nodes[j] is the i-th
        child of parent_nodes[j], where i = child_indexes[j].
      Note that the root is included as a "child" with a `None` parent.
    """
    # This method uses the word "child" and "parent" to refer to the node
    # relationships in the tree structure of a DNA. This should not be confused
    # with the standard use of "child" and "parent" as the genealogic
    # relationship of DNAs generated by an evolutionary algorithm.

    def is_mutable_node(obj):
      return self._is_mutable_node(obj)

    results = pg.query(dna, where=is_mutable_node, enter_selected=True)
    child_nodes = list(results.values())
    parent_nodes = [n.parent_dna for n in child_nodes]
    child_indexes = [
        n.sym_path.key if n.parent_dna else None for n in child_nodes]

    return child_nodes, parent_nodes, child_indexes

  def _is_mutable_node(self, obj: pg.Object) -> bool:
    """Returns whether the branch contains mutateble values."""
    if not isinstance(obj, pg.DNA):
      return False
    if (obj.sym_parent is None and
        # `_immutable_root` is only set by unit tests.
        getattr(self, '_immutable_root', None)):
      return False
    if self.where and not self.where(obj):
      return False
    return isinstance(obj.spec, (pg.geno.Choices,
                                 pg.geno.Float,
                                 pg.geno.CustomDecisionPoint))

@pg.members([
    ('where', where_fn_spec(),
     'A callable to determine which nodes of the DNA are mutable. By default, '
     'all nodes are mutable. When writing a custom `where`, it can be '
     'assumed that the DNA arg has a `spec` field with its DNASpec.'),
    ('seed', pg.typing.Int().noneable(), 'Random seed for mutation.')
], init_arg_list=['where', 'seed'])
class Swap(base.Mutator):
  """Specialized mutator that swaps DNA branches rooted at sibling nodes."""

  def _on_bound(self):
    super()._on_bound()
    if self.seed is None:
      self._random = random
    else:
      self._random = random.Random(self.seed)

  def mutate(self, dna: pg.DNA, step: int = 0) -> pg.DNA:
    """Mutates the DNA. If impossible, returns a clone."""
    dna = dna.clone(deep=True)  # Prevent overwriting argument.
    parent_node_candidates = self._get_candidate_nodes(dna)
    self._random.shuffle(parent_node_candidates)
    parent_node = None
    child_indexes = []
    for parent_node in parent_node_candidates:
      if not parent_node.spec.sorted:
        # If no sorting is required, any swap is valid.
        child_indexes = self._random.sample(range(len(parent_node.children)), 2)
        break  # Found a pair to swap.

    if child_indexes:
      # Swap the two indexes.
      assert len(child_indexes) == 2
      child0 = parent_node.children[child_indexes[0]]
      child1 = parent_node.children[child_indexes[1]]
      parent_node.children.rebind({child_indexes[0]: child1})
      parent_node.children.rebind({child_indexes[1]: child0})
    return dna

  def _get_candidate_nodes(self, dna: pg.DNA) -> List[pg.DNA]:
    """Returns a list of nodes with potentially swappable children."""
    def is_candidate_node(obj):
      if not isinstance(obj, pg.DNA):
        return False
      if self.where and not self.where(obj):
        return False
      return (isinstance(obj.spec, pg.geno.Choices) and
              obj.spec.num_choices > 1)
    selected_nodes = pg.query(
        dna, where=is_candidate_node, enter_selected=True)
    return list(selected_nodes.values())

def _node_needs_distinct(dna_spec: pg.DNASpec) -> bool:
  """Returns whether this node requires distinct children."""
  return (isinstance(dna_spec, pg.geno.Choices)
          and dna_spec.is_subchoice and dna_spec.distinct)

def _node_needs_sorting(dna_spec: pg.DNASpec) -> bool:
  """Returns whether this node requires distinct children."""
  return (isinstance(dna_spec, pg.geno.Choices)
          and dna_spec.is_subchoice and dna_spec.sorted)