# -*- coding: utf-8 -*-
'''
PythonTeX code engines.

Provides a class for managing the different languages/types of code
that may be executed.  A class instance is created for each language/type of
code.  The class provides a method for assembling the scripts that are 
executed, combining user code with templates.  It also creates the records 
needed to synchronize `stderr` with the document.

Each instance of the class is automatically added to the `engines_dict` upon
creation.  Instances are typically accessed via this dictionary.

The class is called `*CodeEngine` by analogy with a template engine, since it
combines user text (code) with existing templates to produce an output
document (script for execution).



Copyright (c) 2012-2014, Geoffrey M. Poore
All rights reserved.
Licensed under the BSD 3-Clause License:
    http://www.opensource.org/licenses/BSD-3-Clause

'''

# Imports
import os
import sys
import textwrap
from hashlib import sha1
from collections import OrderedDict, namedtuple


interpreter_dict = {k:k for k in ('python', 'ruby', 'julia', 'octave')}
# The {file} field needs to be replaced by itself, since the actual 
# substitution of the real file can only be done at runtime, whereas the
# substitution for the interpreter should be done when the engine is 
# initialized.
interpreter_dict['file'] = '{file}'
interpreter_dict['File'] = '{File}'


engine_dict = {}


CodeIndex = namedtuple('CodeIndex', ['input_file', 'command', 
                                     'line_int', 'lines_total', 
                                     'lines_user', 'lines_input',
                                     'inline_count'])


