from __future__ import absolute_import

from os.path import isfile

try:
    import urlparse
except ImportError:
    import urllib.parse as urlparse

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.contrib.staticfiles import finders

from whitenoise.django import DjangoWhiteNoise

A404 = '404 NOT FOUND'
A301 = '301 Moved Permanently'
PI = 'PATH_INFO'


class DjangoRedNoise(DjangoWhiteNoise):
    rednoise_config_attrs = [
        'should_serve_static',
        'should_serve_media',
        'root_aliases'
    ]

    root_aliases = [
        '/favicon.ico', '/sitemap.xml',
        '/robots.txt', '/humans.txt'
    ]

    debug = False
    should_serve_static = True
    should_serve_media = True

    def __init__(self, application):
        """Basic init stuff. We allow overriding a few extra things.
        """
        self.charset = settings.FILE_CHARSET
        self.application = application
        self.staticfiles_dirs = []
        self.static_files = {}
        self.media_files = {}

        # These are commonly used assets that get requested from the root;
        # we catch them and redirect to the prefixed static url in production
        # or just serve them in development.
        #
        # see also: self.find_root_aliases()
        self._root_aliases = {}

        # Allow settings to override default attributes
        # We check for existing WHITENOISE_{} stuff to be compatible, but then
        # add a few RedNoise specific ones in order to not be too confusing.
        self.check_and_set_settings('WHITENOISE_{}', self.config_attrs)
        self.check_and_set_settings('REDNOISE_{}', self.rednoise_config_attrs)

        # If DEBUG=True in settings, then we'll just default Rednoise to debug.
        try:
            setattr(self, 'debug', getattr(settings, 'DEBUG'))
        except AttributeError:
            pass

        if self.should_serve_media:
            self.media_root, self.media_prefix = self.get_structure('MEDIA')

        # Grab the various roots we care about.
        if self.should_serve_static:
            self.static_root, self.static_prefix = self.get_structure('STATIC')

            try:
                setattr(self, 'staticfiles_dirs', getattr(
                    settings, 'STATICFILES_DIRS'
                ))
            except AttributeError:
                pass

            self.make_root_aliases()

    def make_root_aliases(self):
        """For any static files that should be loading from the "root" -
        (e.g, robots.txt, favicon.ico, humans.txt, sitemap.xml, etc) we want
        to catch them and redirect to the actual static file. This basically
        just computes the necessary redirect paths for use in __call__.

        Users can specify overrides via REDNOISE_ROOT_ALIASES in settings.py.

        We do this because the favicon gets requested a lot, and in really
        weird circumstances sometimes. I don't like making Django deal with it
        just to serve a 301, so we just do it here. If the user lacks a file,
        it's just a proper 404 ultimately.
        """
        try:
            static_url = getattr(settings, 'STATIC_URL')
        except:
            raise ImproperlyConfigured('STATIC_URL is not configured.')

        for alias in self.root_aliases:
            self._root_aliases[alias] = static_url + alias.replace('/', '')

    def __call__(self, environ, start_response):
        """Checks to see if a request is inside our designated media or static
        configurations.
        """
        path = environ[PI]
        if path in self.root_aliases:
            start_response(A301, [('Location', self._root_aliases[path])])
            return []

        if self.should_serve_static and self.is_static(path):
            asset = self.load_static_file(path)
            if asset is not None:
                return self.serve(asset, environ, start_response)
            else:
                start_response(A404, [('Content-Type', 'text/plain')])
                return ['Not Found']

        if self.should_serve_media and self.is_media(path):
            asset = self.load_media_file(path)
            if asset is not None:
                return self.serve(asset, environ, start_response)
            else:
                start_response(A404, [('Content-Type', 'text/plain')])
                return ['Not Found']

        return self.application(environ, start_response)

    def file_not_modified(self, static_file, environ):
        """We just hook in here to always return false (i.e, it was modified)
        in DEBUG scenarios. This is optimal for development/reloading
        scenarios.

        In a production scenario, you want the original Whitenoise setup, so
        super().
        """
        if self.debug:
            return False
        return super(DjangoRedNoise, self).file_not_modified(
            static_file,
            environ
        )

    def add_cache_headers(self, static_file, url):
        """Again, we hook in here to blank on adding cache headers in DEBUG
        scenarios. This is optimal for development/reloading
        scenarios.

        In a production scenario, you want the original Whitenoise setup, so
        super().
        """
        if self.debug:
            return
        super(DjangoRedNoise, self).add_cache_headers(static_file, url)

    def check_and_set_settings(self, settings_key, attributes):
        """Checks settings to see if we should override something.
        """
        for attr in attributes:
            key = settings_key.format(attr.upper())
            try:
                setattr(self, attr, getattr(settings, key))
            except AttributeError:
                pass

    def get_structure(self, key):
        """This code is almost verbatim from the Whitenoise project Django
        integration. Little reason to change it, short of string substitution.
        """
        url = getattr(settings, '%s_URL' % key, None)
        root = getattr(settings, '%s_ROOT' % key, None)
        if not url or not root:
            raise ImproperlyConfigured('%s_URL and %s_ROOT \
                must be configured to use RedNoise' % (key, key))
        prefix = urlparse.urlparse(url).path
        prefix = '/{}/'.format(prefix.strip('/'))
        return root, prefix

    def is_static(self, path):
        """Checks to see if a given path is trying to be all up in
        our static director(y||ies).
        """
        return path[:len(self.static_prefix)] == self.static_prefix

    def add_static_file(self, path):
        """Custom, ish. Adopts the same approach as Whitenoise, but instead
        handles creating of a File object per each valid static/media request.
        This is then cached for lookup later if need-be.

        See also: self.add_media_file()
        """
        file_path = self.find_static_file(path)

        if file_path:
            files = {}
            files[path] = self.get_static_file(file_path, path)

            if not self.debug:
                self.find_gzipped_alternatives(files)

            self.static_files.update(files)

    def find_static_file(self, path):
        """Finds a static file; if DEBUG mode is set for Django, it will
        mimic Django's default static files behavior and serve
        (and not cache) the file. If DEBUG mode is False, it will essentially
        mimic the default WhiteNoise behavior.
        """
        file_path = None
        if self.debug:
            file_path = finders.find(path.replace(self.static_prefix, ''))

        # The immediate assumption would be to just only do this in non-DEBUG
        # scenarios, but this here allows us to fall through to ROOT in DEBUG.
        if file_path is None:
            file_path = ('%s/%s' % (
                self.static_root, path.replace(self.static_prefix, '')
            )).replace('\\', '/')

            if not isfile(file_path):
                return None

        return file_path

    def load_static_file(self, path):
        """Retrieves a static file, optimizing along the way.
        Very possible it can return None. TODO: perhaps optimize that
        use case somehow.
        """
        asset = self.static_files.get(path)
        if asset is None or self.debug:
            self.add_static_file(path)
            asset = self.static_files.get(path)

        return asset

    def is_media(self, path):
        """Checks to see if a given path is trying to be all up in our
        media director(y||ies).
        """
        return path[:len(self.media_prefix)] == self.media_prefix

    def add_media_file(self, path):
        """Custom, ish. Adopts the same approach as Whitenoise, but instead
        handles creating of a File object per each valid static/media request.
        This is then cached for lookup later if need-be.

        Media and static assets have differing properties by their very
        nature, so we have separate methods.
        """
        file_path = ('%s/%s' % (
            self.media_root, path.replace(self.media_prefix, '')
        )).replace('\\', '/')
        if isfile(file_path):
            files = {}
            files[path] = self.get_static_file(file_path, path)
            self.media_files.update(files)

    def load_media_file(self, path):
        """Retrieves a media file, optimizing along the way.
        """
        asset = self.media_files.get(path)
        if asset is None or self.debug:
            self.add_media_file(path)
            asset = self.media_files.get(path)

        return asset
