from http.server import HTTPServer, SimpleHTTPRequestHandler
import os
import sys
from urllib.parse import urlparse, unquote
import posixpath
from io import StringIO, BytesIO
import re
import shutil
import threading
import time
import socket
import itertools
import configparser

import Reporter

###
# Various patterns matched or replaced by server.

kReportFileRE = re.compile("(.*/)?report-(.*)\\.html")

kBugKeyValueRE = re.compile("<!-- BUG([^ ]*) (.*) -->")

#  <!-- REPORTPROBLEM file="crashes/clang_crash_ndSGF9.mi" stderr="crashes/clang_crash_ndSGF9.mi.stderr.txt" info="crashes/clang_crash_ndSGF9.mi.info" -->

kReportCrashEntryRE = re.compile("<!-- REPORTPROBLEM (.*?)-->")
kReportCrashEntryKeyValueRE = re.compile(' ?([^=]+)="(.*?)"')

kReportReplacements = []

# Add custom javascript.
kReportReplacements.append(
    (
        re.compile("<!-- SUMMARYENDHEAD -->"),
        """\
<script language="javascript" type="text/javascript">
function load(url) {
  if (window.XMLHttpRequest) {
    req = new XMLHttpRequest();
  } else if (window.ActiveXObject) {
    req = new ActiveXObject("Microsoft.XMLHTTP");
  }
  if (req != undefined) {
    req.open("GET", url, true);
    req.send("");
  }
}
</script>""",
    )
)

# Insert additional columns.
kReportReplacements.append((re.compile("<!-- REPORTBUGCOL -->"), "<td></td><td></td>"))

# Insert report bug and open file links.
kReportReplacements.append(
    (
        re.compile('<!-- REPORTBUG id="report-(.*)\\.html" -->'),
        (
            '<td class="Button"><a href="report/\\1">Report Bug</a></td>'
            + '<td class="Button"><a href="javascript:load(\'open/\\1\')">Open File</a></td>'
        ),
    )
)

kReportReplacements.append(
    (
        re.compile("<!-- REPORTHEADER -->"),
        '<h3><a href="/">Summary</a> > Report %(report)s</h3>',
    )
)

kReportReplacements.append(
    (
        re.compile("<!-- REPORTSUMMARYEXTRA -->"),
        '<td class="Button"><a href="report/%(report)s">Report Bug</a></td>',
    )
)

# Insert report crashes link.

# Disabled for the time being until we decide exactly when this should
# be enabled. Also the radar reporter needs to be fixed to report
# multiple files.

# kReportReplacements.append((re.compile('<!-- REPORTCRASHES -->'),
#                            '<br>These files will automatically be attached to ' +
#                            'reports filed here: <a href="report_crashes">Report Crashes</a>.'))

###
# Other simple parameters

kShare = posixpath.join(posixpath.dirname(__file__), "../share/scan-view")
kConfigPath = os.path.expanduser("~/.scanview.cfg")

###

__version__ = "0.1"

__all__ = ["create_server"]


class ReporterThread(threading.Thread):
    def __init__(self, report, reporter, parameters, server):
        threading.Thread.__init__(self)
        self.report = report
        self.server = server
        self.reporter = reporter
        self.parameters = parameters
        self.success = False
        self.status = None

    def run(self):
        result = None
        try:
            if self.server.options.debug:
                print("%s: SERVER: submitting bug." % (sys.argv[0],), file=sys.stderr)
            self.status = self.reporter.fileReport(self.report, self.parameters)
            self.success = True
            time.sleep(3)
            if self.server.options.debug:
                print(
                    "%s: SERVER: submission complete." % (sys.argv[0],), file=sys.stderr
                )
        except Reporter.ReportFailure as e:
            self.status = e.value
        except Exception as e:
            s = StringIO()
            import traceback

            print("<b>Unhandled Exception</b><br><pre>", file=s)
            traceback.print_exc(file=s)
            print("</pre>", file=s)
            self.status = s.getvalue()


