"""
Tools for generating reports and managing jinja2 templates.

report.py
"""

import os
import json

import jinja2
import pygments
import pygments.lexers
import pygments.formatters
import importlib_resources

from . import logging
from . import html_tools
from . import file_utils


#---------#
# Globals #
#---------#

J2_ENV = None
"""
The global Jinja2 Environment.
"""


RESOURCES_DIR = None
"""
The global resource directory.
"""


BLANK_RUBRIC_TEMPLATE = "rubric.j2"
"""
The template name for blank rubrics.
"""


EVALUATION_REPORT_TEMPLATE = "standalone_report.j2"
"""
The template name for evaluation reports.
"""

ERROR_MSG = "FAILED TO COMPLETE"
"""
The message that will be logged if an error occurs during evaluation.
"""

DONE_MSG = "completed successfully"
"""
The message that will be printed if evaluation completes successfully.
"""


#-----------------------------------#
# Setup & template/resource loading #
#-----------------------------------#

def register_filter(name, filter_function):
    """
    Registers a function for use in jinja2 templates as a filter.
    """
    J2_ENV.filters[name] = filter_function


def setup(templates_directory=None, resources_directory=None):
    """
    Sets up for report generation. Template and resources directories
    are optional, with resources loaded from this package by default.
    """
    global J2_ENV, RESOURCES_DIR

    # Remember resources directory
    RESOURCES_DIR = resources_directory

    # Decide on loader type
    if templates_directory is None:
        loader = jinja2.PackageLoader('potluck', 'templates')
    else:
        loader = jinja2.FileSystemLoader(templates_directory)

    # Set up Jinja2 environment
    J2_ENV = jinja2.Environment(loader=loader, autoescape=False)

    # Register custom Jinja2 filters
    register_filter("fileslug", html_tools.fileslug)
    register_filter("timestamp", file_utils.at_time)


def get_css():
    """
    Returns the CSS code for reports as a string.
    """
    if RESOURCES_DIR is None:
        pkg_files = importlib_resources.files("potluck") / "resources"
        return pkg_files.joinpath("potluck.css").read_text(encoding="utf-8")
    else:
        with open(os.path.join(RESOURCES_DIR, "potluck.css")) as fin:
            return fin.read()


def get_js():
    """
    Returns the Javascript code for reports as a string.
    """
    if RESOURCES_DIR is None:
        pkg_files = importlib_resources.files("potluck") / "resources"
        return pkg_files.joinpath("potluck.js").read_text(encoding="utf-8")
    else:
        with open(os.path.join(RESOURCES_DIR, "potluck.js")) as fin:
            return fin.read()


def load_template(template_name):
    """
    Loads a template by name (a relative path within the configured
    templates directory).
    """
    try:
        return J2_ENV.get_template(template_name)
    except jinja2.exceptions.TemplateSyntaxError as e:
        logging.log(
            (
                "Syntax error while building Jinja2 template:"
                " '{template_name}'\n"
                "Error occurred on line: {lineno}\n"
                "Error was: {error}\n"
                '-' * 80
            ).format(
                template_name=template_name,
                lineno=e.lineno,
                error=str(e)
            )
        )
        raise
    except Exception as e:
        logging.log(
            (
                "Unexpected non-syntax error while building Jinja2"
                " template: '{template_name}'\n"
                "Error was: {error}\n"
                '-' * 80
            ).format(
                template_name=template_name,
                error=str(e)
            )
        )
        raise


#-----------------------------#
# Rendering rubrics & reports #
#-----------------------------#

def render_blank_rubric(blank_report, output_file):
    """
    Renders a blank rubric report as HTML into the specified file. The
    report should come from `potluck.rubrics.Rubric.create_blank_report`.
    """
    # Load our report template
    template = load_template(BLANK_RUBRIC_TEMPLATE)

    # Augment the given blank report object
    augment_report(blank_report, blank=True)

    # Render the report to the output file
    with open(output_file, 'w', encoding="utf-8") as fout:
        fout.write(template.render(blank_report))


