# -*- coding: utf-8 -*-
"""
Created on Thu Aug  4 14:59:48 2022

@author: BEMC
"""

import os
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from hmtoolbox.WB_SWAN import SWAN_read_tab
from hmtoolbox.WB_basic import list_files_folders
from hmtoolbox.WB_basic import save_plot

# results SWAN 1D
path_results_1D = r'z:\130991_Systeemanalyse_ZSS\3.Models\SWAN\1D\Waddenzee\tests\test_04_veg_layers'
files = list_files_folders.list_files('.TAB',path_results_1D)

# input SWAN 1D
path_input = r'z:\130991_Systeemanalyse_ZSS\3.Models\SWAN\1D\Waddenzee\tests\test_04_veg_layers\input'
file_input = r'SWAN2D_output_WZ_6004026_HRext03.csv'

# results SWAN 2D
path_results_2D = r'z:\130991_Systeemanalyse_ZSS\3.Models\SWAN\1D\Waddenzee\tests\test_04_veg_layers\input'
file_output_2D = 'output_batch_03_1D_input_6004026_HRext02.xlsx'

# output path
output_path = r'z:\130991_Systeemanalyse_ZSS\3.Models\SWAN\1D\Waddenzee\tests\test_04_veg_layers'

save_fig = True

Xp_comp= 343
Xpteen = 43
Xp_basis = 99.8

#%% load file with simulation input

df_input = pd.read_csv(os.path.join(path_input, file_input), sep=';',dtype={'ZSS-scenario':str})

# df_swan2d = pd.read_csv(os.path.join(path_results_2D, file_output_2D), sep=';',dtype={'ZSS-scenario':str})

#%% loop trough simulations and plot results

for file in files:

    scene = file.split('\\')[-3]
    simulation = file.split('\\')[-2]

    Hs_ref = df_input[df_input['Scenario']==scene]['HS'].iloc[0]
    Tp_ref = df_input[df_input['Scenario']==scene]['TP'].iloc[0]

    data, headers = SWAN_read_tab.Freadtab(file)

    data['Hsig'][data['Hsig']<=0] = np.nan
    data['Hsig'][data['Hsig']==0] = np.nan
    data['Tm_10'][data['Tm_10']<=0] = np.nan
    data['TPsmoo'][data['TPsmoo']<=0] = np.nan
    data['Wlen'][data['Wlen']<=0] = np.nan
    data['Botlev'][data['Botlev']<-20] = -10
    Wlen = float(data['Lwavp'].iloc[-1])

    #%% Determine location toe of dike
    # ii = 0
    # slope = list()
    # Xpteen = data['Xp'].iloc[-1]
    # Ypteen = data['Yp'].iloc[-1]
    # for x1, x2, y1, y2 in zip(data['Xp'][1:-1], data['Xp'][:-2], data['Botlev'][1:-1], data['Botlev'][:-2]):
    #     dx = x1 - x2
    #     dy = y1 - y2
    #     if dy == 0:
    #         dydx = 0
    #     else:
    #         dydx = dy/dx
    #         if dydx >= 1/60 and ii == 0:
    #             Xpteen = x1
    #             Ypteen = y2
    #             ii = ii +1
    #     slope.append(dydx)

    # # max_slope = max(slope)
    # # imax = np.argmax(slope)

    Xpteen = Xpteen

    #%% wave parameters at incoming boundary
    Xpin = data['Xp'].iloc[-1]
    Hs_in = data['Hsig'].iloc[-1]
    Tm10_in = data['Tm_10'].iloc[-1]

    #%% Get output at output location (1/2 wavelength from toe of dike)
    Xpout = float(Xpteen) + Wlen*0.5
    Ypout = 0
    Hs_out = data['Hsig'][data['Xp'] > Xpout].iloc[0]
    Tm10_out = data['Tm_10'][data['Xp'] > Xpout].iloc[0]

    # maximum values for ylimits
    Hs_max = max(data['Hsig'])
    Tm10_max = max(data['Tm_10'])

    #%% get output at SWAN 2D location
    Hs_comp = data['Hsig'][data['Xp'] >= Xp_comp].iloc[0]
    Tm10_comp = data['Tm_10'][data['Xp'] >= Xp_comp].iloc[0]

    #%% plotting
    fig = plt.figure(figsize=(8,7))
    ax1 = plt.subplot(2,1,1)
    ax1_copy = ax1.twinx()

    ax1.plot(data['Xp'],-data['Botlev'],'k', linewidth = 3, label = 'bodem')
    ax1.plot(data['Xp'], data['Watlev'], 'b-', linewidth = 1.5, label = 'waterstand')
    ax1.axvline(x = Xpteen, color = 'k', linestyle='--', label = 'teen')
    ax1.axvline(x = Xpout, color = 'r', linestyle='--', label = 'teen + 1/2*L')
    ax1.axvline(x = Xp_comp, color = 'tab:orange', linestyle='--', label = 'teen + 300m')
    ax1.axvline(x = Xp_basis, color = 'y', linestyle='--', label = 'HRbasis')
    ax1.set_ylabel('hoogte [m+NAP]')
    ax1.set_xlabel('afstand [m]')
    ax1.legend(loc = 'lower right')
    # ax1.set_xlim(30,100)
    # ax1.set_ylim(-20,10)

    ax1_copy.plot(data['Xp'], data['Hsig'],'g', linewidth = 1.5, label = '$H_s$ [m]')
    ax1_copy.set_ylabel('$H_s$ [m]',color='g')
    ax1_copy.tick_params(labelcolor='g')
    ax1_copy.text(Xpin,Hs_in,f'Hs = {Hs_in:.2f} m',color='g',fontweight = 'bold')
    ax1_copy.text(Xpout+10,Hs_out,f'Hs = {Hs_out:.2f} m',color='r',fontweight = 'bold')
    ax1_copy.text(Xp_comp+10,Hs_comp,f'Hs = {Hs_comp:.2f} m',color='tab:orange',fontweight = 'bold')
    plt.title(f'{scene}\n{simulation}\n Hs = {Hs_ref:.2f} m, Tp = {Tp_ref:.2f} s')
    ax1_copy.legend(loc = 'center right')
    ax1_copy.set_ylim(0,np.ceil(Hs_max))

    ax2 = plt.subplot(2,1,2)
    ax2_copy = ax2.twinx()
    ax2.plot(data['Xp'],-data['Botlev'],'k', linewidth = 3, label = 'bodem')
    ax2.plot(data['Xp'], data['Watlev'], 'b-', linewidth = 1.5, label = 'waterstand')
    ax2.axvline(x = Xpteen, color = 'k', linestyle='--', label = 'teen')
    ax2.axvline(x = Xpout, color = 'r', linestyle='--', label = 'teen + 1/2*L')
    ax2.axvline(x = Xp_comp, color = 'tab:orange', linestyle='--', label = 'teen + 300m')
    ax2.axvline(x = Xp_basis, color = 'y', linestyle='--', label = 'HRbasis')
    ax2.set_ylabel('hoogte [m+NAP]')
    ax2.set_xlabel('afstand [m]')
    ax2.legend(loc = 'lower right')
    # ax2.set_xlim(30,100)
    # ax2.set_ylim(-20,10)

    # ax2_copy.plot(data['Xp'], data['Tm_10'],color='orange')
    ax2_copy.plot(data['Xp'], data['Tm_10'],'m', linewidth = 1.5,label = '$T_m-1,0$ [m]')
    ax2_copy.set_ylabel('$H_s$ [m]',color='m')
    ax2_copy.tick_params(labelcolor='m')
    ax2_copy.text(Xpin,Tm10_in,f'Tm_10 = {Tm10_in:.2f} s',color='m',fontweight = 'bold')
    ax2_copy.text(Xpout+10,Tm10_out,f'Tm_10 = {Tm10_out:.2f} s',color='r',fontweight = 'bold')
    ax2_copy.text(Xp_comp+10,Tm10_comp,f'Tm_10 = {Tm10_comp:.2f} s',color='tab:orange',fontweight = 'bold')
    ax2_copy.set_ylabel('$T_{m-1.0}$ [s]')
    ax2_copy.legend(loc = 'center right')
    ax2_copy.set_ylim(0,np.ceil(Tm10_max))

    if save_fig:
        save_name = os.path.join(output_path, scene+'_'+simulation+'.png')
        save_plot.save_plot(fig,save_name,ax = ax1_copy, dx = -0.05)


import sqlite3
#backend

def employeeData():
    con=sqlite3.connect("employee.db")
    cur = con.cursor()
    cur.execute("CREATE TABLE IF NOT EXISTS employee(id INTEGER PRIMARY KEY, EmpID text, Firstname text, Lastname text, DOB text, \
        Gender text, Address text, Mobile text)")
    con.commit()
    con.close()

def addEmpRec(EmpID, Firstname, Lastname, DOB, Gender, Address, Mobile):
    con=sqlite3.connect("employee.db")
    cur = con.cursor()
    cur.execute("INSERT INTO employee VALUES (NULL, ?,?,?,?,?,?,?)",(EmpID, Firstname, Lastname, DOB , Gender, Address, Mobile))
    con.commit()
    con.close()

def viewData():
    con=sqlite3.connect("employee.db")
    cur = con.cursor()
    cur.execute("SELECT * FROM employee")
    rows=cur.fetchall()
    con.close()
    return rows

def deleteRec(id):
    con=sqlite3.connect("employee.db")
    cur = con.cursor()
    cur.execute("DELETE FROM employee WHERE id=?", (id,))
    con.commit()
    con.close()

def searchData(EmpID="", Firstname="", Lastname="", DOB="", Gender="", Address="", Mobile=""):
    con=sqlite3.connect("employee.db")
    cur = con.cursor()
    cur.execute("SELECT * FROM employee WHERE EmpID=? OR Firstname=? OR Lastname=? OR DOB=? OR \
        Gender=? OR Address=? OR Mobile=? ", (EmpID, Firstname, Lastname, DOB, Gender, Address, Mobile))
    rows=cur.fetchall()
    con.close()
    return rows

def dataUpdate(id, EmpID="", Firstname="", Lastname="", DOB="", Gender="", Address="", Mobile=""):
    con=sqlite3.connect("employee.db")
    cur = con.cursor()
    cur.execute("UPDATE employee SET EmpID=?, Firstname=?, Lastname=?, DOB=?, \
        Gender=?, Address=?, Mobile=?, WHERE id=?",(EmpID, Firstname, Lastname, DOB, Gender, Address, Mobile, id))
    con.commit()
    con.close()

employeeData()


from django.conf.urls import url
from django.urls import path, include
from .views import *

app_name = 'customer'

urlpatterns = [
    url(r'^homepage/$', homepage, name="homepage"),
    url(r'^view_offer/$', view_offer, name="view_offer"),
    url(r'^delete_account/$', delete_account, name="delete_account"),
    url(r'^view_items/$', view_items, name="view_items"),
    url(r'add_to_cart/$', add_to_cart, name="add_to_cart"),
    url(r'^view_cart/$', view_cart, name="view_cart"),
    path(r'^place_order/(?P<total_cost>[0-9])/$', place_order, name="place_order"),
    url(r'^view_orders/$', view_orders, name="view_orders"),
    url(r'^add_credit/$', add_credit, name="add_credit"),
    url(r'^view_feedback/$', view_feedback, name="view_feedback"),
    url(r'^download_zip/(?P<order_id>\w+)$', download_zip, name="download_zip"),
    url(r'^add_feedback/(?P<order_id>\w+)/$', add_feedback, name="add_feedback"),
    path(r'^apply_coupon/<int:pk>/$', apply_coupon, name="apply_coupon"),
    path(r'decrease_from_cart/(?P<name>\w+)', decrease_from_cart, name="decrease_from_cart"),
    path(r'increase_from_cart/(?P<name>\w+)', increase_from_cart, name="increase_from_cart"),
]


from django import forms
from .models import ImageRequests, DescriptorRequests

class ImageForm(forms.ModelForm):
    """Form for the image model"""
    class Meta:
        model = ImageRequests
        fields = ('title', 'image', 'classification', 'subclassification')

class SearchForm(forms.ModelForm):
    class Meta:
        model = DescriptorRequests
        fields = ('descriptor1', 'descriptor2', 'distance', 'top', 'R_precision')