class ScanViewServer(HTTPServer):
    def __init__(self, address, handler, root, reporters, options):
        HTTPServer.__init__(self, address, handler)
        self.root = root
        self.reporters = reporters
        self.options = options
        self.halted = False
        self.config = None
        self.load_config()

    def load_config(self):
        self.config = configparser.RawConfigParser()

        # Add defaults
        self.config.add_section("ScanView")
        for r in self.reporters:
            self.config.add_section(r.getName())
            for p in r.getParameters():
                if p.saveConfigValue():
                    self.config.set(r.getName(), p.getName(), "")

        # Ignore parse errors
        try:
            self.config.read([kConfigPath])
        except:
            pass

        # Save on exit
        import atexit

        atexit.register(lambda: self.save_config())

    def save_config(self):
        # Ignore errors (only called on exit).
        try:
            f = open(kConfigPath, "w")
            self.config.write(f)
            f.close()
        except:
            pass

    def halt(self):
        self.halted = True
        if self.options.debug:
            print("%s: SERVER: halting." % (sys.argv[0],), file=sys.stderr)

    def serve_forever(self):
        while not self.halted:
            if self.options.debug > 1:
                print("%s: SERVER: waiting..." % (sys.argv[0],), file=sys.stderr)
            try:
                self.handle_request()
            except OSError as e:
                print("OSError", e.errno)

    def finish_request(self, request, client_address):
        if self.options.autoReload:
            import ScanView

            self.RequestHandlerClass = reload(ScanView).ScanViewRequestHandler
        HTTPServer.finish_request(self, request, client_address)

    def handle_error(self, request, client_address):
        # Ignore socket errors
        info = sys.exc_info()
        if info and isinstance(info[1], socket.error):
            if self.options.debug > 1:
                print(
                    "%s: SERVER: ignored socket error." % (sys.argv[0],),
                    file=sys.stderr,
                )
            return
        HTTPServer.handle_error(self, request, client_address)


# Borrowed from Quixote, with simplifications.
def parse_query(qs, fields=None):
    if fields is None:
        fields = {}
    for chunk in (_f for _f in qs.split("&") if _f):
        if "=" not in chunk:
            name = chunk
            value = ""
        else:
            name, value = chunk.split("=", 1)
        name = unquote(name.replace("+", " "))
        value = unquote(value.replace("+", " "))
        item = fields.get(name)
        if item is None:
            fields[name] = [value]
        else:
            item.append(value)
    return fields


