# -*- coding: utf-8  -*-

import gzip
from threading import Lock

import yaml

__all__ = ["Universe"]

def _cache(func):
    """Wrap a no-argument method to cache its return value in the object."""
    def inner(self):
        key = func.__name__
        if key in self._cache:
            return self._cache[key]
        value = func(self)
        self._cache[key] = value
        return value
    return inner


class _UniqueObject:
    """Base class for uniquely ID'd objects in the universe."""

    def __init__(self, universe, id_, data):
        self._universe = universe
        self._id = id_
        self._data = data
        self._cache = {}

    @property
    def id(self):
        """The object's unique ID, as an integer."""
        return self._id


class _SolarSystem(_UniqueObject):
    """Represents a solar system."""

    @property
    def name(self):
        """The solar system's name, as a string."""
        return self._data["name"]

    @property
    @_cache
    def constellation(self):
        """The solar system's constellation, as a _Constellation object."""
        return self._universe.constellation(self._data["constellation"])

    @property
    @_cache
    def region(self):
        """The solar system's region, as a _Region object."""
        return self._universe.region(self._data["region"])

    @property
    def security(self):
        """The solar system's security status, as a float."""
        return self._data["security"]

    @property
    def coords(self):
        """The solar system's coordinates, as a 3-tuple of floats (x, y, z)."""
        return tuple(self._data["coords"])

    @property
    @_cache
    def gates(self):
        """The solar system's adjacent systems, via stargate.

        A list of _SolarSystem objects.
        """
        return [self._universe.system(sid) for sid in self._data["gates"]]

    @property
    @_cache
    def faction(self):
        """The solar system's faction, as a _Faction object, or None."""
        if "faction" in self._data:
            return self._universe.faction(self._data["faction"])
        return self.constellation.faction

    @property
    def is_nullsec(self):
        """Whether the solar system is in nullsec."""
        return self.security <= 0.0

    @property
    def is_lowsec(self):
        """Whether the solar system is in nullsec."""
        return self.security > 0.0 and self.security < 0.45

    @property
    def is_highsec(self):
        """Whether the solar system is in nullsec."""
        return self.security >= 0.45

    @property
    def is_whspace(self):
        """Whether the solar system is in wormhole space."""
        return self.region.is_whspace


class _Constellation(_UniqueObject):
    """Represents a constellation."""

    @property
    def name(self):
        """The constellation's name, as a string."""
        return self._data["name"]

    @property
    @_cache
    def region(self):
        """The constellation's region, as a _Region object."""
        return self._universe.region(self._data["region"])

    @property
    @_cache
    def faction(self):
        """The constellation's faction, as a _Faction object, or None."""
        if "faction" in self._data:
            return self._universe.faction(self._data["faction"])
        return self.region.faction

    @property
    def is_whspace(self):
        """Whether the constellation is in wormhole space."""
        return self.region.is_whspace


class _Region(_UniqueObject):
    """Represents a region."""

    @property
    def name(self):
        """The region's name, as a string."""
        return self._data["name"]

    @property
    @_cache
    def faction(self):
        """The region's faction, as a _Faction object, or None."""
        if "faction" in self._data:
            return self._universe.faction(self._data["faction"])
        return None

    @property
    def is_whspace(self):
        """Whether the region is in wormhole space."""
        return self._id >= 11000000


class _Faction(_UniqueObject):
    """Represents a faction."""

    @property
    def name(self):
        """The faction's name, as a string."""
        return self._data["name"]

    @property
    @_cache
    def territory(self):
        """The faction's controlled territory.

        A list of _SolarSystems.
        """
        return [system for system in self._universe.systems()
                if system.faction and system.faction.id == self.id]


class _Type(_UniqueObject):
    """Represents any type, including ships and materials."""

    @property
    def name(self):
        """The item's name, as a string."""
        return self._data["name"]

    @property
    def group_id(self):
        """The item's group ID, as an integer."""
        return self._data["group_id"]

    @property
    def market_group_id(self):
        """The item's market group ID, as an integer, or None."""
        return self._data.get("market_group_id")


class _Killable(_UniqueObject):
    """Represents a killable object, like a ship, structure, or fighter."""

    def __init__(self, universe, kid, cat, data):
        super().__init__(universe, kid, data)
        self._cat = cat

    @property
    def name(self):
        """The killable object's name, as a string."""
        return self._data["name"]

    @property
    def group(self):
        """The killable object's group, as a string."""
        return self._data["group"]

    @property
    def is_ship(self):
        """Whether the killable object is a ship."""
        return self._cat == "ships"

    @property
    def is_structure(self):
        """Whether the killable object is a structure."""
        return self._cat == "structures"

    @property
    def is_fighter(self):
        """Whether the killable object is a fighter."""
        return self._cat == "fighters"


