import os
import shutil
import itertools
import re
from string import ascii_lowercase
import json

from django.apps import apps
from django.contrib.staticfiles.finders import get_finders
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.files.storage import FileSystemStorage
from django.core.management.base import BaseCommand, CommandError
from django.core.management.color import no_style
from django.utils.functional import cached_property
from django.conf import settings

class Command(BaseCommand):
    """
    Copies or symlinks static files from different locations to the
    settings.STATIC_ROOT.
    """
    help = "Collect static files in a single location."
    requires_system_checks = False

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.copied_files = []
        self.symlinked_files = []
        self.unmodified_files = []
        self.post_processed_files = []
        self.storage = staticfiles_storage
        self.style = no_style()

        self.frequency = dict()
        self.collection_of_classes = list()

        self.exclude_js_files = getattr(
            settings, "EXCLUDE_STATIC_JS_FILES", [])
        self.exclude_css_files = getattr(
            settings, "EXCLUDE_STATIC_CSS_FILES", [])
        self.exclude_svg_files = getattr(
            settings, "EXCLUDE_STATIC_SVG_FILES", [])
        self.exclude_static_directory = getattr(
            settings, "EXCLUDE_STATIC_DIRECTORY", [])
        self.static_root = getattr(
            settings, "STATIC_ROOT", None)

        self.default_static_dir = getattr(
            settings, "STATICFILES_DIRS", tuple())

        self.include_static_dir = getattr(
            settings, "STATIC_INCLUDE_DIRS", tuple())

        self.static_dir = self.include_static_dir + self.default_static_dir

        self.json_file_name = getattr(
            settings, "STATIC_CLASSES_FILE_NAME", 'data.json')

        self.not_included_words_by_users = getattr(
            settings, "EXCLUDED_CLASSNAMES_FROM_MINIFYING", [])

        self.not_included_words_by_default = ['ttf', 'woff2', 'www', 'woff', 'js', 'otf', 'eot',
                                              'svg', 'com', 'in', 'css', 'add', 'contains', 'remove', 'toggle', 'move', 'fonts', 'static', 'gstatic', 'manager', 'tag', 'google']

        self.not_included_words = self.not_included_words_by_users + \
            self.not_included_words_by_default

    @cached_property
    def local(self):
        try:
            self.storage.path('')
        except NotImplementedError:
            return False
        return True

    def add_arguments(self, parser):
        parser.add_argument(
            '--noinput', '--no-input', action='store_false', dest='interactive',
            help="Do NOT prompt the user for input of any kind.",
        )
        parser.add_argument(
            '--no-post-process', action='store_false', dest='post_process',
            help="Do NOT post process collected files.",
        )
        parser.add_argument(
            '-i', '--ignore', action='append', default=[],
            dest='ignore_patterns', metavar='PATTERN',
            help="Ignore files or directories matching this glob-style "
                 "pattern. Use multiple times to ignore more.",
        )
        parser.add_argument(
            '-n', '--dry-run', action='store_true',
            help="Do everything except modify the filesystem.",
        )
        parser.add_argument(
            '-c', '--clear', action='store_true',
            help="Clear the existing files using the storage "
                 "before trying to copy or link the original file.",
        )
        parser.add_argument(
            '-l', '--link', action='store_true',
            help="Create a symbolic link to each file instead of copying.",
        )
        parser.add_argument(
            '--no-default-ignore', action='store_false', dest='use_default_ignore_patterns',
            help="Don't ignore the common private glob-style patterns (defaults to 'CVS', '.*' and '*~').",
        )

    def set_options(self, **options):
        """
        Set instance variables based on an options dict
        """
        self.interactive = options['interactive']
        self.verbosity = options['verbosity']
        self.symlink = options['link']
        self.clear = options['clear']
        self.dry_run = options['dry_run']
        ignore_patterns = options['ignore_patterns']
        if options['use_default_ignore_patterns']:
            ignore_patterns += apps.get_app_config(
                'staticfiles').ignore_patterns
        self.ignore_patterns = list(set(os.path.normpath(p)
                                        for p in ignore_patterns))
        self.post_process = options['post_process']

    def collect(self):
        """
        Perform the bulk of the work of collectstatic.
        Split off from handle() to facilitate testing.
        """
        if self.symlink and not self.local:
            raise CommandError("Can't symlink to a remote destination.")

        if self.clear:
            self.clear_dir('')

        if self.symlink:
            handler = self.link_file
        else:
            handler = self.copy_file

        found_files = {}
        for finder in get_finders():
            for path, storage in finder.list(self.ignore_patterns):
                # Prefix the relative path if the source storage contains it
                if getattr(storage, 'prefix', None):
                    prefixed_path = os.path.join(storage.prefix, path)
                else:
                    prefixed_path = path

                if prefixed_path not in found_files:
                    found_files[prefixed_path] = (storage, path)
                    handler(path, prefixed_path, storage)
                else:
                    self.log(
                        "Found another file with the destination path '%s'. It "
                        "will be ignored since only the first encountered file "
                        "is collected. If this is not what you want, make sure "
                        "every static file has a unique path." % prefixed_path,
                        level=1,
                    )

        # Storage backends may define a post_process() method.
        if self.post_process and hasattr(self.storage, 'post_process'):
            processor = self.storage.post_process(found_files,
                                                  dry_run=self.dry_run)
            for original_path, processed_path, processed in processor:
                if isinstance(processed, Exception):
                    self.stderr.write(
                        "Post-processing '%s' failed!" % original_path)
                    # Add a blank line before the traceback, otherwise it's
                    # too easy to miss the relevant part of the error message.
                    self.stderr.write("")
                    raise processed
                if processed:
                    self.log("Post-processed '%s' as '%s'" %
                             (original_path, processed_path), level=2)
                    self.post_processed_files.append(original_path)
                else:
                    self.log("Skipped post-processing '%s'" % original_path)

        return {
            'modified': self.copied_files + self.symlinked_files,
            'unmodified': self.unmodified_files,
            'post_processed': self.post_processed_files,
        }

    def iter_all_strings(self):
        for size in itertools.count(start=1):
            for s in itertools.product(ascii_lowercase, repeat=size):
                yield "".join(s)

    def _create_json_file(self, file, root):
        if file.endswith('.css') and file not in self.exclude_css_files:
            read_css_file = open(os.path.join(root, file)).read()

            # To remove quotes in css
            remove_unwanted_css_fragments = re.sub(re.compile(
                "[\'\"].*?[\'\"]", re.DOTALL), '', read_css_file)

            # To remove stream of comments
            remove_unwanted_css_fragments = re.sub(re.compile(
                "/\*.*?\*/", re.DOTALL), '', remove_unwanted_css_fragments)

            # To remove single line comments
            remove_unwanted_css_fragments = re.sub(re.compile(
                "//.*?\n"), '', remove_unwanted_css_fragments)

            regex = re.compile(
                r'\.-?[_a-zA-Z]+[_a-zA-Z0-9-]*[^#+@+,+.+)+/+(+^+:+!+{+~+ +}+\'+\"+>+<+^+[+]')

            all_classes = regex.findall(
                remove_unwanted_css_fragments)
            for class_instance in all_classes:
                word = class_instance[1:]
                self.collection_of_classes.append(word)

        if file.endswith('.svg'):
            if file not in self.exclude_svg_files:
                read_svg_file = open(os.path.join(root, file)).read()

                regex = re.compile(r'class[ \t]*=[ \t]*"[^"]+"')
                all_classes = regex.findall(read_svg_file)
                for class_instance in all_classes:
                    for class_name in class_instance[7:-1].split():
                        self.collection_of_classes.append(class_name)

        if file.endswith('.js') and not file in self.exclude_js_files:
            read_js_file = open(os.path.join(root, file)).read()

            query_selector_regex = re.compile(
                r'querySelector\([\'\"][^\'\"]*?\.[^\'\"]*?[\'\"]\)')
            query_selector_classname = query_selector_regex.findall(
                read_js_file)

            query_selector_all_regex = re.compile(
                r'querySelectorAll\([\'\"][^\'\"]*?\.[^\'\"]*?[\'\"]\)')
            query_selector_all_classname = query_selector_all_regex.findall(
                read_js_file)

            get_element_by_classname_regex = re.compile(
                r'getElementsByClassName\([\'\"]([^)]+)[\'\"]\)')
            get_elements_by_classes = get_element_by_classname_regex.findall(
                read_js_file)

            get_just_class_from_selectors = re.compile(
                r'\.[_a-zA-Z]+[_a-zA-Z0-9-]*')
            get_query_selector_class_name = get_just_class_from_selectors.findall(
                ''.join(query_selector_classname))

            get_query_selector_all_class_name = get_just_class_from_selectors.findall(
                ''.join(query_selector_all_classname))

            for instance in get_query_selector_all_class_name:
                self.collection_of_classes.append(instance[1:])

            for instance in get_query_selector_class_name:
                self.collection_of_classes.append(instance[1:])

    def _json_creation(self):
        for word in self.collection_of_classes:
            class_instance = word.strip()

            if not word in self.not_included_words:
                if not class_instance in self.frequency:
                    self.frequency[class_instance] = 0
                self.frequency[class_instance] += 1

        length_of_frequency = len(self.frequency)

        sorted_by_value = dict(
            sorted(self.frequency.items(), key=lambda x: x[1], reverse=True))

        for (generated_code_word, key) in zip(itertools.islice(self.iter_all_strings(), length_of_frequency), sorted_by_value.keys()):
            sorted_by_value[key] = generated_code_word

        sorted_by_key_length = dict(
            sorted(sorted_by_value.items(), key=lambda x: len(x[0]), reverse=True))

        with open(self.json_file_name, 'w') as outfile:
            json.dump(sorted_by_key_length, outfile,
                      indent=4, separators=(',', ':'))

    def handle(self, **options):
        self.set_options(**options)

        message = ['\n']
        if self.dry_run:
            message.append(
                'You have activated the --dry-run option so no files will be modified.\n\n'
            )

        message.append(
            'You have requested to collect static files at the destination\n'
            'location as specified in your settings \n\n'
        )

        print("The staticfiles directories look up and minifying {static_file_directories}".format(
            static_file_directories=self.static_dir))

        for directory in self.static_dir:
            for root, dirs, files in os.walk(directory):
                for file in files:
                    self._create_json_file(file, root)
        
        self._json_creation()

        message.append(
            'Initialized {file_name} json file'.format(file_name=self.json_file_name)
        )

        if self.is_local_storage() and self.storage.location:
            destination_path = self.storage.location
            message.append(':\n\n    %s\n\n' % destination_path)
            should_warn_user = (
                self.storage.exists(destination_path) and
                any(self.storage.listdir(destination_path))
            )
        else:
            destination_path = None
            message.append('.\n\n')
            # Destination files existence not checked; play it safe and warn.
            should_warn_user = True

        if self.interactive and should_warn_user:
            if self.clear:
                message.append(
                    'This will DELETE ALL FILES in this location!\n')
            else:
                message.append('This will overwrite existing files!\n')

            message.append(
                'Are you sure you want to do this?\n\n'
                "Type 'yes' to continue, or 'no' to cancel: "
            )
            if input(''.join(message)) != 'yes':
                raise CommandError("Collecting static files cancelled.")

            # Delete the static folder
            if os.path.exists(self.static_root) and os.path.isdir(self.static_root):
                shutil.rmtree(self.static_root)
    
        collected = self.collect()
        modified_count = len(collected['modified'])
        unmodified_count = len(collected['unmodified'])
        post_processed_count = len(collected['post_processed'])

        if self.verbosity >= 1:
            template = ("\n%(modified_count)s %(identifier)s %(action)s"
                        "%(destination)s%(unmodified)s%(post_processed)s.\n")
            summary = template % {
                'modified_count': modified_count,
                'identifier': 'static file' + ('' if modified_count == 1 else 's'),
                'action': 'symlinked' if self.symlink else 'copied',
                'destination': (" to '%s'" % destination_path if destination_path else ''),
                'unmodified': (', %s unmodified' % unmodified_count if collected['unmodified'] else ''),
                'post_processed': (collected['post_processed'] and
                                   ', %s post-processed'
                                   % post_processed_count or ''),
            }
            return summary

    def log(self, msg, level=2):
        """
        Small log helper
        """
        if self.verbosity >= level:
            self.stdout.write(msg)

    def is_local_storage(self):
        return isinstance(self.storage, FileSystemStorage)

    def clear_dir(self, path):
        """
        Delete the given relative path using the destination storage backend.
        """
        if not self.storage.exists(path):
            return

        dirs, files = self.storage.listdir(path)
        for f in files:
            fpath = os.path.join(path, f)
            if self.dry_run:
                self.log("Pretending to delete '%s'" % fpath, level=1)
            else:
                self.log("Deleting '%s'" % fpath, level=1)
                try:
                    full_path = self.storage.path(fpath)
                except NotImplementedError:
                    self.storage.delete(fpath)
                else:
                    if not os.path.exists(full_path) and os.path.lexists(full_path):
                        # Delete broken symlinks
                        os.unlink(full_path)
                    else:
                        self.storage.delete(fpath)
        for d in dirs:
            self.clear_dir(os.path.join(path, d))

    def delete_file(self, path, prefixed_path, source_storage):
        """
        Check if the target file should be deleted if it already exists.
        """
        if self.storage.exists(prefixed_path):
            try:
                # When was the target file modified last time?
                target_last_modified = self.storage.get_modified_time(
                    prefixed_path)
            except (OSError, NotImplementedError, AttributeError):
                # The storage doesn't support get_modified_time() or failed
                pass
            else:
                try:
                    # When was the source file modified last time?
                    source_last_modified = source_storage.get_modified_time(
                        path)
                except (OSError, NotImplementedError, AttributeError):
                    pass
                else:
                    # The full path of the target file
                    if self.local:
                        full_path = self.storage.path(prefixed_path)
                        # If it's --link mode and the path isn't a link (i.e.
                        # the previous collectstatic wasn't with --link) or if
                        # it's non-link mode and the path is a link (i.e. the
                        # previous collectstatic was with --link), the old
                        # links/files must be deleted so it's not safe to skip
                        # unmodified files.
                        can_skip_unmodified_files = not (
                            self.symlink ^ os.path.islink(full_path))
                    else:
                        # In remote storages, skipping is only based on the
                        # modified times since symlinks aren't relevant.
                        can_skip_unmodified_files = True
                    # Avoid sub-second precision (see #14665, #19540)
                    file_is_unmodified = (
                        target_last_modified.replace(microsecond=0) >=
                        source_last_modified.replace(microsecond=0)
                    )
                    if file_is_unmodified and can_skip_unmodified_files:
                        if prefixed_path not in self.unmodified_files:
                            self.unmodified_files.append(prefixed_path)
                        self.log("Skipping '%s' (not modified)" % path)
                        return False
            # Then delete the existing file if really needed
            if self.dry_run:
                self.log("Pretending to delete '%s'" % path)
            else:
                self.log("Deleting '%s'" % path)
                self.storage.delete(prefixed_path)
        return True

    def link_file(self, path, prefixed_path, source_storage):
        """
        Attempt to link ``path``
        """
        # Skip this file if it was already copied earlier
        if prefixed_path in self.symlinked_files:
            return self.log("Skipping '%s' (already linked earlier)" % path)
        # Delete the target file if needed or break
        if not self.delete_file(path, prefixed_path, source_storage):
            return
        # The full path of the source file
        source_path = source_storage.path(path)
        # Finally link the file
        if self.dry_run:
            self.log("Pretending to link '%s'" % source_path, level=1)
        else:
            self.log("Linking '%s'" % source_path, level=2)
            full_path = self.storage.path(prefixed_path)
            os.makedirs(os.path.dirname(full_path), exist_ok=True)
            try:
                if os.path.lexists(full_path):
                    os.unlink(full_path)
                os.symlink(source_path, full_path)
            except AttributeError:
                import platform
                raise CommandError("Symlinking is not supported by Python %s." %
                                   platform.python_version())
            except NotImplementedError:
                import platform
                raise CommandError("Symlinking is not supported in this "
                                   "platform (%s)." % platform.platform())
            except OSError as e:
                raise CommandError(e)
        if prefixed_path not in self.symlinked_files:
            self.symlinked_files.append(prefixed_path)

    def copy_file(self, path, prefixed_path, source_storage):
        """
        Attempt to copy ``path`` with storage
        """
        # Skip this file if it was already copied earlier
        if prefixed_path in self.copied_files:
            return self.log("Skipping '%s' (already copied earlier)" % path)
        # Delete the target file if needed or break
        if not self.delete_file(path, prefixed_path, source_storage):
            return
        # The full path of the source file
        source_path = source_storage.path(path)
        # Finally start copying
        if self.dry_run:
            self.log("Pretending to copy '%s'" % source_path, level=1)
        else:
            self.log("Copying '%s'" % source_path, level=2)
            with source_storage.open(path) as source_file:
                self.storage.save(prefixed_path, source_file)
        self.copied_files.append(prefixed_path)
