"""

Competition program
~~~~~~~~~~~~~~~~~~~

A program to enter data and output data for any event

"""

__title__ = "Competition program"
__author__ = "BSpoones"
__version__ = "1.0.0"
__copyright__ = "Copyright 2021-2026"

import json,re
import sqlite3
from time import sleep
from sqlite3 import connect

class Competiton:
    """Competition object containing all functions to run a competition"""
    def __init__(self):
        # Variables below used for presence checks only, these are newly assigned in enter_competitors()
        self.teams = {}
        self.individuals = {}
        self.events = []

    def event_choice(self):
        self.events = (input("Please enter each event you would like to include, seperated by commas (,): ").split(",")) # Creates list of events from input
        self.events = list(map(lambda x: x.lstrip(),self.events)) # Removes leading space that may occour when entering information
        if any(item=="" for item in self.events): # Prevents blank events
            print("One or more of the events is blank")
            return self.event_choice()
        nl = "\n" # Newline character in variable because python struggles with backslashes in f strings
        confirmation = input(f"Your selected events are:\n{nl.join(self.events)}\nAre you happy with your selection (Y/N)") # Shows formatted list of entered events for confirmation
        if confirmation.lower() in ("y","yes"): # Using .lower() to avoid checking for caps, allows for a smaller check
            pass
        elif confirmation.lower() in ("n","no"):
            return self.event_choice()
        else:
            print("Please select a valid confirmation")
            return self.event_choice()

    def team_input(self):
        team = {} # Blank dict to be filled and appended to self.teams
        team_name = input("Please enter a team name: ")
        if team_name in self.teams.keys() or team_name in self.individuals.keys(): # Checks both dicts to avoid duplicate entries in event_scores
            print("This team name is already in use")
            return self.team_input()
        team_members = input("Please enter the names of each member, seperated by a comma (,): ").split(",") # Creates list of members from input
        team_members = list(map(lambda x: x.lstrip(), team_members)) # Removes leading space that may occour when entering information
        for name in team_members:
            if not self.name_validation(name): # If it fails the name validation
                print(f"{name} is an invalid team name, please try again")
                return self.team_input()
        team["members"] = team_members
        team["positions"] = []
        team["scores"] = []
        self.teams[team_name] = team # Final output = {TEAMNAME: {members:[Name1,Name2,Name3],events:[4,3,2,2,5]}}

    def individual_input(self):
        individual = input("Please enter the name for the individual: ")
        if individual in self.individuals.keys() or individual in self.teams.keys():  # Checks both dicts to avoid duplicate entries in event_scores
            print("This name is already in use")
            return self.individual_input()
        if not self.name_validation(individual): # If it fails name validation
            print("This is an invalid name, please ensure you have spelled it correctly.")
            return self.individual_input()
        self.individuals[individual] = {"positions":[], "scores": []} # Empty lists used later as a presence check for showing scores
        # Final output = {INDIVIDUAL_NAME: []}

    def name_validation(self, name):
        if name is None or not re.fullmatch('[A-z]{2,25}( [A-z]{2,25})?',name): # Presence and Regex check for either 1 or 2 words containing only letters between 2 and 25 characters each
            return False
        else:
            return True

    def enter_scores(self):
        print("Teams:")
        for team_name in self.teams.keys(): # Team names stored as keys in dict
            print(f"{team_name}")
        print("Individuals:")
        for individual_name in self.individuals.keys(): # Individual names stored as keys in dict
            print(f"{individual_name}")
        competitor = input("Please select a competitor from the list below: ")
        if competitor in self.teams.keys():
            competitor_item = self.teams[competitor] # Easier way to search for competitor data than a for loop, competitor_item var still changes dict item
        elif competitor in self.individuals.keys():
            competitor_item = self.individuals[competitor] # Easier way to search for competitor data than a for loop, competitor_item var still changes dict item
        else:
            print("Competitor not found, please ensure you have typed it properly")
            return self.enter_scores()

        if competitor_item["positions"] != []: # If the user has already entered scores, they appear
            scores_list = competitor_item["positions"]
            print("Current scores:")
            for i, event in enumerate(self.events):
                print(f"{event:<10}{scores_list[i]}") # Outputs formatted event scores
        else:
            # If nothing has been input, it just shows the scores
            print("Events:")
            for event in self.events:
                print(event)
        new_positions = input("Please enter your new positions for every event, seperated by a comma (,)").split(",") # Splits input into list
        try:
            new_positions = [int(x) for x in new_positions] # Sets every item from input to be an int
        except:
            print("One or more items in the string was not a number")
            return self.enter_scores()
        if len(new_positions) != len(self.events): # Checks if input matches the amount of events
            print("You have entered too few or too many positions.")
            return self.enter_scores()
        for i, event in enumerate(self.events):
            print(f"{event:<10}{new_positions[i]}") # Outputs events and the selected score
        confirmation = input(f"Are you happy with your selection (Y/N)")
        if confirmation.lower() in ("y","yes"):
            pass
        elif confirmation.lower() in ("n","no"):
            return self.enter_scores()
        else:
            print("Please select a valid confirmation")
            return self.enter_scores()

        new_scores = []
        total_participants = len(self.teams.keys()) + len(self.individuals.keys()) # Sums the length of all competitors
        for position in new_positions:
            # Formula to calculate scores: score = total_participants - position + 1
            score = total_participants - position + 1
            new_scores.append(score)
        competitor_item["scores"] = new_scores
        competitor_item["positions"] = new_positions

    def enter_new_data(self):
        # Resets data if already entered
        self.teams = {}
        self.individuals = {}
        self.events = []
        self.event_choice()
        while True: # Keeps showing menu until user selects finished
            if self.enter_competitors():
                break
        while True: # Keeps showing menu until user selects finished
            if self.enter_scores_menu():
                break

    def enter_competitors(self):
        print("Please enter all teams and indaviduals by selecting one of the following\n\n[1] Enter a team\n[2] Enter an indavidual\n[3] I've entered all competitors")
        selection = input("Please enter your choice: ")
        if selection == '1':
            self.team_input()
        elif selection == '2':
            self.individual_input()
        elif selection == '3':
            if self.teams == {} and self.indaviduals == {}: # Checks if both lists are empty (No data entered)
                print("You need at least one competitor")
                return False
            else:
                return True
        else:
            print("Please select a valid input")
            return self.enter_competitors()

    def enter_scores_menu(self) -> str:
        print("Please enter all teams and indaviduals by selecting one of the following\n\n[1] Enter a score\n[2] I've entered all competitors")
        selection = input("Please enter your choice: ")
        if selection == '1':
            self.enter_scores()
        elif selection == '2':
            return True

    def output_menu(self) -> str:
        print("Please select a method to show the data:\n\n[1] Show podium\n[2] Show all scores\n[3] Show team scores\n[4] Show individual scores\n[5] Show scores for event")
        selection = input("\nPlease enter your choice:")
        if selection == '1':
            self.show_podium()
        elif selection == "2":
            self.show_all_scores()
        elif selection == "3":
            self.team_scores()
        elif selection == "4":
            self.individual_scores()
        elif selection == "5":
            self.event_scores()
        else:
            print("Please select a valid input")
            sleep(1)
            return self.output_menu()

    def import_menu(self) -> str:
        print("Please select one of the following:\n\n[1] Import from JSON\n[2] Import from database")
        selection = input("\nPlease enter your choice: ")
        if selection == "1":
            self.load_from_JSON()
        elif selection == "2":
            self.load_from_db()
        else:
            print("Please select a valid input")
            return self.import_menu()

    def export_menu(self) -> str:
        print("Please select one of the following:\n\n[1] Export to JSON\n[2] Export to database")
        selection = input("\nPlease enter your choice: ")
        if selection == "1":
            self.save_to_JSON()
        elif selection == "2":
            self.save_to_db()
        else:
            print("Please select a valid input")
            return self.export_menu()

    def show_podium(self) -> str:
        final_scores = {}
        for key, val in self.teams.items(): # Iterates through every item in self.teams
            team_name = key
            total_score = sum(val["scores"]) # Sums every item in scores list to get total score
            final_scores[team_name] = total_score
        for key, val in self.individuals.items(): # Iterates through every item in self.individuals
            name = key
            total_score = sum(val["scores"]) # Sums every item in scores list to get total score
            final_scores[name] = total_score
        sorted_final_scores = dict(sorted(final_scores.items(), key=lambda item: item[1],reverse=True)) # Sorts dict so position values can be used for top 3
        final_score_key = [*sorted_final_scores] # Python method to get list of keys of dict
        final_score_values = [*sorted_final_scores.values()] # Python method to get list of values of dict
        if len(sorted_final_scores) < 3:
            print("Final scores")
            for key,value in sorted_final_scores.items():
                print(f"{key:<10}{value}") # Outputs data
        else:
            # No for loop is used as there are only 3 values
            print("Podium:\n")
            print(f"First place: {final_score_key[0]} with {final_score_values[0]}")
            print(f"Second place: {final_score_key[1]} with {final_score_values[1]}")
            print(f"Third place:{final_score_key[2]} with {final_score_values[2]}")
            print("") # New line to seperate output later

    def show_all_scores(self) -> str:
        final_scores = {}
        # For loops iterate through every competitor appending their name and total score
        for key, val in self.teams.items():
            team_name = key
            total_score = sum(val["scores"])
            final_scores[team_name] = total_score
        for key, val in self.individuals.items():
            name = key
            total_score = sum(val["scores"])
            final_scores[name] = total_score
        sorted_final_scores = dict(sorted(final_scores.items(), key=lambda item: item[1],reverse=True)) # Sorts dict to be used for rankings
        print("Ranking     Competitor  Score\n")
        for i,(key,val) in enumerate(sorted_final_scores.items()): # Outputs position, name and final score
            print(f"{i+1:<12}{key:<12}{val:<12}")

    def team_scores(self) -> str:
        final_scores = {}
        for key, val in self.teams.items(): # Gets all data from teams
            team_name = key
            total_score = sum(val["scores"])
            final_scores[team_name] = total_score
        sorted_final_scores = dict(sorted(final_scores.items(), key=lambda item: item[1],reverse=True)) # Sorts dict to show top scores
        print("Ranking     Competitor  Score\n")
        for i,(key,val) in enumerate(sorted_final_scores.items()): # Outputs ranking, name and total score
            print(f"{i+1:<12}{key:<12}{val:<12}")

    def individual_scores(self) -> str:
        final_scores = {}
        for key, val in self.individuals.items(): # Gets all data from individuals
            name = key
            total_score = sum(val["scores"])
            final_scores[name] = total_score
        sorted_final_scores = dict(sorted(final_scores.items(), key=lambda item: item[1],reverse=True)) # Sorts dict to show top scores
        print("Ranking     Competitor  Score\n")
        for i,(key,val) in enumerate(sorted_final_scores.items()): # Outputs ranking, name and total score
            print(f"{i+1:<12}{key:<12}{val:<12}")

    def event_scores(self) -> str:
        print("Events:")
        # Input selects event from events list
        for event in self.events:
            print(event)
        event_choice = input("Please choose an event")
        try:
            event_index = self.events.index(event_choice) # Used to edit scores list
        except:
            print("Event not found, please try again")
            return self.event_scores()
        final_scores = {}
        for key, val in self.teams.items(): # Iterates through all values
            team_name = key
            try:
                score = (val["scores"][event_index])
            except:
                score = 0
            print(score)
            final_scores[team_name] = score
        for key, val in self.individuals.items(): # Iterates through all values
            name = key
            try:
                score = (val["scores"][event_index])
            except:
                score = 0
            final_scores[name] = score
        sorted_final_scores = dict(sorted(final_scores.items(), key=lambda item: item[1],reverse=True)) # Sorts dict to show top ranking
        print(f"Scores for {event_choice}:")
        print("Ranking     Competitor  Score\n")
        for i,(key,val) in enumerate(sorted_final_scores.items()): # Outputs ranking, nane and score
            print(f"{i+1:<12}{key:<12}{val:<12}")

    def save_to_JSON(self) -> json:
        data = {}
        data["events"] = self.events
        data["teams"] = self.teams
        data["individuals"] = self.individuals
        json.dump(data,open("data.json","w+"),indent=4) # Dumps data dict to JSON file. "w+" creates a file if not present

    def load_from_JSON(self) -> json:
        try:
            with open("data.json","r") as f:
                content = json.load(f)
                self.events = content["events"]
                self.teams = content["teams"]
                self.individuals = content["individuals"]
            print("JSON data loaded")
            sleep(1)
        except: # If json doesn't exist or json is empty
            print("Data not found")
            return self.import_menu() # Returns back to choice of imports

    def save_to_db(self) -> sqlite3:
        self.cxn = connect("database.db", check_same_thread=False) # Opens database file
        self.cur = self.cxn.cursor() # Sets cursor from SQlite3
        self.cur.execute("DROP TABLE IF EXISTS events") # Deletes table if it already exists, clearing past data
        self.cur.execute("DROP TABLE IF EXISTS teams")
        self.cur.execute("DROP TABLE IF EXISTS individuals")
        event_table = """
        CREATE TABLE events(
            EventName text
        )""" # SQL to create events table
        individual_table = """
        CREATE TABLE individuals (
            IndividualName text,
            IndividualPositions text,
            IndividualScores text
        )"""# SQL to create individuals table
        team_table = """
        CREATE TABLE teams (
            TeamName text,
            TeamMembers text,
            TeamPositions text,
            TeamScores text
        )"""# SQL to create teams table
        self.cur.execute(event_table) # Runs SQL code from above, in seperate lines because SQlite3 can only execute one at a time
        self.cur.execute(team_table)
        self.cur.execute(individual_table)

        for event in self.events:
            self.cur.execute("INSERT INTO events(EventName) VALUES (?)",(event,)) # Adds each event to db table
        for key,value in self.teams.items(): # Iterating through all team data
            team_name = key
            team_members = " ".join(value["members"]) # Using a string as a list because SQlite3 doesn't support lists
            positions = list(map(str, value["positions"])) # Converting to string as SQlite3 doesn't support int
            team_positions = " ".join(positions)
            scores = list(map(str, value["scores"]))
            team_scores = " ".join(scores)
            # Code below adds all team data as a row in database
            self.cur.execute("INSERT INTO teams(TeamName,TeamMembers,TeamPositions,TeamScores) VALUES (?,?,?,?)",(team_name,team_members,team_positions,team_scores))
        for key,value in self.individuals.items(): # Iterating through all individual data
            name = key
            positions = list(map(str, value["positions"])) # Converting to string as SQlite3 doesn't support int
            positions = " ".join(positions) # Using a string as a list because SQlite3 doesn't support lists
            scores = list(map(str, value["scores"]))
            scores = " ".join(scores)
            # Code below adds all individual data as a row in database
            self.cur.execute("INSERT INTO individuals(IndividualName,IndividualPositions,IndividualScores) VALUES (?,?,?)",(name,positions,scores))
        self.cxn.commit() # Commits (saves) all data back to the db

    def load_from_db(self):
        try:
            self.cxn = connect("database.db", check_same_thread=False) # Connects to database file
            self.cur = self.cxn.cursor() # Sets database cursor

            self.cur.execute("SELECT EventName FROM events") # Selects all event names from events table
            events = self.cur.fetchall() # Creates a list of tuples
            for item in events: # Iterates through all tuples
                self.events.append(item[0]) # SQlite3 retrieves data as a single length tuple (data,) so the first element is used

            self.cur.execute("SELECT * FROM teams") # Selects everything from teams table
            team_list = self.cur.fetchall() # Creates a list of tuples from each row
            for item in team_list:
                team = {}
                team_name = item[0]
                team_members = item[1].split() # Splits member string into list by a space
                team_positions = item[2].split()
                team_positions = list(map(int,team_positions)) # Converts back to integers
                team_scores = item[3].split()
                team_scores = list(map(int,team_scores))
                team["members"] = team_members
                team["positions"] = team_positions
                team["scores"] = team_scores
                self.teams[team_name] = team # Appends team dict back to self.teams

            self.cur.execute("SELECT * FROM individuals") # Selects everything from individuals table
            team_list = self.cur.fetchall() # Creates a list of tuples from each row
            for item in team_list:
                individual = {}
                name = item[0]
                positions = item[1].split() # Splits string into list by space
                positions = list(map(int,positions)) # Converts back into int for calculations
                scores = item[2].split()
                scores = list(map(int,scores))
                individual["positions"] = positions
                individual["scores"] = scores
                self.individuals[name] = individual # Appends data back to self.individuals
            print("Database data loaded")
            sleep(1)
        except:
            print("Failed to retrieve data")
            return self.import_menu()