#!/usr/bin/env python3

"""
get_course_info.py
    gets info associated with a given course ID, then saves it to a local DynamoDB

Usage:
    ./get_course_info [-s] [-w WORKERS]

Output:
    None

Creator:
    Jack Rundle
"""

from bs4 import BeautifulSoup
import requests

import asyncio
import concurrent.futures

from time import time
import sys

from config import Config

def wrapper(args):
    return get_course_info(*args)

def get_course_info(course_id, session):
    """gets tees for a courseId"""
    # load course's page
    s = time()
    url = f"{Config.URL}{Config.COURSE_ENDPOINT}?CourseID={course_id}"
    resp = session.get(url)

    timing = time() - s
    soup = BeautifulSoup(resp.text, "html.parser")

    # load course info
    course_info_table = soup.find(id="gvCourseTees")
    try:
        row = course_info_table.find_all("tr")[1].find_all("td")
        course, club = row[0].text.split(" - ")
        city = row[1].text
        state = row[1].text

        course_info = {
            "course_id": course_id,
            "club_name": course,
            "course_name": club,
            "city": city,
            "state": state
        }

    except (IndexError, AttributeError):
        return

    # load tee table
    tee_table = soup.find(id="gvTee")
    rows = tee_table.find_all("tr")[1:] if tee_table else []

    # extract tee info from each row
    tees = []
    for row in rows:
        color, gender, par, _, bogey, _, front, back = map(lambda info: info.text.strip(), row.find_all("td"))

        front, back = front.split('/'), back.split('/')

        tees.append({
            "tee_name": color,
            "gender": gender,
            "par": par,
            "front_cr": front[0].strip(),
            "front_sr": front[1].strip(),
            "back_cr": back[0].strip(),
            "back_sr": back[1].strip(),
            "bogey_rating": bogey
        })

    course_info["tees"] = tees

    save_course_info(course_info)
    return timing
    # return course_info

def save_course_info(info):
    # save to database
    print(info)
    """
    courses.insert_one(info["course"])
    tees.insert_many(info["tees"])
    """

def process(info_list, count):
    print(f"Average Load Time: {sum(info_list) / count:.2f}")

if __name__ == "__main__":
    use_concurrency = True
    workers = 30

    i = 1
    while i < len(sys.argv):
        arg = sys.argv[i]

        if arg == "-s":
            use_concurrency = False
        elif arg == "-w":
            if i + 1 != len(sys.argv):
                workers = int(sys.argv[i + 1])
                i += 1

        i += 1

    with requests.Session() as session:
        ids = []
        for line in sys.stdin.readlines():
            ids.append(line.rstrip())

        s = time()

        if use_concurrency:
            params = (
                (id, session) for id in ids
            )
            with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as executor:
                info_list = executor.map(wrapper, params)

        else:
            info_list = [
                get_course_info(id, session) for id in ids
            ]
        process(info_list, len(ids))
        print(f"    Total Time: {time() - s:.2f}")


# -*- coding: utf-8 -*-
"""
Created on Tue Sep 11 13:01:08 2018
@author: Jason Ioffe
"""

import numpy as np
import cv2

FRAME_CAPTION = 'OpenCV - Webcam Capture'

# This is all it takes to start capturing
# from the primary camera with OpenCV
print('Starting camera capture...')
cap = cv2.VideoCapture(0)
print('Camera capture started!')

while(True):
    # For each frame, capture the image contents
    # ret contains a boolean: true means the capture read was successful
    # frame will contain the actual pixel data as a numpy array in BGR format
    ret, frame = cap.read()
    cv2.imshow(FRAME_CAPTION, frame)

    #passing 1 means that this will be non-blocking, so this loop continues
    #27 is the ESC key
    if cv2.waitKey(1) & 0xff == 27:
        break

# When everything done, release the capture
cap.release()
print('Camera capture released')
cv2.destroyAllWindows()


import os
import discord
import time
import datetime
import asyncio
import pymongo
from discord.ext import commands
from discord import Webhook, app_commands
from dotenv import load_dotenv
from pymongo import MongoClient
import aiohttp
cluster = MongoClient(os.getenv("Mongo"))
db = cluster["discord"]
collection = db["Leaver"]
class leave(commands.GroupCog):
    def __init__(self, client):
        self.client = client
        load_dotenv()

#leave commands

    @app_commands.command(description="Sets up a leave message")
    @app_commands.describe(channel="The channel you want your leave message in")
    @app_commands.describe(message="The message you want the bot to send on someone leavign")
    async def enable(self,interaction: discord.Interaction,channel : discord.TextChannel,message: str):
        result = collection.find_one({"_id": interaction.guild.id})
        if result :
            collection.update_one({"_id": interaction.guild.id}, {"$set":{f"{interaction.guild.id}": [channel.id,message]}})
            await interaction.response.send_message("leave message updated!")
            return
        else:
            collection.insert_one({"_id": interaction.guild.id, f"{interaction.guild.id}": [channel.id, message]})
            await interaction.response.send_message("leave message enabled!")

    @app_commands.command(description="Removes leave message from database")
    async def disable(self,interaction: discord.Interaction):
        results = collection.find({"_id": interaction.guild.id})
        for result in results:
           if result["_id"] == interaction.guild.id:
                collection.delete_one({"_id": interaction.guild.id})
                await interaction.response.send_message("I have removed the guild from the database!")
           else:
                await interaction.response.send_message("There is no leave message in the database!")

    @commands.Cog.listener()
    async def on_member_remove(self,member : discord.member):
        guild_id = member.guild.id
        result = collection.find_one({"_id": member.guild.id})
        if result is None: return

        else:
            channel_id = result[f'{member.guild.id}'][0]
            channel = self.client.get_channel(channel_id)
            leave_message = result[f'{member.guild.id}'][1]
            first_message = leave_message
            second_message = first_message.replace("{member.mention}",f"{member.mention}")
            third_message = second_message.replace("{member}",f"{member}")
            final_message = third_message.replace("{member.guild}",f"{member.guild}")
            await channel.send(f"{final_message}")

async def setup(client):
    await client.add_cog(leave(client))


import requests
from eelib.config import load

config = load()
base_url = config["backend"]["url"]
token = config["backend"]["token"]

def send_websocket_message(route, event_type, data):
    try:
        url = f"{base_url}/websocket/echo/{route}?tk={token}"
        body = {
            "event_type": event_type,
            "data": data
        }
        requests.post(url, json=body)
    except Exception as e:
        print("Exiting because of error:", repr(e))


from django.shortcuts import render
from django.contrib import messages
from django.shortcuts import redirect
from django.contrib.auth import login
from django.contrib.auth.models import User
from constance import config
from django.contrib.auth.decorators import login_required
from django.views.decorators.csrf import csrf_exempt
from django.utils import timezone
import json, socket, decimal
from django.http import JsonResponse
from django.core.exceptions import ObjectDoesNotExist

from gravity.models import GravitySensor, GravityLog, GravityLogPoint, TiltGravityCalibrationPoint, TiltBridge, TiltConfiguration

from gravity import mdnsLocator

import gravity.gravity_debug as gravity_debug

import csv

try:
    import numpy
    NUMPY_ENABLED = True
except:
    NUMPY_ENABLED = False

from app.decorators import site_is_configured, login_if_required_for_dashboard, gravity_support_enabled

import os, datetime, pytz, logging

import gravity.forms as forms

logger = logging.getLogger(__name__)

import fermentrack_django.settings as settings

@login_required
@site_is_configured
def gravity_tilt_coefficients(request, sensor_id):
    # TODO - Add user permissioning
    # if not request.user.has_perm('app.edit_device'):
    #     messages.error(request, 'Your account is not permissioned to edit devices. Please contact an admin')
    #     return redirect("/")

    try:
        sensor = GravitySensor.objects.get(id=sensor_id)
    except:
        messages.error(request, u'Unable to load sensor with ID {}'.format(sensor_id))
        return redirect('gravity_log_list')

    if sensor.sensor_type != GravitySensor.SENSOR_TILT:
        messages.error(request, u'Sensor {} is not a Tilt and cannot be configured in this way'.format(sensor_id))
        return redirect('gravity_log_list')

    if request.POST:
        tilt_coefficient_form = forms.TiltCoefficientForm(request.POST)
        if tilt_coefficient_form.is_valid():
            # sensor.tilt_configuration.grav_third_degree_coefficient = tilt_coefficient_form.cleaned_data['a']
            if tilt_coefficient_form.cleaned_data['b'] is None:
                sensor.tilt_configuration.grav_second_degree_coefficient = 0
            else:
                sensor.tilt_configuration.grav_second_degree_coefficient = tilt_coefficient_form.cleaned_data['b']

            if tilt_coefficient_form.cleaned_data['c'] is None:
                sensor.tilt_configuration.grav_first_degree_coefficient = 1
            else:
                sensor.tilt_configuration.grav_first_degree_coefficient = tilt_coefficient_form.cleaned_data['c']

            if tilt_coefficient_form.cleaned_data['d'] is None:
                sensor.tilt_configuration.grav_constant_term = 0
            else:
                sensor.tilt_configuration.grav_constant_term = tilt_coefficient_form.cleaned_data['d']

            if sensor.tilt_configuration.coefficients_up_to_date:
                # If we are manually setting the coefficients, then we'll assume they're up to date
                sensor.tilt_configuration.coefficients_up_to_date = True

            sensor.tilt_configuration.save()
            messages.success(request, u"Coefficients updated")

        else:
            messages.error(request, u"Invalid coefficients provided")
    else:
        messages.error(request, u"No coefficients provided")

    return redirect("gravity_manage", sensor_id=sensor_id)

@login_required
@site_is_configured
def gravity_tilt_add_gravity_calibration_point(request, sensor_id):
    # TODO - Add user permissioning
    # if not request.user.has_perm('app.edit_device'):
    #     messages.error(request, 'Your account is not permissioned to edit devices. Please contact an admin')
    #     return redirect("/")

    try:
        sensor = GravitySensor.objects.get(id=sensor_id)
    except:
        messages.error(request, u'Unable to load sensor with ID {}'.format(sensor_id))
        return redirect('gravity_log_list')

    if sensor.sensor_type != GravitySensor.SENSOR_TILT:
        messages.error(request, u'Sensor {} is not a Tilt and cannot be configured in this way'.format(sensor_id))
        return redirect('gravity_log_list')

    if request.POST:
        tilt_calibration_point_form = forms.TiltGravityCalibrationPointForm(request.POST)
        if tilt_calibration_point_form.is_valid():
            tilt_calibration_point_form.save()
            messages.success(request, u"Calibration point added")

            if sensor.tilt_configuration.coefficients_up_to_date:
                # If we're changing any coefficients since the calibration script was last run, clear the 'calibrated'
                # flag so we know.
                messages.warning(request, u"New calibration points have been added since the coefficients were last "
                                          u"calculated - please re-run the coefficient calculation script to update "
                                          u"the specific gravity equation.")
                sensor.tilt_configuration.coefficients_up_to_date = False
                sensor.tilt_configuration.save()

        else:
            messages.error(request, u"Invalid calibration point provided")
    else:
        messages.error(request, u"No calibration point provided")

    return redirect("gravity_manage", sensor_id=sensor_id)

@login_required
@site_is_configured
def gravity_tilt_delete_gravity_calibration_point(request, sensor_id, point_id):
    # TODO - Add user permissioning
    # if not request.user.has_perm('app.edit_device'):
    #     messages.error(request, 'Your account is not permissioned to edit devices. Please contact an admin')
    #     return redirect("/")

    try:
        sensor = GravitySensor.objects.get(id=sensor_id)
    except:
        messages.error(request, u'Unable to load sensor with ID {}'.format(sensor_id))
        return redirect('gravity_log_list')

    if sensor.sensor_type != GravitySensor.SENSOR_TILT:
        messages.error(request, u'Sensor {} is not a Tilt and cannot be configured in this way'.format(sensor_id))
        return redirect('gravity_log_list')

    try:
        point = TiltGravityCalibrationPoint.objects.get(id=point_id)
    except:
        messages.error(request, u'Unable to find calibration point with ID {}'.format(point_id))
        return redirect("gravity_manage", sensor_id=sensor_id)

    if point.sensor != sensor.tilt_configuration:
        messages.error(request, u"Point {} doesn't belong to sensor {}".format(point_id, sensor_id))
        return redirect("gravity_manage", sensor_id=sensor_id)

    # The sensor exists & is a Tilt, the point exists & belongs to the sensor. Delete it.
    point.delete()

    messages.success(request, u"Calibration point removed")

    if sensor.tilt_configuration.coefficients_up_to_date:
        # If we're changing any coefficients since the calibration script was last run, clear the 'calibrated'
        # flag so we know.
        messages.warning(request, u"Calibration points have been removed since the coefficients were last "
                                  u"calculated - please re-run the coefficient calculation script to update "
                                  u"the specific gravity equation.")
        sensor.tilt_configuration.coefficients_up_to_date = False
        sensor.tilt_configuration.save()

    return redirect("gravity_manage", sensor_id=sensor_id)

