import logging
logger = logging.getLogger('server_logger')

class ListenPort:
    def __set__(self, instance, value):
        if value < 1024 or value > 65535:
            logger.critical('Попытка запуска сервера с неверным номером порта. '
                            'Номер порта должен быть из диапазона [1024;65535]')
            exit(1)
        instance.__dict__[self.name] = value

    def __set_name__(self, owner, name):
        self.name = name


import os
import queue
from multiprocessing import Process, cpu_count

def search(paths, query_q, result_q):
    lines = []
    for path in paths:
        lines.extend(l.strip() for l in open(path))
    query = query_q.get()
    while query:
        result_q.put_([l for l in lines if query in l])
        print(result_q.qsize())
        query = query_q.get()

if __name__ == '__main__':
    cpus = cpu_count()
    pathnames = [f for f in os.listdir('.') if os.path.isfile(f)]
    paths = [pathnames[i::cpus] for i in range(cpus)]

    query_queue = [queue.Queue(maxsize=10) for p in range(cpus)]
    result_queue = queue.Queue(maxsize=10)

    search_procs = [
        Process(target=search, args=(p, q, result_queue)) for p, q in zip(paths, query_queue)
    ]

    for proc in search_procs:
        proc.start()

    for q in query_queue:
        q.put('def')
        q.put(None)

    print(result_queue.qsize())

    print(result_queue.qsize())

    for r in range(cpus):
        for match in result_queue.get():
            print(match)

    for proc in search_procs:
        proc.join()


"""
This module takes care of starting the API Server, Loading the DB and Adding the endpoints
"""
from flask import Flask, request, jsonify, url_for, Blueprint
from api.models import db, User, Character, Favorite
from api.utils import generate_sitemap, APIException

api = Blueprint('api', __name__)
####################### GET ###############################

# Get all users
@api.route('/users', methods=['GET'])
def get_users():
    users = User.query.all()
    return jsonify([user.serialize() for user in users]), 200

# Get a user by ID
@api.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    user = User.query.get(user_id)
    if not user:
        return jsonify({'error': 'User not found'}), 404
    return jsonify(user.serialize()), 200

# Get all favorites for all users
@api.route('/favorites', methods=['GET'])
def get_all_favorites():
    favorites = Favorite.query.all()
    return jsonify([favorite.serialize() for favorite in favorites]), 200

# Get a user's favorite characters
@api.route('/users/<int:user_id>/favorites', methods=['GET'])
def get_favorites(user_id):
    user = User.query.get(user_id)
    if not user:
        return jsonify({'error': 'User not found'}), 404
    favorites = user.favorites
    return jsonify([favorite.character.serialize() for favorite in favorites]), 200

# Get all characters
@api.route('/characters', methods=['GET'])
def get_characters():
    characters = Character.query.all()
    return jsonify([character.serialize() for character in characters]), 200

# Get a character by ID
@api.route('/characters/<int:character_id>', methods=['GET'])
def get_character(character_id):
    character = Character.query.get(character_id)
    if not character:
        return jsonify({'error': 'Character not found'}), 404
    return jsonify(character.serialize()), 200

@api.route('/favorites/<int:user_id>', methods=['GET'])
def get_user_favorites(user_id):
    user = User.query.get(user_id)
    if user is None:
        return jsonify({'message': 'User not found'}), 404

    favorites = user.favorites
    serialized_favorites = [f.serialize_with_user() for f in favorites]

    return jsonify(serialized_favorites), 200

# Add a favorite character for a user
@api.route('/users/<int:user_id>/favorites', methods=['POST'])
def add_favorite_character(user_id):
    data = request.get_json()
    character_id = data.get('character_id')

    # Ensure character exists
    character = Character.query.get(character_id)
    if not character:
        return jsonify({'error': 'Character not found'}), 404

    # Ensure user exists
    user = User.query.get(user_id)
    if not user:
        return jsonify({'error': 'User not found'}), 404

    # Create new favorite record
    favorite = Favorite(user=user, character=character)
    db.session.add(favorite)
    db.session.commit()

    return jsonify({'success': 'Character added to favorites'}), 200


# Data-guided multi-granularity selector for attribute reduction
# Jiang Z, Dou H, Song J, et al. Data-guided multi-granularity selector for attribute reduction[J]. Applied Intelligence, 2021, 51: 876-888.

import math
import time
import numpy as np
from sklearn.preprocessing import MinMaxScaler
from scipy.spatial.distance import squareform, pdist

# 现在要做的事情是在给定一个半径下得到全属性集合下各个样本的邻域
# 参数情况: 固定的delta = 0.2, w = 0.1, s = 10
# 写一个函数 可以计算出数据集中每两个样本 在某一个属性子集上的距离
def generateDisMatrixUnderAttrSet(X: np.ndarray, attrSet: list[int]) -> np.array:
    '''
    :param X: 传入的原始数据集
    :param cols: 纳入计算距离的那些列
    :return: 对象之间的距离矩阵
    '''
    distMatrix = squareform(pdist(X[:, attrSet], metric='euclidean'))
    return distMatrix

def generateNeighbor(X:np.ndarray, attrSet:list[int], radius: float) -> dict:
    '''
    :param disMatrix: 距离矩阵
    :param radius:  用来生成邻域的半径
    :return:  字典 键:对象索引 值:该对象所生成邻域中的对象
    '''
    disMatrix = generateDisMatrixUnderAttrSet(X, attrSet) # 生成该属性集之下的各个样本之间的距离矩阵
    neighbors = dict()
    n, _ = disMatrix.shape
    for i in range(n):
        # neighbors[i] = set(np.where(disMatrix[i, :] < radius)[0])
        neighbors[i] = set(np.where(disMatrix[i, :] < radius)[0]) - {i}
    return neighbors

def calDiffBetTwoGran(neighbors1:dict, neighbors2:dict)->float:
    sampleNum = len(neighbors1)
    diffVal = 0
    for i in range(sampleNum):
        s1 = neighbors1[i]
        s2 = neighbors2[i]
        symDif = s1.symmetric_difference(s2)
        diffVal += len(symDif)/sampleNum
    return diffVal/sampleNum

def dataGuidedParameterSelector(X:np.ndarray, s:int=10, w:int=0.01):
    '''
    :param X: 数据集的条件属性部分
    :param s: 默认要选出10个半径
    :param w: 差异值设置为0.1
    :return: 返回选出来具有差异的一些半径
    '''
    selectedRadius = []
    prePartRadius = [0.2-0.02*i for i in range(1, 10)]
    postPartRadius = [0.2+0.02*i for i in range(1, 11)]
    sampleNum, attrNum = X.shape
    AT = list(range(attrNum))
    neighborsWithDelta = generateNeighbor(X, AT, 0.2)

    preDiffAndRadArr = []
    for rad in prePartRadius:
        neighborsCompare = generateNeighbor(X, AT, rad)
        dif = calDiffBetTwoGran(neighborsCompare, neighborsWithDelta)
        preDiffAndRadArr.append([dif, rad])
    preDiffAndRadArr.sort(key=lambda pair: pair[0], reverse=True)
    preSelectedRadius = [e[1] for e in preDiffAndRadArr[2:7]]

    postDiffAndRadArr = []
    for rad in postPartRadius:
        neighborsCompare = generateNeighbor(X, AT, rad)
        dif = calDiffBetTwoGran(neighborsCompare, neighborsWithDelta)
        postDiffAndRadArr.append([dif, rad])
    postDiffAndRadArr.sort(key=lambda pair: pair[0], reverse=True)
    postSelectedRadius = [e[1] for e in postDiffAndRadArr[2:7]]
    selectedRadius = preSelectedRadius+postSelectedRadius

    return selectedRadius

def generateDecisionClasses(Y:np.ndarray):
    '''
    :param Y: 数据集的决策属性部分
    :return: 各个决策类的集合
    '''
    decClasses = dict()
    decValues = np.unique(Y)
    for decValue in decValues:
        decClasses[decValue] = set(np.where(Y==decValue)[0])
    return decClasses

def getPos(decisionClasses: dict, neighbors: dict) -> list:
    '''
    :param decisionClasses: 决策类
    :param neighbors:  生成的邻域
    :return: D的B正域
    '''
    posBD = []
    for decisionClass in decisionClasses.values():
        for k in neighbors.keys():
            neighbor = neighbors[k]
            if neighbor.issubset(decisionClass):
                # print("对象{} 邻域{},被包含在决策类{}中\n".format(k, neighbor, decisionClass))
                posBD.append(k)
        # break
    # 去除重复项
    posBD = list(set(posBD))
    # print("D的B正域为{},其中元素个数为{}".format(posBD, len(posBD)))
    # print("条件属性集相对于决策属性集的重要度为:",len(posBD)/conditionData.shape[0])
    return posBD

def getAttrSetScoreDataGuidedSelector(X:np.ndarray, selectedRadius:list[float], decClasses:dict, attrSet:list[int]):
        score = 0
        for rad in selectedRadius:
            neighbors = generateNeighbor(X, attrSet, rad)
            tmpScore = len(getPos(decClasses, neighbors))
            score += tmpScore
        score = score / len(selectedRadius)
        return score

def reductionUseDataGuidedSelector(X:np.ndarray, Y:np.ndarray):
    sampleNum, attrNum = X.shape
    decClasses = generateDecisionClasses(Y)
    selectedRadius = dataGuidedParameterSelector(X)
    AT = set(range(attrNum))
    A = set() # 用于存储最后的约简结果
    preScore = -100

    fullAttrSetScore = getAttrSetScoreDataGuidedSelector(X, selectedRadius, decClasses, list(AT))
    # print("全属性集的得分为:{}".format(fullAttrSetScore))

    start_time = time.time()  # 程序开始时间
    while True:
        curScore = -100
        selectedAttr = 0
        for a in AT - A:
            tmpScore = getAttrSetScoreDataGuidedSelector(X, selectedRadius, decClasses, list(A | set([a])))
            if tmpScore > curScore:
                curScore = tmpScore
                selectedAttr = a
        A = A | set([selectedAttr])
        if curScore >= fullAttrSetScore:
            break

    # 删除属性的过程 如果从选出的属性集A中去除属性之后仍然满足约束就可以将一个属性从A中删除
    while True:
        nextA = A
        for a in A:
            tmpScore = getAttrSetScoreDataGuidedSelector(X, selectedRadius, decClasses, list(A - set([a])))
            if tmpScore >= fullAttrSetScore:
                nextA = nextA - set([a])
                break  # 每次只删除一个属性
        if len(A) == 1 or nextA == A:
            break
        A = nextA

    end_time = time.time()  # 程序结束时间
    run_time_sec = end_time - start_time  # 程序的运行时间，单位为秒

    return A, curScore/sampleNum, run_time_sec