def run_competition(): # Run competition outisde of class, this is called when running the code providing a UI for the user
    competition = Competiton() # Creates instance of Competition class
    print("-------------------------------\n\nWelcome to the competition menu\n\n-------------------------------\n")
    print("Please select an option to begin\n[1] Enter new data\n[2] Import data from file\n[3] Exit")
    selection = input("\nPlease enter your choice: ")
    if selection == "1":
        competition.enter_new_data()
    elif selection == "2":
        competition.import_menu()
    elif selection == "3":
        print("The program will now close: ")
        sleep(2)
        exit()
    while True:
        print("Please select one of the following:\n[1] Enter new data\n[2] Export data\n[3] Show results\n[4] Exit")
        selection = input("Please enter your choice: ")
        if selection == "1":
            competition.enter_new_data()
        elif selection == "2":
            competition.export_menu()
        elif selection == "3":
            competition.output_menu()
        elif selection == "4":
            print("The program will now close")
            sleep(2)
            exit()

if __name__ == "__main__": # If this code is being run directly instead of being imported to external project
    run_competition()


"""empty message

Revision ID: 441975213ebc
Revises:
Create Date: 2021-03-11 14:52:50.717868

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = '441975213ebc'
down_revision = None
branch_labels = None
depends_on = None

def upgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('booking',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('name', sa.String(length=80), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_table('books',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('bookTitle', sa.String(length=100), nullable=True),
    sa.Column('bookText', sa.String(), nullable=True),
    sa.Column('author', sa.String(), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    op.create_table('user',
    sa.Column('id', sa.Integer(), nullable=False),
    sa.Column('username', sa.String(length=80), nullable=True),
    sa.Column('email', sa.String(length=120), nullable=True),
    sa.PrimaryKeyConstraint('id')
    )
    # ### end Alembic commands ###

def downgrade():
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('user')
    op.drop_table('books')
    op.drop_table('booking')
    # ### end Alembic commands ###


# !/usr/bin/env python3
# _*_coding:utf-8_*_
# __author__：FLS

import base64
from  io import StringIO

'''

Python base64模块真正用的上的方法只有8个，分别是encode, decode, encodestring, decodestring, b64encode,b64decode, urlsafe_b64decode,urlsafe_b64encode。他们8个可以两两分为4组，
encode,decode一组，专门用来编码和解码文件的,也可以对StringIO里的数据做编解码；
encodestring,decodestring一组，专门用来编码和解码字符串；
b64encode和b64decode一组，用来编码和解码字符串，并且有一个替换符号字符的功能。这个功能是这样的：因为base64编码后的字符除 了英文字母和数字外还有三个字符 + / =, 其中=只是为了补全编码后的字符数为4的整数，而+和/在一些情况下需要被替换的，
b64encode和b64decode正是提供了这样的功能。至于什么情况下+和/需要被替换，最常见的就是对url进行base64编码的时候。
urlsafe_b64encode和urlsafe_b64decode 一组，这个就是用来专门对url进行base64编解码的，实际上也是调用的前一组函数。

因为base64编码后的字符除 了英文字母和数字外还有三个字符 + / =, 其中=只是为了补全编码后的字符数为4的整数，而+和/在一些情况下需要被替换的，b64encode和b64decode正是提供了这样的功能。至于什 么情况下+和/需要被替换，最常见的就是对url进行base64编码的时候

'''

# a=b'this is ok '
# res=base64.b64encode(a)
# print(res)
#
# print(base64.b64decode(res))

a=b'this is ok '
res=base64.encodebytes(a)
print(res)

a=b'this is ok'
res=base64.b64encode(a)
print(res)

# url_encode

b=b'i\xb7\x1d\xfb\xef\xff'
res=base64.urlsafe_b64encode(b)
print(res)  # b'abcd--__'  实际上应该是 b'abcd++//'

# 其实在 python3 所有的对象都是unicode 类型，所以所有的
# 将  中文  转成 bash64 编码
# 首先base64 生成的编码都是 acsii码，而 python3 默认都是 unicode 码，而 b64encode 的参数必须是 bytes 类型，所以想要将中文转为 base64 编码，首先要转码

a='你好'
b=a.encode('utf-8')
print(b)

c=base64.b64encode(b)
print(c)

# 解码过程同理


#GRUIDE: https://towardsdatascience.com/data-science-skills-web-scraping-javascript-using-python-97a29738353f

from selenium import webdriver
import time

#Starts the driver
#Opens Dplay
#Presents welcome meny

def main():
    driver = webdriver.Firefox()
    driver.get("https://www.dplay.no")
    inp = ""
    while inp != "0":
        welcomeMeny()
        inp = input()
        checkInput(inp, driver)

    try:
        driver.close()
    except:
        print("Browser window already closed")

#Presents the user with alternatives
def welcomeMeny():
    alternatives = {0: "Exit", 1: "Fjern Reklame"}
    print("Velg hva du vil gjøre!:")
    for alt in alternatives:
        print(str(alt) + " : " + str(alternatives[alt]))

#Checks which input you've given
def checkInput(inp, driver):
    if inp == str(1):
        adBlock(driver)

#Removes the ads
def adBlock(driver):
    try:
        element = driver.find_element_by_id("adsbase")
        attr = element.get_attribute("src")
    except:
        print("No advertisement to remove\n")
        return
    newUrl = checkURL(attr)
    if newUrl:
        driver.execute_script("document.getElementById('adsbase').setAttribute('src', '')", element)
        #Should find a better soluton than sleep (i think)
        time.sleep(0.5)
        adBlock(driver)
    else:
        print("No advertisement to remove\n")

def checkURL(url):
    #Wack måte å gjøre det på, bruk regex eller noe annet
    if url[0:17] == "https://www.dplay":
        print(url[0:17])
        return False
    elif url == "":
        return False
    else:
        print(url[0:17])
        return True

main()


import logging

from trello import TrelloClient

logging.basicConfig(filename='trello_webhook_module.log', level=logging.DEBUG)

NON_CREATOR_STATE = True

class trello_handle_card_deleted(NebriOS):
    listens_to = ['card_deleted']

    def check(self):
        return self.kind == "trello_card" and self.card_deleted == True and not self.card_deleted_handled and self.card_deleted_by_noncreator == NON_CREATOR_STATE

    def action(self):
        client = self.get_client()
        user = self.get_me(client)
        member_data = self.get_member_data(user['id'])
        board = client.get_board(member_data.backup_board_id)
        backup_list = board.get_list(member_data.deleted_list_id)
        for label in self.card_json['labels']:
            board.add_label(label['name'], label['color'])
        add_card_kwargs = {'desc': self.card_json['desc'], 'labels': self.card_json['labels']}
        due_date = self.card_json.get('due', None)
        if due_date:
            add_card_kwargs['due'] = due_date
        card_backup = backup_list.add_card(self.card_json['name'], **add_card_kwargs)
        for checklist in self.card_json['checklists']:
            card_backup.add_checklist(checklist['name'], [item['name'] for item in checklist['checkItems']], itemstates=[item['state'] == 'complete' for item in checklist['checkItems']])
        for attachment in self.card_json['attachments']:
            card_backup.attach(url=attachment['url'])
        self.card_deleted_handled = True

    def get_me(self, client):
        return client.fetch_json('/members/me')

    def get_trello_token(self):
        try:
            return Process.objects.get(kind="trello_oauth_token").token
        except:
            load_card('trello-token-save')
            raise Exception('Token does not exist. Please supply one on the Trello OAuth Token Creation card or run trello_webhook_setup.')

    def get_client(self):
        return TrelloClient(api_key=shared.TRELLO_API_KEY, api_secret=shared.TRELLO_API_SECRET, token=self.get_trello_token())

    def get_member_data(self, member_id):
        member_data, _ = Process.objects.get_or_create(kind="trello_member_data", member_id=member_id)
        return member_data


from __future__ import division, print_function
import pandas as pd
from pandas import DataFrame
from abc import ABC, abstractmethod
from sklearn.metrics import mean_absolute_error
from bfast import BFASTCPU as BFAST
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import kruskal

# Concept drift detection method's (CDDM)
class ConceptDriftDetectionMethod(ABC):

    def __init__(self):
        self._config = None
        self._train = DataFrame()
        self._new_values = DataFrame()
        self._predictions = list()
        self._next_predictions = list()
        self._yearly_freq = 365
        super().__init__()

    def _init_attrs(self):
        self._config = None
        self._train = DataFrame()
        self._new_values = DataFrame()
        self._predictions = list()
        self._next_predictions = list()

    def reset(self):
        self._init_attrs()

    def is_drifting(self, config, train: DataFrame, new_values: DataFrame, predictions: list):
        self._config = config
        self._train = train
        self._new_values = new_values
        self._predictions = predictions

        is_drifting = self._is_drifting()

        return is_drifting

    def notify_new_predictions(self, predictions: list):
        self._next_predictions = predictions
        self._notify_new_predictions()

    @abstractmethod
    def _is_drifting(self):
        pass

    def _notify_new_predictions(self):
        pass

    def plot(self):
        pass

class IgnoreCDDM(ConceptDriftDetectionMethod):

    def _is_drifting(self):
        return False

class AdaptiveCDDM(ConceptDriftDetectionMethod):

    def _is_drifting(self):
        return True

class KruskalWallisInputCDDM(ConceptDriftDetectionMethod):

    def _is_drifting(self):

        y_pred_range = len(self._new_values)
        n_steps = self._config.n_steps

        new_dist = np.concatenate([self._train.y.values[-n_steps+y_pred_range:], self._new_values.y.values])
        old_dist = self._train.y.values[-self._yearly_freq+y_pred_range:-self._yearly_freq+y_pred_range+n_steps]

        print("Length's: " + str(len(new_dist)) + ", " + str(len(old_dist)))
        _new = np.concatenate([self._train.index.values[-n_steps+y_pred_range:], self._new_values.index.values])
        _old = self._train.index.values[-self._yearly_freq+y_pred_range:-self._yearly_freq+y_pred_range+n_steps]
        print("Ranges: " + str(min(_new)) + " - " + str(max(_new)) + ", " + str(min(_old)) + " - " + str(max(_old)))

        stat, p = kruskal(old_dist, new_dist)

        if self._config.verbose > 1:
            print('Statistics=%.3f, p=%.3f' % (stat, p))

        alpha = 0.05 # TODO: add in class as param
        if p > alpha:
            if self._config.verbose > 1:
                print('Same distributions (fail to reject H0)')
            return False

        else:
            if self._config.verbose > 1:
                print('Different distributions (reject H0)')
            return True

class KruskalWallisOutputCDDM(ConceptDriftDetectionMethod):

    def _is_drifting(self):

        y_pred_range = len(self._new_values)
        n_steps = self._config.n_steps

        new_dist = np.array(self._predictions).flatten()
        old_dist = self._train.y.values[-self._yearly_freq:-self._yearly_freq + y_pred_range]

        print("Length's: " + str(len(new_dist)) + ", " + str(len(old_dist)))
        _new = self._new_values.index.values
        _old = self._train.index.values[-self._yearly_freq:-self._yearly_freq + y_pred_range]
        print("Ranges: " + str(min(_new)) + " - " + str(max(_new)) + ", " + str(min(_old)) + " - " + str(max(_old)))

        stat, p = kruskal(old_dist, new_dist)

        if self._config.verbose > 1:
            print('Statistics=%.3f, p=%.3f' % (stat, p))

        alpha = 0.05  # TODO: add in class as param
        if p > alpha:
            if self._config.verbose > 1:
                print('Same distributions (fail to reject H0)')
            return False

        else:
            if self._config.verbose > 1:
                print('Different distributions (reject H0)')
            return True

class BFASTCDDM(ConceptDriftDetectionMethod):

    # https://pypi.org/project/bfast/#files

    def _is_drifting(self):

        x_l = pd.DatetimeIndex(np.concatenate([self._train.index, self._new_values.index]))
        y_l = np.concatenate([self._train.y.values, self._new_values.y.values])

        y_pred_range = len(self._new_values)

        start = (len(x_l) - y_pred_range)

        bf = BFAST(start=x_l[start], dates=x_l, verbose=0)

        bf.fit(y_l)

        drifts = np.array(np.where(bf.breaks)).flatten().any()

        return drifts

    def _plot(self, x_l, y_l, start, bf):

        plt.plot(x_l, y_l)
        plt.axvline(x=x_l[start], color="red")

        plt.plot(x_l, bf.y_pred, color="brown")

        indexes = start + np.array(np.where(bf.breaks)).flatten()
        plt.plot(x_l[indexes], y_l[indexes], 'ro', color="orange")  # , marker='o')
        plt.scatter(x_l[indexes], y_l[indexes], marker='o', edgecolors="orange", color="orange")

        plt.legend(list(["y", "Start", "BFAST Model", "Breakpoints"]), loc="best")
        plt.title("Breaks For Additive Season and Trend (BFAST)")

    @staticmethod
    def plot_bfast(df: DataFrame, start):

        bf = BFAST(start=start, dates=df.index, verbose=0)

        bf.fit(df.y.values)

        BFASTCDDM()._plot(df.index, df.y.values, np.where(start == df.index), bf)

class CUSUMCDDM(ConceptDriftDetectionMethod):

    def __init__(self):
        self._errors = list()
        self._last_predictions = list()
        super().__init__()

    def _init_attrs(self):
        super()._init_attrs()
        self._errors = list()
        self._last_predictions = list()

    def _is_drifting(self):

        mae = self._calculate_error(self._new_values.y.values, self._last_predictions)

        self._errors.extend(mae)

        #errors = np.concatenate([self._errors, mae])

        ta, tai, taf, amp = self.detect_cusum(self._errors, threshold=15, drift=10, ending=True, show=False)

        # If there is signal in index that represents last prediction errors
        drifts = np.any(np.array(ta) >= len(self._errors) - len(self._last_predictions))

        #drifts = np.array(ta).any()

        return drifts

    def _notify_new_predictions(self):
        self._last_predictions = self._next_predictions

    def _calculate_error(self, real: list, predictions: list):
        return np.array([mean_absolute_error(np.array([real[i]]), np.array([predictions[i]])) for i in range(len(predictions))])

    def detect_cusum(self, x, threshold=1, drift=0, ending=False, show=True, ax=None):
        """Cumulative sum algorithm (CUSUM) to detect abrupt changes in data.

        Parameters
        ----------
        x : 1D array_like
            data.
        threshold : positive number, optional (default = 1)
            amplitude threshold for the change in the data.
        drift : positive number, optional (default = 0)
            drift term that prevents any change in the absence of change.
        ending : bool, optional (default = False)
            True (1) to estimate when the change ends; False (0) otherwise.
        show : bool, optional (default = True)
            True (1) plots data in matplotlib figure, False (0) don't plot.
        ax : a matplotlib.axes.Axes instance, optional (default = None).

        Returns
        -------
        ta : 1D array_like [indi, indf], int
            alarm time (index of when the change was detected).
        tai : 1D array_like, int
            index of when the change started.
        taf : 1D array_like, int
            index of when the change ended (if `ending` is True).
        amp : 1D array_like, float
            amplitude of changes (if `ending` is True).

        Notes
        -----
        Tuning of the CUSUM algorithm according to Gustafsson (2000)[1]_:
        Start with a very large `threshold`.
        Choose `drift` to one half of the expected change, or adjust `drift` such
        that `g` = 0 more than 50% of the time.
        Then set the `threshold` so the required number of false alarms (this can
        be done automatically) or delay for detection is obtained.
        If faster detection is sought, try to decrease `drift`.
        If fewer false alarms are wanted, try to increase `drift`.
        If there is a subset of the change times that does not make sense,
        try to increase `drift`.

        Note that by default repeated sequential changes, i.e., changes that have
        the same beginning (`tai`) are not deleted because the changes were
        detected by the alarm (`ta`) at different instants. This is how the
        classical CUSUM algorithm operates.

        If you want to delete the repeated sequential changes and keep only the
        beginning of the first sequential change, set the parameter `ending` to
        True. In this case, the index of the ending of the change (`taf`) and the
        amplitude of the change (or of the total amplitude for a repeated
        sequential change) are calculated and only the first change of the repeated
        sequential changes is kept. In this case, it is likely that `ta`, `tai`,
        and `taf` will have less values than when `ending` was set to False.

        See this IPython Notebook [2]_.

        References
        ----------
        .. [1] Gustafsson (2000) Adaptive Filtering and Change Detection.
        .. [2] http://nbviewer.ipython.org/github/demotu/BMC/blob/master/notebooks/DetectCUSUM.ipynb

        Examples
        --------

        from detect_cusum import detect_cusum
        x = np.random.randn(300)/5
        x[100:200] += np.arange(0, 4, 4/100)
        ta, tai, taf, amp = detect_cusum(x, 2, .02, True, True)

        x = np.random.randn(300)
        x[100:200] += 6
        detect_cusum(x, 20, 10, True, True)

        x = 2*np.sin(2*np.pi*np.arange(0, 3, .01))
        ta, tai, taf, amp = detect_cusum(x, 1, .05, True, True)
        """

        x = np.atleast_1d(x).astype('float64')
        gp, gn = np.zeros(x.size), np.zeros(x.size)
        ta, tai, taf = np.array([[], [], []], dtype=int)
        tap, tan = 0, 0
        amp = np.array([])
        # Find changes (online form)
        for i in range(1, x.size):
            s = x[i] - x[i - 1]
            gp[i] = gp[i - 1] + s - drift  # cumulative sum for + change
            gn[i] = gn[i - 1] - s - drift  # cumulative sum for - change
            if gp[i] < 0:
                gp[i], tap = 0, i
            if gn[i] < 0:
                gn[i], tan = 0, i
            if gp[i] > threshold or gn[i] > threshold:  # change detected!
                ta = np.append(ta, i)  # alarm index
                tai = np.append(tai, tap if gp[i] > threshold else tan)  # start
                gp[i], gn[i] = 0, 0  # reset alarm
        # THE CLASSICAL CUSUM ALGORITHM ENDS HERE

        # Estimation of when the change ends (offline form)
        if tai.size and ending:
            _, tai2, _, _ = self.detect_cusum(x[::-1], threshold, drift, show=False)
            taf = x.size - tai2[::-1] - 1
            # Eliminate repeated changes, changes that have the same beginning
            tai, ind = np.unique(tai, return_index=True)
            ta = ta[ind]
            # taf = np.unique(taf, return_index=False)  # corect later
            if tai.size != taf.size:
                if tai.size < taf.size:
                    taf = taf[[np.argmax(taf >= i) for i in ta]]
                else:
                    ind = [np.argmax(i >= ta[::-1]) - 1 for i in taf]
                    ta = ta[ind]
                    tai = tai[ind]
            # Delete intercalated changes (the ending of the change is after
            # the beginning of the next change)
            ind = taf[:-1] - tai[1:] > 0
            if ind.any():
                ta = ta[~np.append(False, ind)]
                tai = tai[~np.append(False, ind)]
                taf = taf[~np.append(ind, False)]
            # Amplitude of changes
            amp = x[taf] - x[tai]

        if show:
            self._plot(x, threshold, drift, ending, ax, ta, tai, taf, gp, gn)

        return ta, tai, taf, amp

    def _plot(self, x, threshold, drift, ending, ax, ta, tai, taf, gp, gn):
        """Plot results of the detect_cusum function, see its help."""

        try:
            import matplotlib.pyplot as plt
        except ImportError:
            print('matplotlib is not available.')
        else:
            if ax is None:
                _, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 6))

            t = range(x.size)
            ax1.plot(t, x, 'b-', lw=2)
            if len(ta):
                ax1.plot(tai, x[tai], '>', mfc='g', mec='g', ms=10,
                         label='Start')
                if ending:
                    ax1.plot(taf, x[taf], '<', mfc='g', mec='g', ms=10,
                             label='Ending')
                ax1.plot(ta, x[ta], 'o', mfc='r', mec='r', mew=1, ms=5,
                         label='Alarm')
                ax1.legend(loc='best', framealpha=.5, numpoints=1)
            ax1.set_xlim(-.01 * x.size, x.size * 1.01 - 1)
            ax1.set_xlabel('Data #', fontsize=14)
            ax1.set_ylabel('Amplitude', fontsize=14)
            ymin, ymax = x[np.isfinite(x)].min(), x[np.isfinite(x)].max()
            yrange = ymax - ymin if ymax > ymin else 1
            ax1.set_ylim(ymin - 0.1 * yrange, ymax + 0.1 * yrange)
            ax1.set_title('Time series and detected changes ' +
                          '(threshold= %.3g, drift= %.3g): N changes = %d'
                          % (threshold, drift, len(tai)))
            ax2.plot(t, gp, 'y-', label='+')
            ax2.plot(t, gn, 'm-', label='-')
            ax2.set_xlim(-.01 * x.size, x.size * 1.01 - 1)
            ax2.set_xlabel('Data #', fontsize=14)
            ax2.set_ylim(-0.01 * threshold, 1.1 * threshold)
            ax2.axhline(threshold, color='r')
            ax1.set_ylabel('Amplitude', fontsize=14)
            ax2.set_title('Time series of the cumulative sums of ' +
                          'positive and negative changes')
            ax2.legend(loc='best', framealpha=.5, numpoints=1)
            plt.tight_layout()
            plt.show()


import numpy as np
import cv2

# Setting up framework for determining radius of skittles

def loadImage(filepath):
	 return cv2.imread(filepath)

def convertHSV(frame=None):
	return cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)

def GaussianBlur(frame=None):
	return cv2.blur(frame, (10, 10))

# Not currently being used.
def BinaryThreshold(frame=None):
	ret, thresh = cv2.threshold(frame, 127, 255, cv2.THRESH_BIN) # Use THRESH_TRUN?
	return thresh

def findCircles(frame=None):
	circles = None

	# Tweak these parameters
	circles = cv2.HoughCircles(frame, cv2.HOUGH_GRADIENT, 1, 20, param1=50, param2=30, minRadius=40, maxRadius=100)
	return circles

if __name__ == "__main__":

	filepath = "/home/cswain/Documents/skittles.jpg"
	img = loadImage(filepath)

	# Convert from BGR color channels to HSV
	hsv_img = convertHSV(img)

	# Split HSV image into different color channels
	h, s, v = cv2.split(hsv_img)

	# Apply Gaussian Blur to value
	smooth_img = GaussianBlur(s)

	circles = findCircles(smooth_img)

	print("There are: " + str(len(circles[0])) + " circles.")
	print("There are 26 skittles in the image")

	if circles.any():
		for i in circles[0,:]:
			# Draw outer circle
			cv2.circle(img, (i[0], i[1]), i[2], (0, 0, 255), 4)

			#Draw center of circle
			cv2.circle(img, (i[0], i[1]), 2, (0, 0, 255), 4)

	# Resize images to fit on screen
	img = cv2.resize(img, (0,0), None, .25, .25)
	s = cv2.resize(s, (0,0), None, .25, .25)
	smooth_img = cv2.resize(smooth_img, (0,0), None, .25, .25)

	# Can't concatenate img because it has 3 color dimensions
	horizontal_img_concat = np.concatenate((s, smooth_img), axis=1)

	cv2.imshow("Original photo", img)
	cv2.imshow("Horizontal Concatenation", horizontal_img_concat)
	cv2.waitKey(0)
	# cv2.imshow(hsv_img)

	cv2.destroyAllWindows()