import asyncio
import aiohttp

# Example: 1
# use async keyword in front of a function definition

async def async_func():
    print("My async function")

# async function with wait keyword

async def main():
    await async_func()

# calling main function with asyncio.run
# This function runs the passed coroutine, taking care of
# managing the asyncio event loop and finalizing asynchronous
# generators.
asyncio.run(main())

# Example: 2

async def func_1():
    await asyncio.sleep(1)
    print("func 1")

async def func_2():
    await asyncio.sleep(2)
    print("func 2")

async def main():
    await asyncio.gather(func_1(), func_2())

asyncio.run(main())

# Example: 3
async def fetch(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        html = await fetch(session, 'http://python.org')
        print(html)

asyncio.run(main())


"""
POS tags reader for Old Norse texts.
The tagset is available at http://nlp.cs.ru.is/pdf/Tagset.pdf.
Some changes were made:
cc for coordinating conjunctions
ct for

"""

__author__ = ["Clément Besnier <clemsciences@aol.com>", ]
__license__ = "MIT License"

from collections import defaultdict

class POSElement:

    @staticmethod
    def parse(tag, value):
        return value

class Gender(POSElement):
    masculine = "k"
    feminine = "v"
    neuter = "h"

    verbose = defaultdict(str)
    verbose[masculine] = "masculine"
    verbose[feminine] = "feminine"
    verbose[neuter] = "neuter"

    @staticmethod
    def parse(tag, value):
        """
        >>> Gender.parse("k", "")
        ' masculine'
        >>> Gender.parse("v", "")
        ' feminine'
        >>> Gender.parse("h", "")
        ' neuter'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + Gender.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in Gender.verbose

class Number(POSElement):
    singular = "e"
    plural = "f"

    verbose = defaultdict(str)
    verbose[singular] = "singular"
    verbose[plural] = "plural"

    @staticmethod
    def parse(tag, value):
        """
        >>> Number.parse("e", "")
        ' singular'
        >>> Number.parse("f", "")
        ' plural'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + Number.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in Number.verbose

class Case:
    nominative = "n"
    accusative = "o"
    dative = "þ"
    genitive = "e"

    verbose = defaultdict(str)
    verbose[nominative] = "nominative"
    verbose[accusative] = "accusative"
    verbose[dative] = "dative"
    verbose[genitive] = "genitive"

    @staticmethod
    def parse(tag, value):
        """
        >>> Case.parse("n", "")
        ' nominative'
        >>> Case.parse("o", "")
        ' accusative'
        >>> Case.parse("þ", "")
        ' dative'
        >>> Case.parse("e", "")
        ' genitive'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + Case.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in Case.verbose

class Declension:
    strong = "s"
    weak = "v"
    indeclinable = "o"

    verbose = defaultdict(str)
    verbose[strong] = "strong"
    verbose[weak] = "weak"
    verbose[indeclinable] = "indeclinable"

    @staticmethod
    def parse(tag, value):
        """
        >>> Declension.parse("s", "")
        ' strong'
        >>> Declension.parse("v", "")
        ' weak'
        >>> Declension.parse("o", "")
        ' indeclinable'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + Declension.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in Declension.verbose

class Degree:
    positive = "f"
    comparative = "m"
    superlative = "e"

    verbose = defaultdict(str)
    verbose[positive] = "positive"
    verbose[comparative] = "comparative"
    verbose[superlative] = "superlative"

    @staticmethod
    def parse(tag, value):
        """
        >>> Degree.parse("f", "")
        ' positive'
        >>> Degree.parse("m", "")
        ' comparative'
        >>> Degree.parse("e", "")
        ' superlative'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + Degree.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in Degree.verbose

class ProperNoun:
    person = "m"
    place = "ö"
    other = "s"

    verbose = defaultdict(str)
    verbose[person] = "person"
    verbose[place] = "place"
    verbose[other] = "other"

    @staticmethod
    def parse(tag, value):
        """
        >>> ProperNoun.parse("m", "")
        ' person'
        >>> ProperNoun.parse("ö", "")
        " place'
        >>> ProperNoun.parse("s", "")
        ' other'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + ProperNoun.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in ProperNoun.verbose

class Pronoun:
    demonstrative = "a"
    indefinite_demonstrative = "b"
    possessive = "e"
    indefinite = "o"
    personal = "p"
    interrogative = "s"
    relative = "t"

    verbose = defaultdict(str)
    verbose[demonstrative] = "demonstrative"
    verbose[indefinite_demonstrative] = "indefinite demonstrative"
    verbose[possessive] = "possessive"
    verbose[indefinite] = "indefinite"
    verbose[personal] = "personal"
    verbose[interrogative] = "interrogative"
    verbose[relative] = "relative"

    @staticmethod
    def parse(tag, value):
        """
        >>> Pronoun.parse("a", "")
        ' demonstrative'
        >>> Pronoun.parse("b", "")
        ' indefinite demonstrative'
        >>> Pronoun.parse("e", "")
        ' possessive'
        >>> Pronoun.parse("o", "")
        ' indefinite'
        >>> Pronoun.parse("p", "")
        ' personal'
        >>> Pronoun.parse("s", "")
        ' interrogative'
        >>> Pronoun.parse("t", "")
        ' relative'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + Pronoun.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in Pronoun.verbose

class Person:
    first = "1"
    second = "2"
    third = "3"

    verbose = defaultdict(str)
    verbose[first] = "first"
    verbose[second] = "second"
    verbose[third] = "third"

    @staticmethod
    def parse(tag, value):
        """
        >>> Person.parse("1", "")
        ' first'
        >>> Person.parse("2", "")
        ' second'
        >>> Person.parse("3", "")
        ' third'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + Person.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in Person.verbose

class NumberCategory:
    cardinal = "f"
    ordinal = "o"

    verbose = defaultdict(str)
    verbose[cardinal] = "cardinal"
    verbose[ordinal] = "ordinal"

    @staticmethod
    def parse(tag, value):
        """
        >>> NumberCategory.parse("f", "")
        ' cardinal'
        >>> NumberCategory.parse("o", "")
        ' ordinal'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + NumberCategory.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in NumberCategory.verbose

class Mood:
    infinitive = "n"
    imperative = "b"
    indicative = "f"
    subjunctive = "v"
    supine = "s"
    present_participle = "l"
    past_participle = "þ"

    verbose = defaultdict(str)
    verbose[infinitive] = "infinitive"
    verbose[imperative] = "imperative"
    verbose[indicative] = "indicative"
    verbose[subjunctive] = "subjunctive"
    verbose[supine] = "supine"
    verbose[present_participle] = "present participle"
    verbose[past_participle] = "past participle"

    @staticmethod
    def parse(tag, value):
        """
        >>> Mood.parse("n", "")
        ' infinitive'
        >>> Mood.parse("b", "")
        ' imperative'
        >>> Mood.parse("f", "")
        ' indicative'
        >>> Mood.parse("v", "")
        ' subjunctive'
        >>> Mood.parse("s", "")
        ' supine'
        >>> Mood.parse("l", "")
        ' present participle'
        >>> Mood.parse("þ", "")
        ' past participle'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + Mood.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in Mood.verbose

class Voice:
    active = "g"
    middle = "m"

    verbose = defaultdict(str)
    verbose[active] = "active"
    verbose[middle] = "middle"

    @staticmethod
    def parse(tag, value):
        """
        >>> Voice.parse("g", "")
        ' active'
        >>> Voice.parse("m", "")
        ' middle'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + Voice.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in Voice.verbose

class Tense:
    present = "n"
    past = "þ"

    verbose = defaultdict(str)
    verbose[present] = "present"
    verbose[past] = "past"

    @staticmethod
    def parse(tag, value):
        """
        >>> Tense.parse("n", "")
        'present"
        >>> Tense.parse("þ", "")
        'past'

        :param tag:
        :param value:
        :return:
        """
        return value + " " + Tense.verbose[tag]

    @staticmethod
    def can_apply(tag):
        return tag in Tense.verbose

class MainPOS:
    noun = "n"
    adjective = "l"
    pronoun = "f"
    article = "g"
    numeral = "t"
    verb = "s"
    adverb = "a"
    conjunction = "c"
    foreign = "e"
    unanalysed = "x"
    punctuation = "p"

    verbose = defaultdict(str)
    verbose[noun] = "noun"
    verbose[adjective] = "adjective"
    verbose[pronoun] = "pronoun"
    verbose[article] = "article"
    verbose[numeral] = "numeral"
    verbose[verb] = "verb"
    verbose[adverb] = "adverb"
    verbose[conjunction] = "conjunction"
    verbose[unanalysed] = "unanalysed"
    verbose[punctuation] = "punctuation"
    verbose[foreign] = "foreign"

    # A universal part-of-speech tagset by Petrov S., Das D., McDonald R.

    universal = defaultdict(str)
    universal[noun] = "NOUN"
    universal[adjective] = "ADJ"
    universal[pronoun] = "PRON"
    universal[article] = "DET"
    universal[numeral] = "NUM"
    universal[verb] = "VERB"
    universal[adverb] = "ADV"  # in this category, belong ADV, ADP and PRT
    universal[conjunction] = "CONJ"
    universal[unanalysed] = "X"
    universal[foreign] = "X"
    universal[punctuation] = "PUNC"

    @staticmethod
    def apply(tag: str, l_pos: list, value: str):
        i = 1
        for pos in l_pos:
            if isinstance(pos, list):
                for j in pos:
                    if j.can_apply(tag[i]):
                        value = j.parse(tag[i], value)
            else:
                value = pos.parse(tag[i], value)
            i += 1
        return value

    @staticmethod
    def parse(tag):
        """
        >>> MainPOS.parse('fakeþ')
        'pronoun demonstrative masculine singular dative'
        >>> MainPOS.parse('sfg3eþ')
        'verb indicative active third singular past'
        >>> MainPOS.parse('lvensf')
        'adjective feminine singular nominative strong positive'
        >>> MainPOS.parse('fp1en')
        'pronoun personal first singular nominative'
        >>> MainPOS.parse('nkee')
        'noun masculine singular genitive'
        >>> MainPOS.parse('sþgken')
        'verb past participle active masculine singular nominative'
        >>> MainPOS.parse('nhfn')
        'noun neuter plural nominative'
        >>> MainPOS.parse('nveo')
        'noun feminine singular accusative'

        :param tag:
        :return:
        """

        value = ""
        if tag[0] == MainPOS.noun:
            if len(tag) >= 4:
                value = MainPOS.verbose[tag[0]]
                value = MainPOS.apply(tag, [Gender, Number, Case], value)
                if len(tag) == 5:
                    value = ProperNoun.parse(tag[4], value)
            return value

        elif tag[0] == MainPOS.adjective:
            if len(tag) == 6:
                value = MainPOS.verbose[tag[0]]
                value = MainPOS.apply(tag, [Gender, Number, Case, Declension, Degree], value)
            return value

        elif tag[0] == MainPOS.pronoun:
            if len(tag) == 5:
                value = MainPOS.verbose[tag[0]]
                value = MainPOS.apply(tag, [Pronoun, [Person, Gender], Number, Case], value)
            return value

        elif tag[0] == MainPOS.article:
            if len(tag) == 4:
                value = MainPOS.verbose[tag[0]]
                value = MainPOS.apply(tag, [Gender, Number, Case], value)
            return value

        elif tag[0] == MainPOS.numeral:
            if len(tag) == 5:
                value = MainPOS.verbose[tag[0]]
                value = MainPOS.apply(tag, [NumberCategory, Gender, Number, Case], value)

        elif tag[0] == MainPOS.verb:
            if len(tag) == 3 and tag[1] == "n":
                value = MainPOS.verbose[tag[0]]
                value = MainPOS.apply(tag, [Mood, Voice], value)
                value = Mood.parse(tag[1], value)
                value = Voice.parse(tag[2], value)

            elif len(tag) == 6 and tag[1] == "þ":
                value = MainPOS.verbose[tag[0]]
                value = MainPOS.apply(tag, [Mood, Voice, Gender, Number, Case], value)

            elif len(tag) == 6:
                value = MainPOS.verbose[tag[0]]
                value = MainPOS.apply(tag, [Mood, Voice, Person, Number, Tense], value)
            return value

        elif tag[0] == MainPOS.adverb:
            if len(tag) == 2:
                value = MainPOS.verbose[tag[0]]
                if tag[1] == "a":
                    value += " no case "
                elif tag[1] == "u":
                    value += " exclamation"
                elif tag[1] == "o":
                    value += " accusative"
                elif tag[1] == "þ":
                    value += " dative"
                elif tag[1] == "e":
                    value += " genitive"
            return value

        elif tag[0] == MainPOS.conjunction:
            if len(tag) == 2:
                value = MainPOS.verbose[tag[0]]
                if tag[1] == "n":
                    value += ""
                elif tag[1] == "t":
                    value += ""
                return value

        elif tag[0] == MainPOS.foreign:
            value = MainPOS.verbose[tag[0]]
            return value

        elif tag[0] == MainPOS.unanalysed:
            value = MainPOS.verbose[tag[0]]
            return value

        elif tag[0] == MainPOS.punctuation:
            value = MainPOS.verbose[tag[0]]
            return value

        return value

    @staticmethod
    def parse_universal(tag):
        """
        >>> MainPOS.parse_universal('fakeþ')
        'PRON'
        >>> MainPOS.parse_universal('sfg3eþ')
        'VERB'
        >>> MainPOS.parse_universal('lvensf')
        'ADJ'
        >>> MainPOS.parse_universal('fp1en')
        'PRON'
        >>> MainPOS.parse_universal('nkee')
        'NOUN'
        >>> MainPOS.parse_universal('sþgken')
        'VERB'
        >>> MainPOS.parse_universal('nhfn')
        'NOUN'
        >>> MainPOS.parse_universal('nveo')
        'NOUN'

        :param tag:
        :return:
        """

        if tag[0] == MainPOS.noun or tag[0] == MainPOS.adjective or\
                tag[0] == MainPOS.article or tag[0] == MainPOS.pronoun or \
                tag[0] == MainPOS.numeral or tag[0] == MainPOS.verb or \
                tag[0] == MainPOS.conjunction or tag[0] == MainPOS.foreign or \
                tag[0] == MainPOS.unanalysed or tag[0] == MainPOS.punctuation:
            return MainPOS.universal[tag[0]]

        elif tag[0] == MainPOS.adverb:
            if len(tag) == 2:
                if tag[1] == "a":
                    return "ADV"
                elif tag[1] == "u":
                    return "ADV"
                elif tag[1] == "o" or tag[1] == "þ" or tag[1] == "e":
                    return "ADP"
        return ""

    @staticmethod
    def generate_all_possible_tags():
        """
        TODO correct it because this should not return a list whose length is equal to 1249!

        >>> len(MainPOS.generate_all_possible_tags())
        1249

        :return: All the possible tags
        """
        tags = []

        noun_tags = [MainPOS.noun + gender + number + case for gender in Gender.verbose for number in Number.verbose
                     for case in Case.verbose]

        noun_tags.extend([tag + pn_tag for tag in noun_tags for pn_tag in ProperNoun.verbose])
        tags.extend(noun_tags)

        adj_tags = [MainPOS.adjective + gender + number + case + declension + degree for gender in Gender.verbose
                    for number in Number.verbose for case in Case.verbose for declension in Declension.verbose
                    for degree in Degree.verbose]

        tags.extend(adj_tags)

        pron_tags = [MainPOS.pronoun + pronoun + person + number + case for pronoun in Pronoun.verbose
                     for person in Person.verbose for number in Number.verbose for case in Case.verbose]
        pron_tags.extend([MainPOS.pronoun + pronoun + gender + number + case for pronoun in Pronoun.verbose
                          for gender in Gender.verbose for number in Number.verbose for case in Case.verbose])

        tags.extend(pron_tags)

        tags.extend([MainPOS.article + gender + number + case for gender in Gender.verbose for number in Number.verbose
                     for case in Case.verbose])

        tags.extend(
            [MainPOS.numeral + number_category + gender + number + case for number_category in NumberCategory.verbose
             for gender in Gender.verbose for number in Number.verbose for case in Case.verbose])

        verb_tags = [MainPOS.verb + mood + voice for mood in Mood.verbose for voice in Voice.verbose]
        verb_tags.extend([MainPOS.verb + mood + voice + gender + number + case for mood in Mood.verbose
                          for voice in Voice.verbose for gender in Gender.verbose for number in Number.verbose
                          for case in Case.verbose])
        verb_tags.extend(
            [MainPOS.verb + mood + voice + person + number + tense for mood in Mood.verbose for voice in Voice.verbose
             for person in Person.verbose for number in Number.verbose for tense in Tense.verbose])

        tags.extend(verb_tags)

        tags.extend(["aa", "au", "ao", "aþ", "ae"])
        tags.extend(["cn", "ct", "cc"])
        tags.append(MainPOS.foreign)
        tags.append(MainPOS.unanalysed)
        tags.append(MainPOS.punctuation)
        return tags

def parse(tag):
    if len(tag) > 0:
        value = MainPOS.parse(tag.lower())
    else:
        value = ""
    return value

if __name__ == "__main__":
    print(parse("sfg3en"))
    print(parse("p"))
    print(len(MainPOS.generate_all_possible_tags()))
    print(MainPOS.generate_all_possible_tags())


string = input().split()

a = float(string[0])
b = float(string[1])
c = float(string[2])

if ((a + b) > c ) and ((b + c) > a) and ((a + c) > b):
    print('Perimetro = %.1f' % (a + b + c))
else:
    print('Area = %.1f' % (((a + b) * c)/2))


from datetime import datetime
from flask import Flask, request, jsonify
import uuid
import json
from database import DAO

app = Flask(__name__)

mock_pessoa = {
    "apelido": "josé",
    "nome": "José Roberto",
    "nascimento": "2000-10-01",
    "stack": ["C#", "Node", "Oracle"]
}

MSG_ERROR = "ERROR"

class Pessoa(object):

    def __init__(self, id, apelido, nome, nascimento, stack):
        self.id = id
        self.apelido = apelido
        self.nome = nome
        self.nascimento = nascimento
        self.stack = stack

    def dumps(self):
        return json.dumps(self.__dict__, indent=4, sort_keys=True, default=str)

    def parse(data):
        apelido = data['apelido']
        nome = data['nome']
        nascimento = datetime.strptime(data["nascimento"], '%Y-%m-%d').date()
        stack = data['stack']
        return Pessoa(apelido, nome, nascimento, stack)

    def converte_para_pessoa(row):
        id, apelido, nome, nascimento, stack = row
        return Pessoa(id, apelido, nome, nascimento, json.loads(stack))

@app.route("/pessoas/<id>", methods=['GET'])
def get_pessoa(id):
    query = "SELECT id, apelido, nome, nascimento, stack FROM pessoas WHERE id = %s"
    dao = DAO()
    res = dao.select(query, [id])
    pessoa = Pessoa.converte_para_pessoa(res[0])
    return pessoa.dumps(), 200

@app.route("/contagem-pessoas", methods=['GET'])
def count_pessoas():
    query = "SELECT COUNT(*) FROM pessoas"
    dao = DAO()
    res = dao.select(query)
    return str(res[0][0]), 200

@app.route("/pessoas", methods=['POST'])
def set_pessa():
    try:
        data = request.get_json()
        pessoa = Pessoa.parse(data)
        return pessoa.dumps(), 201
    except Exception as e:
        return MSG_ERROR, 400

@app.route("/pessoas", methods=['GET'])
def busca_por_termo():
    args = request.args
    data = jsonify(args.get("t", default="", type=str))
    return data, 200

if __name__ == '__main__':
    app.run(debug=True)


from abc import abstractmethod
from functools import partial
from typing import Callable

from kbc_pul.popularity.logistic_functions import logistic_popularity_function

class AbstractCountToNormalizedPopularityMapper:
    """
    Maps a value in R to [0,1]
    """

    @classmethod
    def is_value_normalized(cls, value) -> bool:
        return 0 <= value <= 1

    @abstractmethod
    def map_count_to_normalized_popularity(self, count: int) -> float:
        pass

class LogisticCountToNormalizedPopularityMapper(AbstractCountToNormalizedPopularityMapper):

    MINIMUM_POPENSITY_SCORE_VALUE = 0.01

    def __init__(self, log_growth_rate: float):
        self.function: Callable = partial(
            logistic_popularity_function, log_growth_rate=log_growth_rate
        )

    def map_count_to_normalized_popularity(self, count: int) -> float:

        value = self.function(count)
        if value < LogisticCountToNormalizedPopularityMapper.MINIMUM_POPENSITY_SCORE_VALUE:
            value = LogisticCountToNormalizedPopularityMapper.MINIMUM_POPENSITY_SCORE_VALUE

        return value


citi_burger = 0
citi_pie = 0
sausage_roll = 0
russian_roll = 0
small_chips = 0
big_chips = 0
coke = 0

def total(citi_burger, citi_pie, sausage_roll, russian_roll, small_chips, big_chips, coke):
    total_cost = (citi_burger * 22) + (citi_pie * 12) + (sausage_roll * 13) + (russian_roll * 20) + (small_chips * 20) + (big_chips * 10) + (coke * 9)

    if citi_burger > 0:
        print(f" CiTi burger: {citi_burger} x R22.00 = R{citi_burger*22}")
    if citi_pie > 0:
        print(f" CiTi pie: {citi_pie} x R12.00 = R{citi_pie*12}")
    if sausage_roll > 0:
        print(f" Sausage roll: {sausage_roll} x R13.00 = R{sausage_roll*13}")
    if russian_roll > 0:
        print(f" Russian roll and chips: {russian_roll} x R20.00 = R{russian_roll*20}")
    if small_chips > 0:
        print(f" Small chips: {small_chips} x R10.00 = R{small_chips*10}")
    if big_chips > 0:
        print(f" Big chips: {big_chips} x R20.00 = R{big_chips*20}")
    if coke > 0:
        print(f" Coke: {coke} x R9.00 = R{coke*9}")

    print("====================================================================")
    print(f" Total cost: R{total_cost}")

print("""
        1. CiTi burger              R22.00
        2. CiTi pie                 R12.00
        3. Sausage/Russian roll     R13.00
        4. Russian roll and chips   R20.00
        5. Small chips              R10.00
        6. Big chips                R20.00
        7. Coke (350ml)             R9.00
        8. Pay Now
        0. Exit
          """)

while True:

    order_list = int(input("What would you like to order?: "))

    if order_list == 0:
        break

    if order_list == 1:
        citi_burger += 1
    elif order_list == 2:
        citi_pie += 1
    elif order_list == 3:
        sausage_roll += 1
    elif order_list == 4:
        russian_roll += 1
    elif order_list == 5:
        small_chips += 1
    elif order_list == 6:
        big_chips += 1
    elif order_list == 7:
        coke += 1
    elif order_list == 8:
        total(citi_burger, citi_pie, sausage_roll, russian_roll, small_chips, big_chips, coke)
        break
    else:
        print("Invalid Request, try again")


import os
import time
import math
import sys

import argparse
import numpy as np
import pandas as pd
import plotly.graph_objects as go
import matplotlib.pyplot as plt

from gcodeparser import GcodeParser
from am_robot.GCodeCommands import GCodeCommands
from mgen import rotation_from_angles

if sys.platform == 'linux':
    from frankx import Measure, MotionData, Reaction
elif sys.platform == 'win32':
    try:
        from frankx import Measure, MotionData, Reaction
    except Exception as e:
        print(e)
    finally:
        print('Running on OS: ' + sys.platform)

class GCodeExecutor(GCodeCommands):
    '''
    Read and parse gcode into a full object variable

    Attributes:
    -----------
    filename: string
        filename to look for when loading G-code
    robot: Class
        Class object of the robot used
    tool: Class
        Class object of the tool head used

    Methods:
    -----------
    load_gcode():
        finds and runs pre-processing of G-code given by the attribute 'filename'
    run_code_segment():
        Used to run a single interval of G-code, such as a motion trajectory or machine setting change
    probe_bed():
        Used to run the bed probing sequence for determining bed flatness
    display():
        Displays basic information about G-code file
    visualize_gcode():
        Visualizes motion trajectories where extrusion is done, plotting a 3D line plot of the G-code model
    visualize_bed_mesh():
        Plots the bed mesh generated from the bed probing sequence

    '''
    def __init__(self,filename,robot,tool):
        '''
        Initializes the Class object

        Input:
        -----
        filename: string
            filename to look for when loading G-code
        robot: Class
            Class object of the robot used
        tool: Class
            Class object of the tool head used

        Returns:
        -----
        Initialized Class object

        '''
        super().__init__()

        self.filename_ = filename
        self.interval = [0,0]  # On the assumption that the first and second gcode command will allways be unique from eachother. This is a valid assumption
        self.list_of_intervals = []

        # initial values that should never be used before finding new values anyway
        self.prev_X = 0
        self.prev_Y = 0
        self.prev_Z = 0
        self.prev_E_total = 0
        self.X = 0
        self.Y = 0
        self.Z = 0
        self.E = 0
        self.F = 0
        self.x_rot = 0.0
        self.y_rot = 0.0
        self.move_type = 'idle'
        self.extrusion_mode = 'absolute'
        self.X_positioning = 'abs'
        self.Y_positioning = 'abs'
        self.Z_positioning = 'abs'

        self.robot = robot
        self.tool = tool

        # default planar bed
        self.bed_plane_abcd = [0,0,0,0]
        self.next_segment = False
        self.use_frenet_serret = True

    def get_interval(self):
        '''
        Returns the current G-code interval

        Input:
        -----

        Returns:
        -----
        self.interval: [int,int]
            list of index of start and end line of interval in G-code
        '''
        return self.interval

    #  Set the current interval of gcode lines
    def set_interval(self,interval):
        '''
        Sets the G-code interval

        Input:
        -----
        interval: [int,int]
            Sets the interval of indexes for the start and end line of G-code interval

        Returns:
        -----

        '''
        self.interval = interval

    def append_interval(self):
        '''
        Appends current self.interval to a list of intervals

        Input:
        -----

        Returns:
        -----

        '''
        self.list_of_intervals.append(self.interval)

    def read_command(self,line_number):
        '''
        Returns the command for the given G-code line number

        Input:
        -----
        line_number: int
            G-code line index for reading command

        Returns:
        -----
        command: string
            Command for the specific line as 'letter+number' i.e. 'G28'

        '''
        return self.gcodelines[line_number].command[0] + str(self.gcodelines[line_number].command[1])

    def get_command(self):
        '''
        Returns the current command

        Input:
        -----

        Returns:
        -----
        command: string
            Command as a string i.e. 'G28'

        '''
        return self.command

    def set_command(self):
        '''
        Sets the first command of the current interval

        Input:
        -----

        Returns:
        -----

        '''
        interval = self.interval
        command = self.read_command(interval[0])
        self.command = command

    # read given param from gcode
    def read_param(self,line_number,param):
        '''
        Reads the parameter specified by 'param' for the given G-code line. If no such param exists, returns False

        Input:
        -----
        line_number: int
            G-code line index for reading parameter
        param: string
            String type of parameter to read, i.e. 'X' or 'F'

        Returns:
        -----
        param_value: float
            Float value of parameter

        '''
        try:
            return self.gcodelines[line_number].params[param]
        except:
            return False

    #  get the current param from self
    def get_param(self,param):
        '''
        Gets the current value of the parameter

        Input:
        -----
        param: string
            String type of parameter to return, i.e. 'X' or 'F'

        Returns:
        -----
        param_value: float
            Float value of parameter

        '''
        return self.__dict__[param]

    #  set the current param to self
    def set_param(self,line_number,param):
        '''
        Sets the parameter value from the given G-code line to the object

        Input:
        -----
        line_number: int
            G-code line index for reading parameter
        param: string
            String type of parameter to set, i.e. 'X' or 'F'

        Returns:
        -----

        '''
        if self.read_param(line_number,param) is not False:
            self.__dict__[param] = self.read_param(line_number,param)

    def set_extremes(self,param,extreme):
        '''
        Update the extremity values of given parameter

        Input:
        -----
        param: string
            String type of parameter to update, i.e. 'X' or 'F'
        extreme: float
            Value of paremeter to update with

        Returns:
        -----

        '''
        try:
            if param > self.__dict__[extreme][1]:
                self.__dict__[extreme][1] = param
            if param < self.__dict__[extreme][0]:
                self.__dict__[extreme][0] = param
        except:
            self.__dict__[extreme] = [param,param]

    def set_params(self,line_number):
        '''
        Sets all present parameters for the given G-code line

        Input:
        -----
        line_number: int
            G-code line index to read parameters from

        Returns:
        -----

        '''
        for key in self.gcodelines[line_number].params:
            self.__dict__[key] = self.read_param(line_number,key)

    def set_prev_xyz(self):
        self.prev_X = self.X
        self.prev_Y = self.Y
        self.prev_Z = self.Z

    # Find the next instance of retraction (NB may be 0mm retraction)
    def find_next_interval(self):
        '''
        Finds the next interval bounded by certain criteria from when an interval should end and adds the interval to the list of intervals. Critria include change in material extrusion direction, command type change, layer change, change in process speed, etc..

        Input:
        -----

        Returns:
        -----
        'End of code': string
            String indicating end of G-code has been reached

        '''
        '''
        priority:
        change on command change
        change on retraction/reversing of material/tool feedrate

        '''
        if self.list_of_intervals == []:
            self.append_interval()

        if self.interval[1]+1 >= self.number_of_lines:
            print("End of code")
            return 'End of code'

        line_number = self.interval[1]+1
        interval = [line_number,line_number]
        while (line_number < self.number_of_lines):
            # Find dimensions of model
            if self.read_param(line_number,'X') is not False:
                self.set_extremes(self.read_param(line_number,'X'),'Xmax')
            if self.read_param(line_number,'Y') is not False:
                self.set_extremes(self.read_param(line_number,'Y'),'Ymax')
            if self.read_param(line_number,'Z') is not False:
                self.set_extremes(self.read_param(line_number,'Z'),'Zmax')
            if self.read_param(line_number,'F') is not False:
                self.set_extremes(self.read_param(line_number,'F'),'Fmax')

            # Set relative process speed to override default absolute
            if self.read_command(line_number) == 'M83':
                self.extrusion_mode = 'relative'

            # Find desired interval change condition
            # Check if next line has a command has changed
            if self.read_command(line_number) != self.read_command(self.interval[1]+1):
                interval = [self.interval[1]+1,line_number-1]
                break

            # Check reversing of material extrusion
            elif self.read_param(line_number,'E') is not False and self.read_param(line_number,'E') < self.get_param('E') and self.extrusion_mode == 'absolute':
                interval = [self.interval[1]+1,line_number-1]
                break

            # Check for change in process speed
            elif self.read_param(line_number,'F') is not False and self.read_param(line_number,'F') != self.get_param('F'):
                interval = [self.interval[1]+1,line_number-1]
                break

            # Check if extrusion should stop
            elif self.read_param(line_number-1,'E') is not False and self.read_param(line_number,'E') is False and self.read_command(line_number-1) == 'G1' and line_number != interval[0]:
                interval = [self.interval[1]+1,line_number-1]
                break

            # Check if extrusion should start
            elif self.read_param(line_number,'E') is not False and self.read_param(line_number-1,'E') is False and self.read_command(line_number-1) == 'G1' and line_number != interval[0]:
                interval = [self.interval[1]+1,line_number-1]
                break

            # Check if no movement or extrusion
            elif self.read_param(line_number,'X') is False and self.read_param(line_number,'Y') is False and self.read_param(line_number,'Z') is False and self.read_param(line_number,'E') is False:
                interval = [self.interval[1]+1,line_number-1]
                break

            # Check the angle between consecutive position vectors
            # elif (line_number > self.interval[1]+1) and self.turn_angle(line_number) > math.pi/3.0:  # An overall turning radius would maybe be better
            #     interval = [self.interval[1]+1,line_number-1]
            #     break

            else:
                # Typically when the next line is just another X-Y coordinate
                self.set_prev_xyz()
                self.set_params(line_number)
                line_number = line_number + 1

        if interval[1] < interval[0]:
            interval = [line_number,line_number]
            self.set_prev_xyz()
            self.set_params(line_number)
        # if self.read_command(self.interval[1]+1) == 'G1':
        #     print(interval)
        self.set_interval(interval)
        self.append_interval()

    def find_intervals(self):
        '''
        Calls find_next_interval() repeatedly until all lines have been added to an interval

        Input:
        -----

        Returns:
        -----

        '''
        while self.number_of_lines > self.interval[1] + 1:
            self.find_next_interval()
        self.reset_parameters()

    def load_gcode(self,lines):
        '''
        Finds the 'filename' file with '.gcode' extension in the data folder and parses the file using GcodeParser. X, Y, Z parameters are converted to the size used by the robot, i.e. G-code millimeter is changed to robot meter. Calls find_intervals() to starts pre-processing of G-code.

        Input:
        -----

        Returns:
        -----

        '''
        # Add .gcode if not given
        filename, extension = os.path.splitext(self.filename_)
        if extension == '':
            print("No file extension given, assuming '.gcode'")
            extension = '.gcode'
        elif extension.lower() == '.gcode':
            filename, extension = os.path.splitext(self.filename_)
        else:
            print("File extension not empty or .gcode")
            input("finding for '.gcode' instead. Enter to continue...")
            extension = '.gcode'

        cwd = os.getcwd()

        for root, dirs, files in os.walk(cwd):
            if filename+extension in files:
                fullpath = os.path.join(root, filename+extension)

        # Read file
        with open(fullpath,'r') as file:
            gcodedata = file.read()

        # Change instances of "-." to "-0.". Was causing issues with visualization plot
        gcodedata = gcodedata.replace('-.','-0.')

        # Parse lines into Dict object
        self.gcodelines = GcodeParser(gcodedata).lines

        # gcode is in mm, change to m for robot
        for line in self.gcodelines:
            for element in line.params:
                if element == 'X':
                    line.update_param('X',line.get_param('X')/1000.0)
                if element == 'Y':
                    line.update_param('Y',line.get_param('Y')/1000.0)
                if element == 'Z':
                    line.update_param('Z',line.get_param('Z')/1000.0)

        self.number_of_lines = len(self.gcodelines)
        if self.number_of_lines > lines:
            self.number_of_lines = lines
        self.find_intervals()

    def home_gcode(self,homing_type):
        '''
        Pauses to let the operator manually position the nozzle a small distance from the desired zero point of the G-code. A known point can also be used

        Input:
        -----
        homing_type: string
            Either 'Guiding' or 'known'. Guiding pauses to let the operator manually adjust nozzle position, known uses pre-provided position and continues.

        Returns:
        -----

        '''
        if homing_type == 'Guiding':
            self.robot.robot_init_move()

            print("INFO: To enter guiding mode, EMERGENCY STOP button needs to be pressed DOWN and the robot light should be continuously WHITE!")
            print("After positioning robot, open the EMERGENCY STOP button to its UP position! The robot should the have BLUE lights")
            input("Position end-effector nozzle < 1cm from desired (0,0) location.\nWhen satisfied with position, press Enter to continue...")

            self.gcode_home_pose = self.robot.read_current_pose()
            self.gcode_home_pose_vec = self.gcode_home_pose.vector()

            # Can potentially add collision detection here to further improve home point.
            # But new (0,0) point can be chosen from mid point of bed probing afterwards

        elif homing_type == 'known':
            print("Set gcode_home to this value. Home point assumed known")
            self.gcode_home_pose_vec = [0.48,0.0,0.0,-math.pi/3,0.0,0.0]
        else:
            print("Failed to home gcode zero... Check RobotMode input")

    def move_to_point(self,x,y,z):  # todo
        '''
        Move to desired location, using x,y,z and G-code home pose offset and tool_pose reference frame

        Input:
        -----
        x: float
            X value of target position
        y: float
            Y value to target position
        z: float
            Z value of target position

        Returns:
        -----

        '''
        self.robot.set_dynamic_rel(0.1)
        base_point = np.array([x,y,z])
        transformed_point = np.matmul(self.bed_plane_transformation_matrix,base_point)
        if self.next_segment:
            transformed_point = np.matmul(self.active_plane,transformed_point)
            transformed_point[0] += self.active_displacement[0]
            transformed_point[1] += self.active_displacement[1]
            transformed_point[2] += self.active_displacement[2]
        motion = self.robot.make_linear_motion(self.robot.make_affine_object(transformed_point[0] + self.gcode_home_pose_vec[0],transformed_point[1] + self.gcode_home_pose_vec[1],transformed_point[2] + self.gcode_home_pose_vec[2]))
        self.robot.execute_move(frame=self.robot.tool_frame,motion=motion)
        self.robot.recover_from_errors()

    def probe_bed(self):
        '''
        Probe the bed in a grid using motion force feedback to detect surface. Contact points are used to calculate the surface flatness.

        Input:
        -----

        Returns:
        -----
        contact_found: bool
            flag to inticate whether all probe points detected the surface or not. True if all points were successful.

        '''
        print("probing...")
        probe_locations_xy = [[[0.05,0.05],[0,0.05],[-0.05,0.05]],[[0.05,0],[0,0],[-0.05,0]],[[0.05,-0.05],[0,-0.05],[-0.05,-0.05]]]  # relative to ghome_gcode

        bed_grid = [[[],[],[]],[[],[],[]],[[],[],[]]]  # ugly ik

        contact_found = True

        for axis1 in range(3):
            for axis2 in range(3):

                self.robot.set_dynamic_rel(0.1)
                # Move to probe location
                affine1 = self.robot.make_affine_object(probe_locations_xy[axis1][axis2][0] + self.gcode_home_pose_vec[0],probe_locations_xy[axis1][axis2][1] + self.gcode_home_pose_vec[1],self.gcode_home_pose_vec[2]+0.02)
                m1 = self.robot.make_linear_motion(affine1)
                self.robot.execute_move(frame=self.robot.tool_frame,motion=m1)

                # Reset data reaction motion, may need to tweek trigger force when extruder is mounted
                # d2 = self.robot.make_Z_reaction_data(-5.0)
                d2 = MotionData().with_reaction(Reaction(Measure.ForceZ < -5.0))
                # d2 = MotionData().with_reaction(Reaction(Measure.ForceXYZNorm > 15.0))

                # Reduce dynamics for probing
                self.robot.set_dynamic_rel(0.02)

                # Move slowly towards print bed
                affine2 = self.robot.make_affine_object(0.1,0.0,-0.1)
                m2 = self.robot.make_linear_relative_motion(affine2)
                self.robot.execute_reaction_move(motion=m2,data=d2)

                # Check if the reaction was triggered
                if d2.did_break:
                    self.robot.recover_from_errors()
                    print('Force exceeded 5N!')
                    print(f"Hit something for probe location x: {axis1}, y: {axis2}")

                    current_pose = self.robot.read_current_pose()
                    vector_pose = current_pose.vector()
                    probe_point = vector_pose[0:3]
                    bed_grid[axis1][axis2] = probe_point

                elif not d2.did_break:
                    print(f"Did not hit anything for probe location x: {axis1}, y: {axis2}")
                    contact_found = False

        if contact_found:
            self.bed_points = bed_grid
            self.calculate_bed_surface_plane()
            self.calculate_bed_rotation_matrice()
            self.gcode_home_pose_vec = bed_grid[1][1]
            print(f"Gcode home location: {self.gcode_home_pose_vec}")
        else:
            print("One or more bed points was not found")

        self.robot.recover_from_errors()

        self.robot.set_dynamic_rel(0.1)
        m1 = self.robot.make_linear_motion(self.robot.robot_home_pose)
        self.robot.execute_move(frame=self.robot.tool_frame,motion=m1)
        self.robot.recover_from_errors()

        return contact_found

    def calculate_bed_surface_plane(self):
        '''
        Calculate the plane equation given by ax + by + cz + d = 0 with points taken from the bed probing process. Uses 3 non-collinear points A, B and C and calculates the cross product AB x AC = [a,b,c] and d = -(aAx + bAy + cAz)

        Input:
        -----

        Returns:
        ----

        '''
        # Points
        A = self.bed_points[1][1]
        B = self.bed_points[2][1]
        C = self.bed_points[1][2]

        AB = [B[0]-A[0],B[1]-A[1],B[2]-A[2]]
        AC = [C[0]-A[0],C[1]-A[1],C[2]-A[2]]

        ABxAC = np.cross(AB,AC)
        a = ABxAC[0]
        b = ABxAC[1]
        c = ABxAC[2]
        d = -(a*A[0] + b*A[1] + c*A[2])
        self.bed_plane_abcd = [a,b,c,d]
        # print(f"Bed plane coefficients: {self.bed_plane_abcd}")

    def calculate_bed_rotation_matrice(self):
        bed_normal_vec = np.array(self.bed_plane_abcd[0:3])
        robot_normal_vec = np.array([0.0,0.0,-1.0])  # Normal vector down

        bed_normal_vec_x = np.copy(bed_normal_vec)
        bed_normal_vec_x[1] = 0.0
        bed_normal_vec_y = np.copy(bed_normal_vec)
        bed_normal_vec_y[0] = 0.0

        y_rot = self.angle_between(bed_normal_vec_x,robot_normal_vec)
        x_rot = self.angle_between(bed_normal_vec_y,robot_normal_vec)

        print(f"Angle between plane normals: {self.angle_between(bed_normal_vec,robot_normal_vec)*180/math.pi}")
        print(f"Angle between plane normals - Rotation around x-axis: {x_rot*180/math.pi}")
        print(f"Angle between plane normals - Rotation around y-axis: {y_rot*180/math.pi}")

        T = self.robot.make_affine_object(0.0, 0.0, 0.0, a=0.0, b=y_rot, c=-x_rot)
        self.T_robot_bed = T
        self.robot.tool_frame = self.robot.tool_frame * self.T_robot_bed
        self.bed_plane_transformation_matrix = self.rotation_matrix(x_rot=x_rot,y_rot=-y_rot)

    def rotation_matrix(self,x_rot=0.0,y_rot=0.0,z_rot=0.0):
        # Rz = np.matrix([[math.cos(z_rot),-math.sin(z_rot), 0.0,0.0],
        #                 [math.sin(z_rot),math.cos(z_rot),0.0,0.0],
        #                 [0.0,0.0,1.0,0.0],
        #                 [0.0,0.0,0.0,1.0]])
        # Ry = np.matrix([[math.cos(y_rot),0.0,math.sin(y_rot),0.0],
        #                 [0.0,1.0,0.0,0.0],
        #                 [-math.sin(y_rot),0.0,math.cos(y_rot),0.0],
        #                 [0.0,0.0,0.0,1.0]])
        # Rx = np.matrix([[1.0,0.0,0.0,0.0],
        #                 [0.0,math.cos(x_rot),-math.sin(x_rot),0.0],
        #                 [0.0,math.sin(x_rot),math.cos(x_rot),0.0],
        #                 [0.0,0.0,0.0,1.0]])
        # R = np.matmul(Rz,np.matmul(Ry,Rx))

        return rotation_from_angles([z_rot, y_rot, x_rot], 'ZYX')

    def does_model_fit_bed(self):
        '''
        Checks whether the physical dimension of  the model will fit in the build area

        Input:
        -----

        Returns:
        -----
        flag: bool
            True if model fits, else False

        '''
        # Assumes a Cubic build volume.. Should use actual spherical volume
        # Change home pose to gcode home, checks then the spedific placement after homeing gcode zero
        if ((self.Xmax[1]+self.gcode_home_pose_vec[0] >= self.robot.radius) or
            (self.Xmax[0]+self.gcode_home_pose_vec[0] <= self.robot.base_area) or
            (self.Ymax[1]+self.gcode_home_pose_vec[1] >= self.robot.radius) or
            # (self.Ymax[0]+self.gcode_home_pose_vec[1] <= self.robot.base_area) or
            (self.Zmax[1]+self.gcode_home_pose_vec[2] >= self.robot.height_up) or
            (self.Zmax[0]+self.gcode_home_pose_vec[2] <= self.robot.height_down)):
            return False
        else:
            return True

    def is_build_feasible(self):
        '''
        Checks if build is feasible for the chosen G-code home point.

        Input:
        -----

        Returns:
        -----
        flag: bool
            True if feasible, else False

        '''
        if not self.does_model_fit_bed():
            print("Model does not fit on build plate")
            return False
        else:  # skipping lower part for now
            return True

        # move in a square and detect collision
        edge_points = [[self.Xmax[1],self.Ymax[1],self.Zmax[0]+0.02],[self.Xmax[0],self.Ymax[1],self.Zmax[0]+0.02],[self.Xmax[0],self.Ymax[0],self.Zmax[0]+0.02],[self.Xmax[1],self.Ymax[0],self.Zmax[0]+0.02],[self.Xmax[1],self.Ymax[1],self.Zmax[0]+0.02]]
        reaction_motion = self.robot.make_linear_motion(self.robot.make_affine_object(self.robot.robot_home_pose_vec[0],self.robot.robot_home_pose_vec[1],self.robot.robot_home_pose_vec[2]))

        for point in edge_points:

            # Stop motion if the overall force is greater than 30N
            # data = MotionData().with_reaction(Reaction(Measure.ForceXYZNorm > 30.0, reaction_motion))
            data = self.robot.make_norm_reaction_data(30.0,reaction=reaction_motion)
            motion = self.robot.make_linear_motion(self.robot.make_affine_object(point[0] + self.robot.robot_home_pose_vec[0],point[1] + self.robot.robot_home_pose_vec[1],point[2]))

            self.robot.execute_move(frame=self.robot.tool_frame,motion=motion,data=data)

            if data.did_break:
                self.robot.recover_from_errors()
                print("Collision when checking build area.")
                return False

            reaction_motion = self.robot.make_linear_motion(self.robot.make_affine_object(point[0] + self.robot.robot_home_pose_vec[0],point[1] + self.robot.robot_home_pose_vec[1],point[2]))

        self.is_area_clear = True

        return True

    def turn_angle(self,line_number):
        '''
        Check if trajectory sharply changes direction
        '''
        prev_point = [self.prev_X,self.prev_Y,self.prev_Z]
        mid_point = [self.X,self.Y,self.Z]
        next_point = [self.X,self.Y,self.Z]

        if self.read_param(line_number,'X') is not False:
            next_point[0] = (self.read_param(line_number,'X'))
        if self.read_param(line_number,'Y') is not False:
            next_point[1] = (self.read_param(line_number,'Y'))
        if self.read_param(line_number,'Z') is not False:
            next_point[2] = (self.read_param(line_number,'Z'))

        v1 = self.make_vector(np.array(prev_point),np.array(mid_point))
        v2 = self.make_vector(np.array(mid_point),np.array(next_point))

        if np.count_nonzero(v1) == 0 or np.count_nonzero(v2) == 0:
            return 0.0  # No direction change
        else:
            angle = self.angle_between(v1,v2)
            return angle

    def make_vector(self,start, end):
        return end - start

    def unit_vector(self,vector):
        """ Returns the unit vector of the vector.  """
        return vector / np.linalg.norm(vector)

    def angle_between(self,v1, v2):
        """ Returns the angle in radians between vectors 'v1' and 'v2'::

                >>> angle_between((1, 0, 0), (0, 1, 0))
                1.5707963267948966
                >>> angle_between((1, 0, 0), (1, 0, 0))
                0.0
                >>> angle_between((1, 0, 0), (-1, 0, 0))
                3.141592653589793
        """
        v1_u = self.unit_vector(v1)
        v2_u = self.unit_vector(v2)
        angle = np.arccos(np.clip(np.dot(v1_u, v2_u), -3.14, 3.14))
        if np.count_nonzero(v1) == len(v1):
            return angle
        else:
            values = v1[np.nonzero(v1)]
            return np.sign(values[0])*angle

    def slope_angles(self,start_point,end_point):
        # Find tangent vector from point to point
        tangent_vec = end_point - start_point
        normal_vec = np.copy(tangent_vec)
        normal_vec[2] = 0.0
        normal_vec = np.matmul(self.rotation_matrix(z_rot=math.pi/2),normal_vec)
        fs_binormal_vec = np.cross(tangent_vec,normal_vec)
        horizontal_plane_normal_vec = np.array([0.0,0.0,1.0])  # Normal vector down

        # Decompose the binomial vector into x-z and y-z plane
        fs_binormal_vec_xz = np.copy(fs_binormal_vec)
        fs_binormal_vec_xz[1] = 0.0
        fs_binormal_vec_yz = np.copy(fs_binormal_vec)
        fs_binormal_vec_yz[0] = 0.0

        if not np.any(fs_binormal_vec_xz):
            y_rot = 0.0
        else:
            y_rot = self.angle_between(fs_binormal_vec_xz,horizontal_plane_normal_vec)
            if abs(y_rot) > 30.0:
                y_rot = np.sign(y_rot)*30.0
        if not np.any(fs_binormal_vec_yz):
            x_rot = 0.0
        else:
            x_rot = self.angle_between(fs_binormal_vec_yz,horizontal_plane_normal_vec)
            if abs(x_rot) > 30.0:
                x_rot = np.sign(x_rot)*30.0
        if not np.any(fs_binormal_vec):
            slope = 0.0
        else:
            slope = self.angle_between(fs_binormal_vec,horizontal_plane_normal_vec)

        # print(f"Angle between plane normals - Rotation around x-axis: {x_rot*180/math.pi}")
        # print(f"Angle between plane normals - Rotation around y-axis: {y_rot*180/math.pi}")

        return [x_rot, y_rot, slope]

    def make_waypoints(self,interval):
        '''
        Generate waypoint objects for motion trajectory to follow

        Input:
        -----
        interval: [int,int]
            Interval for which to make waypoints for

        Returns:
        -----
        waypoints: list of Waypoint objects
            A list of waypoints in the Waypoint object type

        '''
        waypoints = []
        affines = []
        velocity_rels = []
        for point in range(interval[0],interval[1]+1):
            if point >= interval[0] + 2:
                angle = self.turn_angle(point)
                if abs(angle) > math.pi/4:
                    velocity_rels.append(0.02)
                elif abs(angle) > math.pi/8:
                    velocity_rels.append(0.04)
                else:
                    velocity_rels.append(0.06)
            self.set_prev_xyz()
            if point == interval[0] or point == interval[1]:
                velocity_rels.append(0.6)
            for key in self.gcodelines[point].params:
                if key == 'X' or key == 'Y' or key == 'Z':
                    self.__dict__[key] = self.read_param(point,key)
            base_point = np.array([self.X,self.Y,self.Z])
            transformed_point = np.matmul(self.bed_plane_transformation_matrix,base_point)
            if self.next_segment:
                transformed_point = np.matmul(self.active_plane,transformed_point)
                transformed_point[0] += self.active_displacement[0]
                transformed_point[1] += self.active_displacement[1]
                transformed_point[2] += self.active_displacement[2]
            affine = self.robot.make_affine_object(transformed_point[0] + self.gcode_home_pose_vec[0],transformed_point[1] + self.gcode_home_pose_vec[1],transformed_point[2] + self.gcode_home_pose_vec[2])
            affines.append(affine)
        for affine, velocity_rel in affines, velocity_rels:
            waypoint = self.robot.make_waypoint(affine,velocity_rel)
            waypoints.append(waypoint)
        return waypoints

    def make_path(self,interval,corner_blending,motion_data):
        '''
        Generate waypoints for path following with a blending distance for smoothing motion when changing taget waypoint

        Input:
        -----
        interval: [int,int]
            Interval for which to make the PathMotion object for
        corner_blend_threshold: float
            radius for whech a new waypoint should be used as motion target

        Returns:
        -----
        path: PathMotion object

        '''
        if self.extrusion_mode == 'absolute':
            self.path_extrusion = 0
            self.prev_E_total = self.E
        else:
            self.path_extrusion = 0

        path_points = []
        start_pose = self.robot.read_current_pose()
        z_translation = start_pose.vector()
        for point in range(interval[0],interval[1]+1):
            start_point = np.array([self.X,self.Y,self.Z])
            for key in self.gcodelines[point].params:
                if key == 'X' or key == 'Y' or key == 'Z':
                    if self.X_positioning == 'rel' or self.Y_positioning == 'rel' or self.Z_positioning == 'rel':
                        self.__dict__[key] += self.read_param(point,key)
                    else:
                        self.__dict__[key] = self.read_param(point,key)
                if key == 'E':
                    if self.extrusion_mode == 'abs':
                        self.__dict__[key] = self.read_param(point,key)
                        self.path_extrusion = self.__dict__[key]
                    else:
                        self.__dict__[key] += self.read_param(point,key)
                        self.path_extrusion = self.__dict__[key]
            base_point = np.array([self.X,self.Y,self.Z])
            transformed_point = np.matmul(self.bed_plane_transformation_matrix,base_point)
            if self.next_segment is True:
                transformed_point = np.matmul(self.active_plane,transformed_point)
                transformed_point[0] += self.active_displacement[0]
                transformed_point[1] += self.active_displacement[1]
                transformed_point[2] += self.active_displacement[2]

            # Pure translation for non-extrusion move, using previous rotation
            if self.read_param(point,'E') is False:
                # y_rot = -z_translation[4]
                # x_rot = -z_translation[5]
                # if abs(y_rot) > abs(self.y_rot):
                #     self.y_rot = y_rot
                # if abs(x_rot) > abs(self.x_rot):
                #     self.x_rot = x_rot
                if point == interval[0]:
                    path_points.append(start_pose)
            else:
                [x_rot,y_rot,slope] = self.slope_angles(start_point,base_point)
                if abs(y_rot) > abs(self.y_rot) and self.use_frenet_serret is True:
                    self.y_rot = y_rot
                if abs(x_rot) > abs(self.x_rot) and self.use_frenet_serret is True:
                    self.x_rot = x_rot
                if point == interval[0]:
                    first_point = self.robot.make_affine_object(z_translation[0],z_translation[1],z_translation[2],b=self.y_rot,c=-self.x_rot)
                    path_points.append(first_point)
                    self.robot.execute_reaction_move(frame=self.robot.tool_frame,motion=self.robot.make_linear_motion(first_point),data=motion_data)
                    self.robot.recover_from_errors()
            affine = self.robot.make_affine_object(transformed_point[0] + self.gcode_home_pose_vec[0],transformed_point[1] + self.gcode_home_pose_vec[1],transformed_point[2] + self.gcode_home_pose_vec[2],b=self.y_rot,c=-self.x_rot)
            path_points.append(affine)
        path_motion, path = self.robot.make_path_motion(path_points,corner_blending)
        if self.extrusion_mode == 'absolute':
            self.path_extrusion -= self.prev_E_total

        return path_motion, path

    def target_point(self,line_number):
        '''

        Input:
        -----
        line_number: int
            Integer index of target G-code line point

        Returns:
        -----
        The target point with bed level compensation and offset from G-code home location in relation to robot zero

        '''

        print("Target point method!")

        base_point = np.array([self.read_param(line_number,'X'),self.read_param(line_number,'Y'),self.Z])
        transformed_point = np.matmul(self.bed_plane_transformation_matrix,base_point)
        if self.next_segment:
            transformed_point = np.matmul(self.active_plane,transformed_point)
            transformed_point[0] += self.active_displacement[0]
            transformed_point[1] += self.active_displacement[1]
            transformed_point[2] += self.active_displacement[2]
        return transformed_point[0] + self.gcode_home_pose_vec[0], transformed_point[1] + self.gcode_home_pose_vec[1], transformed_point[2] + self.gcode_home_pose_vec[2]

    def run_code_segments(self):
        prev_progress = 0
        self.robot.set_velocity_rel(1.0)
        for interval in self.list_of_intervals:
            progress = math.floor((100 * interval[0])/(self.number_of_lines - 1.0))
            if progress > prev_progress:
                print(f"Current progress is {progress}%, on line {interval[0]} of {self.number_of_lines}.")
                prev_progress = progress
            self.interval = interval
            self.run_code_segment()

    # Blocking action
    def run_code_segment(self):
        '''
        Processes one interval based on the Command in that interval. Machine settings, movement, extrusion is set based on command and parameters.

        Input:
        -----

        Returns:
        -----

        '''
        command = self.read_command(self.interval[0])
        # Handle different M (machine) commands
        if command[0] == 'M':
            # Call method for each M-command (M79(),M42(), etc)
            getattr(self,command,getattr(self,'default'))()

        elif command[0] == 'G':
            # Find desired feedrate / working speed / max velocity
            if self.read_param(self.interval[0],'F') is not False:
                self.F = self.read_param(self.interval[0],'F')

            # Call method for G-command
            getattr(self,command,getattr(self,'default'))()

        else:
            print(f"Command other than M or G... Silently passing on command {command}")
            pass

    def visualize_bed_mesh(self):
        '''
        Plot the bed mesh based on the bed_points from the probing sequence

        Input:
        -----

        Returns:
        -----

        '''
        try:
            bed_points = self.bed_points
        except:
            print("No bed points found, using default flat 3 x 3 surface")
            bed_points = [[[0.2,0.2,0.0],[0,0.2,0],[-0.2,0.2,0.0]],
                        [[0.2,0,0],[0,0,0.0],[-0.2,0,0.0]],
                        [[0.2,-0.2,0.0],[0,-0.2,0.0],[-0.2,-0.2,0.0]]]

        x_axis = len(bed_points)
        y_axis = len(bed_points[0])

        x = []
        y = []
        z = []

        max_z = bed_points[0][0][2]
        min_z = bed_points[0][0][2]

        for i in range(y_axis):
            temp_x = []
            temp_y = []
            temp_z = []

            for j in range(x_axis):
                temp_x.append(bed_points[i][j][0])
                temp_y.append(bed_points[i][j][1])
                temp_z.append(bed_points[i][j][2])

                if bed_points[i][j][2] > max_z:
                    max_z = bed_points[i][j][2]

                if bed_points[i][j][2] < min_z:
                    min_z = bed_points[i][j][2]

            x.append(temp_x)
            y.append(temp_y)
            z.append(temp_z)

        largest_axis = max([bed_points[0][0][0]-bed_points[-1][-1][0],bed_points[0][0][1]-bed_points[-1][-1][1],max_z-min_z])
        axis_scale = [(bed_points[0][0][0]-bed_points[-1][-1][0])/largest_axis,(bed_points[0][0][1]-bed_points[-1][-1][1])/largest_axis,(max_z-min_z)/largest_axis]

        fig = go.Figure(data=[go.Surface(z=z,x=x,y=y)])
        fig.update_traces(contours_z=dict(show=True, usecolormap=True,
                                        highlightcolor="limegreen", project_z=True))
        fig.update_layout(title='Surface flatness of print bed', autosize=False,
                        scene_camera_eye=dict(x=0.0, y=0.0, z=1.50),
                        width=1000, height=1000,
                        margin=dict(l=65, r=50, b=65, t=90),
                        scene=dict(
                            aspectratio=dict(x=axis_scale[0], y=axis_scale[1], z=axis_scale[2]),
                            aspectmode='manual'
                        )
        )

        fig.show()

        # fig.write_image('bedmesh.eps',width=1920,height=1080)

    def visualize_gcode(self):
        '''
        Visualize motion trajectories where extrusion occures. Plot of extrusion paths generates a model

        Input:
        -----

        Returns:
        -----

        '''

        self.reset_parameters()

        # arrays for plotting data
        x_coordinates = []
        y_coordinates = []
        z_coordinates = []
        colors = []

        color_code_index = 1
        color_code = ['Feedrate [mm/min]','Slope angle [degrees]','Line number']
        color_choice = [0,0,0]

        print("May have issues with 1 million+ points...")
        # Points here is vertices in the plot and corresponds to G-code lines

        for interval in self.list_of_intervals:
            if self.read_command(interval[0]) == 'G1':
                if (self.read_param(interval[0],'E') is not False) and ((self.read_param(interval[0],'E') > self.E) or self.extrusion_mode == 'relative') and (self.read_param(interval[0],'X') or self.read_param(interval[0],'Y') or self.read_param(interval[0],'Z')):
                    # front-pad the start position to the coming series of moves
                    x_coordinates.append(self.X * 1000)
                    y_coordinates.append(self.Y * 1000)
                    z_coordinates.append(self.Z * 1000)
                    color_choice[0] = self.F
                    color_choice[2] = interval[0]
                    if color_code_index != 1:
                        colors.append(color_choice[color_code_index])

                    # if x_coordinates[-1] == x_coordinates[-3] and y_coordinates[-1] == y_coordinates[-3] and z_coordinates[-1] == z_coordinates[-3]:
                    #     x_coordinates = x_coordinates[:-2]
                    #     y_coordinates = y_coordinates[:-2]
                    #     z_coordinates = z_coordinates[:-2]
                    #     colors = colors[:-2]

                for point in range(interval[0],interval[1]+1):
                    if (self.read_param(point,'E') is not False) and ((self.read_param(point,'E') > self.E) or self.extrusion_mode == 'relative') and (self.read_param(interval[0],'X') or self.read_param(interval[0],'Y') or self.read_param(interval[0],'Z')):
                        start_point = np.array([self.X,self.Y,self.Z])
                        for key in self.gcodelines[point].params:
                            self.__dict__[key] = self.read_param(point,key)

                        x_coordinates.append(self.X * 1000)
                        y_coordinates.append(self.Y * 1000)
                        z_coordinates.append(self.Z * 1000)
                        color_choice[0] = self.F
                        color_choice[2] = point
                        end_point = np.array([self.X,self.Y,self.Z])
                        [_,__,slope] = self.slope_angles(start_point,end_point)
                        color_choice[1] = slope*(180/math.pi)
                        if point == interval[0] and color_code_index == 1:
                            colors.append(color_choice[color_code_index])
                        colors.append(color_choice[color_code_index])

                    for key in self.gcodelines[point].params:
                        self.__dict__[key] = self.read_param(point,key)

                # To separate trajectories in the plot
                x_coordinates.append(None)
                y_coordinates.append(None)
                z_coordinates.append(None)
                colors.append(color_choice[color_code_index])

            elif self.read_command(interval[0]) == 'G0':
                for point in range(interval[0],interval[1]+1):
                    for key in self.gcodelines[point].params:
                        self.__dict__[key] = self.read_param(point,key)

            else:
                for point in range(interval[0],interval[1]+1):
                    for key in self.gcodelines[point].params:
                        if self.read_param(point,'X') is not False or self.read_param(point,'Y') is not False or self.read_param(point,'Z') is not False or self.read_param(point,'E') is not False or self.read_param(point,'F') is not False:
                            self.__dict__[key] = self.read_param(point,key)

        largest_axis = max([self.Xmax[1]-self.Xmax[0],self.Ymax[1]-self.Ymax[0],self.Zmax[1]-self.Zmax[0]])
        axis_scale = [(self.Xmax[1]-self.Xmax[0])/largest_axis,(self.Ymax[1]-self.Ymax[0])/largest_axis,(self.Zmax[1]-self.Zmax[0])/largest_axis]

        fig = go.Figure(data=go.Scatter3d(
            x=x_coordinates,y=y_coordinates,z=z_coordinates,
            mode="lines",
            line=dict(
                width=5,
                color=colors,
                cmin=min(colors),
                cmax=max(colors),
                colorbar=dict(
                    borderwidth=0,
                    title=dict(
                        text=color_code[color_code_index]
                    )
                ),
                colorscale=[[0,'rgb(0,0,255)'],[1,'rgb(255,0,0)']]
            ),
            connectgaps=False
        ))

        fig.update_layout(
            autosize=True,
            scene=dict(
                aspectratio=dict(x=axis_scale[0], y=axis_scale[1], z=axis_scale[2]),
                aspectmode='manual'
            ),
            title_text=('Visualization of extruded filament paths for ' + self.filename_),
            showlegend=False
        )

        fig.show()

        self.reset_parameters()

    def plot_cart_path(self,t_list, s_list, v_list, a_list, j_list):
        plt.figure()
        plt.plot(t_list, s_list, label='path')
        plt.plot(t_list, v_list, label='velocity')
        plt.plot(t_list, a_list, label='acceleration')
        plt.plot(t_list, j_list, label='jerk')
        plt.legend()
        plt.title('Time parametrization of path motion')
        plt.grid(True)
        plt.xlabel('t (s)')
        plt.ylabel('m, m/s, m/s2, m/s3')

        plt.show()

    def reset_parameters(self):
        '''
        Resets parameters to zero

        Input:
        -----

        Returns:
        -----

        '''
        # corresponding to 0,0,0 of start of printing
        self.X = 0
        self.Y = 0
        self.Z = 0
        self.F = 0
        self.E = 0
        self.set_prev_xyz()

    def __str__(self):
        '''
        Sets the Class object string representation

        Input:
        -----

        Returns:
        -----
        "GCodeExecutor": string
            String representation of Class object

        '''
        return "GCodeExecutor"

    def display(self):
        '''
        Display basic iformation about the Class object such as filename of processed file, number of lines, material use, etc.

        Input:
        -----

        Returns:
        -----

        '''
        if '.gcode' in self.filename_:
            print(f"\nName of file being processed: {self.filename_}")
        else:
            print(f"\nName of file being processed: {self.filename_ + '.gcode'}")
        print(f"Number of command lines processed: {self.number_of_lines}")
        print("Total filament used: \n")

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

    parser.add_argument('--gfile',default='Circle')

    args = parser.parse_args()

    model = GCodeExecutor(args.gfile,{},{})
    model.load_gcode()
    model.visualize_gcode()


# Stock Price Checker

import sys
import requests
import time
from bs4 import BeautifulSoup

print("This application will determine the price of a Stock, and send you an email when a specified price tager is reached")

class Stock:

    Stock_Code = ""
    Stock_Price = 0.0

    def FindStock(self):
        check = True
        while(check == True):
            try:
                self.Stock_Code = input("Enter the Stock Code: ")
                url = ('https://finance.yahoo.com/quote/' + self.Stock_Code + '?p=' + self.Stock_Code + '&.tsrc=fin-srch')
                req = requests.get(url)

                self.Stock_Price = BeautifulSoup(req.text, 'lxml')
                self.Stock_Price = self.Stock_Price.find('div', {"class" : 'My(6px) Pos(r) smartphone_Mt(6px)'})

                self.Stock_Price = self.Stock_Price.find('span').text
                check = False
            except Exception:
                print(self.Stock_Code, " is not a Valid Stock Code")
            else:
                    self.Stock_Price = float(self.Stock_Price)

    def FindStockPrice(self):
        url = ('https://finance.yahoo.com/quote/' + self.Stock_Code + '?p=' + self.Stock_Code + '&.tsrc=fin-srch')
        req = requests.get(url)

        self.Stock_Price = BeautifulSoup(req.text, 'lxml')
        self.Stock_Price = self.Stock_Price.find('div', {"class" : 'My(6px) Pos(r) smartphone_Mt(6px)'})

        self.Stock_Price = self.Stock_Price.find('span').text

    def Send_mail():

        server = smtplib.SMTP('smtp.gmail.com', 587)
        server.ehlo()
        server.starttls()
        server.ehlo()

        server.login('ahmadmaazin1@gmail.com', 'qjtgbeucfsjvnwnd')

        subject = 'Stock Alert!'
        body = 'Your stock has reached the desired price target! Check the Stock: ' ' https://www.tradingview.com/symbols/' + Stock_Code + '/'

        msg = f"Subject: {subject}\n\n{body}"

        server.sendmail('ahmadmaazin1@gmail.com','maahmed@ucdavis.edu', msg)
        print("Email Sent")

        server.quit()

    def PriceChecker(self):

        TargetPrice = float(input("Enter the Price Point for which you want to be notified: "))

        while(self.Stock_Price > TargetPrice):
            print("Price is currently at: ", self.Stock_Price)
            self.FindStockPrice(self)
            time.sleep(60)
        else:
            Send_mail()

Share = Stock
Share.FindStock(Share)
print(Share.Stock_Code, " is ", Share.Stock_Price)
Share.PriceChecker(Share)


import datetime

from django.test import TestCase
from django.utils import timezone
from django.urls import reverse

from .models import Question

# Create your tests here.
class QuestionModelTests(TestCase):

    def test_was_published_recently_with_future_question(self):
        """
        was_published_recently() はpub_dateが将来の質問に対してFalseを返却する
        """
        time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
        self.assertIs(future_question.was_published_recently(), False)

    def test_was_published_recently_with_recent_question(self):
        """
        was_published_recently() はpub_dateが24時間以内の質問に対してTrueを返す
        """
        time = timezone.now() - datetime.timedelta(hours=23, minutes=59, seconds=59)
        recent_question = Question(pub_date=time)

def create_question(question_text, days):
    """
    Questionモデルインタスタンスを作成し保存します。
    質問内容は引数question_textを、pub_dateはシステム日付に引数daysを加減算し設定します。
    """
    time = timezone.now() + datetime.timedelta(days=days)
    return Question.objects.create(question_text=question_text, pub_date=time)

class QuestionIndexViewTests(TestCase):
    def test_no_question(self): # テストメソッド実行時、リセットされた
        """
        質問が存在しない場合、適切なメッセージが表示されること
        """
        response = self.client.get(reverse('polls:index'))
        self.assertEqual(response.status_code, 200)
        self.assertContains(response, "No polls are available.") # responseに存在しない旨のメッセージが含まれることを確認
        self.assertQuerysetEqual(response.context['last_question_list'], []) # responseのコンテキスト辞書に設定されたQuerySetが空であることを確認

    def test_past_question(self):
        """
        pub_dateが過去の質問は、indexページに表示されること
        """
        create_question(question_text="Past question.", days=-30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question.>']
        ) # responseのコンテキスト辞書に設定されたQuerySetに、モデル登録したインスタンスが設定されていることを確認

    def test_future_question(self):
        """
        pub_dateが将来の質問は、indexページに表示されないこと
        :return:
        """
        create_question(question_text="Future question.", days=30) # pub_dateが30日後のQuestionモデルを登録
        response = self.client.get(reverse('polls:index'))
        self.assertContains(response, "No polls are available.") # responseに文字列"No polls are available"が含まれていることを確認
        self.assertQuerysetEqual(response.context['latest_question_list'], []) # responseのコンテキスト辞書に設定されたQuerySetが空であることを確認

    def test_future_question_and_past_question(self):
        """
        pub_dateが過去と将来の質問が存在する場合、過去の質問のみがindexページに表示されること
        :return:
        """
        create_question(question_text="Past question.", days=30)
        create_question(question_text="Future question.", days=30)
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question. >']
        )

    def test_two_past_questions(self):
        """
        pub_dateが過去の質問が複数存在する場合、該当する質問が全てindexページに表示されること
        :return:
        """
        create_question(question_text="Page question 1.", days=-30) # pub_dateが30日前のQuestionモデルを登録
        create_question(question_text="Past question 2.", days=-5) # pub_dateが5日前のQuestionモデルを登録
        response = self.client.get(reverse('polls:index'))
        self.assertQuerysetEqual(
            response.context['latest_question_list'],
            ['<Question: Past question 2.>', '<Question: Past question 1.>']
        ) # responseのコンテキスト辞書に設定されたQuerySetに、モデル登録したインスタンスが全て設定されていることを確認

class QuestionDetailViewTests(TestCase):
    def test_future_question(self):
        """
        pub_dateが将来の質問の場合は、404 Not Foundエラーが表示されること
        :return:
        """
        future_question = create_question(question_text='Future question.', days=5)
        url = reverse('polls:detail', args=(future_question.id,))
        response = self.client.get(url)
        self.assertEqual(response.status_code, 404)

    def test_past_question(self):
        """
        pub_dateが過去の質問は、質問のテキストが表示されること
        :return:
        """
        past_question = create_question(question_text='Past Question.', days=-5)
        url = reverse('polls:detail', args=(past_question))
        response = self.client.get(url)
        self.assertContains(response, past_question.question_text) # モデルに登録した質問のテキストと同じ文字列がresopnseに含まれていることを確認


# source: https://github.com/PyTorchLightning/pytorch-lightning-bolts (Apache2)

import logging
from collections import Counter
from typing import Any, Dict, Optional, Tuple

import segmentation_models_pytorch as smp
import torchmetrics

import numpy as np
import pandas as pd
import pytorch_lightning as pl
import torch
from deadtrees.loss.gdl import GeneralizedDiceLoss
from deadtrees.loss.gwdl import GeneralizedWassersteinDiceLoss
from deadtrees.loss.losses import (  # GeneralizedDice,   we now use another implementation of GDL
    BoundaryLoss,
    class2one_hot,
    DiceLoss,
    FocalLoss,
)
from deadtrees.network.extra import EfficientUnetPlusPlus, ResUnet, ResUnetPlusPlus
from deadtrees.utils import utils
from deadtrees.visualization.helper import show, show_cm
from omegaconf import DictConfig
from torch import Tensor

log = utils.get_logger(__name__)

def concat_extra(
    img: Tensor, mask: Tensor, distmap: Tensor, lu: Tensor, stats, *, extra
) -> Tuple[Tensor]:
    extra_imgs, extra_masks, extra_distmaps, extra_lus, extra_stats = list(zip(*extra))
    img = torch.cat((img, *extra_imgs), dim=0)
    mask = torch.cat((mask, *extra_masks), dim=0)
    distmap = torch.cat((distmap, *extra_distmaps), dim=0)
    lu = torch.cat((lu, *extra_lus), dim=0)
    stats.extend(sum(extra_stats, []))
    return img, mask, distmap, lu, stats

def create_combined_batch(
    batch: Dict[str, Any]
) -> Tuple[Tensor, Tensor, Tensor, Tensor]:
    img, mask, distmap, lu, stats = batch["main"]

    # grab extra datasets and concat tensors
    extra = [v for k, v in batch.items() if k.startswith("extra")]
    if extra:
        img, mask, distmap, lu, stats = concat_extra(
            img, mask, distmap, lu, stats, extra=extra
        )
    return img, mask, distmap, lu, stats

class SemSegment(pl.LightningModule):  # type: ignore
    def __init__(self, network: DictConfig, training: DictConfig):
        super().__init__()

        architecture = network.architecture.lower().strip()
        if architecture == "unet":
            Model = smp.Unet
        elif architecture in ["unetplusplus", "unet++"]:
            Model = smp.UnetPlusPlus
        elif architecture == "resunet":
            Model = ResUnet
        elif architecture in ["resunetplusplus", "resunet++"]:
            Model = ResUnetPlusPlus
        elif architecture in ["efficientunetplusplus", "efficientunet++"]:
            Model = EfficientUnetPlusPlus
        else:
            raise NotImplementedError(
                "Currently only Unet, ResUnet, Unet++, ResUnet++, and EfficientUnet++ architectures are supported"
            )

        # Model does not accept "architecture" as an argument, but we need to store it in hparams for inference
        # TODO: cleanup?
        clean_network_conf = network.copy()
        del clean_network_conf.architecture
        del clean_network_conf.losses
        n_classes = len(clean_network_conf.classes)
        del clean_network_conf.classes

        self.model = Model(**clean_network_conf, classes=n_classes)

        if clean_network_conf.encoder_weights is None:
            log.info("Initializing unset weights with Kaiming")
            self.model.apply(initialize_weights)
        self.encoder_weights = clean_network_conf.encoder_weights

        self.save_hyperparameters()

        self.classes = self.hparams["network"]["classes"]
        self.classes_int = list(range(len(self.classes)))
        self.classes_int_wout_bg = [c for c in self.classes_int if c != 0]

        self.in_channels = self.hparams["network"]["in_channels"]

        # losses
        self.dice_loss = None
        self.focal_loss = None
        self.boundary_loss = None

        # parse loss config
        self.initial_alpha = 0.01  # make this a hyperparameter and/ or scale with epoch
        self.boundary_loss_ramped = False

        assert (
            ("GDICE" in network.losses) and (("DICE" in network.losses))
        ) is False, f"Only GDICE _OR_ DICE allowed {network.losses}"

        for loss_component in network.losses:
            if loss_component == "GDICE":
                # NOTE: we now use another implementation of GDL
                # self.dice_loss = GeneralizedDice(idc=self.classes_int)
                self.dice_loss = GeneralizedDiceLoss()
            elif loss_component == "GWDICE":
                dist_mat = np.array([[0.0, 1.0, 1.0], [1.0, 0.0, 0.5], [1.0, 0.5, 0.0]])
                if self.classes_int == 2:
                    dist_mat = dist_mat[0:2, 0:2]
                self.dice_loss = GeneralizedWassersteinDiceLoss(
                    dist_matrix=dist_mat,
                )
            elif loss_component == "DICE":
                # This the only required loss term
                self.dice_loss = DiceLoss(idc=self.classes_int_wout_bg)
            elif loss_component == "FOCAL":
                self.focal_loss = FocalLoss(idc=self.classes_int, gamma=2)
            elif loss_component == "BOUNDARY":
                self.boundary_loss = BoundaryLoss(idc=self.classes_int_wout_bg)
            elif loss_component == "BOUNDARY-RAMPED":
                self.boundary_loss = BoundaryLoss(idc=self.classes_int_wout_bg)
                self.boundary_loss_ramped = True
            else:
                raise NotImplementedError(
                    f"The loss component <{loss_component}> is not recognized"
                )

        log.info(f"Losses: {network.losses}")

        # checks: we require GDICE!
        assert self.dice_loss is not None

        self.dice_metric = smp.utils.metrics.Fscore(
            ignore_channels=[0],
        )

        self.dice_metric_with_bg = smp.utils.metrics.Fscore()

        self.stats = {
            "train": Counter(),
            "val": Counter(),
            "test": Counter(),
        }

    @property
    def alpha(self):
        """blending parameter for boundary loss - ramps from 0.01 to 0.99 in 0.01 steps by epoch"""
        return min((self.current_epoch + 1) * self.initial_alpha, 0.99)

    def get_progress_bar_dict(self):
        """Hack to remove v_num from progressbar"""
        tqdm_dict = super().get_progress_bar_dict()
        if "v_num" in tqdm_dict:
            del tqdm_dict["v_num"]
        return tqdm_dict

    def calculate_loss(
        self, y_hat: Tensor, y: Tensor, stage: str, distmap: Optional[Tensor] = None
    ) -> Tensor:
        """calculate compound loss"""
        loss, loss_gd, loss_bd, loss_fo = 0, None, None, None

        if self.dice_loss:
            if isinstance(self.dice_loss, GeneralizedWassersteinDiceLoss):
                # hack to make gwdice work...
                loss_gd = self.dice_loss(y_hat, torch.argmax(y, dim=1))
            else:
                loss_gd = self.dice_loss(y_hat, y)

            if torch.isnan(loss_gd) or torch.isinf(loss_gd):
                log.warn("Train dice loss is NaN! What is going on?")

            self.log(f"{stage}/dice_loss", loss_gd, on_step=False, on_epoch=True)
            loss += loss_gd

        if self.boundary_loss and distmap is not None:
            loss_bd = self.boundary_loss(y_hat, distmap)
            self.log(f"{stage}/boundary_loss", loss_bd, on_step=False, on_epoch=True)
            loss += self.alpha * loss_bd if self.boundary_loss_ramped else loss_bd

        if self.focal_loss:
            loss_fo = self.focal_loss(y_hat, y)
            self.log(f"{stage}/focal_loss", loss_fo, on_step=False, on_epoch=True)
            loss += loss_fo

        self.log(f"{stage}/total_loss", loss, on_step=False, on_epoch=True)

        return loss

    def log_metrics(self, y_hat: Tensor, y: Tensor, *, stage: str):
        dice_score = self.dice_metric(y_hat, y)
        dice_score_with_bg = self.dice_metric_with_bg(y_hat, y)
        self.log(f"{stage}/dice", dice_score, on_step=False, on_epoch=True)
        self.log(
            f"{stage}/dice_with_bg", dice_score_with_bg, on_step=False, on_epoch=True
        )

    def training_step(self, batch, batch_idx):

        img, mask, distmap, _, stats = create_combined_batch(batch)

        logits = self.model(img)
        y = class2one_hot(mask, K=len(self.classes_int))
        y_hat = logits.softmax(dim=1)

        loss = self.calculate_loss(y_hat, y, "train", distmap=distmap)

        if torch.isnan(loss) or torch.isinf(loss):
            log.warn("Train loss is NaN! What is going on?")
            return None

        self.log_metrics(y_hat, y, stage="train")

        # track training batch files
        self.stats["train"].update([x["file"] for x in stats])

        return loss

    def validation_step(self, batch, batch_idx):

        img, mask, distmap, lu, stats = create_combined_batch(batch)

        logits = self.model(img)
        y = class2one_hot(mask, K=len(self.classes_int))
        y_hat = logits.softmax(dim=1)

        loss = self.calculate_loss(y_hat, y, stage="val", distmap=distmap)

        self.log_metrics(y_hat, y, stage="val")

        if batch_idx == 0:
            sample_chart = show(
                x=img.cpu(),
                y=mask.cpu(),
                y_hat=y_hat.cpu(),
                n_samples=min(img.shape[0], 8),
                stats=stats,
                dpi=72,
                display=False,
            )
            for logger in self.logger:
                if isinstance(logger, pl.loggers.wandb.WandbLogger):
                    import wandb

                    logger.experiment.log(
                        {
                            "sample": wandb.Image(
                                sample_chart,
                                caption=f"Sample-{self.trainer.global_step}",
                            )
                        },
                        commit=False,
                    )

        # track validation batch files
        self.stats["val"].update([x["file"] for x in stats])

        return {
            "val_loss": loss,
            "target": mask,
            "prediction": y_hat.argmax(dim=1),
            "lu": lu,
        }

    def test_step(self, batch: Tuple[Tensor], batch_idx) -> Dict[str, Any]:
        img, mask, _, lu, stats = batch

        logits = self.model(img)
        y = class2one_hot(mask, K=len(self.classes_int))
        y_hat = logits.softmax(dim=1)

        self.log_metrics(y_hat, y, stage="test")

        # track validation batch files
        self.stats["test"].update([x["file"] for x in stats])

        return {"target": mask, "prediction": y_hat.argmax(dim=1), "lu": lu}

    def validation_epoch_end(self, outputs: Dict[str, Any]):
        prediction = torch.cat([tmp["prediction"] for tmp in outputs])
        target = torch.cat([tmp["target"] for tmp in outputs])

        # --- masked cm, flatten all tensors, subset by forest pixels
        lu = torch.ravel(torch.cat([tmp["lu"] for tmp in outputs]))
        prediction_masked = torch.ravel(prediction)[lu == 1]
        target_masked = torch.ravel(target)[lu == 1]

        confusion_matrix = torchmetrics.functional.confusion_matrix(
            prediction, target, normalize="true", num_classes=len(self.classes)
        )

        confusion_matrix_masked = torchmetrics.functional.confusion_matrix(
            prediction_masked,
            target_masked,
            normalize="true",
            num_classes=len(self.classes),
        )

        dfs = {}
        for label, cm in zip(
            ["cm_norm", "cm_norm_masked"], [confusion_matrix, confusion_matrix_masked]
        ):
            dfs[label] = pd.DataFrame(
                cm.detach().cpu().numpy(),
                index=self.classes,
                columns=self.classes,
            )

        cm_chart = show_cm(dfs["cm_norm"], dfs["cm_norm_masked"], dpi=72, display=False)

        for logger in self.logger:
            if isinstance(logger, pl.loggers.wandb.WandbLogger):
                import wandb

                logger.experiment.log(
                    {
                        "Confusion matrix (Val)": wandb.Image(
                            cm_chart,
                            caption=f"CM-Val-Norm-{self.trainer.global_step}",
                        )
                    },
                    commit=False,
                )

    def test_epoch_end(self, outputs: Dict[str, Any]):

        # --- original cm
        prediction = torch.cat([tmp["prediction"] for tmp in outputs])
        target = torch.cat([tmp["target"] for tmp in outputs])

        # --- masked cm, flatten all tensors, subset by forest pixels
        lu = torch.ravel(torch.cat([tmp["lu"] for tmp in outputs]))
        prediction_masked = torch.ravel(prediction)[lu == 1]
        target_masked = torch.ravel(target)[lu == 1]

        confusion_matrix = torchmetrics.functional.confusion_matrix(
            prediction, target, normalize="true", num_classes=len(self.classes)
        )

        confusion_matrix_px = torchmetrics.functional.confusion_matrix(
            prediction, target, num_classes=len(self.classes)
        )

        confusion_matrix_masked = torchmetrics.functional.confusion_matrix(
            prediction_masked,
            target_masked,
            normalize="true",
            num_classes=len(self.classes),
        )

        confusion_matrix_masked_px = torchmetrics.functional.confusion_matrix(
            prediction_masked, target_masked, num_classes=len(self.classes)
        )

        dfs = {}
        for label, cm in zip(
            ["cm_norm", "cm_px", "cm_norm_masked", "cm_px_masked"],
            [
                confusion_matrix,
                confusion_matrix_px,
                confusion_matrix_masked,
                confusion_matrix_masked_px,
            ],
        ):
            dfs[label] = pd.DataFrame(
                cm.detach().cpu().numpy(),
                index=self.classes,
                columns=self.classes,
            )

        cm_chart = show_cm(dfs["cm_norm"], dfs["cm_norm_masked"], dpi=72, display=False)
        cm_chart_px = show_cm(dfs["cm_px"], dfs["cm_px_masked"], dpi=72, display=False)

        for logger in self.logger:
            if isinstance(logger, pl.loggers.wandb.WandbLogger):
                import wandb

                logger.experiment.log(
                    {
                        "Confusion Matrix (Test) - Normalized": wandb.Image(
                            cm_chart,
                            caption=f"CM-Test-Norm-{self.trainer.global_step}",
                        ),
                        "Confusion Matrix (Test) - Pixel": wandb.Image(
                            cm_chart_px,
                            caption=f"CM-Test-Px-{self.trainer.global_step}",
                        ),
                    },
                    commit=False,
                )

        log.info(f"CM - DEFAULT - NORMALIZED: {dfs['cm_norm'].to_string()}")
        log.info(f"CM - FORESTONLY - NORMALIZED: {dfs['cm_norm_masked'].to_string()}")
        log.info(f"CM - DEFAULT - PIXEL: {dfs['cm_px'].to_string()}")
        log.info(f"CM - FORESTONLY - PIXEL: {dfs['cm_px_masked'].to_string()}")

    def teardown(self, stage=None) -> None:
        log.debug(f"len(stats_train): {len(self.stats['train'])}")
        pd.DataFrame.from_records(
            list(dict(self.stats["train"]).items()), columns=["filename", "count"]
        ).to_csv("train_stats.csv", index=False)

        log.debug(f"len(stats_val): {len(self.stats['val'])}")
        pd.DataFrame.from_records(
            list(dict(self.stats["val"]).items()), columns=["filename", "count"]
        ).to_csv("val_stats.csv", index=False)

    def configure_optimizers(self):
        opt = torch.optim.Adam(
            self.parameters(),
            lr=self.hparams.training.learning_rate,
            # eps=1e-6,
        )
        sch = torch.optim.lr_scheduler.CosineAnnealingLR(
            opt, T_max=self.hparams.training.cosineannealing_tmax
        )
        return [opt], [sch]

def initialize_weights(m):
    if getattr(m, "bias", None) is not None:
        torch.nn.init.constant_(m.bias, 0)
    if isinstance(m, (torch.nn.Conv2d, torch.nn.Linear)):
        torch.nn.init.kaiming_normal_(m.weight)
    for c in m.children():
        initialize_weights(c)


import pyqrcode
import requests
from django.shortcuts import render, HttpResponse, redirect
from django_tables2 import Table, RequestConfig, LazyPaginator
from django.core.exceptions import BadRequest
from django.contrib.auth.decorators import login_required
from django.db.models import Q
from .tables import *
from main.models import *
from .forms import *

# Create your views here.
@login_required(login_url='loginPage')
def usersList(request):
    template ='userslist.html'
    table = UsersTable(User.objects.all().order_by('id'))
    RequestConfig(request, paginate={"paginator_class": LazyPaginator,
                                     "per_page": 10}).configure(table)
    context = {'table': table}
    return render(request, template, context)

@login_required(login_url='loginPage')
#Add user from Web interface
def addUser(request):
    template = 'useradd.html'
    form = UserForm(request.POST or None)
    if form.is_valid():
        form.save()
        user = User.objects.all().order_by('-id')[0]
        user_id = user.id
        filename = str(user.facedata)
        print(user_id)
        image = user.facedata.open(mode='rb')
        url = "https://search.facex.io:8443/images/singleImage/"

        payload = {'user_id': '62c81adb7312e67dcfb98d3f',
                   'name': user_id}
        files = [
            ('image', (filename, image, 'application/octet-stream'))
        ]
        headers = {}
        api_resp = requests.request("POST", url, headers=headers, data=payload, files=files)
        if api_resp:
            return redirect('usersList')
        else:
            raise BadRequest

    context = {
        'form':form
    }
    return render(request, template, context)

@login_required(login_url='loginPage')
# Genere Client QRCode
def qrCode(request,pk):
    code = User.objects.get(id=pk)
    code = str(code.id)
    # # Code pour generer le QRCode
    qrobj = pyqrcode.create(code, encoding='utf-8')
    with open('test.png', 'wb') as f:
        qrobj.png(f, scale=10)
    image_data = open('test.png', 'rb').read()
    response = HttpResponse(image_data, content_type='image/png')
    response['Content-Disposition'] = 'attachment; filename=%s.png'
    return response

@login_required(login_url='loginPage')
# Edit user
def userEdit(request,pk):
    template = 'useredit.html'
    user = User.objects.get(id=pk)
    form = UserForm(request.POST or None, instance=user)
    if form.is_valid():
        form.save()
        return redirect('usersList')
    context = {'form':form}
    return render(request,template,context)

@login_required(login_url='loginPage')
def userDelete(request,pk):
    user = User.objects.get(id=pk)
    user.delete()
    return redirect('usersList')

@login_required(login_url='loginPage')
def userDeactivate(request,pk):
    user = User.objects.get(id=pk)
    user.is_active = False
    user.save(update_fields=['is_active'])
    return redirect('usersList')

@login_required(login_url='loginPage')
def userActivate(request,pk):
    user = User.objects.get(id=pk)
    user.is_active = True
    user.save(update_fields=['is_active'])
    return redirect('usersList')

@login_required(login_url='loginPage')
def usersearch(request):
    template = 'userslist.html'
    if request.method == 'POST':
        search = request.POST['search']
        table = UsersTable(User.objects.filter(Q(firstname=search)|Q(lastname =search)|Q(matricule=search)))
        RequestConfig(request, paginate={"paginator_class": LazyPaginator,
                                         "per_page": 10}).configure(table)
        context = {'table': table}
        return render(request, template, context)
    return redirect('usersList')