# -*- test-case-name: twisted.web.test.test_util -*-
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
An assortment of web server-related utilities.
"""


import linecache
from html import escape

from twisted.python import urlpath
from twisted.python.reflect import fullyQualifiedName
from twisted.web import resource
from twisted.web.template import Element, TagLoader, XMLString, flattenString, renderer


def _PRE(text):
    """
    Wraps <pre> tags around some text and HTML-escape it.

    This is here since once twisted.web.html was deprecated it was hard to
    migrate the html.PRE from current code to twisted.web.template.

    For new code consider using twisted.web.template.

    @return: Escaped text wrapped in <pre> tags.
    @rtype: C{str}
    """
    return "<pre>{}</pre>".format(escape(text))


def redirectTo(URL: bytes, request) -> bytes:
    """
    Generate a redirect to the given location.

    @param URL: A L{bytes} giving the location to which to redirect.

    @param request: The request object to use to generate the redirect.
    @type request: L{IRequest<twisted.web.iweb.IRequest>} provider

    @raise TypeError: If the type of C{URL} a L{str} instead of L{bytes}.

    @return: A L{bytes} containing HTML which tries to convince the client
        agent
        to visit the new location even if it doesn't respect the I{FOUND}
        response code.  This is intended to be returned from a render method,
        eg::

            def render_GET(self, request):
                return redirectTo(b"http://example.com/", request)
    """
    if not isinstance(URL, bytes):
        raise TypeError("URL must be bytes")
    request.setHeader(b"Content-Type", b"text/html; charset=utf-8")
    request.redirect(URL)
    # FIXME: The URL should be HTML-escaped.
    # https://twistedmatrix.com/trac/ticket/9839
    content = b"""
<html>
    <head>
        <meta http-equiv=\"refresh\" content=\"0;URL=%(url)s\">
    </head>
    <body bgcolor=\"#FFFFFF\" text=\"#000000\">
    <a href=\"%(url)s\">click here</a>
    </body>