@login_required
@site_is_configured
def gravity_tilt_calibrate(request, sensor_id):
    # TODO - Add user permissioning
    # if not request.user.has_perm('app.edit_device'):
    #     messages.error(request, 'Your account is not permissioned to edit devices. Please contact an admin')
    #     return redirect("/")

    try:
        sensor = GravitySensor.objects.get(id=sensor_id)
    except:
        messages.error(request, u'Unable to load sensor with ID {}'.format(sensor_id))
        return redirect('gravity_log_list')

    if sensor.sensor_type != GravitySensor.SENSOR_TILT:
        messages.error(request, u'Sensor {} is not a Tilt and cannot be configured in this way'.format(sensor_id))
        return redirect('gravity_log_list')

    if not NUMPY_ENABLED:
        messages.error(request, u'The "numpy" python package is not available which is required for calibration')
        return redirect('gravity_log_list')

    points = TiltGravityCalibrationPoint.objects.filter(sensor=sensor.tilt_configuration)

    # Before we do the polyfit, we need to determine the degree of the equation we want to end up with. It doesn't make
    # sense to do a cubic fit with less than 4 points, a quadratic fit less than 3, linear with less than 2, etc. so
    # determine the maximum degrees here (as num points - 1). Max out at quadratic.
    if points.count() < 2:
        messages.error(request, u"Coefficient calculation requires at least 2 (preferably 3+) points to function")
        return redirect("gravity_manage", sensor_id=sensor_id)
    elif points.count() >= 3:
        # If we have more than 4 points, max out at a cubic function
        degree = 2
    else:
        # If we have 2 or 3 points, do a first or second order polyfit
        degree = points.count() - 1

    # For the Tilt, we're not going to complain about a linear fit
    # if degree == 1:
    #     # Although we can do a linear fit, it's not really a good idea. Let the user know what they're getting into.
    #     messages.warning(request, u"Only 2 calibration points available. Your resulting function will be linear, and "
    #                               u"will likely not be accurate. It is highly recommended to add additional points and "
    #                               u"re-perform calibration.")

    # Now set up the x/y arrays and have numpy do the heavy lifting
    # The input for our equation is the measurement, with the desired output being the actual gravity
    x = [float(point.tilt_measured_gravity) for point in points]
    y = [float(point.actual_gravity) for point in points]
    poly_terms = numpy.polyfit(x, y, degree)

    # Save the results out to our Tilt configuration...
    i = 0  # This is a bit hackish, but it works
    # if degree == 3:
    #     sensor.tilt_configuration.third_degree_coefficient = poly_terms[i]
    #     i += 1
    if degree >= 2:
        sensor.tilt_configuration.grav_second_degree_coefficient = poly_terms[i]
        i += 1
    if degree >= 1:
        sensor.tilt_configuration.grav_first_degree_coefficient = poly_terms[i]
        i += 1
    sensor.tilt_configuration.grav_constant_term = poly_terms[i]

    sensor.tilt_configuration.coefficients_up_to_date = True
    sensor.tilt_configuration.save()

    # ...and we're done!
    messages.success(request, u"Coefficients have been updated based on the calibration points. This may take up to a "
                              u"minute to take effect.")

    return redirect("gravity_manage", sensor_id=sensor_id)

@login_required
@site_is_configured
def gravity_tilt_guided_calibration(request, sensor_id, step):
    # TODO - Add user permissioning
    # if not request.user.has_perm('app.edit_device'):
    #     messages.error(request, 'Your account is not permissioned to edit devices. Please contact an admin')
    #     return redirect("/")

    try:
        sensor = GravitySensor.objects.get(id=sensor_id)
    except:
        messages.error(request, u'Unable to load sensor with ID {}'.format(sensor_id))
        return redirect('gravity_log_list')

    if sensor.sensor_type != GravitySensor.SENSOR_TILT:
        messages.error(request, u"Sensor {} is not a Tilt Hydrometer".format(sensor.name))
        return redirect('gravity_log_list')

    # Let's coerce step to an integer so we can do math on it
    step = int(step)

    # Before we do anything, see if we were passed data. If we were, process it.
    if "sensor" in request.POST:
        tilt_calibration_point_form = forms.TiltGravityCalibrationPointForm(request.POST)
        if tilt_calibration_point_form.is_valid():
            try:
                # If a point exists with the exact same expected specific gravity that we just entered, delete it.
                # This is specifically to prevent the user from accidentally running this calibration twice.
                point_to_delete = TiltGravityCalibrationPoint.objects.get(actual_gravity=tilt_calibration_point_form.cleaned_data['actual_gravity'],
                                                                          sensor=sensor.tilt_configuration)
                point_to_delete.delete()
            except:
                # No point existed. We're good.
                pass

            tilt_calibration_point_form.save()
            messages.success(request, u"Calibration point added")

            if sensor.tilt_configuration.coefficients_up_to_date:
                sensor.tilt_configuration.coefficients_up_to_date = False
                sensor.tilt_configuration.save()

        else:
            messages.error(request, u"Invalid calibration point provided - recheck the form try again")
            return redirect("gravity_tilt_guided_calibration", sensor_id=sensor_id, step=(step-1))
    else:
        # If we hit this, the user isn't submitting data. The user is allowed to skip steps - it just isn't recommended.
        pass

    # Alrighty. Let's calculate where we should land on each step of the calibration.

    # Water additions by step & sugar additions by step are both the amount of water/sugar being added in each step
    # in grams.
    water_additions_by_step = [2750, 250, 250, 250, 250, 500]
    sugar_additions_by_step = [0,    300, 300, 300, 300, 600]

    # Now let's translate that into data, organized by step (Note - step number is one-off from the 'step' parameter)

    step_data = []
    for i in range(len(water_additions_by_step)):
        this_step = {'step': (i+1)}
        this_step['water_addition'] = water_additions_by_step[i]
        this_step['sugar_addition'] = sugar_additions_by_step[i]

        this_step['cumulative_water'] = this_step['water_addition']
        this_step['cumulative_sugar'] = this_step['sugar_addition']
        if i > 0:
            this_step['cumulative_water'] += step_data[i-1]['cumulative_water']
            this_step['cumulative_sugar'] += step_data[i-1]['cumulative_sugar']

        this_step['plato'] = 1.0*this_step['cumulative_sugar'] / (this_step['cumulative_sugar'] + this_step['cumulative_water']) * 100
        this_step['specific_gravity'] = round(decimal.Decimal(1+this_step['plato']/(258.6-(227.1*(this_step['plato']/258.2)))), 3)
        this_step['plato'] = round(decimal.Decimal(this_step['plato']),2)  # Make it pretty to look at

        try:
            point_with_grav = TiltGravityCalibrationPoint.objects.get(actual_gravity=this_step['specific_gravity'],
                                                                      sensor=sensor.tilt_configuration)
            this_step['tilt_gravity'] = point_with_grav.tilt_measured_gravity
        except:
            this_step['tilt_gravity'] = ""

        step_data.append(this_step)

    # Now we're ready to proceed. Let's build the context & then determine what template to output to the user
    context = {'all_steps_data': step_data, 'on_step': step, 'next_step': step+1, 'active_device': sensor}

    if step == 0:
        # Step 0 just lays out the basic instructions. We do want to collect existing points (if any) so we can warn
        # the user, however.
        existing_points = TiltGravityCalibrationPoint.objects.filter(sensor=sensor.tilt_configuration)
        context['existing_points'] = existing_points
        return render(request, template_name='gravity/gravity_tilt_calibrate_start.html', context=context)
    elif step <= len(water_additions_by_step):
        tilt_calibration_point_form = forms.TiltGravityCalibrationPointForm(
            initial={'sensor': sensor.tilt_configuration, 'actual_gravity': step_data[step - 1]['specific_gravity']})
        context['tilt_calibration_point_form'] = tilt_calibration_point_form
        context['this_step_data'] = step_data[step - 1]
        return render(request, template_name='gravity/gravity_tilt_calibrate_step.html', context=context)
    else:
        # Last step is just a message.
        return render(request, template_name='gravity/gravity_tilt_calibrate_end.html', context=context)

@csrf_exempt
def tiltbridge_handler(request):
    if request.body is None:
        logger.error("No data in TiltBridge request body")
        return JsonResponse({'status': 'failed', 'message': "No data in request body"}, safe=False,
                            json_dumps_params={'indent': 4})

    # Data from a traditional TiltBridge should look like this:
    # {
    #   'mdns_id': 'mDNS ID goes here',
    #   'tilts': {'color': 'Purple', 'temp': 74, 'gravity': 1.043},
    #            {'color': 'Orange', 'temp': 66, 'gravity': 1.001}
    # }

    try:
        tiltbridge_data = json.loads(request.body.decode('utf-8'))
    except json.JSONDecodeError:
        return JsonResponse({'status': 'failed', 'message': "Malformed JSON - Unable to parse!"}, safe=False,
                            json_dumps_params={'indent': 4})

    tilts_data = tiltbridge_data.get('tilts')
    if not tilts_data:
        # The TiltBridge has no connected Tilts. Return success (but note as such)
        return JsonResponse({'status': 'success', 'message': "No Tilts in TiltBridge data to process"}, safe=False,
                            json_dumps_params={'indent': 4})

    tiltbridge_junior = tiltbridge_data.get('tiltbridge_junior', False)
    if tiltbridge_junior:
        # There is assumed to be only one TiltBridge Junior connected to a copy of Fermentrack (and is the equivalent
        # of the former "bluetooth" connection)
        tiltbridge_obj = None
    else:
        # A normal Tiltbridge (rather than the Docker container) will have an mDNS ID. Let's use that to find the
        # device.
        mdns_id = tiltbridge_data.get('mdns_id')
        if not mdns_id:
            logger.error("Malformed TiltBridge JSON - No mdns ID provided!")
            return JsonResponse({'status': 'failed', 'message': "Malformed JSON - No mDNS ID provided!"}, safe=False,
                                json_dumps_params={'indent': 4})

        try:
            tiltbridge_obj = TiltBridge.objects.get(mdns_id=mdns_id)
        except ObjectDoesNotExist:
            logger.error("Unable to load TiltBridge with mDNS ID {}".format(mdns_id))
            return JsonResponse({'status': 'failed', 'message': "Unable to load TiltBridge with that mdns_id"}, safe=False,
                                json_dumps_params={'indent': 4})

    for tilt_row in tilts_data:
        if tiltbridge_junior:
            try:
                tilt_obj = TiltConfiguration.objects.get(connection_type=TiltConfiguration.CONNECTION_BLUETOOTH,
                                                         color__iexact=tilt_row['color'])
                tilt_data = tilt_row
            except ObjectDoesNotExist:
                # We received data for an invalid tilt from TiltBridge Junior
                continue
        else:
            try:
                tilt_obj = TiltConfiguration.objects.get(connection_type=TiltConfiguration.CONNECTION_BRIDGE,
                                                         tiltbridge=tiltbridge_obj, color__iexact=tilt_row)
                tilt_data = tilts_data[tilt_row]
            except ObjectDoesNotExist:
                # We received data for an invalid tilt from TiltBridge
                continue

        if tiltbridge_junior:  # TODO - Change this to look for a key that is sent by updated TiltBridges/TiltBridge Juniors
            raw_temp = float(tilt_data['smoothed_temp'])
            converted_smoothed_temp, temp_format = tilt_obj.sensor.convert_temp_to_sensor_format(raw_temp,
                                                                                tilt_data['temp_format'])

            smoothed_gravity = float(tilt_data['smoothed_gravity'])
            normalized_smoothed_gravity = tilt_obj.apply_gravity_calibration(smoothed_gravity)

            # We actually have a raw/smoothed split here, so we need to use the raw gravity to calculate the latest
            latest_gravity = tilt_obj.apply_gravity_calibration(float(tilt_data['raw_gravity']))
            latest_temp, _ = tilt_obj.sensor.convert_temp_to_sensor_format(float(tilt_data['raw_temp']),
                                                                           tilt_data['temp_format'])
        else:
            raw_temp = float(tilt_data['temp'])
            converted_smoothed_temp, temp_format = tilt_obj.sensor.convert_temp_to_sensor_format(raw_temp,
                                                                                tilt_data['tempUnit'])
            smoothed_gravity = float(tilt_data['gravity'])
            normalized_smoothed_gravity = tilt_obj.apply_gravity_calibration(smoothed_gravity)

            latest_gravity = normalized_smoothed_gravity
            latest_temp = converted_smoothed_temp

        new_point = GravityLogPoint(
            gravity=normalized_smoothed_gravity,
            temp=converted_smoothed_temp,
            temp_format=temp_format,
            temp_is_estimate=False,
            associated_device=tilt_obj.sensor,
            gravity_latest=latest_gravity,
            temp_latest=latest_temp,
        )

        if tilt_obj.sensor.active_log is not None:
            new_point.associated_log = tilt_obj.sensor.active_log

        new_point.save()

        # Now that the point is saved, save out the 'extra' data, so we can use it later for calibration
        tilt_obj.raw_gravity = smoothed_gravity
        tilt_obj.raw_temp = raw_temp  # TODO - Determine if we want to record this in F or sensor_format

        # TiltBridge Junior (and, potentially, future versions of TiltBridge) send the following. Save them.
        if 'rssi' in tilt_data:
            tilt_obj.rssi = tilt_data['rssi']
        if 'raw_gravity' in tilt_data:
            tilt_obj.raw_gravity = tilt_data['raw_gravity']
        if 'raw_temp' in tilt_data:
            tilt_obj.raw_temp = tilt_data['raw_temp']
        if 'tilt_pro' in tilt_data:
            tilt_obj.tilt_pro = tilt_data['tilt_pro']
        if 'sends_battery' in tilt_data:
            tilt_obj.sends_battery = tilt_data['sends_battery']
        if 'weeks_on_battery' in tilt_data:
            tilt_obj.weeks_on_battery = tilt_data['weeks_on_battery']
        if 'firmware_version' in tilt_data:
            tilt_obj.firmware_version = tilt_data['firmware_version']

        tilt_obj.save_extras_to_redis()

    return JsonResponse({'status': 'success', 'message': "TiltBridge data processed successfully"}, safe=False,
                        json_dumps_params={'indent': 4})

