#!/usr/bin/python
# -*- coding: utf-8 -*-

import difflib
import mimetypes
import os
import re

import werkzeug
import werkzeug.contrib.atom

pygments = None
try:
    import pygments
    import pygments.formatters
    import pygments.lexers
    import pygments.styles
    import pygments.util
except ImportError:
    pass

Image = None
try:
    import Image
except ImportError:
    pass

from hatta import error
from hatta import parser


def page_mime(title):
    """
    Guess page's mime type based on corresponding file name.
    Default ot text/x-wiki for files without an extension.

    >>> page_mime(u'something.txt')
    'text/plain'
    >>> page_mime(u'SomePage')
    'text/x-wiki'
    >>> page_mime(u'ąęśUnicodePage')
    'text/x-wiki'
    >>> page_mime(u'image.png')
    'image/png'
    >>> page_mime(u'style.css')
    'text/css'
    >>> page_mime(u'archive.tar.gz')
    'archive/gzip'
    """

    addr = title.encode('utf-8')  # the encoding doesn't relly matter here
    mime, encoding = mimetypes.guess_type(addr, strict=False)
    if encoding:
        mime = 'archive/%s' % encoding
    if mime is None:
        mime = 'text/x-wiki'
    return mime


def date_html(date_time):
    """
    Create HTML for a date, according to recommendation at
    http://microformats.org/wiki/date
    """

    return date_time.strftime(
        '<abbr class="date" title="%Y-%m-%dT%H:%M:%SZ">%Y-%m-%d %H:%M</abbr>')