</html>
""" % {
        b"url": URL
    }
    return content


class Redirect(resource.Resource):
    """
    Resource that redirects to a specific URL.

    @ivar url: Redirect target URL to put in the I{Location} response header.
    @type url: L{bytes}
    """

    isLeaf = True

    def __init__(self, url: bytes):
        super().__init__()
        self.url = url

    def render(self, request):
        return redirectTo(self.url, request)

    def getChild(self, name, request):
        return self


# FIXME: This is totally broken, see https://twistedmatrix.com/trac/ticket/9838
class ChildRedirector(Redirect):
    isLeaf = False

    def __init__(self, url):
        # XXX is this enough?
        if (
            (url.find("://") == -1)
            and (not url.startswith(".."))
            and (not url.startswith("/"))
        ):
            raise ValueError(
                (
                    "It seems you've given me a redirect (%s) that is a child of"
                    " myself! That's not good, it'll cause an infinite redirect."
                )
                % url
            )
        Redirect.__init__(self, url)

    def getChild(self, name, request):
        newUrl = self.url
        if not newUrl.endswith("/"):
            newUrl += "/"
        newUrl += name
        return ChildRedirector(newUrl)


class ParentRedirect(resource.Resource):
    """
    Redirect to the nearest directory and strip any query string.

    This generates redirects like::

        /              \u2192  /
        /foo           \u2192  /
        /foo?bar       \u2192  /
        /foo/          \u2192  /foo/
        /foo/bar       \u2192  /foo/
        /foo/bar?baz   \u2192  /foo/

    However, the generated I{Location} header contains an absolute URL rather
    than a path.

    The response is the same regardless of HTTP method.
    """

    isLeaf = 1

    def render(self, request) -> bytes:
        """
        Respond to all requests by redirecting to nearest directory.
        """
        here = str(urlpath.URLPath.fromRequest(request).here()).encode("ascii")
        return redirectTo(here, request)


class DeferredResource(resource.Resource):
    """
    I wrap up a Deferred that will eventually result in a Resource
    object.
    """

    isLeaf = 1

    def __init__(self, d):
        resource.Resource.__init__(self)
        self.d = d

    def getChild(self, name, request):
        return self

    def render(self, request):
        self.d.addCallback(self._cbChild, request).addErrback(self._ebChild, request)
        from twisted.web.server import NOT_DONE_YET

        return NOT_DONE_YET

    def _cbChild(self, child, request):
        request.render(resource.getChildForRequest(child, request))

    def _ebChild(self, reason, request):
        request.processingFailed(reason)


class _SourceLineElement(Element):
    """
    L{_SourceLineElement} is an L{IRenderable} which can render a single line of
    source code.

    @ivar number: A C{int} giving the line number of the source code to be
        rendered.
    @ivar source: A C{str} giving the source code to be rendered.
    """

    def __init__(self, loader, number, source):
        Element.__init__(self, loader)
        self.number = number
        self.source = source

    @renderer
    def sourceLine(self, request, tag):
        """
        Render the line of source as a child of C{tag}.
        """
        return tag(self.source.replace("  ", " \N{NO-BREAK SPACE}"))

    @renderer
    def lineNumber(self, request, tag):
        """
        Render the line number as a child of C{tag}.
        """
        return tag(str(self.number))


class _SourceFragmentElement(Element):
    """
    L{_SourceFragmentElement} is an L{IRenderable} which can render several lines
    of source code near the line number of a particular frame object.

    @ivar frame: A L{Failure<twisted.python.failure.Failure>}-style frame object
        for which to load a source line to render.  This is really a tuple
        holding some information from a frame object.  See
        L{Failure.frames<twisted.python.failure.Failure>} for specifics.
    """

    def __init__(self, loader, frame):
        Element.__init__(self, loader)
        self.frame = frame

    def _getSourceLines(self):
        """
        Find the source line references by C{self.frame} and yield, in source
        line order, it and the previous and following lines.

        @return: A generator which yields two-tuples.  Each tuple gives a source
            line number and the contents of that source line.
        """
        filename = self.frame[1]
        lineNumber = self.frame[2]
        for snipLineNumber in range(lineNumber - 1, lineNumber + 2):
            yield (snipLineNumber, linecache.getline(filename, snipLineNumber).rstrip())

    @renderer
    def sourceLines(self, request, tag):
        """
        Render the source line indicated by C{self.frame} and several
        surrounding lines.  The active line will be given a I{class} of
        C{"snippetHighlightLine"}.  Other lines will be given a I{class} of
        C{"snippetLine"}.
        """
        for (lineNumber, sourceLine) in self._getSourceLines():
            newTag = tag.clone()
            if lineNumber == self.frame[2]:
                cssClass = "snippetHighlightLine"
            else:
                cssClass = "snippetLine"
            loader = TagLoader(newTag(**{"class": cssClass}))
            yield _SourceLineElement(loader, lineNumber, sourceLine)


class _FrameElement(Element):
    """
    L{_FrameElement} is an L{IRenderable} which can render details about one
    frame from a L{Failure<twisted.python.failure.Failure>}.

    @ivar frame: A L{Failure<twisted.python.failure.Failure>}-style frame object
        for which to load a source line to render.  This is really a tuple
        holding some information from a frame object.  See
        L{Failure.frames<twisted.python.failure.Failure>} for specifics.
    """

    def __init__(self, loader, frame):
        Element.__init__(self, loader)
        self.frame = frame

    @renderer
    def filename(self, request, tag):
        """
        Render the name of the file this frame references as a child of C{tag}.
        """
        return tag(self.frame[1])

    @renderer
    def lineNumber(self, request, tag):
        """
        Render the source line number this frame references as a child of
        C{tag}.
        """
        return tag(str(self.frame[2]))

    @renderer
    def function(self, request, tag):
        """
        Render the function name this frame references as a child of C{tag}.
        """
        return tag(self.frame[0])

    @renderer
    def source(self, request, tag):
        """
        Render the source code surrounding the line this frame references,
        replacing C{tag}.
        """
        return _SourceFragmentElement(TagLoader(tag), self.frame)


class _StackElement(Element):
    """
    L{_StackElement} renders an L{IRenderable} which can render a list of frames.
    """

    def __init__(self, loader, stackFrames):
        Element.__init__(self, loader)
        self.stackFrames = stackFrames

    @renderer
    def frames(self, request, tag):
        """
        Render the list of frames in this L{_StackElement}, replacing C{tag}.
        """
        return [
            _FrameElement(TagLoader(tag.clone()), frame) for frame in self.stackFrames
        ]


class FailureElement(Element):
    """
    L{FailureElement} is an L{IRenderable} which can render detailed information
    about a L{Failure<twisted.python.failure.Failure>}.

    @ivar failure: The L{Failure<twisted.python.failure.Failure>} instance which
        will be rendered.

    @since: 12.1
    """

    loader = XMLString(
        """