if __name__ == "__main__":
    path = '../DataSet_TEST/ori/{}.csv'.format("movement")
    data = np.loadtxt(path, delimiter=",", skiprows=1)

    X = data[:, :-1]
    X = MinMaxScaler().fit_transform(X)  # 归一化取值均归为0-1之间
    Y = data[:, -1]

    red = reductionUseDataGuidedSelector(X, Y)
    print(red)

    # sampleNum, attrNum = X.shape
    # selectedRadius = [0.12+0.02*i for i in range(10)]
    # decClasses = generateDecisionClasses(Y)
    #
    # AT = set(range(attrNum))
    # score1 = getAttrSetScoreDataGuidedSelector(X, selectedRadius, decClasses, list(AT))
    # print(score1)
    #
    # score2 = getAttrSetScoreDataGuidedSelector(X, selectedRadius, decClasses, list(AT-{3}))
    # print(score2)


from configuration import listOfCountries
from configuration import listOfColors
from configuration import randomDataMax
from configuration import randomDataMin
import pandas as pd
import matplotlib.pyplot as plt
import random
import numpy as np
from matplotlib import cm
from matplotlib.colors import ListedColormap, LinearSegmentedColormap
from PIL import Image
#funkcja do generowania losowych wykresów kołowych

def isfloat(value):
    try:
        float(value)
        return True
    except ValueError:
        return False

def randomPieChart(title):
    #wybór indeksów
    selectedIndexes = []

    indexDataChoice = input("If you want to randomly select countries to be indexes - press 1 and enter, if you want manually enter indexes press 2 and enter: ")
    if str.isdigit(indexDataChoice):
        indexDataChoice = int(indexDataChoice)
        if indexDataChoice == 1:
            randomisedCountryNumberChoice = input("If you want to number of countries to be randomised enter 1, if you want the specify how many countries you want to have in the graph - enter 2: ")
            if str.isdigit(randomisedCountryNumberChoice):
                randomisedCountryNumberChoice = int(randomisedCountryNumberChoice)
                if randomisedCountryNumberChoice == 1:
                    print("przypisanie")
                    recordsNum = random.randint(1, len(listOfCountries))
                    selectedIndexes = listOfCountries
                    random.shuffle(selectedIndexes)
                    selectedIndexes = selectedIndexes[:recordsNum]
                elif randomisedCountryNumberChoice == 2:
                    isNumOfCountriesSet = False
                    while isNumOfCountriesSet == False:
                        recordsNumInput = input("Enter number of countries you want to chose: ")
                        if str.isdigit(recordsNumInput):
                            recordsNum = int(recordsNumInput)
                            selectedIndexes = listOfCountries
                            random.shuffle(selectedIndexes)
                            selectedIndexes = selectedIndexes[:recordsNum]
                            isNumOfCountriesSet = True
                        else:
                            print("Invalid input")
                else:
                    print("There is no such option you know")
            else:
                print("There is no such option you know")
        #opcja aby użytkownik wpisał se indexy ręcznie
        elif indexDataChoice == 2:
            indexDataInputChoice = ""
            print('When you are done entering data write "Done"')
            while indexDataInputChoice != "Done":
                indexDataInputChoice = input('Enter your custom index or enter "Done": ')
                if(indexDataInputChoice != "Done"):
                    selectedIndexes.append(indexDataInputChoice)
            recordsNum = len(selectedIndexes)
        else:
            print("There is no such option you know")
    else:
        print("There is no such option you know")

    print(title, "\n")

    data = []
    for y in range(0, recordsNum):
        print(selectedIndexes[y])
        newDataRecord = random.random() + 1
        data.append(newDataRecord)
    print(selectedIndexes)
    print(data)

    df = pd.DataFrame({title: data})
    plot = df.plot.pie(y=title, figsize=(11, 11), labels=selectedIndexes, fontsize=12, legend=False, autopct='%1.1f%%')
    fig = plot.get_figure()
    fig.savefig("myplot.png")

    im = Image.open(r"myplot.png")
    im.show()

#funkcja do generowania losowych wykresów słupkowych
def randomBarChart(title):
    selectedIndexes = []
    colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k']
    indexDataChoice = input("If you want to randomly select countries to be indexes - press 1 and enter, if you want manually enter indexes press 2 and enter: ")
    if str.isdigit(indexDataChoice):
        indexDataChoice = int(indexDataChoice)
        if indexDataChoice == 1:
            randomisedCountryNumberChoice = input("If you want to number of countries to be randomised enter 1, if you want the specify how many countries you want to have in the graph - enter 2: ")
            if str.isdigit(randomisedCountryNumberChoice):
                randomisedCountryNumberChoice = int(randomisedCountryNumberChoice)
                if randomisedCountryNumberChoice == 1:
                    print("przypisanie")
                    recordsNum = random.randint(1, len(listOfCountries))
                    selectedIndexes = listOfCountries
                    random.shuffle(selectedIndexes)
                    selectedIndexes = selectedIndexes[:recordsNum]
                elif randomisedCountryNumberChoice == 2:
                    isNumOfCountriesSet = False
                    while isNumOfCountriesSet == False:
                        recordsNumInput = input("Enter number of countries you want to chose: ")
                        if str.isdigit(recordsNumInput):
                            recordsNum = int(recordsNumInput)
                            selectedIndexes = listOfCountries
                            random.shuffle(selectedIndexes)
                            selectedIndexes = selectedIndexes[:recordsNum]
                            isNumOfCountriesSet = True
                        else:
                            print("Invalid input")
                else:
                    print("There is no such option you know")
            else:
                print("There is no such option you know")
        elif indexDataChoice == 2:
            indexDataInputChoice = ""
            print('When you are done entering data write "Done"')
            while indexDataInputChoice != "Done":
                indexDataInputChoice = input('Enter your custom index or enter "Done": ')
                if(indexDataInputChoice != "Done"):
                    selectedIndexes.append(indexDataInputChoice)
            recordsNum = len(selectedIndexes)
        else:
            print("There is no such option you know")
    else:
        print("There is no such option you know")

    print(title, "\n")
    print("Please now specife minimum and maximum value for data: ")

    inputMin = input("Specify the minimum value: ")
    while isfloat(inputMin) == False:
        inputMin = input("This is incorrect format of number. Specify the maximum value once again: ")
    minBarPlotValue = float(inputMin)

    inputMax = input("Specify the maximum value: ")
    while isfloat(inputMax) == False:
        inputMax = input("This is incorrect format of number. Specify the maximum value once again: ")
    maxBarPlotValue = float(inputMax)

    if(minBarPlotValue > maxBarPlotValue):
        temp = minBarPlotValue
        minBarPlotValue = maxBarPlotValue
        maxBarPlotValue = temp

    data = []
    for y in range(0, recordsNum):
        print(selectedIndexes[y])
        newDataRecord = random.uniform(minBarPlotValue, maxBarPlotValue)
        data.append(newDataRecord)
    print(selectedIndexes)
    print(data)

    #color selection
    colorToUse = random.choice(colors)

    df = pd.DataFrame({title: data}, index=selectedIndexes)
    fig = df.plot.bar(rot=0, color = colorToUse)
    fig.figure.savefig("myplot.png")

    im = Image.open(r"myplot.png")
    im.show()

def __main__():
    while True:
        print("""
        RandomChartGenerator
        Menu
        0 - Exit Program
        1 - Generate random pie chart
        #in production
        2 - Generate random bar chart (with one bar per index)
        #to be done
        3 - Generate random bar chart (with more than one bar per index)
        4 - generate random scatter plot
        2137 - Generate random COVID stats
        2138 - Generate random voting poll
        """)
        menuChoice = input("Select positon from menu: ")
        if str.isdigit(menuChoice):
            menuChoice = int(menuChoice)
            if menuChoice == 0:
                break
            elif (menuChoice >= 1 and menuChoice <= 2 or menuChoice == 2137):
                title = input("Add some title to your chart: ")
                if menuChoice == 1:
                    randomPieChart(title)
                elif menuChoice == 2:
                    randomBarChart(title)
                if menuChoice == 2137:
                    print("I am sorry, I didn't add this option yet, please be patient")
                    #plan na ten: bierzemy datę, generujemy liczbę, w dni tygodnia do soboty włącznie zwiększa się statystyka, niedzielę i poniedziałek - zmniejsza
                    #jeszcze można dodać generowanie na podstawie liczby wpisanej przez użytkownika jako dzisiejsza
            else:
                print("There is no such option you know")
        else:
                print("There is no such option you know")

__main__()


from Mordicus.Core.Containers import ProblemData as PD
from Mordicus.Core.Containers import CollectionProblemData as CPD
from Mordicus.Core.Containers import Solution as S
from Mordicus.Core.DataCompressors import SnapshotPOD as SP
from Mordicus.Modules.Scilab_ESI_Group import SciSolutionReader as SSR
from sklearn.gaussian_process.kernels import WhiteKernel, RBF
from sklearn.gaussian_process import GaussianProcessRegressor
from Mordicus.Core.OperatorCompressors import Regression
from Mordicus.Core.IO import StateIO as SIO
import numpy as np
from pathlib import Path
import os

