# Copyright (c) 2006-2009 Nathan Binkert <nate@binkert.org>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met: redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer;
# redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution;
# neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import __builtin__
import inspect
import os
import re
import string

class lookup(object):
    def __init__(self, formatter, frame, *args, **kwargs):
        self.frame = frame
        self.formatter = formatter
        self.dict = self.formatter._dict
        self.args = args
        self.kwargs = kwargs
        self.locals = {}

    def __setitem__(self, item, val):
        self.locals[item] = val

    def __getitem__(self, item):
        if item in self.locals:
            return self.locals[item]

        if item in self.kwargs:
            return self.kwargs[item]

        if item == '__file__':
            return self.frame.f_code.co_filename

        if item == '__line__':
            return self.frame.f_lineno

        if self.formatter.locals and item in self.frame.f_locals:
            return self.frame.f_locals[item]

        if item in self.dict:
            return self.dict[item]

        if self.formatter.globals and item in self.frame.f_globals:
            return self.frame.f_globals[item]

        if item in __builtin__.__dict__:
            return __builtin__.__dict__[item]

        try:
            item = int(item)
            return self.args[item]
        except ValueError:
            pass
        raise IndexError, "Could not find '%s'" % item

class code_formatter_meta(type):
    pattern = r"""
    (?:
      %(delim)s(?P<escaped>%(delim)s)              | # escaped delimiter
      ^(?P<indent>[ ]*)%(delim)s(?P<lone>%(ident)s)$ | # lone identifier
      %(delim)s(?P<ident>%(ident)s)                | # identifier
      %(delim)s%(lb)s(?P<b_ident>%(ident)s)%(rb)s  | # braced identifier
      %(delim)s(?P<pos>%(pos)s)                    | # positional parameter
      %(delim)s%(lb)s(?P<b_pos>%(pos)s)%(rb)s      | # braced positional
      %(delim)s%(ldb)s(?P<eval>.*?)%(rdb)s         | # double braced expression
      %(delim)s(?P<invalid>)                       # ill-formed delimiter exprs
    )
    """
    def __init__(cls, name, bases, dct):
        super(code_formatter_meta, cls).__init__(name, bases, dct)
        if 'pattern' in dct:
            pat = cls.pattern
        else:
            # tuple expansion to ensure strings are proper length
            lb,rb = cls.braced
            lb1,lb2,rb2,rb1 = cls.double_braced
            pat = code_formatter_meta.pattern % {
                'delim' : re.escape(cls.delim),
                'ident' : cls.ident,
                'pos' : cls.pos,
                'lb' : re.escape(lb),
                'rb' : re.escape(rb),
                'ldb' : re.escape(lb1+lb2),
                'rdb' : re.escape(rb2+rb1),
                }
        cls.pattern = re.compile(pat, re.VERBOSE | re.DOTALL | re.MULTILINE)

