# -*- coding: iso-8859-1 -*-
"""
    MoinMoin - Matplotlib integration

    example:   {{{#!mpl src=off,run=on,prefix=mpl,display=inline,klass=mpl,tablestyle="",rowstyle="",style="",persistence=True,debug=False
                ...
                ...
                }}}

    keyword parameters:

    @param display: off|link|inline (default:inline)
    @param columns: arange images in table with No columns (default:1) 
                    if more than 1 image and display is inline
    @param src: on|off (display mpl source, or not, default: off)
    @param run: on|off (if off, execution is disabled, default:on)

    @prefix: prefix for files generated (default: mpl). If you have more than one
             image on a page which is not persistent, you should use a unique prefix
             for each chart.

    @params klass: class attribute for div
    @params tablestyle: style attribute for table
    @params rowstyle: style attribute for table row
    @params style: style attribute for table cell

    @params persistence: if True create delete.me.to.regenerate.images marker (default: True)
    @params debug: if True keep files after processing (default: False)


    Directives:
    -----------

    #! include(pagename)
    #! attach(pagename/*.xls)
    #! page(pagename)
    #! icaedoc(icaedoc-name)

    Mpl scripts can be made very modular using the '#! include(pagename)' directive.
    You can see this similar to the import statement in python. In the Mpl script the
    whole page content (#format python) is included. Use this to store your standard
    settings as reuse them for consistent plotting.

    example: #! include(MplSettings)


    Data sources:
    ============

    1) Page attachments
    *******************

    # --------------------------------
    #! attach(pagename/*.xls) 
    # --------------------------------

    The IN object contains all attached input files in the IN['files'] dictionary with the filenames as keys.
    If you want to make a page attachment available for the plotting process, 
    use the '#! attach(filename)' directive and reference the file in the open
    statement as shown in the example.

    example:
        #! attach('filename')
        files = IN['files']
        f = open(files['filename'])

    2) Page raw data
    ****************

    # --------------------------------
    #! page(pagename) 
    # --------------------------------

    The IN object contains the page raw data (text) in the IN['pages'] dictionary with the pagenames as keys
    and the page content (as obtained from page.get_raw_body()). Absolute page names are used as keys.

    example:
        #! page(PageWithData)
        pgData = IN['pages']['PageWithData']

    3) Form data
    ************

    The IN object also contains the form dictionary from the current request.
    example: value = IN['form'].get('parname',['0'])[0]

    4) iCAE document objects
    ************************

    # --------------------------------
    #! icaedoc(iCAE-doc-name)
    # --------------------------------

    You can add iCAE documents with the correct short path. The documents are made
    available in the IN['files'] dictionary in the same way in the '#! attach()' directive.

    example:
        #! icaedoc('P000000-MainEngineData.xls')
        files = IN['files']
        f = open(files['P000000-MainEngineData.xls'])

    The IN object contains also the current user name from the request in IN['user']

    @copyright: 2008 F. Zieher
    @license: GNU GPL, see COPYING for details.

"""

import os, re
import sys
import exceptions
import tempfile
import sha
import pickle

from path import path

from MoinMoin.Page import Page
from MoinMoin import config, wikiutil
from MoinMoin.action import AttachFile
from MoinMoin import log

# -------------------------------------------------------------------------------

loggin = log.getLogger(__name__)

# ---------------------------------------------------------------------- 

# some config settings, XXX should be moved to the moin cfg object
if 'win' in sys.platform:
    FURL = 'e:/home/zieherf/_ipython/security/ipcontroller-tc.furl'
else:
    FURL = '/s330/moin/furls/ipcontroller-tc.furl'

Dependencies = ['time']

# ---------------------------------------------------------------------- 

MPL_HELP = """
# ------------------------------------------------------------------------------------
# Predefined objects:
#    mpl ... matplotlib object
#    plt ... matplotlib.pyplot object
#    np  ... numpy object
#    IN  ... dictionary containing 'files' dictionary, 'pages' dictionary,
#            'form' dictionary (copy of request.form) and 'user' name.
#            - Access attached file with IN['files']['filename']
#            - Accrss page data with IN['pages']['PageName']
#            - Access form parameter with IN['form'].get(param,['default-value'])[0]
#    mm  ... use mm.imgName(imgno=0) and mm.nextImg() in plt.savefig
#            examples: plt.savefig(mm.imgName()), and for all consecutive
#                      plt.savefig(mm.nextImg())
# ------------------------------------------------------------------------------------
"""

