#    (C) Copyright 2015-2024 Sei Lisa. All rights reserved.
#
#    This file is part of LSL PyOptimizer.
#
#    LSL PyOptimizer is free software: you can redistribute it and/or
#    modify it under the terms of the GNU General Public License as
#    published by the Free Software Foundation, either version 3 of the
#    License, or (at your option) any later version.
#
#    LSL PyOptimizer is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with LSL PyOptimizer. If not, see <http://www.gnu.org/licenses/>.

# Expand inlined functions. This could perhaps be made at parse time, but that
# would obfuscate the source too much.

from lslopt.lslcommon import nr

# Statement-level nodes that have at most 1 child and are of type expression
SINGLE_OPT_EXPR_CHILD_NODES = frozenset({'DECL', 'EXPR', 'RETURN',
    '@', 'STSW', 'JUMP', ';', 'LAMBDA'})

# TODO: We can do a bit better with evaluation order.

class ENameAlreadyExists(Exception):
    def __init__(self, obj, text):
        super(ENameAlreadyExists, self).__init__(text)

class EExpansionLoop(Exception):
    def __init__(self):
        super(EExpansionLoop, self).__init__(u"Loop found in expansion of"
            u" inline functions")

class inliner(object):
    def newId(self, namespace, scope, symdata):
        """Create a new identifier based on a namespace."""
        self.symCtr[namespace] = self.symCtr.get(namespace, 0) + 1
        name = '___%s__%05d' % (namespace, self.symCtr[namespace])
        if name in self.symtab[scope]:
            kinds = {'l':u"Label", 'f':u"Function", 'v':u"Variable",
                     's':u"State"}
            raise ENameAlreadyExists(self, u"%s already exists: %s"
              % (kinds[symdata['Kind']], name.decode('utf8')))
        self.symtab[scope][name] = symdata
        return name

    def newSymtab(self):
        self.symtab.append({})
        return len(self.symtab) - 1

    def FixJumps(self, node):
        """Change name and scope of JUMPs to point to the correct symtab entry
        """
        nt = node.nt
        if nt == 'JUMP':
            orig = self.symtab[node.scope][node.name]
            if 'NewSymbolName' in orig:
                node.name = orig['NewSymbolName']
                node.scope = orig['NewSymbolScope']
                self.symtab[node.scope][node.name]['ref'] += 1
            return

        if nt in SINGLE_OPT_EXPR_CHILD_NODES:
            return

        if nt == '{}':
            for i in node.ch:
                self.FixJumps(i)
            return

        if nt == 'IF' or nt == 'WHILE':
            self.FixJumps(node.ch[1])
            if len(node.ch) > 2:
                self.FixJumps(node.ch[2])
            return

        if nt == 'DO':
            self.FixJumps(node.ch[0])
            return

        if nt == 'FOR':
            self.FixJumps(node.ch[3])
            return

        assert False, u"Unexpected node type: %s" % nt.decode('utf8')

    def GetFuncCopy(self, node, scope=0):
        """Get a copy of the function's body

        Replaces 'return expr' with assignment+jump, 'return' with jump, and
        existing labels with fresh labels. Also creates new symtabs for locals
        and adjusts scopes of symbols.
        """
        nt = node.nt
        if nt == 'FNDEF':
            # We're at the top level. Check return type and create a label,
            # then recurse into the block.
            assert node.ch[0].nt == '{}'
            copy = self.GetFuncCopy(node.ch[0], node.ch[0].scope)
            assert copy.nt == '{}'
            self.FixJumps(copy)
            return copy

        if nt == '{}':
            copy = node.copy()
            copy.scope = self.newSymtab()
            copy.ch = []
            for i in node.ch:
                copy.ch.append(self.GetFuncCopy(i, node.scope))
                if i.nt == 'DECL':
                    self.symtab[copy.scope][i.name] = \
                        self.symtab[i.scope][i.name].copy()
                    self.symtab[node.scope][i.name]['NewSymbolScope'] = \
                        copy.scope
                    copy.ch[-1].scope = copy.scope
            return copy

        if nt == '@':
            copy = node.copy()
            oldscope = node.scope
            oldname = node.name
            copy.name = self.newId('lbl', scope, {'Kind':'l', 'Scope':scope,
                                                  'ref':0})
            copy.scope = scope
            self.symtab[oldscope][oldname]['NewSymbolName'] = copy.name
            self.symtab[oldscope][oldname]['NewSymbolScope'] = scope
            return copy

        if nt == 'RETURN':
            newnode = nr(nt='JUMP', t=None, name=self.retlabel,
                scope=self.retlscope)
            self.symtab[self.retlscope][self.retlabel]['ref'] += 1
            if node.ch:
                # Returns a value. Wrap in {} and add an assignment.
                # BUG: We don't honour ExplicitCast here.
                newnode = nr(nt='{}', t=None, scope=self.newSymtab(), ch=[
                    nr(nt='EXPR', t=self.rettype, ch=[
                        nr(nt='=', t=self.rettype, ch=[
                            nr(nt='IDENT', t=node.ch[0].t,
                               name=self.retvar, scope=self.retscope)
                            , self.GetFuncCopy(node.ch[0])
                        ])
                    ]), newnode
                ])
            return newnode

        if nt == 'IDENT':
            copy = node.copy()
            if 'NewSymbolScope' in self.symtab[node.scope][node.name]:
                copy.scope = \
                    self.symtab[node.scope][node.name]['NewSymbolScope']
            return copy

        if not node.ch:
            return node.copy()

        copy = node.copy()
        copy.ch = []
        for i in node.ch:
            copy.ch.append(self.GetFuncCopy(i, scope))
        return copy


    def ConvertFunction(self, parent, index, scope):
        node = parent[index]
        fns = []
        for i in range(len(node.ch)):
            fns.extend(self.RecurseExpression(node.ch, i, scope))
        fnsym = self.symtab[0][node.name]
        rettype = fnsym['Type']
        self.rettype = rettype
        retvar = None
        if rettype is not None:
            # Returns a value. Create a local variable at the starting level.
            retvar = self.newId('ret', scope, {'Kind':'v', 'Scope':scope,
                                               'Type':rettype})
            # Add the declaration to the list of statements
            fns.append(nr(nt='DECL', t=rettype, name=retvar, scope=scope))

        # Begin expansion
        if node.name in self.expanding:
            raise EExpansionLoop()

        outer = None
        if fnsym['ParamNames']:
            # Add a new block + symbols + assignments for parameter values
            pscope = self.newSymtab()
            outer = nr(nt='{}', t=None, scope=pscope, ch=[])
            origpscope = self.tree[fnsym['Loc']].pscope
            for i in range(len(fnsym['ParamNames'])):
                # Add parameter assignments and symbol table entries
                pname = fnsym['ParamNames'][i]
                ptype = fnsym['ParamTypes'][i]
                value = node.ch[i]
                self.symtab[pscope][pname] = {'Kind':'v','Type':ptype,
                                              'Scope':pscope}
                self.symtab[origpscope][pname]['NewSymbolScope'] = pscope
                # BUG: We don't honour ExplicitCast here.
                outer.ch.append(nr(nt='DECL', t=ptype, name=pname, scope=pscope,
                                   ch=[value]))

        self.expanding.append(node.name)
        self.retvar = retvar
        self.retscope = scope
        self.retlscope = scope
        retlabel = self.newId('rtl', scope, {'Kind':'l', 'Scope':scope,
                                             'ref':0})
        self.retlabel = retlabel

        # Get a copy of the function
        blk = [self.GetFuncCopy(self.tree[fnsym['Loc']])]
        if outer:
            outer.ch.extend(blk)
            blk = [outer]
        retused = self.symtab[scope][retlabel]['ref'] != 0

        self.RecurseStatement(blk, 0, scope)  # recursively expand functions

        # Add return label if used, otherwise remove it from the symbol table
        if retused:
            blk.append(nr(nt='@', name=retlabel, scope=scope))
        else:
            del self.symtab[scope][retlabel]
        self.expanding.pop()
        # End expansion

        fns.extend(blk)

        if rettype is None:
            del parent[index]
        else:
            parent[index] = nr(nt='IDENT', t=rettype, name=retvar, scope=scope)

        return fns

    def RecurseExpression(self, parent, index, scope):
        node = parent[index]
        nt = node.nt
        fns = []

        if nt == 'FNCALL' and self.symtab[0][node.name].get('Inline', False):
            fns.extend(self.ConvertFunction(parent, index, scope))
        elif node.ch:
            for i in range(len(node.ch)):
                fns.extend(self.RecurseExpression(node.ch, i, scope))
        return fns

    def RecurseSingleStatement(self, parent, index, scope):
        # Synthesize a block node whose child is the statement.
        newscope = self.newSymtab()
        node = nr(nt='{}', t=None, scope=newscope, ch=[parent[index]],
            SEF=parent[index].SEF)

        # Recurse into that node, so that any additions are made right there.
        self.RecurseStatement(node.ch, 0, newscope)

        # If it's no longer a single statement, promote it to a block.
        if len(node.ch) != 1:
            parent[index] = node
        else:
            # The new level won't be necessary. We can't delete the symbol
            # table, though, because that shifts any new symbol tables.
            assert not self.symtab[newscope]
            parent[index] = node.ch[0]

    def RecurseStatement(self, parent, index, scope):
        node = parent[index]
        nt = node.nt
        child = node.ch
        fns = None

        if nt in SINGLE_OPT_EXPR_CHILD_NODES:
            if child:
                fns = self.RecurseExpression(child, 0, scope)
                if nt == 'EXPR' and not node.ch:
                    del parent[index]
            else:
                return

        elif nt == '{}':
            i = -len(child)
            while i:
                self.RecurseStatement(child, len(child)+i, node.scope)
                i += 1

        elif nt == 'IF':
            fns = self.RecurseExpression(child, 0, scope)
            self.RecurseSingleStatement(child, 1, scope)
            if len(child) > 2:
                self.RecurseSingleStatement(child, 2, scope)

        # Loop handling is tricky.
        #
        # Consider this:
        #
        #   integer f()
        #   {
        #       llOwnerSay("body of f");
        #       return 1;
        #   }
        #
        #   while (f())
        #       llOwnerSay("doing stuff");
        #
        # In order to execute it every loop iteration, the while() loop must be
        # converted to an if+jump:
        #
        #   integer ___ret__00001;
        #   @___lbl__00001;
        #   {
        #       llOwnerSay("body_of_f");
        #       __ret__00001 = 1;
        #   }
        #   if (___ret__00001)
        #   {
        #       llOwnerSay("doing stuff");
        #       jump ___lbl__00001;
        #   }
        #
        # The for loop is similar, but the initializer and iterator must be
        # expanded as well, to convert it to a while loop. When expanding the
        # iterator, care must be taken to avoid name clashes. For example:
        #
        #   for (i = 0; f(i); i++)
        #   {
        #       integer i;
        #   }
        #
        # should be expanded to:
        #
        #   i = 0;
        #   @loop_label;
        #   integer ___ret__00001;
        #   {
        #       llOwnerSay("body of f");
        #       ___ret__00001 = 1,
        #   }
        #   if (___ret__00001)
        #   {
        #       {
        #           integer i;
        #       }
        #       i++;
        #   }
        #
        # The extra {} inside the 'if' are needed to protect the iterator from
        # being affected by the variables defined in the inner block.
        #
        # Do loops are different:
        #
        #   do
        #       llOwnerSay("doing stuff");
        #   while (f());
        #
        # is converted to:
        #
        #   integer ___ret__00001;
        #   do
        #   {
        #       llOwnerSay("doing stuff");
        #       {
        #           llOwnerSay("body_of_f");
        #           ___ret__00001 = 1;
        #       }
        #   }
        #   while (___ret__00001);
        #

        elif nt == 'DO':
            self.RecurseSingleStatement(child, 0, scope)
            fns = self.RecurseExpression(child, 1, scope)
            if fns:
                # Need to do some plumbing to move the bodies into the loop
                i = 0;
                while i < len(fns):
                    if fns[i].nt != 'DECL':
                        assert fns[i].nt in ('{}', '@')
                        # All bodies must be moved inside the loop
                        if child[0].nt != '{}':
                            # Needs wrapping now
                            child[0] = nr(nt='{}', t=None, ch=[child[0]],
                                          scope=self.newSymtab())
                        child[0].ch.append(fns.pop(i))
                    else:
                        i += 1

        elif nt == 'WHILE':
            # Convert to if()
            lbl = self.newId('whl', scope, {'Kind':'l','Scope':scope,'ref':1})
            parent.insert(index, nr(nt='@', t=None, name=lbl, scope=scope))
            index += 1
            node.nt = 'IF'
            if child[1].nt != '{}':
                # Needs wrapping now
                child[1] = nr(nt='{}', t=None, ch=[child[1]],
                              scope=self.newSymtab())
            child[1].ch.append(nr(nt='JUMP', t=None, name=lbl, scope=scope))
            fns = self.RecurseExpression(child, 0, scope)
            self.RecurseSingleStatement(child, 1, scope)

        elif nt == 'FOR':
            assert child[0].nt == 'EXPRLIST'
            assert child[2].nt == 'EXPRLIST'
            for i in child[0].ch:
                parent.insert(index, nr(nt='EXPR', t=i.t, ch=[i]))
                fns = self.RecurseExpression(parent, index, scope)
                parent[index:index] = fns
                index += 1 + len(fns)
            lbl = self.newId('for', scope, {'Kind':'l','Scope':scope,'ref':1})
            parent.insert(index, nr(nt='@', t=None, name=lbl, scope=scope))
            index += 1
            node.nt = 'IF'
            if child[3].nt != '{}':
                # Needs wrapping now
                child[3] = nr(nt='{}', t=None, ch=[child[3]],
                              scope=self.newSymtab())
            # Needs another wrapping if iterator is not empty
            if child[2].ch:
                child[3] = nr(nt='{}', t=None, ch=[child[3]],
                              scope=self.newSymtab())
            for i in child[2].ch:
                child[3].ch.append(nr(nt='EXPR', t=i.t, ch=[i]))
            del child[2]
            del child[0]
            child[1].ch.append(nr(nt='JUMP', t=None, name=lbl, scope=scope))
            fns = self.RecurseExpression(child, 0, scope)
            self.RecurseSingleStatement(child, 1, scope)

        else:
            assert False, u"Unexpected node type: %s" % nt.decode('utf8')

        if fns:
            parent[index:index] = fns

    def inline(self, tree, symtab):
        self.tree = tree
        self.symtab = symtab
        self.symCtr = {}
        self.expanding = []
        for i in range(len(tree)):
            if tree[i].nt == 'STDEF':
                for j in range(len(tree[i].ch)):  # for each event in the state
                    self.RecurseStatement(tree[i].ch[j].ch, 0,
                                           tree[i].ch[j].ch[0].scope)
            elif (tree[i].nt == 'FNDEF'
                  and not symtab[tree[i].scope][tree[i].name].get('Inline',
                                                                  False)
                 ):
                # Must be an UDF
                self.RecurseStatement(tree[i].ch, 0, tree[i].ch[0].scope)

        # Remove all inline function definitions
        for i in range(len(tree)):
            if (tree[i].nt == 'FNDEF'
                  and symtab[tree[i].scope][tree[i].name].get('Inline', False)
               ):
                tree[i] = nr(nt='LAMBDA', t=None)
