from django.apps import apps as django_apps
from django.conf import settings
from django.core.management.base import BaseCommand, CommandError
from mako.exceptions import MakoException
from django_mako_plus.registry import get_dmp_apps, ensure_dmp_app
from django_mako_plus.provider import create_mako_context
from django_mako_plus.provider.runner import ProviderRun, create_factories
from django_mako_plus.util import get_dmp_instance, split_app, DMP_OPTIONS

import glob
import os, os.path, shutil
import json
from collections import OrderedDict



class Command(BaseCommand):
    args = ''
    help = 'Removes compiled template cache folders in your DMP-enabled app directories.'
    can_import_settings = True


    def add_arguments(self, parser):
        parser.add_argument(
            '--verbose',
            action='store_true',
            dest='verbose',
            default=False,
            help='Set verbosity to level 3 (see --verbosity).'
        )
        parser.add_argument(
            '--quiet',
            action='store_true',
            dest='quiet',
            default=False,
            help='Set verbosity to level 0, which silences all messages (see --verbosity).'
        )
        parser.add_argument(
            '--overwrite',
            action='store_true',
            dest='overwrite',
            default=False,
            help='Overwrite existing __entry__.js if necessary.'
        )
        parser.add_argument(
            '--single',
            type=str,
            metavar='FILENAME',
            help='Instead of per-app entry files, create a single file that includes the JS for all listed apps.'
        )
        parser.add_argument(
            'appname',
            type=str,
            nargs='*',
            help='The name of one or more DMP apps. If omitted, all DMP apps are processed.'
        )


    def message(self, msg, level=1):
        '''Print a message to the console'''
        # verbosity=1 is the default if not specified in the options
        if self.options['verbosity'] >= level:
            print(msg)


    def handle(self, *args, **options):
        # save the options for later
        self.options = options
        if self.options['verbose']:
            self.options['verbosity'] = 3
        if self.options['quiet']:
            self.options['verbosity'] = 0
        self.factories = create_factories('WEBPACK_PROVIDERS')

        # ensure we have a base directory
        try:
            if not os.path.isdir(os.path.abspath(settings.BASE_DIR)):
                raise CommandError('Your settings.py BASE_DIR setting is not a valid directory.  Please check your settings.py file for the BASE_DIR variable.')
        except AttributeError as e:
            print(e)
            raise CommandError('Your settings.py file is missing the BASE_DIR setting.')

        # the apps to process
        apps = []
        for appname in options['appname']:
            ensure_dmp_app(appname)
            apps.append(django_apps.get_app_config(appname))
        if len(apps) == 0:
            apps = get_dmp_apps()

        # main runner for per-app files
        if self.options['single'] is None:
            for app in apps:
                self.message('Searching `{}` app...'.format(app.name))
                filename = os.path.join(app.path, 'scripts', '__entry__.js')
                self.create_entry_file(filename, self.generate_script_map(app), [ app ])

        # main runner for one sitewide file
        else:
            script_map = {}
            for app in apps:
                self.message('Searching `{}` app...'.format(app.name))
                script_map.update(self.generate_script_map(app))
            self.create_entry_file(self.options['single'], script_map, apps)


    def create_entry_file(self, filename, script_map, apps):
        '''Creates an entry file for the given script map'''
        def in_apps(s):
            for app in apps:
                last = None
                path = os.path.dirname(s)
                while last != path:
                    if os.path.samefile(app.path, path):
                        return True
                    last = path
                    path = os.path.dirname(last)
            return False

        filedir = os.path.dirname(filename)
        if os.path.exists(filename):
            if self.options['overwrite']:
                os.remove(filename)
            else:
                raise ValueError('Refusing to destroy existing file: {} (use --overwrite option or remove the file)'.format(filename))
        # create the lines of the entry file
        lines = []
        for page, scripts in script_map.items():
            require = []
            for s in scripts:
                if in_apps(os.path.abspath(s)):
                    require.append('require("./{}")'.format(os.path.relpath(s, filedir)))
            if len(require) > 0:
                lines.append('DMP_CONTEXT.appBundles["{}"] = () => {{ {}; }};'.format(page, '; '.join(require)))
        # if we created at least one line, write the entry file
        if len(lines) > 0:
            self.message('Creating {}'.format(os.path.relpath(filename, settings.BASE_DIR)))
            with open(filename, 'w') as fout:
                fout.write('(context => {\n')
                for line in lines:
                    fout.write('    {}\n'.format(line))
                fout.write('})(DMP_CONTEXT.get());\n')


    def generate_script_map(self, config):
        '''
        Maps templates in this app to their scripts.  This function iterates through
        app/templates/* to find the templates in this app.  Returns the following
        dictionary with paths relative to BASE_DIR:

        {
            'app/template1': [ 'scripts/template1.js', 'scripts/supertemplate1.js' ],
            'app/template2': [ 'scripts/template2.js', 'scripts/supertemplate2.js', 'scripts/supersuper2.js' ],
            ...
        }

        Any files or subdirectories starting with double-underscores (e.g. __dmpcache__) are skipped.
        '''
        script_map = OrderedDict()
        template_root = os.path.join(config.path, 'templates')
        def recurse(folder):
            for filename in os.listdir(os.path.join(template_root, folder)):
                if filename.startswith('__'):
                    continue
                filerel = os.path.join(folder, filename)
                filepath = os.path.join(os.path.join(template_root, filerel))
                if os.path.isdir(filepath):
                    recurse(filerel)

                elif os.path.isfile(filepath):
                    scripts = self.template_scripts(config, filerel)
                    key = '{}/{}'.format(config.name, os.path.splitext(filerel)[0])
                    if len(scripts) > 0:
                        script_map[key] = scripts
                        self.message('\t{}: {}'.format(key, scripts), 3)
                    else:
                        self.message('\t{}: none found'.format(key), 3)
        recurse('')
        return script_map


    def template_scripts(self, config, template_name):
        '''
        Returns a list of scripts used by the given template object AND its ancestors.

        This runs a ProviderRun on the given template (as if it were being displayed).
        This allows the WEBPACK_PROVIDERS to provide the JS files to us.

        For this algorithm to work, the providers must extend static_links.LinkProvider
        because that class creates the 'enabled' list of provider instances, and each
        provider instance has a url.
        '''
        template_obj = get_dmp_instance().get_template_loader(config, create=True).get_mako_template(template_name, force=True)
        mako_context = create_mako_context(template_obj)
        inner_run = ProviderRun(mako_context['self'], factories=self.factories)
        inner_run.run()
        scripts = []
        for data in inner_run.column_data:
            for provider in data.get('enabled', []):
                if getattr(provider, 'url') is not None:
                    url = provider.url.split('?', 1)[0]
                    scripts.append(os.path.relpath(url, settings.BASE_DIR))
        return scripts
