# -*- coding: utf-8 -*-
"""
MoinMoin - SlideShow action

Usage

    Allows treatment of a wiki page as a set of slides.  Displays a
    single slide at a time, along with a navigation aid.

    A slide show page looks like this:

        general introduction or comments
        
        = Slide 1 title =
        Slide 1 contents
        
        = Slide 2 title =
        Slide 2 contents
        ...

    The SlideShow action takes a parameter 'n', which is the (1-based)
    number of the slide to display.  The display uses the large screen
    'projection' media.

    This action adds two sets of navigation aids for the slides:
     * javascript, so that mouse clicks or space bar move to the
       next page
     * a navigation footer, similar to the [[Navigation]] macro

History
    
    1.0 2005-03-25 - first version
    
    1.1 2005-03-30 - revised thanks to some comments from NirSoffer.
    
     * Parse H1 tags instead of using the [[Slide]] macro.
     * Improve navigation.
    
    1.2 2005-03-31 rewrite by NirSoffer:
     
     * fix bug with heading within pre,
     * simplifiying code
     * auto correct slide number
     * much more
     
    1.3 2005-04-04 - complete rewrite by Nir Soffer
    
     * Refactor into 2 classes, Slides and SlideShow
     * Easy to customize design, subclass and override
       few methods to customize behavior.
     * Parse page headers and set pragmas
     * Disable conflicting section-number pragma
     * Support any headers e.g #Author, #Status
     * When slide number out of range, default to first slide
     * Fixed bug when size of navigation changed
     * Complete abstract output using the formatter
     * Added required Dependencies variable
     * Simplified more efficient parsing
     * More efficient link creation
     * Removed unneeded last slide link
     * Improved navigation tooltips
     * Default slide for pages with no heading
     * Complete right to left support - both back forward icons and
       hotkeys are switched according to the user language.
     * Disable link to editor if you can't edit
     * Clear text links instead of funny text mode icons.
     * Changed hotkeys to up, right and left arrow, which make sense and
       work everywhere. As a benefit, space and numbers can be use on
       interactive slides.
  
TODO:

     * Use stylesheet specified on page with #stylesheet pi
     * Have slide show style sheets directory in htdocs - slide show should not
       use theme stylesheets but the slideshow stylesheet. You can use modern
       as your theme and display slide show using another design.
     * Old style one-slide-per-page are not displayed nicely by stylesheet 
       optimized to this action, as the heading is not rendered in a separate
       heading div.
     * Icons for edit, page, first, back and previous links.
     * SlideShow macro to view any slide on any page
     * A way to view any slide from a standard page view, maybe use
       slideshow  parser - sub class wiki parser to add links to slides
       on headings?
     * Easy to use popup menu with all available slideshow styles,
       accessible from the navigation pannel?
     * A way to hide the navigation panel, or have edit mode and
       presentation mode navigation?
     * Move script into .js file?
     * Javascript transitions??
     * Create the html head links in the action, so we can use correct
       <link rel="Start" href="..."> and like.
     * Same for stylesheet links, so we can control the stylesheet
     
    
    @copyright: 2005 Nir Soffer <nirs@freeshell.org>
    @copyright: 2005 Jim Clark
    @license: GNU GPL, see COPYING for details.
"""

import re
from StringIO import StringIO
from MoinMoin import config, wikiutil, i18n
from MoinMoin.util import web
from MoinMoin.Page import Page
from MoinMoin.formatter.text_html import Formatter

Dependencies = ['language']
        
    
class Slides(object):
    """ A collection of slides saved in a wiki page """
    
    def __init__(self, request, page):
        self._request = request
        self._page = page
        self._headers = {}
        self._slides = []
        self._parserClass = None
        self._isEditable = None

        # Parse page body        
        self._parseHeaders()
        self._parseSlides()

    #-- Quering slideshow
    
    def name(self):
        return self._page.split_title(self._request, force=1)
        
    def stylesheet(self):
        return self._headers.get('stylesheet', 'projection')

    def language(self):
        return self._headers.get('language', self._request.lang)
    
    def parserClass(self):
        if not self._parserClass:
            # Ignore the page parser and use always wiki parser
            ## name = self._headers.get('format', 'wiki')
            name = 'wiki'
            Parser = wikiutil.importPlugin(self._request.cfg, 'parser', name, 
                                           'Parser')
            if Parser is None:
                # default to plain text formatter (i.e. show the page source)
                from MoinMoin.parser.plain import Parser
            self._parserClass = Parser
        return self._parserClass
    
    def __len__(self):
        return len(self._slides)
    
    def isEmpty(self):
        return len(self) == 0
    
    def isEditable(self):
        if not self._isEditable:
            self._isEditable = self._request.user.may.write(self._page.page_name)
        return self._isEditable
    
    #-- Accssing slides
    
    def __getitem__(self, index):
        return self.titleAt(index), self.bodyAt(index)

    def titleAt(self, index):
        return self._slides[index - 1].group('text').strip()
        
    def bodyAt(self, index):
        start = self._slides[index - 1].end('heading')
        try:
            end = self._slides[index].start('heading')
            return self._body[start:end]
        except IndexError:
            return self._body[start:]        
        
    #-- Headers parsing
    
    # This generic code is not really related to slideshow and should be
    # moved into page class (replacing send_page).
    
    _headerScanner = re.compile(r'^#(?P<key>[^#]+?)\s+(?P<value>.+?)\s*$',
                                re.MULTILINE | re.UNICODE)
    
    def _parseHeaders(self):
        for match in self._headerScanner.finditer(self._page.getPageHeader()):
            key, value = match.group('key'), match.group('value')
            setter = getattr(self, '_set_' + key, None)
            if setter:
                setter(value)
            else:
                self._headers[key] = value
    
    def _set_language(self, value):
        # Ignore language the wiki does not know
        if value in i18n.wikiLanguages():
            self._headers['language'] = value
            
    def _set_pragma(self, value):    
        try:
            key, value = value.split(' ', 1)
            self._request.setPragma(key, value)
        except (ValueError, TypeError):
            pass # Ignore invalid pragmas

    #-- Slide parsing
    
    # Must skip headings within pre or code area
    # TODO: works with wiki markup only - should use the page parser
    _slideScanner = re.compile(r"""
        (?P<skip>{{{(?:.*\n)+?}}}) |                # skip this
        (?:^\s*(?P<heading>=\s(?P<text>.*)\s=)$)     # match this
        """, re.MULTILINE | re.UNICODE | re.VERBOSE)
    
    def _parseSlides(self):
        self._body = self._page.getPageText()
        for match in self._slideScanner.finditer(self._body):
            if match.start('skip') != -1:
                continue
            self._slides.append(match)
                        

