# Copyright (C) 2011  Bradley N. Miller
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

__author__ = "bmiller"

from docutils import nodes
from docutils.parsers.rst import directives
from .pg_logger import exec_script_str_local
import json
import six
import time
import random
import requests
from runestone.server.componentdb import addQuestionToDB, addHTMLToDB
from runestone.common.runestonedirective import (
    RunestoneIdDirective,
    RunestoneIdNode,
)


def setup(app):
    app.add_directive("codelens", Codelens)

    app.add_config_value("codelens_div_class", "cd_section", "html")
    app.add_config_value("trace_url", "http://tracer.runestone.academy:5000", "html")
    app.add_node(
        CodeLensNode,
        html=(visit_codelens_html, depart_codelens_html),
        xml=(visit_codelens_xml, depart_codelens_xml),
    )


#  data-tracefile="pytutor-embed-demo/java.json"

VIS = """
<div class="runestone codelens %(optclass)s">
    <div class="%(divclass)s" data-component="codelens" data-question_label="%(question_label)s">
        <div id=%(divid)s_question class="ac_question">
        </div>
        <div class="pytutorVisualizer" id="%(divid)s"
           data-params='{"embeddedMode": true, "lang": "%(language)s", "jumpToEnd": false}'>
        </div>
        <p class="runestone_caption"><span class="runestone_caption_text">Activity: CodeLens %(caption)s (%(divid)s)</span> </p>
    </div>
"""


DATA = """
<script>
var %(divid)s_vis;

if (allTraceData === undefined) {
   var allTraceData = {};
}

allTraceData["%(divid)s"] = %(tracedata)s;
</script>
</div>
"""

PTX_TEMPLATE = """
<program xml:id="{divid}" interactive="codelens" language="{language}">
    <input>
{source}
    </input>
</program>
"""


class CodeLensNode(nodes.General, nodes.Element, RunestoneIdNode):
    pass


def visit_codelens_xml(self, node):
    html = VIS
    if "caption" not in node["runestone_options"]:
        node["runestone_options"]["caption"] = node["runestone_options"][
            "question_label"
        ]
    if "tracedata" in node["runestone_options"]:
        node["runestone_options"]["tracedata"] = (
            node["runestone_options"]["tracedata"]
            .replace("<", "&lt;")
            .replace(">", "&gt;")
        )

    res = PTX_TEMPLATE.format(**node["runestone_options"])
    self.output.append(res)


def visit_codelens_html(self, node):
    html = VIS
    if "caption" not in node["runestone_options"]:
        node["runestone_options"]["caption"] = node["runestone_options"][
            "question_label"
        ]
    if "tracedata" in node["runestone_options"]:
        html += DATA
    else:
        html += "</div>"
    html = html % node["runestone_options"]

    self.body.append(html)
    addHTMLToDB(
        node["runestone_options"]["divid"],
        node["runestone_options"]["basecourse"],
        html,
    )


def depart_codelens_html(self, node):
    pass


def depart_codelens_xml(self, node):
    pass


# Some documentation to help the author.
# Here's and example of a single stack frame.
# you might ask a qestion about the value of a global variable
# in which case the correct answer is expressed as:
#
# globals.a
#
# You could ask about a value on the heap
#
# heap.variable
#
# You could ask about a local variable -- not shown here.
#
# locals.variable
#
# You could even ask about what line is going to be executed next
#
# line
# {
#   "ordered_globals": [
#     "a",
#     "b"
#   ],
#   "stdout": "1\n",
#   "func_name": "<module>",
#   "stack_to_render": [],
#   "globals": {
#     "a": 1,
#     "b": 1
#   },
#   "heap": {},
#   "line": 5,
#   "event": "return"
# }


