# ------------------------------------------------------------------------------
# Copyright (c) Acoular Development Team.
# ------------------------------------------------------------------------------

"""Implements a cache for HDF5 files used in Acoular."""

import gc
from pathlib import Path
from weakref import WeakValueDictionary

from .configuration import Config, config
from .h5files import _get_cachefile_class

from traits.api import Bool, Delegate, Dict, HasStrictTraits, Instance


class HDF5Cache(HasStrictTraits):
    """Cache class that handles opening and closing 'tables.File' objects."""

    config = Instance(Config)

    cache_dir = Delegate('config')

    busy = Bool(False)

    open_files = WeakValueDictionary()

    open_file_reference = Dict(key_trait=Path, value_trait=int)

    def _idle_if_busy(self):
        while self.busy:
            pass

    def close_cachefile(self, cachefile):
        """Close a cache file and remove it from the reference counter."""
        self.open_file_reference.pop(Path(cachefile.filename))
        cachefile.close()

    def get_open_cachefiles(self):
        """Get an iterator over all open cache files."""
        try:
            return self.open_files.itervalues()
        except AttributeError:
            return iter(self.open_files.values())

    def close_unreferenced_cachefiles(self):
        """Close cache files that are no longer referenced by any objects."""
        for cachefile in self.get_open_cachefiles():
            if not self.is_reference_existent(cachefile):
                self.close_cachefile(cachefile)

    def is_reference_existent(self, file):
        """Check if a file object still has active references."""
        exist_flag = False
        # inspect all referrers to the file object
        gc.collect()  # clear garbage before collecting referrers
        for ref in gc.get_referrers(file):
            # does the file object have a referrer that has a '_h5f'
            # attribute?
            if isinstance(ref, dict) and '_h5f' in ref:
                # file is still referred, must not be closed
                exist_flag = True
                break
        return exist_flag

    def _increase_file_reference_counter(self, filename):
        self.open_file_reference[filename] = self.open_file_reference.get(filename, 0) + 1

    def _decrease_file_reference_counter(self, filename):
        self.open_file_reference[filename] = self.open_file_reference[filename] - 1

    def get_cache_directories(self):
        """Return a list of all used cache directories (if multiple paths exist)."""
        return list({str(k.parent) for k in self.open_file_reference})

    def _print_open_files(self):
        """Prints open cache files and the number of objects referencing a cache file.

        If multiple cache files are open at different paths, the full path is printed.
        Otherwise, only the filename is logged.
        """
        if len(self.open_file_reference.values()) > 1:
            print(list({str(k): v for k, v in self.open_file_reference.items()}.items()))
        else:
            print(list({str(k.name): v for k, v in self.open_file_reference.items()}.items()))

    def get_cache_file(self, obj, basename, mode='a'):
        """Returns pytables .h5 file to _h5f trait of calling object for caching."""
        self._idle_if_busy()
        self.busy = True
        file_cls = _get_cachefile_class()
        filename = (Path(self.cache_dir) / (basename + '_cache.h5')).resolve()

        if config.global_caching == 'readonly' and not filename.exists():
            obj._h5f = None
            self.busy = False
            return  # cachefile is not created in readonly mode

        if isinstance(obj._h5f, file_cls):
            h5filename = Path(obj._h5f.filename).resolve()
            if h5filename == filename:
                self.busy = False
                return
            self._decrease_file_reference_counter(h5filename)

        if filename not in self.open_files:  # or tables.file._open_files.filenames
            if config.global_caching == 'readonly':
                mode = 'r'
            file = file_cls(filename, mode)
            self.open_files[filename] = file

        obj._h5f = self.open_files[filename]
        self._increase_file_reference_counter(filename)

        # garbage collection
        self.close_unreferenced_cachefiles()

        self.busy = False
        self._print_open_files()


H5cache = HDF5Cache(config=config)