# ---------------------------------------------------------------------- 

class SandBox:
    """Implement Sandbox for executing python code.
    Sandbox has inbox/outbox directories. inbox is where ipengine
    reads all data from. outbox is where ipengine creates
    output files (i.e. image files from matplotlib).
    """

    fmode = 0666
    dmode = 0777

    def __init__(self,request,sbpath='',prefix=''):
        """Instantiate sandbox

        @param sbpath: if given, path to sandbox, otherwise a temporary name will be created
        @param prefix: prefix to use when sandbox name is created automatically
        """
        self.request = request
        self.sb      = sbpath and path(sbpath) or path(tempfile.NamedTemporaryFile(prefix=prefix).name)
        self.inbox   = self.sb + '/inbox'
        self.outbox  = self.sb + '/outbox'
        self.create()
        
    def create(self):
        """Create sandbox
        """
        for d in (self.sb,self.inbox,self.outbox):
            d.mkdir()
            d.chmod(self.dmode)

    def emptyInbox(self):
        """Empty (delete) all files in inbox
        """
        for f in self.inbox.walkfiles():
            f.remove()

    def emptyOutbox(self):
        """Empty (delete) all files in outbox
        """
        for f in self.outbox.walkfiles():
            f.remove()

    def remove(self):
        """Remove sandbox
        """
        self.sb.rmtree()

    def sendTo(self,srcname,dstname=''):
        """Copy file srcname into the inbox. 
        If dstname is not given, srcname is used.
        """
        f = path(srcname)
        if f.isfile() and f.access(os.R_OK):
            dst = dstname and self.inbox + dstname or self.inbox + f.basename()
            f.copy(dst)
            dst.chmod(self.fmode)

    @property
    def outboxFiles(self):
        """Return files in the outbox
        """
        return self.outbox.files()

    @property
    def inDict(self):
        """Input object sent to ipengine"""
        
        indict = dict(files={},form={},user="")

        # file objects from attach and icaedoc directives
        for f in self.inbox.files():
            indict['files'][f.basename()] = f.encode(config.charset).replace('\\','/')

        return indict

# ---------------------------------------------------------------------- 