class Codelens(RunestoneIdDirective):
    """
    .. codelens:: uniqueid
       :tracedata: Autogenerated or provided
       :caption: caption below
       :showoutput: show stdout from program
       :question: Text of question to ask on breakline
       :correct: correct answer to the question
       :feedback: feedback for incorrect answers
       :breakline: Line to stop on and pop up a question dialog
       :python: either py2 or py3

        x = 0
        for i in range(10):
           x = x + i


    config values (conf.py):

    - codelens_div_class - custom CSS class of the component's outermost div
    """

    required_arguments = 1
    optional_arguments = 1
    option_spec = RunestoneIdDirective.option_spec.copy()
    option_spec.update(
        {
            "tracedata": directives.unchanged,
            "caption": directives.unchanged,
            "showoutput": directives.flag,
            "question": directives.unchanged,
            "correct": directives.unchanged,
            "feedback": directives.unchanged,
            "breakline": directives.nonnegative_int,
            "python": directives.unchanged,
            "language": directives.unchanged,
        }
    )

    has_content = True

    def run(self):
        super(Codelens, self).run()

        addQuestionToDB(self)

        self.JS_VARNAME = ""
        self.JS_VARVAL = ""

        def raw_dict(input_code, output_trace):
            ret = dict(code=input_code, trace=output_trace)
            return ret

        def js_var_finalizer(input_code, output_trace):
            ret = dict(code=input_code, trace=output_trace)
            json_output = json.dumps(ret, indent=None)
            return json_output

        if self.content:
            source = "\n".join(self.content)
        else:
            source = "\n"
        self.options["source"] = source.replace("<", "&lt;")
        CUMULATIVE_MODE = False
        self.JS_VARNAME = self.options["divid"] + "_trace"
        env = self.state.document.settings.env
        self.options["divclass"] = env.config.codelens_div_class

        if "showoutput" not in self.options:
            self.options["embedded"] = "true"  # to set embeddedmode to true
        else:
            self.options["embedded"] = "false"

        if "language" not in self.options:
            self.options["language"] = "python"

        if "python" not in self.options:
            if six.PY2:
                self.options["python"] = "py2"
            else:
                self.options["python"] = "py3"

        if "tracedata" not in self.options:
            if "question" in self.options:
                curTrace = exec_script_str_local(
                    source, None, CUMULATIVE_MODE, None, raw_dict
                )
                self.inject_questions(curTrace)
                json_output = json.dumps(curTrace, indent=None)
                self.options["tracedata"] = json_output
            else:
                if self.options["language"] == "python":
                    self.options["tracedata"] = exec_script_str_local(
                        source, None, CUMULATIVE_MODE, None, js_var_finalizer
                    )
                elif self.options["language"] == "java":
                    self.options["tracedata"] = self.get_trace(source, "java")
                elif self.options["language"] == "cpp":
                    self.options["tracedata"] = self.get_trace(source, "cpp")
                elif self.options["language"] == "c":
                    self.options["tracedata"] = self.get_trace(source, "c")
                else:
                    raise ValueError("language not supported")

        cl_node = CodeLensNode()
        cl_node["runestone_options"] = self.options
        cl_node["source"], cl_node["line"] = self.state_machine.get_source_and_line(
            self.lineno
        )
        return [cl_node]

    def inject_questions(self, curTrace):
        if "breakline" not in self.options:
            raise RuntimeError("Must have breakline option")
        breakline = self.options["breakline"]
        for frame in curTrace["trace"]:
            if frame["line"] == breakline:
                frame["question"] = dict(
                    text=self.options["question"],
                    correct=self.options["correct"],
                    div=self.options["divid"] + "_modal",
                    feedback=self.options["feedback"],
                )

    def get_trace(self, src, lang):
        env = self.state.document.settings.env
        url = f"{env.config.trace_url}/trace{lang}"

        tries = 0
        r = None
        while tries < 5:
            try:
                r = requests.post(url, data=dict(src=src), timeout=30)
            except requests.ReadTimeout:
                self.error(
                    "The request to the trace server timed out, you will need to rerun the build"
                )
                tries += 1
                time.sleep(random.random() * tries)
            except Exception as e:
                self.error(
                    "The request to the trace server failed, you will need to rerun the build "
                    + str(e)
                )
                tries += 1
                time.sleep(random.random() * tries)

            if r and r.status_code == 200:
                if lang == "java":
                    return r.text
                else:
                    res = r.text[r.text.find('{"code":') :]
                    return res
            else:
                tries += 1
                self.error(
                    f"Failed to get trace from server, status code: {r.status_code}"
                )
                time.sleep(random.random() * tries)
        if tries == 5:
            self.error("Failed to get trace from server after 5 tries")
            return ""
        return ""
