# -*- coding: utf-8 -*-
#
# This file is a part of the LinkAhead Project.
#
# Copyright (C) 2023 IndiScale GmbH <info@indiscale.com>
# Copyright (C) 2023 Henrik tom Wörden <h.tomwoerden@indiscale.com>
# Copyright (C) 2023 Daniel Hornung <d.hornung@indiscale.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
#

"""
This module provides some cached versions of functions that retrieve Entities from a remote server.

See also
========

- ``cache_initialize(...)`` : Re-initialize the cache.
- ``cache_clear()`` : Clear the cache.
- ``cached_query(query)`` : A cached version of ``execute_query(query)``.
- ``cached_get_entity_by(...)`` : Get an Entity by name, id, ...
"""

from enum import Enum
from functools import lru_cache
from typing import Union

from .utils import get_entity
from .common.models import execute_query, Entity, Container


# roughly 1GB for typical entity sizes
DEFAULT_SIZE = 33333

# This dict cache is solely for filling the real cache manually (e.g. to reuse older query results)
_DUMMY_CACHE = {}


class AccessType(Enum):
    """Different access types for cached queries.  Needed for filling the cache manually with
:func:`cache_fill` .

    """
    QUERY = 1
    PATH = 2
    EID = 3
    NAME = 4


def cached_get_entity_by(eid: Union[str, int] = None, name: str = None, path: str = None, query:
                         str = None) -> Entity:
    """Return a single entity that is identified uniquely by one argument.

You must supply exactly one argument.

If a query phrase is given, the result must be unique.  If this is not what you need, use
:func:`cached_query` instead.

    """
    count = 0
    if eid is not None:
        count += 1
    if name is not None:
        count += 1
    if path is not None:
        count += 1
    if query is not None:
        count += 1
    if count != 1:
        raise ValueError("You must supply exactly one argument.")

    if eid is not None:
        return _cached_access(AccessType.EID, eid, unique=True)
    if name is not None:
        return _cached_access(AccessType.NAME, name, unique=True)
    if path is not None:
        return _cached_access(AccessType.PATH, path, unique=True)
    if query is not None:
        return _cached_access(AccessType.QUERY, query, unique=True)

    raise ValueError("Not all arguments may be None.")


def cached_query(query_string) -> Container:
    """A cached version of :func:`linkahead.execute_query<linkahead.common.models.execute_query>`.

All additional arguments are at their default values.

    """
    return _cached_access(AccessType.QUERY, query_string, unique=False)


@lru_cache(maxsize=DEFAULT_SIZE)
def _cached_access(kind: AccessType, value: Union[str, int], unique=True):
    # This is the function that is actually cached.
    # Due to the arguments, the cache has kind of separate sections for cached_query and
    # cached_get_entity_by with the different AccessTypes. However, there is only one cache size.

    # The dummy dict cache is only for filling the cache manually, it is deleted afterwards.
    if value in _DUMMY_CACHE:
        return _DUMMY_CACHE[value]

    if kind == AccessType.QUERY:
        return execute_query(value, unique=unique)
    if kind == AccessType.NAME:
        return get_entity.get_entity_by_name(value)
    if kind == AccessType.EID:
        return get_entity.get_entity_by_id(value)
    if kind == AccessType.PATH:
        return get_entity.get_entity_by_path(value)

    raise ValueError(f"Unknown AccessType: {kind}")


def cache_clear() -> None:
    """Empty the cache that is used by `cached_query` and `cached_get_entity_by`."""
    _cached_access.cache_clear()


def cache_info():
    """Return info about the cache that is used by `cached_query` and `cached_get_entity_by`.

Returns
-------

out: named tuple
  See the standard library :func:`functools.lru_cache` for details."""
    return _cached_access.cache_info()


def cache_initialize(maxsize=DEFAULT_SIZE) -> None:
    """Create a new cache with the given size for `cached_query` and `cached_get_entity_by`.

    This implies a call of :func:`cache_clear`, the old cache is emptied.

    """
    cache_clear()
    global _cached_access
    _cached_access = lru_cache(maxsize=maxsize)(_cached_access.__wrapped__)


def cache_fill(items: dict, kind: AccessType = AccessType.EID, unique: bool = True) -> None:
    """Add entries to the cache manually.

    This allows to fill the cache without actually submitting queries.  Note that this does not
    overwrite existing entries with the same keys.

Parameters
----------

items: dict
  A dictionary with the entries to go into the cache.  The keys must be compatible with the
  AccessType given in ``kind``

kind: AccessType, optional
  The AccessType, for example ID, name, path or query.

unique: bool, optional
  If True, fills the cache for :func:`cached_get_entity_by`, presumably with
  :class:`linkahead.Entity<linkahead.common.models.Entity>` objects.  If False, the cache should be
  filled with :class:`linkahead.Container<linkahead.common.models.Container>` objects, for use with
  :func:`cached_query`.

    """
    # 1. add the given items to the corresponding dummy dict cache
    _DUMMY_CACHE.update(items)

    # 2. call the cache function with each key (this only results in a dict look up)
    for key in items.keys():
        _cached_access(kind, key, unique=unique)

    # 3. empty the dummy dict cache again
    _DUMMY_CACHE.clear()