from trac.core import *
from trac.wiki.macros import WikiMacroBase
from trac.wiki.formatter import wiki_to_html
from trac.mimeview.api import IHTMLPreviewAnnotator, Mimeview
from os import linesep

class IncludeSourceMacro(WikiMacroBase):
    """Includes a source file from the repository into the Wiki. 

    There is one required parameter, which is the path to the 
    file to include. This should be the repository path, not a 
    full URL. 

    Optional named parameters are:
     * ''start '' The first line of the file to include. Defaults to the 
     beginning of the file. Otherwise should be a numeric value. 

     Note that files start with line 1, not line 0. 
     * ''end'' The last line of the file to include. Defaults to the end
     of the file. 

     Note that both 'start' and 'end' are used for array slicing the 
     lines of the file, so if (for example) you want the last 20 lines 
     of the file, you can use start=-20 and leave end blank. 
     * ''rev'' Which revision to include. This defaults to HEAD if 
     not supplied. Otherwise this should be a valid numeric revision 
     number in your version control repository. 
     * ''mimetype'' Which mimetype to use to determine syntax highlighting. 
     If not supplied, this is determined by the file extension (which
     is normally what you want)

    Examples:
    {{{
        # include entire file
        [[IncludeSource(trunk/proj/file.py)]]

        # includes line 20-50 inclusive
        [[IncludeSource(trunk/proj/file.py, start=20, end=50)]]

        # includes last 30 lines of file at revision 1200
        [[IncludeSource(trunk/proj/file.py, start=-30, rev=1200)]]

        # includes last 30 lines of file at revision 1200
        [[IncludeSource(trunk/proj/file.py, start=-30, rev=1200)]]

        # include entire file but formatted plain
        [[IncludeSource(trunk/proj/file.py, mimetype=text/plain)]]

    }}}

    See TracLinks, TracSyntaxColoring and trac/mimeview/api.py

    TODO
    {{{
    * Fix non-localized strings

    * Fix proper encoding of output

    * Implement some sort of caching (especially in cases where the 
    revision is known and we know that the contents won't change). 

    * Fix issue with calling IncludeSource more than once on a single
    page. This causes some error in the C bindings for Subversion
    so it will cause a server error instead of producing a log message.

    See the comments in IncludeSource.py for more information about
    where this occurs. 

    * Allow multiple chunks from the file in one call. You can do this
    with the existing code, but it will pull the entire file out of
    version control and trim it for each chunk, so this could be 
    optimized a bit. 
    }}}
    """    

    def render_macro(self, req, name, args):
        self.log.warning('Begin render_macro for req: ' + repr(args))

        largs, kwargs = parse_args(args)
        repos = self.env.get_repository()
        
        self.log.warning('have repo')
        if len(largs) == 0:
            # TODO - don't hardcode this in English
            raise Exception("File name to include is required parameter!")

        try:
            file_name = largs[0]
            
            rev = kwargs.get('rev', None)

            # This dies in the C code when we have two instances of the
            # macro on one request.  It dies in trac.versioncontrol.svn_fs
            # in the SubversionNode __init__ method on the line
            # self.root = fs.revision_root(self.fs_ptr, rev, self.pool())
            # 
            # This could be from a bad fs_ptr or bad pool, but it wasn't 
            # obvious during some initial python level debugging as to what 
            # the cause is
            src = repos.get_node(file_name, rev).get_content().read()

            # We'd like to just use Mimeview directly here, but there's no
            # support for the partial output that we want to support here
            mv = IncludeSourceMimeview(self.env)
            mv.file_name = file_name
            mv.rev = rev

            start, end = kwargs.get('start', None), kwargs.get('end', None)
            if start or end:
                src, start, end = self._handle_partial(src, start, end)
                mv.startline = start

            mimetype = kwargs.get('mimetype', None)
            url = None  # render method doesn't seem to use this
            src = mv.render(req, mimetype, src, file_name, url, ['givenlineno'])

        finally:
            repos.close()
            repos = None
        return src

    def _handle_partial(self, src, start, end):
        # we want to only show a certain number of lines, so we
        # break the source into lines and set our numbers for 1-based
        # line numbering. 
        #
        # Note that there are some good performance enhancements that
        # could be done by 
        # a) reading lines out of Subversion, using svn_stream_readline
        #    instead of svn_stream_read when fetching data
        # b) have the render method accept a list/iterator of lines
        #    instead of only accepting a string (which it then splits)
        lines = src.split(linesep)
        linecount = len(lines)

        if start:
            start = int(start)
            if start >= 0:
                start -= 1
        if end:
            end = int(end)
        src = lines[start:end]

        # calculate actual startline for display purposes
        if not start:
            start = 1
        elif start < 0:
            start = linecount + start + 1
        else:
            start += 1

        return linesep.join(src), start, end

