# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

from typing import Dict, Generic, List, Optional, TypeVar

K = TypeVar('K')  # Key type.
V = TypeVar('V')  # Value type.
X = TypeVar('X')  # Index type.


class CacheEntry(Generic[K, V]):
    '''Represents a single entry in a cache.

    The entry must hold two pieces of information:
    - value, the cached result to be returned on a matching lookup
    - key, data needed to determine if the entry matches a lookup (e.g. input
      parameters to the function whose result has been cached)

    Note that this is not a simple key/value store, because a key might match a
    lookup even if it's not an exact match. Determining what exactly needs to
    match is implementation-specific and implemented by subclasses.
    '''
    def __init__(self, key: K, value: V):
        self.key = key
        self.value = value

    def is_match(self, key: K) -> bool:
        '''Returns whether this entry is a match for the key.

        In the simplest case, this could be just self.key == key; however, the
        implementer might choose to ignore certain information when evaluating
        the match, depending on the use-case.
        '''
        raise NotImplementedError()


class Cache(Generic[X, K, V]):
    '''Represents a cache to speed up recursive functions.

    The cache is structured with two layers:
    - The first layer is a dictionary that maps some hashable index type to the
      second layer, a list for each dictionary entry.
    - The second layer is a list of CacheEntry instances to be checked.

    The purpose of the two-layer structure is to speed up lookups; the index
    type should be used to quickly narrow things down to a limited number of
    potentially matching entries (for instance, it could be an input parameter
    to the function that absolutely must match for the cache entries to match).
    '''
    def __init__(self) -> None:
        self.entries: Dict[X, List[CacheEntry[K, V]]] = {}

    def add(self, index: X, entry: CacheEntry[K, V]) -> None:
        # Only add if there's no matching entry already
        if self.lookup(index, entry.key) is None:
            self.entries.setdefault(index, []).append(entry)

    def lookup(self, index: X, key: K) -> Optional[V]:
        for entry in self.entries.get(index, []):
            if entry.is_match(key):
                return entry.value
        return None