class CodeEngine(object):
    '''
    The base class that is used for defining language engines.  Each command 
    and environment family is based on an engine.
    
    The class assembles the individual scripts that PythonTeX executes, using
    templates and user code.  It also creates the records needed for 
    synchronizing `stderr` with the document.
    '''
    def __init__(self, name, language, extension, command, template, wrapper, 
                 formatter, errors=None, warnings=None,
                 linenumbers=None, lookbehind=False, 
                 console=False, startup=None, created=None):

        # Save raw arguments so that they may be reused by subtypes
        self._rawargs = (name, language, extension, command, template, wrapper, 
                         formatter, errors, warnings,
                         linenumbers, lookbehind, console, startup, created)
        
        # Type check all strings, and make sure everything is Unicode
        if sys.version_info[0] == 2:
            if (not isinstance(name, basestring) or 
                    not isinstance(language, basestring) or 
                    not isinstance(extension, basestring) or 
                    not isinstance(command, basestring) or 
                    not isinstance(template, basestring) or
                    not isinstance(wrapper, basestring) or
                    not isinstance(formatter, basestring)):
                raise TypeError('CodeEngine needs string in initialization')
            self.name = unicode(name)
            self.language = unicode(language)
            self.extension = unicode(extension)
            self.command = unicode(command)
            self.template = unicode(template)
            self.wrapper = unicode(wrapper)
            self.formatter = unicode(formatter)
        else:
            if (not isinstance(name, str) or 
                    not isinstance(language, str) or 
                    not isinstance(extension, str) or 
                    not isinstance(command, str) or 
                    not isinstance(template, str) or
                    not isinstance(wrapper, str) or
                    not isinstance(formatter, str)):    
                raise TypeError('CodeEngine needs string in initialization')
            self.name = name
            self.language = language
            self.extension = extension
            self.command = command
            self.template = template
            self.wrapper = wrapper
            self.formatter = formatter
        # Perform some additional formatting on some strings.
        self.extension = self.extension.lstrip('.')
        self.template = self._dedent(self.template)
        self.wrapper = self._dedent(self.wrapper)
        # Make sure formatter string ends with a newline
        if not self.formatter.endswith('\n'):
            self.formatter = self.formatter + '\n'
        
        # Type check errors, warnings, and linenumbers
        if errors is None:
            errors = []
        else:
            if sys.version_info[0] == 2:
                if isinstance(errors, basestring):
                    errors = [errors]
                elif not isinstance(errors, list) and not isinstance(errors, tuple):
                    raise TypeError('CodeEngine needs "errors" to be a string, list, or tuple')
                for e in errors:
                    if not isinstance(e, basestring):
                        raise TypeError('CodeEngine needs "errors" to contain strings')
                errors = [unicode(e) for e in errors]
            else:
                if isinstance(errors, str):
                    errors = [errors]
                elif not isinstance(errors, list) and not isinstance(errors, tuple):
                    raise TypeError('CodeEngine needs "errors" to be a string, list, or tuple')
                for e in errors:
                    if not isinstance(e, str):
                        raise TypeError('CodeEngine needs "errors" to contain strings')
            self.errors = errors
        if warnings is None:
            warnings = []
        else:
            if sys.version_info[0] == 2:
                if isinstance(warnings, basestring):
                    warnings = [warnings]
                elif not isinstance(warnings, list) and not isinstance(warnings, tuple):
                    raise TypeError('CodeEngine needs "warnings" to be a string, list, or tuple')
                for w in warnings:
                    if not isinstance(w, basestring):
                        raise TypeError('CodeEngine needs "warnings" to contain strings')
                warnings = [unicode(w) for w in warnings]
            else:
                if isinstance(warnings, str):
                    warnings = [warnings]
                elif not isinstance(warnings, list) and not isinstance(warnings, tuple):
                    raise TypeError('CodeEngine needs "warnings" to be a string, list, or tuple')
                for w in warnings:
                    if not isinstance(w, str):
                        raise TypeError('CodeEngine needs "warnings" to contain strings')
            self.warnings = warnings
        if linenumbers is None:
            linenumbers = 'line {number}'
        if sys.version_info[0] == 2:
            if isinstance(linenumbers, basestring):
                linenumbers = [linenumbers]
            elif not isinstance(linenumbers, list) and not isinstance(linenumbers, tuple):
                raise TypeError('CodeEngine needs "linenumbers" to be a string, list, or tuple')
            for l in linenumbers:
                if not isinstance(l, basestring):
                    raise TypeError('CodeEngine needs "linenumbers" to contain strings')
            linenumbers = [unicode(l) for l in linenumbers]
        else:
            if isinstance(linenumbers, str):
                linenumbers = [linenumbers]
            elif not isinstance(linenumbers, list) and not isinstance(linenumbers, tuple):
                raise TypeError('CodeEngine needs "linenumbers" to be a string, list, or tuple')
            for l in linenumbers:
                if not isinstance(l, str):
                    raise TypeError('CodeEngine needs "linenumbers" to contain strings')
        # Need to replace tags
        linenumbers = [l.replace('{number}', r'(\d+)') for l in linenumbers]
        self.linenumbers = linenumbers

        # Type check lookbehind
        if not isinstance(lookbehind, bool):
            raise TypeError('CodeEngine needs "lookbehind" to be bool')
        self.lookbehind = lookbehind
        
        # Type check console
        if not isinstance(console, bool):
            raise TypeError('CodeEngine needs "console" to be bool')
        self.console = console
        
        # Type check startup
        if startup is None:
            startup = ''
        if startup and not self.console:
            raise TypeError('PythonTeX can only use "startup" for console types')
        else:
            if sys.version_info[0] == 2:
                if isinstance(startup, basestring):
                    startup = unicode(startup)
                else:
                    raise TypeError('CodeEngine needs "startup" to be a string')
            else:
                if not isinstance(startup, str):
                    raise TypeError('CodeEngine needs "startup" to be a string')
            if not startup.endswith('\n'):
                startup += '\n'
        self.startup = self._dedent(startup)
        
        # Type check created; make sure it is an iterable and contains Unicode
        if created is None:
            created = []
        else:
            if sys.version_info[0] == 2:
                if isinstance(created, basestring):
                    created = [created]
                elif not isinstance(created, list) and not isinstance(created, tuple):
                    raise TypeError('CodeEngine needs "created" to be a string, list, or tuple')
                for f in created:
                    if not isinstance(f, basestring):
                        raise TypeError('CodeEngine "created" to contain strings')
                created = [unicode(f) for f in created]
            else:
                if isinstance(created, str):
                    created = [created]
                elif not isinstance(created, list) and not isinstance(created, tuple):
                    raise TypeError('CodeEngine needs "created" to be a string, list, or tuple')
                for f in created:
                    if not isinstance(f, str):
                        raise TypeError('CodeEngine needs "created" to contain strings')
        self.created = created
        
        # The base PythonTeX type does not support extend; it is used in 
        # subtyping.  But a dummy extend is needed to fill the extend field
        # in templates, if it is provided.
        self.extend = ''
        
        # Create dummy variables for console
        self.banner = ''
        self.filename = ''
        
        # Each type needs to add itself to a dict, for later access by name
        self._register()
    
    def _dedent(self, s):
        '''
        Dedent and strip leading newlines
        '''
        s = textwrap.dedent(s)
        while s.startswith('\n'):
            s = s[1:]
        return s
        
    def _register(self):
        '''
        Add instance to a dict for later access by name
        '''
        engine_dict[self.name] = self
        
    def customize(self, **kwargs):
        '''
        Customize the template on the fly.
        
        This provides customization based on command line arguments 
        (`--interpreter`) and customization from the TeX side (imports from
        `__future__`).  Ideally, this function should be restricted to this 
        and similar cases.  The custom code command and environment are 
        insufficient for such cases, because the command is at a level above
        that of code and because of the requirement that imports from 
        `__future__` be at the very beginning of a script.
        '''
        # Take care of `--interpreter`
        # The `interpreter_dict` has entries that allow `{file}` and
        # `{outputdir}` fields to be replaced with themselves
        self.command = self.command.format(**interpreter_dict)
        # Take care of `__future__`
        if self.language.startswith('python'):
            if sys.version_info[0] == 2 and 'pyfuture' in kwargs:
                pyfuture = kwargs['pyfuture']
                future_imports = None
                if pyfuture == 'all':
                    future_imports = '''
                            from __future__ import absolute_import
                            from __future__ import division
                            from __future__ import print_function
                            from __future__ import unicode_literals
                            {future}'''
                elif pyfuture == 'default':
                    future_imports = '''
                            from __future__ import absolute_import
                            from __future__ import division
                            from __future__ import print_function
                            {future}'''
                if future_imports is not None:
                    future_imports = self._dedent(future_imports)
                    self.template = self.template.replace('{future}', future_imports)
            if self.console:
                if sys.version_info[0] == 2 and 'pyconfuture' in kwargs:
                    pyconfuture = kwargs['pyconfuture']
                    future_imports = None
                    if pyconfuture == 'all':
                        future_imports = '''
                                from __future__ import absolute_import
                                from __future__ import division
                                from __future__ import print_function
                                from __future__ import unicode_literals
                                '''
                    elif pyconfuture == 'default':
                        future_imports = '''
                                from __future__ import absolute_import
                                from __future__ import division
                                from __future__ import print_function
                                '''
                    if future_imports is not None:
                        future_imports = self._dedent(future_imports)
                        self.startup = future_imports + self.startup
                if 'pyconbanner' in kwargs:
                    self.banner = kwargs['pyconbanner']
                if 'pyconfilename' in kwargs:
                    self.filename = kwargs['pyconfilename']

    _hash = None
            
    def get_hash(self):
        '''
        Return a hash of all vital type information (template, etc.).  Create
        the hash if it doesn't exist, otherwise return a stored hash.
        '''
        # This file is encoded in UTF-8, so everything can be encoded in UTF-8.
        # It's not important that this encoding be the same as that given by
        # the user, since a unique hash is all that's needed.
        if self._hash is None:
            hasher = sha1()
            hasher.update(self.command.encode('utf8'))
            hasher.update(self.template.encode('utf8'))
            hasher.update(self.wrapper.encode('utf8'))
            hasher.update(self.formatter.encode('utf8'))
            if self.console:
                hasher.update(self.startup.encode('utf8'))
                hasher.update(self.banner.encode('utf8'))
                hasher.update(self.filename.encode('utf8'))
            self._hash = hasher.hexdigest()
        return self._hash
    
    def _process_future(self, code_list):
        '''
        Go through a given list of code and extract all imports from 
        `__future__`, so that they can be relocated to the beginning of the 
        script.
        
        The approach isn't foolproof and doesn't support compound statements.
        '''
        done = False
        future_imports = []
        for n, c in enumerate(code_list):
            in_triplequote = False
            changed = False
            code = c.code.split('\n')
            for l, line in enumerate(code):
                # Detect __future__ imports
                if (line.startswith('from __future__') or 
                        line.startswith('import __future__') and 
                        not in_triplequote):
                    changed = True
                    if ';' in line:
                        raise ValueError('Imports from __future__ should be simple statements; semicolons are not supported')
                    else:
                        future_imports.append(line)
                        code[l] = ''
                # Ignore comments, empty lines, and lines with complete docstrings
                elif (line.startswith('\n') or line.startswith('#') or 
                        line.isspace() or
                        ('"""' in line and line.count('"""')%2 == 0) or 
                        ("'''" in line and line.count("'''")%2 == 0)):
                    pass
                # Detect if entering or leaving a docstring
                elif line.count('"""')%2 == 1 or line.count("'''")%2 == 1:
                    in_triplequote = not in_triplequote
                # Stop looking for future imports as soon as a non-comment, 
                # non-empty, non-docstring, non-future import line is found
                elif not in_triplequote:
                    done = True
                    break
            if changed:
                code_list[n].code = '\n'.join(code)
            if done:
                break
        if future_imports:
            return '\n'.join(future_imports)
        else:
            return ''
            
    def _get_future(self, cc_list_begin, code_list):
        '''
        Process custom code and user code for imports from `__future__`
        '''
        cc_future = self._process_future(cc_list_begin)
        code_future = self._process_future(code_list)
        if cc_future and code_future:
            return cc_future + '\n' + code_future
        else:
            return cc_future + code_future
    
    def get_script(self, encoding, utilspath, outputdir, workingdir, 
                   cc_list_begin, code_list, cc_list_end, debug, interactive):
        '''
        Assemble the script that will be executed.  In the process, assemble
        an index of line numbers that may be used to correlate script line
        numbers with document line numbers and user code line numbers in the 
        event of errors or warnings.
        '''
        lines_total = 0
        script = []
        code_index = OrderedDict()
        
        # Take care of future
        if self.language.startswith('python'):
            future = self._get_future(cc_list_begin, code_list)
        else:
            future = ''
        
        # Split template into beginning and ending segments
        try:
            script_begin, script_end = self.template.split('{body}')
        except:
            raise ValueError('Template for ' + self.name + ' is missing {body}')
        
        # Add beginning to script
        if os.path.isabs(os.path.expanduser(os.path.normcase(workingdir))):
            workingdir_full = workingdir
        else:
            workingdir_full = os.path.join(os.getcwd(), workingdir).replace('\\', '/')
        # Correct workingdir if in debug or interactive mode, so that it's
        # relative to the script path
        # #### May refactor this once debugging functionality is more 
        # fully implemented
        if debug is not None or interactive is not None:
            if not os.path.isabs(os.path.expanduser(os.path.normcase(workingdir))):
                workingdir = os.path.relpath(workingdir, outputdir)
        script_begin = script_begin.format(encoding=encoding, future=future, 
                                           utilspath=utilspath,
                                           workingdir=os.path.expanduser(os.path.normcase(workingdir)),
                                           Workingdir=workingdir_full,
                                           extend=self.extend,
                                           family=code_list[0].family,
                                           session=code_list[0].session,
                                           restart=code_list[0].restart,
                                           dependencies_delim='=>PYTHONTEX:DEPENDENCIES#',
                                           created_delim='=>PYTHONTEX:CREATED#')
        script.append(script_begin)
        lines_total += script_begin.count('\n')
        
        # Prep wrapper
        try:
            wrapper_begin, wrapper_end = self.wrapper.split('{code}')
        except:
            raise ValueError('Wrapper for ' + self.name + ' is missing {code}')
        if not self.language.startswith('python'):
            # In the event of a syntax error at the end of user code, Ruby
            # (and perhaps others) will use the line number from the NEXT
            # line of code that is non-empty, not from the line of code where
            # the error started.  In these cases, it's important
            # to make sure that the line number is triggered immediately 
            # after user code, so that the line number makes sense.  Hence,
            # we need to strip all whitespace from the part of the wrapper
            # that follows user code.  For symetry, we do the same for both
            # parts of the wrapper.
            wrapper_begin = wrapper_begin.rstrip(' \t\n') + '\n'
            wrapper_end = wrapper_end.lstrip(' \t\n')
        stdoutdelim = '=>PYTHONTEX:STDOUT#{instance}#{command}#'
        stderrdelim = '=>PYTHONTEX:STDERR#{instance}#{command}#'
        wrapper_begin = wrapper_begin.replace('{stdoutdelim}', stdoutdelim).replace('{stderrdelim}', stderrdelim)
        wrapper_begin_offset = wrapper_begin.count('\n')
        wrapper_end_offset = wrapper_end.count('\n')
        
        # Take care of custom code
        # Line counters must be reset for cc begin, code, and cc end, since 
        # all three are separate
        lines_user = 0
        inline_count = 0
        for c in cc_list_begin:
            # Wrapper before
            lines_total += wrapper_begin_offset
            script.append(wrapper_begin.format(command=c.command,
                                               context=c.context,
                                               args=c.args_run,
                                               instance=c.instance,
                                               line=c.line))
            
            # Actual code
            lines_input = c.code.count('\n')
            code_index[c.instance] = CodeIndex(c.input_file, c.command, c.line_int, lines_total, lines_user, lines_input, inline_count)
            script.append(c.code)
            if c.is_inline:
                inline_count += 1
            lines_total += lines_input
            lines_user += lines_input
            
            # Wrapper after
            script.append(wrapper_end)
            lines_total += wrapper_end_offset
        
        # Take care of user code
        lines_user = 0
        inline_count = 0
        for c in code_list:
            # Wrapper before
            lines_total += wrapper_begin_offset
            script.append(wrapper_begin.format(command=c.command,
                                               context=c.context,
                                               args=c.args_run,
                                               instance=c.instance,
                                               line=c.line))
            
            # Actual code
            lines_input = c.code.count('\n')
            code_index[c.instance] = CodeIndex(c.input_file, c.command, c.line_int, lines_total, lines_user, lines_input, inline_count)
            if c.command == 'i':
                script.append(self.formatter.format(code=c.code.rstrip('\n')))
                inline_count += 1
            else:
                script.append(c.code)
            lines_total += lines_input
            lines_user += lines_input                
            
            # Wrapper after
            script.append(wrapper_end)
            lines_total += wrapper_end_offset
        
        # Take care of custom code
        lines_user = 0
        inline_count = 0
        for c in cc_list_end:
            # Wrapper before
            lines_total += wrapper_begin_offset
            script.append(wrapper_begin.format(command=c.command,
                                               context=c.context,
                                               args=c.args_run,
                                               instance=c.instance,
                                               line=c.line))
            
            # Actual code
            lines_input = c.code.count('\n')
            code_index[c.instance] = CodeIndex(c.input_file, c.command, c.line_int, lines_total, lines_user, lines_input, inline_count)
            script.append(c.code)
            if c.is_inline:
                inline_count += 1
            lines_total += lines_input
            lines_user += lines_input
            
            # Wrapper after
            script.append(wrapper_end)
            lines_total += wrapper_end_offset
        
        # Finish script
        script.append(script_end.format(dependencies_delim='=>PYTHONTEX:DEPENDENCIES#', created_delim='=>PYTHONTEX:CREATED#'))
        
        return script, code_index


        