@login_required
@site_is_configured
def gravity_tiltbridge_add(request):
    # TODO - Add user permissioning
    # if not request.user.has_perm('app.edit_device'):
    #     messages.error(request, 'Your account is not permissioned to edit devices. Please contact an admin')
    #     return redirect("/")
    installed_devices, available_devices = mdnsLocator.find_mdns_tiltbridge_devices()

    if request.POST:
        form = forms.TiltBridgeForm(request.POST)
        if form.is_valid():
            new_tiltbridge = form.save()
            messages.success(request, u"TiltBridge '{}' created".format(new_tiltbridge.name))

            fermentrack_host = request.META['HTTP_HOST']

            if new_tiltbridge.update_fermentrack_url_on_tiltbridge(fermentrack_host):
                messages.success(request, u"Updated Fermentrack URL on TiltBridge '{}'".format(new_tiltbridge.name))
                return redirect("gravity_add_board")
            else:
                messages.error(request, u"Unable to automatically update Fermentrack URL on TiltBridge {}".format(new_tiltbridge.name))
                return redirect("gravity_tiltbridge_urlerror", tiltbridge_id=new_tiltbridge.mdns_id)

        else:
            messages.error(request, u"Invalid TiltBridge specification")

    else:
        form = forms.TiltBridgeForm()

    return render(request, template_name='gravity/gravity_tiltbridge_add.html',
                  context={'form': form, 'available_devices': available_devices,})

@login_required
@site_is_configured
def gravity_tiltbridge_set_url(request, tiltbridge_id, sensor_id=None):
    # TODO - Add user permissioning
    # if not request.user.has_perm('app.edit_device'):
    #     messages.error(request, 'Your account is not permissioned to edit devices. Please contact an admin')
    #     return redirect("/")

    try:
        this_tiltbridge = TiltBridge.objects.get(mdns_id=tiltbridge_id)
    except ObjectDoesNotExist:
        messages.error(request, "Unable to locate TiltBridge with mDNS ID {}".format(tiltbridge_id))
        if sensor_id is not None:
            return redirect("gravity_manage", sensor_id=sensor_id)
        else:
            return redirect("siteroot")

    fermentrack_host = request.META['HTTP_HOST']

    if this_tiltbridge.update_fermentrack_url_on_tiltbridge(fermentrack_host):
        messages.success(request, u"Updated Fermentrack URL on TiltBridge '{}'".format(this_tiltbridge.name))
    else:
        messages.error(request, u"Unable to automatically update Fermentrack URL at {}.local".format(this_tiltbridge.mdns_id))

    # If we were passed a sensor ID, we want to return to the management screen for that ID.
    if sensor_id is not None:
        return redirect("gravity_manage", sensor_id=sensor_id)
    else:
        return redirect("siteroot")

@login_required
@site_is_configured
def gravity_tiltbridge_urlerror(request, tiltbridge_id):
    # TODO - Add user permissioning
    # if not request.user.has_perm('app.edit_device'):
    #     messages.error(request, 'Your account is not permissioned to edit devices. Please contact an admin')
    #     return redirect("/")

    try:
        selected_tiltbridge = TiltBridge.objects.get(mdns_id=tiltbridge_id)
    except:
        messages.error(request, u"Unable to load TiltBridge with mDNS ID '{}'".format(tiltbridge_id))
        return redirect("gravity_add_board")

    # In order to tell the user what to do, we need to look up Fermentrack's IP address. Attempt it here, and send back
    # an error if this fails.
    try:
        fermentrack_host = request.META['HTTP_HOST']
        if ":" in fermentrack_host:
            fermentrack_host = fermentrack_host[:fermentrack_host.find(":")]
        ais = socket.getaddrinfo(fermentrack_host, 0, 0, 0, 0)
        ip_list = [result[-1][0] for result in ais]
        ip_list = list(set(ip_list))
        resolved_address = ip_list[0]
        fermentrack_url = "http://{}/tiltbridge/".format(resolved_address)

    except:
        # For some reason we failed to resolve the IP address of Fermentrack. Return an error.
        messages.error(request, u"Unable to identify Fermentrack's IP Address ")
        return redirect("gravity_add_board")

    return render(request, template_name='gravity/gravity_tiltbridge_urlerror.html',
                  context={'tiltbridge': selected_tiltbridge, 'fermentrack_url': fermentrack_url,})


import logging

from flask_appbuilder import expose, BaseView as AppBuilderBaseView

from folioclient import FolioClient
from airflow.models import Variable
from flask import make_response

logger = logging.getLogger(__name__)

class Healthcheck(AppBuilderBaseView):
    default_view = "home"
    route_base = "/healthcheck"

    @expose("/")
    def home(self):
        statuses = self._statuses
        http_status = 200 if all(statuses.values()) else 500
        return make_response(
            self.render_template("healthcheck/index.html", statuses=statuses),
            http_status,
        )

    @property
    def _statuses(self):
        if not self._check_folio_login():
            return {"Folio login": False}

        return {
            "Folio login": True,
            "Migration login": self._check_migration_login(),
            "Holdings custom mappings": self._check_holdings_custom_mappings(),
            "Bib custom mappings": self._check_bib_custom_mappings(),
        }

    @property
    def _folio_client(self):
        return FolioClient(
            Variable.get("OKAPI_URL"),
            "sul",
            Variable.get("FOLIO_USER"),
            Variable.get("FOLIO_PASSWORD"),
        )

    def _check_folio_login(self):
        try:
            self._folio_client
            return True
        except Exception:
            return False

    def _check_holdings_custom_mappings(self):
        mapping_rules = self._folio_client.folio_get("/mapping-rules/marc-holdings")
        entities = mapping_rules['852'][0]['entity']
        matching_entities = [
            entity
            for entity in entities
            if entity['target'] == 'permanentLocationId'
            and entity['subfield'] == ['b', 'c']
        ]
        return len(matching_entities) > 0

    def _check_bib_custom_mappings(self):
        mapping_rules = self._folio_client.folio_get("/mapping-rules/marc-bib")
        return '910' in mapping_rules

    def _check_migration_login(self):
        try:
            migration_client = FolioClient(
                Variable.get("OKAPI_URL"),
                "sul",
                Variable.get("migration_user"),
                Variable.get("migration_password"),
            )
            return migration_client is not None
        except Exception:
            return False


import builtins
import numpy as np
import symengine as se
from importlib import import_module
from core.cbfs.cbf_wrappers import symbolic_cbf_wrapper_singleagent
from core.mathematics.symbolic_functions import ramp

vehicle = builtins.PROBLEM_CONFIG["vehicle"]
control_level = builtins.PROBLEM_CONFIG["control_level"]
mod = "models." + vehicle + "." + control_level + ".system"

# Programmatic import
try:
    module = import_module(mod)
    globals().update({"f": getattr(module, "f")})
    globals().update({"ss": getattr(module, "xs")})
except ModuleNotFoundError as e:
    print("No module named '{}' -- exiting.".format(mod))
    raise e

# Defining Physical Params
R = 1.0
cx1 = -2.0
cy1 = 0.0
cx2 = 1.25
cy2 = -1.25

# Define Physical Distances
dx1 = ss[0] - cx1
dy1 = ss[1] - cy1
dx2 = ss[0] - cx2
dy2 = ss[1] - cy2

# Define more quantities
phidot = f(np.zeros((len(ss),)), True)[6]
thedot = f(np.zeros((len(ss),)), True)[7]

# Speed CBF Symbolic
h_oa1_symbolic = dx1**2 + dy1**2 - R**2
h_oa2_symbolic = dx2**2 + dy2**2 - R**2
h_oa3_symbolic = ss[2] + f(np.zeros((len(ss),)), True)[2]
h_oa4_symbolic = (
    -phidot * se.sin(ss[6]) * se.cos(ss[7])
    - thedot * se.cos(ss[6]) * se.sin(ss[7])
    + (se.cos(ss[6]) * se.cos(ss[7]) - np.cos(np.pi / 3)) * 0.5
)

# h_oa4_symbolic = -10 * (
#     phidot * (se.sin(ss[6]) * se.cos(ss[7]))
#     + thedot * (se.cos(ss[7]) + se.sin(ss[6]) * se.cos(ss[7]) + se.cos(ss[6]) * se.sin(ss[7]))
#     + (se.sin(ss[7]) - se.sin(ss[6]) * se.cos(ss[7]) - se.cos(ss[6]) * se.cos(ss[7]))
# )

dhdx_oa1_symbolic = (se.DenseMatrix([h_oa1_symbolic]).jacobian(se.DenseMatrix(ss))).T
dhdx_oa2_symbolic = (se.DenseMatrix([h_oa2_symbolic]).jacobian(se.DenseMatrix(ss))).T
dhdx_oa3_symbolic = (se.DenseMatrix([h_oa3_symbolic]).jacobian(se.DenseMatrix(ss))).T
dhdx_oa4_symbolic = (se.DenseMatrix([h_oa4_symbolic]).jacobian(se.DenseMatrix(ss))).T

d2hdx2_oa1_symbolic = dhdx_oa1_symbolic.jacobian(se.DenseMatrix(ss))
d2hdx2_oa2_symbolic = dhdx_oa2_symbolic.jacobian(se.DenseMatrix(ss))
d2hdx2_oa3_symbolic = dhdx_oa3_symbolic.jacobian(se.DenseMatrix(ss))
d2hdx2_oa4_symbolic = dhdx_oa4_symbolic.jacobian(se.DenseMatrix(ss))