class code_formatter(object):
    __metaclass__ = code_formatter_meta

    delim = r'$'
    ident = r'[_A-z]\w*'
    pos = r'[0-9]+'
    braced = r'{}'
    double_braced = r'{{}}'

    globals = True
    locals = True
    fix_newlines = True
    def __init__(self, *args, **kwargs):
        self._data = []
        self._dict = {}
        self._indent_level = 0
        self._indent_spaces = 4
        self.globals = kwargs.pop('globals', type(self).globals)
        self.locals = kwargs.pop('locals', type(self).locals)
        self._fix_newlines = \
                kwargs.pop('fix_newlines', type(self).fix_newlines)

        if args:
            self.__call__(args)

    def indent(self, count=1):
        self._indent_level += self._indent_spaces * count

    def dedent(self, count=1):
        assert self._indent_level >= (self._indent_spaces * count)
        self._indent_level -= self._indent_spaces * count

    def fix(self, status):
        previous = self._fix_newlines
        self._fix_newlines = status
        return previous

    def nofix(self):
        previous = self._fix_newlines
        self._fix_newlines = False
        return previous

    def clear():
        self._data = []

    def write(self, *args):
        f = file(os.path.join(*args), "w")
        for data in self._data:
            f.write(data)
        f.close()

    def __str__(self):
        data = string.join(self._data, '')
        self._data = [ data ]
        return data

    def __getitem__(self, item):
        return self._dict[item]

    def __setitem__(self, item, value):
        self._dict[item] = value

    def __delitem__(self, item):
        del self._dict[item]

    def __contains__(self, item):
        return item in self._dict

    def __iadd__(self, data):
        self.append(data)

    def append(self, data):
        if isinstance(data, code_formatter):
            self._data.extend(data._data)
        else:
            self._append(str(data))

    def _append(self, data):
        if not self._fix_newlines:
            self._data.append(data)
            return

        initial_newline = not self._data or self._data[-1] == '\n'
        for line in data.splitlines():
            if line:
                if self._indent_level:
                    self._data.append(' ' * self._indent_level)
                self._data.append(line)

            if line or not initial_newline:
                self._data.append('\n')

            initial_newline = False

    def __call__(self, *args, **kwargs):
        if not args:
            self._data.append('\n')
            return

        format = args[0]
        args = args[1:]

        frame = inspect.currentframe().f_back

        l = lookup(self, frame, *args, **kwargs)
        def convert(match):
            ident = match.group('lone')
            # check for a lone identifier
            if ident:
                indent = match.group('indent') # must be spaces
                lone = '%s' % (l[ident], )

                def indent_lines(gen):
                    for line in gen:
                        yield indent
                        yield line
                return ''.join(indent_lines(lone.splitlines(True)))

            # check for an identifier, braced or not
            ident = match.group('ident') or match.group('b_ident')
            if ident is not None:
                return '%s' % (l[ident], )

            # check for a positional parameter, braced or not
            pos = match.group('pos') or match.group('b_pos')
            if pos is not None:
                pos = int(pos)
                if pos > len(args):
                    raise ValueError \
                        ('Positional parameter #%d not found in pattern' % pos,
                         code_formatter.pattern)
                return '%s' % (args[int(pos)], )

            # check for a double braced expression
            eval_expr = match.group('eval')
            if eval_expr is not None:
                result = eval(eval_expr, {}, l)
                return '%s' % (result, )

            # check for an escaped delimiter
            if match.group('escaped') is not None:
                return '$'

            # At this point, we have to match invalid
            if match.group('invalid') is None:
                # didn't match invalid!
                raise ValueError('Unrecognized named group in pattern',
                                 code_formatter.pattern)

            i = match.start('invalid')
            if i == 0:
                colno = 1
                lineno = 1
            else:
                lines = format[:i].splitlines(True)
                colno = i - reduce(lambda x,y: x+y, (len(z) for z in lines))
                lineno = len(lines)

                raise ValueError('Invalid format string: line %d, col %d' %
                                 (lineno, colno))

        d = code_formatter.pattern.sub(convert, format)
        self._append(d)

__all__ = [ "code_formatter" ]

if __name__ == '__main__':
    from code_formatter import code_formatter
    f = code_formatter()

    class Foo(dict):
        def __init__(self, **kwargs):
            self.update(kwargs)
        def __getattr__(self, attr):
            return self[attr]

    x = "this is a test"
    l = [ [Foo(x=[Foo(y=9)])] ]

    y = code_formatter()
    y('''
{
    this_is_a_test();
}
''')
    f('    $y')
    f('''$__file__:$__line__
{''')
    f("${{', '.join(str(x) for x in xrange(4))}}")
    f('${x}')
    f('$x')
    f.indent()
    for i in xrange(5):
        f('$x')
        f('$i')
        f('$0', "zero")
        f('$1 $0', "zero", "one")
        f('${0}', "he went")
        f('${0}asdf', "he went")
    f.dedent()

    f('''
    ${{l[0][0]["x"][0].y}}
}
''', 1, 9)

    print f,