def render_report(report, output_file, html_output_file):
    """
    Renders an evaluation report object as JSON into the specified output
    file, and as HTML into the specified html output file. The report
    should come from `potluck.rubrics.Rubric.evaluate`.
    """
    # Load our report template
    template = load_template(EVALUATION_REPORT_TEMPLATE)

    # Add a timestamp to our report object
    report["timestamp"] = file_utils.timestamp()

    # Render the basic report to the JSON output file
    with open(output_file, 'w', encoding="utf-8") as fout:
        json.dump(report, fout)

    # Augment the rubric report object with pre-rendered HTML stuff
    augment_report(report)

    # Render the HTML report to the html output file
    with open(html_output_file, 'w', encoding="utf-8") as fout:
        fout.write(template.render(report=report))


#-------------------#
# Rendering helpers #
#-------------------#


def augment_report(report, blank=False):
    """
    Adds the following keys to the given rubric report:
        'rendered_table' - HTML code representing the report's table
        'rendered_contexts' - HTML code representing the report's
            contexts list

    For each file dictionary in the 'files' slot, it also adds:
        'code_html' - a rendering of the file's submitted code as HTML
    (but only if there's a "code" slot).
    """
    if "files" in report:
        for file_entry in report["files"]:
            if "code" in file_entry:
                file_entry["code_html"] = render_code(
                    report["taskid"],
                    file_entry["filename"],
                    file_entry["code"]
                )
            # TODO: Something else for "raw" files?

    if "table" in report:
        report['rendered_table'] = render_rubric_table(
            report["table"],
            blank=blank
        )
    else:
        report["rendered_table"] = "No table found."

    if "contexts" in report:
        # Note: contexts have already been formatted with/without values
        # before this step, so blank can be ignored here.
        report['rendered_contexts'] = render_contexts_list(report["contexts"])
    else:
        report['rendered_contexts'] = "No context information available."

    # Add CSS & JS to report
    report["css"] = get_css()
    report["js"] = get_js()


def render_code(taskid, filename, raw_code):
    """
    Given a task ID, a filename, and a string containing submitted code,
    creates an HTML string containing markup for displaying that code in
    a report. Each line is given a line ID so that other parts of the
    report can reference specific lines of the file.
    """
    if raw_code == '':
        return "<pre># NO CODE AVAILABLE</pre>"

    markup = pygments.highlight(
        raw_code,
        pygments.lexers.PythonLexer(),
        pygments.formatters.HtmlFormatter()
    )

    # TODO: Re-instate some kind of annotation mechanism where code lines
    # can have back-links to annotations that target them?

    highlighted = markup\
        .replace('<div class="highlight"><pre>', '')\
        .replace('</pre></div>', '')

    annotated = '\n'.join(
        annotate_line(taskid, filename, n + 1, line)
        for n, line in enumerate(highlighted.split('\n'))
    )
    return (
        '<pre id="{}">'.format(html_tools.block_id(taskid, filename))
      + annotated
      + '</pre>'
    )


def annotate_line(taskid, filename, lineno, line_html):
    """
    Takes a task ID, a file name, a line number, and a line of
    already-highlighted Python code (an HTML string), and returns the
    same line wrapped in a span which includes a line ID, as well as an
    onclick property that will highlight that line using the
    `focusCodeLine` function which is defined in `resources/potluck.js`.
    The returned span also includes a `linenum` span before the line text
    which includes a line number.
    """
    lineid = html_tools.line_id(taskid, filename, lineno)
    block_id = html_tools.block_id(taskid, filename)
    return (
        '<span id="{lineid}"'
        ' class="codeline codeline_plain"'
        ' onclick="focusCodeLine(\'{block_id}\', \'{lineid}\');'
        ' return false">'
        '<span class="linenum">{lineno} </span>'
        ' {line_html}'
        '</span>'
    ).format(
        lineid=lineid,
        block_id=block_id,
        lineno=lineno,
        line_html=line_html
    )


def render_rubric_table(table, blank=False):
    """
    Renders a rubric table (from the 'table' slot of an evaluation; see
    `potluck.rubrics.Rubric.evaluate`) as an HTML string. Returns an HTML
    string that builds a

    <div class="rubric_table">

    blank can be set to True (instead of the default False) to make
    rubric details more visible in blank rubrics where explanations
    haven't been generated yet, but it means that explanations will not
    be rendered in the output (warnings and notes still will be). Setting
    blank to True will also cause table rows to display the rubric
    versions of row topics and details even when feedback versions are
    available.
    """
    return '<div class="rubric_table">\n{}\n</div>'.format(
        '\n'.join(
            render_rubric_row(row, blank)
            for row in table
        )
    )