class SubCodeEngine(CodeEngine):
    '''
    Create Engine instances that inherit from existing instances.
    '''
    def __init__(self, base, name, language=None, extension=None, command=None, 
                 template=None, wrapper=None, formatter=None, errors=None,
                 warnings=None, linenumbers=None, lookbehind=False,
                 console=None, created=None, startup=None, extend=None):
        
        self._rawargs = (name, language, extension, command, template, wrapper, 
                         formatter, errors, warnings,
                         linenumbers, lookbehind, console, startup, created)
                         
        base_rawargs = engine_dict[base]._rawargs
        args = []
        for n, arg in enumerate(self._rawargs):
            if arg is None:
                args.append(base_rawargs[n])
            else:
                args.append(arg)
        
        CodeEngine.__init__(self, *args)
        
        self.extend = engine_dict[base].extend
        
        if extend is not None:
            if sys.version_info[0] == 2:
                if not isinstance(extend, basestring):
                    raise TypeError('PythonTeXSubtype needs a string for "extend"')
                extend = unicode(extend)
            else:
                if not isinstance(extend, str):
                    raise TypeError('PythonTeXSubtype needs a string for "extend"')
            if not extend.endswith('\n'):
                extend = extend + '\n'
            self.extend += self._dedent(extend)




class PythonConsoleEngine(CodeEngine):
    '''
    This uses the Engine class to store information needed for emulating
    Python interactive consoles.
    
    In the current form, it isn't used as a real engine, but rather as a 
    convenient storage class that keeps the treatment of all languages/code 
    types uniform.
    '''
    def __init__(self, name, startup=None):
        CodeEngine.__init__(self, name=name, language='python', 
                            extension='', command='', template='', 
                            wrapper='', formatter='', errors=None, 
                            warnings=None, linenumbers=None, lookbehind=False,
                            console=True, startup=startup, created=None)




