# -*- coding: utf-8 -*-
"""Functions accessible from inside Emacs.

Filename: pym.py

Usage in Emacs: import them with
M-x pymacs-load pym

And then all functions here become available in Emacs as
pym-function-name with every _ replaced by - in the names.
"""

__author__ = "Fernando Perez. <fperez@pizero.colorado.edu>"
__license__= "GPL"

import re
from Pymacs import lisp

# lisp is the global which handles lisp-based interaction with the active
# Emacs buffer from which any function is called

# interactions is a global dict which MUST be updated for each new function
# defined which we want to be visible to Emacs. Each function must have an
# entry in it with the function as key (the function *object*, NOT its name as
# a string) and a string as value. At a minimum, this string will be empty,
# but it can contain the names of variables to be read interactively by the
# function, lisp-style.

# Functions meant to be used internally only (not exposed to Emacs) don't need
# an entry in interactions.

interactions = {}

#***************************************************************************
# WARNING: things from genutils copied verbatim here. For some reason pymacs
# does not import other modules correctly (my own, it seems ok with system
# stuff).

def indent(str,nspaces=4,ntabs=0):
    """Indent a string a given number of spaces or tabstops.

    indent(str,nspaces=4,ntabs=0) -> indent str by ntabs+nspaces.
    """

    ind = '\t'*ntabs+' '*nspaces
    outstr = '%s%s' % (ind,str.replace('\n','\n'+ind))
    if outstr.endswith('\n'+ind):
        return outstr[:-len(ind)]
    else:
        return outstr

# End of genutils copy/paste job.

#***************************************************************************
# Lisp utility functions, snatched from elsewhere.

def clean_undo_after(checkpoint):
        """\
Remove all intermediate boundaries from the Undo list since CHECKPOINT.
"""
        lisp("""
(let ((undo-list %s))
  (if (not (eq buffer-undo-list undo-list))
      (let ((cursor buffer-undo-list))
        (while (not (eq (cdr cursor) undo-list))
          (if (car (cdr cursor))
              (setq cursor (cdr cursor))
            (setcdr cursor (cdr (cdr cursor)))))))
  nil)
"""
             % (checkpoint or 'nil'))

#***************************************************************************
# Utility functions, none of which need an interactions[] entry.

def lisp_obj_info(obj):
    """Return various details about a lisp object as a string.

    Useful mainly for debugging purposes."""

    info = [obj,obj.__class__,obj.index,type(obj.index),repr(obj)]
    info = map(str,info)
    info = '\n'.join(info)
    return info

#---------------------------------------------------------------------------
def lisp_char(lisp_obj):
    """Return a single character string from a lisp char object.

    Used to extract characters from their lisp form as obtained in interactive
    functions with the c code. """
    text_form = repr(lisp_obj)
    try:
        return re.search(r"'\?(.)'",text_form).group(1)
    except:
        return None

#---------------------------------------------------------------------------
def is_yes(lisp_obj):
    """Check whether an interactive lisp character reply is a yes (y/Y)"""

    try:
        return lisp_char(lisp_obj).lower() == 'y'
    except:
        return 0

#---------------------------------------------------------------------------
def cut_region(mode='string'):
    """Return the active region and remove it from Emacs.

    The mode parameter (default 'string') defines whether to return the region
    as a string or as a list of lines (mode='list').

    It is the caller's responsibility to insert the updated text at the
    end back in the Emacs buffer with a call to lisp.insert(...)."""

    start, end = lisp.point(), lisp.mark(lisp.t)
    # BUG: buffer_substring() can't extract regions with dos line endings (\r\n)
    # It dumps a traceback.
    region = lisp.buffer_substring(start, end)
    if mode == 'list':
        region = region.splitlines()
    lisp.delete_region(start, end)
    return region
    # cut_region() doesn't need an entry in interactions[] b/c it's meant to
    # be used internally by other functions in this module, not directly
    # from Emacs

#---------------------------------------------------------------------------
def insert_text(text,offset=0):
    """Insert text in buffer and move cursor to a certain offset.

    If called with no offset, leaves the cursor at the current position."""

    # save undo state so we can roll everything into a single operation for undo
    checkpoint = lisp.buffer_undo_list.value()
    user_pos = lisp.point()
    lisp.insert(text)
    lisp.goto_char(user_pos+offset)
    # Collapse all operations into a single one, for Undo.
    clean_undo_after(checkpoint)