class ScanViewRequestHandler(SimpleHTTPRequestHandler):
    server_version = "ScanViewServer/" + __version__
    dynamic_mtime = time.time()

    def do_HEAD(self):
        try:
            SimpleHTTPRequestHandler.do_HEAD(self)
        except Exception as e:
            self.handle_exception(e)

    def do_GET(self):
        try:
            SimpleHTTPRequestHandler.do_GET(self)
        except Exception as e:
            self.handle_exception(e)

    def do_POST(self):
        """Serve a POST request."""
        try:
            length = self.headers.getheader("content-length") or "0"
            try:
                length = int(length)
            except:
                length = 0
            content = self.rfile.read(length)
            fields = parse_query(content)
            f = self.send_head(fields)
            if f:
                self.copyfile(f, self.wfile)
                f.close()
        except Exception as e:
            self.handle_exception(e)

    def log_message(self, format, *args):
        if self.server.options.debug:
            sys.stderr.write(
                "%s: SERVER: %s - - [%s] %s\n"
                % (
                    sys.argv[0],
                    self.address_string(),
                    self.log_date_time_string(),
                    format % args,
                )
            )

    def load_report(self, report):
        path = os.path.join(self.server.root, "report-%s.html" % report)
        data = open(path).read()
        keys = {}
        for item in kBugKeyValueRE.finditer(data):
            k, v = item.groups()
            keys[k] = v
        return keys

    def load_crashes(self):
        path = posixpath.join(self.server.root, "index.html")
        data = open(path).read()
        problems = []
        for item in kReportCrashEntryRE.finditer(data):
            fieldData = item.group(1)
            fields = dict(
                [i.groups() for i in kReportCrashEntryKeyValueRE.finditer(fieldData)]
            )
            problems.append(fields)
        return problems

    def handle_exception(self, exc):
        import traceback

        s = StringIO()
        print("INTERNAL ERROR\n", file=s)
        traceback.print_exc(file=s)
        f = self.send_string(s.getvalue(), "text/plain")
        if f:
            self.copyfile(f, self.wfile)
            f.close()

    def get_scalar_field(self, name):
        if name in self.fields:
            return self.fields[name][0]
        else:
            return None

    def submit_bug(self, c):
        title = self.get_scalar_field("title")
        description = self.get_scalar_field("description")
        report = self.get_scalar_field("report")
        reporterIndex = self.get_scalar_field("reporter")
        files = []
        for fileID in self.fields.get("files", []):
            try:
                i = int(fileID)
            except:
                i = None
            if i is None or i < 0 or i >= len(c.files):
                return (False, "Invalid file ID")
            files.append(c.files[i])

        if not title:
            return (False, "Missing title.")
        if not description:
            return (False, "Missing description.")
        try:
            reporterIndex = int(reporterIndex)
        except:
            return (False, "Invalid report method.")

        # Get the reporter and parameters.
        reporter = self.server.reporters[reporterIndex]
        parameters = {}
        for o in reporter.getParameters():
            name = "%s_%s" % (reporter.getName(), o.getName())
            if name not in self.fields:
                return (
                    False,
                    'Missing field "%s" for %s report method.'
                    % (name, reporter.getName()),
                )
            parameters[o.getName()] = self.get_scalar_field(name)

        # Update config defaults.
        if report != "None":
            self.server.config.set("ScanView", "reporter", reporterIndex)
            for o in reporter.getParameters():
                if o.saveConfigValue():
                    name = o.getName()
                    self.server.config.set(reporter.getName(), name, parameters[name])

        # Create the report.
        bug = Reporter.BugReport(title, description, files)

        # Kick off a reporting thread.
        t = ReporterThread(bug, reporter, parameters, self.server)
        t.start()

        # Wait for thread to die...
        while t.isAlive():
            time.sleep(0.25)
        submitStatus = t.status

        return (t.success, t.status)

    def send_report_submit(self):
        report = self.get_scalar_field("report")
        c = self.get_report_context(report)
        if c.reportSource is None:
            reportingFor = "Report Crashes > "
            fileBug = (
                """\
<a href="/report_crashes">File Bug</a> > """
                % locals()
            )
        else:
            reportingFor = '<a href="/%s">Report %s</a> > ' % (c.reportSource, report)
            fileBug = '<a href="/report/%s">File Bug</a> > ' % report
        title = self.get_scalar_field("title")
        description = self.get_scalar_field("description")

        res, message = self.submit_bug(c)

        if res:
            statusClass = "SubmitOk"
            statusName = "Succeeded"
        else:
            statusClass = "SubmitFail"
            statusName = "Failed"

        result = (
            """
<head>
  <title>Bug Submission</title>
  <link rel="stylesheet" type="text/css" href="/scanview.css" />
</head>
<body>
<h3>
<a href="/">Summary</a> > 
%(reportingFor)s
%(fileBug)s
Submit</h3>
<form name="form" action="">
<table class="form">
<tr><td>
<table class="form_group">
<tr>
  <td class="form_clabel">Title:</td>
  <td class="form_value">
    <input type="text" name="title" size="50" value="%(title)s" disabled>
  </td>
</tr>
<tr>
  <td class="form_label">Description:</td>
  <td class="form_value">
<textarea rows="10" cols="80" name="description" disabled>
%(description)s
</textarea>
  </td>
</table>
</td></tr>
</table>
</form>
<h1 class="%(statusClass)s">Submission %(statusName)s</h1>
%(message)s
<p>
<hr>
<a href="/">Return to Summary</a>
</body>
</html>"""
            % locals()
        )
        return self.send_string(result)

    def send_open_report(self, report):
        try:
            keys = self.load_report(report)
        except IOError:
            return self.send_error(400, "Invalid report.")

        file = keys.get("FILE")
        if not file or not posixpath.exists(file):
            return self.send_error(400, 'File does not exist: "%s"' % file)

        import startfile

        if self.server.options.debug:
            print('%s: SERVER: opening "%s"' % (sys.argv[0], file), file=sys.stderr)

        status = startfile.open(file)
        if status:
            res = 'Opened: "%s"' % file
        else:
            res = 'Open failed: "%s"' % file

        return self.send_string(res, "text/plain")

    def get_report_context(self, report):
        class Context(object):
            pass

        if report is None or report == "None":
            data = self.load_crashes()
            # Don't allow empty reports.
            if not data:
                raise ValueError("No crashes detected!")
            c = Context()
            c.title = "clang static analyzer failures"

            stderrSummary = ""
            for item in data:
                if "stderr" in item:
                    path = posixpath.join(self.server.root, item["stderr"])
                    if os.path.exists(path):
                        lns = itertools.islice(open(path), 0, 10)
                        stderrSummary += "%s\n--\n%s" % (
                            item.get("src", "<unknown>"),
                            "".join(lns),
                        )

            c.description = """\
The clang static analyzer failed on these inputs:
%s

STDERR Summary
--------------
%s
""" % (
                "\n".join([item.get("src", "<unknown>") for item in data]),
                stderrSummary,
            )
            c.reportSource = None
            c.navMarkup = "Report Crashes > "
            c.files = []
            for item in data:
                c.files.append(item.get("src", ""))
                c.files.append(posixpath.join(self.server.root, item.get("file", "")))
                c.files.append(
                    posixpath.join(self.server.root, item.get("clangfile", ""))
                )
                c.files.append(posixpath.join(self.server.root, item.get("stderr", "")))
                c.files.append(posixpath.join(self.server.root, item.get("info", "")))
            # Just in case something failed, ignore files which don't
            # exist.
            c.files = [f for f in c.files if os.path.exists(f) and os.path.isfile(f)]
        else:
            # Check that this is a valid report.
            path = posixpath.join(self.server.root, "report-%s.html" % report)
            if not posixpath.exists(path):
                raise ValueError("Invalid report ID")
            keys = self.load_report(report)
            c = Context()
            c.title = keys.get("DESC", "clang error (unrecognized")
            c.description = """\
Bug reported by the clang static analyzer.

Description: %s
File: %s
Line: %s
""" % (
                c.title,
                keys.get("FILE", "<unknown>"),
                keys.get("LINE", "<unknown>"),
            )
            c.reportSource = "report-%s.html" % report
            c.navMarkup = """<a href="/%s">Report %s</a> > """ % (
                c.reportSource,
                report,
            )

            c.files = [path]
        return c

    def send_report(self, report, configOverrides=None):
        def getConfigOption(section, field):
            if (
                configOverrides is not None
                and section in configOverrides
                and field in configOverrides[section]
            ):
                return configOverrides[section][field]
            return self.server.config.get(section, field)

        # report is None is used for crashes
        try:
            c = self.get_report_context(report)
        except ValueError as e:
            return self.send_error(400, e.message)

        title = c.title
        description = c.description
        reportingFor = c.navMarkup
        if c.reportSource is None:
            extraIFrame = ""
        else:
            extraIFrame = """\
<iframe src="/%s" width="100%%" height="40%%"
        scrolling="auto" frameborder="1">
  <a href="/%s">View Bug Report</a>
</iframe>""" % (
                c.reportSource,
                c.reportSource,
            )

        reporterSelections = []
        reporterOptions = []

        try:
            active = int(getConfigOption("ScanView", "reporter"))
        except:
            active = 0
        for i, r in enumerate(self.server.reporters):
            selected = i == active
            if selected:
                selectedStr = " selected"
            else:
                selectedStr = ""
            reporterSelections.append(
                '<option value="%d"%s>%s</option>' % (i, selectedStr, r.getName())
            )
            options = "\n".join(
                [o.getHTML(r, title, getConfigOption) for o in r.getParameters()]
            )
            display = ("none", "")[selected]
            reporterOptions.append(
                """\
<tr id="%sReporterOptions" style="display:%s">
  <td class="form_label">%s Options</td>
  <td class="form_value">
    <table class="form_inner_group">
%s
    </table>
  </td>
</tr>
"""
                % (r.getName(), display, r.getName(), options)
            )
        reporterSelections = "\n".join(reporterSelections)
        reporterOptionsDivs = "\n".join(reporterOptions)
        reportersArray = "[%s]" % (
            ",".join([repr(r.getName()) for r in self.server.reporters])
        )

        if c.files:
            fieldSize = min(5, len(c.files))
            attachFileOptions = "\n".join(
                [
                    """\
<option value="%d" selected>%s</option>"""
                    % (i, v)
                    for i, v in enumerate(c.files)
                ]
            )
            attachFileRow = """\
<tr>
  <td class="form_label">Attach:</td>
  <td class="form_value">
<select style="width:100%%" name="files" multiple size=%d>
%s
</select>
  </td>
</tr>
""" % (
                min(5, len(c.files)),
                attachFileOptions,
            )
        else:
            attachFileRow = ""

        result = (
            """<html>
<head>
  <title>File Bug</title>
  <link rel="stylesheet" type="text/css" href="/scanview.css" />
</head>
<script language="javascript" type="text/javascript">
var reporters = %(reportersArray)s;
function updateReporterOptions() {
  index = document.getElementById('reporter').selectedIndex;
  for (var i=0; i < reporters.length; ++i) {
    o = document.getElementById(reporters[i] + "ReporterOptions");
    if (i == index) {
      o.style.display = "";
    } else {
      o.style.display = "none";
    }
  }
}
</script>
<body onLoad="updateReporterOptions()">
<h3>
<a href="/">Summary</a> > 
%(reportingFor)s
File Bug</h3>
<form name="form" action="/report_submit" method="post">
<input type="hidden" name="report" value="%(report)s">

<table class="form">
<tr><td>
<table class="form_group">
<tr>
  <td class="form_clabel">Title:</td>
  <td class="form_value">
    <input type="text" name="title" size="50" value="%(title)s">
  </td>
</tr>
<tr>
  <td class="form_label">Description:</td>
  <td class="form_value">
<textarea rows="10" cols="80" name="description">
%(description)s
</textarea>
  </td>
</tr>

%(attachFileRow)s

</table>
<br>
<table class="form_group">
<tr>
  <td class="form_clabel">Method:</td>
  <td class="form_value">
    <select id="reporter" name="reporter" onChange="updateReporterOptions()">
    %(reporterSelections)s
    </select>
  </td>
</tr>
%(reporterOptionsDivs)s
</table>
<br>
</td></tr>
<tr><td class="form_submit">
  <input align="right" type="submit" name="Submit" value="Submit">
</td></tr>
</table>
</form>

%(extraIFrame)s

</body>
</html>"""
            % locals()
        )

        return self.send_string(result)

    def send_head(self, fields=None):
        if self.server.options.onlyServeLocal and self.client_address[0] != "127.0.0.1":
            return self.send_error(401, "Unauthorized host.")

        if fields is None:
            fields = {}
        self.fields = fields

        o = urlparse(self.path)
        self.fields = parse_query(o.query, fields)
        path = posixpath.normpath(unquote(o.path))

        # Split the components and strip the root prefix.
        components = path.split("/")[1:]

        # Special case some top-level entries.
        if components:
            name = components[0]
            if len(components) == 2:
                if name == "report":
                    return self.send_report(components[1])
                elif name == "open":
                    return self.send_open_report(components[1])
            elif len(components) == 1:
                if name == "quit":
                    self.server.halt()
                    return self.send_string("Goodbye.", "text/plain")
                elif name == "report_submit":
                    return self.send_report_submit()
                elif name == "report_crashes":
                    overrides = {"ScanView": {}, "Radar": {}, "Email": {}}
                    for i, r in enumerate(self.server.reporters):
                        if r.getName() == "Radar":
                            overrides["ScanView"]["reporter"] = i
                            break
                    overrides["Radar"]["Component"] = "llvm - checker"
                    overrides["Radar"]["Component Version"] = "X"
                    return self.send_report(None, overrides)
                elif name == "favicon.ico":
                    return self.send_path(posixpath.join(kShare, "bugcatcher.ico"))

        # Match directory entries.
        if components[-1] == "":
            components[-1] = "index.html"

        relpath = "/".join(components)
        path = posixpath.join(self.server.root, relpath)

        if self.server.options.debug > 1:
            print(
                '%s: SERVER: sending path "%s"' % (sys.argv[0], path), file=sys.stderr
            )
        return self.send_path(path)

    def send_404(self):
        self.send_error(404, "File not found")
        return None

    def send_path(self, path):
        # If the requested path is outside the root directory, do not open it
        rel = os.path.abspath(path)
        if not rel.startswith(os.path.abspath(self.server.root)):
            return self.send_404()

        ctype = self.guess_type(path)
        if ctype.startswith("text/"):
            # Patch file instead
            return self.send_patched_file(path, ctype)
        else:
            mode = "rb"
        try:
            f = open(path, mode)
        except IOError:
            return self.send_404()
        return self.send_file(f, ctype)

    def send_file(self, f, ctype):
        # Patch files to add links, but skip binary files.
        self.send_response(200)
        self.send_header("Content-type", ctype)
        fs = os.fstat(f.fileno())
        self.send_header("Content-Length", str(fs[6]))
        self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
        self.end_headers()
        return f

    def send_string(self, s, ctype="text/html", headers=True, mtime=None):
        encoded_s = s.encode("utf-8")
        if headers:
            self.send_response(200)
            self.send_header("Content-type", ctype)
            self.send_header("Content-Length", str(len(encoded_s)))
            if mtime is None:
                mtime = self.dynamic_mtime
            self.send_header("Last-Modified", self.date_time_string(mtime))
            self.end_headers()
        return BytesIO(encoded_s)

    def send_patched_file(self, path, ctype):
        # Allow a very limited set of variables. This is pretty gross.
        variables = {}
        variables["report"] = ""
        m = kReportFileRE.match(path)
        if m:
            variables["report"] = m.group(2)

        try:
            f = open(path, "rb")
        except IOError:
            return self.send_404()
        fs = os.fstat(f.fileno())
        data = f.read().decode("utf-8")
        for a, b in kReportReplacements:
            data = a.sub(b % variables, data)
        return self.send_string(data, ctype, mtime=fs.st_mtime)


def create_server(address, options, root):
    import Reporter

    reporters = Reporter.getReporters()

    return ScanViewServer(address, ScanViewRequestHandler, root, reporters, options)