python_template = '''
    # -*- coding: {encoding} -*-
    
    {future}

    import os
    import sys
    import codecs
    
    if '--interactive' not in sys.argv[1:]:
        if sys.version_info[0] == 2:
            sys.stdout = codecs.getwriter('{encoding}')(sys.stdout, 'strict')
            sys.stderr = codecs.getwriter('{encoding}')(sys.stderr, 'strict')
        else:
            sys.stdout = codecs.getwriter('{encoding}')(sys.stdout.buffer, 'strict')
            sys.stderr = codecs.getwriter('{encoding}')(sys.stderr.buffer, 'strict')
    
    if '{utilspath}' and '{utilspath}' not in sys.path:
        sys.path.append('{utilspath}')    
    from pythontex_utils import PythonTeXUtils
    pytex = PythonTeXUtils()
    
    pytex.docdir = os.getcwd()
    if os.path.isdir('{workingdir}'):
        os.chdir('{workingdir}')
        if os.getcwd() not in sys.path:
            sys.path.append(os.getcwd())
    else:
        if len(sys.argv) < 2 or sys.argv[1] != '--manual':
            sys.exit('Cannot find directory {workingdir}')
    if pytex.docdir not in sys.path:
        sys.path.append(pytex.docdir)
    
    {extend}
    
    pytex.id = '{family}_{session}_{restart}'
    pytex.family = '{family}'
    pytex.session = '{session}'
    pytex.restart = '{restart}'
    
    {body}

    pytex.cleanup()
    '''

