"""
This command finds and creates a report for all the usage of css rules in
an app. It aims to optimize existing codebase as well as assist the frontend
developer when designing new components by avoiding unnecessary duplication and
suggesting more/optimal alternatives.

Features:
    Currently the command can find out and display:
        - Media Breakpoints used in a stylesheet
        - Duplicate selectors in a stylesheet
        - Unused selectors
    Work in progress to enable these features:
        - Duplicate style declaration for same selector
        - DOM validation
        - Finding out dead styles (those that are always cancelled)
        - Optimize media declarations

Example:
    $ python manage.py optimize_frontend datacenterlight
    above command produces a file ../optimize_frontend.html which contains a
    report with the above mentioned features
"""

# import csv
import json
import logging
import os
import re
from collections import Counter, OrderedDict
# from itertools import zip_longest

from django import template
from django.conf import settings
from django.contrib.staticfiles import finders
from django.core.management.base import BaseCommand


logger = logging.getLogger(__name__)

RE_PATTERNS = {
    'view_html': '[\'\"](.*\.html)',
    'html_html': '{% (?:extends|include) [\'\"]?(.*\.html)',
    'html_style': '{% static [\'\"]?(.*\.css)',
    'css_media': (
        '^\s*\@media([^{]+)\{\s*([\s\S]*?})\s*}'
    ),
    'css_selector': (
        '^\s*([.#\[:_A-Za-z][^{]*?)\s*'
        '\s*{\s*([\s\S]*?)\s*}'
    ),
    'html_class': 'class=[\'\"]([a-zA-Z0-9-_\s]*)',
    'html_id': 'id=[\'\"]([a-zA-Z0-9-_]*)'
}


class Command(BaseCommand):
    help = (
        'Finds unused and duplicate style declarations from the stylesheets '
        'used in the templates of each app'
    )
    requires_system_checks = False

    def add_arguments(self, parser):
        # positional arguments
        parser.add_argument(
            'apps', nargs='+', type=str,
            help='name of the apps to be optimized'
        )

        # Named (optional) arguments
        parser.add_argument(
            '--together',
            action='store_true',
            help='optimize the apps together'
        )
        parser.add_argument(
            '--css',
            action='store_true',
            help='optimize only the css rules declared in each stylesheet'
        )

    def handle(self, *args, **options):
        apps_list = options['apps']
        report = {}
        for app in apps_list:
            if options['css']:
                report[app] = self.optimize_css(app)
        # write report
        write_report(report)

    def optimize_css(self, app_name):
        """Optimize declarations inside a css stylesheet

        Args:
            app_name (str): The application name
        """
        # get html and css files used in the app
        files = get_files(app_name)
        # get_selectors_from_css
        css_selectors = get_selectors_css(files['style'])
        # get_selectors_from_html
        html_selectors = get_selectors_html(files['html'])
        report = {
            'css_dup': get_css_duplication(css_selectors),
            'css_unused': get_css_unused(css_selectors, html_selectors)
        }
        return report


def get_files(app_name):
    """Get all the `html` and `css` files used in an app.

    Args:
        app_name (str): The application name

    Returns:
        dict: A dictonary containing Counter of occurence of each
        html and css file in `html` and `style` fields respectively.
        For example:
        {
            'html': {'datacenterlight/success.html': 1},
            'style': {'datacenterlight/css/bootstrap.min.css': 2}
        }
    """
    # the view file for the app
    app_view = os.path.join(settings.PROJECT_DIR, app_name, 'views.py')
    # get template files called from the view
    all_html_list = file_match_pattern(app_view, 'view_html')
    # list of unique template files
    uniq_html_list = list(OrderedDict.fromkeys(all_html_list).keys())
    # list of stylesheets
    all_style_list = []
    file_patterns = ['html_html', 'html_style']
    # get html and css files called from within templates
    i = 0
    while i < len(uniq_html_list):
        template_name = uniq_html_list[i]
        try:
            temp_files = templates_match_pattern(
                template_name, file_patterns
            )
        except template.exceptions.TemplateDoesNotExist as e:
            print("template file not found: ", str(e))
            all_html_list = [
                h for h in all_html_list if h != template_name
            ]
            del uniq_html_list[i]
        else:
            all_html_list.extend(temp_files[0])
            uniq_html_list = list(
                OrderedDict.fromkeys(all_html_list).keys()
            )
            all_style_list.extend(temp_files[1])
            i += 1
    # counter dict for the html files called from view
    result = {
        'html': Counter(all_html_list),
        'style': Counter(all_style_list)
    }
    # print(result)
    return result