class MplClient:
    """Client interface to ipengines.

    1) Create sandbox
    2) Copy all required input data into inbox
    3) Supply python script file to run --> copy into inbox as run.py
    4) Run run.py with TaskClient and pull output out (dictionary)
    5) Get png files from outbox and attach to page name
    6) Cleanup
    """

    furl = FURL

    def __init__(self,request,pagename,script,fmt='png',prefix='mpl',debug=False):

        self.request    = request
        self.pagename   = pagename
        self.fmt        = fmt
        self.prefix     = prefix
        self.debug      = debug
        self.attach_dir = path(AttachFile.getAttachDir(self.request,self.pagename))
        self.sb     = None
        self.script = ''
        self.inbox  = []
        self.pageData = {}
        self.imgdata = []
        self.logdata = None
        self.outdata = {}

        # base script   
        self.script = script

        # treat attachments referenced in script and create input list
        # XXX could be done more efficient (parse once :-)
        self.parseAttachments()
        self.parseIcaeDocs()
        self.parsePages()
        self.parseIncludes()

    @property
    def imgPrefix(self):
        raw = self.script
        for f in self.inbox:
            raw += str(f.stat().st_mtime)
        for pg,data in self.pageData.items():
            raw += data
        self.imgPrefix = self.prefix + "_" + sha.new(raw).hexdigest() + '_chart'
        return self.imgPrefix

    def addInput(self,infile):
        """Add infile to the set of input files.
        """
        f = path(infile)
        if f.isfile() and f.access(os.R_OK):
            self.inbox.append(f)

    def parseAttachments(self):
        """Collect input files from "#! attach(file-attachment-path)" in self.inbox

        The path can reference an attachment on a differenet page than the
        current. It supports relative path syntax, i.e. "../file.dat".
        The file part can include widcard character that are used for
        globing more than one file.
        
        Don't use quotation marks on the file-path.
        """
        rec = re.compile(r"""(#! *attach\()(?P<fname>[A-Za-z0-9 ._=+*/-]+)(\).*)""")
        for line in self.script.splitlines():
            m = rec.match(line)
            if m:
                # find page and attach_dir where attachments are stored
                aName = m.group('fname')

                # get pageName and attchDir
                fparts = aName.split('/')
                pageName = '/'.join(fparts[0:-1])

                if not pageName:
                    pageName = self.pagename
                else:
                    if pageName.endswith('..'):
                        pageName += '/'
                    pageName = wikiutil.AbsPageName(self.pagename,pageName)
                filePat  = fparts[-1]

                attachDir = AttachFile.getAttachDir(self.request,pageName)

                # attachment found, could be pattern
                for fname in path(attachDir).files(filePat):
                    self.inbox.append(fname)

    def parseIcaeDocs(self):
        """Collect input files from "#! icaedoc(file-attachment-path)" in self.inbox
        The path must be a conform icae document path, i.e. having the short path
        prepended. Don't use quotation marks on the file-path.
        example:
            #! icaedoc(P000000_4010-benchmark.xls) 
        """
        return
		
        from icaebase import getPathFromSPath

        rec = re.compile(r"""(#! *icaedoc\()(?P<fname>[A-Za-z0-9 ._=+*/-]+)(\).*)""")
        for line in self.script.splitlines():
            m = rec.match(line)
            if m:
                fname = m.group('fname')
                try:
                    spath,doc = fname.split('-')
                except:
                    continue

                fname = (getPathFromSPath(spath) + '0-documentation') + fname
                if fname.exists():
                    # attachment found, could be pattern
                    self.inbox.append(fname)

    def parsePages(self):
        """Read data from #! page(pagename) and store text data in self.pages
        """
        rec = re.compile(r"""(#! *page\()(?P<pname>[A-Za-z0-9 ._=+*/-]+)(\).*)""")
        script = self.script.splitlines()

        for lno,line in enumerate(script):
            m = rec.match(line)
            if m:
                # page directive detected
                pageName = m.group('pname')
                if pageName.endswith('..'):
                    pageName += '/'
                pageName  = wikiutil.AbsPageName(self.pagename,pageName)
                self.pageData[pageName] = Page(self.request, pageName).get_raw_body()

    def parseIncludes(self):
        """Replace #! include(pagename) with the python src from page.
        """
        rec = re.compile(r"""(#! *include\()(?P<pname>[A-Za-z0-9 ._=+*/-]+)(\).*)""")
        script = self.script.splitlines()

        for lno,line in enumerate(script):
            m = rec.match(line)
            if m:
                # include detected
                pageName = m.group('pname')
                if pageName.endswith('..'):
                    pageName += '/'
                pageName  = wikiutil.AbsPageName(self.pagename,pageName)
                pageTxt = Page(self.request, pageName).get_raw_body()
                pageTxt = '\n'.join(pageTxt.splitlines()[1:])
                script[lno] = pageTxt 

        self.script = '\n'.join(script)