#---------------------------------------------------------------------------
def insert_indented_text(text,offset):
    """Insert indented text in buffer and move cursor to a certain offset."""

    # save undo state so we can roll everything into a single operation for undo
    checkpoint = lisp.buffer_undo_list.value()
    # figure out if we are indented or not, and adapt text accordingly
    indent_level = get_line_offset()
    if indent_level > 0:
        text = indent(text,indent_level)
    # perform actual insertion with proper cursor positioning
    offset += indent_level
    lisp.beginning_of_line()
    user_pos = lisp.point()
    lisp.insert(text)
    lisp.goto_char(user_pos+offset)
    # Collapse all operations into a single one, for Undo.
    clean_undo_after(checkpoint)

#---------------------------------------------------------------------------
def get_line_offset():
    """Return number of characters cursor is offset from margin.    """
    user_pos = lisp.point()
    lisp.beginning_of_line()
    line_start = lisp.point()
    lisp.goto_char(user_pos)
    return user_pos - line_start
# end get_line_offset()

#---------------------------------------------------------------------------
def newfn_string(name,sep,end,args=''):
    """Template for a new function definition.

    Returns the string containing the definition and the integer offset for
    cursor positioning."""

    # prepare text
    out = ''
    sep = lisp_char(sep)
    if sep is not None:
        out += '#'+sep*77+'\n'
    out += 'def '+name+'('+args
    offset = len(out)
    out += '):\n'
    out += '    """\n'*2
    if is_yes(end):
        out += '# end '+name+'()\n'
    return out,offset

#***************************************************************************
# 'Public' functions (exposed to Emacs). All these MUST have an interactions[]
# entry

def bow():
    """Break a region replacing all whitespace with newlines.

    Originally an example in Pymacs' README."""

    region = cut_region()
    lisp.insert('\n'.join(region.split()))

# Update interactions[] for functions meant to be visible in Emacs.

# Interaction strings follow some funny emacs-lisp conventions, with the first
# letter being a code and the rest a prompt. Use `C-h f interactive' in Emacs
# to get a description.  The simplest one is a prompt for a string, which is
# given as a string of the form 's<prompt>'.

# Will print 'name ' in the minibuffer and get a string:
#interactions[deft] = 'sNew function name? '

# The c code is for characters, and the Pymacs readme says they are returned
# to python as ints, but that doesn't seem to be the case. Instead I'm getting
# Pymacs.Lisp objects, which have a repr() of the form "lisp('?<char>')" where
# <char> is the returned character.

interactions[bow] = ''

# Note that trying to set interactions as a function attribute:
# bow.interactions = ''
# is NOT WORKING. The module loads in Emacs, but no functions are actually
# recognized. Tested with Python 2.1, it might work with Python 2.2

#-----------------------------------------------------------------------------
def dos2unix():
    """Remove DOS line endings from a region.
    """
    # Save undo state so we can roll everything into a single operation for undo
    checkpoint = lisp.buffer_undo_list.value()
    region = cut_region('list')
    lisp.insert('\n'.join(region)+'\n')
    # Collapse all operations into a single one, for Undo.
    clean_undo_after(checkpoint)

# BUG: it's not working b/c of a bug in lisp.buffer_substring(), so let's not
# activate it for now.
#interactions[dos2unix] = ''

#---------------------------------------------------------------------------
def newfn(name,sep,end,args=''):
    """Insert a template for a new function definition."""

    insert_indented_text(*newfn_string(name,sep,end))

new_template = 'sNew %s name? \n'\
               'cEnter separator (RET for none): \n'\
               'cPrint end marker (y/[N])? '

interactions[newfn] = new_template % 'function'

#-----------------------------------------------------------------------------
def newweave(name,sep,end,use_blitz):
    """Insert a template for a new weave function definition.
    """

    blitz,ending = '',''
    if is_yes(use_blitz):
        blitz = ",type_factories = blitz_type_factories"
    if is_yes(end):
        ending = "\n# end %s()" % (name,)

    head,offset = newfn_string(name,sep,0)
    head += \
'''
    code = \\
"""

"""
    return weave.inline(code,[]%(blitz)s)%(ending)s
''' % locals()
    insert_indented_text(head,offset)

interactions[newweave] = new_template % 'weave function'
interactions[newweave] += '\ncUse blitz type factories (y/[N])? '

#---------------------------------------------------------------------------
def newmeth(name,sep,end):
    """Insert a template for a new method definition.    """

    insert_indented_text(*newfn_string(name,sep,end,'self'))

interactions[newmeth] = new_template % 'method'

#---------------------------------------------------------------------------
def newclass(name,sep,end):
    """Template for new class definition.    """
    out =  ('class %s:\n' % (name,)) + ('    """\n'*2) + '\n'
    offset = get_line_offset()+len(out) + len ("    def __init__(self")
    new_str = newfn_string('__init__',None,None,'self')[0]
    out += indent(new_str)
    if is_yes(end):
        out += '# end class '+name+'\n'
    insert_indented_text(out,offset)

interactions[newclass] = new_template % 'class'
