#!/usr/bin/python
#
# Python-bases PmWiki Editor (Pywe)
# =================================
#
# Copyright and Legalese
# ----------------------
#
# Copyright (c) 2006 Benjamin C. Wilson. All Rights Reserved.  
#
# This software referred to as Pywe ("Software") was developed by Benjamin C.
# Wilson, and may include voluntary contributions. For more information on this
# Software, visit its web site at http://www.dausha.net/Pywe.
#
# Installation, use, reproduction, and display of this Software, without
# modification, are not permitted.  You may not republish without The express,
# written concent of the copyright holder.  Permission to publish is granted to
# pmwiki.org, but no permission to modify is granted. Any exercise of rights
# under this license by you is subject to the following conditions.
#    1. Redistribution of this software is not permitted.
#    2. Any user documentation must include the copyright statement.
#    3. This copyright statement, disclaimers and limitations must remain with 
#       the software.
#    4. Consent to the following disclaimer and limitations of liability.
#
# Disclaimer
# ~~~~~~~~~~
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY, OF SATISFACTORY QUALITY, AND FITNESS
# FOR A PARTICULAR PURPOSE OR USE ARE DISCLAIMED. THE COPYRIGHT HOLDER AND
# CONTRIBUTORS MAKE NO REPRESENTATION THAT THE SOFTWARE, MODIFICATIONS,
# ENHANCEMENTS OR DERIVATIVE WORKS THEREOF, WILL NOT INFRINGE ANY PATENT,
# COPYRIGHT, TRADEMARK, TRADE SECRET OR OTHER PROPRIETARY RIGHT. 
# 
# Limitations of Liability
# ~~~~~~~~~~~~~~~~~~~~~~~~
#
# THE COPYRIGHT HOLDER AND CONTRIBUTORS SHALL HAVE NO LIABILITY TO LICENSEE OR
# OTHER PERSONS FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, CONSEQUENTIAL,
# EXEMPLARY, OR PUNITIVE DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT
# LIMITATION, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, LOSS OF USE, DATA OR
# PROFITS, OR BUSINESS INTERRUPTION, HOWEVER CAUSED AND ON ANY THEORY OF
# CONTRACT, WARRANTY, TORT (INCLUDING NEGLIGENCE), PRODUCT LIABILITY OR
# OTHERWISE, ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 
# 
# License Revisions
# ~~~~~~~~~~~~~~~~~
#
# The Software Copyright Holder may publish revised and/or new versions of this
# this License from time to time. Each version will be given a distinguishing
# version number. Once a version of Software has been published under a
# particular version of this License, you may continue to use it under the
# terms of that license version. You may also choose to use such Software under
# the terms of any subsequent version of this License published by the
# Copyright Holder. No one other than the Copyright Holder has the right to
# modify the terms of this License.
#
# If you have any questions, comments or concerns, please contact the author at
# <ameen@dausha.net>
#
# License Version: v1.0 - May 20, 2006.
#
# Software version: v1.0 - Public Release May 20, 2006
# Software version: v1.1 - Public Release May 30, 2006
__version__ = "v1.1 - May 30, 2006"

import ConfigParser
import optparse
import getpass
import logging
import os
import re
import string
import signal
import sys
import tempfile
import time
import urllib
import urlparse

#===================================
# PmWiki Configuration Class
#-----------------------------------
class PmwikiConfig:
    def __init__(self, dom='DEFAULT', url=None):

        if dom is None: dom = 'DEFAULT'
        dom = dom.upper()

        setattr(self,'_config',ConfigParser.ConfigParser())
        setattr(self,'dom', dom) 
        setattr(self,'file', os.path.expanduser('~/.pywerc'))

        c = self._config
        c.read(self.file)

        config_keys = [
            'author',
            'browser',
            'editor',
            'enablepathinfo',
            'page',
            'password',
            'url',
        ]
        defaults = c.defaults()
        for option in config_keys:
            val = ''
            if c.has_section(dom) and c.has_option(dom, option):
                val = c.get(dom,option)
            elif defaults.has_key(option):
                val = defaults[option]

            if val == 'yes': val = 1
            elif val == 'no' : val = 0
            setattr(self,option,val)

        if url is not None: self.url = url
# class PmwikiClass