# ------------------- assyncclient ----------------------------------------------------------

    def run(self):
        """Execute matplotlib script by ipengine
        """

        # make sandbox
        self.sb = SandBox(self.request,prefix='mpl_')  

        # send files (data files and python files)
        for f in self.inbox:
            self.sb.sendTo(f)

        # -------------------------------------------------------------
        # run StringTask on ipengine / most cool feature :-)
        # -------------------------------------------------------------

        # IN object 
        inDict = self.sb.inDict.copy()
        inDict['pages']   = self.pageData
        inDict['form']    = self.request.form.copy()
        inDict['user']    = self.request.user.name
        inDict['imgBase'] = str(self.sb.outbox + '/mplplot').replace('\\','/')
        inDict['fmt']     = self.fmt

        # execute script on ipengine and obtain output (pullObjs)
        script   = str(MplScript(self.script).mplScript)
        pushObjs = {'IN':inDict}
        pushObjs['OUT'] = {}

        # the tricky part of handling the execution on the client
        # this is valid when using twisted as web framework in moin
        
        #import sys
        #sys.path.append("/home/zjfhach/usr/lib/python2.6/site-packages")
        #if True:#hasattr(self.request,'reactor'):
        #    from IPython.kernel import asyncclient
        #    from twisted.internet.threads import blockingCallFromThread
        #    tc  = blockingCallFromThread(self.request.reactor,asyncclient.get_task_client,self.furl)
        #    tc  = tc.adapt_to_blocking_client()
        #    st  = asyncclient.StringTask(script,clear_before=True,pull=pullObjs,push=pushObjs)
        #else:
        #    from IPython.kernel import client
        #    tc  = client.TaskClient(self.furl)
        #    st  = client.StringTask(script,clear_before=True,pull=pullObjs,push=pushObjs)

        #tid = tc.run(st)
        exec(script,pushObjs) #tc.get_task_result(tid,block=True)

        #if res.failure:
         #   self.failure = res.failure
          #  self.logdata = res.failure.getTraceback()

        #if res.results.get('OUT',{}):
        #    self.outdata = res.results.get('OUT',{})
        self.outdata = pushObjs['OUT']
        # -------------------------------------------------------------

        # read image data
        self.imgdata = []
        for no,imgfile in enumerate(self.sb.outbox.files('mplplot-*.%s'%self.fmt)):
            self.imgdata.append(file(imgfile,'rb').read())

        # finally cleanup
        self.cleanup()

    def cleanup(self):
        """Cleanup sandbox
        """
        if not self.debug:
            self.sb.remove()


# ---------------------------------------------------------------------- 

class MplScript:

    pre = """
# --- PRE CODE -------------------------------------------
import matplotlib as mpl
mpl.use('Agg')
import matplotlib.pyplot as plt
import numpy as np

# reset mpl default settings
mpl.rcdefaults()

class MoinMpl:
    def __init__(self,base,fmt='png'):
        self.base = base
        self.imgno  = 0
        self.fmt    = fmt
    def imgName(self,imgno=0):
        self.imgno = int(imgno)
        return '%s-%d.%s'% (self.base,self.imgno,self.fmt)
    def nextImg(self):
        return self.imgName(self.imgno+1)
# ------------------------------------------

mm = MoinMpl(IN['imgBase'],IN['fmt'])
OUT = {} # output will be handed to caller
# --- END PRE CODE ---------------------------------------

"""
    post = """

# --- POST CODE ------------------------------------------
plt.close()
# --- END POST CODE --------------------------------------
"""

    def __init__(self,pyscript=''):
        self.script = pyscript

    @property
    def mplScript(self):
        return self.pre+self.script+self.post

# ---------------------------------------------------------------------- 

def mpl_settings(run='on',src='off',mxsrc=9999,fmt='png',display='inline',columns=1,prefix='mpl',
                 klass="mpl",tablestyle="",rowstyle="",style="",persistence=True,debug=False):
    """
    Initialize default parameters.
    """
    return locals()

# ---------------------------------------------------------------------- 

