"""
Base classes for common session-related state shared between clients and proxies
"""
from __future__ import annotations

import abc
import logging
import weakref
from typing import *

import multidict

from hippolyzer.lib.base.datatypes import UUID, Vector3
from hippolyzer.lib.base.message.circuit import ConnectionHolder, Circuit
from hippolyzer.lib.base.message.message import Message
from hippolyzer.lib.base.message.message_handler import MessageHandler
from hippolyzer.lib.base.network.caps_client import CapsClient
from hippolyzer.lib.base.network.transport import ADDR_TUPLE
from hippolyzer.lib.base.objects import handle_to_global_pos
from hippolyzer.lib.base.xfer_manager import XferManager
from hippolyzer.lib.client.inventory_manager import InventoryManager

from hippolyzer.lib.client.object_manager import ClientObjectManager, ClientWorldObjectManager


class BaseClientRegion(ConnectionHolder, abc.ABC):
    """Represents a client's view of a remote region"""
    handle: Optional[int]
    # Actually a weakref
    session: Callable[[], BaseClientSession]
    objects: ClientObjectManager
    xfer_manager: XferManager
    caps_client: CapsClient
    cap_urls: multidict.MultiDict[str]
    circuit_addr: ADDR_TUPLE
    circuit: Optional[Circuit]
    _name: Optional[str]

    def __init__(self):
        self._name = None
        self.circuit = None

    @abc.abstractmethod
    def update_caps(self, caps: Mapping[str, str]) -> None:
        pass

    @property
    def name(self):
        if self._name:
            return self._name
        return "Pending %r" % (self.circuit_addr,)

    @name.setter
    def name(self, val):
        self._name = val

    @property
    def global_pos(self) -> Vector3:
        if self.handle is None:
            raise ValueError("Can't determine global region position without handle")
        return handle_to_global_pos(self.handle)

    @property
    def is_alive(self):
        if not self.circuit:
            return False
        return self.circuit.is_alive

    def mark_dead(self):
        logging.info("Marking %r dead" % self)
        if self.circuit:
            self.circuit.is_alive = False
        self.objects.clear()

    def __repr__(self):
        return "<%s %s (%r)>" % (self.__class__.__name__, self.name, self.handle)


class BaseClientSessionManager:
    pass


class BaseClientSession(abc.ABC):
    """Represents a client's view of a remote session"""
    id: UUID
    agent_id: UUID
    secure_session_id: UUID
    active_group: UUID
    groups: Set[UUID]
    message_handler: MessageHandler[Message, str]
    regions: MutableSequence[BaseClientRegion]
    region_by_handle: Callable[[int], Optional[BaseClientRegion]]
    region_by_circuit_addr: Callable[[ADDR_TUPLE], Optional[BaseClientRegion]]
    objects: ClientWorldObjectManager
    inventory: InventoryManager
    login_data: Dict[str, Any]
    REGION_CLS = Type[BaseClientRegion]

    def __init__(self, id, secure_session_id, agent_id, circuit_code,
                 session_manager: Optional[BaseClientSessionManager], login_data=None):
        self.login_data = login_data or {}
        self.pending = True
        self.id: UUID = id
        self.secure_session_id: UUID = secure_session_id
        self.agent_id: UUID = agent_id
        self.circuit_code = circuit_code
        self.global_caps = {}
        self.session_manager = session_manager
        self.active_group: UUID = UUID.ZERO
        self.groups: Set[UUID] = set()
        self.regions = []
        self._main_region = None
        self.message_handler: MessageHandler[Message, str] = MessageHandler()
        super().__init__()

    @classmethod
    def from_login_data(cls, login_data, session_manager):
        sess = cls(
            id=UUID(login_data["session_id"]),
            secure_session_id=UUID(login_data["secure_session_id"]),
            agent_id=UUID(login_data["agent_id"]),
            circuit_code=int(login_data["circuit_code"]),
            session_manager=session_manager,
            login_data=login_data,
        )
        appearance_service = login_data.get("agent_appearance_service")
        map_image_service = login_data.get("map-server-url")
        if appearance_service:
            sess.global_caps["AppearanceService"] = appearance_service
        if map_image_service:
            sess.global_caps["MapImageService"] = map_image_service
        # Login data also has details about the initial sim
        sess.register_region(
            circuit_addr=(login_data["sim_ip"], login_data["sim_port"]),
            handle=(login_data["region_x"] << 32) | login_data["region_y"],
            seed_url=login_data["seed_capability"],
        )
        return sess

    def register_region(self, circuit_addr: Optional[ADDR_TUPLE] = None, seed_url: Optional[str] = None,
                        handle: Optional[int] = None) -> BaseClientRegion:
        if not any((circuit_addr, seed_url)):
            raise ValueError("One of circuit_addr and seed_url must be defined!")

        for region in self.regions:
            if region.circuit_addr == circuit_addr:
                if seed_url and region.cap_urls.get("Seed") != seed_url:
                    region.update_caps({"Seed": seed_url})
                if handle:
                    region.handle = handle
                return region
            if seed_url and region.cap_urls.get("Seed") == seed_url:
                return region

        if not circuit_addr:
            raise ValueError("Can't create region without circuit addr!")

        logging.info("Registering region for %r" % (circuit_addr,))
        region = self.REGION_CLS(circuit_addr, seed_url, self, handle=handle)
        self.regions.append(region)
        return region

    @property
    def main_region(self) -> Optional[BaseClientRegion]:
        if self._main_region and self._main_region() in self.regions:
            return self._main_region()
        return None

    @main_region.setter
    def main_region(self, val: BaseClientRegion):
        self._main_region = weakref.ref(val)

    def transaction_to_assetid(self, transaction_id: UUID):
        return UUID.combine(transaction_id, self.secure_session_id)

    def region_by_circuit_addr(self, circuit_addr) -> Optional[BaseClientRegion]:
        for region in self.regions:
            if region.circuit_addr == circuit_addr and region.circuit:
                return region
        return None

    def region_by_handle(self, handle: int) -> Optional[BaseClientRegion]:
        for region in self.regions:
            if region.handle == handle:
                return region
        return None

    def __repr__(self):
        return "<%s %s>" % (self.__class__.__name__, self.id)
