"""The auto-rebuild system is an optional part of webassets that can be used
during development, and can also be quite convenient on small sites that don't
have the performance requirements where a rebuild-check on every request is
fatal.

This module contains classes that help determine whether a rebuild is required
for a bundle. This is more complicated than simply comparing the timestamps of
the source and output files.

First, certain filters, in particular CSS compilers like SASS, allow bundle
source files to reference additional files which the user may not have listed
in the bundle definition. The bundles support an additional ``depends``
argument that can list files that should be watched for modification.

Second, if the bundle definition itself changes, i.e., source files being added
or removed, or the list of applied filters modified, the bundle needs to be
rebuilt also. Since there is no single fixed place where bundles are defined,
simply watching the timestamp of that bundle definition file is not good enough.

To solve the latter problem, we employ an environment-specific cache of bundle
definitions.

Note that there is no ``HashUpdater``. This doesn't make sense for two reasons.
First, for a live system, it isn't fast enough. Second, for prebuilding assets,
the cache is a superior solution for getting essentially the same speed
increase as using the hash to reliably determine which bundles to skip.
"""

from . import six
from .six.moves import map
from .six.moves import zip
from .exceptions import BundleError, BuildError
from .utils import RegistryMetaclass, is_url, hash_func


__all__ = ('get_updater', 'SKIP_CACHE',
           'TimestampUpdater', 'AlwaysUpdater',)


SKIP_CACHE = object()
"""An updater can return this value as hint that a cache, if enabled,
should probably not be used for the rebuild; This is currently used
as a return value when a bundle's dependencies have changed, which
would currently not cause a different cache key to be used.

This is marked a hint, because in the future, the bundle may be smart
enough to make this decision by itself.
"""


class BaseUpdater(six.with_metaclass(RegistryMetaclass(
    clazz=lambda: BaseUpdater, attribute='needs_rebuild',
    desc='an updater implementation'))):
    """Base updater class.

    Child classes that define an ``id`` attribute are accessible via their
    string id in the configuration.

    A single instance can be used with different environments.
    """

    def needs_rebuild(self, bundle, ctx):
        """Returns ``True`` if the given bundle needs to be rebuilt,
        ``False`` otherwise.
        """
        raise NotImplementedError()

    def build_done(self, bundle, ctx):
        """This will be called once a bundle has been successfully built.
        """


get_updater = BaseUpdater.resolve


class BundleDefUpdater(BaseUpdater):
    """Supports the bundle definition cache update check that child
    classes are usually going to want to use also.
    """

    def check_bundle_definition(self, bundle, ctx):
        if not ctx.cache:
            # If no global cache is configured, we could always
            # fall back to a memory-cache specific for the rebuild
            # process (store as env._update_cache); however,
            # whenever a bundle definition changes, it's likely that
            # a process restart will be required also, so in most cases
            # this would make no sense.
            return False

        cache_key = ('bdef', bundle.output)
        current_hash = "%s" % hash_func(bundle)
        cached_hash = ctx.cache.get(cache_key)
        # This may seem counter-intuitive, but if no cache entry is found
        # then we actually return "no update needed". This is because
        # otherwise if no cache / a dummy cache is used, then we would be
        # rebuilding every single time.
        if not cached_hash is None:
            return cached_hash != current_hash
        return False

    def needs_rebuild(self, bundle, ctx):
        return self.check_bundle_definition(bundle, ctx)

    def build_done(self, bundle, ctx):
        if not ctx.cache:
            return False
        cache_key = ('bdef', bundle.output)
        cache_value = "%s" % hash_func(bundle)
        ctx.cache.set(cache_key, cache_value)


class TimestampUpdater(BundleDefUpdater):

    id = 'timestamp'

    def check_timestamps(self, bundle, ctx, o_modified=None):
        from .bundle import Bundle
        from .version import TimestampVersion

        if not o_modified:
            try:
                resolved_output = bundle.resolve_output(ctx)
            except BundleError:
                # This exception will occur when the bundle output has
                # placeholder, but a version cannot be found. If the
                # user has defined a manifest, this will just be the first
                # build. Return True to let it happen.
                # However, if no manifest is defined, raise an error,
                # because otherwise, this updater would always return True,
                # and thus not do its job at all.
                if ctx.manifest is None:
                    raise BuildError((
                        '%s uses a version placeholder, and you are '
                        'using "%s" versions. To use automatic '
                        'building in this configuration, you need to '
                        'define a manifest.' % (bundle, ctx.versions)))
                return True

            try:
                o_modified = TimestampVersion.get_timestamp(resolved_output)
            except OSError:
                # If the output file does not exist, we'll have to rebuild
                return True

       # Recurse through the bundle hierarchy. Check the timestamp of all
        # the bundle source files, as well as any additional
        # dependencies that we are supposed to watch.
        from .bundle import wrap
        for iterator, result in (
            (lambda e: map(lambda s: s[1], bundle.resolve_contents(e)), True),
            (bundle.resolve_depends, SKIP_CACHE)
        ):
            for item in iterator(ctx):
                if isinstance(item, Bundle):
                    nested_result = self.check_timestamps(item, wrap(ctx, item), o_modified)
                    if nested_result:
                        return nested_result
                elif not is_url(item):
                    try:
                        s_modified = TimestampVersion.get_timestamp(item)
                    except OSError:
                        # If a file goes missing, always require
                        # a rebuild.
                        return result
                    else:
                        if s_modified > o_modified:
                            return result
        return False

    def needs_rebuild(self, bundle, ctx):
        return \
            super(TimestampUpdater, self).needs_rebuild(bundle, ctx) or \
            self.check_timestamps(bundle, ctx)

    def build_done(self, bundle, ctx):
        # Reset the resolved dependencies, so any globs will be
        # re-resolved the next time we check if a rebuild is
        # required. This ensures that we begin watching new files
        # that are created, while still caching the globs as long
        # no changes happen.
        bundle._resolved_depends = None
        super(TimestampUpdater, self).build_done(bundle, ctx)


class AlwaysUpdater(BaseUpdater):

    id = 'always'

    def needs_rebuild(self, bundle, ctx):
        return True