def test():
    initFolder = os.getcwd()
    currentFolder = str(Path(__file__).parents[0])
    os.chdir(currentFolder)

    ####################################################
    # DEFINE READERS
    ####################################################
    solutionFileName = "data/my2Dpressure.csv"
    solutionReader = SSR.SciSolutionReader(solutionFileName)

    ####################################################
    # DEFINE COLLECTION PROBLEM DATA ARCHITECTURE
    ####################################################
    collectionProblemData = CPD.CollectionProblemData()
    # ADD? DOE = SCENARIO1 SCENARIO2 ... SCENARIO10
    collectionProblemData.AddVariabilityAxis('mu1',float,description="my_parameter")
    collectionProblemData.DefineQuantity("P", full_name="pressure", unit="Pa")

    nbOfComp = 1
    nbOfNodes = 10906
    #nbOfNodes = mesh.GetNumberOfNodes()

    nbRuns = 21
    parameter = range(-10, 11)
    for i in range(nbRuns):
        ####################################################
        # DEFINE PROBLEM ARCHITECTURE
        ####################################################
        Scenario = "SCENARIO" + str(i)
        problemData = PD.ProblemData(Scenario)

        ####################################################
        # DEFINE SOLUTION ARCHITECTURE
        ####################################################
        solutionP = S.Solution("P", nbOfComp, nbOfNodes, primality=True)

        ####################################################
        # FILL SOLUTION & PROBLEM OBJECT & COLLECTION PB OBJECT
        ####################################################
        snapshot = solutionReader.ReadSnapshotComponent("P", i, primality=True)
        solutionP.AddSnapshot(snapshot, 0)
        # Fill DOE
        problemData.AddParameter(np.array([parameter[i]]), 0)
        problemData.AddSolution(solutionP)
        collectionProblemData.AddProblemData(problemData,mu1=float(parameter[i]))

    ####################################################
    # FILL COLLECTION PROBLEM WITH OFFLINE
    ####################################################

    ## DEFINE INNER PRODUCT (default is EUCLIDEAN)
    # l2ScalarProducMatrix = FT.ComputeL2ScalarProducMatrix(mesh, 3)
    # collectionProblemData.SetSnapshotCorrelationOperator("P", l2ScalarProducMatrix)

    ## NORMALIZED SVD ON CORRELATION MATRIX (SNAPSHOT METHOD)
    reducedOrderBasisP = SP.ComputeReducedOrderBasisFromCollectionProblemData(collectionProblemData, "P", 1.0e-4)
    collectionProblemData.AddReducedOrderBasis("P", reducedOrderBasisP)

    ## COMPUTE PROJECTED COEFFICIENTS
    collectionProblemData.CompressSolutions("P")

    ## RECONSTRUCT SOLUTION FROM COEFFICIENTS
    #CompressedSolutionP = solutionP.GetCompressedSnapshots()
    #print(CompressedSolutionP)

    ## REGRESSION = COMPRESS OPERATOR
    kernel = 1.0 * RBF(length_scale=100.0, length_scale_bounds=(1e-2, 1e3)) + WhiteKernel(noise_level=1, noise_level_bounds=(1e-10, 1e1))
    gpr = GaussianProcessRegressor(kernel=kernel, alpha=0.0)
    # Training
    Regression.CompressOperator(collectionProblemData, "P", gpr)
    # NOTA: Le compressOperator est associe au collectionProblemData. Pas evident!
    # D'autant qu'on réutilise Regression pour calculer les snapshots compresses. Faire de regression un container ?

    ####################################################
    # SAVE DATA
    ####################################################
    SIO.SaveState("collectionProblemData", collectionProblemData)

    ####################################################
    # TEST
    ####################################################
    os.chdir(initFolder)

    return "ok"

if __name__ == "__main__":
    print(test())


# здесь контроллеры/хендлеры/представления для обработки запросов (flask ручки). сюда импортируются сервисы из пакета service
from flask import request
# Пример
from flask_restx import Resource, Namespace

from models import Movie, movies_schema, movie_schema
from setup_db import db

movie_ns = Namespace('movies')

@movie_ns.route('/')
class BooksView(Resource):
    def get(self):
        all_movies = db.session.query(Movie)
        director_id = request.args.get("director_id")
        genre_id = request.args.get("genre_id")
        if director_id is not None:
            all_movies = all_movies.filter(Movie.director_id == director_id)
        if genre_id is not None:
            all_movies = all_movies.filter(Movie.genre_id == genre_id)
        return movies_schema.dump(all_movies), 200

    def post(self):
        req_json = request.json
        new_movie = Movie(**req_json)
        with db.session.begin():
            db.session.add(new_movie)
        return "Movie created", 201

# ----------MovieViews--------------
@movie_ns.route('/<int:mid>')
class MovieView(Resource):
    def get(self, mid):
        movie = Movie.query.get(mid)
        if not movie:
            return "Movie not found", 404
        return movie_schema.dump(movie), 200

    def put(self, mid):
        updated_rows = db.session.query(Movie).filter(Movie.id == mid).update(request.json)
        if updated_rows != 1:
            return "Not updated", 400
        db.session.commit()
        return "", 204

    def delete(self, mid):
        movie = Movie.query.get(mid)
        if not movie:
            return "Movie not found", 404
        db.session.delete(movie)
        db.session.commit()
        return "", 204


import tensorflow as tf
from tfac.queue_input import QueueInput
from tensorflow.examples.tutorials.mnist import input_data
from functools import partial
import time
from tfac.ops_runner import OpRunner

class ModeKeys(object):
    """Standard names for model modes.

    The following standard keys are defined:

    * `TRAIN`: training mode.
    * `EVAL`: evaluation mode.
    * `PREDICT`: inference mode.
    """

    TRAIN = 'train'
    EVAL = 'eval'
    PREDICT = 'infer'

def build_net(features, labels, mode, reuse, prefix):
    # Remember to set reuse to tf.AUTO_REUSE
    dense_1 = tf.layers.dense(features["x"], 128, name=prefix + "dense_1",
                              reuse=reuse, activation=tf.nn.relu)
    dense_2 = tf.layers.dense(dense_1, 256, name=prefix + "dense_2",
                              reuse=reuse, activation=tf.nn.relu)
    dense_3 = tf.layers.dense(dense_2, 256, name=prefix + "dense_3",
                              reuse=reuse, activation=tf.nn.relu)
    logits = tf.layers.dense(dense_3, 10, name=prefix + "logit",
                             reuse=reuse)

    predictions = {
        "classes": tf.argmax(input=logits, axis=1),
        "probabilities": tf.nn.softmax(logits, name="softmax_tensor")
    }

    if mode == ModeKeys.PREDICT:
        net = {"predictions": predictions}
        return net

    loss = tf.losses.sparse_softmax_cross_entropy(labels=labels["y"],
                                                  logits=logits)
    optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.001)
    train_op = optimizer.minimize(
        loss=loss,
        global_step=tf.train.get_or_create_global_step())

    net = {
        "predictions": predictions,
        "loss": loss,
        "train_op": train_op
    }
    return net