class WikiPage(object):
    """Everything needed for rendering a page."""

    def __init__(self, wiki, request, title, mime):
        self.request = request
        self.title = title
        self.mime = mime
        # for now we just use the globals from wiki object
        if request:
            self.get_url = request.get_url
            self.get_download_url = request.get_download_url
        self.wiki = wiki
        self.storage = self.wiki.storage
        self.index = self.wiki.index
        self.config = self.wiki.config
        if self.wiki.alias_page and self.wiki.alias_page in self.storage:
            self.aliases = dict(
                self.index.page_links_and_labels(self.wiki.alias_page))
        else:
            self.aliases = {}

    def link_alias(self, addr):
        """Find a target address for an alias."""

        try:
            alias, target = addr.split(':', 1)
        except ValueError:
            return self.wiki.alias_page
        try:
            pattern = self.aliases[alias]
        except KeyError:
            return self.wiki.alias_page
        try:
            link = pattern % target
        except TypeError:
            link = pattern + target
        return link

    def wiki_link(self, addr, label=None, class_=None, image=None, lineno=0):
        """Create HTML for a wiki link."""

        addr = addr.strip()
        text = werkzeug.escape(label or addr)
        chunk = ''
        if class_ is not None:
            classes = [class_]
        else:
            classes = []
        if parser.external_link(addr):
            classes.append('external')
            if addr.startswith('mailto:'):
                # Obfuscate e-mails a little bit.
                classes.append('mail')
                text = text.replace('@', '&#64;').replace('.', '&#46;')
                href = werkzeug.escape(addr,
                    quote=True).replace('@', '%40').replace('.', '%2E')
            else:
                href = werkzeug.escape(werkzeug.url_fix(addr), quote=True)
        else:
            if '#' in addr:
                addr, chunk = addr.split('#', 1)
                chunk = '#' + werkzeug.url_fix(chunk)
            if addr.startswith(':'):
                alias = self.link_alias(addr[1:])
                href = werkzeug.escape(werkzeug.url_fix(alias) + chunk, True)
                classes.append('external')
                classes.append('alias')
            elif addr.startswith('+'):
                href = '/'.join([self.request.script_root,
                                 '+' + werkzeug.escape(addr[1:], quote=True)])
                classes.append('special')
            elif addr == u'':
                href = werkzeug.escape(chunk, True)
                classes.append('anchor')
            else:
                classes.append('wiki')
                href = werkzeug.escape(self.get_url(addr) + chunk, True)
                if addr not in self.storage:
                    classes.append('nonexistent')
        class_ = werkzeug.escape(' '.join(classes) or '', True)
        # We need to output HTML on our own to prevent escaping of href
        return '<a href="%s" class="%s" title="%s">%s</a>' % (
                href, class_, werkzeug.escape(addr + chunk, True),
                image or text)

    def wiki_image(self, addr, alt, class_='wiki', lineno=0):
        """Create HTML for a wiki image."""

        addr = addr.strip()
        html = werkzeug.html
        chunk = ''
        if parser.external_link(addr):
            return html.img(src=werkzeug.url_fix(addr), class_="external",
                            alt=alt)
        if '#' in addr:
            addr, chunk = addr.split('#', 1)
        if addr == '':
            return html.a(name=chunk)
        elif addr.startswith(':'):
            if chunk:
                chunk = '#' + chunk
            alias = self.link_alias(addr[1:])
            href = werkzeug.url_fix(alias + chunk)
            return html.img(src=href, class_="external alias", alt=alt)
        elif addr in self.storage:
            mime = page_mime(addr)
            if mime.startswith('image/'):
                return html.img(src=self.get_download_url(addr), class_=class_,
                                alt=alt)
            else:
                return html.img(href=self.get_download_url(addr), alt=alt)
        else:
            return html.a(html(alt), href=self.get_url(addr))

    def menu(self):
        """Generate the menu items"""
        _ = self.wiki.gettext
        if self.wiki.menu_page in self.storage:
            items = self.index.page_links_and_labels(self.wiki.menu_page)
        else:
            items = [
                (self.wiki.front_page, self.wiki.front_page),
                ('+history', _(u'Recent changes')),
            ]
        for link, label in items:
            if link == self.title:
                class_ = "current"
            else:
                class_ = None
            yield self.wiki_link(link, label, class_=class_)

    def template(self, template_name, **kwargs):
        template = self.wiki.template_env.get_template(template_name)
        edit_url = None
        if self.title:
            try:
                self.wiki._check_lock(self.title)
                edit_url = self.get_url(self.title, self.wiki.edit)
            except error.ForbiddenErr:
                pass
        context = {
            'request': self.request,
            'wiki': self.wiki,
            'title': self.title,
            'mime': self.mime,
            'url': self.get_url,
            'download_url': self.get_download_url,
            'config': self.config,
            'page': self,
            'edit_url': edit_url,
        }
        context.update(kwargs)
        stream = template.stream(**context)
        stream.enable_buffering(10)
        return stream

    def dependencies(self):
        """Refresh the page when any of those pages was changed."""

        dependencies = set()
        for title in [self.wiki.logo_page, self.wiki.menu_page]:
            if title not in self.storage:
                dependencies.add(werkzeug.url_quote(title))
        for title in [self.wiki.menu_page]:
            if title in self.storage:
                inode, size, mtime = self.storage.page_file_meta(title)
                etag = '%s/%d-%d' % (werkzeug.url_quote(title), inode, mtime)
                dependencies.add(etag)
        return dependencies

    def render_editor(self, preview=None):
        _ = self.wiki.gettext
        author = self.request.get_author()
        if self.title in self.storage:
            comment = _(u'changed')
            (rev, old_date, old_author,
                old_comment) = self.storage.page_meta(self.title)
            if old_author == author:
                comment = old_comment
        else:
            comment = _(u'uploaded')
            rev = -1
        return self.template('edit_file.html', comment=comment,
                             author=author, parent=rev)


class WikiPageSpecial(WikiPage):
    """Special pages, like recent changes, index, etc."""