class SlideShow:
    """ Show single slide from a page """

    action = 'SlideShow'
    
    #-- Public methods

    def __init__(self, request, pagename):
        # General action code, move to Action base class
        self.request = request
        self.pagename = pagename
        self.page = request.page = Page(request, pagename)
        self.formatter = request.formatter = Formatter(request)
        self.formatter.page = self.page

        # SlideShow specific init
        self.slides = Slides(self.request, self.page)
        # Disable section-numbers - a slide can have only one section.
        request.setPragma('section-numbers', 'off')
        try:
            self.current = int(request.form.get('n', [1])[0])
            self.normalizeCurrent()
        except ValueError:
            self.current = 1           
        self.maxSlideLinks = 15        
        
        # Lazy initialized values that does not change during request.
        self._urlToPageTemplate = None
        self._urlToSlideTemplate = None
        self._userMayEdit = None

    def render(self):
        """ Render current slide """
        request = self.request
        _ = request.getText
        
        request.http_headers()
        
        # Slide content uses the slideshow language
        request.setContentLanguage(self.slides.language())
        wikiutil.send_title(request, self.slides.name(), page=self.page,
                            print_mode=1, media='projection')                    
        if self.slides.isEmpty():
            request.write(self.defaultSlide())
        else:
            request.write(self.heading())
            request.write(self.content())
        
        # Navigation uses the user language
        self.request.setContentLanguage(self.request.lang)
        request.write(self.navigationScript())
        request.write(self.navigation())
        
        wikiutil.send_footer(request, self.pagename, print_mode=1)

    #-- User input handling 
    
    def normalizeCurrent(self):
        """ Default silently to first slide if value is out of range """
        if not 1 <= self.current <= len(self.slides):
            self.current = 1

    #-- Link creation

    def urlToPage(self, query=None):
        if not self._urlToPageTemplate:
            self._urlToPageTemplate = self.page.url(self.request)
        if query:
            return '%s?%s' % (self._urlToPageTemplate, query)
        return self._urlToPageTemplate
        
    def urlToSlide(self, index):
        """ Return url to slide at index """
        if not self._urlToSlideTemplate:
            self._urlToSlideTemplate = (self.urlToPage('action=%s&n=' % 
                                                        self.action))
        return self._urlToSlideTemplate + str(index)


    def linkTo(self, url, text, attributes=None):
        """ Don't call directly, use one of the linkToX methods """
        if attributes is None:
            attributes = {}
        items = [self.formatter.url(1, url, **attributes),
                 self.formatter.text(text),
                 self.formatter.url(0),]
        return ''.join(items)

    def linkToSlide(self, index, text=None, attributes=None):
        if text is None:
            text = unicode(index)
        if index == self.current:
            return self.disabledLink(text, attributes)
        return self.linkTo(self.urlToSlide(index), text, attributes)
    
    def linkToEditor(self, text, attributes=None):
        if self.slides.isEditable():
            return self.linkTo(self.urlToPage('action=edit'), text, attributes)
        return self.disabledLink(text)
        
    def linkToPage(self, text=None, attributes=None):
        if text is None:
            text = self.page.page_name
        return self.linkTo(self.urlToPage(), text, attributes)

    def disabledLink(self, text, attributes=None):
        cssClass = attributes and attributes.get('class')
        if cssClass:
            # Use code_token as a hack to get a span
            items = [self.formatter.code_token(1, cssClass),
                     self.formatter.text(text),
                     self.formatter.code_token(0, cssClass),]
            return ''.join(items)
        return self.formatter.text(text)

    #-- Adpating to language direction
    
    def adaptToLanguage(self, direction):
        # In RTL, directional items should be switched
        if i18n.getDirection(self.request.lang) == 'rtl':
            return not direction
        return direction
    
    def forwardIcon(self, forward=True):
        return [u'\u2190', u'\u2192'][self.adaptToLanguage(forward)]
    
    def backIcon(self):
        return self.forwardIcon(False)

    def forwardArrowKey(self, forward=True):
        rightArrowKey = 39
        leftArrowKey = 37
        return [leftArrowKey, rightArrowKey][self.adaptToLanguage(forward)]
        
    def backArrowKey(self):
        return self.forwardArrowKey(False)
  
    #-- Rendering default slide
  
    def defaultSlide(self):
        """ Rendered for pages without headings """
        _ = self.request.getText
        items = [self.formatter.paragraph(1),
                 self.linkToEditor(_('Edit page to add slides')),
                 self.formatter.paragraph(0),]
        return ''.join(items)

    #-- Rendering current slide

    def heading(self):
        """ Return formatted heading """
        formatter = self.formatter
        items = [self.formatter.startContent("heading"),
                 self.formatter.heading(1, 1),
                 self.formatter.text(self.slides.titleAt(self.current)),
                 self.formatter.heading(0, 1),
                 self.formatter.endContent(),]
        return ''.join(items)

    def content(self):
        """ Return formatted body """
        content = StringIO()
        self.request.redirect(content)
        Parser = self.slides.parserClass()
        Parser(self.slides.bodyAt(self.current), 
               self.request).format(self.formatter)
        self.request.redirect()
        items = [self.formatter.startContent("content"),
                 content.getvalue(),
                 self.formatter.endContent(),]
        return ''.join(items)

    #-- Rendering navigation interface
    
    def navigationScript(self):
        """ Return javascript keydown event handler """
        return """
<script type="text/javascript">
<!--// SlideShow functions
function getKey(e) {
    // Support multiple browsers stupidity
    var key;
    if (e == null) {
        // IE
        key = event.keyCode;
    } else { 
        // Standard complaient
        if (e.altKey || e.ctrlKey) {
            return null;
        }
        key = e.which;
    }
    return key;
}
function go(slide) {window.location="%(base)s" + slide;}
function onkeydown(e) {
    switch(getKey(e)) {
        case 38: go('1'); break; // up arrow
        case %(back)d: go('%(next)d'); break; // back arrow
        case %(forward)d: go('%(previous)d'); break; // forward arrow
        default: return true; // pass event to browser
    }
    // Return false to consume the event
    return false;
}
document.onkeydown = onkeydown
//-->
</script>
""" % {'base': self.urlToSlide(''), # slide number added by javascript
       'next': self.current - 1,
       'previous': self.current + 1,
       'back': self.backArrowKey(),
       'forward': self.forwardArrowKey(),}

    def slideLinksRange(self):
        """ Return range of slides to display, current centered """
        other = self.maxSlideLinks -1 # other slides except current
        start = max(1, self.current - other / 2)
        end = min(start + other, len(self.slides))
        start = max(1, end - other)
        return range(start, end + 1)
     
    def addLinksBeforeSlides(self, items):
        _ = self.request.getText
        add = items.append
        add(self.linkToEditor(_('Edit'), 
                              attributes={'title': _('Edit slide show')}))
        add(self.linkToPage(_('Quit'), 
                            attributes={'title': _('Quit slide show')}))
        add(self.linkToSlide(1, text=_('Start'),
            attributes={'title': _('Show first slide (up arrow)')}))
        add(self.linkToSlide(max(self.current - 1, 1), text=self.backIcon(), 
            attributes={'title': _('Show previous slide (back arrow)')}))
    
    def addLinksToSlides(self, items):
        _ = self.request.getText
        for i in self.slideLinksRange():
            attributes = {'title': self.slides.titleAt(i)}
            if i == self.current:
                attributes = {'class': 'current'}
            items.append(self.linkToSlide(i, attributes=attributes))

    def addLinksAfterSlides(self, items):
        _ = self.request.getText
        index = min(self.current + 1, max(len(self.slides), 1))
        items.append(self.linkToSlide(index, text=self.forwardIcon(), 
                     attributes={'title': _('Show next slide (forward arrow)')}))
    
    def navigationItems(self):
        items = []
        self.addLinksBeforeSlides(items)
        self.addLinksToSlides(items)
        self.addLinksAfterSlides(items)
        return items
        
    def navigation(self):
        """ Return navigation section """
        formatter = self.formatter
        items = [formatter.startContent("navigation"),
                 formatter.bullet_list(1),
                 ''.join([formatter.listitem(1) + i + formatter.listitem(0)
                          for i in self.navigationItems()]),
                 formatter.bullet_list(0),
                 formatter.endContent(),]
        return ''.join(items)


def execute(pagename, request):
    """ Glue to current plugin system """
    request.clock.start('slideshow')
    SlideShow(request, pagename).render()
    request.clock.stop('slideshow')