<div xmlns:t="http://twistedmatrix.com/ns/twisted.web.template/0.1">
  <style type="text/css">
    div.error {
      color: red;
      font-family: Verdana, Arial, helvetica, sans-serif;
      font-weight: bold;
    }

    div {
      font-family: Verdana, Arial, helvetica, sans-serif;
    }

    div.stackTrace {
    }

    div.frame {
      padding: 1em;
      background: white;
      border-bottom: thin black dashed;
    }

    div.frame:first-child {
      padding: 1em;
      background: white;
      border-top: thin black dashed;
      border-bottom: thin black dashed;
    }

    div.location {
    }

    span.function {
      font-weight: bold;
      font-family: "Courier New", courier, monospace;
    }

    div.snippet {
      margin-bottom: 0.5em;
      margin-left: 1em;
      background: #FFFFDD;
    }

    div.snippetHighlightLine {
      color: red;
    }

    span.code {
      font-family: "Courier New", courier, monospace;
    }
  </style>

  <div class="error">
    <span t:render="type" />: <span t:render="value" />
  </div>
  <div class="stackTrace" t:render="traceback">
    <div class="frame" t:render="frames">
      <div class="location">
        <span t:render="filename" />:<span t:render="lineNumber" /> in
        <span class="function" t:render="function" />
      </div>
      <div class="snippet" t:render="source">
        <div t:render="sourceLines">
          <span class="lineno" t:render="lineNumber" />
          <code class="code" t:render="sourceLine" />
        </div>
      </div>
    </div>
  </div>
  <div class="error">
    <span t:render="type" />: <span t:render="value" />
  </div>
</div>
"""
    )

    def __init__(self, failure, loader=None):
        Element.__init__(self, loader)
        self.failure = failure

    @renderer
    def type(self, request, tag):
        """
        Render the exception type as a child of C{tag}.
        """
        return tag(fullyQualifiedName(self.failure.type))

    @renderer
    def value(self, request, tag):
        """
        Render the exception value as a child of C{tag}.
        """
        return tag(str(self.failure.value).encode("utf8"))

    @renderer
    def traceback(self, request, tag):
        """
        Render all the frames in the wrapped
        L{Failure<twisted.python.failure.Failure>}'s traceback stack, replacing
        C{tag}.
        """
        return _StackElement(TagLoader(tag), self.failure.frames)


def formatFailure(myFailure):
    """
    Construct an HTML representation of the given failure.

    Consider using L{FailureElement} instead.

    @type myFailure: L{Failure<twisted.python.failure.Failure>}

    @rtype: L{bytes}
    @return: A string containing the HTML representation of the given failure.
    """
    result = []
    flattenString(None, FailureElement(myFailure)).addBoth(result.append)
    if isinstance(result[0], bytes):
        # Ensure the result string is all ASCII, for compatibility with the
        # default encoding expected by browsers.
        return result[0].decode("utf-8").encode("ascii", "xmlcharrefreplace")
    result[0].raiseException()


__all__ = [
    "redirectTo",
    "Redirect",
    "ChildRedirector",
    "ParentRedirect",
    "DeferredResource",
    "FailureElement",
    "formatFailure",
]