#===================================
# PmWiki Page Class
#-----------------------------------
class PmwikiPage :
    def __init__(self, url, page, epi):
        setattr(self, 'enablepathinfo', epi)
        setattr(self, 'page', page)
        setattr(self, 'passwd', None)
        setattr(self, 'text', None)
        setattr(self, 'url', url)

    def _fmtPage(self, action):
        page = self.page
        url = self.url
        if self.enablepathinfo:
            if url[-1] != '/': url += '/'
            return urlparse.urljoin(url, page) + '?action=' + action
        else:
            url = re.sub('/$','',url)
            page = re.sub('/','.', page)
            return '%s?n=%s&action=%s' % (url, page, action)

    def readpage(self, author='', passwd=None):
        """Retrieves the PmWiki source from the web site"""
        source = self._fmtPage('source')

        params = urllib.urlencode({'authid' : author, 'authpw' : passwd})
        try:                                
            fh  = urllib.urlopen(source, params)
        except IOError:
            say_error("Could not access: " + self.url)

        return fh.read()

    def writepage(self, text, src, author='', passwd=None):
        """Writes the page back to the web site"""
        page = self.page
        url = self.url
        if  src == text :
            say_info("Original and revision are the same. Not uploading.")
        else :
            text = self.editMark(text)
            url = self._fmtPage('edit')
            opts = {'action' : 'edit', 'authid' : author, 'author' : author,
             'authpw' : passwd, 'n' : page, 'post' : 1, 'text' : text
            }
            if passwd is None:
                del(opts['authid'])
                del(opts['authpw'])

            params = urllib.urlencode(opts)
            try:                                
                fh  = urllib.urlopen(url, params)
            except IOError, e:
                fn = '%s-%s' %(page, time.time())
                f = open(fn,'w+')
                f.write(text)
                f.close()
                msg = "Failed Write to web site, check for '%s' (%s)" % (fn, e)
                say_error(msg)

    def editpage(self, editor, text=None):
        """Sends page to your favorite editor"""
        if text is None: text = self.readpage()
        if len(text) == 0: text = "%s is a new page." % self.page

        f = tempfile.NamedTemporaryFile(
                'r+w', -1, '.pmwiki', 'pywe-', tempfile.tempdir
            )
        say_info("Using Tempfile: " + f.name)
        try:                                
            f.write(text)
            f.flush()
            f.seek(0)
        except IOError:
            say_error("Could not write to the temporary file")
            sys.exit(0)

        cmd = editor + ' ' + f.name
        os.system(cmd)
        output = f.read()
        f.close()
        return output

    def editMark(self, t) :
        m = "\n(:comment This page has been edited using Pywe:)"
        m_RE = re.compile("\n+\(:comment This page has been edited using Pywe:\)")
        t = m_RE.sub('', t)
        t += m
        return t

# class PmWikiPage

def findApp(f,m="Could not find application: "):
    """If we don't have the application at first, we go looking."""
    if os.path.isfile(f): return f
    dirs = sys.path
    dirs.insert(0,os.environ['HOME'])
    for d in dirs:
      c = os.path.join(d, f)
      if os.path.isfile(c): return c
    say_error(m+f)

def say_info(msg):
    print msg
    logging.info(msg)

def say_error(msg):
    """Prints errors to stderr, logs the error and quits."""
    sys.stderr.write(msg)
    logging.error(msg)
    sys.exit(0)

def shorthelp(option, opt_str, value, p):
    """Prints terse help message in technicolor"""
    esc_seq = "\x1b["
    codes = {}
    codes["reset"] = esc_seq + "39;49;00m"
    codes["bold"] = esc_seq + "01m"
    codes["green"] = esc_seq + "32;01m"
    codes["turq"] = esc_seq + "36;01m"
    def green(txt): return "%s%s%s" % (codes["green"],txt,codes["reset"])
    def bold(txt): return "%s%s%s" % (codes["bold"],txt,codes["reset"])
    def turq(txt): return "%s%s%s" % (codes["turq"],txt,codes["reset"])

    opt = {}
    def getem(d,k):
        ret = ''
        try:
            if d.has_key(k) and d[k] is not None: 
                if isinstance(d[k], list) and len(d[k]): ret = d[k][0]
                elif not len(d[k]): ret = ''
                else: ret = d[k]
        except:
            print d
        return ret

    def help_msg(d,k,m=1):
        s = getem(d[k],'short')
        l = getem(d[k],'long')
        dst = getem(d[k],'dest')
        h = getem(d[k],'help')
        d = dst.upper()
        if len(dst) and len(l): dl = "="+dst.upper()
        else: dl = ''

        if len(s):
            if len(d): d = ' '+d
            out = "%s%s, %s%s" % (s,d,l,dl)
            pad = 36
            clor = "  %s%s, %s%s" % (green(s),turq(d), green(l),turq(dl))
        else:
            out = "  %s%s" % (l,dl)
            pad = 38
            clor = "  %s%s" % (green(l),turq(dl))
        return clor + (' '* (pad - len(out))) + h

    for o in p.option_list:
        e = "%s" % o # Options convert to strings when asked.
        e = e.split('/')[0]
        e = e[e.rindex('-')+1:]
        opt[e] = { 
            'help': o.help, 
            'short': o._short_opts, 
            'long': o._long_opts, 
            'dest': o.dest, 
        }
        if not len(opt[e]['short']) : opt[e]['short'] = None

    prog = turq('pywe')
    opts = "[ %s ]" % green('options')
    acts = "[ %s ]" % green('action')

    print bold("\nUsage:")
    print '  '+' '.join([prog, opts, acts, turq('dom:Group.Pagename' )])
    print '  '+' '.join(
      [prog, opts, acts, turq('http://www.example.org/pmwiki.php/Main/Sandbox')]
    )
    print '  '+' '.join([prog, turq('--help')])
    print bold("Options:")

    #-----------------------------------
    # The DRY principle in motion. All the work above allows adding an option
    # to Optparse that will dynamically print a short message about itself when
    # asked.
    keys = opt.keys()
    keys.sort()
    for k in keys: print help_msg(opt,k)

    print
    sys.exit(0)