class _DummySolarSystem(_SolarSystem):
    """Represents an unknown or invalid solar system."""

    def __init__(self, universe):
        super().__init__(universe, -1, {
            "name": "Unknown",
            "constellation": -1,
            "region": -1,
            "security": 0.0,
            "coords": (0.0, 0.0, 0.0),
            "gates": []
        })


class _DummyConstellation(_Constellation):
    """Represents an unknown or invalid constellation."""

    def __init__(self, universe):
        super().__init__(universe, -1, {
            "name": "Unknown",
            "region": -1
        })


class _DummyRegion(_Region):
    """Represents an unknown or invalid region."""

    def __init__(self, universe):
        super().__init__(universe, -1, {
            "name": "Unknown"
        })


class _DummyFaction(_Faction):
    """Represents an unknown or invalid faction."""

    def __init__(self, universe):
        super().__init__(universe, -1, {
            "name": "Unknown"
        })


class _DummyType(_Type):
    """Represents an unknown or invalid type."""

    def __init__(self, universe):
        super().__init__(universe, -1, {
            "name": "Unknown",
            "group_id": -1,
            "market_group_id": -1
        })


class _DummyKillable(_Killable):
    """Represents an unknown or invalid killable object."""

    def __init__(self, universe):
        super().__init__(universe, -1, None, {
            "name": "Unknown",
            "group": "Unknown"
        })


class Universe:
    """EVE API module for static universe data."""

    def __init__(self, datadir):
        self._dir = datadir

        self._lock = Lock()
        self._loaded = False

        self._systems = {}
        self._constellations = {}
        self._regions = {}
        self._factions = {}
        self._types = {}
        self._killable_idx = {}
        self._killable_tab = {}

    @staticmethod
    def _load_yaml(path):
        """Load in and return a YAML file with the given path."""
        with gzip.open(str(path), "rb") as fp:
            return yaml.load(fp, Loader=yaml.CLoader)

    def _load(self):
        """Load in universe data. This can be called multiple times safely."""
        if self._loaded:
            return

        with self._lock:
            if self._loaded:
                return

            galaxy = self._load_yaml(self._dir / "galaxy.yml.gz")
            self._systems = galaxy["systems"]
            self._constellations = galaxy["constellations"]
            self._regions = galaxy["regions"]
            del galaxy

            entities = self._load_yaml(self._dir / "entities.yml.gz")
            self._factions = entities["factions"]
            del entities

            self._types = self._load_yaml(self._dir / "types.yml.gz")

            killables = self._load_yaml(self._dir / "killables.yml.gz")
            self._killable_idx = {kid: cat for cat, kids in killables.items()
                                  for kid in kids}
            self._killable_tab = killables
            del killables

            self._loaded = True

    def system(self, sid):
        """Return a _SolarSystem with the given ID.

        If the ID is invalid, return a dummy unknown object with ID -1.
        """
        self._load()
        if sid not in self._systems:
            return _DummySolarSystem(self)
        return _SolarSystem(self, sid, self._systems[sid])

    def systems(self):
        """Return an iterator over all _SolarSystems."""
        self._load()
        for sid in self._systems:
            yield self.system(sid)

    def constellation(self, cid):
        """Return a _Constellation with the given ID.

        If the ID is invalid, return a dummy unknown object with ID -1.
        """
        self._load()
        if cid not in self._constellations:
            return _DummyConstellation(self)
        return _Constellation(self, cid, self._constellations[cid])

    def constellations(self):
        """Return an iterator over all _Constellations."""
        self._load()
        for cid in self._constellations:
            yield self.constellation(cid)

    def region(self, rid):
        """Return a _Region with the given ID.

        If the ID is invalid, return a dummy unknown object with ID -1.
        """
        self._load()
        if rid not in self._regions:
            return _DummyRegion(self)
        return _Region(self, rid, self._regions[rid])

    def regions(self):
        """Return an iterator over all _Regions."""
        self._load()
        for rid in self._regions:
            yield self.region(rid)

    def faction(self, fid):
        """Return a _Faction with the given ID.

        If the ID is invalid, return a dummy unknown object with ID -1.
        """
        self._load()
        if fid not in self._factions:
            return _DummyFaction(self)
        return _Faction(self, fid, self._factions[fid])

    def factions(self):
        """Return an iterator over all _Factions."""
        self._load()
        for fid in self._factions:
            yield self.faction(fid)

    def type(self, tid):
        """Return a _Type with the given ID.

        If the ID is invalid, return a dummy unknown object with ID -1.
        """
        self._load()
        if tid not in self._types:
            return _DummyKillable(self)
        return _Type(self, tid, self._types[tid])

    def killable(self, kid):
        """Return a _Killable with the given ID.

        If the ID is invalid, return a dummy unknown object with ID -1.
        """
        self._load()
        if kid not in self._killable_idx:
            return _DummyKillable(self)
        cat = self._killable_idx[kid]
        return _Killable(self, kid, cat, self._killable_tab[cat][kid])