python_wrapper = '''
    pytex.command = '{command}'
    pytex.set_context('{context}')
    pytex.args = '{args}'
    pytex.instance = '{instance}'
    pytex.line = '{line}'
    
    print('{stdoutdelim}')
    sys.stderr.write('{stderrdelim}\\n')
    pytex.before()
    
    {code}
    
    pytex.after()
    '''


CodeEngine('python', 'python', '.py', '{python} {file}.py',
              python_template, python_wrapper, 'print(pytex.formatter({code}))',
              'Error:', 'Warning:', ['line {number}', ':{number}:'])

SubCodeEngine('python', 'py')

SubCodeEngine('python', 'pylab', extend='from pylab import *')

sympy_extend = '''
    from sympy import *
    pytex.set_formatter('sympy_latex')
    '''

SubCodeEngine('python', 'sympy', extend=sympy_extend)




PythonConsoleEngine('pycon')

PythonConsoleEngine('pylabcon', startup='from pylab import *')

PythonConsoleEngine('sympycon', startup='from sympy import *')




ruby_template = '''
    # -*- coding: {encoding} -*-
    
    unless ARGV.include?('--interactive')
        $stdout.set_encoding('{encoding}')
        $stderr.set_encoding('{encoding}')
    end
    
    class RubyTeXUtils
        attr_accessor :id, :family, :session, :restart, 
                :command, :context, :args, 
                :instance, :line, :dependencies, :created,
                :docdir, :_context_raw
        def initialize
            @dependencies = Array.new
            @created = Array.new
            @_context_raw = nil
        end
        def formatter(expr)
            return expr.to_s
        end
        def before
        end
        def after
        end
        def add_dependencies(*expr)
            self.dependencies.push(*expr)
        end
        def add_created(*expr)
            self.created.push(*expr)
        end
        def set_context(expr)
            if expr != "" and expr != @_context_raw
                @context = expr.split(',').map{{|x| x1,x2 = x.split('='); {{x1.strip() => x2.strip()}}}}.reduce(:merge)
                @_context_raw = expr
            end
        end
        def pt_to_in(expr)
            if expr.is_a?String
                if expr.end_with?'pt'
                    expr = expr[0..-3]
                end
                return expr.to_f/72.27
            else
                return expr/72.27
            end
        end
        def pt_to_cm(expr)
            return pt_to_in(expr)*2.54
        end
        def pt_to_mm(expr)
            return pt_to_in(expr)*25.4
        end
        def pt_to_bp(expr)
            return pt_to_in(expr)*72
        end
        def cleanup
            puts '{dependencies_delim}'
            if @dependencies
                @dependencies.each {{ |x| puts x }}
            end
            puts '{created_delim}'
            if @created
                @created.each {{ |x| puts x }}
            end
        end        
    end
            
    rbtex = RubyTeXUtils.new
    
    rbtex.docdir = Dir.pwd
    if File.directory?('{workingdir}')
        Dir.chdir('{workingdir}')
        $LOAD_PATH.push(Dir.pwd) unless $LOAD_PATH.include?(Dir.pwd)
    elsif ARGV[0] != '--manual'
        abort('Cannot change to directory {workingdir}')
    end
    $LOAD_PATH.push(rbtex.docdir) unless $LOAD_PATH.include?(rbtex.docdir)
    
    {extend}
    
    rbtex.id = '{family}_{session}_{restart}'
    rbtex.family = '{family}'
    rbtex.session = '{session}'
    rbtex.restart = '{restart}'
    
    {body}

    rbtex.cleanup
    '''