h_oa1_func = symbolic_cbf_wrapper_singleagent(h_oa1_symbolic, ss)
h_oa2_func = symbolic_cbf_wrapper_singleagent(h_oa2_symbolic, ss)
h_oa3_func = symbolic_cbf_wrapper_singleagent(h_oa3_symbolic, ss)
h_oa4_func = symbolic_cbf_wrapper_singleagent(h_oa4_symbolic, ss)

dhdx_oa1_func = symbolic_cbf_wrapper_singleagent(dhdx_oa1_symbolic, ss)
dhdx_oa2_func = symbolic_cbf_wrapper_singleagent(dhdx_oa2_symbolic, ss)
dhdx_oa3_func = symbolic_cbf_wrapper_singleagent(dhdx_oa3_symbolic, ss)
dhdx_oa4_func = symbolic_cbf_wrapper_singleagent(dhdx_oa4_symbolic, ss)

d2hdx2_oa1_func = symbolic_cbf_wrapper_singleagent(d2hdx2_oa1_symbolic, ss)
d2hdx2_oa2_func = symbolic_cbf_wrapper_singleagent(d2hdx2_oa2_symbolic, ss)
d2hdx2_oa3_func = symbolic_cbf_wrapper_singleagent(d2hdx2_oa3_symbolic, ss)
d2hdx2_oa4_func = symbolic_cbf_wrapper_singleagent(d2hdx2_oa4_symbolic, ss)

# Tau Formulation for PCA-CBF
dvx = f(np.zeros((len(ss),)), True)[0]
dvy = f(np.zeros((len(ss),)), True)[1]

tau_sym = se.Symbol("tau", real=True)

# tau* for computing tau
epsilon = 1e-3
tau1_star_symbolic = -(dx1 * dvx + dy1 * dvy) / (dvx**2 + dvy**2 + epsilon)
dtau1stardx_symbolic = (se.DenseMatrix([tau1_star_symbolic]).jacobian(se.DenseMatrix(ss))).T
d2tau1stardx2_symbolic = dtau1stardx_symbolic.jacobian(se.DenseMatrix(ss))
tau1_star = symbolic_cbf_wrapper_singleagent(tau1_star_symbolic, ss)
dtau1stardx = symbolic_cbf_wrapper_singleagent(dtau1stardx_symbolic, ss)
d2tau1stardx2 = symbolic_cbf_wrapper_singleagent(d2tau1stardx2_symbolic, ss)

tau2_star_symbolic = -(dx2 * dvx + dy2 * dvy) / (dvx**2 + dvy**2 + epsilon)
dtau2stardx_symbolic = (se.DenseMatrix([tau2_star_symbolic]).jacobian(se.DenseMatrix(ss))).T
d2tau2stardx2_symbolic = dtau2stardx_symbolic.jacobian(se.DenseMatrix(ss))
tau2_star = symbolic_cbf_wrapper_singleagent(tau2_star_symbolic, ss)
dtau2stardx = symbolic_cbf_wrapper_singleagent(dtau2stardx_symbolic, ss)
d2tau2stardx2 = symbolic_cbf_wrapper_singleagent(d2tau2stardx2_symbolic, ss)

# tau for computing PCA-CBF
Tmax = 10.0
kh = 1000.0
tau_star_sym = se.Symbol("tau_star", real=True)
tau_symbolic = tau_star_sym * ramp(tau_star_sym, kh, 0.0) - (tau_star_sym - Tmax) * ramp(
    tau_star_sym, kh, Tmax
)
dtaudtaustar_symbolic = se.diff(tau_symbolic, tau_star_sym)
d2taudtaustar2_symbolic = se.diff(dtaudtaustar_symbolic, tau_star_sym)
tau = symbolic_cbf_wrapper_singleagent(tau_symbolic, [tau_star_sym])
dtaudtaustar = symbolic_cbf_wrapper_singleagent(dtaudtaustar_symbolic, [tau_star_sym])
d2taudtaustar2 = symbolic_cbf_wrapper_singleagent(d2taudtaustar2_symbolic, [tau_star_sym])

# Predictive Obstacle Avoidance CBF1
h1_predictive_oa_symbolic = (dx1 + tau_sym * dvx) ** 2 + (dy1 + tau_sym * dvy) ** 2 - R**2
dhdx1_predictive_oa_symbolic = (
    se.DenseMatrix([h1_predictive_oa_symbolic]).jacobian(se.DenseMatrix(ss))
).T
dh1dtau_predictive_oa_symbolic = se.diff(h1_predictive_oa_symbolic, tau_sym)
d2h1dx2_predictive_oa_symbolic = dhdx1_predictive_oa_symbolic.jacobian(se.DenseMatrix(ss))
d2h1dtau2_predictive_oa_symbolic = se.diff(dh1dtau_predictive_oa_symbolic, tau_sym)
d2h1dtaudx_predictive_oa_symbolic = (
    se.DenseMatrix([dh1dtau_predictive_oa_symbolic]).jacobian(se.DenseMatrix(ss))
).T
h1_predictive_oa = symbolic_cbf_wrapper_singleagent(h1_predictive_oa_symbolic, ss)
dh1dx_predictive_oa = symbolic_cbf_wrapper_singleagent(dhdx1_predictive_oa_symbolic, ss)
dh1dtau_predictive_oa = symbolic_cbf_wrapper_singleagent(dh1dtau_predictive_oa_symbolic, ss)
d2h1dx2_predictive_oa = symbolic_cbf_wrapper_singleagent(d2h1dx2_predictive_oa_symbolic, ss)
d2h1dtaudx_predictive_oa = symbolic_cbf_wrapper_singleagent(d2h1dtaudx_predictive_oa_symbolic, ss)
d2h1dtau2_predictive_oa = symbolic_cbf_wrapper_singleagent(d2h1dtau2_predictive_oa_symbolic, ss)

# Predictive Obstacle Avoidance CBF2
h2_predictive_oa_symbolic = (dx2 + tau_sym * dvx) ** 2 + (dy2 + tau_sym * dvy) ** 2 - R**2
dhdx2_predictive_oa_symbolic = (
    se.DenseMatrix([h2_predictive_oa_symbolic]).jacobian(se.DenseMatrix(ss))
).T
dh2dtau_predictive_oa_symbolic = se.diff(h2_predictive_oa_symbolic, tau_sym)
d2h2dx2_predictive_oa_symbolic = dhdx2_predictive_oa_symbolic.jacobian(se.DenseMatrix(ss))
d2h2dtau2_predictive_oa_symbolic = se.diff(dh2dtau_predictive_oa_symbolic, tau_sym)
d2h2dtaudx_predictive_oa_symbolic = (
    se.DenseMatrix([dh2dtau_predictive_oa_symbolic]).jacobian(se.DenseMatrix(ss))
).T
h2_predictive_oa = symbolic_cbf_wrapper_singleagent(h2_predictive_oa_symbolic, ss)
dh2dx_predictive_oa = symbolic_cbf_wrapper_singleagent(dhdx2_predictive_oa_symbolic, ss)
dh2dtau_predictive_oa = symbolic_cbf_wrapper_singleagent(dh2dtau_predictive_oa_symbolic, ss)
d2h2dx2_predictive_oa = symbolic_cbf_wrapper_singleagent(d2h2dx2_predictive_oa_symbolic, ss)
d2h2dtaudx_predictive_oa = symbolic_cbf_wrapper_singleagent(d2h2dtaudx_predictive_oa_symbolic, ss)
d2h2dtau2_predictive_oa = symbolic_cbf_wrapper_singleagent(d2h2dtau2_predictive_oa_symbolic, ss)

# Relaxed Predictive Collision Avoidance
relaxation = 0.1  # Quadrotor Simulation

def h0_oa1(ego):
    return h_oa1_func(ego)

def h0_oa2(ego):
    return h_oa2_func(ego)

