# -*- coding: utf-8 -*-
"""
MoinMoin - GlobalSearchAndReplace action

This action allows you to select a set of pages by a search pattern
and do a search and replace on each page from the set in one go.
It offers a preview feature that lets you review what would edited
before doing the actual replace.

To hide misleading links in preview add the following to common.css of your
theme:

.global-replace-diff .diff-title {
    visibility: collapse;
}

Adapted to moin-1.9.8 and extended by David Linke.
Based on version 1.0 of "SearchAndReplaceMultiplePages.py" downloaded
from http://www.egenix.com/library/moinmoin/

Written by Marc-Andre Lemburg <mal@egenix.com>,
Based on the RenamePage action by Jürgen Hermann <jh@web.de>

@copyright: 2015-2018, David Linke, MoinMoin:DavidLinke, https://github.com/dalito
@copyright: 2008, eGenix.com Software, Skills and Services GmbH <info@egenix.com>
@license: GNU GPL 2.0, see COPYING for details.
"""

import re

from MoinMoin import wikiutil
from MoinMoin.PageEditor import PageEditor
from MoinMoin.action import ActionBase
from MoinMoin import log
from MoinMoin.util import diff_html

__version__ = '2.0'

# Should only the superuser be allowed to use this action ?
SUPERUSER_ONLY = False

logging = log.getLogger(__name__)


class GlobalSearchAndReplace(ActionBase):
    """Search and replace on multiple pages action

    Note: the action name is the class name
    """

    def __init__(self, pagename, request):
        ActionBase.__init__(self, pagename, request)
        self.page = PageEditor(self.request, pagename)
        self.orig_page = pagename
        self.feedback = u''
        self.no_refused_access = 0

    def is_allowed(self, pagename=None):
        """Check if user is allowed to access page"""
        if pagename is None:
            pagename = self.pagename
        may = self.request.user.may
        return may.write(pagename)

    def render(self):
        """
        Render action

        This action returns a wiki page with optional message, or
        redirect to original page.
        """
        _ = self.request.getText
        form = self.request.form

        if 'cancel' in form:
            # User cancelled
            return self.page.send_page()

        # Validate user rights and page state. If we get error here, we
        # return an error message, without the rename form.
        error = None
        if self.is_excluded():
            error = _('Action %(actionname)s is excluded in this wiki!') % {
                'actionname': self.actionname}
        elif SUPERUSER_ONLY and not self.request.user.isSuperUser():
            error = _(u'Only superusers are allowed to use this action.')
        elif not self.page.exists():
            error = _(u'This page is already deleted or was never created!')
        if error:
            # Send page with an error message
            self.request.theme.add_msg(msg=error)
            return self.page.send_page()

        if not form.get('searchtext'):
            self.request.theme.add_msg(_(u'Please fill in a search text and '
                                         '(optional) a replacement text!'),
                                       'info')
        # Run search & replace on the pages
        elif ('replace' in form or 'preview' in form) and 'ticket' in form:
            self.replace()