class GivenLineNumberAnnotator(Component):
    """Text annotator that adds a column with given line numbers."""
    implements(IHTMLPreviewAnnotator)

    file_name = ''
    rev = None

    # ITextAnnotator methods

    def get_annotation_type(self):
        return 'givenlineno', 'Line', 'Line numbers'

    def annotate_line(self, number, content):
        if self.file_name.startswith('/'):
            self.file_name = self.file_name[1:]

        rev = self.rev and '@' + str(self.rev) or ''

        return '<th id="L%s"><a href="../browser/%s%s#L%s">%s</a></th>' % \
            (number, self.file_name, rev, number, number)

class IncludeSourceMimeview(Mimeview):

    startline = 1
    file_name = ''
    rev = None

    def _annotate(self, lines, annotations):
        import re
        from StringIO import StringIO
        from trac.mimeview.api import _html_splitlines

        buf = StringIO()
        buf.write('<table class="code"><thead><tr>')
        annotators = []
        for annotator in self.annotators:
            atype, alabel, adesc = annotator.get_annotation_type()
            if atype in annotations:

                # kludge to get lineno CSS class
                if atype == 'givenlineno':
                    atype = 'lineno'    

                buf.write('<th class="%s">%s</th>' % (atype, alabel))
                annotators.append(annotator)
        buf.write('<th class="content">&nbsp; </th>')
        buf.write('</tr></thead><tbody>')

        space_re = re.compile('(?P<spaces> (?: +))|'
                              '^(?P<tag><\w+.*?>)?( )')
        def htmlify(match):
            m = match.group('spaces')
            if m:
                div, mod = divmod(len(m), 2)
                return div * '&nbsp; ' + mod * '&nbsp;'
            return (match.group('tag') or '') + '&nbsp;'

        num = -1
        for num, line in enumerate(_html_splitlines(lines)):
            cells = []
            linenum = num + self.startline
            for annotator in annotators:

                # kludgey way of passing data to annotator
                if annotator.get_annotation_type()[0] == 'givenlineno':
                    annotator.file_name = self.file_name
                    annotator.rev = self.rev

                cells.append(annotator.annotate_line(linenum, line))
            cells.append('<td>%s</td>\n' % space_re.sub(htmlify, line))
            buf.write('<tr>' + '\n'.join(cells) + '</tr>')
        else:
            if num < 0:
                return ''
        buf.write('</tbody></table>')
        return buf.getvalue()



# see ticket 2983 - this parse_args is added to Trac there
# so we'll define it here if someone is on a version without that
# http://trac.edgewall.org/ticket/2983
try:
    from trac.wiki.api import parse_args
except:
    import re
    def parse_args(args):
        """Utility for parsing macro "content" and splitting them into arguments.

        The content is split along commas, unless they are escaped with a
        backquote (like this: \,).
        Named arguments a la Python are supported, and keys must be  valid python
        identifiers immediately followed by the "=" sign.

        >>> parse_args('')
        ([], {})
        >>> parse_args('Some text')
        (['Some text'], {})
        >>> parse_args('Some text, mode= 3, some other arg\, with a comma.')
        (['Some text', ' some other arg, with a comma.'], {'mode': ' 3'})
        
        """    
        largs, kwargs = [], {}
        if args:
            for arg in re.split(r'(?<!\\),', args):
                arg = arg.replace(r'\,', ',')
                m = re.match(r'\s*[a-zA-Z_]\w+=', arg)
                if m:
                    kwargs[arg[:m.end()-1].lstrip()] = arg[m.end():]
                else:
                    largs.append(arg)
        return largs, kwargs



