import sys
import time
import traceback
from datetime import datetime
import random
import re
from decimal import Decimal
import numexpr
from typing import List, Dict

import disnake
from disnake.ext import commands, tasks
from disnake import ActionRow, Button

from disnake.enums import OptionType
from disnake.app_commands import Option

from disnake.enums import ButtonStyle
import copy

import store
from Bot import logchanbot, EMOJI_ZIPPED_MOUTH, EMOJI_ERROR, EMOJI_RED_NO, EMOJI_ARROW_RIGHTHOOK, \
    EMOJI_MONEYFACE, NOTIFICATION_OFF_CMD, EMOJI_SPEAK, EMOJI_BELL, EMOJI_BELL_SLASH, EMOJI_HOURGLASS_NOT_DONE, \
    EMOJI_INFORMATION, EMOJI_PARTY, SERVER_BOT, seconds_str, RowButtonCloseMessage, RowButtonRowCloseAnyMessage, \
    text_to_num, truncate

from cogs.wallet import WalletAPI
from cogs.utils import Utils, num_format_coin

class MyMathBtn(disnake.ui.Button):
    def __init__(self, label, _style, _custom_id):
        super().__init__(label=label, style=_style, custom_id=_custom_id)

class MathButton(disnake.ui.View):
    message: disnake.Message
    a_index: int
    coin_list: Dict

    def __init__(self, bot, ctx, answer_list, answer_index: int, timeout: float, coin_list):
        super().__init__(timeout=timeout)
        i = 0
        self.bot = bot
        self.utils = Utils(self.bot)
        self.a_index = answer_index
        self.coin_list = coin_list
        self.ctx = ctx
        for name in answer_list:
            custom_id = "mathtip_answers_" + str(i)
            self.add_item(MyMathBtn(name, ButtonStyle.green, custom_id))
            i += 1

    async def on_timeout(self):
        i = 0
        for child in self.children:
            if isinstance(child, disnake.ui.Button):
                child.disabled = True
                if i == self.a_index:
                    child.style = ButtonStyle.red
                i += 1
        ## Update content
        get_mathtip = None
        try:
            original_message = await self.ctx.original_message()
            get_mathtip = await store.get_discord_mathtip_by_msgid(str(original_message.id))
        except Exception:
            traceback.print_exc(file=sys.stdout)
            return

        if get_mathtip is None:
            await logchanbot(f"[ERROR MATH TIP] Failed timeout in guild {self.ctx.guild.name} / {self.ctx.guild.id}!")
            return
        if get_mathtip['status'] == "ONGOING":
            answered_msg_id = await store.get_math_responders_by_message_id(str(self.message.id))
            amount = get_mathtip['real_amount']
            coin_name = get_mathtip['token_name']
            owner_displayname = get_mathtip['from_username']
            total_answer = answered_msg_id['total']
            coin_decimal = getattr(getattr(self.coin_list, coin_name), "decimal")
            contract = getattr(getattr(self.coin_list, coin_name), "contract")
            token_display = getattr(getattr(self.coin_list, coin_name), "display_name")
            price_with = getattr(getattr(self.coin_list, coin_name), "price_with")

            coin_emoji = getattr(getattr(self.coin_list, coin_name), "coin_emoji_discord")
            coin_emoji = coin_emoji + " " if coin_emoji else ""

            indiv_amount_str = num_format_coin(truncate(amount / len(answered_msg_id['right_ids']), 12)) if \
                len(answered_msg_id['right_ids']) > 0 else num_format_coin(truncate(amount, 12))
            indiv_amount = truncate(amount / len(answered_msg_id['right_ids']), 12) if \
                len(answered_msg_id['right_ids']) > 0 else truncate(amount, 12)

            amount_in_usd = 0.0
            each_amount_in_usd = 0.0

            total_equivalent_usd = ""
            per_unit = None
            if price_with:
                per_unit = get_mathtip['unit_price_usd']
                if per_unit and per_unit > 0 and len(answered_msg_id['right_ids']) > 0:
                    each_amount_in_usd = per_unit * float(indiv_amount)
                    if each_amount_in_usd > 0.0001:
                        num = len(answered_msg_id['right_ids']) if len(answered_msg_id['right_ids']) > 0 else 1
                        total_equivalent_usd = " ~ {:,.4f} USD".format(each_amount_in_usd * num)
                elif per_unit and per_unit > 0 and len(answered_msg_id['right_ids']) == 0:
                    each_amount_in_usd = per_unit * float(indiv_amount)
                    total_equivalent_usd = " ~ {:,.4f} USD".format(each_amount_in_usd)

            embed = disnake.Embed(
                title=f"ðŸ§® Math Tip {coin_emoji}{num_format_coin(amount)} "\
                    f"{token_display} {total_equivalent_usd} - Total answer {total_answer}",
                description=get_mathtip['eval_content'],
                timestamp=datetime.fromtimestamp(get_mathtip['math_endtime']
            )
            )
            embed.add_field(
                name="Correct answer",
                value=get_mathtip['eval_answer'],
                inline=False
            )
            embed.add_field(
                name="Correct ( {} )".format(len(answered_msg_id['right_ids'])),
                value="{}".format(
                    " | ".join(answered_msg_id['right_names']) if len(answered_msg_id['right_names']) > 0 else "N/A"),
                inline=False
            )
            embed.add_field(
                name="Incorrect ( {} )".format(len(answered_msg_id['wrong_ids'])),
                value="{}".format(
                    " | ".join(answered_msg_id['wrong_names']) if len(answered_msg_id['wrong_names']) > 0 else "N/A"),
                inline=False
            )
            if len(answered_msg_id['right_ids']) > 0:
                embed.add_field(
                    name='Each Winner Receives:',
                    value=f"{coin_emoji}{indiv_amount_str} {token_display}",
                    inline=True
                )
            embed.set_footer(text=f"MathTip by {owner_displayname}")

            if len(answered_msg_id['right_ids']) > 0:
                await store.sql_user_balance_mv_multiple(
                    get_mathtip['from_userid'], answered_msg_id['right_ids'],
                    get_mathtip['guild_id'], get_mathtip['channel_id'],
                    float(indiv_amount), coin_name, "MATHTIP", coin_decimal, SERVER_BOT, contract,
                    float(each_amount_in_usd), None
                )
            # Change status
            change_status = await store.discord_mathtip_update(get_mathtip['message_id'], "COMPLETED")
            await original_message.edit(embed=embed, view=self)
        else:
            await original_message.edit(view=self)