def dh0dx_oa1(ego):
    ret = dhdx_oa1_func(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

def dh0dx_oa2(ego):
    ret = dhdx_oa2_func(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

def d2h0dx2_oa1(ego):
    ret = d2hdx2_oa1_func(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

def d2h0dx2_oa2(ego):
    ret = d2hdx2_oa2_func(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

def h_poa1(ego):
    func = h1_predictive_oa(ego)

    try:
        ret = func.subs({tau_sym: tau([tau1_star(ego)])})
    except AttributeError:
        ret = func

    return ret

def h_poa2(ego):
    func = h2_predictive_oa(ego)

    try:
        ret = func.subs({tau_sym: tau([tau2_star(ego)])})
    except AttributeError:
        ret = func

    return ret

def dhdx_poa1(ego):
    func1 = dh1dx_predictive_oa(ego)
    func2 = dh1dtau_predictive_oa(ego)

    try:
        ret1 = func1.subs({tau_sym: tau([tau1_star(ego)])})
    except AttributeError:
        ret1 = func1

    try:
        ret2 = func2.subs({tau_sym: tau([tau1_star(ego)])})
    except AttributeError:
        ret2 = func2

    ret = ret1 + ret2 * dtaudtaustar([tau1_star(ego)]) * dtau1stardx(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

def dhdx_poa2(ego):
    func1 = dh2dx_predictive_oa(ego)
    func2 = dh2dtau_predictive_oa(ego)

    try:
        ret1 = func1.subs({tau_sym: tau([tau2_star(ego)])})
    except AttributeError:
        ret1 = func1

    try:
        ret2 = func2.subs({tau_sym: tau([tau2_star(ego)])})
    except AttributeError:
        ret2 = func2

    ret = ret1 + ret2 * dtaudtaustar([tau2_star(ego)]) * dtau2stardx(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

# Necessary for stochastic systems
def d2hdx2_poa1(ego):
    func1 = d2h1dx2_predictive_oa(ego)
    func2 = dh1dtau_predictive_oa(ego)

    try:
        ret1 = func1.subs({tau_sym: tau([tau1_star(ego)])})
    except AttributeError:
        ret1 = func1

    try:
        ret2 = func2.subs({tau_sym: tau([tau1_star(ego)])})
    except AttributeError:
        ret2 = func2

    d2hdx2_eval = ret1
    dtaustardx_eval = dtau1stardx(ego)
    dtaudtaustar_eval = dtaudtaustar([tau1_star(ego)])
    dhdtau_eval = ret2
    d2hdtau2_eval = d2h1dtau2_predictive_oa(ego)
    d2taudtaustar2_eval = d2taudtaustar2([tau1_star(ego)])
    d2taustardx2_eval = d2tau1stardx2(ego)
    outer = np.outer(dtaustardx_eval, dtaustardx_eval)

    ret = (
        d2hdx2_eval
        + dtaudtaustar_eval * d2hdtau2_eval * dtaudtaustar_eval * outer
        + dhdtau_eval * d2taudtaustar2_eval * outer
        + dhdtau_eval * d2taustardx2_eval * dtaudtaustar_eval
    )

    return np.squeeze(np.array(ret).astype(np.float64))

def d2hdx2_poa2(ego):
    func1 = d2h2dx2_predictive_oa(ego)
    func2 = dh2dtau_predictive_oa(ego)

    try:
        ret1 = func1.subs({tau_sym: tau([tau2_star(ego)])})
    except AttributeError:
        ret1 = func1

    try:
        ret2 = func2.subs({tau_sym: tau([tau2_star(ego)])})
    except AttributeError:
        ret2 = func2

    d2hdx2_eval = ret1
    dtaustardx_eval = dtau2stardx(ego)
    dtaudtaustar_eval = dtaudtaustar([tau2_star(ego)])
    dhdtau_eval = ret2
    d2hdtau2_eval = d2h2dtau2_predictive_oa(ego)
    d2taudtaustar2_eval = d2taudtaustar2([tau2_star(ego)])
    d2taustardx2_eval = d2tau2stardx2(ego)
    outer = np.outer(dtaustardx_eval, dtaustardx_eval)

    ret = (
        d2hdx2_eval
        + dtaudtaustar_eval * d2hdtau2_eval * dtaudtaustar_eval * outer
        + dhdtau_eval * d2taudtaustar2_eval * outer
        + dhdtau_eval * d2taustardx2_eval * dtaudtaustar_eval
    )

    return np.squeeze(np.array(ret).astype(np.float64))

def h_oa1(ego):
    return relaxation * h0_oa1(ego) + h_poa1(ego)

def h_oa2(ego):
    return relaxation * h0_oa2(ego) + h_poa2(ego)

def h_oa3(ego):
    return h_oa3_func(ego)

def h_oa4(ego, force):
    return h_oa4_func(ego)

def dhdx_oa1(ego):
    ret = relaxation * dh0dx_oa1(ego) + dhdx_poa1(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

def dhdx_oa2(ego):
    ret = relaxation * dh0dx_oa2(ego) + dhdx_poa2(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

def dhdx_oa3(ego):
    ret = dhdx_oa3_func(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

def dhdx_oa4(ego, force):
    ret = dhdx_oa4_func(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

# Necessary for stochastic systems
def d2hdx2_oa1(ego):
    ret = relaxation * d2h0dx2_oa1(ego) + d2hdx2_poa1(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

def d2hdx2_oa2(ego):
    ret = relaxation * d2h0dx2_oa2(ego) + d2hdx2_poa2(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

def d2hdx2_oa3(ego):
    ret = d2hdx2_oa3_func(ego)

    return np.squeeze(np.array(ret).astype(np.float64))

def d2hdx2_oa4(ego):
    ret = d2hdx2_oa4_func(ego)

    return np.squeeze(np.array(ret).astype(np.float64))


import tkinter as tk
from functools import partial
import socket

class BoardClass:
    """BoardClass provides method to use and create the interface and also the method that used to run Tic Tac Toe"""
    window = 0
    hostAddress = 0
    hostPort = 0
    userName = 0
    otherPlayer = 0
    lastPlayer = 0
    numTotal = 0
    numWins = 0
    numTies = 0
    numLoss = 0
    move = 0
    socket = 0
    buttonList = [[' ', ' ', ' '],
                  [' ', ' ', ' '],
                  [' ', ' ', ' ']]
    boardList = [[' ', ' ', ' '],
                 [' ', ' ', ' '],
                 [' ', ' ', ' ']]

    def __init__(self, name: str, move: str, socket: socket) -> None:
        """set up window and some basic buttons
            transform all variable to the type can be used in tk inter
            disable all board button for player2

            args:
                name: user's name that's going to be presented at the stats area's first line
                move: character presents your move
                socket: object to receive and send information with the other player"""
        self.setUpWindow()
        self.initTkVariable()
        self.createLabelAndEntry()
        self.createButton()
        self.runUI()
        self.move.set(move)
        self.userName.set(name)
        self.createStatsArea()
        self.socketSetUp(socket)
        if move == 'o' or move == 'O':
            for i in range(3):
                for j in range(3):
                    self.buttonList[i][j]['state'] = 'disabled'

    def socketSetUp(self, socket: socket) -> None:
        """create socket attribute for later information change with competitor"""
        self.socket = socket

    def runUI(self) -> None:
        """keep updating the interface"""
        self.window.update_idletasks()
        self.window.update()

    def initTkVariable(self) -> None:
        """Change all variable type then they can be used for tk interface"""
        self.hostAddress = tk.StringVar()
        self.hostPort = tk.IntVar()
        self.move = tk.StringVar()
        self.userName = tk.StringVar()
        self.numTotal = tk.IntVar()
        self.numTies = tk.IntVar()
        self.numWins = tk.IntVar()
        self.numLoss = tk.IntVar()
        self.otherPlayer = tk.StringVar()
        self.lastPlayer = tk.StringVar()

    def setUpWindow(self) -> None:
        """create interface"""
        self.window = tk.Tk()
        self.window.geometry('600x600')
        self.window.title('Tic Tac Toe')
        self.window.configure(background='LightSteelBlue')
        self.window.resizable(0, 0)

    def createLabelAndEntry(self) -> None:
        """create host address and port labels and entrys
            create last player's label and winner's label"""
        self.hostAddressEntry = tk.Entry(self.window, textvariable=self.hostAddress, width=10)
        self.hostAddressEntry.grid(row=0, column=4)
        self.AddressLabel = tk.Label(self.window, text='Please Enter The Host Address Here:')
        self.AddressLabel.grid(row=0, column=3)

        self.hostPortEntry = tk.Entry(self.window, textvariable=self.hostPort, width=10)
        self.hostPortEntry.grid(row=1, column=4)
        self.portLabel = tk.Label(self.window, text='Please Enter Your Port Number Here:')
        self.portLabel.grid(row=1, column=3)

        self.lastPlayer = tk.Label(self.window, text=self.lastPlayer.get())
        self.lastPlayer.grid(row=0, column=1)

        self.winner = tk.Label(self.window, text='winner:')
        self.winner.grid(row=4, column=1)

    def createButton(self) -> None:
        """create 9 board buttons, by clicking on the button, player can indicate occupation of the place"""
        self.quitButton = tk.Button(self.window, text='Quit', command=self.window.destroy)
        self.quitButton.grid(row=5, column=4)
        for row in range(3):
            for col in range(3):
                new_command = partial(self.changeBoardAndCheckWinner, row, col)
                self.buttonList[row][col] = tk.Button(self.window, text='', command=new_command, width=5, height=4)
                self.buttonList[row][col].grid(row=row + 1, column=col)

    def changeBoardAndCheckWinner(self, row, col) -> None:
        """Command for board button
            change the text of button to player's move character and disable all 9 buttons untill the player's next round arrive
            check if the updated board has a winner, """
        if self.move.get() == 'o' or self.move.get() == 'O':
            self.buttonList[row][col].config(text='O')
            self.boardList[row][col] = 'O'
        elif self.move.get() == 'x' or self.move.get() == 'X':
            self.buttonList[row][col].config(text='X')
            self.boardList[row][col] = 'X'

        for i in range(3):
            for j in range(3):
                self.buttonList[i][j]['state'] = 'disabled'

        self.lastPlayer.config(text=self.otherPlayer.get())
        self.socket.send(bytes('Move ' + str(row) + ' ' + str(col), 'ascii'))
        if self.isWinner() == True:
            self.numTotal.set(self.numTotal.get() + 1)
            self.numWins.set(self.numWins.get() + 1)
            self.winner.config(text='winner: ' + self.userName.get())
            if self.move.get() == 'x' or self.move.get() == 'X':
                self.createPlayAgainButton()
            self.createStatsArea()
            for i in range(3):
                for j in range(3):
                    self.buttonList[i][j]['state'] = 'disabled'
        elif self.boardIsFull() == True and self.isWinner() == False:
            self.numTotal.set(self.numTotal.get() + 1)
            self.numTies.set(self.numTies.get() + 1)
            self.winner.config(text='Tie')
            if self.move.get() == 'x' or self.move.get() == 'X':
                self.createPlayAgainButton()
            self.createStatsArea()
            for i in range(3):
                for j in range(3):
                    self.buttonList[i][j]['state'] = 'disabled'

    def createStatsArea(self) -> None:
        """Create a label displays user name, other player's n=ame, total number of games
            number of wins, loss and ties"""
        self.stats = tk.Label(self.window, text=f'Player: {self.userName.get()}\n\n'
                                                f'Player: {self.otherPlayer.get()}\n\n'
                                                f'Total number of games: {self.numTotal.get()}\n\n'
                                                f'Number of wins: {self.numWins.get()}\n\n'
                                                f'Number of ties: {self.numTies.get()}\n\n'
                                                f'Number of loss: {self.numLoss.get()}\n\n')
        self.stats.grid(row=4, column=3)

    def boardIsFull(self) -> bool:
        """Check whether the board is full."""
        emptyNum  = 9
        for rows in self.boardList:
            for col in rows:
                if col != ' ':
                    emptyNum -= 1
        if emptyNum == 0:
            return True
        else:
            return False

    def isWinner(self) -> bool:
        """Check if the last player win this game."""
        isWin = False
        for row in self.boardList:
            if row[0] == row[1] and row[0] == row[2] and row[0] != ' ':
                isWin = True

        for c in range(3):
            if self.boardList[0][c] == self.boardList[1][c] and self.boardList[0][c] == self.boardList[2][c] and self.boardList[0][c] != ' ':
                isWin = True

        if self.boardList[1][1] == self.boardList[0][0] and self.boardList[1][1] == self.boardList[2][2] and self.boardList[1][1] != ' ':
            isWin = True
        elif self.boardList[1][1] == self.boardList[0][2] and self.boardList[1][1] == self.boardList[2][0] and self.boardList[1][1] != ' ':
            isWin = True

        return isWin

    def createBindButton(self) -> None:
        """create bind button"""
        self.bind = tk.Button(self.window, text='Bind', command=self.bindSocket)
        self.bind.grid(row=2, column=3)

    def bindSocket(self) -> None:
        """command for bind button. By clicking a server socket will be bind. If error occurs during this process
            ifBindAgain function will be called.
            And before connect client, it will call ifAcceptConnectWindow to check if player2 wants to connect"""
        try:
            address = self.hostAddressEntry.get()
            port = int(self.hostPortEntry.get())
            self.socket.bind((address, port))
            self.bind.destroy()
            self.socket.listen(5)
            clientSocket, clientAddress = self.socket.accept()
            self.socket = clientSocket
            self.ifAcceptConnectWindow()
        except:
            self.ifBindAgain()

    def ifAcceptConnectWindow(self) -> None:
        """create a little window asking player2 if he/she wants to play
            yesButton will destroy the little window and back to normal playing process
            noButton will destry the little window and wait for player1 for his/her next move"""
        self.acceptWindow = tk.Toplevel(self.window)
        self.acceptWindow.title('if accept connection')
        self.info = tk.Label(self.acceptWindow, text='Do you want to accept the connection?')
        self.info.grid(row=0, column=1)
        self.yesButton = tk.Button(self.acceptWindow, text='Yes', command=self.acceptWindow.destroy)
        self.yesButton.grid(row=1, column=0)
        self.noButton = tk.Button(self.acceptWindow, text='No', command=lambda: [self.socket.send(bytes('reject', 'ascii')),
                                                                                self.socket.close(),
                                                                                self.acceptWindow.destroy()])
        self.noButton.grid(row=1, column=2)

    def ifBindAgain(self) -> None:
        """Create a little window displaying the error message and two buttons asking user to
            decide whether bind again."""
        self.failBindWindow = tk.Toplevel(self.window)
        self.failBindWindow.title('Bind Fail')
        self.errorInfo = tk.Label(self.failBindWindow, text='Unsuccessful Binding. Try Again?')
        self.errorInfo.grid(row=0, column=1)
        self.yesButton = tk.Button(self.failBindWindow, text='Yes', command=self.failBindWindow.destroy)
        self.yesButton.grid(row=1, column=0)
        self.noButton = tk.Button(self.failBindWindow, text='No', command=self.window.destroy)
        self.noButton.grid(row=1, column=2)

    def createConnectButton(self) -> None:
        """create connect button"""
        self.connect = tk.Button(self.window, text='Connect', command=self.connectSocketAndCreateNameEntry)
        self.connect.grid(row=2, column=3)

    def connectSocketAndCreateNameEntry(self) -> None:
        """command for connectButton. By clicking it, player1's socket will try to connect player2's.
            If failed, call the createFailConnectWindow function"""
        try:
            address = self.hostAddressEntry.get()
            port = int(self.hostPortEntry.get())
            self.socket.connect((address, port))
            self.connect.destroy()

            self.nameLabel = tk.Label(self.window, text='Enter Your Username Here:')
            self.nameLabel.grid(row=2, column=3)
            self.nameEntry = tk.Entry(self.window, textvariable=self.userName, width=10)
            self.nameEntry.grid(row=2, column=4)

            self.nameButton = tk.Button(self.window, text='Send', command=self.getAndSendName)
            self.nameButton.grid(row=3, column=3)
        except:
            self.createFailConnectWindow()

    def createNewConnectButton(self) -> None:
        """create a New ConnectButton"""
        self.connect = tk.Button(self.window, text='Connect', command=self.connectSocketAndCreateNameEntry)
        self.connect.grid(row=3, column=4)

    def createFailConnectWindow(self) -> None:
        """create a window displaying error message
             yesButton to close the little window
             noButton to close all windows for player1"""
        self.failConnectWindow = tk.Toplevel(self.window)
        self.failConnectWindow.title('Connect Fail')
        self.errorInfo = tk.Label(self.failConnectWindow, text='Unsuccessful Connection. Try Again?')
        self.errorInfo.grid(row=0, column=1)
        self.yesButton = tk.Button(self.failConnectWindow, text='Yes', command=self.failConnectWindow.destroy)
        self.yesButton.grid(row=1, column=0)
        self.noButton = tk.Button(self.failConnectWindow, text='No', command=self.window.destroy)
        self.noButton.grid(row=1, column=2)

    def getAndSendName(self) -> None:
        """check if the username is alphanumeric, if is, send it to player2
            if not, call createWarningwindow function"""
        try:
            for element in self.nameEntry.get():
                if ord(element) not in range(48, 58) and ord(element) not in range(65, 91) and ord(element) not in range(97,123):
                 raise ValueError
            self.nameButton.destroy()
            self.userName.set(self.nameEntry.get())
            self.lastPlayer.config(text=self.userName.get())
            self.createStatsArea()
            self.socket.send(bytes('Name' + ' ' + self.userName.get(), 'ascii'))
        except:
            self.createWarningWindow()

    def createWarningWindow(self) -> None:
        """create a little window displaying name error message
            okButton to close the little window"""
        self.warningWindow = tk.Toplevel(self.window)
        self.warningInfo = tk.Label(self.warningWindow, text='Please enter alphanumeric username.')
        self.warningInfo.grid(row=0, column=0)
        self.okButton = tk.Button(self.warningWindow, text="Ok", command=self.warningWindow.destroy)
        self.okButton.grid(row=1, column=0)

    def createPlayAgainButton(self) -> None:
        """create playAgainButton and stopPlayButton"""
        self.playAgainButton = tk.Button(self.window, text='Play Again', command=self.playAgain)
        self.playAgainButton.grid(row=3, column=3)

        self.stopPlayButton = tk.Button(self.window, text='Stop Play', command=self.stopPlay)
        self.stopPlayButton.grid(row=3, column=4)

    def playAgain(self) -> None:
        """command for playAgainButton. it will destroy both playAgainButton and StopPlay button
            and send play again message to player 2"""
        self.stopPlayButton.destroy()
        self.playAgainButton.destroy()
        self.resetBoard()
        self.socket.send(bytes('Play Again', 'ascii'))

    def stopPlay(self) -> None:
        """command for playAgainButton. it will destroy both playAgainButton and StopPlay button
                and send stop play message to player 2"""
        self.stopPlayButton.destroy()
        self.playAgainButton.destroy()
        self.socket.send(bytes('Fun Times', 'ascii'))
        self.createStatsWindow()

    def resetBoard(self) -> None:
        """Reset both button list which destroys 9 old button and create 9 news
            and board list which used to tell if a winner if exist"""
        self.buttonList = [[' ', ' ', ' '],
                           [' ', ' ', ' '],
                           [' ', ' ', ' ']]

        for row in range(3):
            for col in range(3):
                new_command = partial(self.changeBoardAndCheckWinner, row, col)
                self.buttonList[row][col] = tk.Button(self.window, text='', command=new_command, width=5, height=4)
                self.buttonList[row][col].grid(row=row + 1, column=col)

        if self.move.get() == 'o' or self.move.get() == 'O':
            for i in range(3):
                for j in range(3):
                    self.buttonList[i][j]['state'] = 'disabled'

        self.boardList = [[' ', ' ', ' '],
                          [' ', ' ', ' '],
                          [' ', ' ', ' ']]

        self.winner.config(text='winner:')

        if self.move.get() == 'X':
            self.lastPlayer.config(text=self.userName.get())
        elif self.move.get() == "O":
            self.lastPlayer.config(text=self.otherPlayer.get())

    def createStatsWindow(self) -> None:
        """This will create a window to show stats when the player decides not to play again.
            and the window has a button that can destroy the board window."""
        self.statsWindow = tk.Toplevel(self.window)
        self.statsLabel = tk.Label(self.statsWindow, text=f'Player: {self.userName.get()}\n\n'
                                                          f'Player: {self.otherPlayer.get()}\n\n'
                                                          f'Total number of games: {self.numTotal.get()}\n\n'
                                                          f'Number of wins: {self.numWins.get()}\n\n'
                                                          f'Number of ties: {self.numTies.get()}\n\n'
                                                          f'Number of loss: {self.numLoss.get()}\n\n')
        self.statsLabel.grid(row=1, column=1)

        self.quit = tk.Button(self.statsWindow, text='Quit', command=self.window.destroy)
        self.quit.grid(row=1, column=0)


'''DOCUMENTATION

Script:     This script contains the functions that will be used to prepare the dataset
            our dataset for the machine learning model
'''

## Import Packages
import pandas as pd
import sklearn
from sklearn import preprocessing
from sklearn.model_selection import train_test_split
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import BernoulliNB
from sklearn.naive_bayes import MultinomialNB
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt

## Import Project Modules
import Module_7_DataAnalysis as m7
import Module_0_utility_functions as m0

## Directory Object
output_dir = r'/home/ccirelli2/Desktop/Programming/SCA_Web_scaper/ML_Algorithm_Results'

'''We need to modify the first function below to allow the user to limit the data by min
and max year as we did in Module 7.  This will ensure that we choose a data set with
an equivalent number of dismissed and settled cases'''

### DATA RETREIVAL & TRANSFORMATION___________________________________________________________

def sql_query_machine_learning_data_set(min_year, max_year):
    '''Purpose:  Initial query of db to retrieve data for ML application'''
    Query = '''SELECT *
               FROM SCA_data
               WHERE case_status IS NOT NULL
               AND YEAR_FILED > {}
               AND YEAR_FILED < {}
               AND case_status != 'ongoing'
               AND Plaintiff_firm != 'Error'
               AND Judge != 'None'
               AND CHAR_LENGTH(Judge) > 2
               ;'''.format(min_year, max_year)
    return Query

def transform_target_binary(df):
    '''Purpose:  Convert the column with our target value to 1/0.
    1         Dismissed
    0         Settled
    '''
    # Create List to Capture Binary Values
    List_case_status_binary = []
    # Isolate the Case_status column
    Series_case_status = df['case_status']
    # Iterate the series and append values to our the list_case_status object
    for x in Series_case_status:
        if x == 'Dismissed':
            List_case_status_binary.append(1)
        elif x == 'Settled':
            List_case_status_binary.append(0)
        else:
            print('values other than dismissed and settled found in case_summary column')
    # Create a new column with binary representations
    df['Target_case_status_binary'] = List_case_status_binary
    # Drop old column
    df_final= df.drop(labels = 'case_status', axis = 1)

    return df_final

def transform_plaintiff_firm(df):
    '''Purpose is to shorten the name of the plaintiff firm in order to aviod issues differences
    that could arise with punctuation or text variations with a longer name, thereby avoiding
    a situation in which the same plaintiff firm comes up twice.
    '''
    List_plaintiff_firm_modified = []
    series_plaintiff_firm = df['Plaintiff_firm']
    for x in series_plaintiff_firm:
        List_plaintiff_firm_modified.append(x[:25])
    df['Plaintiff_firm_modified'] = List_plaintiff_firm_modified
    df_final = df.drop(labels = 'Plaintiff_firm', axis = 1)
    return df_final

def get_list_attributes_by_type(Type):

    if Type == 'categorical':
        return ['Sector', 'Industry', 'Headquarters', 'Company_market', 'Court',
       'Judge', 'Plaintiff_firm_modified']
    elif Type == 'numerical':
        return ['Breach_Fiduciary_Duties', 'Merger', 'Proxy_violation',
       'Related_parties', 'Stock_Drop', 'Cash_Flow', 'Revenue_Rec',
       'Net_Income', 'Customers', 'Fourth_Quarter', 'Third_Quarter',
       'Second_Quarter', 'Press_Release', '10K_Filling', '10Q_Filling',
       'Corporate_Governance', 'Conflicts_Interest', 'Accounting', 'Fees',
       'Failed_disclose', 'False_misleading', 'Commissions', 'Bankruptcy',
       'Secondary_Offering', 'IPO', '1934_Exchange_Act', 'Derivative', '10b5',
       '1933_Act', 'Heavy_trading', 'Sexual_Misconduct', 'class_action',
       'ERISA', 'FCPA', 'SEC_Investigation', 'Data_breach', 'Proxy',
       'Class_Duration']

### One Hot Encode Data------------------------------------------------------------------
def Encode_categorical_data(df, List_attributes_by_type):
    le = preprocessing.LabelEncoder()
    Attributes_categorical = df[List_attributes_by_type]
    Attributes_encoded = Attributes_categorical.apply(le.fit_transform)
    return Attributes_encoded

### DRIVER FUNCTION - PREPARE DATA SET FOR ML ALGORITHM----------------------------------
'''Includes all the above functions to prepare dataset'''

def prepare_dataset(conn, min_year, max_year):
    '''
    Purpose:    Prepare the dataset that we will use for our machine learning model
    Conn:       mysql connection
    Year:       Min year to be used to limit dataset to years > than this value
    Output:     Dataset prepared for ML algorithm
    '''

    # 1.) Import SCA_data
    '''Input:  Year_Filed to exclude'''
    df_SCA_data_table = m7.sql_query_executor(conn,
                        sql_query_machine_learning_data_set(min_year, max_year))

    # 2.) Drop Columns - Defendant_address, case_summary, page_number
    '''Based on our preliminary analysis, these two columns were not propertly scraped and contain
    too many null values to be included in our final dataset. Therefore, they will be dropped
    '''
    df_drop_columns = df_SCA_data_table.drop(labels = ['defendant_address', 'case_summary',
                                                        'page_number', 'Ref_court', 'Ref_docket',
                                                        'Ref_judge', 'Ref_date_filed',
                                                        'Ref_class_period_start',
                                                        'Ref_class_period_end',
                                                        'filling_date', 'defendant_name',
                                                        'close_date', 'Date_Filed', 'Docket',
                                                        'Class_Period_Start', 'Class_Period_End',
                                                        'Symbol', 'YEAR_FILED', 'Status_2'], axis = 1)

    # 3.) Convert case_status to binary, where 1 = 'dimissed', 0 = 'settled'
    df_transform_case_status = transform_target_binary(df_drop_columns)

    # 4.) Transform Plaintiff Firm - Limit to first 25 Characters
    df_transform_plaintiff_firm = transform_plaintiff_firm(df_transform_case_status)

    # Return Transformed Dataset
    return df_transform_plaintiff_firm

##########################           ALGORITHMS                 ###############################

### KNN____________________________________________________________________________________

# Test Number of Neighbors-----------------------------------------------------------------

def train_KNN_predictor(X, Y, random_state_value, min_year, max_year, write_2_excel = False,
                        plot = False, results = 'DataFrame'):
    '''Documentation:
    random_state:      seed used by the random generator.
    stratify:          separation of data into homogenious groups before sampling.
    range:             range over which to iterate to generate predictions
    lists:             capture predictions

    '''
    # Split dataset
    x_train, x_test, y_train, y_test = train_test_split(
                                        X, Y,
                                        stratify = Y,
                                        random_state = random_state_value,
                                        test_size = .15)

    # Ratio Dissmissed to Settled;
    Dismissal_percentage = round(sum(Y) / len(Y), 2)

    # Case Count
    Case_count = len(Y)

    # Lists to Capture Predictions
    accuracy_training_list = []
    accuracy_test_list = []
    dismissal_percentage_list = [Dismissal_percentage for x in range(1,10)]

    # Range of Nearest Neighbors
    num_range_neighbors = range(1,10)
    # Run Loop
    for num in num_range_neighbors:
        # Instantiate KNN Algorithm
        knn = KNeighborsClassifier(n_neighbors = num)
        # Fit algorithm to training data
        knn.fit(x_train, y_train)
        y_predict = knn.predict
        accuracy_training_list.append(knn.score(x_train, y_train))
        accuracy_test_list.append(knn.score(x_test, y_test))

    # Create DataFrame for scores
    df = pd.DataFrame({}, index = [2,3,4,5,6,7,8,9,10])
    df['Accuracy_Training'] = accuracy_training_list
    df['Accuracy_Test'] = accuracy_test_list
    df['Dismissal_Percentage'] = dismissal_percentage_list

    # Write Results To Excel
    if write_2_excel == True:
        m0.write_to_excel(df, 'KNN_output', output_dir)

    # Plotting
    if plot == True:
        plt.plot(num_range_neighbors, accuracy_training_list, label = 'Accuracy of training')
        plt.plot(num_range_neighbors, accuracy_test_list, label = 'Accuracy of test')
        plt.plot(num_range_neighbors, dismissal_percentage_list, label = 'Dismissal Percentage')
        plt.ylabel('Accuracy', fontsize = 20)
        plt.xlabel('Number of Neighbors' , fontsize = 20)
        plt.title('''Performance KNN Algorithm SCA Dataset
                 For Years: {} to {}
                 Case Count => {}
                 Ratio Dismissed to Total Cases => {}'''.format(min_year, max_year, Case_count,
                 Dismissal_percentage), fontsize = 25)
        plt.legend(fontsize = 15)
        plt.xticks(fontsize = 15)
        plt.yticks(fontsize = 15)
        plt.grid(b=None, which='major')
        plt.show()

    # Confusion Matrix
    if results == 'Confusion_matrix':
        clf_predict_y_test = knn.predict(y_test)
        clf_confusion_matrix = confusion_matrix(y_test, clf_predict_y_test)
        return clf_confusion_matrix

    # Results in Dataframe
    if results == 'DataFrame':
        return df
    #-------------------------------------------------------------------------------

## KNN - Single Neighbor - Generate Classification Report------------------------------------------

def train_KNN_single_neighbor_classifier(X, Y, NN, random_state_value, result):
    '''
    x_tain , y_train:  This represents the training data for our algorithm.  x_train is our
                       features and y_train the target.
    y_pred_class:      Once trained, we input the x_test data, which our model has not yet seen,
                       from which the model generates a prediction.  That prediction is saved to
                       y_pred_class object.  We then compare this prediction to the actual y_values
                       which are saved in y_test.  Its important to remember that x_test is the 'test'
                       data that our model has not yet seen. Hence the word 'test'.
    '''
    # Split Data Into Training & Test Sets:
    x_train, x_test, y_train, y_test = train_test_split(
                                        X, Y,
                                        stratify = Y,
                                        random_state = random_state_value,
                                        test_size = .15)

    # Instantiate KNN Algorithm
    knn = KNeighborsClassifier(n_neighbors = NN)

    # Fit algorithm to training data
    knn.fit(x_train, y_train)

    # Predict for Test Data
    '''Feed our model the x_test data (features) and make a prediction for our y-variable'''
    y_predict = knn.predict(x_test)

    # Generate Results
    if result == 'Classification_report':
        '''We now compare our y-prediction to the actual y saved in the y_test object'''
        knn_class_report_train = sklearn.metrics.classification_report(y_test, y_predict)
        return knn_class_report_train
    elif result == 'f1_score':
        knn_f1_score = sklearn.metrics.f1_score(y_test, y_predict)
        return knn_f1_score
    elif result == 'precision_score':
        knn_precision_score = sklearn.metrics.precision_score(y_test, y_predict)
        return knn_precision_score
    elif result == 'recall_score':
        knn_recall_score = sklearn.metrics.recall_score(y_test, y_predict)
        return knn_recall_score

    #-------------------------------------------------------------------------------

## LOGISTIC REGRESSION________________________________________________________________

def train_log_regressor_classifier(X, Y, random_state_value, result):
    x_train, x_test, y_train, y_test = train_test_split(X, Y,
                                                    stratify = Y,
                                                    random_state = random_state_value)
    # Standardize Data
    from sklearn.preprocessing import StandardScaler
    sc = StandardScaler()

    #x_train_sc = sc.fit_transform(x_train)
    #x_test_sc = sc.fit_transform(x_test)

    # Instantiate Model & Generate Prediction
    log_reg = LogisticRegression()
    log_reg.fit(x_train, y_train)
    y_predict = log_reg.predict(x_test)

    # Generate Results
    if result == 'Classification_report':
        log_reg_class_report = sklearn.metrics.classification_report(y_test, y_predict)
        return log_reg_class_report
    elif result == 'f1_score':
        log_reg_f1_score = sklearn.metrics.f1_score(y_test, y_predict)
        return log_reg_f1_score
    elif result == 'precision_score':
        log_reg_precision_score = sklearn.metrics.precision_score(y_test, y_predict)
        return log_reg_precision_score
    elif result == 'recall_score':
        log_reg_recall_score = sklearn.metrics.recall_score(y_test, y_predict)
        return log_reg_recall_score

    #-------------------------------------------------------------------------------

## NAIVE BAYES______________________________________________________________________

'''DOCUMENTATION:
Bernoulli Naive Bayes:  The binomial model is useful if your feature vectors
                        are binary (i.e., 0s and 1s). One application would be text
                        classification with a bag of words model where the 0s 1s are
                        "word occurs in the document" and "word does not occur
                        in the document"

Multinomial Naive:      Bayes The multinomial naive Bayes model is typically used for discrete counts.                         E.g., if we have a text classification problem, we can take the idea
                        of bernoulli trials one step further and instead of "word occurs in the
                        document" we have "count how often word occurs in the document",
                        you can think of it as "number of times outcome number x_i is observed
                        over the n trials"

Gaussian Naive Bayes:    Here, we assume that the features follow a normal distribution.
                         Instead of discrete counts, we have continuous features (e.g.,
                         the popular Iris dataset where the features are sepal width, petal
                         width, sepal length, petal length).
Source:                  http://users.sussex.ac.uk/~christ/crs/ml/lec02b.html
'''

def train_NaiveBayes_classifier(X,Y, random_state_value, NB_type, result):

    x_train, x_test, y_train, y_test = train_test_split(X, Y,
                                                        stratify = Y,
                                                        random_state = random_state_value)
    # Multinomial Model:-------------------------
    if NB_type == 'Multinomial':
        NB_multi = MultinomialNB(alpha = 1)
        NB_multi.fit(x_train, y_train)
        y_predict = NB_multi.predict(x_test)

        # Generate Results
        if result == 'Classification_report':
            NB_class_report = sklearn.metrics.classification_report(y_test, y_predict)
            return NB_class_report
        elif result == 'f1_score':
            NB_f1_score = sklearn.metrics.f1_score(y_test, y_predict)
            return NB_f1_score
        elif result == 'precision_score':
            NB_precision_score = sklearn.metrics.precision_score(y_test, y_predict)
            return NB_precision_score
        elif result == 'recall_score':
            NB_recall_score = sklearn.metrics.recall_score(y_test, y_predict)
            return NB_recall_score

    # Bernoulli Model----------------------------
    elif NB_type == 'Bernoulli':
        NB_bern = BernoulliNB()
        NB_bern.fit(x_train, y_train)
        y_predict = NB_bern.predict(x_test)

        # Generate Results
        if result == 'Classification_report':
            NB_class_report = sklearn.metrics.classification_report(y_test, y_predict)
            return NB_class_report
        elif result == 'f1_score':
            NB_f1_score = sklearn.metrics.f1_score(y_test, y_predict)
            return NB_f1_score
        elif result == 'precision_score':
            NB_precision_score = sklearn.metrics.precision_score(y_test, y_predict)
            return NB_precision_score
        elif result == 'recall_score':
            NB_recall_score = sklearn.metrics.recall_score(y_test, y_predict)
            return NB_recall_score

    #-------------------------------------------------------------------------------

# RANDOM FOREST_______________________________________________________________________________

def train_RandomForecast_classifier(X,Y, random_state_value, result):

    x_train, x_test, y_train, y_test = train_test_split(X, Y,
                                                        stratify = Y)

    # Generate Prediction
    clf_RF = RandomForestClassifier(n_estimators = 100)
    clf_RF.fit(x_train, y_train)
    y_predict = clf_RF.predict(x_test)

    # Generate Results
    if result == 'Classification_report':
        NB_class_report = sklearn.metrics.classification_report(y_test, y_predict)
        return NB_class_report
    elif result == 'f1_score':
        NB_f1_score = sklearn.metrics.f1_score(y_test, y_predict)
        return NB_f1_score
    elif result == 'precision_score':
        NB_precision_score = sklearn.metrics.precision_score(y_test, y_predict)
        return NB_precision_score
    elif result == 'recall_score':
        NB_recall_score = sklearn.metrics.recall_score(y_test, y_predict)
        return NB_recall_score
    elif result == 'feature_importance':
        Feature_important = clf_RF.feature_importances_
        df = pd.DataFrame({}, index = X.columns)
        df['Feature Importance'] = Feature_important
        m0.write_to_excel(df, 'Feature_Importance', output_dir)
        return clf_RF.score(x_test, y_test)
    #-------------------------------------------------------------------------------

# ELIMINATE ATTRIBUTES & RUN MODEL
'''Documentation

Purpose:        The purpose of this section is to generate lists in order to

'''

list_categorical_features = ['Sector', 'Industry', 'Company_market',
                             'Court', 'Judge', 'Plaintiff_firm_modified',
                             'Target_case_status_binary']

def limit_feature_selection(df, limit_selection):
    if limit_selection == 'Drop_derived_values':
        return df[['Sector', 'Industry', 'Company_market',
                             'Court', 'Judge', 'Plaintiff_firm_modified',
                             'Target_case_status_binary']]
    elif limit_selection == 'Use_all':
        return df

    elif limit_selection == 'Drop_merger_value':
        return df.drop(['Derivative', 'Merger', 'Proxy'], axis = 1)

# Graph Comparison - Average Performance All Features vs Dropping Derived Values

def graph_comparison_performance_features():
    year = [x for x in range(2000, 2017)]
    All_features = [.7, .689, .693, .689, .68, .693, .699, .704, .688, .726, .706,
                    .711, .705, .740, .822, .889, .954]
    Categorical_features_only = [.628, .595, .595, .612, .624, .617, .631, .655, .661,
                                 .680, .674, .696, .716, .757, .834, .876, .954]

    df = pd.DataFrame({}, index = year)
    df['Average_Performance_All_Features'] = All_features
    df['Categorical_features_only'] = Categorical_features_only

    plt.plot(year, df['Average_Performance_All_Features'], label = 'All_Features')
    plt.plot(year, df['Categorical_features_only'], label = 'Categorical_Features_Only')
    plt.ylabel('Average_Score', fontsize = 20)
    plt.xlabel('Range of Years Case Was Filed' , fontsize = 20)
    plt.title('Comparison: All Features vs Categorical Features Only', fontsize = 30)
    plt.legend(fontsize = 15)
    plt.xticks(fontsize = 15)
    plt.yticks(fontsize = 15)
    plt.grid(b=None, which='major')
    plt.show()
    return df

# Feature Importance - Un-groupped Categories
'''
Because of the one-hot-encoding certain features where broken into their subgroups.
Example the feature judge has a column for every single judge.  The below functions
rever these columns back to the single lvl features and then sum the feature importance
exported from our model.
'''

def generate_sum_importance_ungrouped_features(df, Feature):
    Feature_importance_list = []

    for x in df['Category']:
        if Feature in x:
            Feature_importance_list.append(1)
        else:
            Feature_importance_list.append(0)
    df[Feature + '_Importance'] = Feature_importance_list
    df_limit_feature = df[Feature +'_Importance'] == 1
    df_final = df[df_limit_feature]

    Feature_importance_sum = sum(df_final['Feature Importance'])

    return Feature_importance_sum

def record_feature_importance_ungrouped_categories(df, grouping_function, target_dir, target_file):

    # Load File
    os.chdir(target_dir)
    df = pd.read_excel(target_file)

    Dict_feature_importance = {}
    Ungrouped_feature_list = ['Judge', 'Court', 'Plaintiff_firm',
                              'Headquarters', 'Sector', 'Industry']

    for feature in Ungrouped_feature_list:
        feature_importance = round(grouping_function(df, feature), 4)
        Dict_feature_importance[feature] = feature_importance

    df = pd.DataFrame(Dict_feature_importance, index = ['Feature_importance']).transpose()

    return df