def measure_time(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        func(*args, **kwargs)
        stop = time.time()
        print("runtime: " + str(stop - start) + " sec")

    return wrapper

@measure_time
def original_tf(mnist):
    graph = tf.Graph()
    with graph.as_default():
        # define features and labels
        features = {
            "x": tf.placeholder(tf.float32, [None, 784])
        }

        labels = {
            "y": tf.placeholder(tf.int64, [None])
        }

        # build net
        train_net = build_net(features, labels, ModeKeys.TRAIN, False, "org_")

        # initialize tensorflow session
        sess = tf.InteractiveSession()
        init_op = tf.global_variables_initializer()
        graph.finalize()
        sess.run(init_op)

    # training
    for _ in range(50000):
        batch_xs, batch_ys = mnist.train.next_batch(32)
        sess.run(train_net["train_op"], feed_dict={
            features["x"]: batch_xs,
            labels["y"]: batch_ys})

    # predict
    batch_xs, batch_ys = mnist.test.next_batch(50)
    pred = sess.run(train_net["predictions"], feed_dict={
        features["x"]: batch_xs,
        labels["y"]: batch_ys})

    sess.close()

@measure_time
def with_tfac(mnist):
    graph = tf.Graph()
    with graph.as_default():
        # define features and labels
        features = {
            "x": tf.placeholder(tf.float32, [None, 784])
        }

        labels = {
            "y": tf.placeholder(tf.int64, [None])
        }

        # use QueueInput to build input op
        qi = QueueInput(features, labels, [400, 100])
        idx, batch_features, batch_labels = qi.build_op(32)
        idx, pred_features, pred_labels = qi.build_op(50)

        # build net on two different input with same weights
        train_net = build_net(batch_features, batch_labels,
                              ModeKeys.TRAIN, False, "tfac_")
        pred_net = build_net(pred_features, pred_labels,
                             ModeKeys.PREDICT, tf.AUTO_REUSE, "tfac_")

        train_runner = OpRunner(train_net["train_op"])

        # build sample function
        sample_fn = []
        sample_fn.append(partial(mnist.train.next_batch, 100))
        sample_fn.append(partial(mnist.test.next_batch, 50))

        # initialize tensorflow session
        sess = tf.InteractiveSession()
        init_op = tf.global_variables_initializer()
        # run QueueInput before graph finalize
        qi.run(sess, sample_fn)
        graph.finalize()
        sess.run(init_op)

    # run train runner
    train_runner.run(sess)

    # training
    for _ in range(50000):
        train_runner.execute()

    train_runner.close()

    # predict
    pred = sess.run(pred_net["predictions"])

    # close QueueInput
    qi.close()

    sess.close()

def main():
    sess = tf.InteractiveSession()
    sess.close()
    print("running original")
    mnist = input_data.read_data_sets('/tmp/tensorflow/mnist/input_data',
                                      source_url='http://yann.lecun.com/exdb/mnist/')
    original_tf(mnist)
    print("\n")
    time.sleep(10)
    print("running tfac")
    mnist = input_data.read_data_sets('/tmp/tensorflow/mnist/input_data',
                                      source_url='http://yann.lecun.com/exdb/mnist/')
    with_tfac(mnist)

if __name__ == '__main__':
    main()


import platform

from pathlib import Path
import shutil

from pbpy import pblog, pbtools, pbunreal

def publish_build(branch_type, butler_exec_path, publish_stagedir, butler_project, butler_manifest):
    # Test if our configuration values exist
    if not butler_project or not butler_manifest:
        pblog.error("butler was not configured.")
        return False

    if branch_type == "default":
      branch_type = "internal"

    plat = platform.system()
    if plat == "Windows":
      plat = "win"
    elif plat == "Darwin":
      plat = "mac"
    elif plat == "Linux":
      plat = "linux"
    else:
      plat = plat.lower()

    manifest_path = Path(publish_stagedir) / ".itch.toml"

    shutil.copyfile(butler_manifest.format(plat), manifest_path)

    channel = f"{branch_type}-{plat}"

    # Push and Publish the build
    proc = pbtools.run([butler_exec_path, "push", publish_stagedir, f"{butler_project}:{channel}", "--userversion", pbunreal.get_project_version()])
    result = proc.returncode
    manifest_path.unlink()
    return result


from .task import LatexTask, TaskResult
from .gtk import GObject
from .types import TemplateName
from .notify import Notify
from ..abstraction import Letter, Design
from ..latex.workspace import Workspace
from ..latex.preamblecache import PreambleCache
from typing import Optional, Tuple
from warnings import warn
from threading import Thread, Timer
from queue import Queue
from datetime import datetime

class TaskLoop(Thread):
    """
    A loop that receives jobs and starts latex tasks one at a time.
    """
    def __init__(self, queue: Queue, manager: "TaskManager",
                 workspace: Workspace, preamble_cache: PreambleCache):
        # Set it as a daemon thread:
        super().__init__(daemon=True)
        self.notify = Notify()
        self.queue = queue
        self.manager = manager
        self.workspace = workspace
        self.preamble_cache = preamble_cache

    def run(self):
        while True:
            # Get a new job, the most recent entry in the job queue:
            letter, design, template = self.queue.get()
            while not self.queue.empty():
                self.queue.task_done()
                letter, design, template = self.queue.get()

            # Start a new task and wait for it to finish:
            job = LatexTask(letter, design, template, self.workspace,
                            self.preamble_cache)
            job.notify.connect("notify_result", self.manager.receive_result)
            t0 = datetime.now()
            job.start()
            job.join()
            t1 = datetime.now()

            # Finish that task.
            self.queue.task_done()
            self.notify.emit("notify_compile_time", (t1-t0).total_seconds())

class TaskManager(GObject.GObject):
    """
    Manage the LaTeX task(s).
    """
    task: Optional[LatexTask]

    __gsignals__ = {
        "notify_result" : (GObject.SIGNAL_RUN_FIRST, None, (object,)),
        "notify_compile_time" : (GObject.SIGNAL_RUN_FIRST, None, (object,))
    }

    def __init__(self, workspace: Workspace, preamble_cache: PreambleCache):
        super().__init__()
        self.task = None
        self.workspace = workspace
        self.preamble_cache = preamble_cache
        self.queue = Queue()
        self.task_loop = TaskLoop(self.queue, self, workspace, preamble_cache)
        self.task_loop.start()
        self.timer = None

    def submit(self, delay: float, letter: Letter, design: Design,
               template: TemplateName):
        """
        Submit a job for execution.
        """
        # A later-executed job submission:
        def submission():
            self.start((letter, design, template))
        if self.timer is not None:
            self.timer.cancel()
        self.timer = Timer(delay, submission)
        self.timer.start()

    def start(self, job: Tuple[Letter, Design, TemplateName]):
        """
        Start a job.
        """
        self.queue.put(job)

    def receive_result(self, notification: Notify, result: TaskResult):
        """
        Receive the results of a task.
        """
        self.task = None
        self.emit("notify_result", result)

    def receive_compile_time(self, compile_time: float):
        """
        Receive the runtime of the LaTeX compilation in seconds.
        """
        self.emit("notify_compile_time", compile_time)


import graphene
from graphene_django import DjangoObjectType
from django.db.models import Q
from graphene import relay, ObjectType
from graphene_django.filter import DjangoFilterConnectionField

from notes.models import Note, UserPreferences, UserInfo, User

class NoteType(DjangoObjectType):
    class Meta:
        model = Note
        fields = ('id', 'note_text', 'note_name', 'note_created')
        filter_fields = ['id', 'note_name']
        interfaces = (relay.Node, )

class UserPreferencesType(DjangoObjectType):
    class Meta:
        model = UserPreferences
        fields = ('id', 'note_color_preference', 'note_name_preference', 'user_info')
        filter_fields = ['id', 'note_color_preference', 'note_name_preference']
        interfaces = (relay.Node, )

class UserInfoType(DjangoObjectType):
    class Meta:
        model = UserInfo
        fields = ('id', 'user_preferences', 'user_mail', 'user_chosen_name', 'user')
        filter_fields = ['id', 'user_mail', 'user_chosen_name']
        interfaces = (relay.Node, )

class UserType(DjangoObjectType):
    class Meta:
        model = User
        fields = ('id', 'user_info')
        filter_fields = ['id']
        interfaces = (relay.Node, )

class Query(graphene.ObjectType):
    note = relay.Node.Field(NoteType)
    all_notes = DjangoFilterConnectionField(NoteType)

    user_preferences = relay.Node.Field(UserPreferencesType)
    all_user_preferences = DjangoFilterConnectionField(UserPreferencesType)

    user_info = relay.Node.Field(UserInfoType)
    all_user_info = DjangoFilterConnectionField(UserInfoType)

    user = relay.Node.Field(UserType)
    all_users = DjangoFilterConnectionField(UserType)

schema = graphene.Schema(query=Query)


import sklearn.datasets as datasets
import matplotlib.pyplot as plt
import numpy as np

from timeit import default_timer as timer
from datetime import timedelta
from sklearn import preprocessing
from force.force_scheme import ForceScheme

def main():
    raw = datasets.load_iris(as_frame=True)
    X = raw.data.to_numpy()
    X = preprocessing.StandardScaler().fit_transform(X)

    start = timer()
    y = ForceScheme().fit_transform(X)
    end = timer()

    print(np.amin(y, axis=0))

    print('ForceScheme took {0} to execute'.format(timedelta(seconds=end - start)))

    plt.figure()
    plt.scatter(y[:, 0], y[:, 1], c=raw.target,
                cmap='Set1', edgecolors='face', linewidths=0.5, s=4)
    plt.grid(linestyle='dotted')
    plt.show()

    return

if __name__ == "__main__":
    main()
    exit(0)


from flask import Flask, render_template

app = Flask(__name__)

COMMUNITIES =[
 {
    'id': 1,
    'No. of Members': '40',
    'title': 'Virtual Code Club',
    'location': 'Bhopal'
 },
  {
     'id': 2,
    'No. of Members': '50',
    'title': 'Anime Club',
    'location': 'Delhi'
  },
 {
    'id': 3,
    'No. of Members': '50',
    'title': 'Music Club',
    'location': 'Mumbai'
  }
]

@app.route("/")
def hello_communities():
  return render_template('home.html', communities=COMMUNITIES)

@app.route("/api/communities")
def list_communities():
  return jsonify(COMMUNITIES)

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


"""Blender script to composite shots built with render_manager

To test:

"C:\Program Files\Blender Foundation\Blender 3.0\blender" -b "D:\Assets\Models\Mine\compositor recipes\defaullt_compositor_chain.blend" --python compositor_script.py -- "D:\Projects\Sitting Duck\Blender\blender_shot_list.json" film 1 1 .png

"""
import bpy
import sys
import copy
import os
import functools
import logging
import time

logging.getLogger().setLevel(logging.INFO)

# Find the directory where this file (render_manager.py) resides
# NOTE: This will break if os.chdir() is called before this line runs
script_py_path = os.path.dirname(os.path.realpath(__file__))

# Append this directory to path, so that we can find "shot_list_db.py"
sys.path.append(script_py_path)

import shot_list_db
from common import *

# DEFAULT_COMPOSITOR_CHAIN_BLEND_FILE = "D:\\Assets\\Models\\Mine\\compositor recipes\\default_compositor_chain.blend"

def configure_compositor_chain(compositor_chain_db):
    """Configure the compositor chain to match the requirements for current shot

    compositor_chain_db:  Sub-db of the shot list JSON file

    Example:

        "compositor_chain": {
            "Denoise": {
                "mute": true,
                "prefilter": "ACCURATE",
                "use_hdr": true
            },
            "Vector Blur": {
                "mute": true,
                "samples": 32,
                "factor": 1.0,
                "speed_min": 0,
                "speed_max": 0,
                "use_curved": false
            },
            "Glare": {
                "mute": false,
                "glare_type": "FOG_GLOW",
                "quality": "MEDIUM",
                "mix": 0.0,
                "threshold": 1.0,
                "size": 8
            },
            "Lens Distortion": {
                "mute": false,
                "use_projector": false,
                "use_jitter": false,
                "use_fit": true,
                "distortion": 0.005,
                "dispersion": 0.01
            },
            "Filter": {
                "mute": true,
        "filter_type": "SOFTEN",
                "factor": 0.0
            },
            "Blur": {
                "mute": true,
                "filter_type": "GAUSS",
                "use_variable_size": false,
                "use_bokeh": false,
                "use_gamma_correction": false,
                "use_relative": false,
                "size_x": 1,
                "size_y": 1,
                "use_extended_bounds": false,
                "size": 2
            }
        }

    """

    ## We mostly just use the Python attribute names in the JSON file.
    ## However, in some cases, this is not possible and so we need
    ## custom setter functions.

    def set_node_input_default_value(index, node, prop, value):
        """Set the default value of the node's index'th input"""
        node.inputs[index].default_value = value

    attribute_setters = {
        # XXX Maybe we should, in fact, search through the inputs for one with the given name?
        # Distort node has distortion and dispersion in its inputs (not properties)
        ("Lens Distortion", "distortion"): functools.partial(set_node_input_default_value, 1),
        ("Lens Distortion", "dispersion"): functools.partial(set_node_input_default_value, 2),
        ("Filter", "factor"): functools.partial(set_node_input_default_value, 0),
        ("Blur", "size"): functools.partial(set_node_input_default_value, 1),
    }

    # Loop through the nodes that we expect in the chain
    for node_name, node_properties in compositor_chain_db.items():
        for node_property, property_value in node_properties.items():
            try:
                attribute_setter = attribute_setters.get((node_name, node_property), setattr)
                attribute_setter(bpy.data.scenes["Scene"].node_tree.nodes[node_name], node_property, property_value)
            except KeyError:
                logging.exception("Compositor config specified node \"%s\" which is missing from blend file." % node_name)
            except AttributeError:
                logging.exception("Compositor config specified non-existant property \"%s\" for node \"%s\"." % (node_property, node_name))

def setup_compositor_source_image(src_filepath):
    """Setup the compositor source image node to load the rendered image"""

    # First we have to load the source imageinto Blender's data object tree
    #result = bpy.ops.image.open(
    #                        directory=os.path.dirname(src_filepath),
    #                        files=[ {"name": os.path.basename(src_filepath)} ],
    #                        show_multiview=False
    #                    )

    #if result != {'FINISHED'}:
    #    raise FileNotFoundError("Failed to load source image sequence for composition")

    #try:
    #    image = bpy.data.images[os.path.basename(src_filepath)]
    #except KeyError as e:
    #    raise ValueError("Source image loaded, but not available.")

    image = bpy.data.images.load(src_filepath, check_existing = True)
    scene = bpy.data.scenes[0]

    # We expect the compositor node setup to have an Image Sequenece node called "Source Image"
    try:
        source_image_node = scene.node_tree.nodes["Source Image"]
    except KeyError as e:
        raise ValueError("Compositor missing 'Source Image' node.")

    source_image_node.image = image
    image.colorspace_settings.name = 'Raw'
    image.source = 'FILE'
    scene.node_tree.nodes["Source Image"].layer = 'View Layer'
    print(src_filepath)

## This is a Blender python script. It should
## 1. Get from the command line
##   a. the shot-list JSON file
##   b. ID of the shot to render + slate number and quality
##      (from this, we know where the render is save, it's resolution etc.)
## 2. Set up the compositor nodes as specified in the JSON
## 3. Do any configuration of standard Blender settings, render engine etc. needed for compositing.
## 4. Monitor the incoming directory for frames that have not yet been composited
## 5. Setup the Source Image node to point to the incoming frame
## 6. Setup the output resolution to match the incoming frame
## 7. Render to write the composite frame
## 8. Loop back to 4

# Parse command line
#
argv = sys.argv
argv = argv[argv.index("--") + 1:]  # get all args after "--"

if len(argv) == 5:
    [shot_list_db_filepath, shot_category, shot_id, quality, slate_number] = argv
else:
    raise ValueError("Not enough command line parameters supplied to compositor_script.py")

##
## Load the shot list, which contains the compositor chain configuration
##
shot_list_db = shot_list_db.ShotListDb.from_file(shot_list_db_filepath)

# Look up the shot using category + ID.
shot_info = shot_list_db.get_shot_info(shot_category, shot_id)

# Get the scene name from the shot info, but default to the first scene.
scene_name = shot_info.get("scene", bpy.data.scenes[0].name)
scene = bpy.data.scenes[scene_name]

##
## Figure out the incoming filestub from the shot list DB settings
##

# Use output path override given in the shot list, if given. Otherwise, fallback on eg. Renders/Title/slate_3/...
# XXX Duplicate code
if shot_info.get("output_filepath_override"):
    incoming_filestub = shot_info.get("output_filepath_override")
else:
    output_path_base =  os.path.join(shot_list_db.render_root, shot_info["title"])

    if slate_number is None:
        slate_number = find_next_slate_number(output_path_base)

    filename =  shot_category + "_" + shot_id + "_" + slate_number + "_"
    incoming_filestub = os.path.join(output_path_base, "slate_%s/" % str(slate_number)) + filename

incoming_file_format = shot_list_db.get_shot_info(shot_category, shot_id).get("render_file_format", "PNG")
incoming_file_extension = IMAGE_FILE_EXTENSIONS[incoming_file_format]
outgoing_file_format = shot_list_db.get_shot_info(shot_category, shot_id).get("composite_file_format", "PNG")
outgoing_file_extension = IMAGE_FILE_EXTENSIONS[outgoing_file_format]

filename =  shot_category + "_" + shot_id + "_" + slate_number + "_comp_"
outgoing_filestub = os.path.join(output_path_base, "slate_%s_composite/" % str(slate_number)) + filename

##
## Setup the nodes in the compositor chain.
##
compositor_chain_db = shot_info.get("compositor_chain", None)
if compositor_chain_db:
    logging.info("Setting up compositor chain...")
    print(compositor_chain_db)
    configure_compositor_chain(compositor_chain_db)

##
## Set render settings
##
scene.render.engine = 'CYCLES'
scene.cycles.device = 'CPU'
set_render_resolution(scene, shot_info, quality)
scene.render.image_settings.file_format = shot_info.get("composite_file_format", "PNG")
scene.render.image_settings.color_mode = shot_info.get("composite_color_mode", 'RGBA')
scene.render.image_settings.color_depth = shot_info.get("composite_color_depth", "16")

##
## Some helper functions
##
frame_filepath = lambda filestub, file_extension, frame_number: filestub + ("%04d" % frame_number) + "." + file_extension
incoming_frame_filepath = functools.partial(frame_filepath, incoming_filestub, incoming_file_extension)
outgoing_frame_filepath = functools.partial(frame_filepath, outgoing_filestub, outgoing_file_extension)

##
## Define a function to composite a single frame.
##
def composite_frame(frame_number):
    """Composite a single frame"""
    setup_compositor_source_image(incoming_frame_filepath(frame_number))

    ## Set output
    scene.render.filepath = outgoing_filestub

    scene.frame_start = frame_number
    scene.frame_end = frame_number
    bpy.ops.render.render(animation=True)

##
## Monitor incoming
##

# render_script.py defaults frame_start and frame_end to whatever is in the blend file. However,
# we don't want to have to read the blend file here, so you need to specify them if you want to
# composite
#
try:
    frame_start = shot_info['frame_start']
    frame_end = shot_info['frame_end']
except KeyError as e:
    raise Exception("For compositing, 'frame_start' and 'frame_end' must be specified in the JSON shot-info file") from e

print("INCOMING FRAME PATH:", incoming_frame_filepath(0))
print("OUTGOING FRAME PATH:", outgoing_frame_filepath(0))

num_frames = (frame_end - frame_start + 1)
while True:
    composited_frames = set(
        frame_number
        for frame_number in range(frame_start, frame_end + 1)
        if os.path.exists(outgoing_frame_filepath(frame_number))
     )

    incoming_frames = set(
        frame_number
        for frame_number in range(frame_start, frame_end + 1)
        if os.path.exists(incoming_frame_filepath(frame_number))
    )

    print("INCOMING:", incoming_frames)
    print("FRAMES ALREADY COMPOSITED:", composited_frames)

    # Quit when we have composited all frames
    if len(composited_frames) == num_frames:
        logging.info("Composited %d of %d frames; exiting"% (num_frames, num_frames))
        exit()

    frames_waiting_to_be_composited = incoming_frames - composited_frames

    for frame in frames_waiting_to_be_composited:
        composite_frame(frame)

    if len(frames_waiting_to_be_composited) == 0:
        logging.info("No frames waiting to be composited; sleeping 30s")
        time.sleep(30)


from django.urls import path
from . import views

urlpatterns = [
    path("api/get_messages", views.GetMessages.as_view() , name="get_messages"),
    path("api/add_new_message", views.AddNewMessage.as_view() , name="add_new_message"),
    path("chat", views.index, name="index"),
    path("chat/<str:room_name>", views.room, name="room"),
]


from pickle import MARK
import matplotlib.pyplot as plt
from matplotlib.ticker import (MultipleLocator, FormatStrFormatter,AutoMinorLocator)

import pandas as pd
import numpy as np
import math
import os
import shutil
from PIL import Image

import ipywidgets as widgets
from IPython.display import display

import nafuma.electrochemistry as ec
import nafuma.plotting as btp
import nafuma.auxillary	as aux

def plot_gc(data, options=None):

	# Update options
	default_options = {
		'force_reload': False,
		'x_vals': 'capacity', 'y_vals': 'voltage',
		'which_cycles': 'all',
		'limit': None, # Limit line to be drawn
		'exclude_cycles': [],
		'show_plot': True,
		'summary': False,
		'charge': True, 'discharge': True,
		'colours': None,
		'markers': None,
		'differentiate_charge_discharge': True,
		'gradient': False,
		'interactive': False,
		'interactive_session_active': False,
		'rc_params': {},
		'format_params': {},
		'save_gif': False,
		'save_path': 'animation.gif',
		'fps': 1,
		'fig': None, 'ax': None,
		'edgecolor': plt.rcParams['lines.markeredgecolor'],
		'plot_every': 1,
		}

	options = aux.update_options(options=options, default_options=default_options)

	# Read data if not already loaded
	if not 'cycles' in data.keys() or options['force_reload']:
		data['cycles'] = ec.io.read_data(data=data, options=options)

	# Update list of cycles to correct indices
	update_cycles_list(data=data, options=options)

	if options['interactive']:
		options['interactive'], options['interactive_session_active'] = False, True
		plot_gc_interactive(data=data, options=options)
		return

	colours = generate_colours(options=options)
	markers = generate_markers(options=options)

	if not options['summary']:

		if options['show_plot']:
			# Prepare plot

			if not options['fig'] and not options['ax']:
				fig, ax = btp.prepare_plot(options=options)
			else:
				fig, ax = options['fig'], options['ax']

			for i, cycle in enumerate(options['which_cycles']):
					if options['charge']:
						data['cycles'][cycle][0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0])

					if options['discharge']:
						data['cycles'][cycle][1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1])

			if options['interactive_session_active']:
				update_labels(options, force=True)
			else:
				update_labels(options)

		if options['save_gif'] and not options['interactive_session_active']:
			if not os.path.isdir('tmp'):
				os.makedirs('tmp')

			# Scale image to make GIF smaller
			options['format_params']['width'] = 7.5
			options['format_params']['height'] = 3

			options['format_params']['dpi'] = 200

			for i, cycle in enumerate(data['cycles']):
				if i in options['which_cycles']:

					giffig, gifax = btp.prepare_plot(options=options)

					if options['charge']:
						cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][0])
					if options['discharge']:
						cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][1])

					gifax.text(x=gifax.get_xlim()[1]*0.8, y=3, s=f'{i+1}')
					update_labels(options)

					giffig, gifax = btp.adjust_plot(fig=giffig, ax=gifax, options=options)

					plt.savefig(os.path.join('tmp', str(i+1).zfill(4)+'.png'))
					plt.close()

			img_paths = [os.path.join('tmp', path) for path in os.listdir('tmp') if path.endswith('png')]
			frames = []
			for path in img_paths:
				frame = Image.open(path)
				frames.append(frame)

			frames[0].save(options['save_path'], format='GIF', append_images=frames[1:], save_all=True, duration=(1/options['fps'])*1000, loop=0)

			shutil.rmtree('tmp')

	elif options['summary'] and options['show_plot']:
		# Prepare plot
		if not options['fig'] and not options['ax']:
			fig, ax = btp.prepare_plot(options=options)
		else:
			fig, ax = options['fig'], options['ax']

		mask = []
		for i in range(data['cycles'].shape[0]):
			if i+1 in options['which_cycles']:
				mask.append(True)
			else:
				mask.append(False)

		# Drop the last row if it is midway through a charge in order to avoid mismatch of length of mask and dataset.
		if len(mask) > data['cycles'].shape[0]:
			del mask[-1]
			data['cycles'].drop(data['cycles'].tail(1).index, inplace=True)

		# FIXME To begin, the default is that y-values correspond to x-values. This should probably be implemented in more logical and consistent manner in the future.
		if options['x_vals'] in ['coulombic_efficiency', 'energy_efficiency']:
			data['cycles'].loc[mask].plot(x='cycle', y=options['x_vals'], ax=ax, color=colours[0][1], kind='scatter', s=plt.rcParams['lines.markersize']*10,  marker=markers[0], edgecolor=plt.rcParams['lines.markeredgecolor'])
			if options['limit']:
				ax.axhline(y=options['limit'], ls='--', c='black')

		else:
			if options['charge']:
				yval = 'charge_' + options['x_vals']
				data['cycles'].loc[mask].plot(x='cycle', y=yval, ax=ax, color=colours[0][0], kind='scatter', s=plt.rcParams['lines.markersize']*10, marker=markers[0], edgecolor=plt.rcParams['lines.markeredgecolor'])

			if options['discharge']:
				yval = 'discharge_' + options['x_vals']
				data['cycles'].loc[mask].plot(x='cycle', y=yval, ax=ax, color=colours[0][1], kind='scatter', s=plt.rcParams['lines.markersize']*10, marker=markers[1], edgecolor=plt.rcParams['lines.markeredgecolor'])

			if options['limit']:
				ax.axhline(y=options['limit'], ls='--', c='black')

		if options['interactive_session_active']:
			update_labels(options, force=True)
		else:
			update_labels(options)

	if options['show_plot']:
		fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options)
		return data['cycles'], fig, ax
	else:
		return data['cycles'], None, None