class Parser:
    """
        Sends plot images generated by matplotlib
    """
    
    extensions = []
    Dependencies = Dependencies

    def __init__(self, raw, request, **kw):
        self.raw = raw
        self.request = request

        args = kw.get('format_args', '')
        # we use a macro definition to initialize the default init parameters
        # if a user enters a wrong parameter the failure is shown by the exception
        try:
            pyplot = mpl_settings()
            for k,v in pyplot.iteritems():
                setattr(self,k,v)
            settings = wikiutil.invoke_extension_function(request, mpl_settings, args)
            for k, v in settings.iteritems():
                if v != None: 
                    pyplot[k]=v
                    setattr(self,k,v)

        except ValueError, err:
            msg = u"matplotlib: %s" % err.args[0]
            request.write(self.request.formatter.text(msg))

    def format(self, formatter):
        """ Send the text. """

        #self.request.flush() # to identify error text
        self.formatter = formatter

        if self.src.lower() in ('on','1','true'):
            # use highlight parser to show python code
            from MoinMoin.parser.highlight import Parser as HLParser
            hlp = HLParser(MPL_HELP+self.raw,self.request,format_args='python')
            hlp.format(formatter)

        if self.run.lower() in ('off','0','false'):
            self.request.write(formatter.preformatted(1)+'Execution disabled, set run=on.'+formatter.preformatted(0))
            return

        self.pagename = formatter.page.page_name
        self.attach_dir=AttachFile.getAttachDir(self.request,self.pagename,create=1)

        # -------------------- 

        # mplclient is created here, need to include data files in hexdigest
        self.mpl = MplClient(self.request,self.pagename,self.raw,self.fmt,prefix=self.prefix,debug=self.debug)
        update, charts = self._updateImgs(formatter)

        if update:

            self.mpl.run()

            if self.mpl.logdata:
                # error occured
                self.request.write(formatter.preformatted(1)+self.mpl.logdata+formatter.preformatted(0))
                return

            # attach generated image(s)
            for no,imgdata in enumerate(self.mpl.imgdata):
                imgName = "%s-%d.%s" % (self.mpl.imgPrefix,no,self.fmt)
                attached_file = file(self.attach_dir + "/" + imgName, 'wb')
                attached_file.write(imgdata)
                attached_file.close()
                charts.append(imgName)

        self.renderCharts(charts)

    def renderCharts(self,charts):
        """Render charts according to settings
        """
    
        if self.display.lower() in ('off','0','false') or not charts:
            return

        fmt = self.formatter

        html = []
        html.append(fmt.div(1,attr={'class':self.klass}))

        if self.display.lower() == 'link':
            # link display
            html.append(fmt.bullet_list(1))
            for chart in charts:
                url = AttachFile.getAttachUrl(self.pagename, chart, self.request)
                html.append(fmt.listitem(1))
                html.append(fmt.url(1,url)+chart+fmt.url(0))
                html.append(fmt.listitem(0))
            html.append(fmt.bullet_list(0))
        
        else:
            # inline display in table form
            noImgs = len(charts)
            rows  = noImgs / self.columns
            rows += noImgs%self.columns and 1 or 0

            T = fmt.table
            R = fmt.table_row
            C = fmt.table_cell

            html.append(T(1,style=self.tablestyle))
            id = 0
            for row in range(rows):
                html.append(R(1,style=self.rowstyle))
                for col in range(self.columns):
                    chart = charts[id]
                    url = AttachFile.getAttachUrl(self.pagename, chart, self.request)
                    html.append(C(1,style=self.style)+fmt.url(1,url)+fmt.image(src="%s" % url, alt=chart)+fmt.url(0)+C(0))
                    id += 1
                    if id >= noImgs:
                        break
                html.append(R(0))
                if id >= noImgs:
                    break
            html.append(T(0))

        html.append(fmt.div(0))
        self.request.write('\n'.join(html))

    def _removeChart(self,chart):
        """Remove chart attachment from page and from xapian index.
        @param chart: chart name (without path)
        """
        fpath = os.path.join(self.attach_dir, chart).encode(config.charset)
        os.remove(fpath)
        if self.request.cfg.xapian_search:
            from MoinMoin.search.Xapian import Index
            index = Index(self.request)
            if index.exists:
                index.remove_item(self.pagename, chart)

    def _updateImgs(self, formatter):
        """Delete outdated charts
        @param formatter: formatter object
        """
        imgPrefix = self.mpl.imgPrefix

        # use delete.me.to.regenerate.images trick from dot.py
        dm2ri = self.attach_dir + '/' + "%s.delete.me.to.regenerate.images"%self.prefix
        charts = []

        updateImgs = True
        if not self.persistence and path(dm2ri).exists():
            path(dm2ri).remove()

        deleteImgs = not path(dm2ri).exists()

        # delete.me. ... exists, if with pref exists, then continue and do not 
        # rerun mpl execution
        attach_files = AttachFile._get_files(self.request, self.pagename)
        reChart  = re.compile(r"%s_.*_chart-[0-9]+.%s"%(self.prefix,self.fmt))
        for chart in attach_files:
            if reChart.match(chart) and deleteImgs:
                self._removeChart(chart)

            if not deleteImgs and imgPrefix in chart:
                charts.append(chart)
                updateImgs = False

        # create persistence marker if not existing and persistence requested
        if deleteImgs and self.persistence:
            open(dm2ri,'w').close()

        return updateImgs,charts