class WikiPageText(WikiPage):
    """Pages of mime type text/* use this for display."""

    def content_iter(self, lines):
        yield '<pre>'
        for line in lines:
            yield werkzeug.html(line)
        yield '</pre>'

    def plain_text(self):
        """
        Get the content of the page with all markup removed, used for
        indexing.
        """

        return self.storage.page_text(self.title)

    def view_content(self, lines=None):
        """
        Read the page content from storage or preview and return iterator.
        """

        if lines is None:
            f = self.storage.open_page(self.title)
            lines = self.storage.page_lines(f)
        return self.content_iter(lines)

    def render_editor(self, preview=None):
        """Generate the HTML for the editor."""

        _ = self.wiki.gettext
        author = self.request.get_author()
        lines = []
        try:
            page_file = self.storage.open_page(self.title)
            lines = self.storage.page_lines(page_file)
            (rev, old_date, old_author,
                old_comment) = self.storage.page_meta(self.title)
            comment = _(u'modified')
            if old_author == author:
                comment = old_comment
        except error.NotFoundErr:
            comment = _(u'created')
            rev = -1
        except error.ForbiddenErr, e:
            return werkzeug.html.p(werkzeug.html(unicode(e)))
        if preview:
            lines = preview
            comment = self.request.form.get('comment', comment)
        return self.template('edit_text.html', comment=comment,
                             preview=preview,
                             author=author, parent=rev, lines=lines)

    def diff_content(self, from_text, to_text, message=u''):
        """Generate the HTML markup for a diff."""

        def infiniter(iterator):
            """Turn an iterator into an infinite one, padding it with None"""

            for i in iterator:
                yield i
            while True:
                yield None

        diff = difflib._mdiff(from_text.split('\n'), to_text.split('\n'))
        mark_re = re.compile('\0[-+^]([^\1\0]*)\1|([^\0\1])')
        yield message
        yield u'<pre class="diff">'
        for old_line, new_line, changed in diff:
            old_no, old_text = old_line
            new_no, new_text = new_line
            line_no = (new_no or old_no or 1) - 1
            if changed:
                yield u'<div class="change" id="line_%d">' % line_no
                old_iter = infiniter(mark_re.finditer(old_text))
                new_iter = infiniter(mark_re.finditer(new_text))
                old = old_iter.next()
                new = new_iter.next()
                buff = u''
                while old or new:
                    while old and old.group(1):
                        if buff:
                            yield werkzeug.escape(buff)
                            buff = u''
                        yield u'<del>%s</del>' % werkzeug.escape(old.group(1))
                        old = old_iter.next()
                    while new and new.group(1):
                        if buff:
                            yield werkzeug.escape(buff)
                            buff = u''
                        yield u'<ins>%s</ins>' % werkzeug.escape(new.group(1))
                        new = new_iter.next()
                    if new:
                        buff += new.group(2)
                    old = old_iter.next()
                    new = new_iter.next()
                if buff:
                    yield werkzeug.escape(buff)
                yield u'</div>'
            else:
                yield u'<div class="orig" id="line_%d">%s</div>' % (
                    line_no, werkzeug.escape(old_text))
        yield u'</pre>'


class WikiPageColorText(WikiPageText):
    """Text pages, but displayed colorized with pygments"""

    def view_content(self, lines=None):
        """Generate HTML for the content."""

        if lines is None:
            text = self.storage.page_text(self.title)
        else:
            text = ''.join(lines)
        return self.highlight(text, mime=self.mime)

    def highlight(self, text, mime=None, syntax=None, line_no=0):
        """Colorize the source code."""

        if pygments is None:
            yield werkzeug.html.pre(werkzeug.html(text))
            return

        formatter = pygments.formatters.HtmlFormatter()
        formatter.line_no = line_no

        def wrapper(source, unused_outfile):
            """Wrap each line of formatted output."""

            yield 0, '<div class="highlight"><pre>'
            for lineno, line in source:
                yield (lineno,
                       werkzeug.html.span(line, id_="line_%d" %
                                         formatter.line_no))
                formatter.line_no += 1
            yield 0, '</pre></div>'

        formatter.wrap = wrapper
        try:
            if mime:
                lexer = pygments.lexers.get_lexer_for_mimetype(mime)
            elif syntax:
                lexer = pygments.lexers.get_lexer_by_name(syntax)
            else:
                lexer = pygments.lexers.guess_lexer(text)
        except pygments.util.ClassNotFoundErr:
            yield werkzeug.html.pre(werkzeug.html(text))
            return
        html = pygments.highlight(text, lexer, formatter)
        yield html