def plot_gc_interactive(data, options):

	w = widgets.interactive(btp.ipywidgets_update, func=widgets.fixed(plot_gc), data=widgets.fixed(data), options=widgets.fixed(options),
	charge=widgets.ToggleButton(value=True),
	discharge=widgets.ToggleButton(value=True),
	x_vals=widgets.Dropdown(options=['specific_capacity', 'capacity', 'ions', 'voltage', 'time', 'energy'], value='specific_capacity', description='X-values')
	)

	options['widget'] = w

	display(w)

def plot_cv(data, options):

	# Update options
	default_options = {
		'force_reload': False,
		'x_vals': 'voltage', 'y_vals': 'current',
		'which_cycles': 'all',
		'limit': None, # Limit line to be drawn
		'exclude_cycles': [],
		'show_plot': True,
		'charge': True, 'discharge': True,
		'colours': None,
		'differentiate_charge_discharge': True,
		'gradient': False,
		'interactive': False,
		'interactive_session_active': False,
		'rc_params': {},
		'format_params': {},
		'save_gif': False,
		'save_path': 'animation.gif',
		'fps': 1,
		'plot_every': 1,
		'fig': None,
		'ax': None
		}

	options = aux.update_options(options=options, default_options=default_options)

	# Read data if not already loaded
	if not 'cycles' in data.keys() or options['force_reload']:
		data['cycles'] = ec.io.read_data(data=data, options=options)

	# Update list of cycles to correct indices
	update_cycles_list(data=data, options=options)

	colours = generate_colours(options=options)

	if options['show_plot']:
		# Prepare plot
		if not options['fig'] and not options['ax']:
			fig, ax = btp.prepare_plot(options=options)
		else:
			fig, ax = options['fig'], options['ax']

		for i, cycle in enumerate(options['which_cycles']):
			if options['charge']:
				data['cycles'][cycle][0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0])

			if options['discharge']:
				data['cycles'][cycle][1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1])

		# for i, cycle in enumerate(data['cycles']):
		# 	if i in options['which_cycles']:
		# 		if options['charge']:
		# 			cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][0])

		# 		if options['discharge']:
		# 			cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=ax, c=colours[i][1])

		update_labels(options)

	if options['save_gif'] and not options['interactive_session_active']:
		if not os.path.isdir('tmp'):
			os.makedirs('tmp')

		# Scale image to make GIF smaller
		options['format_params']['width'] = 7.5
		options['format_params']['height'] = 3

		options['format_params']['dpi'] = 200

		for i, cycle in enumerate(data['cycles']):
			if i in options['which_cycles']:

				giffig, gifax = btp.prepare_plot(options=options)

				if options['charge']:
					cycle[0].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][0])
				if options['discharge']:
					cycle[1].plot(x=options['x_vals'], y=options['y_vals'], ax=gifax, c=colours[i][1])

				gifax.text(x=gifax.get_xlim()[1]*0.8, y=3, s=f'{i+1}')
				update_labels(options)

				giffig, gifax = btp.adjust_plot(fig=giffig, ax=gifax, options=options)

				plt.savefig(os.path.join('tmp', str(i+1).zfill(4)+'.png'))
				plt.close()

		img_paths = [os.path.join('tmp', path) for path in os.listdir('tmp') if path.endswith('png')]
		frames = []
		for path in img_paths:
			frame = Image.open(path)
			frames.append(frame)

		frames[0].save(options['save_path'], format='GIF', append_images=frames[1:], save_all=True, duration=(1/options['fps'])*1000, loop=0)

		shutil.rmtree('tmp')

	if options['show_plot']:
		fig, ax = btp.adjust_plot(fig=fig, ax=ax, options=options)
		return data['cycles'], fig, ax
	else:
		return data['cycles'], None, None