def get_selectors_css(files):
    """Gets the selectors and declarations from a stylesheet.

    Args:
        files (list): A list of path of stylesheets.

    Returns:
        dict: A nested dictionary with the structre as
        `{'file': {'media-selector': [('selectors',`declarations')]}}`
        For example:
        {
            'datacenterlight/css/landing-page.css':{
                '(min-width: 768px)': [
                    ('.lead-right', 'text-align: right;'),
                ]
            }
        }
    """
    selectors = {}
    media_selectors = {}
    # get media selectors and other simple declarations
    for file in files:
        if any(vendor in file for vendor in ['bootstrap', 'font-awesome']):
            continue
        result = finders.find(file)
        if result:
            with open(result) as f:
                data = f.read()
            media_selectors[file] = string_match_pattern(data, 'css_media')
            new_data = string_remove_pattern(data, 'css_media')
            default_match = string_match_pattern(new_data, 'css_selector')
            selectors[file] = {
                'default': [
                    [' '.join(grp.split()) for grp in m] for m in default_match
                ]
            }
    # get declarations from media queries
    for file, match_list in media_selectors.items():
        for match in match_list:
            query = match[0]
            block_text = ' '.join(match[1].split())
            results = string_match_pattern(
                block_text, 'css_selector'
            )
            f_query = ' '.join(query.replace(':', ': ').split())
            if f_query in selectors[file]:
                selectors[file][f_query].extend(results)
            else:
                selectors[file][f_query] = results
    return selectors


def get_selectors_html(files):
    """Get `class` and `id` used in html files.

    Args:
        files (list): A list of html files path.

    Returns:
        dict: a dictonary of all the classes and ids found in the file, in
        `class` and `id` field respectively.
    """
    selectors = {}
    for file in files:
        results = templates_match_pattern(file, ['html_class', 'html_id'])
        class_dict = {c: 1 for match in results[0] for c in match.split()}
        selectors[file] = {
            'classes': list(class_dict.keys()),
            'ids': results[1],
        }
    return selectors


def file_match_pattern(file, patterns):
    """Match a regex pattern in a file

    Args:
        file (str): Complete path of file
        patterns (list or str): The pattern(s) to be searched in the file

    Returns:
        list: A list of all the matches in the file. Each item is a list of
        all the captured groups in the pattern. If multiple patterns are given,
        the returned list is a list of such lists.
        For example:
        [('.lead', 'font-size: 18px;'), ('.btn-lg', 'min-width: 180px;')]
    """
    with open(file) as f:
        data = f.read()
    results = string_match_pattern(data, patterns)
    return results


def string_match_pattern(data, patterns):
    """Match a regex pattern in a string

    Args:
        data (str): the string to search for the pattern
        patterns (list or str): The pattern(s) to be searched in the file

    Returns:
        list: A list of all the matches in the string. Each item is a list of
        all the captured groups in the pattern. If multiple patterns are given,
        the returned list is a list of such lists.
        For example:
        [('.lead', 'font-size: 18px;'), ('.btn-lg', 'min-width: 180px;')]
    """
    if not isinstance(patterns, str):
        results = []
        for p in patterns:
            re_pattern = re.compile(RE_PATTERNS[p], re.MULTILINE)
            results.append(re.findall(re_pattern, data))
    else:
        re_pattern = re.compile(RE_PATTERNS[patterns], re.MULTILINE)
        results = re.findall(re_pattern, data)
    return results


def string_remove_pattern(data, patterns):
    """Remove a pattern from a string

    Args:
        data (str): the string to search for the patter
        patterns (list or str): The pattern(s) to be removed from the file

    Returns:
        str: The new string with all instance of matching pattern
        removed from it
    """
    if not isinstance(patterns, str):
        for p in patterns:
            re_pattern = re.compile(RE_PATTERNS[p], re.MULTILINE)
            data = re.sub(re_pattern, '', data)
    else:
        re_pattern = re.compile(RE_PATTERNS[patterns], re.MULTILINE)
        data = re.sub(re_pattern, '', data)
    return data