class MathTips(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.wallet_api = WalletAPI(self.bot)
        self.utils = Utils(self.bot)

        self.math_duration_min = 5
        self.math_duration_max = 45

        self.max_ongoing_by_user = self.bot.config['discord']['max_ongoing_by_user']
        self.max_ongoing_by_guild = self.bot.config['discord']['max_ongoing_by_guild']

    async def async_mathtip(self, ctx, amount: str, token: str, duration: str, math_exp: str = None):
        coin_name = token.upper()
        if len(self.bot.coin_alias_names) > 0 and coin_name in self.bot.coin_alias_names:
            coin_name = self.bot.coin_alias_names[coin_name]
        # Token name check
        if not hasattr(self.bot.coin_list, coin_name):
            msg = f'{ctx.author.mention}, **{coin_name}** does not exist with us.'
            await ctx.response.send_message(msg)
            return
        # End token name check

        await ctx.response.send_message(f"{ctx.author.mention}, /mathtip preparation... ")

        serverinfo = self.bot.other_data['guild_list'].get(str(ctx.guild.id))
        if serverinfo and serverinfo['tiponly'] and serverinfo['tiponly'] != "ALLCOIN" and coin_name not in serverinfo[
            'tiponly'].split(","):
            allowed_coins = serverinfo['tiponly']
            msg = f"{ctx.author.mention}, **{coin_name}** is not allowed here. Currently, allowed `{allowed_coins}`. "\
                "You can ask guild owner to allow. `/SETTING TIPONLY coin1,coin2,...`"
            await ctx.edit_original_message(content=msg)
            return

        try:
            self.bot.commandings.append((str(ctx.guild.id) if hasattr(ctx, "guild") and hasattr(ctx.guild, "id") else "DM",
                                         str(ctx.author.id), SERVER_BOT, "/mathtip", int(time.time())))
            await self.utils.add_command_calls()
        except Exception:
            traceback.print_exc(file=sys.stdout)

        # check lock
        try:
            is_user_locked = self.utils.is_locked_user(str(ctx.author.id), SERVER_BOT)
            if is_user_locked is True:
                await ctx.edit_original_message(
                    content = f"{EMOJI_RED_NO} {ctx.author.mention}, your account is locked for using the Bot. "\
                    "Please contact bot dev by /about link."
                )
                return
        except Exception:
            traceback.print_exc(file=sys.stdout)
        # end check lock

        # Check if there is many airdrop/mathtip/triviatip
        try:
            count_ongoing = await store.discord_freetip_ongoing(str(ctx.author.id), "ONGOING")
            if count_ongoing >= self.max_ongoing_by_user and \
                ctx.author.id != self.bot.config['discord']['owner_id']:
                msg = f"{EMOJI_INFORMATION} {ctx.author.mention}, you still have some ongoing tips. "\
                    f"Please wait for them to complete first!"
                await ctx.edit_original_message(content=msg)
                return
            count_ongoing = await store.discord_freetip_ongoing_guild(str(ctx.guild.id), "ONGOING")
            # Check max if set in guild
            if serverinfo and count_ongoing >= serverinfo['max_ongoing_drop'] and\
                 ctx.author.id != self.bot.config['discord']['owner_id']:
                msg = f"{EMOJI_INFORMATION} {ctx.author.mention}, there are still some ongoing drops"\
                    f" or tips in this guild. Please wait for them to complete first!"
                await ctx.edit_original_message(content=msg)
                return
            elif serverinfo is None and count_ongoing >= self.max_ongoing_by_guild and\
                 ctx.author.id != self.bot.config['discord']['owner_id']:
                msg = f"{EMOJI_INFORMATION} {ctx.author.mention}, there are still some ongoing drops or"\
                    f" tips in this guild. Please wait for them to complete first!"
                await ctx.edit_original_message(content=msg)
                await logchanbot(f"[MATHTIP] server {str(ctx.guild.id)} has no data in discord_server.")
                return
        except Exception:
            traceback.print_exc(file=sys.stdout)
        # End of ongoing check

        try:
            token_display = getattr(getattr(self.bot.coin_list, coin_name), "display_name")
            contract = getattr(getattr(self.bot.coin_list, coin_name), "contract")
            try:
                coin_emoji = ""
                if ctx.guild.get_member(int(self.bot.user.id)).guild_permissions.external_emojis is True:
                    coin_emoji = getattr(getattr(self.bot.coin_list, coin_name), "coin_emoji_discord")
                    coin_emoji = coin_emoji + " " if coin_emoji else ""
            except Exception:
                traceback.print_exc(file=sys.stdout)
            net_name = getattr(getattr(self.bot.coin_list, coin_name), "net_name")
            type_coin = getattr(getattr(self.bot.coin_list, coin_name), "type")
            deposit_confirm_depth = getattr(getattr(self.bot.coin_list, coin_name), "deposit_confirm_depth")
            coin_decimal = getattr(getattr(self.bot.coin_list, coin_name), "decimal")
            min_tip = getattr(getattr(self.bot.coin_list, coin_name), "real_min_tip")
            max_tip = getattr(getattr(self.bot.coin_list, coin_name), "real_max_tip")
            price_with = getattr(getattr(self.bot.coin_list, coin_name), "price_with")
            get_deposit = await self.wallet_api.sql_get_userwallet(
                str(ctx.author.id), coin_name, net_name, type_coin, SERVER_BOT, 0
            )
            if get_deposit is None:
                get_deposit = await self.wallet_api.sql_register_user(
                    str(ctx.author.id), coin_name, net_name, type_coin, SERVER_BOT, 0
                )

            wallet_address = get_deposit['balance_wallet_address']
            if type_coin in ["TRTL-API", "TRTL-SERVICE", "BCN", "XMR"]:
                wallet_address = get_deposit['paymentid']
            elif type_coin in ["XRP"]:
                wallet_address = get_deposit['destination_tag']

        except Exception:
            traceback.print_exc(file=sys.stdout)
            msg = f"{EMOJI_RED_NO} {ctx.author.mention}, some internal error. Please try again."
            await ctx.edit_original_message(content=msg)
            return

        height = await self.wallet_api.get_block_height(type_coin, coin_name, net_name)
        # check if amount is all
        all_amount = False
        if not amount.isdigit() and amount.upper() == "ALL":
            all_amount = True
            userdata_balance = await store.sql_user_balance_single(
                str(ctx.author.id), coin_name, wallet_address, type_coin,
                height, deposit_confirm_depth, SERVER_BOT
            )
            amount = float(userdata_balance['adjust'])
        # If $ is in amount, let's convert to coin/token
        elif "$" in amount[-1] or "$" in amount[0]:  # last is $
            # Check if conversion is allowed for this coin.
            amount = amount.replace(",", "").replace("$", "")
            if price_with is None:
                msg = f"{EMOJI_RED_NO} {ctx.author.mention}, dollar conversion is not "\
                    f"enabled for this `{coin_name}`."
                await ctx.edit_original_message(content=msg)
                return
            else:
                per_unit = await self.utils.get_coin_price(coin_name, price_with)
                if per_unit and per_unit['price'] and per_unit['price'] > 0:
                    per_unit = per_unit['price']
                    amount = float(Decimal(amount) / Decimal(per_unit))
                else:
                    msg = f"{EMOJI_RED_NO} {ctx.author.mention}, I cannot fetch equivalent price. "\
                        f"Try with different method."
                    await ctx.edit_original_message(content=msg)
                    return
        else:
            amount = amount.replace(",", "")
            amount = text_to_num(amount)
            if amount is None:
                msg = f'{EMOJI_RED_NO} {ctx.author.mention}, invalid given amount.'
                await ctx.edit_original_message(content=msg)
                return
        # end of check if amount is all

        # Check if tx in progress
        if str(ctx.author.id) in self.bot.tipping_in_progress and \
            int(time.time()) - self.bot.tipping_in_progress[str(ctx.author.id)] < 150:
            msg = f"{EMOJI_ERROR} {ctx.author.mention}, you have another transaction in progress."
            await ctx.edit_original_message(content=msg)
            return

        try:
            amount = float(amount)
        except ValueError:
            msg = f'{EMOJI_RED_NO} {ctx.author.mention}, invalid amount.'
            await ctx.edit_original_message(content=msg)
            return

        if amount <= 0:
            msg = f'{EMOJI_RED_NO} {ctx.author.mention}, please get more {token_display}.'
            await ctx.edit_original_message(content=msg)
            return

        def hms_to_seconds(time_string):
            duration_in_second = 0
            if time_string.isdigit():
                return int(time_string)
            try:
                time_string = time_string.replace("hours", "h")
                time_string = time_string.replace("hour", "h")
                time_string = time_string.replace("hrs", "h")
                time_string = time_string.replace("hr", "h")

                time_string = time_string.replace("minutes", "mn")
                time_string = time_string.replace("mns", "mn")
                time_string = time_string.replace("mins", "mn")
                time_string = time_string.replace("min", "mn")
                time_string = time_string.replace("mn", "mn")
                mult = {'h': 60 * 60, 'mn': 60, 's': 1}
                duration_in_second = sum(
                    int(num) * mult.get(val, 1) for num, val in re.findall('(\d+)(\w+)', time_string))
            except Exception:
                traceback.print_exc(file=sys.stdout)
            return duration_in_second

        default_duration = 60
        duration_s = 0
        try:
            duration_s = hms_to_seconds(duration)
        except Exception:
            traceback.print_exc(file=sys.stdout)
            msg = f'{EMOJI_RED_NO} {ctx.author.mention}, invalid duration.'
            await ctx.edit_original_message(content=msg)
            return

        if duration_s == 0:
            # Skip message
            duration_s = default_duration
            # Just info, continue
        elif duration_s < self.math_duration_min or duration_s > self.math_duration_max:
            msg = f"{EMOJI_RED_NO} {ctx.author.mention}, invalid duration. "\
                f"Please use between {str(self.math_duration_min)}s to {str(self.math_duration_max)}s."
            await ctx.edit_original_message(content=msg)
            return

        try:
            amount = float(amount)
        except ValueError:
            msg = f'{EMOJI_RED_NO} {ctx.author.mention}, invalid amount.'
            await ctx.edit_original_message(content=msg)
            return

        result_float = None
        wrong_answer_1 = None
        wrong_answer_2 = None
        wrong_answer_3 = None
        eval_string_original = ""
        if math_exp and len(math_exp) > 0:
            eval_string_original = math_exp
            math_exp = math_exp.replace(",", "").replace(" ", "")
            supported_function = ['+', '-', '*', '/', '(', ')', '.', ',', '!', '^']
            additional_support = ['exp', 'sqrt', 'abs', 'log10', 'log', 'sinh', 'cosh', 'tanh', 'sin', 'cos', 'tan']
            has_operation = False
            for each_op in ['exp', 'sqrt', 'abs', 'log10', 'log', 'sinh', 'cosh', 'tanh', 'sin', 'cos', 'tan', '+', '-',
                            '*', '/', '!', '^']:
                if each_op in math_exp: has_operation = True
            if has_operation is False:
                msg = f'{EMOJI_RED_NO} {ctx.author.mention}, nothing to calculate.'
                await ctx.edit_original_message(content=msg)
                return
            test_string = math_exp
            for each in additional_support:
                test_string = test_string.replace(each, "")
            if all([c.isdigit() or c in supported_function for c in test_string]):
                try:
                    result = numexpr.evaluate(math_exp).item()
                    listrand = [2, 3, 4, 5, 6, 7, 8, 9, 10]
                    # OK have result. Check it if it's bigger than 10**10 or below 0.0001
                    if abs(result) > 10 ** 10:
                        msg = f'{EMOJI_RED_NO} Result for `{eval_string_original}` is too big.'
                        await ctx.edit_original_message(content=msg)
                        return
                    elif abs(result) < 0.0001:
                        msg = f'{EMOJI_RED_NO} Result for `{eval_string_original}` is too small.'
                        await ctx.edit_original_message(content=msg)
                        return
                    else:
                        # store result in float XX.XXXX
                        if result >= 0:
                            result_float = truncate(float(result), 4)
                            wrong_answer_1 = truncate(float(result * random.choice(listrand)), 4)
                            wrong_answer_2 = truncate(float(result + random.choice(listrand)), 4)
                            wrong_answer_3 = truncate(float(result - random.choice(listrand)), 4)
                        else:
                            result_float = - abs(truncate(float(result), 4))
                            wrong_answer_1 = - abs(truncate(float(result * random.choice(listrand)), 4))
                            wrong_answer_2 = - abs(truncate(float(result + random.choice(listrand)), 4))
                            wrong_answer_3 = - abs(truncate(float(result - random.choice(listrand)), 4))
                except Exception:
                    msg = f'{EMOJI_RED_NO}, invalid result for `{eval_string_original}`.'
                    await ctx.edit_original_message(content=msg)
                    return
            else:
                msg = f'{EMOJI_ERROR} {ctx.author.mention}, unsupported usage for `{eval_string_original}`.'
                await ctx.edit_original_message(content=msg)
                return
        else:
            msg = f'{EMOJI_RED_NO} {ctx.author.mention}, invalid math expression.'
            await ctx.edit_original_message(content=msg)
            return

        userdata_balance = await store.sql_user_balance_single(
            str(ctx.author.id), coin_name, wallet_address, type_coin,
            height, deposit_confirm_depth, SERVER_BOT
        )
        actual_balance = float(userdata_balance['adjust'])

        if amount > max_tip or amount < min_tip:
            msg = f"{EMOJI_RED_NO} {ctx.author.mention}, transactions cannot be bigger "\
                f"than **{num_format_coin(max_tip)} {token_display}** "\
                f"or smaller than **{num_format_coin(min_tip)} {token_display}**."
            await ctx.edit_original_message(content=msg)
            return
        elif amount > actual_balance:
            msg = f"{EMOJI_RED_NO} {ctx.author.mention}, insufficient balance to do a math tip of "\
                f"**{num_format_coin(amount)} {token_display}**."
            await ctx.edit_original_message(content=msg)
            return

        ## add to queue
        if str(ctx.author.id) not in self.bot.tipping_in_progress:
            self.bot.tipping_in_progress[str(ctx.author.id)] = int(time.time())

        equivalent_usd = ""
        total_in_usd = 0.0
        per_unit = None
        if price_with:
            native_token_name = getattr(getattr(self.bot.coin_list, coin_name), "native_token_name")
            coin_name_for_price = coin_name
            if native_token_name:
                coin_name_for_price = native_token_name
            if coin_name_for_price in self.bot.token_hints:
                id = self.bot.token_hints[coin_name_for_price]['ticker_name']
                per_unit = self.bot.coin_paprika_id_list[id]['price_usd']
            else:
                per_unit = self.bot.coin_paprika_symbol_list[coin_name_for_price]['price_usd']
            if per_unit and per_unit > 0:
                total_in_usd = float(Decimal(amount) * Decimal(per_unit))
                if total_in_usd >= 0.0001:
                    equivalent_usd = " ~ {:,.4f} USD".format(total_in_usd)

        owner_displayname = "{}#{}".format(ctx.author.name, ctx.author.discriminator)
        embed = disnake.Embed(
            title=f"ðŸ§® Math Tip {coin_emoji}{num_format_coin(amount)} {token_display} {equivalent_usd}",
            description=eval_string_original, timestamp=datetime.fromtimestamp(int(time.time()) + duration_s))
        embed.add_field(
            name="Answering",
            value="None",
            inline=False
        )
        embed.set_footer(text=f"Math tip by {owner_displayname}")

        answers = [str(result_float), str(wrong_answer_1), str(wrong_answer_2), str(wrong_answer_3)]
        random.shuffle(answers)
        index_answer = answers.index(str(result_float))
        try:
            view = MathButton(self.bot, ctx, answers, index_answer, duration_s, self.bot.coin_list)
            view.message = await ctx.original_message()
            await store.insert_discord_mathtip(
                coin_name, contract, str(ctx.author.id),
                owner_displayname, str(view.message.id),
                eval_string_original, result_float, wrong_answer_1,
                wrong_answer_2, wrong_answer_3, str(ctx.guild.id),
                str(ctx.channel.id), amount, total_in_usd,
                equivalent_usd, per_unit, coin_decimal,
                int(time.time()) + duration_s, net_name
            )
            await ctx.edit_original_message(content=None, embed=embed, view=view)
        except disnake.errors.Forbidden:
            await ctx.edit_original_message(content="Missing permission! Or failed to send embed message.")
        except Exception:
            traceback.print_exc(file=sys.stdout)
        try:
            del self.bot.tipping_in_progress[str(ctx.author.id)]
        except Exception:
            pass

    @commands.guild_only()
    @commands.bot_has_permissions(send_messages=True)
    @commands.slash_command(
        dm_permission=False,
        usage='mathtip <amount> <token> <duration> <math expression>',
        options=[
            Option('amount', 'amount', OptionType.string, required=True),
            Option('token', 'token', OptionType.string, required=True),
            Option('duration', 'duration', OptionType.string, required=True),
            Option('math_exp', 'math_exp', OptionType.string, required=True)
        ],
        description="Spread math tip by user's answer"
    )
    async def mathtip(
        self,
        ctx,
        amount: str,
        token: str,
        duration: str,
        math_exp: str
    ):
        await self.async_mathtip(ctx, amount, token, duration, math_exp)

    @mathtip.autocomplete("token")
    async def mathtip_token_name_autocomp(self, inter: disnake.CommandInteraction, string: str):
        string = string.lower()
        return [name for name in self.bot.coin_name_list if string in name.lower()][:10]

def setup(bot):
    bot.add_cog(MathTips(bot))


import dataclasses
import io
import logging
import math
import shutil
import tempfile
from pathlib import Path

import discord
import graphviz
import PIL.Image
from discord import Embed
from discord.ext.commands import Context, Converter
from PIL import ImageDraw

from randovania.game_description import default_database, pretty_print
from randovania.game_description.db.area import Area
from randovania.game_description.db.node import Node, NodeLocation
from randovania.game_description.db.region import Region
from randovania.game_description.game_description import GameDescription
from randovania.games.game import RandovaniaGame
from randovania.lib import enum_lib
from randovania.server.discord.bot import RandovaniaBot
from randovania.server.discord.randovania_cog import RandovaniaCog

@dataclasses.dataclass()
class AreaWidget:
    area: Area
    command_id: str
    view: discord.ui.View = None

@dataclasses.dataclass()
class SplitRegion:
    region: Region
    name: str
    areas: list[AreaWidget]
    command_id: str
    view: discord.ui.View = None

def render_area_with_graphviz(area: Area) -> io.BytesIO | None:
    dot = graphviz.Digraph(comment=area.name)
    for node in area.nodes:
        if node.is_derived_node:
            continue
        dot.node(node.name)

    known_edges = set()
    for source, target in area.connections.items():
        if source.is_derived_node:
            continue

        for target_node, requirement in target.items():
            if target_node.is_derived_node:
                continue

            direction = None
            if source in area.connections.get(target_node):
                direction = "both"
                known_edges.add((target_node.name, source.name))

            if (source.name, target_node.name) not in known_edges:
                dot.edge(source.name, target_node.name, dir=direction)
                known_edges.add((source.name, target_node.name))

    output_dir = tempfile.mkdtemp()
    try:
        p = Path(dot.render(directory=output_dir, format="png", cleanup=True))
        return io.BytesIO(p.read_bytes())

    except graphviz.backend.ExecutableNotFound as e:
        logging.info("Unable to render graph for %s: %s", area.name, str(e))
        return None
    finally:
        shutil.rmtree(output_dir)

def render_area_with_pillow(area: Area, data_path: Path) -> io.BytesIO | None:
    image_path = data_path.joinpath("assets", "maps", f"{area.map_name}.png")
    if not image_path.exists():
        return None

    with PIL.Image.open(image_path) as im:
        assert isinstance(im, PIL.Image.Image)

        def location_to_pos(loc: NodeLocation):
            return loc.x, im.height - loc.y

        draw = ImageDraw.Draw(im)

        def draw_connections_from(source_node: Node):
            for target_node, requirement in area.connections[source_node].items():
                if target_node.is_derived_node:
                    continue

                source = location_to_pos(source_node.location)
                target = location_to_pos(target_node.location)

                if sum((a - b) ** 2 for a, b in zip(source, target)) < 4:
                    continue

                draw.line(source + target, width=2, fill=(255, 255, 255, 255))
                draw.line(source + target, width=1, fill=(0, 0, 0, 255))

        for node in area.nodes:
            if not node.is_derived_node:
                draw_connections_from(node)

        for node in area.nodes:
            if node.location is None or node.is_derived_node:
                continue

            p = location_to_pos(node.location)
            draw.ellipse((p[0] - 5, p[1] - 5, p[0] + 5, p[1] + 5), fill=(255, 255, 255, 255), width=5)
            draw.ellipse((p[0] - 5, p[1] - 5, p[0] + 5, p[1] + 5), fill=(0, 0, 0, 255), width=4)
            draw.text(
                (p[0] - draw.textlength(node.name) / 2, p[1] + 15),
                node.name,
                stroke_width=2,
                stroke_fill=(255, 255, 255, 255),
            )
            draw.text(
                (p[0] - draw.textlength(node.name) / 2, p[1] + 15),
                node.name,
                stroke_width=1,
                stroke_fill=(0, 0, 0, 255),
            )

        result = io.BytesIO()
        im.save(result, "PNG")

    result.seek(0)
    return result

async def create_split_regions(db: GameDescription) -> list[SplitRegion]:
    region_options = []

    def create_id():
        return f"{db.game.value}_region_{len(region_options)}"

    def create_areas(a):
        return [AreaWidget(it, f"{create_id()}_area_{i}") for i, it in enumerate(a)]

    for region in db.region_list.regions:
        for use_dark_name in [False, True]:
            if use_dark_name and region.dark_name is None:
                continue

            areas = sorted(
                (area for area in region.areas if area.in_dark_aether == use_dark_name and area.nodes),
                key=lambda it: it.name,
            )
            name = region.correct_name(use_dark_name)
            if len(areas) > 25:
                per_part = math.ceil(len(areas) / math.ceil(len(areas) / 25))

                while areas:
                    areas_part = areas[:per_part]
                    del areas[:per_part]

                    region_options.append(
                        SplitRegion(
                            region,
                            f"{name} ({areas_part[0].name[:2]}-{areas_part[-1].name[:2]})",
                            create_areas(areas_part),
                            create_id(),
                        )
                    )
            else:
                region_options.append(SplitRegion(region, name, create_areas(areas), create_id()))

    region_options.sort(key=lambda it: it.name)
    return region_options

class EnumConverter(Converter):
    async def convert(self, ctx: Context, argument: str):
        return RandovaniaGame(argument)

_GameChoices = discord.Option(
    str,
    description="The game's database to check.",
    choices=[
        discord.OptionChoice(name=game.long_name, value=game.value) for game in enum_lib.iterate_enum(RandovaniaGame)
    ],
)
_GameChoices.converter = EnumConverter()
_GameChoices._raw_type = RandovaniaGame

class SelectNodesItem(discord.ui.Select):
    def __init__(self, game: RandovaniaGame, area: AreaWidget):
        self.game = game
        self.area = area

        options = [discord.SelectOption(label=node.name) for node in self.valid_nodes()]

        super().__init__(
            custom_id=area.command_id,
            placeholder="Choose the nodes",
            options=options,
            max_values=min(10, len(options)),
        )

    def valid_nodes(self):
        return [node for node in sorted(self.area.area.nodes, key=lambda it: it.name) if not node.is_derived_node]

    def _describe_selected_connections(self, original_content: str):
        db = default_database.game_description_for(self.game)

        def snipped_message(n: str) -> str:
            return f"\n{n}: *Skipped*\n"

        body_by_node = {}
        embeds = []

        for node in sorted(self.area.area.nodes, key=lambda it: it.name):
            if node.name not in self.values:
                continue

            name = node.name
            if node.heal:
                name += " (Heals)"
            if self.area.area.default_node == node.name:
                name += "; Default Node"

            body = pretty_print.pretty_print_node_type(node, db.region_list) + "\n"

            node_bodies = []

            for target_node, requirement in db.region_list.area_connections_from(node):
                if target_node.is_derived_node:
                    continue

                extra_lines = []
                for level, text in pretty_print.pretty_print_requirement(requirement.simplify()):
                    extra_lines.append("{}{}".format("  " * level, text))

                inner = "\n".join(extra_lines)
                new_entry = f"\n{target_node.name}:\n```\n{inner}\n```"
                node_bodies.append([target_node.name, new_entry])

            space_left = 4096 - len(body)
            for node_name, _ in node_bodies:
                space_left -= len(snipped_message(node_name))

            node_bodies.sort(key=lambda it: len(it[1]))
            for node_i, (node_name, node_entry) in enumerate(node_bodies):
                snipped = snipped_message(node_name)
                space_left += len(snipped)
                if len(node_entry) <= space_left:
                    space_left -= len(node_entry)
                else:
                    node_bodies[node_i][1] = snipped

            node_bodies.sort(key=lambda it: it[0])
            for _, node_entry in node_bodies:
                body += node_entry

            body_by_node[name] = body

        snipped = "*(message too long, skipped)*"
        space_left = 6000 - len(original_content)
        for name, body in body_by_node.items():
            space_left -= len(name) + len(snipped)

        for name, body in sorted(body_by_node.items(), key=lambda it: len(it[1])):
            space_left += len(snipped)
            if len(body) <= space_left:
                space_left -= len(body)
            else:
                body_by_node[name] = snipped

        for name, body in body_by_node.items():
            embeds.append(
                discord.Embed(
                    title=name,
                    description=body,
                )
            )

        return embeds

    async def callback(self, interaction: discord.Interaction):
        r = interaction.response
        assert isinstance(r, discord.InteractionResponse)

        await r.defer()
        original_response = await interaction.original_response()

        try:
            embeds = self._describe_selected_connections(original_response.content)
        except Exception as e:
            logging.exception("Error updating visible nodes of %s: %s", self.area.area.name, str(self.values))
            embeds = [
                discord.Embed(
                    title="Error describing area",
                    description=str(e),
                )
            ]

        logging.info("Updating visible nodes of %s: %s", self.area.area.name, str(self.values))
        await original_response.edit(
            embeds=embeds,
        )

class SelectAreaItem(discord.ui.Select):
    def __init__(self, game: RandovaniaGame, split_region: SplitRegion):
        self.game = game
        self.split_region = split_region

        options = [
            discord.SelectOption(
                label=area.area.name,
                value=area.command_id,
            )
            for area in split_region.areas
        ]
        super().__init__(
            custom_id=split_region.command_id,
            placeholder="Choose the room",
            options=options,
        )

    async def callback(self, interaction: discord.Interaction):
        r = interaction.response
        assert isinstance(r, discord.InteractionResponse)
        option_selected = self.values[0]

        valid_items = [area for area in self.split_region.areas if area.command_id == option_selected]
        if not valid_items:
            await r.defer()
            return await interaction.edit_original_response(
                view=None,
                embeds=[],
                content=f"Invalid selected option, unable to find given db subset '{option_selected}'.",
            )
        area = valid_items[0]

        db = default_database.game_description_for(self.game)
        title = f"{self.game.long_name}: {db.region_list.area_name(area.area)}"

        files = []

        area_image = render_area_with_pillow(area.area, self.game.data_path)
        if area_image is not None:
            files.append(discord.File(area_image, filename=f"{area.area.name}_image.png"))

        area_graph = render_area_with_graphviz(area.area)
        if area_graph is not None:
            files.append(discord.File(area_graph, filename=f"{area.area.name}_graph.png"))

        logging.info("Responding to area for %s with %d attachments.", area.area.name, len(files))
        await r.send_message(
            content=f"**{title}**\nRequested by {interaction.user.display_name}.",
            files=files,
            view=area.view,
        )

class SelectSplitRegionItem(discord.ui.Select):
    def __init__(self, game: RandovaniaGame, split_regions: list[SplitRegion]):
        self.game = game
        self.split_regions = split_regions

        options = [
            discord.SelectOption(label=split_region.name, value=split_region.command_id)
            for split_region in split_regions
        ]

        super().__init__(
            custom_id=f"{game.value}_region",
            placeholder="Choose your region",
            options=options,
        )

    async def callback(self, interaction: discord.Interaction):
        r = interaction.response
        assert isinstance(r, discord.InteractionResponse)

        option_selected = self.values[0]

        valid_items = [it for it in self.split_regions if it.command_id == option_selected]
        if not valid_items:
            return await r.send_message(
                view=None,
                embeds=[],
                ephemeral=True,
                content=f"Invalid selected option, unable to find given db subset '{option_selected}'.",
            )
        split_region = valid_items[0]

        embed = Embed(
            title=f"{self.game.long_name} Database", description=f"Choose the room in {split_region.name} to visualize."
        )

        logging.info(
            "Responding to area selection for section %s with %d options.", split_region.name, len(split_region.areas)
        )
        return await r.send_message(
            embed=embed,
            view=split_region.view,
            ephemeral=True,
        )

class DatabaseCommandCog(RandovaniaCog):
    _split_regions: dict[RandovaniaGame, list[SplitRegion]]
    _select_split_region_view: dict[RandovaniaGame, discord.ui.View]

    def __init__(self, configuration: dict, bot: RandovaniaBot):
        self.configuration = configuration
        self.bot = bot
        self._split_regions = {}
        self._select_split_region_view = {}
        self._on_database_component_listener = {}

    @discord.commands.slash_command(name="database")
    async def database_inspect(self, context: discord.ApplicationContext, game: _GameChoices):
        """Consult the Randovania's logic database for one specific room."""
        assert isinstance(game, RandovaniaGame)

        embed = Embed(title=f"{game.long_name} Database", description="Choose the db subset to visualize.")
        view = self._select_split_region_view[game]
        logging.info("Responding requesting list of regions for game %s.", game.long_name)

        await context.respond(embed=embed, view=view, ephemeral=True)

    async def add_commands(self):
        for game in enum_lib.iterate_enum(RandovaniaGame):
            db = default_database.game_description_for(game)
            region_options = await create_split_regions(db)
            self._split_regions[game] = region_options

            view = discord.ui.View(
                SelectSplitRegionItem(game, region_options),
                timeout=None,
            )
            self.bot.add_view(view)
            self._select_split_region_view[game] = view

            for split_region in region_options:
                split_region.view = discord.ui.View(
                    SelectAreaItem(game, split_region),
                    timeout=None,
                )
                self.bot.add_view(split_region.view)
                for area in split_region.areas:
                    area.view = discord.ui.View(
                        SelectNodesItem(game, area),
                        timeout=None,
                    )
                    self.bot.add_view(area.view)

def setup(bot: RandovaniaBot):
    bot.add_cog(DatabaseCommandCog(bot.configuration, bot))


import argparse
import mmread_utils as mmu
import pickle as pkl

def parse_annots(fn):
    """
    Columns are:
    * 0 - run_barcode
    * 1 - cell_ontology_class
    * 17 - tissue
    """

    annots_dict = {}
    for i, line in enumerate(open(fn)):
        if i == 0: continue

        line = line.split(',')
        if len(line[1]) == 0:
            continue
        ## Combine cell types with spaces in them
        elif line[1][0] == '"':
            annot = [line[1]]
            idx = 2
            while line[idx][-1] != '"':
                annot.append(line[idx])
                idx += 1
            annot.append(line[idx])
            annot = ';'.join(annot)
            annot = annot[1:-1]
        else:
            annot = line[1]
            idx = 1

        re_rep = r"\s+"
        cell_type = (f'{re.subn(re_rep, "_", line[idx+16])[0]}:'
        f'{re.subn(re_rep, "_", annot)[0]}')
        annots_dict[line[0]] = cell_type

    return(annots_dict)

def load_sj_genes_num(gd):
    """
    Return dictionary structure:
    sj (chrom, start_pos, end_pos, strand):
        {gene_ids}
    """
    sj_list = f'{gd}/sjdbList.fromGTF.out.tab'

    sj_genes_dict = {}
    for line in open(sj_list, 'r'):
        line = line.strip().split('\t')
        # Fix chromosome
        line[0] = line[0].strip('chr')
        if line[0] == 'X':
            line[0] = '23'
        elif line[0] == 'Y':
            line[0] = '24'
        elif line[0][0] in {'M', 'm'}:
            line[0] = '25'
        elif not re.match(r'^[0-9]+$', line[0]):
            continue
        # Fix sign
        line[3] = '1' if line[3] == '+' else '2'
        idx = tuple(map(int, line[:4]))
        sj_genes_dict[idx] = set(map(int, line[4].split(',')))

    return(sj_genes_dict)

################################################################################
def get_args():
    parser = argparse.ArgumentParser(description='')

    parser.add_argument('-i')
    parser.add_argument('-o')
    parser.add_argument('-gd')
    parser.add_argument('-annots')

    return(parser.parse_args())

def main():
    args = get_args()

    annots_dict = parse_annots(args.annots)

    ## Load gene counts
    all_counts_gene_orig, mtx_fns_gene = mmu.load_all_gene_counts(args.i)

    ## Load SJ counts
    sj_annots = load_sj_genes_num(args.gd)
    all_sjs, sj_lists = mmu.get_all_sjs(args.i, sj_annots)
    all_counts_sj, mtx_fns_sj = mmu.load_all_sj_counts(args.i, all_sjs,
        sj_lists)

    ## Make sure both mats are in CSR format
    all_counts_sj = all_counts_sj.tocsr()
    all_counts_gene_orig = all_counts_gene_orig.tocsr()

    ## Need to add on to the mats so there's 1:1 sj:gene
    ##  (some SJs have multiple genes)
    all_counts_sj, all_counts_gene, sj_ord, g_idx = mmu.adjust_mats(
        all_counts_sj, all_counts_gene_orig, all_sjs, sj_annots)

    ## Split up gene and SJ counts
    genes_dict, _ = mmu.split_counts_mat(all_counts_gene, mtx_fns_gene,
        annots_dict)
    sj_dict, _ = mmu.split_counts_mat(all_counts_sj, mtx_fns_sj, annots_dict)
    genes_dict_orig, cell_bcs = mmu.split_counts_mat(all_counts_gene_orig, mtx_fns_gene,
        annots_dict)

    pkl.dump([sj_dict, genes_dict, sj_ord, genes_dict_orig, g_idx, cell_bcs],
        open(args.o, 'wb'))

if __name__ == '__main__':
    main()


import datetime
import matplotlib.pyplot as plt
import matplotlib.dates
import pandas as pd
import numpy as np
import os.path

class Graphs:
    def __init__(self, data_base):
        self.data_base = data_base

    def overall_results_per(self):
        correct = self.data_base["correct"].sum() / len(self.data_base)
        wrong = (self.data_base["correct"]==False).sum() / len(self.data_base)
        results = [correct, wrong]
        outcome = (0.30, 0.50)
        fig, ax = plt.subplots()
        ax.grid(axis="y", zorder=0, color="#9698A1")
        width = 0.15
        bars = ax.bar(outcome, results, width, zorder=3)
        bars[0].set_color('lightseagreen')
        bars[1].set_color('coral')
        ax.set_xticks((0.375, 0.575))
        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
        ax.yaxis.set_ticks_position('left')
        ax.xaxis.set_ticks_position('bottom')
        ax.set_xticklabels(('Right','Wrong'))
        fig.suptitle("Percentage Right/Wrong", fontsize=15)
        #plt.show()

    def results_per_gen(self):
        grouped = (self.data_base["correct"]==True).groupby(self.data_base["article"])
        correct = grouped.mean()
        incorrect = 1 - grouped.mean()
        gender_track = correct.append(incorrect)
        fig, ax = plt.subplots()
        ind_der = (1, 1.25)
        ind_die = (1.75, 2)
        ind_das = (2.50, 2.75)
        width = 0.25
        ax.grid(axis="y", zorder=0, color="#9698A1")
        bars_der = ax.bar(ind_der, gender_track["der"], width)
        bars_der[0].set_color('lightseagreen')
        bars_der[1].set_color('coral')
        bars_die = ax.bar(ind_die, gender_track["die"], width)
        bars_die[0].set_color('lightseagreen')
        bars_die[1].set_color('coral')
        bars_das = ax.bar(ind_das, gender_track["das"], width)
        bars_das[0].set_color('lightseagreen')
        bars_das[1].set_color('coral')
        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
        ax.yaxis.set_ticks_position('left')
        ax.xaxis.set_ticks_position('bottom')
        ax.set_xticks((1.25, 2, 2.75))
        ax.set_xticklabels(('der','die', 'das'))
        ax.set_xlim([0.9, 3.1])
        fig.suptitle("Percentage Right/Wrong by Gender", fontsize=15)
        # plt.show()

    def results_per_date(self):
        grouped = (self.data_base["correct"]==True).groupby(self.data_base["date"])
        correct = grouped.mean()
        wrong = 1 - grouped.mean()
        data_date = correct.append(wrong)
        fig, ax = plt.subplots()
        x_loc = np.array([0, 0.15])
        ax.grid(axis="y", zorder=0, color="#9698A1")
        ticks = []
        dates_played = []
        for date in pd.unique(data_date.index):
            width = 0.15
            x_loc += 0.75
            bars_date = ax.bar(x_loc, data_date[date], width, zorder=3)
            bars_date[0].set_color('lightseagreen')
            bars_date[1].set_color('coral')
            ticks.append((width+x_loc)[0])
            dates_played.append(datetime.datetime.strptime(date, '%d-%m-%Y' ).strftime("%d %b"))

        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
        ax.yaxis.set_ticks_position('left')
        ax.xaxis.set_ticks_position('bottom')
        ax.set_xticks(ticks)
        ax.set_xticklabels(dates_played)
        fig.suptitle("Right/Wrong by date", fontsize=15)
        ax.set_xlim([ax.get_xlim()[0], ax.get_xlim()[1]+0.15])
        # plt.show()

    def wrong_rank(self):
        wrong_words = self.data_base[self.data_base["correct"]==False]
        wrong_by_word = wrong_words.groupby(wrong_words["wort"])
        func = {"correct": "count", "article": lambda x: x.iloc[0]} ## "article": Getting the first value of entry
        wrong_by_word =  wrong_by_word.agg(func).rename(columns = {"correct": "wrong"})
        wrong_by_word = wrong_by_word.reset_index()
        wrong_ones_sort = wrong_by_word.sort_values(['wrong', 'wort'], ascending = [0, 0])
        first_twenty_wrong = wrong_ones_sort[0:20]
        col_num = np.arange(first_twenty_wrong.shape[0])
        fig, ax = plt.subplots(figsize=(8, 6))
        width = 0.65
        bars_worng = ax.bar(col_num, first_twenty_wrong["wrong"], width, color= 'powderblue'
                           , linewidth=0.5)
        ax.set_xlim([-0.5, ax.get_xlim()[1]])
        ax.set_ylim([0, ax.get_ylim()[1] +1])
        ax.set_title('Ranking of wrong words', fontsize=15)
        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
        ax.grid(axis="y", zorder=0, color="#9698A1")
        ax.yaxis.set_ticks_position('left')
        ax.xaxis.set_ticks_position('bottom')
        ax.set_yticks(np.arange(first_twenty_wrong.iloc[-1]["wrong"], first_twenty_wrong.iloc[0]["wrong"]+2, 1.0))
        ax.set_xticks(col_num + 0.325)
        ax.set_xticklabels(list(first_twenty_wrong['wort']))
        plt.setp(ax.get_xticklabels(), rotation=90)

        for article in range(len(list(first_twenty_wrong['article']))):
            ax.text(article + 0.4, 0.15 , list(first_twenty_wrong['article'])[article], rotation= 90,
                   horizontalalignment='center',
                verticalalignment='center', fontsize=12)
        # plt.tight_layout()
        # plt.show()

    def daily_stats(self):
        """Overall right/wrong percent by date"""
        grouped = (self.data_base["correct"]==True).groupby(self.data_base["date"])
        correct = grouped.mean().reset_index()
        correct["wrong"] =  1 - correct["correct"]
        correct["date"] = pd.to_datetime(correct["date"], dayfirst=True)
        correct = correct.sort_values("date")
        fig, ax = plt.subplots()
        left_limit = (datetime.datetime.strptime(pd.unique(self.data_base["date"])[0], '%d-%m-%Y' ) -
                      datetime.timedelta(days= 1)).date()
        right_limit = (datetime.datetime.strptime(pd.unique(self.data_base["date"])[-1], '%d-%m-%Y' )
                       + datetime.timedelta(days=1)).date()
        ax.plot(correct['date'], correct['correct'], marker = '.', color='lightseagreen',
                                           ms = 15, lw = 2, linestyle= '-' , label="correct" )
        ax.plot(correct['date'], correct['wrong'], color='coral', marker = '.',
                                           ms = 15, lw = 2, linestyle= '-', label="wrong"  )
        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
        ax.yaxis.set_ticks_position('left')
        ax.xaxis.set_ticks_position('bottom')
        ax.grid(axis="y", zorder=0, color="#9698A1")
        ax.set_xticks((correct['date'].values))
        ax.set_xlim([left_limit, right_limit])
        ax.set_ylim([0., 1.])
        ax.legend(loc='upper right').get_frame().set_alpha(0.3)
        ax.set_title('Daily Stats', fontsize=15)
        ax.set_xticklabels(correct['date'].map(lambda x: x.strftime("%d %b")))
        ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%d \n %b'))

    def daily_stats_per_gender(self):
        """Overall right/wrong percent by date and gender"""
        grouped = (self.data_base["correct"]==True).groupby(
            [self.data_base["date"], self.data_base["article"]])
        correct_gender = grouped.mean().reset_index()
        correct_gender["wrong"] = 1 - correct_gender["correct"]
        correct_gender = correct_gender.set_index(["date", "article"]).unstack().reset_index()
        correct_gender["date"] = pd.to_datetime(correct_gender["date"], dayfirst=True)
        correct_gender = correct_gender.sort_values("date")
        left_limit = (datetime.datetime.strptime(pd.unique(self.data_base["date"])[0], '%d-%m-%Y' )  -
                      datetime.timedelta(days= 1)).date()
        right_limit = (datetime.datetime.strptime(pd.unique(self.data_base["date"])[-1], '%d-%m-%Y' )
                       + datetime.timedelta(days=1)).date()
        fig= plt.figure()
        ax = fig.add_axes([0.1, 0.2, 0.85, 0.70])
        lab_desc, lab_chars = [], []
        gen_spec = {"der":"#6191C5" , "die":"#D56054" , "das": "#69B17D"}
        for gender, color_gen in gen_spec.items():
            ax.plot(correct_gender["date"], correct_gender["correct"][gender], color= color_gen, marker = '.',
                    ms = 12, lw = 2, linestyle= '-' )
            ax.plot(correct_gender["date"], correct_gender["wrong"][gender], color= color_gen, marker = '.',
                    ms = 12, lw = 2, linestyle= '--')
            leg_char_corr = plt.Line2D((0,1),(0,0), color=color_gen, marker='.', linestyle='-')
            leg_char_wrong = plt.Line2D((0,1),(0,0), color=color_gen, marker='.', linestyle='--')
            lab_chars.extend([leg_char_corr, leg_char_wrong])
            lab_desc.extend([gender + " right", gender + " wrong"])

        ax.spines['top'].set_visible(False)
        ax.spines['right'].set_visible(False)
        ax.yaxis.set_ticks_position('left')
        ax.xaxis.set_ticks_position('bottom')
        ax.grid(axis="y", zorder=0, color="#9698A1")
        ax.set_xticks((correct_gender['date'].values))
        ax.set_xlim([left_limit, right_limit])
        ax.set_ylim([0., 1.1])
        ax.set_title('Daily Stats per gender', fontsize=15)
        ax.set_xticklabels(correct_gender['date'].map(lambda x: x.strftime("%d %b")))
        ax.xaxis.set_major_formatter(matplotlib.dates.DateFormatter('%d \n %b'))
        ax.legend(lab_chars, lab_desc, loc='upper center',
                  bbox_to_anchor=(0.5, -0.15), fancybox=True, shadow=True, ncol=3)

    def generate_report(self):
        if not os.path.exists("img"):
            os.makedirs("img")
        self.overall_results_per()
        plt.savefig('img/overall_results_per', bbox_inches='tight')
        self.results_per_gen()
        plt.savefig('img/results_per_gen', bbox_inches='tight')
        self.results_per_date()
        plt.savefig('img/results_per_date', bbox_inches='tight')
        self.wrong_rank()
        plt.savefig('img/wrong_rank', bbox_inches='tight')
        self.daily_stats()
        plt.savefig('img/daily_stats', bbox_inches='tight')
        self.daily_stats_per_gender()
        plt.savefig('img/daily_stats_per_gender', bbox_inches='tight')


# 2016년 1월 1일은 금요일입니다. 2016년 a월 b일은 무슨 요일일까요? 두 수 a ,b를 입력받아 2016년 a월 b일이 무슨 요일인지 리턴하는 함수, solution을 완성하세요. 요일의 이름은 일요일부터 토요일까지 각각 SUN,MON,TUE,WED,THU,FRI,SAT

# 입니다. 예를 들어 a=5, b=24라면 5월 24일은 화요일이므로 문자열 "TUE"를 반환하세요.

# 제한 조건
# 2016년은 윤년입니다.
# 2016년 a월 b일은 실제로 있는 날입니다. (13월 26일이나 2월 45일같은 날짜는 주어지지 않습니다)

# jan = 31
# feb = 29
# march = 31
# apr = 30
# may = 31
# june = 30
# july = 31
# august = 31
# september = 30
# october = 31
# november = 30
# december = 31

# 방법 1
# def solution(a, b):
#     day = ['FRI', 'SAT', 'SUN', 'MON', 'TUE', 'WED', 'THU']
#     date = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
#     return day[(sum(date[:a-1]) + b) % 7  - 1]

# 다른 풀이 참고 방법2
# datetime 라이브러리 사용하기
# 과정1
    # print(datetime.datetime(2016, a, b))
# 과정2
    # print(datetime.datetime(2016, a, b).weekday())
import datetime

def solution(a, b):
    day = ['MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT', 'SUN']
    return day[datetime.datetime(2016, a, b).weekday()]

a = 5
b = 24
solution(a, b)


""" Main Qt widget and file as well. """

import sys

from PySide6.QtWidgets import QApplication, QWidget, QPushButton, QHBoxLayout
from PySide6.QtCore import QTimer

import globals
from main_gl_widget import MainGlWidget

# Important:
# You need to run the following command to generate the ui_form.py file
#     pyside6-uic form.ui -o ui_form.py, or
#     pyside2-uic form.ui -o ui_form.py
from ui_form import Ui_Widget

class Widget(QWidget):
    """ Program main Qt Widget. """

    def __init__(self, parent=None):
        super().__init__(parent)
        self.ui = Ui_Widget()
        self.ui.setupUi(self)

        self.setWindowTitle("Python based Conway's Game of Life")

        self.main_gl_widget = MainGlWidget(self)
        self.ui.horizontalLayout.insertWidget(0, self.main_gl_widget, 7)
        self.ui.horizontalLayout.setStretch(1, 4)

        self.__connect_signals()

        # Set refresh rate.
        timer = QTimer(self)
        timer.setInterval(1000 / globals.FPS)
        timer.timeout.connect(self.main_gl_widget.repaint)
        timer.start()

    def __connect_signals(self) -> None:
        self.ui.toggleButton.clicked.connect(self.__toggle_button_clicked)
        self.ui.rewindButton.clicked.connect(
            lambda: self.main_gl_widget.restart_game(globals.TEST_RLE)
        )

        self.ui.frequencySlider.valueChanged.connect(
            self.__freqency_slider_val_changed
        )

    def __toggle_button_clicked(self) -> None:
        idk = {"⏵": "⏸", "⏸": "⏵"}
        self.ui.toggleButton.setText(idk[self.ui.toggleButton.text()])
        self.main_gl_widget.toggle_game()

    def __freqency_slider_val_changed(self, freq: int) -> None:
        self.main_gl_widget.game.updater.set_frequency(freq)
        self.ui.frequencyLabel.setText(f"{freq}/s")

if __name__ == "__main__":
    app = QApplication(sys.argv)
    widget = Widget()
    widget.show()
    sys.exit(app.exec())


# Create your views here.
from bet_data_fetcher.models import *
from django.conf import settings
from django.core.cache import cache
from exceptions import *
import logging

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

def _fetch_data_from_sites():
    """ Fetches data from site classes in bet_sites.
    Supposed to iterate through to fetch
    """
    from bet_sites import StanJamer
    data = StanJamer().get_data()
    if reduce(lambda x, y: bool(x) or bool(y), data.values()):
        logger.info("Fetched data from StanJamer")
        return data
    else:
        #TODO: try other class
        return data

def _map_data_to_obj(data):
    def match_bet_values(bet_value_arr, bet_obj):
        bet_val_objs = BetValue.objects.filter(bet=bet_obj)
        logger.debug("Found %d bet values objects for %s bet" % (len(bet_val_objs), str(bet_obj)))
        for bet_val_dict in bet_value_arr:
            for bet_val_obj in bet_val_objs:
                if bet_val_obj.name == bet_val_dict['name']:
                    bet_val_dict['bet_val_obj'] = bet_val_obj
                    break
            if not bet_val_dict.has_key("bet_val_obj"):
                logger.info('Bet value %s does not exist for %s bet. Creating one.' % (bet_val_dict['name'], str(bet_obj)))
                bet_val_obj = BetValue.objects.create(name=bet_val_dict['name'], bet=bet_obj)
                bet_val_dict['bet_val_obj'] = bet_val_obj

    if data['tournament_data']['bets']:
        tourn_bet_obj = Bet.objects.exclude(tournament = None)
        if tourn_bet_obj:
            logger.debug('Tournament bet - %s found' % str(tourn_bet_obj))
            tourn_bet_obj = tourn_bet_obj[0]
        else:
            logger.info('No Tournament obj exists. Creating one.')
            categ, created = BetCategory.objects.get_or_create(name="Tournament Winner")
            if created:
                logger.info("New Bet Category %s created" % str(categ))
            tourn_bet_obj = Bet.objects.create(category=categ, match=None, tournament=Tournament.objects.all()[0])
        data['tournament_data']['bets'][0]['tourn_obj'] = tourn_bet_obj
        match_bet_values(data['tournament_data']['bets'][0]['values'], tourn_bet_obj)

    for matches_data in [data['live_data'], data['upcoming_data']]:
        for match_data in matches_data:
            logger.debug("Got match from stie - %s, %s" % (str(match_data['date'].date()), match_data['name']))
            match_data['match_obj'] = match_obj = Match.objects.get(match_date=match_data['date'].date(), name_lower=match_data['name'].lower())
            match_bet_objs = Bet.objects.filter(match=match_obj)
            logger.debug("Found %d bets for match - %s" % (len(match_bet_objs), str(match_obj)))
            for bet in match_data['bets']:
                for match_bet_obj in match_bet_objs:
                    if match_bet_obj.category.name == bet['name']:
                        bet['bet_obj'] = match_bet_obj
                        break
                if not bet.has_key('bet_obj'):
                    logger.info("Bet %s doesn't exists for match %s. Create one" % (bet['name'], str(match_obj)))
                    categ, created = BetCategory.objects.get_or_create(name=bet['name'])
                    if created:
                        logger.info("New Bet Category %s created" % str(categ))
                    match_bet_obj = Bet.objects.create(match=match_obj,category=categ)
                    bet['bet_obj'] = match_bet_obj
                match_bet_values(bet['values'], match_bet_obj)

def get_all_bet_data():
    data = cache.get('bet_data')
    if not data:
        data = _fetch_data_from_sites()
        _map_data_to_obj(data)
        cache.set('bet_data', data, settings.REFRESH_TIME)
    return data

def get_match_bet_data(match_id):
    if match_id is None:
        return
    data = get_all_bet_data()
    for match_bet_data in data['live_data'] + data['upcoming_data']:
        if match_bet_data['match_obj'].id == match_id:
            return match_bet_data
    return None

def get_tournament_bet_data():
    data = get_all_bet_data()
    return data['tournament_data']

def get_bet_value_data(bet_value_id):
    if bet_value_id is None:
        return

    data = get_all_bet_data()
    for bet_val_data in [i for match in (data['upcoming_data'] + data['live_data'] + [data['tournament_data']]) for bet in match['bets'] for i in bet['values']]:
        if bet_val_data['bet_val_obj'].id == bet_value_id:
            return bet_val_data
    return None


from __future__ import division
import os
import sys
import subprocess
import threading
import json
import numpy as np
import ast
import tempfile

# Assumes spice.jar is in the same directory as spice.py.  Change as needed.
SPICE_JAR = 'SPICE-1.0/spice-1.0.jar'
TEMP_DIR = 'tmp'
CACHE_DIR = 'cache'

class Spice:
    """
    Main Class to compute the SPICE metric
    """

    def float_convert(self, obj):
        try:
          return float(obj)
        except:
          return np.nan

    def compute_score(self, gts, res):
        assert(sorted(gts.keys()) == sorted(res.keys()))
        imgIds = sorted(gts.keys())

        # Prepare temp input file for the SPICE scorer
        input_data = []
        for id in imgIds:
            hypo = res[id]
            ref = gts[id]

            # Sanity check.
            assert(type(hypo) is list)
            assert(len(hypo) == 1)
            assert(type(ref) is list)
            assert(len(ref) >= 1)

            input_data.append({
              "image_id" : id,
              "test" : hypo[0],
              "refs" : ref
            })

        cwd = os.path.dirname(os.path.abspath("__file__"))
        temp_dir=os.path.join(cwd, TEMP_DIR)
        if not os.path.exists(temp_dir):
          os.makedirs(temp_dir)
        in_file = tempfile.NamedTemporaryFile(delete=False, dir=temp_dir, mode='w')
        json.dump(input_data, in_file, indent=2)
        in_file.close()

        # Start job
        out_file = tempfile.NamedTemporaryFile(delete=False, dir=temp_dir, mode='w')
        out_file.close()
        cache_dir=os.path.join(cwd, CACHE_DIR)
        if not os.path.exists(cache_dir):
          os.makedirs(cache_dir)
        spice_cmd = ['java', '-jar', '-Xmx8G', SPICE_JAR, in_file.name,
          '-cache', cache_dir,
          '-out', out_file.name,
          '-subset',
          '-silent'
        ]
        subprocess.check_call(spice_cmd,
            cwd=os.path.dirname(os.path.abspath("__file__")))

        # Read and process results
        with open(out_file.name) as data_file:
          results = json.load(data_file)
        os.remove(in_file.name)
        os.remove(out_file.name)

        imgId_to_scores = {}
        spice_scores = []
        for item in results:
          imgId_to_scores[item['image_id']] = item['scores']
          spice_scores.append(self.float_convert(item['scores']['All']['f']))
        average_score = np.mean(np.array(spice_scores))
        scores = []
        for image_id in imgIds:
          # Convert none to NaN before saving scores over subcategories
          score_set = {}
          for category,score_tuple in imgId_to_scores[image_id].items():
            score_set[category] = {k: self.float_convert(v) for k, v in score_tuple.items()}
          scores.append(score_set)
        return average_score, scores

    def method(self):
        return "SPICE"
#!/usr/bin/env python
#
# File Name : ptbtokenizer.py
#
# Description : Do the PTB Tokenization and remove punctuations.
#
# Creation Date : 29-12-2014
# Last Modified : Thu Mar 19 09:53:35 2015
# Authors : Hao Fang <hfang@uw.edu> and Tsung-Yi Lin <tl483@cornell.edu>

import os
import sys
import subprocess
import tempfile
import itertools

# path to the stanford corenlp jar
STANFORD_CORENLP_3_4_1_JAR = 'stanford-corenlp-3.4.1.jar'

# punctuations to be removed from the sentences
PUNCTUATIONS = ["''", "'", "``", "`", "-LRB-", "-RRB-", "-LCB-", "-RCB-", \
        ".", "?", "!", ",", ":", "-", "--", "...", ";"]

class PTBTokenizer:
    """Python wrapper of Stanford PTBTokenizer"""

    def tokenize(self, captions_for_image):
        cmd = ['java', '-cp', STANFORD_CORENLP_3_4_1_JAR, \
                'edu.stanford.nlp.process.PTBTokenizer', \
                '-preserveLines', '-lowerCase']

        # ======================================================
        # prepare data for PTB Tokenizer
        # ======================================================
        final_tokenized_captions_for_image = {}
        image_id = [k for k, v in captions_for_image.items() for _ in range(len(v))]
        sentences = '\n'.join([c['caption'].replace('\n', ' ') for k, v in captions_for_image.items() for c in v])

        # ======================================================
        # save sentences to temporary file
        # ======================================================
        path_to_jar_dirname=os.path.dirname(os.path.abspath("__file__"))
        tmp_file = tempfile.NamedTemporaryFile(delete=False, dir=path_to_jar_dirname, mode='w')
        tmp_file.write(sentences)
        tmp_file.close()

        # ======================================================
        # tokenize sentence
        # ======================================================
        cmd.append(os.path.basename(tmp_file.name))
        p_tokenizer = subprocess.Popen(cmd, cwd=path_to_jar_dirname, \
                stdout=subprocess.PIPE)
        token_lines = p_tokenizer.communicate(input=sentences.rstrip())[0]
        lines = token_lines.decode().split('\n')
        # remove temp file
        os.remove(tmp_file.name)

        # ======================================================
        # create dictionary for tokenized captions
        # ======================================================
        for k, line in zip(image_id, lines):
            if not k in final_tokenized_captions_for_image:
                final_tokenized_captions_for_image[k] = []
            tokenized_caption = ' '.join([w for w in line.rstrip().split(' ') \
                    if w not in PUNCTUATIONS])
            final_tokenized_captions_for_image[k].append(tokenized_caption)

        return final_tokenized_captions_for_image


import json
from typing import List, Dict, Any

def extract_json(string: str) -> List[Dict[str, Any]]:
    json_strings = []
    json_objects = []
    open_brackets = 0
    start_index = None

    for index, char in enumerate(string):
        if char == '{':
            open_brackets += 1
            if open_brackets == 1:
                start_index = index
        elif char == '}':
            open_brackets -= 1
            if open_brackets == 0:
                json_strings.append(string[start_index : index + 1])

    for json_str in json_strings:
        try:
            json_object = json.loads(json_str)
            json_objects.append(json_object)
        except json.JSONDecodeError as e:
            pass

    return json_objects

def loads_or_none(str:str):
    try:
        return json.loads(str)
    except:
        return None

# input_string = "博士：兔兔你好。{\"reply\": \"你好博士。\", \"mental\": \"\", \"activity\": \"\"}"

# extracted_json = extract_json(input_string)

# print(extracted_json)


#!/usr/bin/env python

"""The setup script."""

from setuptools import setup, find_packages

with open("README.rst") as readme_file:
    readme = readme_file.read()

with open("HISTORY.rst") as history_file:
    history = history_file.read()

with open("requirements.txt") as requirements_file:
    requirements = requirements_file.read().splitlines()

setup_requirements = [
    "pytest-runner",
]

test_requirements = [
    "pytest>=3",
]

setup(
    author="Harsh Parekh",
    author_email="harsh_parekh@outlook.com",
    python_requires=">=3.6",
    classifiers=[
        "Framework :: Flask",
        "Development Status :: 2 - Pre-Alpha",
        "Environment :: Web Environment",
        "Intended Audience :: Developers",
        "License :: OSI Approved :: MIT License",
        "Operating System :: OS Independent",
        "Natural Language :: English",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.6",
        "Programming Language :: Python :: 3.7",
        "Programming Language :: Python :: 3.8",
        "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
        "Topic :: Software Development :: Libraries :: Python Modules",
    ],
    description="Rest API on Flask made a little too easy.",
    install_requires=requirements,
    license="MIT license",
    long_description=readme + "\n\n" + history,
    include_package_data=True,
    keywords="flask_easyapi",
    name="flask_easyapi",
    packages=find_packages(include=["flask_easyapi", "flask_easyapi.*"]),
    setup_requires=setup_requirements,
    test_suite="tests",
    tests_require=test_requirements,
    url="https://github.com/hXtreme/flask-easyapi",
    version="0.1.1",
    zip_safe=False,
)


from Model.Animal import animalModel
from Controller.Animal import animalController
from fastapi import APIRouter, Depends, HTTPException

router = APIRouter(
    prefix="/animals",
    tags=["animals"],
    responses={404: {"description": "Not found"}},
)

_animalCon = animalController.animalController()

@router.get("/")
async def root():
    return _animalCon.getAllAnimal()

@router.get("/{animalname}")
async def root(animalname : str):
    return _animalCon.getAnimalbyName(animalname)

@router.put("/")
async def root(item : animalModel.animal):
    return _animalCon.insertAnimal(item)


from __future__ import annotations
import math
from typing import TYPE_CHECKING
from adapter.path_adapter import PathAdapter, PathAttributeID
from adapter.straight_adapter import StraightAdapter
from common.image_manager import ImageID
from entity_base.image.image_state import ImageState
from models.path_models.path_segment_state.abstract_segment_state import AbstractSegmentState, SerializedSegmentStateState
from models.path_models.path_segment_state.arc_segment_state import ArcSegmentState
from models.path_models.path_segment_state.bezier_segment_state import BezierSegmentState
from models.path_models.path_segment_state.segment_type import SegmentType
from models.path_models.path_segment_state.straight_segment_state import StraightSegmentState
from models.path_models.segment_direction import SegmentDirection
from entities.root_container.field_container.segment.arc_segment_entity import ArcSegmentEntity
from entities.root_container.field_container.segment.bezier_segment_entity import BezierSegmentEntity
from entities.root_container.field_container.segment.straight_segment_entity import StraightSegmentEntity
from models.project_history_interface import ProjectHistoryInterface
from services.constraint_solver_service import ConstraintSolver
from utility.format_functions import formatDegrees, formatInches
from utility.math_functions import distanceTuples, thetaFromPoints
if TYPE_CHECKING:
    from entities.root_container.field_container.field_entity import FieldEntity
    from models.path_models.path_node_model import PathNodeModel
    from models.path_models.path_model import PathModel
    from entity_base.entity import Entity

from models.path_models.path_element_model import PathElementModel, SerializedPathElementState

class SerializedPathSegmentState(SerializedPathElementState):

    def __init__(self, direction: SegmentDirection, states: dict[SegmentType, SerializedSegmentStateState], current: SegmentType):
        self.direction = direction
        self.states = states
        self.current = current

    def _deserialize(self, pathModel: PathModel) -> 'PathNodeModel':
        segment = PathSegmentModel(pathModel)
        segment.direction = self.direction

        segment.states = {}
        for key, value in self.states.items():
            segment.states[key] = value.deserialize(segment)

        segment.currentStateType = self.current

        segment.generateUI()
        return segment

    def makeAdapterDeserialized(self):
        for state in self.states.values():
            state.adapter.makeDeserialized()

class PathSegmentModel(PathElementModel):

    def makeAdapterSerialized(self):
        for state in self.states.values():
            state.adapter.makeSerialized()

    def _serialize(self) -> SerializedPathSegmentState:
        sStates: dict[SegmentType, SerializedSegmentStateState] = {}
        for key, value in self.states.items():
            sStates[key] = value.serialize()

        return SerializedPathSegmentState(self.direction, sStates, self.currentStateType)

    def __init__(self, pathModel: PathModel):
        super().__init__(pathModel)

        self.direction = SegmentDirection.FORWARD

        self.states: dict[SegmentType, AbstractSegmentState] = {
            SegmentType.STRAIGHT: StraightSegmentState(self),
            SegmentType.ARC: ArcSegmentState(self),
            SegmentType.BEZIER: BezierSegmentState(self)
        }

        self.currentStateType = SegmentType.STRAIGHT
        self.bConstraints = None # before theta constraint solver
        self.aConstraints = None # after theta constraint solver

        self.generateUI()

    """
    UPDATE methods that update values based on model state
    """

    # update thetas based on segment states and segment direction
    def updateThetas(self):

        # need to recompute state thetas first
        self.getState().onUpdate()

        self.START_THETA = self.getState().getStartTheta()
        self.END_THETA = self.getState().getEndTheta()

        # if segment is reversed, flip thetas
        if self.getDirection() == SegmentDirection.REVERSE:
            self.START_THETA = (self.START_THETA + math.pi) % (2 * math.pi)
            self.END_THETA = (self.END_THETA + math.pi) % (2 * math.pi)

        self.getAdapter().set(PathAttributeID.THETA1, self.START_THETA, formatDegrees(self.START_THETA))
        self.getAdapter().set(PathAttributeID.THETA2, self.END_THETA, formatDegrees(self.END_THETA))

    # called when the distance of the segment is changed
    def updateDistance(self):
        self.DISTANCE = distanceTuples(self.getPrevious().getPosition(), self.getNext().getPosition())
        # if segment is reversed, negate distance
        if self.getDirection() == SegmentDirection.REVERSE:
            self.DISTANCE *= -1

        self.getAdapter().set(PathAttributeID.DISTANCE, self.DISTANCE, formatInches(self.DISTANCE))

    # Update adapter for endpoint position
    def updateEndpointPosition(self, node: PathNodeModel):

        # new endpoint position
        pos = node.getPosition()

        # update the correct endpoint
        if self.getPrevious() == node:
            x, y = PathAttributeID.X1, PathAttributeID.Y1
        else:
            x, y = PathAttributeID.X2, PathAttributeID.Y2

        # set adapter
        self.getAdapter().set(x, pos[0], formatInches(pos[0]))
        self.getAdapter().set(y, pos[1], formatInches(pos[1]))

    """
    CALLBACK METHODS FOR WHEN THINGS NEED TO BE UPDATED
    """

    def onInit(self):
        self.updateThetas()
        self.updateDistance()
        self.updateEndpointPosition(self.getPrevious())
        self.updateEndpointPosition(self.getNext())

        self.getPrevious().onThetaChange()
        self.getNext().onThetaChange()

    def onInitSegmentOnly(self):
        self.updateThetas()
        self.updateDistance()
        self.updateEndpointPosition(self.getPrevious())
        self.updateEndpointPosition(self.getNext())

    # called when a node attached to segment is moved
    def onNodePositionChange(self, node: PathNodeModel = None):
        # assert that node is attached to segment
        assert(node is None or node == self.getPrevious() or node == self.getNext())

        # update segment start/end thetas
        self.updateThetas()
        self.getPrevious().onThetaChange()
        self.getNext().onThetaChange()

        # update segment distance
        self.updateDistance()

        if node is None:
            self.updateEndpointPosition(self.getPrevious())
            self.updateEndpointPosition(self.getNext())
            self.getPrevious().onThetaChange()
            self.getNext().onThetaChange()
        else:
            # update endpoint that changed
            self.updateEndpointPosition(node)
            # update the other endpoint's angle
            self.getOther(node).onThetaChange()

        # redraw segment ui. No need to update segment model as
        # segment endpoint positions are just refs to node models
        self.recomputeUI()

    """
    SETTER METHODS THAT MODIFY MODEL AND THEN SEND NOTIF TO UPDATE UI
    """

    def setState(self, type: SegmentType):

        assert(type in self.states)
        self.currentStateType = type

        # callback for state change
        self.getState().onSwitchToState()

        self.onInit()

        # regenerate ui with new state
        self.generateUI()
        self.recomputeUI()

        command = self.path.getCommandFromPath(self)
        command.setNewAdapter(self.getAdapter())
        command.rebuild()
        command.ui.recomputeEntity()

        # absolutely atrocious code to dig through interactor shit to
        # sustain menu across changing segment entity
        self.ui.interactor.removeAllEntities()
        self.ui.interactor.addEntity(self.ui)

    def toggleDirection(self):
        if self.direction == SegmentDirection.FORWARD:
            self.direction = SegmentDirection.REVERSE
        else:
            self.direction = SegmentDirection.FORWARD
        self.getState()._updateIcon()

        self.updateThetas()
        self.getPrevious().onThetaChange()
        self.getNext().onThetaChange()

    """
    GETTER METHODS THAT READ FROM MODEL. DO NOT MODIFY MODEL OR SEND NOTIFICATIONS
    """

    def getState(self) -> AbstractSegmentState:
        return self.states[self.currentStateType]

    def getStraightState(self) -> StraightSegmentState:
        return self.states[SegmentType.STRAIGHT]

    def getArcState(self) -> ArcSegmentState:
        return self.states[SegmentType.ARC]

    def getBezierState(self) -> BezierSegmentState:
        return self.states[SegmentType.BEZIER]

    def getAdapter(self) -> PathAdapter:
        return self.getState().getAdapter()

    def getPrevious(self) -> PathNodeModel:
        return super().getPrevious()

    def getNext(self) -> PathNodeModel:
        return super().getNext()

    def getBeforePos(self) -> tuple:
        return self.getPrevious().getPosition()

    def getAfterPos(self) -> tuple:
        return self.getNext().getPosition()

    def getStartTheta(self) -> float:
        return self.START_THETA

    def getEndTheta(self) -> float:
        return self.END_THETA

    def getDirection(self) -> SegmentDirection:
        return self.direction

    def getType(self) -> SegmentType:
        return self.getState().getType()

    def getCenterInches(self) -> tuple:
        return self.getState()._defineCenterInches()

    def getOther(self, node: PathNodeModel) -> PathNodeModel:
        if node == self.getPrevious():
            return self.getNext()
        elif node == self.getNext():
            return self.getPrevious()
        else:
            raise Exception("Node not attached to segment")

    # initialize constraint solver for snapping to before node with angle (arc/bezier)
    def initBeforeThetaConstraints(self):
        self.bConstraints = ConstraintSolver(self.field)

        prevNode = self.getPrevious()

        # snap to cardinal directions for itself
        self.bConstraints.addCardinalConstraints(prevNode)

        # if segment before prevNode exists, snap to segment end angle
        prevSegment = prevNode.getPrevious()
        if prevSegment is not None:
            prevAngle = prevSegment.getEndTheta()
            self.bConstraints.addAngleConstraint(prevNode, prevAngle)

    # initialize constraint solver for snapping to after node with angle (arc/bezier)
    def initAfterThetaConstraints(self):
        self.aConstraints = ConstraintSolver(self.field)

        prevNode = self.getPrevious()

        # snap to cardinal directions for itself
        self.aConstraints.addCardinalConstraints(prevNode)

        # if segment before prevNode exists, snap to segment end angle
        prevSegment = prevNode.getPrevious()
        if prevSegment is not None:
            prevAngle = prevSegment.getEndTheta()
            self.aConstraints.addAngleConstraint(prevNode, prevAngle)

    # given a hypothetical start theta, return the "snapped" version if close enough
    # return None if no snapping
    def getConstrainedStartTheta(self, startTheta: float) -> float | None:
        snappedTheta = self.bConstraints.constrainAngle(startTheta)

        if snappedTheta is None:
            # nothing to snap
            return None
        else:
            # can snap
            return snappedTheta

    # given a hypothetical start theta, return the "snapped" version if close enough
    # return None if no snapping
    def getConstrainedEndTheta(self, endTheta: float) -> float | None:
        snappedTheta = self.aConstraints.constrainAngle( endTheta)

        if snappedTheta is None:
            # nothing to snap
            return None
        else:
            # can snap
            return snappedTheta

    """
    PRIVATE METHODS
    """

    def _generateUI(self, fieldEntity: FieldEntity) -> Entity:
        if self.getType() == SegmentType.STRAIGHT:
            return StraightSegmentEntity(fieldEntity, self)
        elif self.getType() == SegmentType.ARC:
            return ArcSegmentEntity(fieldEntity, self)
        elif self.getType() == SegmentType.BEZIER:
            return BezierSegmentEntity(fieldEntity, self)
        else:
            raise Exception("Invalid segment type")

    def __str__(self) -> str:
        return f"PathSegmentModel"