def update_labels(options, force=False):

	if 'xlabel' not in options.keys() or force:
		options['xlabel'] = options['x_vals'].capitalize().replace('_', ' ')

	if 'ylabel' not in options.keys() or force:
		options['ylabel'] = options['y_vals'].capitalize().replace('_', ' ')

	if 'xunit' not in options.keys() or force:
		if options['x_vals'] == 'capacity':
			options['xunit'] = options['units']['capacity']
		elif options['x_vals'] == 'specific_capacity':
			options['xunit'] = f"{options['units']['capacity']} {options['units']['mass']}$^{{-1}}$"
		elif options['x_vals'] == 'time':
			options['xunit'] = options['units']['time']
		elif options['x_vals'] == 'ions':
			options['xunit'] = None

	if 'yunit' not in options.keys() or force:
		if options['y_vals'] == 'voltage':
			options['yunit'] = options['units']['voltage']

def update_cycles_list(data, options: dict) -> None:

	if options['which_cycles'] == 'all':
		options['which_cycles'] = [i for i in range(len(data['cycles']))]

	elif isinstance(options['which_cycles'], list):

		cycles =[]

		for cycle in options['which_cycles']:
			if isinstance(cycle, int):
				cycles.append(cycle-1)

			elif isinstance(cycle, tuple):
				interval = [i-1 for i in range(cycle[0], cycle[1]+1)]
				cycles.extend(interval)

		options['which_cycles'] = cycles

	# Tuple is used to define an interval - as elements tuples can't be assigned, I convert it to a list here.
	elif isinstance(options['which_cycles'], tuple):
		which_cycles = list(options['which_cycles'])

		if which_cycles[0] <= 0:
			which_cycles[0] = 1

		elif which_cycles[1] < 0:
			which_cycles[1] = len(options['which_cycles'])

		options['which_cycles'] = [i-1 for i in range(which_cycles[0], which_cycles[1]+1)]

	for i, cycle in enumerate(options['which_cycles']):
		if cycle in options['exclude_cycles']:
			del options['which_cycles'][i]

	options['which_cycles'] = options['which_cycles'][::options['plot_every']]

def prettify_gc_plot(fig, ax, options=None):

	##################################################################
	######################### UPDATE OPTIONS #########################
	##################################################################

	# Define the required options
	required_options = [
	'columns',
	'xticks', 'yticks',
	'show_major_ticks', 'show_minor_ticks',
	'xlim', 'ylim',
	'hide_x_axis', 'hide_y_axis',
	'positions',
	'x_vals', 'y_vals',
	'xlabel', 'ylabel',
	'units', 'sizes',
	'title'
	]

	# Define the default options
	default_options = {
		'columns': 1,
		'xticks': None, 'yticks': None,
		'show_major_ticks': [True, True, True, True], 'show_minor_ticks': [True, True, True, True],
		'xlim': None,'ylim': None,
		'hide_x_axis': False, 'hide_y_axis': False,
		'positions': {'xaxis': 'bottom', 'yaxis': 'left'},
		'x_vals': 'specific_capacity', 'y_vals': 'voltage',
		'xlabel': None, 'ylabel': None,
		'units': {'capacity': 'mAh', 'specific_capacity': r'mAh g$^{-1}$', 'time': 's', 'current': 'mA', 'energy': 'mWh', 'mass': 'g', 'voltage': 'V'},
		'sizes': None,
		'title': None
	}

	aux.update_options(options, required_options, default_options)

	##################################################################
	########################## DEFINE SIZES ##########################
	##################################################################

	# Define the required sizes
	required_sizes = [
		'labels',
		'legend',
		'title',
		'line', 'axes',
		'tick_labels',
		'major_ticks', 'minor_ticks']

	# Define default sizes
	default_sizes = {
			'labels': 30*options['columns'],
			'legend': 30*options['columns'],
			'title': 30*options['columns'],
			'line': 3*options['columns'],
			'axes': 3*options['columns'],
			'tick_labels': 30*options['columns'],
			'major_ticks': 20*options['columns'],
			'minor_ticks': 10*options['columns']
	}

	# Initialise dictionary if it doesn't exist
	if not options['sizes']:
		options['sizes'] = {}

	# Update dictionary with default values where none is supplied
	for size in required_sizes:
		if size not in options['sizes']:
			options['sizes'][size] = default_sizes[size]

	##################################################################
	########################## AXIS LABELS ###########################
	##################################################################

	if not options['xlabel']:
		options['xlabel'] = prettify_labels(options['x_vals']) + ' [{}]'.format(options['units'][options['x_vals']])

	else:
		options['xlabel'] = options['xlabel'] + ' [{}]'.format(options['units'][options['x_vals']])

	if not options['ylabel']:
		options['ylabel'] = prettify_labels(options['y_vals']) + ' [{}]'.format(options['units'][options['y_vals']])

	else:
		options['ylabel'] = options['ylabel'] + ' [{}]'.format(options['units'][options['y_vals']])

	ax.set_xlabel(options['xlabel'], size=options['sizes']['labels'])
	ax.set_ylabel(options['ylabel'], size=options['sizes']['labels'])

	##################################################################
	###################### TICK MARKS & LABELS #######################
	##################################################################

	ax.tick_params(direction='in', which='major', bottom=options['show_major_ticks'][0], left=options['show_major_ticks'][1], top=options['show_major_ticks'][2], right=options['show_major_ticks'][0], length=options['sizes']['major_ticks'], width=options['sizes']['axes'])
	ax.tick_params(direction='in', which='minor', bottom=options['show_minor_ticks'][0], left=options['show_minor_ticks'][1], top=options['show_minor_ticks'][2], right=options['show_minor_ticks'][0], length=options['sizes']['minor_ticks'], width=options['sizes']['axes'])

	# DEFINE AND SET TICK DISTANCES

	from . import unit_tables

	# Define default ticks and scale to desired units
	default_ticks = {
		'specific_capacity': [100 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']] / unit_tables.mass()['g'].loc[options['units']['mass']]), 50 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']] / unit_tables.mass()['g'].loc[options['units']['mass']])],
		'capacity': [0.1 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']]), 0.05 * (unit_tables.capacity()['mAh'].loc[options['units']['capacity']])],
		'voltage': [0.5 * (unit_tables.voltage()['V'].loc[options['units']['voltage']]), 0.25 * (unit_tables.voltage()['V'].loc[options['units']['voltage']])],
		'time': [10 * (unit_tables.time()['h'].loc[options['units']['time']]), 5 * (unit_tables.time()['h'].loc[options['units']['time']])]
		}

	if options['positions']['yaxis'] == 'right':
		ax.yaxis.set_label_position("right")
		ax.yaxis.tick_right()

	# Set default tick distances for x-axis if not specified
	if not options['xticks']:

		major_xtick = default_ticks[options['x_vals']][0]
		minor_xtick = default_ticks[options['x_vals']][1]

	# Otherwise apply user input
	else:
		major_xtick = options['xticks'][0]
		minor_xtick = options['xticks'][1]

	# Set default tick distances for x-axis if not specified
	if not options['yticks']:

		major_ytick = default_ticks[options['y_vals']][0]
		minor_ytick = default_ticks[options['y_vals']][1]

	# Otherwise apply user input
	else:
		major_ytick = options['yticks'][0]
		minor_ytick = options['yticks'][1]

	# Apply values
	ax.xaxis.set_major_locator(MultipleLocator(major_xtick))
	ax.xaxis.set_minor_locator(MultipleLocator(minor_xtick))

	ax.yaxis.set_major_locator(MultipleLocator(major_ytick))
	ax.yaxis.set_minor_locator(MultipleLocator(minor_ytick))

	# SET FONTSIZE OF TICK LABELS

	plt.xticks(fontsize=options['sizes']['tick_labels'])
	plt.yticks(fontsize=options['sizes']['tick_labels'])

	##################################################################
	########################## AXES LIMITS ###########################
	##################################################################

	if options['xlim']:
		plt.xlim(options['xlim'])

	if options['ylim']:
		plt.ylim(options['ylim'])

	##################################################################
	############################# TITLE ##############################
	##################################################################

	if options['title']:
		ax.set_title(options['title'], size=options['sizes']['title'])

	##################################################################
	############################# LEGEND #############################
	##################################################################

	if ax.get_legend():
		ax.get_legend().remove()

	return fig, ax

