from typing import List
from PIL import Image
from io import BytesIO
from datetime import datetime
from collections import defaultdict

from .base_rust_api import BaseRustSocket
from .structures import RustInfo, RustMap, RustMarker, RustChatMessage, RustTeamInfo, RustEntityInfo, RustContents, \
    RustItem
from .remote.rustplus_pb2 import *
from .remote import HeartBeat
from ..commands import CommandOptions
from ..exceptions import *
from ..utils import *


class RustSocket(BaseRustSocket):

    def __init__(self, ip: str = None, port: str = None, steamid: int = None, playertoken: int = None,
                 command_options: CommandOptions = None, raise_ratelimit_exception: bool = True,
                 ratelimit_limit: int = 25, ratelimit_refill: int = 3, use_proxy: bool = False) -> None:
        super().__init__(ip=ip, port=port, steamid=steamid, playertoken=playertoken, command_options=command_options,
                         raise_ratelimit_exception=raise_ratelimit_exception, ratelimit_limit=ratelimit_limit,
                         ratelimit_refill=ratelimit_refill, heartbeat=HeartBeat(self), use_proxy=use_proxy)

    async def get_time(self) -> RustTime:

        await self._handle_ratelimit()

        app_request = self._generate_protobuf()
        app_request.getTime.CopyFrom(AppEmpty())

        await self.remote.send_message(app_request)

        response = await self.remote.get_response(app_request.seq, app_request)

        return format_time(response)

    async def send_team_message(self, message: str) -> None:

        await self._handle_ratelimit(2)

        app_send_message = AppSendMessage()
        app_send_message.message = message

        app_request = self._generate_protobuf()
        app_request.sendTeamMessage.CopyFrom(app_send_message)

        self.remote.ignored_responses.append(app_request.seq)

        await self.remote.send_message(app_request)

    async def get_info(self) -> RustInfo:

        await self._handle_ratelimit()

        app_request = self._generate_protobuf()
        app_request.getInfo.CopyFrom(AppEmpty())

        await self.remote.send_message(app_request)

        response = await self.remote.get_response(app_request.seq, app_request)

        return RustInfo(response.response.info)

    async def get_team_chat(self) -> List[RustChatMessage]:

        await self._handle_ratelimit()

        app_request = self._generate_protobuf()
        app_request.getTeamChat.CopyFrom(AppEmpty())

        await self.remote.send_message(app_request)

        messages = (await self.remote.get_response(app_request.seq, app_request)).response.teamChat.messages

        return [RustChatMessage(message) for message in messages]

    async def get_team_info(self):

        await self._handle_ratelimit()

        app_request = self._generate_protobuf()
        app_request.getTeamInfo.CopyFrom(AppEmpty())

        await self.remote.send_message(app_request)

        app_message = await self.remote.get_response(app_request.seq, app_request)

        return RustTeamInfo(app_message.response.teamInfo)

    async def get_markers(self) -> List[RustMarker]:

        await self._handle_ratelimit()

        app_request = self._generate_protobuf()
        app_request.getMapMarkers.CopyFrom(AppEmpty())

        await self.remote.send_message(app_request)

        app_message = await self.remote.get_response(app_request.seq, app_request)

        return [RustMarker(marker) for marker in app_message.response.mapMarkers.markers]

    async def get_raw_map_data(self) -> RustMap:

        await self._handle_ratelimit(5)

        app_request = self._generate_protobuf()
        app_request.getMap.CopyFrom(AppEmpty())

        await self.remote.send_message(app_request)

        app_message = await self.remote.get_response(app_request.seq, app_request)

        return RustMap(app_message.response.map)

    async def get_map(self, add_icons: bool = False, add_events: bool = False, add_vending_machines: bool = False,
                      override_images: dict = None) -> Image:

        if override_images is None:
            override_images = {}

        map_size = int((await self.get_info()).size)

        await self._handle_ratelimit(5 + 1 if [add_icons, add_events, add_vending_machines].count(True) >= 1 else 0)

        app_request = self._generate_protobuf()
        app_request.getMap.CopyFrom(AppEmpty())

        await self.remote.send_message(app_request)

        app_message = await self.remote.get_response(app_request.seq, app_request)

        game_map = app_message.response.map
        monuments = list(game_map.monuments)

        try:
            image = Image.open(BytesIO(game_map.jpgImage))
        except Exception:
            raise ImageError("Invalid bytes for the image")

        image = image.crop((500, 500, game_map.height - 500, game_map.width - 500))

        game_map = image.resize((map_size, map_size), Image.ANTIALIAS)

        if add_icons or add_events or add_vending_machines:
            map_markers = await self.get_markers()

            if add_icons:
                for monument in monuments:
                    if str(monument.token) == "DungeonBase":
                        continue
                    icon = convert_monument(monument.token, override_images)
                    if monument.token in override_images:
                        icon = icon.resize((150, 150))
                    if str(monument.token) == "train_tunnel_display_name":
                        icon = icon.resize((100, 125))
                    game_map.paste(icon, (format_coord(int(monument.x), int(monument.y), map_size)), icon)

            if add_vending_machines:
                with resources.path("rustplus.api.icons", "vending_machine.png") as path:
                    vending_machine = Image.open(path).convert("RGBA")
                    vending_machine = vending_machine.resize((100, 100))

            for marker in map_markers:
                if add_events:
                    if marker.type == 2 or marker.type == 4 or marker.type == 5 or marker.type == 6 or marker.type == 8:
                        icon = convert_marker(str(marker.type), marker.rotation)
                        if marker.type == 6:
                            x = marker.x
                            y = marker.y
                            if y > map_size: y = map_size
                            if y < 0: y = 100
                            if x > map_size: x = map_size - 75
                            if x < 0: x = 50
                            game_map.paste(icon, (int(x), map_size - int(y)), icon)
                        else:
                            game_map.paste(icon, (format_coord(int(marker.x), int(marker.y), map_size)), icon)
                if add_vending_machines and marker.type == 3:
                    game_map.paste(vending_machine, (int(marker.x) - 50, map_size - int(marker.y) - 50),
                                   vending_machine)

        return game_map.resize((2000, 2000), Image.ANTIALIAS)

    async def get_entity_info(self, eid: int = None) -> RustEntityInfo:

        await self._handle_ratelimit()

        if eid is None:
            raise ValueError("EID cannot be None")

        app_request = self._generate_protobuf()
        app_request.entityId = eid
        app_request.getEntityInfo.CopyFrom(AppEmpty())

        await self.remote.send_message(app_request)

        app_message = await self.remote.get_response(app_request.seq, app_request)

        return RustEntityInfo(app_message.response.entityInfo)

    async def _update_smart_device(self, eid: int, value: bool) -> None:

        await self._handle_ratelimit()

        entity_value = AppSetEntityValue()
        entity_value.value = value

        app_request = self._generate_protobuf()

        app_request.entityId = eid
        app_request.setEntityValue.CopyFrom(entity_value)

        self.remote.ignored_responses.append(app_request.seq)

        await self.remote.send_message(app_request)

    async def turn_on_smart_switch(self, eid: int = None) -> None:

        if eid is None:
            raise ValueError("EID cannot be None")

        await self._update_smart_device(eid, True)

    async def turn_off_smart_switch(self, eid: int = None) -> None:

        if eid is None:
            raise ValueError("EID cannot be None")

        await self._update_smart_device(eid, False)

    async def promote_to_team_leader(self, steamid: int = None) -> None:

        if steamid is None:
            raise ValueError("SteamID cannot be None")

        await self._handle_ratelimit()

        leader_packet = AppPromoteToLeader()
        leader_packet.steamId = steamid

        app_request = self._generate_protobuf()
        app_request.promoteToLeader.CopyFrom(leader_packet)

        self.remote.ignored_responses.append(app_request.seq)

        await self.remote.send_message(app_request)

    async def get_current_events(self) -> List[RustMarker]:

        return [marker for marker in (await self.get_markers()) if
                marker.type == 2 or marker.type == 4 or marker.type == 5 or marker.type == 6 or marker.type == 8]

    async def get_tc_storage_contents(self, eid: int = None, combine_stacks: bool = False) -> RustContents:

        if eid is None:
            raise ValueError("EID cannot be None")

        returned_data = await self.get_entity_info(eid)

        target_time = datetime.utcfromtimestamp(int(returned_data.protectionExpiry))
        difference = target_time - datetime.utcnow()

        items = []

        for item in returned_data.items:
            items.append(RustItem(translate_id_to_stack(item.itemId), item.itemId, item.quantity, item.itemIsBlueprint))

        if combine_stacks:
            merged_map = defaultdict(tuple)

            for item in items:
                data = merged_map[str(item.itemId)]
                if data:
                    count = int(data[0]) + int(item.quantity)
                    merged_map[str(item.itemId)] = (count, bool(item.isBlueprint))
                else:
                    merged_map[str(item.itemId)] = (int(item.quantity), bool(item.isBlueprint))

            items = []
            for key in merged_map.keys():
                items.append(
                    RustItem(translate_id_to_stack(key), key, int(merged_map[key][0]), bool(merged_map[key][1])))

        return RustContents(difference, bool(returned_data.hasProtection), items)