def templates_match_pattern(template_name, patterns):
    """Match a regex pattern in the first found template file

    Args:
        file (str): Path of template file
        patterns (list or str): The pattern(s) to be searched in the file

    Returns:
        list: A list of all the matches in the file. Each item is a list of
        all the captured groups in the pattern. If multiple patterns are given,
        the returned list is a list of such lists.
        For example:
        [('.lead', 'font-size: 18px;'), ('.btn-lg', 'min-width: 180px;')]
    """
    t = template.loader.get_template(template_name)
    data = t.template.source
    results = string_match_pattern(data, patterns)
    return results


def get_css_duplication(css_selectors):
    """Get duplicate selectors from the same stylesheet

    Args:
        css_selectors (dict): A dictonary containing css selectors from
        all the files in the app in the below structure.
        `{'file': {'media-selector': [('selectors',`declarations')]}}`

    Returns:
        dict: A dictonary containing the count of any duplicate selector in
        each file.
        `{'file': {'media-selector': {'selector': count}}}`
    """
    # duplicate css selectors in stylesheets
    rule_count = {}
    for file, media_selectors in css_selectors.items():
        rule_count[file] = {}
        for media, rules in media_selectors.items():
            rules_dict = Counter([rule[0] for rule in rules])
            dup_rules_dict = {k: v for k, v in rules_dict.items() if v > 1}
            if dup_rules_dict:
                rule_count[file][media] = dup_rules_dict
    return rule_count


def get_css_unused(css_selectors, html_selectors):
    """Get selectors from stylesheets that are not used in any of the html
    files in which the stylesheet is used.

    Args:
        css_selectors (dict): A dictonary containing css selectors from
        all the files in the app in the below structure.
        `{'file': {'media-selector': [('selectors',`declarations')]}}`
        html_selectors (dict): A dictonary containing the 'class' and 'id'
        declarations from all html files
    """
    with open('utils/optimize/test.json', 'w') as f:
        json.dump([html_selectors, css_selectors], f, indent=4)
    # print(html_selectors, css_selectors)


def write_report(all_reports, filename='frontend'):
    """Write the generated report to a file for re-use

    Args;
        all_reports (dict): A dictonary of report obtained from different tests
        filename (str): An optional suffix for the output file
    """
    # full_filename = 'utils/optimize/optimize_' + filename + '.html'
    # output_file = os.path.join(
    #     settings.PROJECT_DIR, full_filename
    # )
    with open('utils/optimize/op_frontend.json', 'w') as f:
        json.dump(all_reports, f, indent=4)
    # with open(output_file, 'w', newline='') as f:
    #     f.write(
    #         template.loader.render_to_string(
    #             'utils/report.html', {'all_reports': all_reports}
    #         )
    #     )
        # w = csv.writer(f)
        # print(zip_longest(*results))
        # for r in zip_longest(*results):
        #     w.writerow(r)


# a list of all the html tags (to be moved in a json file)
html_tags = [
    "a",
    "abbr",
    "address",
    "article",
    "area",
    "aside",
    "audio",
    "b",
    "base",
    "bdi",
    "bdo",
    "blockquote",
    "body",
    "br",
    "button",
    "canvas",
    "caption",
    "cite",
    "code",
    "col",
    "colgroup",
    "datalist",
    "dd",
    "del",
    "details",
    "dfn",
    "div",
    "dl",
    "dt",
    "em",
    "embed",
    "fieldset",
    "figcaption",
    "figure",
    "footer",
    "form",
    "h1",
    "h2",
    "h3",
    "h4",
    "h5",
    "h6",
    "head",
    "header",
    "hgroup",
    "hr",
    "html",
    "i",
    "iframe",
    "img",
    "input",
    "ins",
    "kbd",
    "keygen",
    "label",
    "legend",
    "li",
    "link",
    "map",
    "mark",
    "menu",
    "meta",
    "meter",
    "nav",
    "noscript",
    "object",
    "ol",
    "optgroup",
    "option",
    "output",
    "p",
    "param",
    "pre",
    "progress",
    "q",
    "rp",
    "rt",
    "ruby",
    "s",
    "samp",
    "script",
    "section",
    "select",
    "source",
    "small",
    "span",
    "strong",
    "style",
    "sub",
    "summary",
    "sup",
    "textarea",
    "table",
    "tbody",
    "td",
    "tfoot",
    "thead",
    "th",
    "time",
    "title",
    "tr",
    "u",
    "ul",
    "var",
    "video",
    "wbr"
]