def prettify_labels(label):

	labels_dict = {
		'capacity': 'Capacity',
		'specific_capacity': 'Specific capacity',
		'voltage': 'Voltage',
		'current': 'Current',
		'energy': 'Energy',
		'time': 'Time'
	}

	return labels_dict[label]

def generate_colours(options):

	default_options = {
		'gradient_colours': None,
	}

	aux.update_options(options=options, default_options=default_options)

	# Assign colours from the options dictionary if it is defined, otherwise use standard colours.
	if options['colours']:
		charge_colour = options['colours'][0]
		discharge_colour = options['colours'][1]

		if isinstance(charge_colour, tuple):
			charge_colour = list(charge_colour)
		if isinstance(discharge_colour, tuple):
			discharge_colour = list(discharge_colour)

	else:
		charge_colour = [(40/255, 70/255, 75/255)] # Dark Slate Gray #28464B, coolors.co
		discharge_colour = [(239/255, 160/255, 11/255)] # Marigold #EFA00B, coolors.co

		if not options['differentiate_charge_discharge']:
			discharge_colour = charge_colour

	# If gradient is enabled, find start and end points for each colour
	if options['gradient']:

		if not options['gradient_colours']:

			options['gradient_colours'] = [[None, None], [None, None]]

			add_charge = min([(1-x)*0.75 for x in charge_colour])
			add_discharge = min([(1-x)*0.75 for x in discharge_colour])

			options['gradient_colours'][0][0] = charge_colour
			options['gradient_colours'][0][1] = [x+add_charge for x in charge_colour]

			options['gradient_colours'][1][0] = discharge_colour
			options['gradient_colours'][1][1] = [x+add_discharge for x in discharge_colour]

	# Generate lists of colours
	colours = []

	if len(charge_colour) != len(options['which_cycles']):
		if options['gradient']:
			options['number_of_colours'] = len(options['which_cycles'])

			charge_colours = btp.mix_colours(colour1=options['gradient_colours'][0][0], colour2=options['gradient_colours'][0][1], options=options)
			discharge_colours = btp.mix_colours(colour1=options['gradient_colours'][1][0], colour2=options['gradient_colours'][1][1], options=options)

			for chg, dchg in zip(charge_colours, discharge_colours):
				colours.append([chg, dchg])

		else:
			for i in options['which_cycles']:
				colours.append([charge_colour, discharge_colour])

	else:
		for chg, dchg in zip(charge_colour, discharge_colour):
			colours.append([chg, dchg])

	return colours

def generate_markers(options):

	if not options['markers']:
		markers = ['o', 'v']

	else:
		markers = [options['markers'][0], options['markers'][1]]

	return markers

def get_tickmarks(df: pd.DataFrame, ticks: list, value: str, exclude=None):

	min_val = df[value].min()
	max_val = df[value].max()

	# Get major ticks
	major_ticks = [np.round((min_val + ticks[0]*i),2) for i in range(int(np.floor((max_val-min_val)/ticks[0]))+1)]
	major_ticks.append(np.round(max_val, 2))

	major_ticks = aux.get_unique(major_ticks)

	major_ticklabels = [i*ticks[0] for i in range(len(major_ticks)-1)]
	major_ticklabels.append(np.round((max_val-min_val),1))

	if exclude:
		for i, tick in enumerate(major_ticklabels):
			if tick in exclude:
				del major_ticks[i]
				del major_ticklabels[i]

	# Get minor ticks
	minor_ticks = [np.round((min_val + ticks[1]*i),2) for i in range(int(np.floor((max_val-min_val)/ticks[1]))+1) if np.round((min_val + ticks[1]*i),2) not in major_ticks]
	minor_ticklabels = [np.round(tick - min_val, 2) for tick in minor_ticks]

	return major_ticks, major_ticklabels, minor_ticks, minor_ticklabels

def assign_tickmarks(dfs: list, options, fig, ax, exclude=None):

	major_ticks, major_ticklabels, minor_ticks = [], [], []

	if not exclude:
		exclude = [[None, None] for i in range(len(options['which_cycles']))]

	for i, cycle in enumerate(options['which_cycles']):
		#Get ticks from charge cycle
		major_tick, major_ticklabel, minor_tick, minor_ticklabel = ec.plot.get_tickmarks(dfs[cycle][0], ticks=options['x_tick_locators'], value=options['x_vals'], exclude=exclude[i][0])
		major_ticks += major_tick
		major_ticklabels += major_ticklabel
		minor_ticks += minor_tick

		# Get ticks from discharge cycle
		major_tick, major_ticklabel, minor_tick, minor_ticklabel = ec.plot.get_tickmarks(dfs[cycle][1], ticks=[1, 0.25], value='ions', exclude=exclude[i][1])
		major_ticks += major_tick
		major_ticklabels += major_ticklabel
		minor_ticks += minor_tick

	ax.set_xticks(major_ticks, minor=False)
	ax.set_xticklabels(major_ticklabels)
	ax.set_xticks(minor_ticks, minor=True)

	return fig, ax


from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
import os

root = os.path.abspath(os.path.join(__file__, '..', '..', '..'))

setup(ext_modules=cythonize(Extension(
    './target/client',
    sources=['client.pyx', 'InstrumnetGainer/InstrumnetGainer.cpp', 'CSimpleHandler/CSimpleHandler.cpp'],
    language='c++',
    include_dirs=[os.path.join(root, 'include'), os.path.abspath('./InstrumnetGainer/'), os.path.abspath('./CSimpleHandler/')],
    library_dirs=[os.path.join(root, 'lib')],
    libraries=['thostmduserapi', 'thosttraderapi', 'hiredis'],
    extra_compile_args=[],
    extra_link_args=[]
)))


"""API for fitbit bound to Home Assistant OAuth."""

import logging
from typing import Any

from fitbit import Fitbit

from homeassistant.core import HomeAssistant

from .model import FitbitDevice, FitbitProfile

_LOGGER = logging.getLogger(__name__)

class FitbitApi:
    """Fitbit client library wrapper base class."""

    def __init__(
        self,
        hass: HomeAssistant,
        client: Fitbit,
    ) -> None:
        """Initialize Fitbit auth."""
        self._hass = hass
        self._profile: FitbitProfile | None = None
        self._client = client

    @property
    def client(self) -> Fitbit:
        """Property to expose the underlying client library."""
        return self._client

    def get_user_profile(self) -> FitbitProfile:
        """Return the user profile from the API."""
        response: dict[str, Any] = self._client.user_profile_get()
        _LOGGER.debug("user_profile_get=%s", response)
        profile = response["user"]
        return FitbitProfile(
            encoded_id=profile["encodedId"],
            full_name=profile["fullName"],
            locale=profile.get("locale"),
        )

    def get_devices(self) -> list[FitbitDevice]:
        """Return available devices."""
        devices: list[dict[str, str]] = self._client.get_devices()
        _LOGGER.debug("get_devices=%s", devices)
        return [
            FitbitDevice(
                id=device["id"],
                device_version=device["deviceVersion"],
                battery_level=int(device["batteryLevel"]),
                battery=device["battery"],
                type=device["type"],
            )
            for device in devices
        ]

    def get_latest_time_series(self, resource_type: str) -> dict[str, Any]:
        """Return the most recent value from the time series for the specified resource type."""
        response: dict[str, Any] = self._client.time_series(resource_type, period="7d")
        _LOGGER.debug("time_series(%s)=%s", resource_type, response)
        key = resource_type.replace("/", "-")
        dated_results: list[dict[str, Any]] = response[key]
        return dated_results[-1]


# -*- coding: utf-8 -*-
"""
Code to compile Tulsa football hit data into a DataFrame
"""
from __future__ import print_function, division
import pandas as pd
import numpy as np
import toolz
import copy

#------------#
# FUNCTIONS  #
#------------#
@toolz.curry
def during_practice(practice, hit_date):
    """
    during_practice takes a hit_date that is a datetime object and a pandas
    dataframe called practices. practices should have a start and end time for
    the practice session that occurs on a given day.
    """
    # Find day of hit and format as a string
    hit_day = hit_date.date().strftime('%Y-%m-%d')
    # Find the start and end time of the practice of the hit day
    start_time = practice[hit_day].loc[practice.period == 'start'].date
    end_time = practice[hit_day].loc[practice.period == 'end'].date
    if start_time.empty or end_time.empty:
        return False
    # determine if the hit occurred during practice
    return np.logical_and(start_time <= hit_date, hit_date <= end_time).values[0]

@toolz.curry
def during_event(practices, hit):
    return during_practice(practices[hit.ptype], hit.date)

@toolz.curry
def before_break(practice, hit_date):
    # Find day of hit and format as a string
    hit_day = hit_date.date().strftime('%Y-%m-%d')
    # Find the start time of the break period
    break_time = practice[hit_day].loc[practice.period == 'break'].date
    if break_time.empty:
        # If the practice session didn't have a break, by definition all events
        # for that session occurred before the break.
        return True
    # determine if the hit occurred during practice
    return (hit_date <= break_time).values[0]

def read_hit_data(filename, skiprows=0, sheet='FINAL- bad hits and 1 out',
                  date_column={'date': [1,2]}):
    df = pd.read_excel(filename, skiprows=skiprows, sheetname=sheet,
                       parse_dates=date_column)
    df.columns = [col.strip() for col in df.columns]
    df.set_index('date', inplace=True)
    return df

def read_attendance_data(filename, date_column='date'):
    df = pd.read_excel(filename)
    df.columns = [str(col).strip() for col in df.columns]
    df.set_index(date_column, inplace=True)
    return df

def read_practice_data(filename, skiprows=None, date_column='date'):
    df = pd.read_excel(filename, skiprows=skiprows, sheetname=None)
    df['offensive']['day'] = df['offensive']['day'].ffill()
    df['offensive']['name'] = df['offensive']['name'].ffill()
    df['offensive']['type'] = df['offensive']['type'].ffill()
    df['offensive'].index = df['offensive'][date_column]
    df['defensive']['day'] = df['defensive']['day'].ffill()
    df['defensive']['name'] = df['defensive']['name'].ffill()
    df['defensive']['type'] = df['defensive']['type'].ffill()
    df['defensive'].index = df['defensive'][date_column]
    return df