ruby_wrapper = '''
    rbtex.command = '{command}'
    rbtex.set_context('{context}')
    rbtex.args = '{args}'
    rbtex.instance = '{instance}'
    rbtex.line = '{line}'
    
    puts '{stdoutdelim}'
    $stderr.puts '{stderrdelim}'
    rbtex.before
    
    {code}
    
    rbtex.after
    '''

CodeEngine('ruby', 'ruby', '.rb', '{ruby} {file}.rb', ruby_template, 
           ruby_wrapper, 'puts rbtex.formatter({code})', 
           ['Error)', '(Errno', 'error'], 'warning:', ':{number}:')

SubCodeEngine('ruby', 'rb')




julia_template = '''
    # -*- coding: UTF-8 -*-
    
    # Currently, Julia only supports UTF-8
    # So can't set stdout and stderr encoding
    
    type JuliaTeXUtils
        id::String
        family::String
        session::String
        restart::String
        command::String
        context::Dict
        args::String
        instance::String
        line::String
        
        _dependencies::Array{{String}}
        _created::Array{{String}}
        docdir::String
        _context_raw::String
        
        formatter::Function
        before::Function
        after::Function
        add_dependencies::Function
        add_created::Function
        set_context::Function
        pt_to_in::Function
        pt_to_cm::Function
        pt_to_mm::Function
        pt_to_bp::Function
        cleanup::Function
        
        self::JuliaTeXUtils
        
        function JuliaTeXUtils()
            self = new()
            self.self = self
            self._dependencies = Array(String, 0)
            self._created = Array(String, 0)
            self._context_raw = ""
            
            function formatter(expr)
                string(expr)
            end
            self.formatter = formatter
            
            function null()
            end
            self.before = null
            self.after = null
            
            function add_dependencies(files...)
                for file in files
                    push!(self._dependencies, file)
                end
            end
            self.add_dependencies = add_dependencies
            function add_created(files...)
                for file in files
                    push!(self._created, file)
                end
            end
            self.add_created = add_created
            
            function set_context(expr)
                if expr != "" && expr != self._context_raw
                    self.context = {{strip(x[1]) => strip(x[2]) for x in map(x -> split(x, "="), split(expr, ","))}}
                    self._context_raw = expr
                end
            end
            self.set_context = set_context
            
            function pt_to_in(expr)
                if isa(expr, String)
                    if sizeof(expr) > 2 && expr[end-1:end] == "pt"
                        expr = expr[1:end-2]
                    end
                    return float(expr)/72.27
                else
                    return expr/72.27
                end
            end
            self.pt_to_in = pt_to_in
            
            function pt_to_cm(expr)
                return self.pt_to_in(expr)*2.54
            end
            self.pt_to_cm = pt_to_cm
            
            function pt_to_mm(expr)
                return self.pt_to_in(expr)*25.4
            end
            self.pt_to_mm = pt_to_mm
            
            function pt_to_bp(expr)
                return self.pt_to_in(expr)*72
            end
            self.pt_to_bp = pt_to_bp
                        
            function cleanup()
                println("{dependencies_delim}")
                for f in self._dependencies
                    println(f)
                end
                println("{created_delim}")
                for f in self._created
                    println(f)
                end
            end
            self.cleanup = cleanup
            
            return self
        end
    end
    
    jltex = JuliaTeXUtils()
    
    jltex.docdir = pwd()
    try
        cd("{workingdir}")
    catch
        if !(length(ARGS) > 0 && ARGS[1] == "--manual")
            error("Could not find directory {workingdir}")
        end
    end
    if !(in(jltex.docdir, LOAD_PATH))
        push!(LOAD_PATH, jltex.docdir)
    end 
    
    {extend}
    
    jltex.id = "{family}_{session}_{restart}"
    jltex.family = "{family}"
    jltex.session = "{session}"
    jltex.restart = "{restart}"
    
    {body}
    
    jltex.cleanup()
    '''

