"""
MoinMoin - SearchInPagesAndSort Macro
A line-oriented search macro over multiple pages, with sorting

Pascal Bauermeister <pascal DOT bauermeister AT hispeed DOT ch>

Original version:
* [v0.1.0] 2003/04/24 10:32:04
    
Updates:

* [v0.2.4] Pascal Mon Jul 19 23:40:54 CEST 2004
  - Comparisons to None use the 'is' and 'is not' operator (nicer)
  - Use get() for dict lookup w/ default value
  - Do not quote args and retry to compile if they are not valid regexes
  - Corrected usage samples in the comment below

* [v0.2.3] Pascal Sun Jul 18 13:45:46 CEST 2004
  Avoid endless recursion when matching page contains this macro

* [v0.2.2] Fri Jul 16 14:43:23 CEST 2004
  - Use Request.redirect(). Thanks to Craig Johnson <cpjohnson AT edcon DOT
    co DOT za>
    and Thomas Waldmann <tw DASH public AT g m x DOT d e>.
  - No more unused imports.
  - Catch only expected exceptions.

* [v0.2.1] Mon Jun  7 11:54:52 CEST 2004
  - options: links, heading
  - works now with MoinMoin Release 1.2 too
      
* [v0.1.1] Wed Oct 29 14:48:02 CET 2003
  works with MoinMoin Release 1.1 [Revision 1.173] and Python 2.3.2

-------------------------------------------------------------------------------

Usage:
  [[ SearchInPagesAndSort (PageRegex, TextRegex, SortKey [,OPTIONS] ) ]]

Search for TextRegex in pages marching PagesRegex, and sort on
  1) SortKey
  2) TextRegex
  3) found line

Options: (they are all contained in a coma-separated list of name=value pairs)
  links=0 (or 1)   Turns links to page for each hit off or on ; default is on.

  heading=Regex    After each hit, insert the string maching Regex, that
                   preceeds the hit in the source page.

  unassigned=text  Header for hits not matching the sort key. Default:
                   '[unassigned]'


-------------------------------------------------------------------------------

Sample 1:

  Given a page named 'ProjectA':
        1. Action Items
          1. [Alan] {2} to launch this task
          1. [Alan] {1} to do this urgent thing
          1. [Ben][Clara] {3} do this as background task      

        1. Deadlines
          1. 2003-03-12 <!> [Alan][Clara]: deliver 1st version of the Release X
      
  ...and a page named 'ProjectB':
        * [Denise] {2} Development of task Xyz
        * [Eric] {1} Tests of feature F
        * [Eric] (./) Tests of feature E
      
  ...using the macro in a page named 'ActionItems' like this:
        = ActionItems =
        [[SearchInPagesAndSort("Project.*","{[123]}","\[[A-Za-z_]*\]")]]
      
        = Deadlines =
        [[SearchInPagesAndSort("Project.*","<!>")]]
      
        = Completed tasks =
        [[SearchInPagesAndSort("Project.*","(\./)","\[[A-Za-z_]*\]")]]
      
  ...will give this output (note: _text_ are links):
        ActionItems
          * [Alan]
            * [Alan] {1} to do this urgent thing _ProjectA_
            * [Alan] {2} to launch this task _ProjectA_
          * [Denise]
            * [Denise] {2} Development of task Xyz _ProjectB_
          * [Ben]
            * [Ben][Clara] {3} do this as background task _ProjectA_
          * [Eric]
            * [Eric] {1} Tests of feature F _ProjectB_
          * [Clara]
            * [Ben][Clara] {3} do this as background task _ProjectA_
    
        Deadlines
          * 2003-03-12 <!> [Alan][Clara]: deliver 1st version of the Release X
            _ProjectA_
    
        Completed tasks
          * [Eric]
            * [Eric] (./) Tests of feature E _ProjectB_
      

Sample 2:

  Given a page containing:
        == Tasks for (ABC) ==
         * {1} (due:2003-12-16) [Mike] Do this
        == Tasks for (XYZ) ==
         * {2} (due:2003-12-17) [John_Doe][Mike] Do that

  ...the following macro call:
        [[SearchInPagesAndSort("MyProjectStatus","{[123]}","\[[A-Za-z_ -]*\]",         "links=0,heading=\([ab]*[0-9][0-9][0-9]\)")]]
      
  ...will produce:
        * [John_Doe]
          * {2} (due:2003-12-17) [John_Doe][Mike] Do that (XYZ) 

        * [Mike]
          * {1} (due:2003-12-16) [Mike] Do this (ABC)
          * {2} (due:2003-12-17) [John_Doe][Mike] Do that (XYZ)
"""