#        logging.info('form in search and replace: %s' % repr(form))

        # Show the form (with error or feedback information)
        self.request.theme.add_msg(self.makeform(), 'info')
        return self.page.send_page()

    def replace(self):
        """Replace text on pages matching a regexp"""
        _ = self.request.getText
        form = self.request.form
        formatter = self.request.formatter
        self.feedback = u''
        self.no_refused_access = 0

        # Require a valid ticket. Make outside attacks harder by
        # requiring two full HTTP transactions
        if not wikiutil.checkTicket(self.request, form['ticket']):
            self.error = _(u'Please use the interactive user interface to '
                           'do search & replace on multiple pages!')
            return

        # Get new name from form and normalize.
        comment = form.get('comment', [u''])
        comment = wikiutil.clean_input(comment)

        pagename = form.get('pagename')
        pagename = wikiutil.normalize_pagename(pagename, self.cfg)

        searchtext = form.get('searchtext')
        replacetext = form.get('replacetext', '')

        # test if regular expressions are OK
        try:
            re.compile(pagename)
        except Exception as err:
            self.error = _(u'Error in page name pattern: %s' % err)
            return
        try:
            re.compile(searchtext)
        except Exception as err:
            self.error = _(u'Error in search text pattern: %s' % err)
            return

        # Get list of all pages
        pages = self.request.rootpage.getPageList(user='', exists='')
        # Check which page names match
        pages_to_update = [p for p in pages if re.search(pagename, p)]

        self.feedback += formatter.rule()
        self.feedback += formatter.paragraph(1)
        self.feedback += _('Found %i matching pagename(s).' %
                           len(pages_to_update))
        self.feedback += formatter.paragraph(0)

        if not pages_to_update:
            return

        self.feedback += formatter.rule()
        if 'preview' not in self.request.form:
            self.feedback += formatter.paragraph(1)
            self.feedback += _('Updating pages...')
            self.feedback += formatter.paragraph(0)
            self.feedback += formatter.bullet_list(1)

        # replace on all matching pages one by one
        for page in pages_to_update:
            self.replace_on_page(page, searchtext, replacetext, comment)

        if 'preview' not in self.request.form:
            self.feedback += formatter.bullet_list(0)

        if self.no_refused_access:
            self.feedback += formatter.paragraph(1)
            self.feedback += formatter.smiley('<!>')
            self.feedback += _(u' %i pages skipped because of access '
                               'restrictions.' % self.no_refused_access)
            self.feedback += formatter.paragraph(0)

    def replace_on_page(self, pagename, searchtext, replacetext, comment):
        """Open page for editing and do search and replace in raw code
        """
        _ = self.request.getText
        formatter = self.request.formatter

        # Check permissions. Don't reveal the page name, just count.
        if not self.is_allowed(pagename):
            self.no_refused_access += 1
            return

        # Open page
        page = PageEditor(self.request, pagename)

        # Get page text
        oldtext = page.get_raw_body()

        if (self.request.form.get('ignore_pi', False)
                and oldtext.startswith('#')):
            # do not replace in processing instruction (pi) / header
            header, content = [], []
            in_header = True
            for line in oldtext.split('\n'):
                if line.startswith('#') and in_header:
                    header.append(line)
                else:
                    content.append(line)
                    in_header = False
            # Apply replacements
            try:
                newcontent = re.sub(searchtext, replacetext, u'\n'.join(content))
            except Exception as err:
                self.error = _(u'Error in replace text pattern: %s' % err)
                return
            newtext = u'%s\n%s' % (u'\n'.join(header), newcontent)
        else:
            # Apply replacements
            try:
                newtext = re.sub(searchtext, replacetext, oldtext)
            except Exception as err:
                self.error = _(u'Error in replace text pattern: %s' % err)
                return

        # Either save page text or show preview of edit, if the text changed.
        if newtext.strip() != oldtext.strip():  # avoid hit on changed tail
            if 'preview' in self.request.form:
                self.feedback += formatter.paragraph(1)
                self.feedback += _('Would edit page <b>%s</b> as follows:' %
                                   wikiutil.escape(pagename))
                self.feedback += formatter.paragraph(0)
                self.feedback += u'<div class="global-replace-diff">'
                self.feedback += formatter.rawHTML(
                    diff_html.diff(self.request, oldtext, newtext)
                )
                self.feedback += u'</div>\n'
            else:
                try:
                    page.saveText(newtext, 0, comment=comment)
                    self.feedback += formatter.listitem(1)
                    self.feedback += formatter.smiley('(./)')
                    self.feedback += _('%s' % wikiutil.escape(pagename))
                    self.feedback += formatter.listitem(0)
                except PageEditor.SaveError, reason:
                    self.error += _(u'Cannot save page "%s": %s ! <br>' %
                                    (wikiutil.escape(pagename), reason))
                    return

    def makeform(self):
        """Display a search&replace page form

        The form also provides error feedback in case there was an
        error during the replace.
        """
        from MoinMoin.widget.dialog import Dialog
        _ = self.request.getText

        error = ''
        if self.error:
            error = u'<p class="error">%s</p>\n' % self.error

        form = self.request.form
        namespace = {
            'error': error,
            'feedback': self.feedback,
            'action': self.__class__.__name__,
            'ticket': wikiutil.createTicket(self.request),
            'pagename': form.get('pagename', self.orig_page),
            'ignore_pi': 'checked' if form.get('ignore_pi', False) else '',
            'searchtext': form.get('searchtext', ''),
            'replacetext': form.get('replacetext', ''),
            'comment': form.get('comment', ''),
            'replace': _(u'Search & Replace'),
            'preview': _(u'Preview'),
            'cancel': _(u'Cancel'),
            'pagename_label': _(u"Page name pattern"),
            'ignore_pi_label': _(u"Skip processing instructions in header"),
            'searchtext_label': _(u"Search text pattern"),
            'replacetext_label': _(u"Replace text pattern"),
            'comment_label': _(u"Optional reason for the replacement"),
            'note': _(u"Note: Page name and search pattern may use regular "
                      "expression syntax (Python re-module syntax). "
                      "Groups can be referenced in the replace pattern "
                      "using \\1, \\2, etc."),
        }
        form = """
%(error)s
<form method="post" action="">
<input type="hidden" name="action" value="%(action)s">
<input type="hidden" name="ticket" value="%(ticket)s">
<table>
    <tr>
        <td class="label"><label>%(pagename_label)s</label></td>
        <td class="content">
            <input type="text" name="pagename" size="60" value="%(pagename)s">
        </td>
    </tr>
    <tr>
        <td class="label"><label>%(ignore_pi_label)s</label></td>
        <td class="content">
            <input type="checkbox" name="ignore_pi" value="1" %(ignore_pi)s>
        </td>
    </tr>
    <tr>
        <td class="label"><label>%(searchtext_label)s</label></td>
        <td class="content">
            <input type="text" name="searchtext" size="60" value="%(searchtext)s">
        </td>
    </tr>
    <tr>
        <td class="label"><label>%(replacetext_label)s</label></td>
        <td class="content">
            <input type="text" name="replacetext" size="60" value="%(replacetext)s">
        </td>
    </tr>
    <tr>
        <td class="label"><label>%(comment_label)s</label></td>
        <td class="content">
            <input type="text" name="comment" size="60" maxlength="80" value="%(comment)s">
        </td>
    </tr>
    <tr>
        <td></td>
        <td class="buttons">
            <input type="submit" name="preview" value="%(preview)s">
            <input type="submit" name="replace" value="%(replace)s">
            <input type="submit" name="cancel" value="%(cancel)s">
        </td>
    </tr>
</table>
<p style="font-weight: normal">%(note)s</p>
<p>%(feedback)s</p>
</form>
""" % namespace

        return Dialog(self.request, content=form)


def execute(pagename, request):
    """Glue code for actions"""
    GlobalSearchAndReplace(pagename, request).render()