julia_wrapper = '''
    jltex.command = "{command}"
    jltex.set_context("{context}")
    jltex.args = "{args}"
    jltex.instance = "{instance}"   
    jltex.line = "{line}"
    
    println("{stdoutdelim}")
    write(STDERR, "{stderrdelim}\\n")
    jltex.before()   
    
    {code}
    
    jltex.after()
    '''

CodeEngine('julia', 'julia', '.jl', '{julia} "{file}.jl"', julia_template, 
              julia_wrapper, 'println(jltex.formatter({code}))', 
              'ERROR:', 'WARNING:', ':{number}', True)

SubCodeEngine('julia', 'jl')


octave_template = '''
    # Octave only supports @CLASS, not classdef
    # So use a struct plus functions as a substitute for a utilities class
    
    global octavetex = struct();
    octavetex.docdir = pwd();
    try
        cd '{Workingdir}';
    catch
        arg_list = argv()
        if size(arg_list, 1) == 1 && arg_list{{1}} == '--manual'
        else
            error("Could not find directory {workingdir}");
        end
    end
    if find_dir_in_path(octavetex.docdir)
    else
        addpath(octavetex.docdir);
    end
    
    {extend}
    
    octavetex.dependencies = {{}};
    octavetex.created = {{}};
    octavetex._context_raw = '';
    
    function octavetex_formatter(argin)
        disp(argin);
    end
    octavetex.formatter = @(argin) octavetex_formatter(argin);
    
    function octavetex_before()
    end
    octavetex.before = @() octavetex_before();
    
    function octavetex_after()
    end
    octavetex.after = @() octavetex_after();
    
    function octavetex_add_dependencies(varargin)
        global octavetex;
        for i = 1:length(varargin)
            octavetex.dependencies{{end+1}} = varargin{{i}};
        end
    end
    octavetex.add_dependencies = @(varargin) octavetex_add_dependencies(varargin{{:}});
    
    function octavetex_add_created(varargin)
        global octavetex;
        for i = 1:length(varargin)
            octavetex.created{{end+1}} = varargin{{i}};
        end
    end
    octavetex.add_created = @(varargin) octavetex_add_created(varargin{{:}});
    
    function octavetex_set_context(argin)
        global octavetex;
        if ~strcmp(argin, octavetex._context_raw)
            octavetex._context_raw = argin;
            hash = struct;
            argin_kv = strsplit(argin, ',');
            for i = 1:length(argin_kv)
                kv = strsplit(argin_kv{{i}}, '=');
                k = strtrim(kv{{1}});
                v = strtrim(kv{{2}});
                hash = setfield(hash, k, v);
            end
            octavetex.context = hash;
        end
    end
    octavetex.set_context = @(argin) octavetex_set_context(argin);
    
    function out = octavetex_pt_to_in(argin)
        if ischar(argin)
            if length(argin) > 2 && argin(end-1:end) == 'pt'
                out = str2num(argin(1:end-2))/72.27;
            else
                out = str2num(argin)/72.27;
            end
        else
            out = argin/72.27;
        end
    end
    octavetex.pt_to_in = @(argin) octavetex_pt_to_in(argin);
    
    function out = octavetex_pt_to_cm(argin)
        out = octavetex_pt_to_in(argin)*2.54;
    end
    octavetex.pt_to_cm = @(argin) octavetex_pt_to_cm(argin);
    
    function out = octavetex_pt_to_mm(argin)
        out = octavetex_pt_to_in(argin)*25.4;
    end
    octavetex.pt_to_mm = @(argin) octavetex_pt_to_mm(argin);
    
    function out = octavetex_pt_to_bp(argin)
        out = octavetex_pt_to_in(argin)*72;
    end
    octavetex.pt_to_bp = @(argin) octavetex_pt_to_bp(argin);
    
    function octavetex_cleanup()
        global octavetex;
        fprintf(strcat('{dependencies_delim}', "\\n"));
        for i = 1:length(octavetex.dependencies)
            fprintf(strcat(octavetex.dependencies{{i}}, "\\n"));
        end
        fprintf(strcat('{created_delim}', "\\n"));
        for i = 1:length(octavetex.created)
            fprintf(strcat(octavetex.created{{i}}, "\\n"));
        end        
    end
    octavetex.cleanup = @() octavetex_cleanup();
    
    octavetex.id = '{family}_{session}_{restart}';
    octavetex.family = '{family}';
    octavetex.session = '{session}';
    octavetex.restart = '{restart}';
    
    {body}

    octavetex.cleanup()    
    '''

octave_wrapper = '''
    octavetex.command = '{command}';
    octavetex.set_context('{context}');
    octavetex.args = '{args}';
    octavetex.instance = '{instance}';
    octavetex.line = '{line}';
    
    octavetex.before()   
    
    fprintf(strcat('{stdoutdelim}', "\\n"));
    fprintf(stderr, strcat('{stderrdelim}', "\\n"));
    {code}
    
    octavetex.after()
    '''

CodeEngine('octave', 'octave', '.m',
           '{octave} -q "{File}.m"', 
           octave_template, octave_wrapper, 'disp({code})',
           'error', 'warning', 'line {number}')