def render_rubric_row(row, blank=False):
    """
    Renders a single row of a rubric table into an HTML div (see
    render_rubric_table). The resulting div has class "rubric_row", plus
    an additional class depending on the status of the given row with the
    string 'status_' prefixed and any spaces replaced by underscores.
    There are also 'tag_' classes added for each tag that the row has.

    If blank is given as True, the explanation for this row will be
    discarded and replaced by the rubric details, which will then be
    omitted from the topic area. By default (when blank is False), the
    rubric details are available on expansion within the topic div and
    the explanation is put in the assessment area. Also, when blank is
    True, the rubric versions of the topic and details for this row are
    displayed instead of the feedback versions.
    """
    contents = ''

    description = row.get(
        "description",
        ("Unknown", "No description available.")
    )
    if blank:
        topic, details = description[:2]
    else:
        topic = description[::2][-1]
        details = description[1::2][-1]

    topic_class = "topic"
    details_node = ' ' + html_tools.create_help_button(details)
    if blank:
        topic_class += " no_details"
        details_node = ''

    contents += (
        '<div class="{}" tabindex="0">\n{}\n{}\n{}\n{}\n</div>'
    ).format(
        topic_class,
        html_tools.build_status_indicator(row["status"]),
        "<span class='sr-only'>{}</span>".format(row["status"]),
        topic,
        details_node
    )

    expl_text = row.get("explanation", '')
    if blank:
        expl_text = details

    explanation = '<div class="explanation">{}</div>'.format(expl_text)
    # Add notes to explanation if they exist:
    if row.get("notes"):
        notes = '<details class="notes">\n<summary>Notes</summary>\n<ul>'
        for note in row.get("notes"):
            notes += '\n<li class="note">{}</li>'.format(note)
        notes += '</ul>\n</details>'
        explanation += '\n' + notes

    # Add warnings to explanation if they exist:
    if row.get("warnings"):
        warnings = (
            '<details class="warnings">\n<summary>Warnings</summary>\n<ul>'
        )
        for warning in row.get("warnings"):
            warnings += '\n<li class="warning">{}</li>'.format(warning)
        warnings += '</ul>\n</details>'
        explanation += '\n' + warnings

    # Add assessment (explanation + notes + warnings) to result:
    contents += '<div class="assessment">{}</div>'.format(explanation)

    # Add subtable if there is one:
    subtable = ''
    if row.get("subtable"):
        subtable = render_rubric_table(row.get("subtable"), blank)

    contents += subtable

    classes = [
        "rubric_row",
        html_tools.status_css_class(row["status"])
    ] + [
        "tag-{k}-{v}".format(k=k, v=v)
        for (k, v) in row.get("tags", {}).items()
    ]
    return '<div class="{}">{}</div>'.format(
        ' '.join(classes),
        contents
    )


def render_contexts_list(contexts_list):
    """
    Accepts a contexts list in the format returned by
    `potluck.contexts.list_and_render_contexts`. Each context gets its
    own details element that collapses to a single row, but expands to
    show the context value(s).

    This function returns a single HTML string containing code for all of
    the contexts, wrapped in a div with class 'context_info'.
    """
    result = "<div class='context_info'>"
    for index, ctx in enumerate(contexts_list):
        tl = min(ctx["level"], 8) # truncated level value
        # TODO: Some way of drawing connectors that's not horribly
        # fragile...
        if ctx["value"] != "":
            result += """
<details id='ctx_{index}' class='context_details level_{tl}'>
  <summary>{desc}</summary>
  {val}
</details>
""".format(
    index=index,
    tl=tl,
    desc=ctx["description"],
    val=ctx["value"],
)
        else:
            result += """
<div id='ctx_{index}' class='context_details level_{tl}'>
  {desc}
</div>
""".format(
    index=index,
    tl=tl,
    desc=ctx["description"]
)

    result += "</div>"
    return result