class WikiPageWiki(WikiPageColorText):
    """Pages of with wiki markup use this for display."""

    def __init__(self, *args, **kw):
        super(WikiPageWiki, self).__init__(*args, **kw)
        if self.config.get_bool('wiki_words', False):
            self.parser = parser.WikiWikiParser
        else:
            self.parser = parser.WikiParser
        if self.config.get_bool('ignore_indent', False):
            try:
                del self.parser.block['indent']
            except KeyError:
                pass

    def extract_links(self, text=None):
        """Extract all links from the page."""

        if text is None:
            try:
                text = self.storage.page_text(self.title)
            except error.NotFoundErr:
                text = u''
        return self.parser.extract_links(text)

    def view_content(self, lines=None):
        if lines is None:
            f = self.storage.open_page(self.title)
            lines = self.storage.page_lines(f)
        if self.wiki.icon_page and self.wiki.icon_page in self.storage:
            icons = self.index.page_links_and_labels(self.wiki.icon_page)
            smilies = dict((emo, link) for (link, emo) in icons)
        else:
            smilies = None
        content = self.parser(lines, self.wiki_link, self.wiki_image,
                             self.highlight, self.wiki_math, smilies)
        return content

    def wiki_math(self, math):
        math_url = self.config.get('math_url',
                            'http://www.mathtran.org/cgi-bin/mathtran?tex=')
        if '%s' in math_url:
            url = math_url % werkzeug.url_quote(math)
        else:
            url = '%s%s' % (math_url, werkzeug.url_quote(math))
        label = werkzeug.escape(math, quote=True)
        return werkzeug.html.img(src=url, alt=label, class_="math")

    def dependencies(self):
        dependencies = WikiPage.dependencies(self)
        for title in [self.wiki.icon_page, self.wiki.alias_page]:
            if title in self.storage:
                inode, size, mtime = self.storage.page_file_meta(title)
                etag = '%s/%d-%d' % (werkzeug.url_quote(title), inode, mtime)
                dependencies.add(etag)
        for link in self.index.page_links(self.title):
            if link not in self.storage:
                dependencies.add(werkzeug.url_quote(link))
        return dependencies


class WikiPageFile(WikiPage):
    """Pages of all other mime types use this for display."""

    def view_content(self, lines=None):
        if self.title not in self.storage:
            raise error.NotFoundErr()
        content = ['<p>Download <a href="%s">%s</a> as <i>%s</i>.</p>' %
                   (self.request.get_download_url(self.title),
                    werkzeug.escape(self.title), self.mime)]
        return content


class WikiPageImage(WikiPageFile):
    """Pages of mime type image/* use this for display."""

    render_file = '128x128.png'

    def view_content(self, lines=None):
        if self.title not in self.storage:
            raise error.NotFoundErr()
        content = ['<img src="%s" alt="%s">'
                   % (self.request.get_url(self.title, self.wiki.render),
                      werkzeug.escape(self.title))]
        return content

    def render_mime(self):
        """Give the filename and mime type of the rendered thumbnail."""

        if not Image:
            raise NotImplementedError('No Image library available')
        return  self.render_file, 'image/png'

    def render_cache(self, cache_dir):
        """Render the thumbnail and save in the cache."""

        if not Image:
            raise NotImplementedError('No Image library available')
        page_file = self.storage.open_page(self.title)
        cache_path = os.path.join(cache_dir, self.render_file)
        cache_file = open(cache_path, 'wb')
        try:
            im = Image.open(page_file)
            im = im.convert('RGBA')
            im.thumbnail((128, 128), Image.ANTIALIAS)
            im.save(cache_file, 'PNG')
        except IOError:
            raise error.UnsupportedMediaTypeErr('Image corrupted')
        cache_file.close()
        return cache_path


