import json
import logging
import re
import xml.etree.ElementTree as ET
from io import StringIO

from requests import __build__ as requests_version
from svtplay_dl.utils.http import get_full_url
from svtplay_dl.utils.http import HTTP
from svtplay_dl.utils.output import output
from svtplay_dl.utils.text import decode_html_entities

class subtitle:
    def __init__(self, config, subtype, url, subfix=None, **kwargs):
        self.url = url
        self.subtitle = None
        self.config = config
        self.subtype = subtype
        self.http = HTTP(config)
        self.subfix = subfix
        self.bom = False
        self.output = kwargs.pop("output", None)
        self.kwargs = kwargs

    def __repr__(self):
        return "<Subtitle(type={}, url={}>".format(self.subtype, self.url)

    def download(self):
        subdata = self.http.request("get", self.url)
        if subdata.status_code != 200:
            logging.warning("Can't download subtitle file")
            return

        data = None
        if "mtgx" in self.url and subdata.content[:3] == b"\xef\xbb\xbf":
            subdata.encoding = "utf-8"
            self.bom = True

        if self.subtype == "tt":
            data = self.tt(subdata)
        if self.subtype == "json":
            data = self.json(subdata)
        if self.subtype == "sami":
            data = self.sami(subdata)
        if self.subtype == "smi":
            data = self.smi(subdata)
        if self.subtype == "wrst":
            if "tv4play" in self.url and subdata.content[:3] == b"\xef\xbb\xbf":
                self.bom = True
            subdata.encoding = subdata.apparent_encoding
            data = self.wrst(subdata)
        if self.subtype == "wrstsegment":
            data = self.wrstsegment(subdata)
        if self.subtype == "raw":
            data = self.raw(subdata)

        if self.subfix:
            if self.config.get("get_all_subtitles"):
                if self.output["episodename"]:
                    self.output["episodename"] = "{}-{}".format(self.output["episodename"], self.subfix)
                else:
                    self.output["episodename"] = self.subfix

        if self.config.get("get_raw_subtitles"):
            subdata = self.raw(subdata)
            self.save_file(subdata, self.subtype)

        self.save_file(data, "srt")

    def save_file(self, data, subtype):
        file_d = output(self.output, self.config, subtype, mode="w", encoding="utf-8")
        if hasattr(file_d, "read") is False:
            return
        file_d.write(data)
        file_d.close()

    def raw(self, subdata):
        return subdata.text

    def tt(self, subdata):
        i = 1
        data = ""
        subs = subdata.text

        subdata = re.sub(' xmlns="[^"]+"', "", subs, count=1)
        tree = ET.XML(subdata)
        xml = tree.find("body").find("div")
        plist = list(xml.findall("p"))
        for node in plist:
            tag = norm(node.tag)
            if tag == "p" or tag == "span":
                begin = node.attrib["begin"]
                if not ("dur" in node.attrib):
                    duration = node.attrib["duration"]
                else:
                    duration = node.attrib["dur"]
                if not ("end" in node.attrib):
                    begin2 = begin.split(":")
                    duration2 = duration.split(":")
                    try:
                        sec = float(begin2[2]) + float(duration2[2])
                    except ValueError:
                        sec = 0.000
                    end = "%02d:%02d:%06.3f" % (int(begin2[0]), int(begin2[1]), sec)
                else:
                    end = node.attrib["end"]
                data += "{}\n{} --> {}\n".format(i, begin.replace(".", ","), end.replace(".", ","))
                data = tt_text(node, data)
                data += "\n"
                i += 1

        return data

    def json(self, subdata):
        data = json.loads(subdata.text)
        number = 1
        subs = ""
        for i in data:
            subs += "{}\n{} --> {}\n".format(number, timestr(int(i["startMillis"])), timestr(int(i["endMillis"])))
            subs += "%s\n\n" % i["text"]
            number += 1

        return subs

    def sami(self, subdata):
        text = subdata.text
        text = re.sub(r"&", "&", text)
        tree = ET.fromstring(text)
        allsubs = tree.findall(".//Subtitle")
        subs = ""
        increase = 0
        for sub in allsubs:
            try:
                number = int(sub.attrib["SpotNumber"])
            except ValueError:
                number = int(re.search(r"(\d+)", sub.attrib["SpotNumber"]).group(1))
                increase += 1
            n = number + increase

            texts = sub.findall(".//Text")
            all = ""
            for text in texts:
                line = ""
                for txt in text.itertext():
                    line += "{}".format(txt)
                all += "{}\n".format(decode_html_entities(line.lstrip()))
            subs += "{}\n{} --> {}\n{}\n".format(n, timecolon(sub.attrib["TimeIn"]), timecolon(sub.attrib["TimeOut"]), all)
        subs = re.sub("&", r"&", subs)
        return subs

    def smi(self, subdata):
        if requests_version < 0x20300:
            subdata = subdata.content.decode("latin")
        else:
            subdata.encoding = "ISO-8859-1"
            subdata = subdata.text
        ssubdata = StringIO(subdata)
        timea = 0
        number = 1
        data = None
        subs = ""
        TAG_RE = re.compile(r"<(?!\/?i).*?>")
        bad_char = re.compile(r"\x96")
        for i in ssubdata.readlines():
            i = i.rstrip()
            sync = re.search(r"<SYNC Start=(\d+)>", i)
            if sync:
                if int(sync.group(1)) != int(timea):
                    if data and data != " ":
                        subs += "{}\n{} --> {}\n".format(number, timestr(timea), timestr(sync.group(1)))
                        text = "%s\n" % TAG_RE.sub("", data.replace("
", "\n"))
                        text = decode_html_entities(text)
                        if text[len(text) - 2] != "\n":
                            text += "\n"
                        subs += text
                        number += 1
                timea = sync.group(1)
            text = re.search("
(.*)", i)
            if text:
                data = text.group(1)
        recomp = re.compile(r"\r")
        text = bad_char.sub("-", recomp.sub("", subs))
        return text

    def wrst(self, subdata):
        ssubdata = StringIO(subdata.text)
        srt = ""
        subtract = False
        number_b = 1
        number = 0
        block = 0
        subnr = False

        for i in ssubdata.readlines():
            match = re.search(r"^[\r\n]+", i)
            match2 = re.search(r"([\d:\.]+ --> [\d:\.]+)", i)
            match3 = re.search(r"^(\d+)\s", i)
            if i[:6] == "WEBVTT":
                continue
            elif "X-TIMESTAMP" in i:
                continue
            elif match and number_b == 1 and self.bom:
                continue
            elif match and number_b > 1:
                block = 0
                srt += "\n"
            elif match2:
                if not subnr:
                    srt += "%s\n" % number_b
                matchx = re.search(r"(?P
\d+):(?P<m1>\d+):(?P<s1>[\d\.]+) --> (?P
\d+):(?P<m2>\d+):(?P<s2>[\d\.]+)", i)
                if matchx:
                    hour1 = int(matchx.group("h1"))
                    hour2 = int(matchx.group("h2"))
                    if int(number) == 1:
                        if hour1 > 9:
                            subtract = True
                    if subtract:
                        hour1 -= 10
                        hour2 -= 10
                else:
                    matchx = re.search(r"(?P<m1>\d+):(?P<s1>[\d\.]+) --> (?P<m2>\d+):(?P<s2>[\d\.]+)", i)
                    hour1 = 0
                    hour2 = 0
                time = "{:02d}:{}:{} --> {:02d}:{}:{}\n".format(
                    hour1,
                    matchx.group("m1"),
                    matchx.group("s1").replace(".", ","),
                    hour2,
                    matchx.group("m2"),
                    matchx.group("s2").replace(".", ","),
                )
                srt += time
                block = 1
                subnr = False
                number_b += 1

            elif match3 and block == 0:
                number = match3.group(1)
                srt += "%s\n" % number
                subnr = True
            else:
                if self.config.get("convert_subtitle_colors"):
                    colors = {
                        "30": "#000000",
                        "31": "#ff0000",
                        "32": "#00ff00",
                        "33": "#ffff00",
                        "34": "#0000ff",
                        "35": "#ff00ff",
                        "36": "#00ffff",
                        "37": "#ffffff",
                        "c.black": "#000000",
                        "c.red": "#ff0000",
                        "c.green": "#00ff00",
                        "c.yellow": "#ffff00",
                        "c.blue": "#0000ff",
                        "c.magneta": "#ff00ff",
                        "c.cyan": "#00ffff",
                        "c.gray": "#ffffff",
                    }
                    sub = i
                    for tag, color in colors.items():
                        regex1 = "<" + tag + ">"
                        replace = ''
                        sub = re.sub(regex1, replace, sub)

                    sub = re.sub("</.+>", "", sub)
                else:
                    sub = re.sub("<[^>]*>", "", i)
                srt += sub.strip()
                srt += "\n"
        srt = decode_html_entities(srt)
        return srt

    def wrstsegment(self, subdata):
        time = 0
        subs = []
        for i in self.kwargs["m3u8"].media_segment:
            itemurl = get_full_url(i["URI"], self.url)
            cont = self.http.get(itemurl)
            if "cmore" in self.url:
                cont.encoding = "utf-8"
            text = cont.text.split("\n")
            for t in text:  # is in text[1] for tv4play, but this should be more future proof
                if "X-TIMESTAMP-MAP=MPEGTS" in t:
                    time = float(re.search(r"X-TIMESTAMP-MAP=MPEGTS:(\d+)", t).group(1)) / 90000 - 10
            text = text[3 : len(text) - 2]
            itmes = []
            if len(text) > 1:
                for n in text:
                    if n:  # don't get the empty lines.
                        itmes.append(n)

            several_items = False
            skip = False
            sub = []

            for x in range(len(itmes)):
                item = itmes[x]
                if strdate(item) and len(subs) > 0 and itmes[x + 1] == subs[-1][1]:
                    ha = strdate(subs[-1][0])
                    ha3 = strdate(item)
                    second = str2sec(ha3.group(2)) + time
                    subs[-1][0] = "{} --> {}".format(ha.group(1), sec2str(second))
                    skip = True
                    continue
                has_date = strdate(item)
                if has_date:
                    if several_items:
                        subs.append(sub)
                        sub = []
                    skip = False
                    first = str2sec(has_date.group(1)) + time
                    second = str2sec(has_date.group(2)) + time
                    sub.append("{} --> {}".format(sec2str(first), sec2str(second)))
                    several_items = True
                elif has_date is None and skip is False:
                    sub.append(item)

            if sub:
                subs.append(sub)
        string = ""
        nr = 1
        for sub in subs:
            string += "{}\n{}\n\n".format(nr, "\n".join(sub))
            nr += 1

        return string

def timestr(msec):
    """
    Convert a millisecond value to a string of the following
    format:

        HH:MM:SS,SS

    with 10 millisecond precision. Note the , seperator in
    the seconds.
    """
    sec = float(msec) / 1000

    hours = int(sec / 3600)
    sec -= hours * 3600

    minutes = int(sec / 60)
    sec -= minutes * 60

    output = "%02d:%02d:%06.3f" % (hours, minutes, sec)
    return output.replace(".", ",")

def timecolon(data):
    match = re.search(r"(\d+:\d+:\d+):(\d+)", data)
    return "{},{}".format(match.group(1), match.group(2))

def norm(name):
    if name[0] == "{":
        _, tag = name[1:].split("}")
        return tag
    else:
        return name

def tt_text(node, data):
    if node.text:
        data += "%s\n" % node.text.strip(" \t\n\r")
    for i in node:
        if i.text:
            data += "%s\n" % i.text.strip(" \t\n\r")
        if i.tail:
            text = i.tail.strip(" \t\n\r")
            if text:
                data += "%s\n" % text
    return data

def strdate(datestring):
    match = re.search(r"^(\d+:\d+:[\.0-9]+) --> (\d+:\d+:[\.0-9]+)", datestring)
    return match

def sec2str(seconds):
    m, s = divmod(seconds, 60)
    h, m = divmod(m, 60)
    return "{:02d}:{:02d}:{:06.3f}".format(int(h), int(m), s)

def str2sec(string):
    return sum(x * float(t) for x, t in zip([3600, 60, 1], string.split(":")))


"""
    Author        : Sravya Motepalli

    Date created  : 06/01/2022

    Functionality : PORTFOLIO PROGRAMMING ASSIGNMENT - IMPROVING THE STOCK PROBLEM WITH ADDITIONAL FUNCTIONALITY

"""
# Import required libraries
import sqlite3
import plotly.express as px
import csv
import pandas as pd
import json
from datetime import datetime

# Establishing Database Connection
from pandas import read_csv
dbPath = r'C:\Users\shrav\Documents\Python_files_classes\Stocks_json.db'

try:
    connection = sqlite3.connect(dbPath)
    cursor = connection.cursor()

except:
    print('failed to connect to database')

# Creating a dataset using pandas from json file
filepath = r'C:\Users\shrav\Documents\Python_files_classes\AllStocks.json'
with open(filepath) as json_file:
    dataset = json.load(json_file)

# Creating a class for stocks and to calculate profit or loss
class Stock:
    def __init__(self, ID, symbol, quantity, purchasePrice, currentPrice, purchaseDate):
        self.ID = ID
        self.symbol = symbol
        self.quantity = quantity
        self.purchasePrice = purchasePrice
        self.currentPrice = currentPrice
        self.purchaseDate = purchaseDate
        self.ShareGainLoss = round((self.currentPrice - self.purchasePrice), 2)
        self.ProfitLoss = round((self.ShareGainLoss * self.quantity), 2)

# Creating Class for creating database table and exporting data into it
class create_data:
    def __init__(self, symbol, date, value):
        self.symbol = symbol
        self.date = date
        self.value = value

    def create_table(self):
        stock_table = " CREATE TABLE IF NOT EXISTS " + self.symbol
        stock_table = stock_table + "( Date date PRIMARY KEY," + self.symbol + " real NOT NULL ) "
        cursor.execute(stock_table)

    def insert_data(self):
        insert_data = 'INSERT or IGNORE INTO ' + self.symbol + " VALUES ('"
        insert_data = insert_data + str(self.date)
        insert_data = insert_data + " ',' " + str(self.value) + "' );"
        cursor.execute(insert_data)

#Reading CSV file and creating symbol list
workspace = r'C:\Users\shrav\PycharmProjects\pythonProject'
Stock_File = workspace + '\\' + 'Lesson6_Data_Stocks.csv'
Read_Stocks = open(Stock_File, 'r')
data = read_csv("Lesson6_Data_Stocks.csv")
stockSymbols = data['SYMBOL'].tolist()

# looping to remove '-' in the list
for symbol in stockSymbols:
    if symbol == "RDS-A":
        index = stockSymbols.index('RDS-A')
        stockSymbols.remove("RDS-A")
        stockSymbols.insert(index, 'RDSA')

#Reading CSV file and creating dictinory list
stock_dict = {}
with open('Lesson6_Data_Stocks.csv', mode='r') as inp:
    reader = csv.reader(inp)
    next(reader)
    stock_dict = {rows[0]:int(rows[1]) for rows in reader}
    stock_dict["RDSA"] = stock_dict.pop("RDS-A")

# Create a loop to create individual stock tables
for data in dataset:
    try:
        stockDate = datetime.strptime(data['Date'], '%d-%b-%y')
    except ValueError:
        print('Date format of this stock is not correct')

#writing a if else condition to make sure file reads correctly
    if data['Symbol'] == 'RDS-A':
        stockVal = round(data['Close'] * stock_dict['RDSA'], 2)
        newStock = create_data('RDSA', stockDate, stockVal)
        newStock.create_table()
        newStock.insert_data()
    else:
        stockVal = round(data['Close'] * stock_dict[data['Symbol']], 2)
        newStock = create_data(data['Symbol'], stockDate, stockVal)
        newStock.create_table()
        newStock.insert_data()

# Merge all the individual stocks into single dataframe
select_dates = "SELECT Date FROM AIG"
df_all_stocks = pd.read_sql_query(select_dates, connection)
for stock in stockSymbols:
    select_table = "SELECT * from " + stock
    df_name = stock + '_df'
    df_name = pd.read_sql_query(select_table, connection)
    df_all_stocks = pd.merge(df_all_stocks, df_name, how='left', on=["Date"])

# Plot the graph using Plotly
fig = px.line(df_all_stocks, x="Date", y=df_all_stocks.columns,
              hover_data={"Date": "|%Y,%m,%d"},
              title=' Stock Portfolio')
fig.update_xaxes(dtick="M1", tickformat="%b\n%Y")
fig.show()

# Commit and Close Database
connection.commit()
connection.close()


"""
Helper functions for dealing with calendar dates.
"""
import datetime as dtime
from datetime import timedelta, tzinfo
from functools import total_ordering

import numpy as np
import pandas as pd
import pytz
import xarray as xr

INC_ONE_DAY = timedelta(hours=24)

def is_leap_year(year):
    """
    Is the given year a leap year.
    """
    return year % 4 == 0

def days_per_month(month, is_leap):
    """
    Get number of days in given month.

    Accounts for extra day in feb during leap year.
    """
    if is_leap:
        month_arr = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    else:
        month_arr = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    return month_arr[month - 1]

def date_range(start_date, end_date=None, increment=timedelta(hours=24)):
    """
    Generate all dates between start and end date (inclusive lower, exclusive upper).

    Can use date or datetime.
    """
    if not end_date:
        while True:
            yield start_date
            start_date += increment
    else:
        while start_date < end_date:
            yield start_date
            start_date += increment

def date_range_days(start_date, end_date):
    for n in range(int((end_date - start_date).days)):
        yield start_date + dtime.timedelta(n)

def date_range_months(start_date, end_date):
    """Iterate through months in date range (inclusive)."""

    cur_month, cur_year = start_date.month, start_date.year
    end_month, end_year = end_date.month, end_date.year

    while (cur_month != end_month) or (cur_year != end_year):
        yield dtime.date(cur_year, cur_month, 1)

        cur_year += cur_month == 12
        cur_month = np.remainder(cur_month, 12) + 1

    yield dtime.date(end_year, end_month, 1)

def filter_fire_season(ds, start=(5, 14), end=(8, 31), years=range(2007, 2016 + 1)):
    dates = ds.time.values
    ind = np.zeros(dates.shape, dtype=np.bool)
    for year in years:
        start_date = np.datetime64(dtime.date(year, start[0], start[1]))
        end_date = np.datetime64(dtime.date(year, end[0], end[1]))
        ind = ind | ((dates >= start_date) & (dates <= end_date))

    data_vars = {}
    for k in ds.data_vars.keys():
        data_vars[k] = (('y', 'x', 'time'), np.array(ds[k].values)[:, :, ind], ds[k].attrs)

    new_ds = xr.Dataset(data_vars, coords={'time': ds.time.values[ind]}, attrs=ds.attrs)

    return new_ds

def create_true_dates(start_date, end_date, times, offsets):
    """
    Used to build list of datetime and offsets for a range of dates. Returns datetime64 dates.

    Inclusive of dates.
    """
    dates = list(date_range_days(start_date, end_date + dtime.timedelta(1)))
    times = list(map(lambda x: dtime.time(x), times))
    offsets = list(map(lambda x: dtime.timedelta(hours=x), offsets))

    true_dates, true_offsets = zip(*[(dtime.datetime.combine(d, t), o) for d in dates for t in times for o in offsets])

    true_dates = pd.to_datetime(true_dates)
    return true_dates, np.array(true_offsets)

def round_to_nearest_multiple(x, multiple):
    return multiple * round(x / multiple)

def round_to_nearest_quarter_hour(x):
    return round_to_nearest_multiple(x, .25)

def utc_to_local_time_offset(lon, round_func=round_to_nearest_quarter_hour):
    """
    Returns utc to local offset time in hours.

    By default rounds to nearest quarter hour.
    """
    offset = (lon / 15.)

    if round_func:
        offset = round_func(offset)

    return timedelta(hours=offset)

def utc_to_local_time(datetime_utc, lon, round_func=None):
    """
    Calculate local time based on longitude and utc time.

    By default rounds to nearest quarter hour.
    """
    tz = TrulyLocalTzInfo(lon, round_func)

    return datetime_utc.astimezone(tz)

class TrulyLocalTzInfo(tzinfo):
    def __init__(self, lon, round_func=None):
        super(TrulyLocalTzInfo, self).__init__()
        self.lon = lon
        self.round_func = round_func

    def utcoffset(self, dt):
        offset = utc_to_local_time_offset(self.lon, self.round_func) + self.dst(dt)
        return offset

    def dst(self, dt):
        return timedelta(0)

    def tzname(self, dt):
        return 'local'

@total_ordering
class DatetimeMeasurement(object):
    """
    Tracks a datetime with an offset (usually used for integrated measurements) and supports conversion to truly local
    time.

    Stores all datetime in UTC local. Supports using lon for a truly localized offset
    """

    def __init__(self, dt, td_offset=timedelta(0)):
        # Check that datetime dt is timezone aware
        if dt.tzinfo is None or dt.tzinfo.utcoffset(dt) is None:
            raise ValueError('Datetime object must be timezone aware')

        self.dt = dt.astimezone(pytz.UTC)
        self.td_offset = td_offset

    def get(self, lon=None):
        """
        Return datetime. Can be truly localized if lat/lon are provided.

        WARNING: Cannot pickle a truly localized datetime because it relies on a tzinfo which requires init() arguments.
        """
        if lon:
            return self.dt.astimezone(self.get_localized_tz(lon))

        return self.dt

    def get_offset(self):
        return self.td_offset

    @staticmethod
    def get_localized_tz(lon):
        if lon is None:
            raise ValueError('lon must be defined to get a truly localized tzinfo')

        return TrulyLocalTzInfo(lon)

    def __eq__(self, other):
        return self.get() == other.get()

    def __lt__(self, other):
        return self.get() < other.get()

def day2monthday(my_day, leap_year=False):
    if leap_year:
        month_arr = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    else:
        month_arr = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    month = 1
    days_left = my_day
    while days_left >= month_arr[month - 1]:
        days_left -= month_arr[month - 1]
        month += 1
    day = days_left + 1
    return month, day

def monthday2day(month, day, leap_year=False):
    """Convert month/day into days since Jan 1"""
    if leap_year:
        month_arr = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    else:
        month_arr = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    days = 0
    for mon in range(1, month):
        days += month_arr[mon - 1]
    days += day - 1
    return days

def increment_day(year, month, day):
    if year % 4:  # not leap year
        days_arr = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]  # Days in a month
    else:  # leap year
        days_arr = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]  # Days in a month

    if day == days_arr[month - 1]:
        day = 1
        month += 1
        if month == 13:
            month = 1
            year += 1
    else:
        day += 1

    return year, month, day

def day_of_year_from_datetime(dt_):
    return dt_.timetuple().tm_yday


import argparse
import functools
import json
import logging
import os
import random
import sys
from systemd.journal import JournalHandler
from io import BytesIO

import bottle
from bottle import (
    response,
    request,
    jinja2_view,
    static_file,
    redirect,
    Bottle,
)
from PIL import Image, ImageDraw, ImageFont

from brother_ql.devicedependent import label_type_specs, label_sizes
from brother_ql.devicedependent import ENDLESS_LABEL, DIE_CUT_LABEL, ROUND_DIE_CUT_LABEL
from brother_ql import BrotherQLRaster, create_label
from brother_ql.backends import backend_factory, guess_backend

from .font_helpers import get_fonts

bottle.TEMPLATE_PATH.insert(0, os.getenv("VIEWS_PATH"))

logger = logging.getLogger(__name__)

LABEL_SIZES = [(name, label_type_specs[name]["name"]) for name in label_sizes]

CONFIG = {}

# https://stackoverflow.com/a/31093434
def log_to_logger(fn):
    """Wrap a Bottle request so that a log line is emitted after it's handled."""

    @functools.wraps(fn)
    def _log_to_logger(*args, **kwargs):
        logger = logging.getLogger("bottle")
        actual_response = fn(*args, **kwargs)
        logger.debug(
            f"{request.remote_addr} {request.method} {request.url} {response.status}"
        )
        return actual_response

    return _log_to_logger

app = Bottle()
app.install(log_to_logger)

@app.route("/")
def index():
    redirect("/labeldesigner")

@app.route("/static/<filename:path>")
def serve_static(filename):
    return static_file(filename, root=os.getenv("STATIC_PATH"))

@app.route("/labeldesigner")
@jinja2_view("labeldesigner.jinja2")
def labeldesigner():
    font_family_names = sorted(list(FONTS.keys()))
    return {
        "font_family_names": font_family_names,
        "fonts": FONTS,
        "label_sizes": LABEL_SIZES,
        "website": CONFIG["WEBSITE"],
        "label": CONFIG["LABEL"],
    }

def get_label_context(request):
    """might raise LookupError()"""

    d = request.params.decode()  # UTF-8 decoded form data

    font_family = d.get("font_family").rpartition("(")[0].strip()
    font_style = d.get("font_family").rpartition("(")[2].rstrip(")")
    context = {
        "text": d.get("text", None),
        "font_size": int(d.get("font_size", 100)),
        "font_family": font_family,
        "font_style": font_style,
        "label_size": d.get("label_size", "62"),
        "kind": label_type_specs[d.get("label_size", "62")]["kind"],
        "margin": int(d.get("margin", 10)),
        "threshold": int(d.get("threshold", 70)),
        "align": d.get("align", "center"),
        "orientation": d.get("orientation", "standard"),
        "margin_top": float(d.get("margin_top", 24)) / 100.0,
        "margin_bottom": float(d.get("margin_bottom", 45)) / 100.0,
        "margin_left": float(d.get("margin_left", 35)) / 100.0,
        "margin_right": float(d.get("margin_right", 35)) / 100.0,
    }
    context["margin_top"] = int(context["font_size"] * context["margin_top"])
    context["margin_bottom"] = int(context["font_size"] * context["margin_bottom"])
    context["margin_left"] = int(context["font_size"] * context["margin_left"])
    context["margin_right"] = int(context["font_size"] * context["margin_right"])

    context["fill_color"] = (255, 0, 0) if "red" in context["label_size"] else (0, 0, 0)

    def get_font_path(font_family_name, font_style_name):
        try:
            if font_family_name is None or font_style_name is None:
                font_family_name = CONFIG["LABEL"]["DEFAULT_FONTS"]["family"]
                font_style_name = CONFIG["LABEL"]["DEFAULT_FONTS"]["style"]
            font_path = FONTS[font_family_name][font_style_name]
        except KeyError:
            raise LookupError("Couln't find the font & style")
        return font_path

    context["font_path"] = get_font_path(context["font_family"], context["font_style"])

    def get_label_dimensions(label_size):
        try:
            ls = label_type_specs[context["label_size"]]
        except KeyError:
            raise LookupError("Unknown label_size")
        return ls["dots_printable"]

    width, height = get_label_dimensions(context["label_size"])
    if height > width:
        width, height = height, width
    if context["orientation"] == "rotated":
        height, width = width, height
    context["width"], context["height"] = width, height

    return context

def create_label_im(text, **kwargs):
    label_type = kwargs["kind"]
    im_font = ImageFont.truetype(kwargs["font_path"], kwargs["font_size"])
    im = Image.new("L", (20, 20), "white")
    draw = ImageDraw.Draw(im)
    # workaround for a bug in multiline_textsize()
    # when there are empty lines in the text:
    lines = []
    for line in text.split("\n"):
        if line == "":
            line = " "
        lines.append(line)
    text = "\n".join(lines)
    # TODO: this was assigned but never used
    # linesize = im_font.getsize(text)
    textsize = draw.multiline_textsize(text, font=im_font)
    width, height = kwargs["width"], kwargs["height"]
    if kwargs["orientation"] == "standard":
        if label_type in (ENDLESS_LABEL,):
            height = textsize[1] + kwargs["margin_top"] + kwargs["margin_bottom"]
    elif kwargs["orientation"] == "rotated":
        if label_type in (ENDLESS_LABEL,):
            width = textsize[0] + kwargs["margin_left"] + kwargs["margin_right"]
    im = Image.new("RGB", (width, height), "white")
    draw = ImageDraw.Draw(im)
    if kwargs["orientation"] == "standard":
        if label_type in (DIE_CUT_LABEL, ROUND_DIE_CUT_LABEL):
            vertical_offset = (height - textsize[1]) // 2
            vertical_offset += (kwargs["margin_top"] - kwargs["margin_bottom"]) // 2
        else:
            vertical_offset = kwargs["margin_top"]
        horizontal_offset = max((width - textsize[0]) // 2, 0)
    elif kwargs["orientation"] == "rotated":
        vertical_offset = (height - textsize[1]) // 2
        vertical_offset += (kwargs["margin_top"] - kwargs["margin_bottom"]) // 2
        if label_type in (DIE_CUT_LABEL, ROUND_DIE_CUT_LABEL):
            horizontal_offset = max((width - textsize[0]) // 2, 0)
        else:
            horizontal_offset = kwargs["margin_left"]
    offset = horizontal_offset, vertical_offset
    draw.multiline_text(
        offset, text, kwargs["fill_color"], font=im_font, align=kwargs["align"]
    )
    return im

@app.get("/api/preview/text")
@app.post("/api/preview/text")
def get_preview_image():
    context = get_label_context(request)
    im = create_label_im(**context)
    return_format = request.query.get("return_format", "png")
    if return_format == "base64":
        import base64

        response.set_header("Content-type", "text/plain")
        return base64.b64encode(image_to_png_bytes(im))
    else:
        response.set_header("Content-type", "image/png")
        return image_to_png_bytes(im)

def image_to_png_bytes(im):
    image_buffer = BytesIO()
    im.save(image_buffer, format="PNG")
    image_buffer.seek(0)
    return image_buffer.read()

@app.post("/api/print/text")
@app.get("/api/print/text")
def print_text():
    """
    API to print a label

    returns: JSON

    Ideas for additional URL parameters:
    - alignment
    """

    return_dict = {"success": False}

    try:
        context = get_label_context(request)
    except LookupError as e:
        return_dict["error"] = e.msg
        return return_dict

    if context["text"] is None:
        return_dict["error"] = "Please provide the text for the label"
        return return_dict

    im = create_label_im(**context)

    if context["kind"] == ENDLESS_LABEL:
        rotate = 0 if context["orientation"] == "standard" else 90
    elif context["kind"] in (ROUND_DIE_CUT_LABEL, DIE_CUT_LABEL):
        rotate = "auto"

    qlr = BrotherQLRaster(CONFIG["PRINTER"]["MODEL"])
    red = False
    if "red" in context["label_size"]:
        red = True
    create_label(
        qlr,
        im,
        context["label_size"],
        red=red,
        threshold=context["threshold"],
        cut=True,
        rotate=rotate,
    )

    if not DEBUG:
        try:
            be = BACKEND_CLASS(CONFIG["PRINTER"]["PRINTER"])
            be.write(qlr.data)
            be.dispose()
            del be
        except Exception as e:
            return_dict["message"] = str(e)
            logger.warning("Exception happened: %s", e)
            return return_dict

    return_dict["success"] = True
    if DEBUG:
        return_dict["data"] = str(qlr.data)
    return return_dict

def main():
    global DEBUG, FONTS, BACKEND_CLASS, CONFIG
    parser = argparse.ArgumentParser(description="Brother QL WebUI")
    parser.add_argument("config", help="Path to the config file")
    args = parser.parse_args()

    with open(args.config, "r") as f:
        CONFIG = json.load(f)

    loglevel = CONFIG["SERVER"]["LOGLEVEL"]
    if loglevel == "DEBUG":
        DEBUG = True
    else:
        DEBUG = False

    additional_font_folder = CONFIG["SERVER"]["ADDITIONAL_FONT_FOLDER"]

    handler = JournalHandler(SYSLOG_IDENTIFIER="brother_ql_web")
    handler.setLevel(loglevel)
    formatter = logging.Formatter("[{name}] {message}", style="{")
    handler.setFormatter(formatter)

    root_logger = logging.getLogger()
    root_logger.setLevel(logging.DEBUG)
    root_logger.addHandler(handler)

    logger.debug("logging initialized")

    try:
        selected_backend = guess_backend(CONFIG["PRINTER"]["PRINTER"])
    except ValueError:
        parser.error(
            "Couln't guess the backend to use from the printer string descriptor"
        )
    BACKEND_CLASS = backend_factory(selected_backend)["backend_class"]

    if CONFIG["LABEL"]["DEFAULT_SIZE"] not in label_sizes:
        parser.error(
            "Invalid default label size. Please choose on of the following:\n"
            + " ".join(label_sizes)
        )

    FONTS = get_fonts()
    if additional_font_folder:
        FONTS.update(get_fonts(additional_font_folder))

    if not FONTS:
        logger.error("Not a single font was found on your system. Please install some.")
        sys.exit(2)

    for font in CONFIG["LABEL"]["DEFAULT_FONTS"]:
        try:
            FONTS[font["family"]][font["style"]]
            CONFIG["LABEL"]["DEFAULT_FONTS"] = font
            logger.debug(f"Selected the following default font: {font}")
            break
        except Exception:
            pass
    if CONFIG["LABEL"]["DEFAULT_FONTS"] is None:
        logger.error("Could not find any of the default fonts. Choosing a random one.")
        family = random.choice(list(FONTS.keys()))
        style = random.choice(list(FONTS[family].keys()))
        CONFIG["LABEL"]["DEFAULT_FONTS"] = {"family": family, "style": style}
        logger.error(
            "The default font is now set to: {family} ({style})\n".format(
                **CONFIG["LABEL"]["DEFAULT_FONTS"]
            )
        )

    app.run(host=CONFIG["SERVER"]["HOST"], port=CONFIG["SERVER"]["PORT"], quiet=True)


from datetime import datetime, timedelta, timezone
from enum import Enum
from random import SystemRandom
from typing import Any, Dict, List, Optional, Tuple

from mlib.localization import tr

def _t(key: str, language: str = "en", **kwargs):
    return tr("bot.events.Halloween.translations." + key, "en-US", **kwargs)

import sqlalchemy as sa
from MFramework import (
    Attachment,
    Chance,
    Context,
    Embed,
    EventBetween,
    Groups,
    Snowflake,
    User,
    register,
)
from MFramework.commands.cooldowns import CacheCooldown, cooldown
from MFramework.database.alchemy.mixins import ServerID
from mlib.database import Base, Timestamp

from ...database.mixins import UserID
from ...database.types import HalloweenRaces as Race
from ...database.types import Statistic

# When | Where | Who | to Whom | to What | From what?

class HalloweenCooldown(CacheCooldown):
    current_faction: int = 0
    target_faction: int = 0
    remaining_factions: int = 0
    multipler: float = 1.0

    def __init__(
        self,
        ctx: Context,
        cooldown: timedelta,
        cooldown_type: str,
        func_args: Dict[str, Any],
        target_user: Optional[Snowflake] = None,
        *args,
        **kwargs,
    ) -> None:
        super().__init__(ctx=ctx, cooldown=cooldown, cooldown_type=cooldown_type)
        s = func_args.get("session", None)
        this_user = func_args.get("this_user", None)
        self.session = s or self.ctx.db.sql.session()
        # session.query(db.Inventory).filter(db.Inventory.user_id == ctx.user_id, db.Inventory.item_id in list(Race)).first()
        self.race = (
            this_user
            or self.session.query(Halloween.race)
            .filter(Halloween.server_id == ctx.guild_id, Halloween.user_id == ctx.user_id)
            .first()
            or Race.Human
        )
        self.get_race_counts(func_args.get("user", ctx.user).id)

    @property
    def cooldown(self) -> timedelta:
        c = (self._cooldown + self.cooldown_var) * self.multipler
        return c if c.total_seconds() > 300 else timedelta(minutes=5)

    @property
    def is_top_faction(self) -> bool:
        """Whether it's currently a top faction"""
        return (self.remaining_factions // 2) < self.current_faction

    @property
    def faction_difference(self) -> int:
        """Difference to other factions"""
        return self.current_faction - (self.remaining_factions // 2)

    @property
    def cooldown_var(self) -> timedelta:
        """Current Cooldown Variance based on difference to other factions"""
        difference = self.faction_difference
        if not self.is_top_faction:
            difference = difference * 2
        return timedelta(minutes=difference * 3)

    def get_race_counts(self, target_user_id: Snowflake) -> Tuple[int, int, int]:
        """Returns count for current user faction, user's target faction and collectively remaining factions"""
        user = Halloween.fetch_or_add(self.session, server_id=self.ctx.guild_id, user_id=self.ctx.user_id)
        total = Halloween.get_total(self.session, self.ctx.guild_id, user.race)
        if user.race not in IMMUNE_TABLE.keys():
            if user.race is Race.Human:
                return
            try:
                top = sorted(filter(lambda x: x[0] in HUNTERS, total.items()), key=lambda x: x[1])[-1]
            except:
                return
            top_hunted = total.get(CURE_TABLE[top[0]], 1)
            top_hunter = top[1]
            top_ratio = 1 / (top_hunted / top_hunter)
            current_faction = total.get(user.race, 1)
            currently_hunted = total.get(CURE_TABLE[user.race], 1)
            current_ratio = 1 / (currently_hunted / current_faction)
            self.multipler = 1 - (top_ratio - current_ratio)
            return
        self.current_faction = total.get(user.race, 1)

        target_user = Halloween.fetch_or_add(self.session, server_id=self.ctx.guild_id, user_id=target_user_id)
        # Halloween.get_total(session, self.ctx.guild_id, target_user.race)
        self.target_faction = total.get(target_user.race, 1)

        remaining_monsters = list(i for i in IMMUNE_TABLE.keys() if i not in (user.race, target_user.race))
        # Halloween.get_total(session, self.ctx.guild_id, remaining_monsters[0])
        self.remaining_factions = total.get(remaining_monsters[0], 1) + self.target_faction

from MFramework.commands.exceptions import Error

class HalloweenException(Error):
    _key = ""

    def __init__(self, key: str, language: str = "en", *args: object, **kwargs) -> None:
        value = _t(self._key + key, language, default=_t(self._key + "generic", language), **kwargs)
        super().__init__(value, *args)

class Cant(HalloweenException):
    """User has wrong state"""

    _key = "cant_"

class Failed(HalloweenException):
    """Failed to bite or cure"""

    _key = "failed_"

class Immune(HalloweenException):
    """Target is on the same side or is immune"""

    _key = "immune_"

class COOLDOWNS(Enum):
    DEFEND = timedelta(hours=1)
    CURE = timedelta(hours=2)
    BITE = timedelta(hours=3)
    BETRAY = timedelta(hours=4)

IMMUNE_TABLE = {Race.Vampire: Race.Hunter, Race.Werewolf: Race.Huntsmen, Race.Zombie: Race.Enchanter}

HUNTERS = IMMUNE_TABLE.values()
CURE_TABLE = {v: k for k, v in IMMUNE_TABLE.items()}

class DRINKS(Enum):
    Nightmare = "Random"  # Race.Human
    Wine = "Vampire"  # Race.Vampire
    Moonshine = "Werewolf"  # Race.Werewolf
    Vodka = "Zombie"  # Race.Zombie

class Guilds(Enum):
    Any = "Random"
    Dawnguards = "Hunter"  # Race.Hunter
    Rangers = "Huntsmen"  # Race.Huntsmen
    Inquisition = "Enchanter"  # Race.Enchanter

ROLES = {}

class HalloweenLog(ServerID, UserID, Timestamp, Base):
    timestamp: datetime = sa.Column(sa.TIMESTAMP(timezone=True), primary_key=True, server_default=sa.func.now())
    target_id: User = sa.Column(
        sa.ForeignKey("User.id", ondelete="Cascade", onupdate="Cascade"), primary_key=False, nullable=False
    )
    race: Race = sa.Column(sa.Enum(Race))
    previous: Race = sa.Column(sa.Enum(Race))

class Halloween(ServerID, UserID, Base):
    server_id: Snowflake = sa.Column(
        sa.ForeignKey("Server.id", ondelete="Cascade", onupdate="Cascade"), primary_key=True, nullable=False, default=0
    )
    user_id: Snowflake = sa.Column(
        sa.ForeignKey("User.id", ondelete="Cascade", onupdate="Cascade"), primary_key=True, nullable=False, default=0
    )
    race: Race = sa.Column(sa.Enum(Race), default=Race.Human)
    protected: datetime = sa.Column(sa.TIMESTAMP(timezone=True))

    @classmethod
    def fetch_or_add(
        cls: "Halloween", s: sa.orm.Session, user_id: Snowflake, server_id: Snowflake, **kwargs
    ) -> "Halloween":
        from ...database.models import User

        user = User.fetch_or_add(s, id=user_id)
        r = super().fetch_or_add(s, user_id=user_id, server_id=server_id, **kwargs)
        if r.race == None:
            r.race = Race.Human
            s.commit()
        return r

    @classmethod
    def get_total(cls, s: sa.orm.Session, guild_id: Snowflake, race: Race = None) -> Dict[Race, int]:
        q = s.query(sa.func.count(cls.race), cls.race).filter(cls.server_id == guild_id)
        if race:
            q.filter(cls.race == race)
        r = q.group_by(cls.race).all()  # .count()
        return {k: v for v, k in r}

    def turn_another(self, s: sa.orm.Session, target_user: Snowflake, race: Race = None) -> List[Race]:
        if self.race is Race.Human:
            if self.user_id == target_user:
                t = self
                if race in [i.name for i in HUNTERS]:
                    self.protected = datetime.now(tz=timezone.utc) + timedelta(hours=1)
                else:
                    self.protected = datetime.now(tz=timezone.utc) + timedelta(minutes=30)
        else:
            t = Halloween.fetch_or_add(s, server_id=self.server_id, user_id=target_user)

            if t.protected and (t.protected > datetime.now(tz=timezone.utc)):
                raise HalloweenException("error_protected")

            turn_count = (
                s.query(sa.func.count(HalloweenLog.target_id))
                .filter(
                    HalloweenLog.server_id == self.server_id,
                    HalloweenLog.target_id == target_user,
                    HalloweenLog.user_id != HalloweenLog.target_id,
                    HalloweenLog.previous != HalloweenLog.race,
                )
                .first()
            )

            if turn_count and SystemRandom().randint(0, 1000) <= (turn_count[0] if turn_count[0] < 1000 else 999):
                raise HalloweenException("error_self_defend")

        remaining = Halloween.get_total(s, self.server_id, t.race).get(t.race, 1)
        if (
            not race
            and (
                self.race in IMMUNE_TABLE
                and (t.race in IMMUNE_TABLE or IMMUNE_TABLE.get(self.race) == t.race or self.race == t.race)
            )
            or remaining == 1
        ):
            raise Immune("target")
        elif not race and self.race in HUNTERS and CURE_TABLE.get(self.race, None) != t.race:
            raise HalloweenException("error_cure", currentClass=CURE_TABLE.get(self.race))

        if race:
            pass
        elif self.race in HUNTERS:
            race = Race.Human
        elif self.race in IMMUNE_TABLE.keys():
            race = self.race

        previous = t.race
        s.add(
            HalloweenLog(
                server_id=self.server_id, user_id=self.user_id, target_id=target_user, race=race, previous=previous
            )
        )
        t.race = race
        s.commit()
        return previous, race

async def update_user_roles(ctx: Context, user_id: Snowflake, previous_race: Race, new_race: Race, s: sa.orm.Session):
    """Updates roles of user to reflect race change"""
    get_roles(ctx.guild_id, s)
    if type(previous_race) is Race or previous_race in list(i.name for i in Race):
        if type(previous_race) is Race:
            previous_race = previous_race.name
        p_role = ROLES.get(ctx.guild_id, {}).get(previous_race, None)
        if p_role:
            await ctx.bot.remove_guild_member_role(ctx.guild_id, user_id, p_role, "Halloween Minigame")
    if type(new_race) is Race or new_race in list(i.name for i in Race):
        if type(new_race) is Race:
            new_race = new_race.name
        role = ROLES.get(ctx.guild_id, {}).get(new_race, None)
        if role:
            await ctx.bot.add_guild_member_role(ctx.guild_id, user_id, role, "Halloween Minigame")

async def turn(
    ctx: Context,
    s: sa.orm.Session,
    this_user: Halloween,
    target_user_id: Snowflake,
    to_race: Race = None,
    action: str = None,
) -> str:
    """Turns target user into race of invoking user or provided race"""
    p, r = this_user.turn_another(s, target_user_id, to_race)
    await update_user_roles(ctx, target_user_id, p, r, s=s)
    return _t(
        f"success_{action}", ctx.language, author=ctx.user_id, target=target_user_id, currentClass=r, previousClass=p
    )

def get_total(total: List[Statistic]) -> Tuple[int, int]:
    """Returns total bites and population"""
    pass

# Factions
#   reinforce - send reinforcments (turn x% of users on that server into that faction) to another server - only for leading faction on a server!

@register(group=Groups.GLOBAL)
@EventBetween(after_month=10, after_day=14, before_month=11, before_day=7)
async def halloween(ctx: Context) -> Embed:
    """Halloween Event commands"""
    e = Embed()
    e.setTitle("Halloween Biting Minigame")
    e.addField("Monster Rules", _t("rules_main_monsters", language=ctx.language))
    e.addField("Hunter Rules", _t("rules_main_hunters", language=ctx.language))
    e.addField("\u200b", "\u200b")
    e.addField(
        name=_t("rules_drinking_title", language=ctx.language),
        value=(
            "- "
            + _t(
                "rules_drinking",
                language=ctx.language,
                drink_cmd="/halloween humans drink",
                drink_nightmare="Nightmare",
            )
            + "\n- "
            + _t("rules_enlisting", language=ctx.language, enlist_cmd="/halloween humans enlist")
        ),
    )
    e.addField(
        name=_t("rules_joining_title", language=ctx.language),
        value=_t(
            "rules_joining",
            language=ctx.language,
            drink_cmd="/halloween humans drink",
            enlist_cmd="/halloween humans enlist",
        ),
    )
    e.addField(
        name=_t("rules_last_protection_title", language=ctx.language),
        value=_t("rules_last_protection", language=ctx.language),
    )
    e.addField(
        name="Attributions",
        value="Minigame (Biting & Fear) was inspired by event in (now inactive) web browser game called Nightwood that appeared between 2010 and 2014"
        + "\nRole icons were contributed by Johnpaul Leon#0725",
    )
    return e

@register(group=Groups.ADMIN, main=halloween)
async def settings():
    """Shows settings for Halloween Event"""
    pass

def get_roles(guild_id, s):
    from MFramework.database.alchemy import Role
    from MFramework.database.alchemy.types import Setting

    if guild_id not in ROLES:
        ROLES[guild_id] = {}
        roles = s.query(Role).filter(Role.server_id == guild_id, Role.settings.any(name=Setting.Special)).all()
        for role in roles:
            ROLES[guild_id][role.get_setting(Setting.Special)] = role.id
    return ROLES[guild_id]

@register(group=Groups.ADMIN, main=settings)
async def roles(ctx: Context, delete: bool = False, update_permissions: bool = False):
    """Create and/or add roles related to Halloween Event
    Params
    ------
    delete:
        Delete Event roles (*Only ones that were made by bot)
    update_permissions:
        Sets permissions for commands to be only used by appropriate race
    """
    s = ctx.db.sql.session()
    roles = get_roles(ctx.guild_id, s)
    if roles:
        # if update_permissions:
        #    permissions = []
        #    commands = []
        #    for cmd in commands:
        #        command_permissions = []
        #        from MFramework import Application_Command_Permissions, Application_Command_Permission_Type, Guild_Application_Command_Permissions
        #        for role in roles: #TODO: get only roles according to faction by command
        #            command_permissions.append(Application_Command_Permissions(
        #                id=role,
        #                type=Application_Command_Permission_Type.ROLE,
        #                permission=True
        #            ))
        #        if command_permissions != []:
        #            permissions.append(Guild_Application_Command_Permissions(
        #                id = cmd.id,
        #                permissions=command_permissions
        #            ))
        #    await ctx.bot.batch_edit_application_command_permissions(ctx.bot.application.id, ctx.guild_id, permissions)

        users = s.query(Halloween).filter(Halloween.server_id == ctx.guild_id).all()
        for user in users:
            await ctx.bot.add_guild_member_role(
                ctx.guild_id, user.user_id, roles.get(user.race.name), "Halloween Minigame"
            )
    else:
        for race in Race:
            role = await ctx.bot.create_guild_role(
                ctx.guild_id,
                _t(race.name.lower().replace(" ", "_"), ctx.language, count=1).title(),
                0,
                reason="Created Role for Halloween Minigame",
            )
            from MFramework.database.alchemy import Role
            from MFramework.database.alchemy.types import Setting

            r = Role.fetch_or_add(s, server_id=ctx.guild_id, id=role.id)
            r.add_setting(Setting.Special, race.name)
        s.commit()

import functools

def inner(f, races: List[Race], main: object, should_register: bool = True):
    @functools.wraps(f)
    def wrapped(ctx: Context, s: sa.orm.Session = None, this_user: Halloween = None, **kwargs):
        s = s or ctx.db.sql.session()
        user = this_user or Halloween.fetch_or_add(s, server_id=ctx.guild_id, user_id=ctx.user_id)
        if user.race in races:
            return f(ctx=ctx, session=s, this_user=user, **kwargs)
        elif user.race is Race.Human:
            raise Error("You want to do what?")
        raise Cant(f.__name__)

    if should_register:
        register(group=Groups.GLOBAL, main=main)(
            EventBetween(after_month=10, after_day=14, before_month=11, before_day=7)(wrapped)
        )
    return wrapped

##########
# HUMANS #
##########

@register(group=Groups.GLOBAL, main=halloween)
def humans(cls=None, *, should_register: bool = True):
    """Commands related to humans"""
    i = functools.partial(
        inner,
        races=[Race.Human, Race.Vampire, Race.Werewolf, Race.Zombie],
        main=humans,
        should_register=should_register,
    )
    if cls:
        return i(cls)
    return i

@humans
async def enlist(ctx: Context, guild: Guilds, *, session: sa.orm.Session, this_user: Halloween) -> str:
    """Enlist as a hunter
    Params
    ------
    guild:
        Hunter's guild you want to join"""
    if this_user.race is not Race.Human and guild is not Guilds.Any:
        total = Halloween.get_total(session, ctx.guild_id)
        _race = Race.get(guild.value)
        if total.get(_race) > total.get(CURE_TABLE.get(_race)) // 3:
            raise Cant("enlist", ctx.language)
    if guild is Guilds.Any:
        total = sorted(Halloween.get_total(session, ctx.guild_id).items(), key=lambda x: x[1])
        if total:
            for race in total:
                if race[0] in HUNTERS:
                    guild = race[0]
                    break
        else:
            from random import SystemRandom

            guild = SystemRandom().choice([Race.Hunter, Race.Huntsmen, Race.Enchanter])
    return await turn(ctx, session, this_user, ctx.user_id, guild.value, action="enlist")

@humans
async def drink(ctx: Context, type: DRINKS, *, session: sa.orm.Session, this_user: Halloween) -> str:
    """Drink an unknown beverage to become a monster
    Params
    ------
    type:
        Drink you want to drink"""
    if this_user.race is not Race.Human:
        raise Cant("drink", ctx.language)
    previous = (
        session.query(HalloweenLog)
        .filter(HalloweenLog.server_id == ctx.guild_id, HalloweenLog.user_id == ctx.user_id)
        .first()
    )
    if previous and type is not DRINKS.Nightmare:
        return "Sadly, you can choose your poison only first time around as a freshblood. Nightmarish potion is your only option to drink now."

    if type is DRINKS.Nightmare:
        total = sorted(Halloween.get_total(session, ctx.guild_id).items(), key=lambda x: x[1])
        if total:
            for race in total:
                if race[0] in IMMUNE_TABLE:
                    type = race[0]
                    break
        else:
            from random import SystemRandom

            type = SystemRandom().choice([Race.Vampire, Race.Werewolf, Race.Zombie])
    return await turn(ctx, session, this_user, ctx.user_id, type.value, action="drink")

############
# MONSTERS #
############

@register(group=Groups.GLOBAL, main=halloween)
def monsters(cls=None, *, should_register: bool = True):
    """Commands related to Monster's factions"""
    i = functools.partial(inner, races=IMMUNE_TABLE.keys(), main=monsters, should_register=should_register)
    if cls:
        return i(cls)
    return i

@monsters
@cooldown(hours=3, logic=HalloweenCooldown)
async def bite(ctx: Context, target: User, *, session: sa.orm.Session, this_user: Halloween) -> str:
    """Bite your target to turn into one of your own kin
    Params
    ------
    target:
        Target you want to bite"""
    return await turn(ctx, session, this_user, target.id, action="bite")

###########
# HUNTERS #
###########

@register(group=Groups.GLOBAL, main=halloween)
def hunters(cls=None, *, should_register: bool = True):
    """Commands related to Hunter's factions"""
    i = functools.partial(inner, races=HUNTERS, main=hunters, should_register=should_register)
    if cls:
        return i(cls)
    return i

@hunters
@cooldown(hours=2, logic=HalloweenCooldown)
async def cure(ctx: Context, target: User, *, session: sa.orm.Session, this_user: Halloween) -> str:
    """Cure your target from the darkness back into human being
    Params
    ------
    target:
        Target you want to cure"""
    return await turn(ctx, session, this_user, target.id, action="cure")

@hunters
@cooldown(hours=1, logic=HalloweenCooldown)
async def defend(ctx: Context, target: User, *, session: sa.orm.Session, this_user: Halloween) -> str:
    """Protect fellow hunter from being bitten
    Params
    ------
    target:
        Target you want to protect"""
    if target.id == ctx.user_id:
        raise HalloweenException("error_defend", ctx.language)

    target_user = (
        session.query(Halloween).filter(Halloween.server_id == ctx.guild_id, Halloween.user_id == target.id).first()
    )

    if target_user.race not in HUNTERS:
        raise Failed("defend", ctx.language)

    now = datetime.now(tz=timezone.utc)
    if target_user.protected is None or target_user.protected < now:
        duration = SystemRandom().randint(5, 40)
        delta = now + timedelta(minutes=duration)
        target_user.protected = delta
        session.add(
            HalloweenLog(
                server_id=ctx.guild_id,
                target_id=target.id,
                previous=target_user.race,
                race=target_user.race,
                user_id=this_user.user_id,
            )
        )
        session.commit()
        return _t("success_defend", ctx.language, duration=duration, target=target.id)

    raise HalloweenException("error_protected", ctx.language)

@hunters
@cooldown(hours=4, logic=HalloweenCooldown)
@Chance(8, fail_message="Couldn't convince them to betray their kind.")
async def betray(ctx: Context, target: User, *, session: sa.orm.Session, this_user: Halloween) -> str:
    """Attempt to convince a monster you are hunting to join the cause and fight with the darkness instead
    Params
    ------
    target:
        Target you want to convince"""
    return await turn(ctx, session, this_user, target.id, this_user.race, action="betray")

@hunters
@cooldown(hours=2, logic=HalloweenCooldown)
@Chance(30, fail_message="Couldn't find any monster nearby!")
async def track(ctx: Context, *, session: sa.orm.Session, this_user: Halloween) -> str:
    """Attempt to track a monster's footprints
    Params
    ------
    target:
        Target you want to convince"""
    # NOTE: Alternative would be to also check monsters this hunter hunts for
    # TODO: Weighted chance, prioritize users that are inactive/have less xp/were active long ago
    r = (
        session.query(Halloween)
        .filter(Halloween.race.in_(list(IMMUNE_TABLE.keys())), Halloween.server_id == ctx.guild_id)
        .order_by(sa.func.random())
        .limit(1)
        .first()
    )
    return f"You find signs of struggle. Investigating spilled blood leads you to a {r.race.name}: <@{r.user_id}>"

@hunters
@cooldown(hours=2, logic=HalloweenCooldown)
@Chance(50, fail_message="You are trying to do WHAT?! KEEP FIGHTING!")
async def desert(ctx: Context, *, session: sa.orm.Session, this_user: Halloween) -> str:
    """Attempt to desert from being a hunter"""
    return await turn(ctx, session, this_user, ctx.user_id, Race.Human, action="desert")

# @register(group=Groups.GLOBAL, name="Defend or Betray")
@hunters(should_register=False)
async def defend_or_betray(ctx: Context, user: User, *, session: sa.orm.Session, this_user: Halloween):
    """
    Defend fellow hunter or convince monster depending on target's class
    """
    target_user = (
        session.query(Halloween).filter(Halloween.server_id == ctx.guild_id, Halloween.user_id == user.id).first()
    )
    if target_user.race in HUNTERS:
        return await defend(ctx, target=user, s=session, this_user=this_user)
    elif target_user.race is CURE_TABLE.get(this_user.race):
        return await betray(ctx, target=user, s=session, this_user=this_user)
    await ctx.deferred(private=True)
    raise Cant("generic", ctx.language)

# @register(group=Groups.GLOBAL, name="Bite or Cure")
async def bite_or_cure(ctx: Context, user: User):
    """
    Bite or cure user depending on your class
    """
    session = ctx.db.sql.session()
    this_user = (
        session.query(Halloween).filter(Halloween.server_id == ctx.guild_id, Halloween.user_id == ctx.user.id).first()
    )
    if this_user.race in IMMUNE_TABLE:
        return await bite(ctx, target=user, s=session, this_user=this_user)
    elif this_user.race in HUNTERS:
        return await cure(ctx, target=user, s=session, this_user=this_user)
    await ctx.deferred(private=True)
    raise Cant("generic", ctx.language)

@register(group=Groups.GLOBAL, main=halloween)
async def leaderboard(ctx: Context, user: User = None, limit: int = 10) -> Embed:
    """
    Shows Event leaderboard based on amount of turns
    Params
    ------
    user:
        Shows stats of another user
    limit:
        How many scores to show
    """
    s = ctx.db.sql.session()
    total_turned = (
        s.query(sa.func.count(HalloweenLog.user_id), HalloweenLog.user_id)
        .filter(
            HalloweenLog.server_id == ctx.guild_id,
            HalloweenLog.user_id != HalloweenLog.target_id,
            HalloweenLog.previous != HalloweenLog.race,
        )
        .group_by(HalloweenLog.user_id)
        .order_by(sa.func.count(HalloweenLog.user_id).desc())
        .limit(limit)
        .all()
    )
    # top_race_turns = s.query(sa.func.count(HalloweenLog.race), HalloweenLog.race).filter(HalloweenLog.server_id == ctx.guild_id).group_by(HalloweenLog.race).all()
    # turned_by_user = s.query(sa.func.count(HalloweenLog.race), HalloweenLog.race, HalloweenLog.user_id).filter(HalloweenLog.server_id == ctx.guild_id).group_by(HalloweenLog.user_id, HalloweenLog.race).all()
    if not any(user.id == i.user_id for i in total_turned):
        _user = (
            s.query(sa.func.count(HalloweenLog.user_id), HalloweenLog.user_id)
            .filter(
                HalloweenLog.server_id == ctx.guild_id,
                HalloweenLog.user_id == user.id,
                HalloweenLog.user_id != HalloweenLog.target_id,
                HalloweenLog.previous != HalloweenLog.race,
            )
            .group_by(HalloweenLog.user_id)
            .first()
        )
        if _user:
            total_turned.append(_user)
    from MFramework.utils.leaderboards import Leaderboard, Leaderboard_Entry

    entries = []
    for row in total_turned:
        entries.append(Leaderboard_Entry(ctx, row[1], row[0]))
    return [Leaderboard(ctx, user.id, entries, limit).as_embed()]

@register(group=Groups.GLOBAL, main=halloween)
async def history(ctx: Context, user: User = None, limit: int = 10) -> Embed:
    """
    Shows turn history
    Params
    ------
    user:
        User's history to show
    limit:
        How many turns to show
    """
    s = ctx.db.sql.session()
    from MFramework import Guild_Member

    turns = []
    turn_history = (
        s.query(HalloweenLog)
        .filter(HalloweenLog.server_id == ctx.guild_id, HalloweenLog.target_id == user.id)
        .order_by(HalloweenLog.timestamp.desc())
        .limit(limit)
        .all()
    )
    for entry in turn_history[: int(limit)]:
        _u = entry.user_id
        _u = ctx.cache.members.get(int(_u), Guild_Member(user=User(username=_u))).user.username
        if entry.previous == entry.race:
            line = "Defended"
        elif entry.user_id == entry.target_id:
            if entry.race in IMMUNE_TABLE:
                line = f"Drank potion and became `{entry.race}`"
            elif entry.race in HUNTERS:
                line = f"Enlisted as `{entry.race}`"
            else:
                line = f"Deserted from `{entry.previous}`"
        elif entry.previous in IMMUNE_TABLE and entry.race in HUNTERS:
            line = f"Convinced to fight as `{entry.race}`"
        else:
            line = f"`{entry.previous}` into `{entry.race}`"
        turns.append(f"[<t:{int(entry.timestamp.timestamp())}:d>] {line} by `{_u}`")

    targets = []
    targets_history = (
        s.query(HalloweenLog)
        .filter(
            HalloweenLog.server_id == ctx.guild_id,
            HalloweenLog.user_id == user.id,
            HalloweenLog.user_id != HalloweenLog.target_id,
        )
        .order_by(HalloweenLog.timestamp.desc())
        .limit(limit)
        .all()
    )
    for entry in targets_history[: int(limit)]:
        _u = entry.target_id
        _u = ctx.cache.members.get(int(_u), Guild_Member(user=User(username=_u))).user.username
        if entry.previous == entry.race:
            line = "was protected from being turned"
        elif entry.previous in IMMUNE_TABLE and entry.race in HUNTERS:
            line = f"was convinced to enlist as `{entry.race}`"
        else:
            line = f"from `{entry.previous}` into `{entry.race}`"
        targets.append(f"[<t:{int(entry.timestamp.timestamp())}:d>] `{_u}` {line}")

    e = Embed().setTitle(f"{user.username}'s History")
    if turns:
        e.addField("Turns", "\n".join(turns))
    if targets:
        e.addField("Targets", "\n".join(targets))
    turn_count = (
        s.query(sa.func.count(HalloweenLog.target_id))
        .filter(
            HalloweenLog.server_id == ctx.guild_id,
            HalloweenLog.target_id == user.id,
            HalloweenLog.user_id != HalloweenLog.target_id,
            HalloweenLog.previous != HalloweenLog.race,
        )
        .first()
    )
    targets_count = (
        s.query(sa.func.count(HalloweenLog.user_id))
        .filter(
            HalloweenLog.server_id == ctx.guild_id,
            HalloweenLog.user_id == user.id,
            HalloweenLog.user_id != HalloweenLog.target_id,
            HalloweenLog.previous != HalloweenLog.race,
        )
        .first()
    )
    e.addField("Statistics", f"Total Turns: `{turn_count[0]}`\nTotal Targets: `{targets_count[0]}`")
    return [e]

@register(group=Groups.GLOBAL, main=halloween)
async def statistics(ctx: Context) -> Embed:
    """
    Shows event related statistics
    """
    s = ctx.db.sql.session()
    e = Embed().set_title("Server Statistics")
    _total = sorted(Halloween.get_total(s, ctx.guild_id).items(), key=lambda x: x[1], reverse=True)
    total = "\n".join([f"`{i[0]:10}`: `{i[1]:>5}`" for i in _total])
    e.add_field("Members", total, True)
    turns_to = (
        s.query(sa.func.count(HalloweenLog.race), HalloweenLog.race)
        .filter(HalloweenLog.server_id == ctx.guild_id)
        .group_by(HalloweenLog.race)
        .order_by(sa.func.count(HalloweenLog.race).desc())
        .all()
    )
    turns_to = "\n".join([f"`{i[1]:10}`: `{i[0]:>5}`" for i in turns_to])
    e.add_field("Mostly Turned Into", turns_to, True)
    turns_from = (
        s.query(sa.func.count(HalloweenLog.previous), HalloweenLog.previous)
        .filter(HalloweenLog.server_id == ctx.guild_id)
        .group_by(HalloweenLog.previous)
        .order_by(sa.func.count(HalloweenLog.previous).desc())
        .all()
    )
    turns_from = "\n".join([f"`{i[1]:10}`: `{i[0]:>5}`" for i in turns_from])
    e.add_field("Mostly Turned From", turns_from, True)

    active_players = (
        s.query(sa.func.count(HalloweenLog.user_id))
        .filter(
            HalloweenLog.server_id == ctx.guild_id,
            HalloweenLog.user_id != HalloweenLog.target_id,
            HalloweenLog.race != HalloweenLog.previous,
        )
        .group_by(HalloweenLog.user_id)
        .all()
    )
    total_players = (
        s.query(sa.func.count(HalloweenLog.user_id))
        .filter(HalloweenLog.server_id == ctx.guild_id)
        .group_by(HalloweenLog.user_id)
        .all()
    )
    e.set_footer(
        f"Active/Total/Participants: {len([i for i in active_players if i[0] > 1])}/{len(total_players)}/{sum([i[1] for i in _total])}"
    )
    return e

@register(group=Groups.GLOBAL, main=halloween)
async def cooldowns(ctx: Context):
    """
    Shows current faction cooldowns
    """
    s = ctx.db.sql.session()
    e = Embed().set_title("Current Cooldowns").set_footer("Illustrative only. Actual cooldowns might differ")
    t = Halloween.get_total(s, ctx.guild_id)

    def cooldown_var(race, a, b):
        diff = t.get(race) - ((t.get(a) + t.get(b)) // 2)
        if ((t.get(a) + t.get(b)) // 2) > t.get(race):
            diff *= 2
        return timedelta(minutes=diff * 3)

    def get(cooldown: COOLDOWNS, race: Race, other1: Race, other2: Race, multipler=1):
        c = (cooldown.value + cooldown_var(race, other1, other2)) * multipler
        return c if c.total_seconds() > 300 else timedelta(minutes=5)

    def ratio(hunter: Race):
        return 1 / (t.get(CURE_TABLE[hunter], 1) / t.get(hunter, 1))

    top = sorted(filter(lambda x: x[0] in HUNTERS, t.items()), key=lambda x: x[1], reverse=True)[0]
    r = 1 / (t.get(CURE_TABLE[top[0]], 1) / top[1])
    top = top[0]

    e.add_field(
        "Monsters",
        "\n".join(
            [
                f"Vampire ({t.get(Race.Vampire)}): {get(COOLDOWNS.BITE, Race.Vampire, Race.Werewolf, Race.Zombie)}",
                f"Werewolf ({t.get(Race.Werewolf)}): {get(COOLDOWNS.BITE, Race.Werewolf, Race.Vampire, Race.Zombie)}",
                f"Zombie ({t.get(Race.Zombie)}): {get(COOLDOWNS.BITE, Race.Zombie, Race.Werewolf, Race.Vampire)}",
            ]
        ),
    )
    e.add_field(
        f"Hunters ({t.get(Race.Hunter)})",
        "\n".join(
            [
                f"Cure: {get(COOLDOWNS.CURE, Race.Hunter, Race.Huntsmen, Race.Enchanter, 1 - (r - ratio(Race.Hunter)))}",
                f"Betray: {get(COOLDOWNS.BETRAY, Race.Hunter, Race.Huntsmen, Race.Enchanter, 1 - (r - ratio(Race.Hunter)))}",
                f"Defend: {get(COOLDOWNS.DEFEND, Race.Hunter, Race.Huntsmen, Race.Enchanter, 1 - (r - ratio(Race.Hunter)))}",
            ]
        ),
    )
    e.add_field(
        f"Huntsmen ({t.get(Race.Huntsmen)})",
        "\n".join(
            [
                f"Cure: {get(COOLDOWNS.CURE, Race.Huntsmen, Race.Hunter, Race.Enchanter, 1 - (r - ratio(Race.Huntsmen)))}",
                f"Betray: {get(COOLDOWNS.BETRAY, Race.Huntsmen, Race.Hunter, Race.Enchanter, 1 - (r - ratio(Race.Huntsmen)))}",
                f"Defend: {get(COOLDOWNS.DEFEND, Race.Huntsmen, Race.Hunter, Race.Enchanter, 1 - (r - ratio(Race.Huntsmen)))}",
            ]
        ),
    )
    e.add_field(
        f"Enchanters ({t.get(Race.Enchanter)})",
        "\n".join(
            [
                f"Cure: {get(COOLDOWNS.CURE, Race.Enchanter, Race.Huntsmen, Race.Hunter, 1 - (r - ratio(Race.Enchanter)))}",
                f"Betray: {get(COOLDOWNS.BETRAY, Race.Enchanter, Race.Huntsmen, Race.Hunter, 1 - (r - ratio(Race.Enchanter)))}",
                f"Defend: {get(COOLDOWNS.DEFEND, Race.Enchanter, Race.Huntsmen, Race.Hunter, 1 - (r - ratio(Race.Enchanter)))}",
            ]
        ),
    )
    return e

@register(group=Groups.GLOBAL, main=halloween, private_response=True)
async def info(ctx: Context, user: User = None):
    """
    Shows user's event info
    Params
    ------
    user:
        User whose statistics to show
    """
    s = ctx.db.sql.session()
    e = Embed().setTitle(user.username)
    cooldowns = []
    h_user = Halloween.fetch_or_add(s, server_id=ctx.guild_id, user_id=user.id)
    from mlib.localization import secondsToText as s2t

    for cooldown in COOLDOWNS:
        r = HalloweenCooldown(ctx, cooldown.value, cooldown.name.lower(), {"session": s, "this_user": h_user})
        if r.on_cooldown:
            cooldowns.append((r._type, s2t(int(r.remaining.total_seconds()))))
    if user.id == ctx.user_id and cooldowns:
        e.addField("Cooldowns", "\n".join(f"`{i[0].title()}`: `{i[1]}`" for i in cooldowns))
    e.addField("Currently is", h_user.race, True)
    now = datetime.now(tz=timezone.utc)
    if h_user.protected and h_user.protected > now:
        e.addField("Protected for", s2t(int((h_user.protected - now).total_seconds())), True)
    turn_count = (
        s.query(sa.func.count(HalloweenLog.target_id))
        .filter(
            HalloweenLog.server_id == ctx.guild_id,
            HalloweenLog.target_id == user.id,
            HalloweenLog.user_id != HalloweenLog.target_id,
            HalloweenLog.previous != HalloweenLog.race,
        )
        .first()
    )
    e.setFooter(f"Chance for self-defence: {turn_count[0]/10}%")
    return [e]

from MFramework import Bot, Guild_Member_Add, Message, onDispatch

@onDispatch
@EventBetween(after_month=10, after_day=14, before_month=11, before_day=7)
async def guild_member_add(self: Bot, data: Guild_Member_Add):
    s = self.db.sql.session()
    r = s.query(Halloween).filter(Halloween.user_id == data.user.id).first()
    if r and ROLES.get(data.guild_id, None):
        if type(r.race) is Race:
            race = r.race.name
        else:
            race = r.race
        role = ROLES.get(data.guild_id, {}).get(race, None)
        if role:
            await self.add_guild_member_role(data.guild_id, data.user.id, role, "Halloween Minigame")

CACHED_WEBHOOKS = {}

@onDispatch
@EventBetween(after_month=10, after_day=14, before_month=11, before_day=7)
async def message_create(self: Bot, data: Message):
    zombie_role = ROLES.get(data.guild_id, {}).get("Zombie", None)
    if zombie_role in data.member.roles:
        if SystemRandom().random() > (3 / 100):
            return
        if data.guild_id not in CACHED_WEBHOOKS:
            CACHED_WEBHOOKS[data.guild_id] = {}
        if not CACHED_WEBHOOKS.get(data.guild_id, {}):
            webhooks = await self.get_guild_webhooks(data.guild_id)
            for webhook in webhooks:
                if webhook.channel_id in CACHED_WEBHOOKS[data.guild_id]:
                    continue
                CACHED_WEBHOOKS[data.guild_id][webhook.channel_id] = (webhook.id, webhook.token)
        webhook = CACHED_WEBHOOKS[data.guild_id].get(data.channel_id, None)
        if not webhook:
            webhook = await self.create_webhook(data.channel_id, "Halloween", reason="Halloween Minigame")
            CACHED_WEBHOOKS[data.guild_id][data.channel_id] = (webhook.id, webhook.token)
            webhook = CACHED_WEBHOOKS[data.guild_id][data.channel_id]
        from MFramework import Allowed_Mentions

        try:
            await self.execute_webhook(
                webhook[0],
                webhook[1],
                wait=True,
                thread_id=getattr(data.thread, "id", None),
                content=data.content.replace(f"@everyone", "`@everyone`")
                + f" *...Br{'a'*SystemRandom().randint(1,5)}ins?*",
                username=data.member.nick or data.author.username,
                avatar_url=data.author.get_avatar(),
                allowed_mentions=Allowed_Mentions(parse=[]),
            )
        except:
            return
        await data.delete(reason="Halloween Minigame: Zombified message")

@register(group=Groups.OWNER, main=halloween, interaction=False)
async def summary(ctx: Context, include_humans: bool = False):
    """
    Shows event summary
    Params
    ------
    include_humans:
        Whether to draw line for humans as well or not
    """
    e = Embed()
    e.setTitle("Thanks for Participating!")
    s = ctx.db.sql.session()

    most_bites = {}
    most_cures = {}

    # NOTE: Deduplicating these queries could be nice
    query = s.query(HalloweenLog.race, HalloweenLog.user_id, sa.func.count(HalloweenLog.user_id)).filter(
        HalloweenLog.server_id == ctx.guild_id,
        HalloweenLog.user_id != HalloweenLog.target_id,
        HalloweenLog.previous != HalloweenLog.race,
        HalloweenLog.race.in_(list(IMMUNE_TABLE.keys())),
    )
    monsters = (
        query.group_by(HalloweenLog.user_id, HalloweenLog.race)
        .order_by(sa.func.count(HalloweenLog.user_id).desc(), HalloweenLog.race)
        .all()
    )

    query = s.query(HalloweenLog.previous, HalloweenLog.user_id, sa.func.count(HalloweenLog.user_id)).filter(
        HalloweenLog.server_id == ctx.guild_id,
        HalloweenLog.user_id != HalloweenLog.target_id,
        HalloweenLog.previous != HalloweenLog.race,
        HalloweenLog.previous.in_(list(IMMUNE_TABLE.keys())),
    )
    hunters = (
        query.group_by(HalloweenLog.user_id, HalloweenLog.previous)
        .order_by(sa.func.count(HalloweenLog.user_id).desc())
        .all()
    )

    # FIXME: This can be simplified if we can get only top per race from db
    for monster, user, count in monsters:
        if monster not in most_bites or count > most_bites[monster][1]:
            most_bites[monster] = (user, count)

    for hunted, user, count in hunters:
        hunter = IMMUNE_TABLE.get(hunted)
        if hunter not in most_cures or count > most_cures[hunter][1]:
            most_cures[hunter] = (user, count)

    most_turned = (
        s.query(HalloweenLog.target_id, sa.func.count(HalloweenLog.target_id))
        .filter(
            HalloweenLog.server_id == ctx.guild_id,
            HalloweenLog.user_id != HalloweenLog.target_id,
            HalloweenLog.previous != HalloweenLog.race,
        )
        .group_by(HalloweenLog.target_id)
        .order_by(sa.func.count(HalloweenLog.target_id).desc())
        .first()
    )
    # TODO: Get total bites/cures and players
    bites = (
        s.query(sa.func.count(HalloweenLog.race))
        .filter(
            HalloweenLog.server_id == ctx.guild_id,
            HalloweenLog.user_id != HalloweenLog.target_id,
            HalloweenLog.previous != HalloweenLog.race,
            HalloweenLog.race.in_(IMMUNE_TABLE),
        )
        .first()
    )
    cures = (
        s.query(sa.func.count(HalloweenLog.race))
        .filter(
            HalloweenLog.server_id == ctx.guild_id,
            HalloweenLog.user_id != HalloweenLog.target_id,
            HalloweenLog.previous != HalloweenLog.race,
            HalloweenLog.race == Race.Human,
        )
        .first()
    )
    players = (
        s.query(sa.func.count(sa.distinct(HalloweenLog.user_id))).filter(HalloweenLog.server_id == ctx.guild_id).first()
    )
    unique_victims = s.query(sa.func.count(Halloween.server_id)).filter(Halloween.server_id == ctx.guild_id).first()
    turns = (
        s.query(sa.func.count(HalloweenLog.server_id))
        .filter(
            HalloweenLog.server_id == ctx.guild_id,
            HalloweenLog.previous != HalloweenLog.race,
        )
        .first()
    )
    PLURALS = {
        Race.Human: "Humans",
        Race.Vampire: "Vampires",
        Race.Werewolf: "Werewolves",
        Race.Zombie: "Zombies",
        Race.Hunter: "Hunters",
        Race.Huntsmen: "Huntsmen",
        Race.Enchanter: "Enchanters",
    }

    stats = [
        ("Players", players[0]),
        ("Victims", unique_victims[0]),
        ("Turns", turns[0]),
        ("Bites", bites[0]),
        ("Cures", cures[0]),
    ]
    e.addField("Total", "\n".join([f"`{i[0]:10}`: `{i[1]:>5}`" for i in stats]))
    total = sorted(Halloween.get_total(s, ctx.guild_id).items(), key=lambda x: x[1], reverse=True)
    total = "\n".join([f"`{PLURALS.get(i[0], i[0]):10}`: `{i[1]:>5}`" for i in total])
    e.addField("Member Counts", total)
    # TODO: format nicely!
    def get_username(user_id):
        from MFramework import Guild_Member

        return ctx.cache.members.get(int(user_id), Guild_Member(user=User(username=user_id))).user.username

    if most_bites:
        most_bites = "\n".join([f"**{k.name}**: `{get_username(v[0])}` ({v[1]})" for k, v in most_bites.items()])
        e.addField("Most Bites as", most_bites, True)
    if most_cures:
        most_cures = "\n".join([f"**{k.name}**: `{get_username(v[0])}` ({v[1]})" for k, v in most_cures.items()])
        e.addField("Most Cures as", most_cures, True)
    turned_user = get_username(most_turned[0])
    e.addField("Mostly Turned", f"`{turned_user}` ({most_turned[1]})")

    history = (
        s.query(HalloweenLog)
        .filter(HalloweenLog.server_id == ctx.guild_id, HalloweenLog.previous != HalloweenLog.race)
        .order_by(HalloweenLog.timestamp.asc())
        .all()
    )

    from collections import Counter

    counters = Counter(list(Race))
    races = {i: {"y": [], "t": []} for i in list(Race)}
    counters[Race.Human] = stats[1][1]

    import pandas as pd

    for entry in history:
        counters[entry.race] += 1
        counters[entry.previous] -= 1
        if not include_humans and entry.race is Race.Human:
            continue
        t = pd.to_datetime(entry.timestamp).tz_convert(None)
        races[entry.race]["y"].append(counters[entry.race])
        races[entry.race]["t"].append(t)

    import matplotlib.pyplot as plt

    fig, ax = plt.subplots(figsize=(12, 8))
    ax.patch.set_facecolor("#333333")
    plt.style.use(["dark_background"])

    LINE_STYLES = {
        Race.Human: "dashed",
        Race.Vampire: "dotted",
        Race.Werewolf: "dotted",
        Race.Zombie: "dotted",
        Race.Hunter: "-",
        Race.Huntsmen: "-",
        Race.Enchanter: "-",
    }

    COLORS = {
        Race.Human: "white",
        Race.Vampire: "#942710",
        Race.Werewolf: "#aa6b00",
        Race.Zombie: "#03803c",
        Race.Hunter: "#ad4949",
        Race.Huntsmen: "#be923d",
        Race.Enchanter: "#4abe5f",
    }

    for race, v in races.items():
        if not include_humans and race is Race.Human:
            continue
        y, t = v.values()
        df = pd.Series(y, index=t)
        # df = df.resample(resample).count()
        ax.plot(df, label=race.name, marker="", linestyle=LINE_STYLES.get(race), color=COLORS.get(race))

    from mlib import graphing

    graphing.set_locator(ax, "Day", 3)
    fig.autofmt_xdate()
    now = datetime.now()

    graphing.set_legend(
        ax,
        f"Halloween {now.year} Summary",
        "Population",
        "Days",
        f"upper {'left' if not include_humans else 'right'}",
        framealpha=0,
    )
    fig.tight_layout()

    img = graphing.create_image(fig)
    filename = f"halloween_summary-{datetime.now().year}.png"
    e.setImage("attachment://" + filename).setColor(16744206)
    await ctx.send(embeds=[e], attachments=[Attachment(file=img, filename=filename)])

@register(group=Groups.SYSTEM, interaction=False)
async def crossref(ctx: Context, *, language):
    """
    Description to use with help command
    """
    s = ctx.db.sql.session()
    players = s.query(sa.distinct(HalloweenLog.user_id)).filter(HalloweenLog.server_id == ctx.guild_id).all()
    unique_victims = s.query(Halloween.user_id).filter(Halloween.server_id == ctx.guild_id).all()
    unique_victims = [i for i in unique_victims if i not in players]
    not_found = []
    for victim in unique_victims:
        m = ctx.cache.members.get(victim[0])
        if not m:
            not_found.append(victim[0])
    from ...database import log, types

    users = {}
    avg_chat = []
    avg_voice = []
    with_infractions = []
    for left in not_found:
        r = log.Statistic.filter(s, server_id=ctx.guild_id, user_id=left).all()
        infractions = s.query(sa.func.count(log.Infraction.user_id)).filter(log.Infraction.user_id == left).first()
        if infractions[0]:
            with_infractions.append((left, infractions[0]))
            continue

        chatted = False
        voiced = False
        for _ in r:
            if _.name == types.Statistic.Chat:
                avg_chat.append((left, _.value))
                chatted = True
            elif _.name == types.Statistic.Voice:
                avg_voice.append((left, _.value))
                voiced = True
        if not chatted:
            avg_chat.append((left, 0))
        if not voiced:
            avg_voice.append((left, 0))

    top_chat = sorted(avg_chat, key=lambda x: x[1])
    top_voice = sorted(avg_voice, key=lambda x: x[1])
    tc = max(top_chat[0], top_chat[-1], key=lambda x: x[1])
    tv = max(top_voice[0], top_voice[-1], key=lambda x: x[1])
    avg_chat = [i[1] for i in avg_chat if i[1] < 1000]
    avg_voice = [i[1] for i in avg_voice if i[1] < 1000]
    with_infractions = [i[1] for i in with_infractions if i[1]]
    return f"{len(avg_chat)}/{len(ctx.cache.members)}\nAvg chat activity: {sum(avg_chat)/len(avg_chat)}\nAvg voice activity: {sum(avg_voice)/len(avg_voice)}\nWith infractions: {len(with_infractions)}\nAvg infractions: {sum(with_infractions)/len(with_infractions)}\nChatter: <@{tc[0]}> - {tc[1]}\nVoice: <@{tv[0]}> - {tv[1]}"


import pygame
from ctypes import windll
from substick import SubStick
from stick import Stick
try:  # Python 2.X support
    from tkFileDialog import askopenfilename
except ImportError:  # Python 3.0+ support
    from tkinter.filedialog import askopenfilename

WHITE = (255,255,255)

class BigStick(Stick):
    def __init__(self,stickfaceFileLoc,pos=(0,0)):
        Stick.__init__(self,stickfaceFileLoc,'big.ini',pos)
        self.subSticks = []
        self.keysOut = self.stickfaceIni['keysOut']
        self.keysPressed = []
        self.disableStart = False
        self.startButton = self.stickfaceIni['startButton']
        self.cutoff = 1
        self.text = pygame.font.SysFont('Consolas',20)

    def update(self):
        self.buttons = [0]*12
        self.hat = (0,0)
        for stick in self.subSticks:
            self.buttons = [x+y for x,y in zip(self.buttons,stick.buttons)]
            self.hat = tuple(x+y for x,y in zip(self.hat,stick.hat))

    def draw(self,surface):
        finalImg = self.drawController()
        for b in range(12):
            if self.buttons[b] >= 1:
                textSurf = self.text.render(str(self.buttons[b]), True, WHITE)
                textRect = textSurf.get_rect()
                buttonRect = pygame.Rect(self.buttonLoc[b], self.buttonSize[b])
                textRect.center = buttonRect.center
                finalImg.blit(textSurf,textRect)
        surface.blit(finalImg,self.pos)

    def getNewStickface(self):
        newstickfaceFileLoc = askopenfilename()
        if newstickfaceFileLoc != '':
            self.stickfaceFileLoc = newstickfaceFileLoc
            self.loadStickfaceFile(self.stickfaceFileLoc)
            for stick in self.subSticks:
                stick.loadStickfaceFile(self.stickfaceFileLoc)

    def addSubStick(self,joystick,pos=(0,0)):
        subStick = SubStick(self.stickfaceFileLoc,joystick,pos)
        self.subSticks.append(subStick)

    def changeCutoff(self,i):
        self.cutoff += i
        if self.cutoff > len(self.subSticks):
            self.cutoff = len(self.subSticks)
        if self.cutoff < 1:
            self.cutoff = 1

    def pressKeyDown(self,key):
        vk = windll.user32.VkKeyScanA(ord(key))
        scan = windll.user32.MapVirtualKeyA(vk,0)
        windll.user32.keybd_event(vk,scan,0,0)

    def pressKeyUp(self,key):
        vk = windll.user32.VkKeyScanA(ord(key))
        scan = windll.user32.MapVirtualKeyA(vk,0)
        windll.user32.keybd_event(vk,scan,2,0)

    def sendKeyPresses(self):
        self.keysLastPressed = self.keysPressed
        self.keysPressed = []
        for b in range(8):
            if self.buttons[b] >= self.cutoff:
                self.keysPressed.append(self.keysOut[b])
        start = self.keysOut[self.startButton]
        if self.disableStart and start in self.keysPressed:
            self.keysPressed.remove(start)
        if self.hat[0] < 0:
            self.keysPressed.append(self.keysOut[8]) #left
        if self.hat[0] > 0:
            self.keysPressed.append(self.keysOut[9]) #right
        if self.hat[1] > 0:
            self.keysPressed.append(self.keysOut[10])#up
        if self.hat[1] < 0:
            self.keysPressed.append(self.keysOut[11]) #down

        for key in self.keysPressed:
            if key in self.keysLastPressed:
                self.keysLastPressed.remove(key)
            else:
                self.pressKeyDown(key)
        for key in self.keysLastPressed:
            self.pressKeyUp(key)


"""export implementation for storage"""
from typing import Any

from src.core.config import read_config
from src.storage.conoha.storage import ConohaStorage
from src.storage.storage_interface import StorageInterface
from src.core.logger import create_logger

logger = create_logger(__file__)

class StorageConfigure(StorageInterface):
    _instance: Any
    _storage_impl: StorageInterface

    def __new__(cls, *args, **kargs):
        if not hasattr(cls, "_instance"):
            cls._instance = super(StorageConfigure, cls).__new__(cls)
            # configure storage type and setting on read storage module.
            config_file_name = "env.yml"  # noqa
            try:
                storage_config = read_config(config_file_name)["storage"]
                if storage_config["type"] == "conoha":
                    cls._storage_impl = ConohaStorage(**storage_config["configs"])
                else:
                    logger.info(f"can not read 'type' in your config {config_file_name}, use type = 'local'")
            except KeyError:
                import traceback
                logger.info(traceback.format_exc())
                logger.warn(f"your {config_file_name} has appropriate key.")

            return cls._instance
        else:
            return cls._instance

    def create(self, from_: str, to: str):
        self._storage_impl.create(from_, to)

    def read(self, from_: str, to: str):
        self._storage_impl.read(from_, to)

    def update(self, from_: str, to: str):
        self._storage_impl.update(from_, to)

    def delete(self, path: str):
        self._storage_impl.delete(path)

def create(from_: str, to: str):
    StorageConfigure().create(from_, to)

def read(from_: str, to: str):
    StorageConfigure().read(from_, to)

def update(from_: str, to):
    StorageConfigure().update(from_, to)

def delete(path: str):
    StorageConfigure().delete(path)

if __name__ == '__main__':
    a = create("tests.html", "public/tests.html")
    a = read("tests/sample.json", "read.json")
    a = delete("tests/sample.json")
    # a = create("sample", {})
    # a = create("sample", {})