# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.
# ruff: noqa: RUF005, RUF012
"""Memoize result of function via pickle, used for cache testcases."""

# pylint: disable=broad-except,superfluous-parens
import atexit
import functools
import os
import pathlib
import sys

try:
    import cPickle as pickle
except ImportError:
    import pickle


def _get_global_cache_dir() -> pathlib.Path:
    if "XDG_CACHE_HOME" in os.environ:
        cache_home = pathlib.Path(os.environ.get("XDG_CACHE_HOME"))
    else:
        cache_home = pathlib.Path.home().joinpath(".cache")
    return cache_home.joinpath("tvm", f"pkl_memoize_py{sys.version_info[0]}")


GLOBAL_CACHE_DIR = _get_global_cache_dir()


class Cache:
    """A cache object for result cache.

    Parameters
    ----------
    key: str
       The file key to the function
    save_at_exit: bool
        Whether save the cache to file when the program exits
    """

    cache_by_key = {}

    def __init__(self, key, save_at_exit):
        self._cache = None

        self.path = GLOBAL_CACHE_DIR.joinpath(key)
        self.dirty = False
        self.save_at_exit = save_at_exit

    @property
    def cache(self):
        """Return the cache, initializing on first use."""

        if self._cache is not None:
            return self._cache

        if self.path.exists():
            with self.path.open("rb") as cache_file:
                try:
                    cache = pickle.load(cache_file)
                except pickle.UnpicklingError:
                    cache = {}
        else:
            cache = {}

        self._cache = cache
        return self._cache

    def save(self):
        if self.dirty:
            self.path.parent.mkdir(parents=True, exist_ok=True)

            with self.path.open("wb") as out_file:
                pickle.dump(self.cache, out_file, pickle.HIGHEST_PROTOCOL)


@atexit.register
def _atexit():
    """Save handler."""
    for value in Cache.cache_by_key.values():
        if value.save_at_exit:
            value.save()


def memoize(key, save_at_exit=False):
    """Memoize the result of function and reuse multiple times.

    Parameters
    ----------
    key: str
        The unique key to the file
    save_at_exit: bool
        Whether save the cache to file when the program exits

    Returns
    -------
    fmemoize : function
        The decorator function to perform memoization.
    """

    def _register(f):
        """Registration function"""
        allow_types = (str, int, float, tuple)
        fkey = key + "." + f.__name__ + ".pkl"
        if fkey not in Cache.cache_by_key:
            Cache.cache_by_key[fkey] = Cache(fkey, save_at_exit)
        cache = Cache.cache_by_key[fkey]
        cargs = tuple(x.cell_contents for x in f.__closure__) if f.__closure__ else ()
        cargs = (len(cargs),) + cargs

        @functools.wraps(f)
        def _memoized_f(*args, **kwargs):
            assert not kwargs, "Only allow positional call"
            key = cargs + args
            for arg in key:
                if isinstance(arg, tuple):
                    for x in arg:
                        assert isinstance(x, allow_types)
                else:
                    assert isinstance(arg, allow_types)
            if key in cache.cache:
                return cache.cache[key]
            res = f(*args)
            cache.cache[key] = res
            cache.dirty = True
            return res

        return _memoized_f

    return _register