def checkApp(o, a, m):
    if not o: return 0
    msg = {
      'noeditor': "You must configure an editor to edit a page.",
      'nobrowser': "You must configure a browser to us this option."
    }
    check = a.split(' ',1)[0]
    
    a = findApp(check)
    if not os.path.isfile(check): say_error(msg[m])
    return True

#===================================
# Main:
#-----------------------------------
def main(argv=None):
    dom = 'DEFAULT'
    page = None
    url = None

    def siftUrl(s):
        """Tries to produce a valid web page when user munges things"""
        page = group = ''
        bits = urlparse.urlsplit(s)
        url = '://'.join([bits[0],bits[1]]) + '/' # http://www.example.org/
        query = bits[2].split('/')
        if '' in query: query.remove('')
        if len(query) > 1: page = query.pop()
        if len(query) > 0 and query[-1][0] == query[-1].capitalize()[0]:
            group = query.pop()
        if len(query): url += '/'.join(query) + '/'
        if page == '': page = 'Main'
        if group == '': group = 'Main'
        page = '.'.join([group,page])
        return url, page

    # TODO: Commented options are not available at time of publication. 
    #       Planned. v.1.2.0
    #-----------------------------------
    # Optparse allows me to easily set up the base options. Additionally, it
    # lets me add an option here and it will appear in the shorthelp display.
    p = optparse.OptionParser(
            conflict_handler="resolve",version="%prog "+__version__)
    p.add_option(
            '-a','--author',dest='author', 
            help="sets author's name from the command line")
    p.add_option(
            '-b',action='store_true',dest='browse',
            help='after edit, load the page in the configured browser.')
    p.add_option(
            '-e','--editor',dest='editor', 
            help='sets editor (full path) from the command line.')
    #p.add_option(
            #'-i','--infile',dest='infile', 
            #help='edit local source and save.')
    p.add_option(
            '-h','--help',action='callback', callback=shorthelp, 
            help='show this help message and exit.')
    p.add_option(
            '-j','--journal',action='store_true',
            help="append today's date to page")
    #p.add_option(
            #'-l','--localcopy',action='store_true',dest='localcopy',
            #help='retain local copy of page source after edit.')
    p.add_option(
            '-n','--nopass',action='store_true',dest='nopass',
            help='site does not require a password.')
    #p.add_option(
            #'-o','--outfile',dest='outfile', 
            #help='pull source and save locally sans editing.')
    p.add_option(
            '-v','--verbose',type='int',dest='verbose')
    option, args = p.parse_args()
    # - Done Optparse: options are in "option" object.

    argv_e = {
        'mas' : "Too many arguments.",
    }

    if len(args):
        (dom, page) = args[0].split(':')
        if dom == 'http': url, page = siftUrl(args[0])

    c = PmwikiConfig(dom, url)
    if not page: page = c.page

    pm = PmwikiPage(c.url, page, c.enablepathinfo)

    #-----------------------------------
    # Editor checksum
    if (option.editor): c.editor = option.editor
    checkApp(True, c.editor, 'noeditor')

    #-----------------------------------
    # Get the password.
    if option.nopass or not c.password: password = None
    else: password = getpass.getpass()

    #-----------------------------------
    # Looking for an infile source. TODO: Planned v.1.2.0
    """
    if option.infile:
        if os.path.isfile(option.infile):
           f.open(option.infile,'r')
           src = f.read()
           f.close()
        else:
            say_error("No local source file to read from.")
    else:
        if option.journal: page += time.strftime('-%Y-%m-%d')
        msg = "Editing: %s (%s)" % (page,c.url)
        say_info(msg)
        src = pm.readpage(c.author, password)
    """
    if option.journal: pm.page += time.strftime('-%Y-%m-%d')
    say_info("Editing: "+page+" ("+c.url+")")
    src = pm.readpage(c.author, password)

    #-----------------------------------
    # Magic. Edit the page, write the page.
    new = pm.editpage(c.editor, src) # LOCAL
    out = pm.writepage(new, src, c.author, password)

    if checkApp(option.browse, c.browser, 'nobrowser'):
        cmd = "%s %s" % (c.browser, c.url)
        os.system(cmd)

if __name__ == '__main__':
    '''When we're running from the command line. Perhaps in the future a GUI
    will come?'''

    try:
        logging.basicConfig(
            level=logging.ERROR,
            format='%(asctime)s %(levelname)-8s %(message)s',
            datefmt='%a, %d %b %Y %H:%M:%S',
            filename = "/tmp/pywe.log",
            filemode='a+')
    except TypeError:
        logging.basicConfig()

    try:
        main(sys.argv[1:])
    except KeyboardInterrupt:
        say_error('User terminated program via keyboard')