def read_participation_data(filename, skiprows=None, date_column='date'):
    df = pd.read_excel(filename, skiprows=skiprows, sheetname=None)
    df['offensive']['day'] = df['offensive']['day'].ffill()
    df['offensive']['name'] = df['offensive']['name'].ffill()
    df['offensive'].set_index(date_column, inplace=True)
    df['defensive']['day'] = df['defensive']['day'].ffill()
    df['defensive']['name'] = df['defensive']['name'].ffill()
    df['defensive'].set_index(date_column, inplace=True)
    return df

def read_serial_num_to_position(filename, index_col='number'):
    return pd.read_excel(filename).set_index(index_col)

def read_player_types(filename, index_col='type'):
    return pd.read_excel(filename).set_index(index_col)

def read_event_types(filename, index_col='date'):
    return pd.read_excel(filename).set_index(index_col)

def read_game_durations(filename, index_col=0):
    return pd.read_excel(filename, index_col=index_col, header=[0,1])

@toolz.curry
def get_player_type(position_type_df, player_code):
    if not player_code in position_type_df.columns:
        print('Player code not in position type table. Call Seth.')
        raise KeyError
    try:
        return position_type_df[position_type_df[player_code]==True].index[0]
    except:
        print('Something went wrong while determining player type (offensive or defensive). Call Seth.')
        raise RuntimeError

@toolz.curry
def get_player_position(positions, player_number):
    return positions.loc[player_number].position

@toolz.curry
def get_player_position_code(positions, player_number):
    return positions.loc[player_number]['code']

@toolz.curry
def get_practice(practices, ptype):
    return practices[ptype]

def get_closest_activity_idx(practice, hit_date):
    # Find the index of the practice activity during which the hit occurred
    return practice.index.get_loc(hit_date, method='pad')

def get_activity_idx(practice, hit_date):
    # Return the activity during which the hit occurred
    activity_idx = get_closest_activity_idx(practice, hit_date)
    act_row = practice.iloc[activity_idx]
    ## Hits shouldn't occur during breaks or the all up activities. Assign hits
    ## during these activities to the activity immediately prior.
    if act_row.period == 'break' or act_row.period == 'all up':
        activity_idx -= 1
        ## NOTE: I could do the above recursively, which may be more robust but
        ##       not strictly necessary since I'm basically looking for the
        ##       above result.
        #return get_activity(practice, player_code, hit_date-1)
    return activity_idx

@toolz.curry
def get_activity(practice, pcode, hit_date):
    # Return the activity during which the hit occurred
    activity_idx = get_activity_idx(practice, hit_date)
    return practice.loc[practice.index[activity_idx], pcode]

def get_parti(practice, pcode, hit_date):
    # Return the pcodes of the players that were participating in the event
    # in which the hit occurred
    activity_idx = get_activity_idx(practice, hit_date)
    activity = get_activity(practice, pcode, hit_date)
    return list(practice.columns[practice.iloc[activity_idx] == activity])

@toolz.curry
def get_practice_name(practice, hit_date):
    activity_idx = get_activity_idx(practice, hit_date)
    return practice.loc[practice.index[activity_idx], 'name']

@toolz.curry
def get_event_type(event_df, hit_date):
    return event_df.loc[hit_date.date()].type

def get_num_players(method='by_position', data={}):
    att_df = data['attendance_df']
    pcode = data['pcode']
    hit_date = data['hit_date']
    # Look up number of players
    if method == 'by_position':
        # How many people were playing this position on the hit date?
        return att_df.loc[hit_date.date()][pcode]
    elif method == 'total':
        # How many total people were participating on the hit date?
        att_df = data['df']
        return att_df.loc[hit_date.date()]['total']
    elif method == 'by_activity':
        # How many people were participating in the activity on the hit date?
        # What player codes participated in the activity?
        parti_codes = get_parti(data['practice_df'], pcode, hit_date)
        return att_df.loc[hit_date.date()][parti_codes].sum()
    else:
        raise NotImplementedError

def get_duration(data={}):
    if data['event_type'] == 'game':
        hit_date = data['hit_date']
        game_dur_df = data['game_dur_df']
        ptype = data['ptype']
        activity = data['activity']
        date_mask = [dd.date() == hit_date.date() for dd in game_dur_df.index]
        return game_dur_df.loc[date_mask, (ptype, activity)].iloc[0]
    else:
        durs = data['durations']
        event = data['event']
        prebrk = data['prebreak']
        return durs[np.logical_and(durs.prebreak == prebrk,
                                   durs.name == event)].length.values[0]

def normalize_hits(hit, player_method=None, time_method=None, data={}):
    hit = copy.deepcopy(hit).astype(np.float64)
    if player_method:
        hit = norm_by_players(hit, method=player_method, data=data)
    if time_method:
        hit = norm_by_time(hit, data=data)
    return hit

def norm_by_players(hit, **kwargs):
    num_players = get_num_players(**kwargs)
    if num_players == 0:
        #hit['sum 1'] = np.NAN
        hit['sum 2'] = np.NAN
        hit['sum 3'] = np.NAN
        hit['sum 4'] = np.NAN
        hit['sum 5'] = np.NAN
    else:
        #hit['sum 1'] /= num_players
        hit['sum 2'] /= num_players
        hit['sum 3'] /= num_players
        hit['sum 4'] /= num_players
        hit['sum 5'] /= num_players
    return hit

def norm_by_time(hit, **kwargs):
    duration = get_duration(**kwargs)
    if duration == 0:
        #hit['sum 1'] = np.NAN
        hit['sum 2'] = np.NAN
        hit['sum 3'] = np.NAN
        hit['sum 4'] = np.NAN
        hit['sum 5'] = np.NAN
    else:
        #hit['sum 1'] /= duration
        hit['sum 2'] /= duration
        hit['sum 3'] /= duration
        hit['sum 4'] /= duration
        hit['sum 5'] /= duration
    return hit

def get_hits_out_of_event(input_files):
    practices = read_practice_data(input_files['practices'])
    psn_to_pcode = read_serial_num_to_position(input_files['psn_to_pcode'])
    pcode_to_ptype = read_player_types(input_files['pcode_to_ptype'])

    de = during_event(practices)
    psn_to_ptype = toolz.compose(get_player_type(pcode_to_ptype),
                                 get_player_position_code(psn_to_pcode))

    hits = (read_hit_data(input_files['hits'])
             .reset_index()
             .assign(ptype=lambda x: x.Number.apply(psn_to_ptype))
             .assign(in_event=lambda x: x.apply(de, axis=1)))
    hits = hits.drop(hits.columns[range(2, 22)], axis=1)
    hits[hits.in_event == False].to_csv('hits_not_during_events.csv')

def get_practice_times_pre_post(practices):
    # Just select the offensive practices - they should be the same
    for p in practices:
        practice = practices[p]
        practice['day'] = [pd.to_datetime(x).date() for x in practice['date']]
        practice['prebreak'] = ['pre' if before_break(practice, x['date'])
                                else 'post'
                                for idx, x in practice.iterrows()]
        (practice.groupby(['name', 'prebreak', 'day'], as_index=False)
            .sum()
            .sort_values(by=['day', 'name', 'prebreak'])
            .to_csv('{}_practice_lengths.csv'.format(p), index=False))

def compile_data(input_files, norm_methods=(None, None)):
    hits = read_hit_data(input_files['hits'])
    practices = read_practice_data(input_files['practices'])
    att = read_attendance_data(input_files['attendance'])
    #game_dur = read_game_durations(input_files['game_duration'])
    #duration_data = pd.read_csv('practice_lengths.csv')
    #parti = read_participation_data(input_files['participation'])
    event_df = read_event_types(input_files['event_types'])
    psn_to_pcode = read_serial_num_to_position(input_files['psn_to_pcode'])
    pcode_to_ptype = read_player_types(input_files['pcode_to_ptype'])

    results = []
    for hit_date, hit in hits.iterrows():
        # Look up the hit player's position code
        pcode = get_player_position_code(psn_to_pcode, hit.Number)
        # Determine the player type
        player_type = get_player_type(pcode_to_ptype, pcode)
        # Load the correct set of practices
        practice = practices[player_type]
        # Filter the hit out if it doesn't occur during a practice period
        if not during_practice(practice, hit_date):
            continue
        # Look up the hit player's position
        player_position = get_player_position(psn_to_pcode, hit.Number)
        # Look up the event
        event = get_practice_name(practice, hit_date)
        # Look up the event type
        event_type = get_event_type(event_df, hit_date)
        # Look up the activity
        activity = get_activity(practice, pcode, hit_date)
        # Determine pre/post break
        prebreak = 'pre' if before_break(practice, hit_date) else 'post'
        # Normalize the hit data
        data = {'attendance_df': att,
                'practice_df': practice,
                #'game_dur_df': game_dur,
                'pcode': pcode,
                'ptype': player_type,
                'event': event,
                'event_type': event_type,
                'activity': activity,
                'hit': hit,
                'hit_date': hit_date,
                #'durations': duration_data,
                'prebreak': prebreak}
        hit = normalize_hits(hit, *norm_methods, data)

        print('Processing hit...\n'+
              '\tHit Date: {}\n'.format(hit_date))

        # Add results to list
        results.append({'event': event,
                        'type': event_type,
                        'date': hit_date,
                        'activity': activity,
                        'before_break': prebreak,
                        'player': player_position,
                        'pcode': pcode,
                        'ptype': player_type,
                        #'h1': hit['sum 1'],
                        'h2': hit['sum 2'],
                        'h3': hit['sum 3'],
                        'h4': hit['sum 4'],
                        'h5': hit['sum 5']})
    return (pd.DataFrame(results)
              .assign(day=lambda x: [pd.to_datetime(y).date() for y in x.date])
              .sort_values(by=['date']))

#--------------#
# MAIN PROGRAM #
#--------------#
if __name__ == '__main__':
    input_files = {'hits': 'Tulsa_HIE_5 minute increments final- corrected 7-10.xlsx',
                   'practices': 'practices.xlsx',
                   'attendance': 'attendance.xlsx',
                   'event_types': 'event_type.xlsx',
                    #'game_duration': 'minutes_per_game.xlsx',
                   'pcode_to_ptype': 'player_code_to_type.xlsx',
                   'psn_to_pcode': 'serial_number_to_position.xlsx'}

    compile_data(input_files, norm_methods=(None, None)).to_csv('compiled_raw.csv')
    #compile_data(input_files, norm_methods=('by_position', None)).to_csv('compiled_by_position.csv')
    #compile_data(input_files, norm_methods=('by_activity', None)).to_csv('compiled_by_activity.csv')
    #compile_data(input_files, norm_methods=('by_activity', 'by_time')).to_csv('compiled_by_time_activity_test.csv')