class WikiPageCSV(WikiPageFile):
    """Display class for type text/csv."""

    def content_iter(self, lines=None):
        import csv
        _ = self.wiki.gettext
        # XXX Add preview support
        csv_file = self.storage.open_page(self.title)
        reader = csv.reader(csv_file)
        html_title = werkzeug.escape(self.title, quote=True)
        yield u'<table id="%s" class="csvfile">' % html_title
        try:
            for row in reader:
                yield u'<tr>%s</tr>' % (u''.join(u'<td>%s</td>' % cell
                                                 for cell in row))
        except csv.Error, e:
            yield u'</table>'
            yield werkzeug.html.p(werkzeug.html(
                _(u'Error parsing CSV file %{file}s on'
                  u'line %{line}d: %{error}s') %
                {'file': html_title, 'line': reader.line_num, 'error': e}))
        finally:
            csv_file.close()
        yield u'</table>'

    def view_content(self, lines=None):
        if self.title not in self.storage:
            raise error.NotFoundErr()
        return self.content_iter(lines)


class WikiPageRST(WikiPageText):
    """
    Display ReStructured Text.
    """

    def content_iter(self, lines):
        try:
            from docutils.core import publish_parts
        except ImportError:
            return super(WikiPageRST, self).content_iter(lines)
        text = ''.join(lines)
        SAFE_DOCUTILS = dict(file_insertion_enabled=False, raw_enabled=False)
        content = publish_parts(text, writer_name='html',
                                settings_overrides=SAFE_DOCUTILS)['html_body']
        return [content]


class WikiPageBugs(WikiPageText):
    """
    Display class for type text/x-bugs
    Parse the ISSUES file in (roughly) format used by ciss
    """

    def content_iter(self, lines):
        last_lines = []
        in_header = False
        in_bug = False
        attributes = {}
        title = None
        for line_no, line in enumerate(lines):
            if last_lines and line.startswith('----'):
                title = ''.join(last_lines)
                last_lines = []
                in_header = True
                attributes = {}
            elif in_header and ':' in line:
                attribute, value = line.split(':', 1)
                attributes[attribute.strip()] = value.strip()
            else:
                if in_header:
                    if in_bug:
                        yield '</div>'
                    #tags = [tag.strip() for tag in
                    #        attributes.get('tags', '').split()
                    #        if tag.strip()]
                    yield '<div id="line_%d">' % (line_no)
                    in_bug = True
                    if title:
                        yield werkzeug.html.h2(werkzeug.html(title))
                    if attributes:
                        yield '<dl>'
                        for attribute, value in attributes.iteritems():
                            yield werkzeug.html.dt(werkzeug.html(attribute))
                            yield werkzeug.html.dd(werkzeug.html(value))
                        yield '</dl>'
                    in_header = False
                if not line.strip():
                    if last_lines:
                        if last_lines[0][0] in ' \t':
                            yield werkzeug.html.pre(werkzeug.html(
                                            ''.join(last_lines)))
                        else:
                            yield werkzeug.html.p(werkzeug.html(
                                            ''.join(last_lines)))
                        last_lines = []
                else:
                    last_lines.append(line)
        if last_lines:
            if last_lines[0][0] in ' \t':
                yield werkzeug.html.pre(werkzeug.html(
                                ''.join(last_lines)))
            else:
                yield werkzeug.html.p(werkzeug.html(
                                ''.join(last_lines)))
        if in_bug:
            yield '</div>'

filename_map = {
    'README': (WikiPageText, 'text/plain'),
    'ISSUES': (WikiPageBugs, 'text/x-bugs'),
    'ISSUES.txt': (WikiPageBugs, 'text/x-bugs'),
    'COPYING': (WikiPageText, 'text/plain'),
    'CHANGES': (WikiPageText, 'text/plain'),
    'MANIFEST': (WikiPageText, 'text/plain'),
    'favicon.ico': (WikiPageImage, 'image/x-icon'),
}

mime_map = {
    'text': WikiPageColorText,
    'application/x-javascript': WikiPageColorText,
    'application/x-python': WikiPageColorText,
    'text/csv': WikiPageCSV,
    'text/x-rst': WikiPageRST,
    'text/x-wiki': WikiPageWiki,
    'image': WikiPageImage,
    '': WikiPageFile,
}

mimetypes.add_type('application/x-python', '.wsgi')
mimetypes.add_type('application/x-javascript', '.js')
mimetypes.add_type('text/x-rst', '.rst')