# Imports
import re, sys, cStringIO
from MoinMoin import config, wikiutil
from MoinMoin.Page import Page
from MoinMoin.parser.wiki import Parser


# Constants
_arg_page = r'(?P<hquote1>[\'"])(?P<hpage>.+?)(?P=hquote1)'
_arg_text = r'(?P<hquote2>[\'"])(?P<htext>.+?)(?P=hquote2)'
_arg_key  = r'(?P<hquote3>[\'"])(?P<hkey>.+?)(?P=hquote3)'
_arg_opts = r'(?P<hquote4>[\'"])(?P<hopts>.+?)(?P=hquote4)'
_args_re  = re.compile(r'^(%s( *, *%s( *, *%s( *, *%s)?)?)?)?$' %
                       (_arg_page, _arg_text, _arg_key, _arg_opts))

recursions = 0

def execute(macro, text, args_re=_args_re):

    global recursions
    if recursions: return '' ## 'SearchInPagesAndSort(%s)' % text

    recursions += 1
    try:     return _execute(macro, text, args_re)
    finally: recursions -=1

        
# The "raison d'etre" of this module
def _execute(macro, text, args_re=_args_re):

    # parse and check arguments
    args = args_re.match(text)
    if text is None or not args:
        return ( '<p><strong class="error">Invalid SearchInPages arguments' +
                 ' "%s"!</strong></p>' ) % text

    text = args.group('htext')
    pages = args.group('hpage')
    key = args.group('hkey')
    opts = args.group('hopts')
     
    # get a list of pages matching the PageRegex
    pages_re = re.compile(pages, re.IGNORECASE)
    all_pages = wikiutil.getPageList(config.text_dir)
    hits = filter(pages_re.search, all_pages)
    hits.sort()
 
    if len(hits) == 0:
        return (
            '<p><strong class="error">'
            'No page matching "%s"!</strong></p>' % pages )
 
    # parse options
    options = {}
    if opts is not None:
        for element in opts.split(','):
            pair = element.split('=')
            options[ pair[0] ] = pair[1]
 
    # All these try except could be reduced to a simple get:
    opt_links = eval(options.get('links', '1'))
    opt_heading = options.get('heading', None)
    opt_unassigned_text = options.get('unassigned', "[unassigned]")

    # compile all regex
    text_re = re.compile(text, re.IGNORECASE)
 
    if key is not None:
        key_re = re.compile(key, re.IGNORECASE)
 
    if opt_heading is not None:
        heading_re = re.compile(opt_heading, re.IGNORECASE)

    # we will collect matching lines in each matching page
    all_matches = []

    # treat each found page
    for page_name in hits:
        body = Page(page_name).get_raw_body()
        pos = 0
        last_start = -1
        last_end = -1
        heading_text = ""
        while 1:
            keep_line = 1
            
            # search text
            match = text_re.search(body, pos)
            if not match: break

            # text is found; now search for heading
            if opt_heading is not None:
                heading_pos = pos
                heading_match = True
                # keep the nearest heading to the found text
                while heading_match:
                    heading_match = heading_re.search(body, heading_pos)
                    if heading_match and heading_match.start() < match.start():
                        heading_text = heading_match.group(0)
                        heading_pos = heading_match.end()
                    else: heading_match = False

            # point to found text
            pos = match.end()+1

            # cut before start of line
            start_pos = match.start()
            rev = 0
            while body[start_pos] != '\n' and start_pos:
                start_pos = start_pos - 1
                rev = 1
            if rev:
                start_pos = start_pos + 1

            # cut at end of line
            end_pos = body.find("\n", match.end())

            # extract line
            line = body[start_pos:end_pos].strip()

            # store this record if it differs from previous one
            if start_pos == last_start or end_pos == last_end: keep_line = 0

            # store this record if it it is not a comment
            elif line.startswith("##"): keep_line = 0

            # remove possible list item leaders
            if keep_line:
                for heading in ["*", "1.", "a.", "A.", "i.", "I."]:
                    if line.startswith(heading):
                        line = line.replace(heading, "", 1)
                line = line.strip()
                if len(line)==0: keep_line = 0

            # handle this record
            if keep_line:

                # find the sort key
                nbmatches = 0
                keypos = 0
                found = 0
                while 1:
                    if key is None:
                        keyval = ""
                    else:
                        keymatch = key_re.search(line, keypos)
                        if keymatch:
                            keyval = line[keymatch.start():keymatch.end()]
                            keypos = keymatch.end()
                            nbmatches = nbmatches + 1
                            found = 1
                        else:
                            if nbmatches>0: break
                            keyval = opt_unassigned_text

                    # store info
                    item = []
                    item.append(keyval)                          # key text
                    item.append(body[match.start():match.end()]) # search text
                    item.append(line)                            # line text
                    item.append(page_name)                       # page name
                    item.append(heading_text)                    # heading
                    all_matches.append(item)
                    if found == 0: break

                last_start = start_pos
                last_end = end_pos

        # sort and format records
        bullet_list_open = macro.formatter.bullet_list(1)
        bullet_list_close = macro.formatter.bullet_list(0)
        listitem_open = macro.formatter.listitem(1)
        listitem_close = macro.formatter.listitem(0)

        all_matches.sort()
        result = ""
        result = result+"\n" + bullet_list_open
        keyval = ""
        head_count = 0

        # treat records for output
        for item in all_matches:
            text = item[2]
            pagename = item[3]
            heading_text = item[4]

            # parse the text (in wiki source format) and make HTML,
            # after diverting sys.stdout to a string
            str_out = cStringIO.StringIO()   # create str to collect output
            macro.request.redirect(str_out)  # divert output to that string
            # parse this line (this will also execute macros !) :
            Parser(text, macro.request).format(macro.formatter)
            macro.request.redirect()         # restore output
            text_fmtted = str_out.getvalue() # get what was generated
            text_fmtted = text_fmtted.strip(' ') # preserve newlines

            # empty text => drop this item
            if len(text_fmtted)==0: continue

            # insert heading (only if not yet done)
            if key is not None and item[0] != keyval:
                # this is a new heading
                keyval = item[0]
                if head_count:
                    result = result+"\n    " + bullet_list_close
                    result = result+"\n  " + listitem_close
                head_count = head_count +1
                result = result+"\n  " + listitem_open
                result = result+ keyval
                result = result+"\n    " + bullet_list_open

            # correct text the format (berk)
            if text_fmtted.startswith("\n<p>"):
                 text_fmtted = text_fmtted[4:]
            if text_fmtted.endswith("</p>\n"):
                text_fmtted = text_fmtted[:-5]
                text_trailer = "\n</p>\n"
            else: text_trailer = ""
                
            # insert text
            result = result+"\n      " + listitem_open
            result = result + text_fmtted
            if opt_links:
                result = result + "&nbsp;&nbsp;&nbsp;<font size=-1>"
                try: # try MoinMoin 1.1 API
                    link_text = wikiutil.link_tag(pagename)
                except TypeError: # try MoinMoin 1.2 API
                    link_text = wikiutil.link_tag(macro.request, pagename)
                result = result + link_text
                result = result + "</font>"
            if opt_heading is not None:
                result = result + "&nbsp;&nbsp;&nbsp;<font size=-1>"
                result = result + heading_text
                result = result + "</font>"                
            result = result + text_trailer + "\n      " + listitem_close

        # all items done, close (hopefully) gracefully
        if head_count:
            result = result+"\n      " + listitem_close
            result = result+"\n    " + bullet_list_close
        if key is not None:
            result = result+"\n  " + listitem_close
        result = result+"\n" + bullet_list_close

    # done
    return result
